Skip to content

Commit 2a0ef0d

Browse files
committed
Merge origin/main into feat/app-tool-registration
Conflict resolution: - src/app.ts: kept _registeredTools (#72) alongside main's fields; took main's assertCapabilityForMethod implementation (sampling check from #530). - src/react/useApp.tsx: took main's version (#72 doesn't touch this file; main has #622's autoResize forwarding). - typedoc.config.mjs: kept #72's RequestHandlerExtra in intentionallyNotExported.
2 parents ac02415 + 926dcb4 commit 2a0ef0d

11 files changed

Lines changed: 333 additions & 10 deletions

File tree

specification/draft/apps.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ Note that `tools/call` and `tools/list` flow **bidirectionally**:
528528

529529
- `resources/read` - Read resource content
530530

531+
**Sampling:**
532+
533+
- `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.
534+
531535
**Notifications:**
532536

533537
- `notifications/message` - Log messages to host
@@ -674,6 +678,14 @@ interface HostCapabilities {
674678
};
675679
/** Host accepts log messages. */
676680
logging?: {};
681+
/**
682+
* Host supports LLM sampling (sampling/createMessage) from the view.
683+
* Mirrors MCP ClientCapabilities.sampling so hosts can pass it through.
684+
*/
685+
sampling?: {
686+
/** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */
687+
tools?: {};
688+
};
677689
/** Sandbox configuration applied by the host. */
678690
sandbox?: {
679691
/** Permissions granted by the host (camera, microphone, geolocation, clipboard-write). */

src/app-bridge.examples.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
1212
import {
1313
CallToolResult,
1414
CallToolResultSchema,
15+
CreateMessageRequest,
16+
CreateMessageResult,
1517
ListResourcesResultSchema,
1618
ReadResourceResultSchema,
1719
ListPromptsResultSchema,
@@ -228,6 +230,26 @@ function AppBridge_oncalltool_forwardToServer(
228230
//#endregion AppBridge_oncalltool_forwardToServer
229231
}
230232

233+
/**
234+
* Example: Forward sampling requests to your LLM provider.
235+
*/
236+
function AppBridge_oncreatesamplingmessage_forwardToLlm(
237+
bridge: AppBridge,
238+
myLlmProvider: {
239+
complete: (
240+
params: CreateMessageRequest["params"],
241+
opts: { signal: AbortSignal },
242+
) => Promise<CreateMessageResult>;
243+
},
244+
) {
245+
//#region AppBridge_oncreatesamplingmessage_forwardToLlm
246+
bridge.oncreatesamplingmessage = async (params, extra) => {
247+
// Apply rate limiting, user approval, cost controls here
248+
return await myLlmProvider.complete(params, { signal: extra.signal });
249+
};
250+
//#endregion AppBridge_oncreatesamplingmessage_forwardToLlm
251+
}
252+
231253
/**
232254
* Example: Forward list resources requests to the MCP server.
233255
*/

src/app-bridge.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,43 @@ describe("App <-> AppBridge integration", () => {
18241824
expect(result.content).toEqual(resultContent);
18251825
});
18261826

1827+
it("oncreatesamplingmessage setter registers handler for sampling/createMessage requests", async () => {
1828+
// Re-create bridge with sampling capability so App's capability check passes
1829+
bridge = new AppBridge(null, testHostInfo, {
1830+
...testHostCapabilities,
1831+
sampling: { tools: {} },
1832+
});
1833+
1834+
const receivedParams: unknown[] = [];
1835+
bridge.oncreatesamplingmessage = async (params) => {
1836+
receivedParams.push(params);
1837+
return {
1838+
role: "assistant",
1839+
content: { type: "text", text: "Hello from the model" },
1840+
model: "test-model",
1841+
stopReason: "endTurn",
1842+
};
1843+
};
1844+
1845+
await bridge.connect(bridgeTransport);
1846+
await app.connect(appTransport);
1847+
1848+
expect(app.getHostCapabilities()?.sampling?.tools).toEqual({});
1849+
1850+
const result = await app.createSamplingMessage({
1851+
messages: [{ role: "user", content: { type: "text", text: "Hi" } }],
1852+
maxTokens: 50,
1853+
});
1854+
1855+
expect(receivedParams).toHaveLength(1);
1856+
expect(receivedParams[0]).toMatchObject({ maxTokens: 50 });
1857+
expect(result.model).toEqual("test-model");
1858+
expect(result.content).toEqual({
1859+
type: "text",
1860+
text: "Hello from the model",
1861+
});
1862+
});
1863+
18271864
it("ondownloadfile setter registers handler for ui/download-file requests", async () => {
18281865
const downloadParams = {
18291866
contents: [

src/app-bridge.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
CallToolRequestSchema,
66
CallToolResult,
77
CallToolResultSchema,
8+
CreateMessageRequest,
9+
CreateMessageRequestSchema,
10+
CreateMessageResult,
11+
CreateMessageResultWithTools,
812
EmptyResult,
913
Implementation,
1014
ListPromptsRequest,
@@ -1049,6 +1053,49 @@ export class AppBridge extends ProtocolWithEvents<
10491053
);
10501054
}
10511055

1056+
/**
1057+
* Register a handler for LLM sampling requests from the view.
1058+
*
1059+
* The view sends standard MCP `sampling/createMessage` requests to obtain
1060+
* LLM completions via the host's model connection. The host has full
1061+
* discretion over which model to use and SHOULD apply rate limiting,
1062+
* cost controls, and user approval (human-in-the-loop) before sampling.
1063+
*
1064+
* Hosts that register this handler SHOULD advertise `sampling` (and
1065+
* `sampling.tools` if tool-calling is supported) in
1066+
* {@link McpUiHostCapabilities `McpUiHostCapabilities`}.
1067+
*
1068+
* @param callback - Handler that receives `CreateMessageRequest` params and
1069+
* returns a `CreateMessageResult` (or `CreateMessageResultWithTools` when
1070+
* `params.tools` was provided)
1071+
* - `params` - Standard MCP sampling params (messages, maxTokens, tools, etc.)
1072+
* - `extra` - Request metadata (abort signal, session info)
1073+
*
1074+
* @example Forward to your LLM provider
1075+
* ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm"
1076+
* bridge.oncreatesamplingmessage = async (params, extra) => {
1077+
* // Apply rate limiting, user approval, cost controls here
1078+
* return await myLlmProvider.complete(params, { signal: extra.signal });
1079+
* };
1080+
* ```
1081+
*
1082+
* @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type
1083+
* @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types
1084+
*/
1085+
set oncreatesamplingmessage(
1086+
callback: (
1087+
params: CreateMessageRequest["params"],
1088+
extra: RequestHandlerExtra,
1089+
) => Promise<CreateMessageResult | CreateMessageResultWithTools>,
1090+
) {
1091+
this.setRequestHandler(
1092+
CreateMessageRequestSchema,
1093+
async (request, extra) => {
1094+
return callback(request.params, extra);
1095+
},
1096+
);
1097+
}
1098+
10521099
/**
10531100
* Notify the view that the MCP server's tool list has changed.
10541101
*

src/app.examples.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,54 @@ async function App_callServerTool_fetchWeather(app: App) {
346346
//#endregion App_callServerTool_fetchWeather
347347
}
348348

349+
/**
350+
* Example: Simple LLM completion via host sampling.
351+
*/
352+
async function App_createSamplingMessage_simple(app: App) {
353+
//#region App_createSamplingMessage_simple
354+
const result = await app.createSamplingMessage({
355+
messages: [
356+
{
357+
role: "user",
358+
content: { type: "text", text: "Summarize this in one line." },
359+
},
360+
],
361+
maxTokens: 100,
362+
});
363+
console.log(result.content);
364+
//#endregion App_createSamplingMessage_simple
365+
}
366+
367+
/**
368+
* Example: Agentic loop with tools (requires host sampling.tools capability).
369+
*/
370+
async function App_createSamplingMessage_withTools(
371+
app: App,
372+
messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[],
373+
) {
374+
//#region App_createSamplingMessage_withTools
375+
if (!app.getHostCapabilities()?.sampling?.tools) return;
376+
377+
const result = await app.createSamplingMessage({
378+
messages,
379+
maxTokens: 1024,
380+
tools: [
381+
{
382+
name: "get_weather",
383+
description: "Get the current weather",
384+
inputSchema: {
385+
type: "object",
386+
properties: { city: { type: "string" } },
387+
},
388+
},
389+
],
390+
});
391+
if (result.stopReason === "toolUse") {
392+
// result.content may be an array containing tool_use blocks
393+
}
394+
//#endregion App_createSamplingMessage_withTools
395+
}
396+
349397
/**
350398
* Example: Read a video resource and play it.
351399
*/

src/app.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import {
99
CallToolRequestSchema,
1010
CallToolResult,
1111
CallToolResultSchema,
12+
CreateMessageRequest,
13+
CreateMessageResult,
14+
CreateMessageResultSchema,
15+
CreateMessageResultWithTools,
16+
CreateMessageResultWithToolsSchema,
1217
EmptyResultSchema,
1318
Implementation,
1419
ListResourcesRequest,
@@ -1059,8 +1064,16 @@ export class App extends ProtocolWithEvents<
10591064
* Verify that the host supports the capability required for the given request method.
10601065
* @internal
10611066
*/
1062-
assertCapabilityForMethod(_method: AppRequest["method"]): void {
1063-
// TODO
1067+
assertCapabilityForMethod(method: AppRequest["method"]): void {
1068+
switch (method) {
1069+
case "sampling/createMessage":
1070+
if (!this._hostCapabilities?.sampling) {
1071+
throw new Error(
1072+
`Host does not support sampling (required for ${method})`,
1073+
);
1074+
}
1075+
break;
1076+
}
10641077
}
10651078

10661079
/**
@@ -1269,6 +1282,90 @@ export class App extends ProtocolWithEvents<
12691282
);
12701283
}
12711284

1285+
/**
1286+
* Request an LLM completion from the host (standard MCP `sampling/createMessage`).
1287+
*
1288+
* Enables the app to use the host's model connection for completions. The host
1289+
* has full discretion over which model to select and MAY modify or reject the
1290+
* request (human-in-the-loop). Check {@link getHostCapabilities `getHostCapabilities`}`()?.sampling`
1291+
* before calling — hosts without this capability will reject the request.
1292+
*
1293+
* This method reuses the stock MCP `CreateMessageRequest` shape. When `params.tools`
1294+
* is provided, the result is parsed with the extended schema that permits
1295+
* `stopReason: "toolUse"` and array content containing `tool_use` blocks.
1296+
*
1297+
* @param params - Standard MCP `CreateMessageRequest` params (messages, maxTokens,
1298+
* systemPrompt, temperature, modelPreferences, tools, toolChoice, etc.)
1299+
* @param options - Request options (timeout, abort signal)
1300+
* @returns `CreateMessageResult` (single content block) or `CreateMessageResultWithTools`
1301+
* (array content, may include `tool_use` blocks) depending on whether `tools` was set
1302+
*
1303+
* @throws {Error} If the host rejects the request or does not support sampling
1304+
* @throws {Error} If the request times out or the connection is lost
1305+
*
1306+
* @example Simple completion
1307+
* ```ts source="./app.examples.ts#App_createSamplingMessage_simple"
1308+
* const result = await app.createSamplingMessage({
1309+
* messages: [
1310+
* {
1311+
* role: "user",
1312+
* content: { type: "text", text: "Summarize this in one line." },
1313+
* },
1314+
* ],
1315+
* maxTokens: 100,
1316+
* });
1317+
* console.log(result.content);
1318+
* ```
1319+
*
1320+
* @example Agentic loop with tools
1321+
* ```ts source="./app.examples.ts#App_createSamplingMessage_withTools"
1322+
* if (!app.getHostCapabilities()?.sampling?.tools) return;
1323+
*
1324+
* const result = await app.createSamplingMessage({
1325+
* messages,
1326+
* maxTokens: 1024,
1327+
* tools: [
1328+
* {
1329+
* name: "get_weather",
1330+
* description: "Get the current weather",
1331+
* inputSchema: {
1332+
* type: "object",
1333+
* properties: { city: { type: "string" } },
1334+
* },
1335+
* },
1336+
* ],
1337+
* });
1338+
* if (result.stopReason === "toolUse") {
1339+
* // result.content may be an array containing tool_use blocks
1340+
* }
1341+
* ```
1342+
*
1343+
* @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type
1344+
* @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types
1345+
*/
1346+
async createSamplingMessage(
1347+
params: CreateMessageRequest["params"] & { tools?: undefined },
1348+
options?: RequestOptions,
1349+
): Promise<CreateMessageResult>;
1350+
async createSamplingMessage(
1351+
params: CreateMessageRequest["params"],
1352+
options?: RequestOptions,
1353+
): Promise<CreateMessageResultWithTools>;
1354+
async createSamplingMessage(
1355+
params: CreateMessageRequest["params"],
1356+
options?: RequestOptions,
1357+
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
1358+
this._assertInitialized("createSamplingMessage");
1359+
const resultSchema = params.tools
1360+
? CreateMessageResultWithToolsSchema
1361+
: CreateMessageResultSchema;
1362+
return await this.request(
1363+
{ method: "sampling/createMessage", params },
1364+
resultSchema,
1365+
options,
1366+
);
1367+
}
1368+
12721369
/**
12731370
* Send a message to the host's chat interface.
12741371
*

src/generated/schema.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)