Address review: MCPB build, guard the SDK's own handlers, doc fixes
- pdf-server build-mcpb.mjs packs the ext-apps build from this checkout
into the staging dir instead of asking the registry for ^2.0.0, which
is not published yet; CI's "Build MCPB bundle" step failed on ETARGET.
- Claim notifications/cancelled and notifications/progress in the
MethodRegistry right after super() so a direct setNotificationHandler
cannot silently replace the base Protocol's own handlers.
- Migration guide and add-app-to-server skill: custom methods use the
three-argument setRequestHandler("m", { params }, handler) form (the
two-argument form is only for spec methods); zod schemas live in
@modelcontextprotocol/core, not client/server; it is
toolInfo.tool.outputSchema that loosened in schema.json, and the
recursive JSON-value def is nested under three definitions; core is a
required peer that client already depends on.
- app-bridge.ts: fix "forthe"/"forresult" typos in @see links, update
three JSDoc examples to the method-string setNotificationHandler form,
move the UntypedHandlerSetter type below the import block (same in
app.ts).
- wire-compat.test.ts: say the fixtures are 1.x-shaped, not captures.
This commit is contained in:
parent
f722753c8f
commit
6d083cd69f
7 changed files with 65 additions and 34 deletions
|
|
@ -123,7 +123,7 @@ resources:
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of
|
Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of
|
||||||
`ext-apps` (`@modelcontextprotocol/core` is pulled in transitively).
|
`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you).
|
||||||
|
|
||||||
For a View or host:
|
For a View or host:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ host-side deltas are in error responses (see below).
|
||||||
| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) |
|
| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) |
|
||||||
|
|
||||||
`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend
|
`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend
|
||||||
its `Protocol` class); `@modelcontextprotocol/core` is installed transitively;
|
its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it;
|
||||||
`@modelcontextprotocol/server` stays optional and is only needed for the
|
`@modelcontextprotocol/server` stays optional and is only needed for the
|
||||||
`./server` helpers. Node.js 20+ is required.
|
`./server` helpers. Node.js 20+ is required.
|
||||||
|
|
||||||
|
|
@ -48,8 +48,10 @@ its `Protocol` class); `@modelcontextprotocol/core` is installed transitively;
|
||||||
`extra.mcpReq.id`.
|
`extra.mcpReq.id`.
|
||||||
- **`setRequestHandler` / `setNotificationHandler` take method names.**
|
- **`setRequestHandler` / `setNotificationHandler` take method names.**
|
||||||
`app.setRequestHandler(SomeRequestSchema, handler)` becomes
|
`app.setRequestHandler(SomeRequestSchema, handler)` becomes
|
||||||
`app.setRequestHandler("some/method", handler)` (pass the params schema as the
|
`app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)`
|
||||||
optional extra argument for validation).
|
for custom methods (the handler receives the parsed params); the two-argument
|
||||||
|
`setRequestHandler("tools/call", handler)` form exists only for spec-defined
|
||||||
|
method names.
|
||||||
- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`);
|
- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`);
|
||||||
local conditions are `SdkError` with a string `code`: request timeout →
|
local conditions are `SdkError` with a string `code`: request timeout →
|
||||||
`"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a
|
`"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a
|
||||||
|
|
@ -74,10 +76,15 @@ The published `./schema.json` export is regenerated from the 2.x core schemas:
|
||||||
|
|
||||||
- `McpUiToolResultNotification.params.structuredContent` is any JSON value (was
|
- `McpUiToolResultNotification.params.structuredContent` is any JSON value (was
|
||||||
`type: "object"`).
|
`type: "object"`).
|
||||||
- Result `_meta` documents `io.modelcontextprotocol/serverInfo` and no longer
|
- `McpUiToolResultNotification.params._meta` documents
|
||||||
lists `progressToken` / `related-task` (both still pass through).
|
`io.modelcontextprotocol/serverInfo` and no longer lists `progressToken` /
|
||||||
- `McpUiHostContext.toolInfo.tool.inputSchema` is a loose object.
|
`related-task` (both still pass through).
|
||||||
- A recursive `$defs.__schema0` JSON-value definition is added.
|
- `McpUiHostContext.toolInfo.tool.outputSchema` is a loose object (only
|
||||||
|
`$schema` is documented); `inputSchema.properties` values are now typed as
|
||||||
|
JSON values.
|
||||||
|
- A recursive JSON-value definition (`__schema0`) is added under the `$defs` of
|
||||||
|
`McpUiHostContext`, `McpUiHostContextChangedNotification` and
|
||||||
|
`McpUiInitializeResult`.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
|
@ -87,7 +94,8 @@ The published `./schema.json` export is regenerated from the 2.x core schemas:
|
||||||
`@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` →
|
`@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` →
|
||||||
`NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`,
|
`NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`,
|
||||||
`sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js`
|
`sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js`
|
||||||
→ `@modelcontextprotocol/client` or `server`).
|
→ `@modelcontextprotocol/client` or `server` for the types,
|
||||||
|
`@modelcontextprotocol/core` for the zod schemas).
|
||||||
3. Wrap raw zod shapes with `z.object({...})`.
|
3. Wrap raw zod shapes with `z.object({...})`.
|
||||||
4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed
|
4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed
|
||||||
`setRequestHandler` calls.
|
`setRequestHandler` calls.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,21 @@ writeFileSync(
|
||||||
path.join(stage, "manifest.json"),
|
path.join(stage, "manifest.json"),
|
||||||
JSON.stringify(manifest, null, 2),
|
JSON.stringify(manifest, null, 2),
|
||||||
);
|
);
|
||||||
|
// The bundle must ship the ext-apps build from this checkout, not whatever
|
||||||
|
// the registry has: pack the repo root and point the staged manifest at it.
|
||||||
|
const repoRoot = path.resolve(here, "..", "..");
|
||||||
|
const packed = JSON.parse(
|
||||||
|
execSync(
|
||||||
|
"npm pack --json --ignore-scripts --pack-destination " +
|
||||||
|
JSON.stringify(stage),
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
).replace(/^[^[]*/, ""),
|
||||||
|
);
|
||||||
|
pkg.dependencies["@modelcontextprotocol/ext-apps"] =
|
||||||
|
"file:./" + packed[0].filename;
|
||||||
writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2));
|
writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2));
|
||||||
|
|
||||||
const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" });
|
const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" });
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,11 @@ handler schemas before adding the App integration:
|
||||||
| `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` |
|
| `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` |
|
||||||
| Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` |
|
| Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` |
|
||||||
| `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
|
| `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
|
||||||
| `sdk/types.js` (types, schemas) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
|
| `sdk/types.js` types (`CallToolResult`, …) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
|
||||||
|
| `sdk/types.js` zod schemas (`CallToolResultSchema`, …) | `@modelcontextprotocol/core` |
|
||||||
| Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` |
|
| Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` |
|
||||||
| `extra.signal` in tool callbacks | `extra.mcpReq.signal` |
|
| `extra.signal` in tool callbacks | `extra.mcpReq.signal` |
|
||||||
| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", handler)` |
|
| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", { params: ParamsSchema }, handler)` (the 2-arg form is only for spec methods such as `"tools/call"`) |
|
||||||
|
|
||||||
## Step 3: Set Up the Build Pipeline
|
## Step 3: Set Up the Build Pipeline
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,6 @@ type MethodSchema = ZodObject<{
|
||||||
params: ZodType;
|
params: ZodType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type McpUiSandboxResourceReadyNotification,
|
type McpUiSandboxResourceReadyNotification,
|
||||||
type McpUiSizeChangedNotification,
|
type McpUiSizeChangedNotification,
|
||||||
|
|
@ -93,6 +91,8 @@ import {
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js";
|
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js";
|
||||||
import { RESOURCE_URI_META_KEY } from "./constants.js";
|
import { RESOURCE_URI_META_KEY } from "./constants.js";
|
||||||
|
|
||||||
|
type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
|
||||||
export { PostMessageTransport } from "./message-transport.js";
|
export { PostMessageTransport } from "./message-transport.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -527,6 +527,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
options?: HostOptions,
|
options?: HostOptions,
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
|
this._methods.replace("notifications/cancelled");
|
||||||
|
this._methods.replace("notifications/progress");
|
||||||
|
|
||||||
this._ensureEventSlot("initialized");
|
this._ensureEventSlot("initialized");
|
||||||
|
|
||||||
|
|
@ -1213,8 +1215,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `CallToolRequest` from @modelcontextprotocol/client forthe request type
|
* @see `CallToolRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `CallToolResult` from @modelcontextprotocol/client forthe result type
|
* @see `CallToolResult` from @modelcontextprotocol/client for the result type
|
||||||
*/
|
*/
|
||||||
private _oncalltool?: (
|
private _oncalltool?: (
|
||||||
params: CallToolRequest["params"],
|
params: CallToolRequest["params"],
|
||||||
|
|
@ -1267,8 +1269,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `CreateMessageRequest` from @modelcontextprotocol/client forthe request type
|
* @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client forresult types
|
* @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types
|
||||||
*/
|
*/
|
||||||
private _oncreatesamplingmessage?: (
|
private _oncreatesamplingmessage?: (
|
||||||
params: CreateMessageRequest["params"],
|
params: CreateMessageRequest["params"],
|
||||||
|
|
@ -1314,12 +1316,12 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // In your MCP client notification handler:
|
* // In your MCP client notification handler:
|
||||||
* mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
* mcpClient.setNotificationHandler("notifications/tools/list_changed", () => {
|
||||||
* bridge.sendToolListChanged();
|
* bridge.sendToolListChanged();
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ToolListChangedNotification` from @modelcontextprotocol/client forthe notification type
|
* @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type
|
||||||
*/
|
*/
|
||||||
sendToolListChanged(params: ToolListChangedNotification["params"] = {}) {
|
sendToolListChanged(params: ToolListChangedNotification["params"] = {}) {
|
||||||
return this.notification({
|
return this.notification({
|
||||||
|
|
@ -1350,8 +1352,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ListResourcesRequest` from @modelcontextprotocol/client forthe request type
|
* @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `ListResourcesResult` from @modelcontextprotocol/client forthe result type
|
* @see `ListResourcesResult` from @modelcontextprotocol/client for the result type
|
||||||
*/
|
*/
|
||||||
private _onlistresources?: (
|
private _onlistresources?: (
|
||||||
params: ListResourcesRequest["params"],
|
params: ListResourcesRequest["params"],
|
||||||
|
|
@ -1403,8 +1405,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client forthe request type
|
* @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `ListResourceTemplatesResult` from @modelcontextprotocol/client forthe result type
|
* @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type
|
||||||
*/
|
*/
|
||||||
private _onlistresourcetemplates?: (
|
private _onlistresourcetemplates?: (
|
||||||
params: ListResourceTemplatesRequest["params"],
|
params: ListResourceTemplatesRequest["params"],
|
||||||
|
|
@ -1459,8 +1461,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ReadResourceRequest` from @modelcontextprotocol/client forthe request type
|
* @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `ReadResourceResult` from @modelcontextprotocol/client forthe result type
|
* @see `ReadResourceResult` from @modelcontextprotocol/client for the result type
|
||||||
*/
|
*/
|
||||||
private _onreadresource?: (
|
private _onreadresource?: (
|
||||||
params: ReadResourceRequest["params"],
|
params: ReadResourceRequest["params"],
|
||||||
|
|
@ -1502,12 +1504,12 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // In your MCP client notification handler:
|
* // In your MCP client notification handler:
|
||||||
* mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
|
* mcpClient.setNotificationHandler("notifications/resources/list_changed", () => {
|
||||||
* bridge.sendResourceListChanged();
|
* bridge.sendResourceListChanged();
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ResourceListChangedNotification` from @modelcontextprotocol/client forthe notification type
|
* @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type
|
||||||
*/
|
*/
|
||||||
sendResourceListChanged(
|
sendResourceListChanged(
|
||||||
params: ResourceListChangedNotification["params"] = {},
|
params: ResourceListChangedNotification["params"] = {},
|
||||||
|
|
@ -1540,8 +1542,8 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `ListPromptsRequest` from @modelcontextprotocol/client forthe request type
|
* @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type
|
||||||
* @see `ListPromptsResult` from @modelcontextprotocol/client forthe result type
|
* @see `ListPromptsResult` from @modelcontextprotocol/client for the result type
|
||||||
*/
|
*/
|
||||||
private _onlistprompts?: (
|
private _onlistprompts?: (
|
||||||
params: ListPromptsRequest["params"],
|
params: ListPromptsRequest["params"],
|
||||||
|
|
@ -1582,12 +1584,12 @@ export class AppBridge extends Protocol<BaseContext> {
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // In your MCP client notification handler:
|
* // In your MCP client notification handler:
|
||||||
* mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
* mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => {
|
||||||
* bridge.sendPromptListChanged();
|
* bridge.sendPromptListChanged();
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @see `PromptListChangedNotification` from @modelcontextprotocol/client forthe notification type
|
* @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type
|
||||||
*/
|
*/
|
||||||
sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) {
|
sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) {
|
||||||
return this.notification({
|
return this.notification({
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js";
|
||||||
import { EventDispatcher, MethodRegistry } from "./events.js";
|
import { EventDispatcher, MethodRegistry } from "./events.js";
|
||||||
export { EventDispatcher } from "./events.js";
|
export { EventDispatcher } from "./events.js";
|
||||||
|
|
||||||
type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
|
|
||||||
import { PostMessageTransport } from "./message-transport.js";
|
import { PostMessageTransport } from "./message-transport.js";
|
||||||
import {
|
import {
|
||||||
LATEST_PROTOCOL_VERSION,
|
LATEST_PROTOCOL_VERSION,
|
||||||
|
|
@ -69,6 +68,8 @@ import {
|
||||||
} from "./standard-schema.js";
|
} from "./standard-schema.js";
|
||||||
import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4";
|
import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4";
|
||||||
|
|
||||||
|
type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
|
||||||
|
|
||||||
type MethodSchema = ZodObject<{
|
type MethodSchema = ZodObject<{
|
||||||
method: ZodLiteral<string>;
|
method: ZodLiteral<string>;
|
||||||
params: ZodType;
|
params: ZodType;
|
||||||
|
|
@ -530,6 +531,10 @@ export class App extends Protocol<BaseContext> {
|
||||||
private options: AppOptions = { autoResize: true },
|
private options: AppOptions = { autoResize: true },
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
|
// Claim the handlers the base Protocol constructor installed so a direct
|
||||||
|
// setNotificationHandler cannot silently replace them either.
|
||||||
|
this._methods.replace("notifications/cancelled");
|
||||||
|
this._methods.replace("notifications/progress");
|
||||||
|
|
||||||
if (!options.allowUnsafeEval) {
|
if (!options.allowUnsafeEval) {
|
||||||
z.config({ jitless: true });
|
z.config({ jitless: true });
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const hostContext: McpUiHostContext = {
|
||||||
locale: "en-US",
|
locale: "en-US",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Captured from ext-apps 1.7.x View (sdk 1.x key order, numeric ids from 0). */
|
/** Shaped like ext-apps 1.7.x View traffic (sdk-1 key order, 0-based numeric ids); not a recorded capture. */
|
||||||
const v1View = {
|
const v1View = {
|
||||||
initialize: {
|
initialize: {
|
||||||
method: "ui/initialize",
|
method: "ui/initialize",
|
||||||
|
|
@ -120,7 +120,7 @@ const v1View = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Captured from ext-apps 1.7.x AppBridge (sdk 1.x). */
|
/** Shaped like ext-apps 1.7.x AppBridge traffic (sdk 1.x); not a recorded capture. */
|
||||||
const v1Host = {
|
const v1Host = {
|
||||||
initializeResult: (id: number) => ({
|
initializeResult: (id: number) => ({
|
||||||
result: {
|
result: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue