Skip to content

fix(helius-mcp): scope results handles to the caller and label getAccountPlan as JWT - #209

Merged
0xIchigo merged 1 commit into
mainfrom
fix/hosted-surface-followups
Oct 22, 2026
Merged

0xIchigo merged 1 commit into
mainfrom
fix/hosted-surface-followups

Conversation

@0xIchigo

Copy link
Copy Markdown
Collaborator

This PR introduces two follow-ups from the #181 review, both on the hosted surface. Nothing hosted is deployed yet, so neither was exploitable. We had to fix both before the hosted work relies on these paths

1. Result handles are scoped per caller

dispatch.ts stored and looked up every expandResult handle under one process-wide key from getRouterContext(). On a hosted server, caller B could expand caller A's handle. Dispatch would re-run A's stored recipe (A's addresses and filters) under B's credentials. RequestContext.sessionKey existed for this, and its doc comment said bridging it to RouterContext was deferred

The change:

  • getRouterContext(extra?) now derives the result owner from the request:
    • No request context (stdio): the process-wide key, as before. Stdio behavior is unchanged
    • A request context: the caller's sessionKey
    • Plus a transport sessionId: a hash of the two, so two connections on one API key don't read or evict each other's handles. The feat(helius-mcp): Add an Optional Per-Request Context #182 review flagged that they shared one bucket capped at 10
  • All three dispatch paths take the owner from extra: storing a handle, expanding one, and the new handle issued when expanding with continuation: "next". The compiler caught that third path once buildStoredResult required an owner.
  • An expansion by a non-owner returns RESULT_NOT_FOUND without calling the handler
  • The stale comment in request-context.ts now describes the live behavior

2. getAccountPlan is labelled jwt

It inherited authRequirement: 'none' from the account-group default, but it reads plan, credits and project from the dashboard session, the same path as getAccountStatus. Without a session it can only report "plan unknown". The runtime was already safe (getJwt(ctx) returns nothing for a request context), but the catalog contradicted it, and getHostedActions() is built from the catalog

It's now labelled jwt explicitly and keeps its scalar response settings. The hosted surface drops from 78 to 77 actions. The hosted-eligibility snapshot is updated deliberately, and a new test pins the label

I checked every heliusAccount action's label against what its handler reads. getStarted, getHeliusPlanInfo and compareHeliusPlans also read the session but are labelled none. All three are already excluded by hand and work without a session by reporting less, so I left them as is

Tests

tests/result-ownership.test.ts (8 tests) drives the real store-and-expand round trip through dispatchRoutedTool and expandStoredResult, with only the action handler mocked. It covers:

  • Stdio unchanged
  • Two callers with different keys
  • No handler call on a rejected expansion
  • Two transport sessions on one key
  • Stdio versus hosted in both directions
  • Next-page handles keep their owner
  • The owner key is stable and never contains the API key

Negative control: against main's context.ts and dispatch.ts, the five isolation tests fail. The three that pass cover the stdio behavior this PR keeps

Verification

  • pnpm build and pnpm validate pass (77 hosted-eligible)
  • 405 tests pass
  • compile-skills.ts --check in sync

@0xIchigo
0xIchigo merged commit 6c4061d into main Oct 22, 2026
6 checks passed
@0xIchigo
0xIchigo deleted the fix/hosted-surface-followups branch October 22, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants