diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index e7c343b8..17ad9269 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -516,6 +516,10 @@ UI iframes can use the following subset of standard MCP protocol messages: - `resources/read` - Read resource content +**Sampling:** + +- `sampling/createMessage` - Request an LLM completion from the host (uses the standard MCP [`CreateMessageRequest`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) / `CreateMessageResult` types, including SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks). The host has full discretion over model selection and SHOULD apply rate limiting, cost controls, and user approval (human-in-the-loop). Apps MUST check `hostCapabilities.sampling` before sending this request, and `hostCapabilities.sampling.tools` before including `tools` in the request params. + **Notifications:** - `notifications/message` - Log messages to host @@ -662,6 +666,14 @@ interface HostCapabilities { }; /** Host accepts log messages. */ logging?: {}; + /** + * Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors MCP ClientCapabilities.sampling so hosts can pass it through. + */ + sampling?: { + /** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */ + tools?: {}; + }; /** Sandbox configuration applied by the host. */ sandbox?: { /** Permissions granted by the host (camera, microphone, geolocation, clipboard-write). */ diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index b613451c..d5dc5bc7 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -12,6 +12,8 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageResult, ListResourcesResultSchema, ReadResourceResultSchema, ListPromptsResultSchema, @@ -228,6 +230,26 @@ function AppBridge_oncalltool_forwardToServer( //#endregion AppBridge_oncalltool_forwardToServer } +/** + * Example: Forward sampling requests to your LLM provider. + */ +function AppBridge_oncreatesamplingmessage_forwardToLlm( + bridge: AppBridge, + myLlmProvider: { + complete: ( + params: CreateMessageRequest["params"], + opts: { signal: AbortSignal }, + ) => Promise; + }, +) { + //#region AppBridge_oncreatesamplingmessage_forwardToLlm + bridge.oncreatesamplingmessage = async (params, extra) => { + // Apply rate limiting, user approval, cost controls here + return await myLlmProvider.complete(params, { signal: extra.signal }); + }; + //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm +} + /** * Example: Forward list resources requests to the MCP server. */ diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 310e38ec..0de8778b 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -712,6 +712,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: [ diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 793782be..4980c37a 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -5,6 +5,10 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageRequestSchema, + CreateMessageResult, + CreateMessageResultWithTools, EmptyResult, Implementation, ListPromptsRequest, @@ -833,6 +837,49 @@ export class AppBridge extends Protocol< }); } + /** + * Register a handler for LLM sampling requests from the view. + * + * The view sends standard MCP `sampling/createMessage` requests to obtain + * LLM completions via the host's model connection. The host has full + * discretion over which model to use and SHOULD apply rate limiting, + * cost controls, and user approval (human-in-the-loop) before sampling. + * + * Hosts that register this handler SHOULD advertise `sampling` (and + * `sampling.tools` if tool-calling is supported) in + * {@link McpUiHostCapabilities `McpUiHostCapabilities`}. + * + * @param callback - Handler that receives `CreateMessageRequest` params and + * returns a `CreateMessageResult` (or `CreateMessageResultWithTools` when + * `params.tools` was provided) + * - `params` - Standard MCP sampling params (messages, maxTokens, tools, etc.) + * - `extra` - Request metadata (abort signal, session info) + * + * @example Forward to your LLM provider + * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" + * bridge.oncreatesamplingmessage = async (params, extra) => { + * // Apply rate limiting, user approval, cost controls here + * return await myLlmProvider.complete(params, { signal: extra.signal }); + * }; + * ``` + * + * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + */ + set oncreatesamplingmessage( + callback: ( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + /** * Notify the view that the MCP server's tool list has changed. * diff --git a/src/app.examples.ts b/src/app.examples.ts index 4705071b..4774745d 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -297,6 +297,54 @@ async function App_callServerTool_fetchWeather(app: App) { //#endregion App_callServerTool_fetchWeather } +/** + * Example: Simple LLM completion via host sampling. + */ +async function App_createSamplingMessage_simple(app: App) { + //#region App_createSamplingMessage_simple + const result = await app.createSamplingMessage({ + messages: [ + { + role: "user", + content: { type: "text", text: "Summarize this in one line." }, + }, + ], + maxTokens: 100, + }); + console.log(result.content); + //#endregion App_createSamplingMessage_simple +} + +/** + * Example: Agentic loop with tools (requires host sampling.tools capability). + */ +async function App_createSamplingMessage_withTools( + app: App, + messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[], +) { + //#region App_createSamplingMessage_withTools + if (!app.getHostCapabilities()?.sampling?.tools) return; + + const result = await app.createSamplingMessage({ + messages, + maxTokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get the current weather", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + }, + }, + ], + }); + if (result.stopReason === "toolUse") { + // result.content may be an array containing tool_use blocks + } + //#endregion App_createSamplingMessage_withTools +} + /** * Example: Send a text message from user interaction. */ diff --git a/src/app.ts b/src/app.ts index fb952a23..9374a916 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,11 @@ import { CallToolRequestSchema, CallToolResult, CallToolResultSchema, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultSchema, + CreateMessageResultWithTools, + CreateMessageResultWithToolsSchema, EmptyResultSchema, Implementation, ListToolsRequest, @@ -638,7 +643,15 @@ export class App extends Protocol { * @internal */ assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO + switch (method) { + case "sampling/createMessage": + if (!this._hostCapabilities?.sampling) { + throw new Error( + `Host does not support sampling (required for ${method})`, + ); + } + break; + } } /** @@ -739,6 +752,83 @@ export class App extends Protocol { ); } + /** + * Request an LLM completion from the host (standard MCP `sampling/createMessage`). + * + * Enables the app to use the host's model connection for completions. The host + * has full discretion over which model to select and MAY modify or reject the + * request (human-in-the-loop). Check {@link getHostCapabilities `getHostCapabilities`}`()?.sampling` + * before calling — hosts without this capability will reject the request. + * + * This method reuses the stock MCP `CreateMessageRequest` shape. When `params.tools` + * is provided, the result is parsed with the extended schema that permits + * `stopReason: "toolUse"` and array content containing `tool_use` blocks. + * + * @param params - Standard MCP `CreateMessageRequest` params (messages, maxTokens, + * systemPrompt, temperature, modelPreferences, tools, toolChoice, etc.) + * @param options - Request options (timeout, abort signal) + * @returns `CreateMessageResult` (single content block) or `CreateMessageResultWithTools` + * (array content, may include `tool_use` blocks) depending on whether `tools` was set + * + * @throws {Error} If the host rejects the request or does not support sampling + * @throws {Error} If the request times out or the connection is lost + * + * @example Simple completion + * ```ts source="./app.examples.ts#App_createSamplingMessage_simple" + * const result = await app.createSamplingMessage({ + * messages: [ + * { role: "user", content: { type: "text", text: "Summarize this in one line." } }, + * ], + * maxTokens: 100, + * }); + * console.log(result.content); + * ``` + * + * @example Agentic loop with tools + * ```ts source="./app.examples.ts#App_createSamplingMessage_withTools" + * if (!app.getHostCapabilities()?.sampling?.tools) return; + * + * const result = await app.createSamplingMessage({ + * messages, + * maxTokens: 1024, + * tools: [ + * { + * name: "get_weather", + * description: "Get the current weather", + * inputSchema: { type: "object", properties: { city: { type: "string" } } }, + * }, + * ], + * }); + * if (result.stopReason === "toolUse") { + * // result.content may be an array containing tool_use blocks + * } + * ``` + * + * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + */ + async createSamplingMessage( + params: CreateMessageRequest["params"] & { tools?: undefined }, + options?: RequestOptions, + ): Promise; + async createSamplingMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, + ): Promise; + async createSamplingMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, + ): Promise { + const resultSchema = params.tools + ? CreateMessageResultWithToolsSchema + : CreateMessageResultSchema; + return await this.request( + { method: "sampling/createMessage", params }, + resultSchema, + options, + ); + } + /** * Send a message to the host's chat interface. * diff --git a/src/generated/schema.json b/src/generated/schema.json index 522f1592..55004156 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -498,6 +498,19 @@ } }, "additionalProperties": false + }, + "sampling": { + "description": "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + "type": "object", + "properties": { + "tools": { + "description": "Host supports tool use via `tools` and `toolChoice` parameters.", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2838,6 +2851,19 @@ } }, "additionalProperties": false + }, + "sampling": { + "description": "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + "type": "object", + "properties": { + "tools": { + "description": "Host supports tool use via `tools` and `toolChoice` parameters.", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 88797b50..9fee3648 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -545,6 +545,24 @@ export const McpUiHostCapabilitiesSchema = z.object({ message: McpUiSupportedContentBlockModalitiesSchema.optional().describe( "Host supports receiving content messages (ui/message) from the view.", ), + /** + * @description Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through. + */ + sampling: z + .object({ + /** @description Host supports tool use via `tools` and `toolChoice` parameters. */ + tools: z + .object({}) + .optional() + .describe( + "Host supports tool use via `tools` and `toolChoice` parameters.", + ), + }) + .optional() + .describe( + "Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.", + ), }); /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 8e0e2eb0..202ff200 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -508,6 +508,14 @@ export interface McpUiHostCapabilities { updateModelContext?: McpUiSupportedContentBlockModalities; /** @description Host supports receiving content messages (ui/message) from the view. */ message?: McpUiSupportedContentBlockModalities; + /** + * @description Host supports LLM sampling (sampling/createMessage) from the view. + * Mirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through. + */ + sampling?: { + /** @description Host supports tool use via `tools` and `toolChoice` parameters. */ + tools?: {}; + }; } /** diff --git a/src/types.ts b/src/types.ts index 739da6fa..3ac28232 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,6 +136,9 @@ export { import { CallToolRequest, CallToolResult, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, EmptyResult, ListPromptsRequest, ListPromptsResult, @@ -161,6 +164,7 @@ import { * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) * - MCP server requests forwarded from the app (tools/call, tools/list, resources/list, * resources/templates/list, resources/read, prompts/list) + * - MCP client requests forwarded to the host (sampling/createMessage) * - Protocol requests (ping) */ export type AppRequest = @@ -177,6 +181,7 @@ export type AppRequest = | ListResourceTemplatesRequest | ReadResourceRequest | ListPromptsRequest + | CreateMessageRequest | PingRequest; /** @@ -225,4 +230,6 @@ export type AppResult = | ListResourceTemplatesResult | ReadResourceResult | ListPromptsResult + | CreateMessageResult + | CreateMessageResultWithTools | EmptyResult;