feat: MCP app sampling support via stock SDK types (alt to #477) (#530)

* feat: MCP app sampling support via stock SDK types

Alternative to #477. Instead of forking a stripped-down sampling schema,
this reuses the stock `CreateMessageRequest` / `CreateMessageResult` /
`CreateMessageResultWithTools` types from `@modelcontextprotocol/sdk`
directly — same pattern already used for `tools/call`.

- Spec: `sampling/createMessage` listed under `### Standard MCP Messages`,
  `sampling?: { tools?: {} }` added to `HostCapabilities` (mirrors MCP
  `ClientCapabilities.sampling` shape for easy pass-through).
- SDK: `App.createSamplingMessage()` with overloads that narrow the return
  type based on whether `params.tools` is set; `AppBridge.oncreatesamplingmessage`
  setter; `CreateMessageRequest`/`CreateMessageResult*` added to the
  `AppRequest`/`AppResult` unions.
- Picks up SEP-1577 tool-calling support (`tools`, `toolChoice`, `tool_use`
  content blocks, `stopReason: "toolUse"`, array content) for free — unblocks
  the "nested agents" use case motivated by the original PR.

https://claude.ai/code/session_01ENGWTtsfcyP4S6fTWtUMAh

* feat(app): guard createSamplingMessage before handshake completes

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Olivier Chafik 2026-04-21 13:29:09 +01:00 • committed by GitHub
commit 926dcb4fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -737,6 +737,43 @@ describe("App <-> AppBridge integration", () => {
expect(result.content).toEqual(resultContent);
});
it("oncreatesamplingmessage setter registers handler for sampling/createMessage requests", async () => {
// Re-create bridge with sampling capability so App's capability check passes
bridge = new AppBridge(null, testHostInfo, {
...testHostCapabilities,
sampling: { tools: {} },
});
const receivedParams: unknown[] = [];
bridge.oncreatesamplingmessage = async (params) => {
receivedParams.push(params);
return {
role: "assistant",
content: { type: "text", text: "Hello from the model" },
model: "test-model",
stopReason: "endTurn",
};
};
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
expect(app.getHostCapabilities()?.sampling?.tools).toEqual({});
const result = await app.createSamplingMessage({
messages: [{ role: "user", content: { type: "text", text: "Hi" } }],
maxTokens: 50,
});
expect(receivedParams).toHaveLength(1);
expect(receivedParams[0]).toMatchObject({ maxTokens: 50 });
expect(result.model).toEqual("test-model");
expect(result.content).toEqual({
type: "text",
text: "Hello from the model",
});
});
it("ondownloadfile setter registers handler for ui/download-file requests", async () => {
const downloadParams = {
contents: [