Add readResource for ext-apps resources/read#2
Conversation
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.
|
Addressed QA review in d56a7ec. Warning 1 (narrow public type) — fixed. Warning 2 (typed method constant) — fixed. Added Suggestion 2 (re-export request type) — fixed. Suggestion 1 (split commits) — already split. No change; the vite test fix lives in Verification: Ready for re-review. |
Summary
Exposes MCP
resources/readas 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 carriestools/call.The ext-apps spec already defines this capability (
ReadResourceRequestin theAppRequestunion,readServerResourceon the low-levelApptype). This implements the spec in the SDK — it's a wiring change, not a new invention.Synapse.readResource(uri): Promise<ReadResourceResult>— mirrorscallTool, delegates totransport.request("resources/read", { uri })App.readServerResource(params): Promise<ReadResourceResult>— named after the ext-apps spec; same transport pathReadResourceResultre-exported fromsrc/index.tsso callers don't need a direct@modelcontextprotocol/sdkimportWhy
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:
tools/call— conflates tools with resources, blows up tool result size, misses out onresources/subscribe/list semantics.resources/readover 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-appsonly exportsMETHODconstants for ext-apps-specificui/*methods, not for standard MCP methods likeresources/read. Hand-typing matches the existingtools/callprecedent. If upstream adds constants later, bothcore.tsandconnect.tsshould switch to importing them (and the IIFE shim inCLAUDE.mdwill need the corresponding update).Hard-rules compliance (per
packages/synapse/CLAUDE.md)ReadResourceRequest["params"]as anyon content): none addedReadResourceRequest["params"]andReadResourceResultvariantsTest plan
src/__tests__/core.test.ts— 2 new tests:readResourcesendsresources/readwith correct params; returns text and blob contents unchanged; error envelopes rejectsrc/__tests__/connect.test.ts— 1 new test:readServerResourcesends{ uri }in params and returnscontentsuntouchedsrc/__tests__/spec-compliance.test.ts— outbound-shape assertion + 2 compile-time type guards (ReadResourceRequest["params"]shape,ReadResourceResulttext/blob variants)src/__tests__/vite/plugin.test.ts— updated to match the current preview-harness contract (the harness intentionally does NOT emitsynapse/data-changedfrom UI-initiated tool calls, to avoid auseDataSyncfeedback loop). Stale assertion was unrelated to this change; caught while runningnpm run cinpm run ci— lint clean, typecheck clean, build clean, 245/245 tests passConsumers already wired up (on their own branches, not part of this PR)
nimblebrain:POST /v1/resources/readendpoint + bridgecase "resources/read":that proxies to this SDK methodsynapse-collateral: UI usessynapse.readResourceto fetch preview PDFs and uploaded asset bytesBoth 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/synapsedep can point at a published version instead of the localnpm link.