Skip to content

Add readResource for ext-apps resources/read#2

Merged
mgoldsborough merged 3 commits intomainfrom
feat/read-resource
Apr 17, 2026
Merged

Add readResource for ext-apps resources/read#2
mgoldsborough merged 3 commits intomainfrom
feat/read-resource

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

Exposes MCP resources/read as a first-class method on the SDK so iframe apps can fetch bundle-owned resources (PDFs, images, arbitrary bytes) via the same ext-apps postMessage bridge that already carries tools/call.

The ext-apps spec already defines this capability (ReadResourceRequest in the AppRequest union, readServerResource on the low-level App type). This implements the spec in the SDK — it's a wiring change, not a new invention.

  • Synapse.readResource(uri): Promise<ReadResourceResult> — mirrors callTool, delegates to transport.request("resources/read", { uri })
  • App.readServerResource(params): Promise<ReadResourceResult> — named after the ext-apps spec; same transport path
  • ReadResourceResult re-exported from src/index.ts so callers don't need a direct @modelcontextprotocol/sdk import

Why

Today iframe apps that render bundle-produced artifacts (e.g. Collateral's PDF previews) have no SDK-level way to read MCP resources. Workarounds fall into two buckets:

  1. Hit a host-internal HTTP URL directly from the iframe — brittle (requires host URL knowledge, no workspace auth, leaks the platform's API surface into app code).
  2. Smuggle bytes through tools/call — conflates tools with resources, blows up tool result size, misses out on resources/subscribe/list semantics.

resources/read over the app bridge is the right seam. Everything spec-aligned, nothing invented.

Wire format

Request (sent via transport.request):

```json
{ "jsonrpc": "2.0", "method": "resources/read", "params": { "uri": "collateral://exports/exp_abc.pdf" } }
```

Response (forwarded by the host verbatim from the bundle):

```json
{
"jsonrpc": "2.0",
"result": {
"contents": [
{ "uri": "collateral://exports/exp_abc.pdf", "mimeType": "application/pdf", "blob": "" }
]
}
}
```

No new method constants were added — @modelcontextprotocol/ext-apps only exports METHOD constants for ext-apps-specific ui/* methods, not for standard MCP methods like resources/read. Hand-typing matches the existing tools/call precedent. If upstream adds constants later, both core.ts and connect.ts should switch to importing them (and the IIFE shim in CLAUDE.md will need the corresponding update).

Hard-rules compliance (per packages/synapse/CLAUDE.md)

  • Rule 2 (typed param shapes): params typed as ReadResourceRequest["params"]
  • Rule 3 (no as any on content): none added
  • Rule 4 (spec-compliance test): extended, not weakened — new assertions for outbound shape + compile-time type coverage for ReadResourceRequest["params"] and ReadResourceResult variants

Test plan

  • src/__tests__/core.test.ts — 2 new tests: readResource sends resources/read with correct params; returns text and blob contents unchanged; error envelopes reject
  • src/__tests__/connect.test.ts — 1 new test: readServerResource sends { uri } in params and returns contents untouched
  • src/__tests__/spec-compliance.test.ts — outbound-shape assertion + 2 compile-time type guards (ReadResourceRequest["params"] shape, ReadResourceResult text/blob variants)
  • src/__tests__/vite/plugin.test.ts — updated to match the current preview-harness contract (the harness intentionally does NOT emit synapse/data-changed from UI-initiated tool calls, to avoid a useDataSync feedback loop). Stale assertion was unrelated to this change; caught while running npm run ci
  • npm run ci — lint clean, typecheck clean, build clean, 245/245 tests pass

Consumers already wired up (on their own branches, not part of this PR)

  • nimblebrain: POST /v1/resources/read endpoint + bridge case "resources/read": that proxies to this SDK method
  • synapse-collateral: UI uses synapse.readResource to fetch preview PDFs and uploaded asset bytes

Both depend on this SDK change being released. Suggested: merge this + cut a release (e.g. 0.4.3) before the collateral PR lands so its @nimblebrain/synapse dep can point at a published version instead of the local npm link.

The plugin's tools/call path used to emit synapse/data-changed after
a successful proxy. That was deliberately removed (plugin.ts:333
comment) because UI-initiated tool calls emitting data-changed creates
a feedback loop with useDataSync — same failure class we just fixed
in the collateral hooks.

The test was stale, still asserting the old emission. Updated to
assert the current contract: the harness proxies via /__mcp but does
not emit synapse/data-changed as a JSON-RPC method.
- App.readServerResource params widen from { uri: string } to
  ReadResourceRequest["params"] so callers can pass spec _meta fields
  (progress tokens, related-task). Public interface was narrower than
  the implementation; narrower-than-spec is the wrong direction.
- 'resources/read' literal replaced with a typed READ_RESOURCE_METHOD
  const in core.ts and connect.ts. Upstream spec renames now produce a
  compile error. Matches the Rule 1 spirit without waiting on
  @modelcontextprotocol/ext-apps to add the constant.
- ReadResourceRequest re-exported from src/index.ts so consumers can
  type their own wrapper params without a direct SDK import.
@mgoldsborough
Copy link
Copy Markdown
Contributor Author

Addressed QA review in d56a7ec.

Warning 1 (narrow public type) — fixed. App.readServerResource now declares params: ReadResourceRequest["params"] in src/types.ts instead of the hand-typed { uri: string }. Callers can now pass spec _meta fields. Agree with QA's framing: callTool's precedent is broader-than-spec (permissive), this was narrower-than-spec (restrictive) — narrower is the wrong direction, and Rule 2 compliance was explicitly cited in the PR, so tightening is the right move.

Warning 2 (typed method constant) — fixed. Added const READ_RESOURCE_METHOD: ReadResourceRequest["method"] = "resources/read"; in both src/core.ts and src/connect.ts. Upstream spec rename now produces a compile error. Matches the Rule 1 protective intent without waiting on @modelcontextprotocol/ext-apps to add the constant.

Suggestion 2 (re-export request type) — fixed. src/index.ts now re-exports ReadResourceRequest alongside ReadResourceResult. Consumers typing their own wrappers no longer need a direct @modelcontextprotocol/sdk import.

Suggestion 1 (split commits) — already split. No change; the vite test fix lives in ce40622, separate from the feature commit.

Verification: npm run ci — lint clean, typecheck clean, build clean, 245/245 tests (unchanged from pre-fix; no test assumptions needed to change).

Ready for re-review.

@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Apr 17, 2026
@mgoldsborough mgoldsborough merged commit 4cfe6a9 into main Apr 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant