Accept raw zod shapes in registerAppTool as a deprecated overload
SDK v2 registerTool takes Standard JSON Schema objects (z.object(...))
and keeps a deprecated overload for the v1 raw-shape form. Mirror that
here so existing servers passing { field: z.string() } keep compiling;
the call is forwarded to the SDK, which auto-wraps the shape. Tests
exercise both forms end to end through a real McpServer and Client.
This commit is contained in:
parent
040942b3af
commit
d6ebcca9a0
2 changed files with 195 additions and 4 deletions
|
|
@ -7,7 +7,13 @@ import {
|
||||||
getUiCapability,
|
getUiCapability,
|
||||||
EXTENSION_ID,
|
EXTENSION_ID,
|
||||||
} from "./index.js";
|
} from "./index.js";
|
||||||
import type { McpServer } from "@modelcontextprotocol/server";
|
import {
|
||||||
|
InMemoryTransport,
|
||||||
|
McpServer as RealMcpServer,
|
||||||
|
type McpServer,
|
||||||
|
} from "@modelcontextprotocol/server";
|
||||||
|
import { Client } from "@modelcontextprotocol/client";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
describe("registerAppTool", () => {
|
describe("registerAppTool", () => {
|
||||||
it("should pass through config to server.registerTool", () => {
|
it("should pass through config to server.registerTool", () => {
|
||||||
|
|
@ -197,6 +203,119 @@ describe("registerAppTool", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("registerAppTool schema forms", () => {
|
||||||
|
/** Registers tools on a fresh server, then connects a client to it. */
|
||||||
|
async function connect(register: (server: RealMcpServer) => void) {
|
||||||
|
const server = new RealMcpServer({ name: "test", version: "0.0.0" });
|
||||||
|
register(server);
|
||||||
|
const client = new Client({ name: "client", version: "0.0.0" });
|
||||||
|
const [clientTransport, serverTransport] =
|
||||||
|
InMemoryTransport.createLinkedPair();
|
||||||
|
await server.connect(serverTransport);
|
||||||
|
await client.connect(clientTransport);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("accepts a z.object() input schema and validates arguments", async () => {
|
||||||
|
const client = await connect((server) =>
|
||||||
|
registerAppTool(
|
||||||
|
server,
|
||||||
|
"greet",
|
||||||
|
{
|
||||||
|
inputSchema: z.object({ name: z.string() }),
|
||||||
|
_meta: { ui: { resourceUri: "ui://greet/view.html" } },
|
||||||
|
},
|
||||||
|
async ({ name }) => ({
|
||||||
|
content: [{ type: "text", text: `hello ${name}` }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
expect(tools).toHaveLength(1);
|
||||||
|
expect(tools[0].inputSchema).toMatchObject({
|
||||||
|
type: "object",
|
||||||
|
properties: { name: { type: "string" } },
|
||||||
|
});
|
||||||
|
expect(tools[0]._meta).toMatchObject({
|
||||||
|
ui: { resourceUri: "ui://greet/view.html" },
|
||||||
|
[RESOURCE_URI_META_KEY]: "ui://greet/view.html",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: "greet",
|
||||||
|
arguments: { name: "world" },
|
||||||
|
});
|
||||||
|
expect(result.content).toEqual([{ type: "text", text: "hello world" }]);
|
||||||
|
|
||||||
|
const invalid = await client.callTool({
|
||||||
|
name: "greet",
|
||||||
|
arguments: { name: 42 },
|
||||||
|
});
|
||||||
|
expect(invalid.isError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a raw zod shape (deprecated form) and wraps it with z.object()", async () => {
|
||||||
|
const client = await connect((server) =>
|
||||||
|
registerAppTool(
|
||||||
|
server,
|
||||||
|
"greet",
|
||||||
|
{
|
||||||
|
inputSchema: { name: z.string() },
|
||||||
|
outputSchema: { greeting: z.string() },
|
||||||
|
_meta: { ui: { resourceUri: "ui://greet/view.html" } },
|
||||||
|
},
|
||||||
|
async ({ name }) => ({
|
||||||
|
content: [{ type: "text", text: `hello ${name}` }],
|
||||||
|
structuredContent: { greeting: `hello ${name}` },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
expect(tools[0].inputSchema).toMatchObject({
|
||||||
|
type: "object",
|
||||||
|
properties: { name: { type: "string" } },
|
||||||
|
});
|
||||||
|
expect(tools[0].outputSchema).toMatchObject({
|
||||||
|
type: "object",
|
||||||
|
properties: { greeting: { type: "string" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: "greet",
|
||||||
|
arguments: { name: "world" },
|
||||||
|
});
|
||||||
|
expect(result.structuredContent).toEqual({ greeting: "hello world" });
|
||||||
|
|
||||||
|
const invalid = await client.callTool({
|
||||||
|
name: "greet",
|
||||||
|
arguments: { name: 42 },
|
||||||
|
});
|
||||||
|
expect(invalid.isError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts the raw-shape form without an input schema", async () => {
|
||||||
|
const client = await connect((server) =>
|
||||||
|
registerAppTool(
|
||||||
|
server,
|
||||||
|
"ping",
|
||||||
|
{
|
||||||
|
outputSchema: { ok: z.boolean() },
|
||||||
|
_meta: { ui: { resourceUri: "ui://ping/view.html" } },
|
||||||
|
},
|
||||||
|
async () => ({
|
||||||
|
content: [{ type: "text", text: "pong" }],
|
||||||
|
structuredContent: { ok: true },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await client.callTool({ name: "ping", arguments: {} });
|
||||||
|
expect(result.structuredContent).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("registerAppResource", () => {
|
describe("registerAppResource", () => {
|
||||||
it("should register a resource with default MIME type", () => {
|
it("should register a resource with default MIME type", () => {
|
||||||
let capturedName: string | undefined;
|
let capturedName: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -39,31 +39,69 @@ import {
|
||||||
} from "../spec.types.js";
|
} from "../spec.types.js";
|
||||||
import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js";
|
import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js";
|
||||||
import type {
|
import type {
|
||||||
|
CallToolResult,
|
||||||
ClientCapabilities,
|
ClientCapabilities,
|
||||||
|
InputRequiredResult,
|
||||||
McpServer,
|
McpServer,
|
||||||
ReadResourceResult,
|
ReadResourceResult,
|
||||||
RegisteredTool,
|
RegisteredTool,
|
||||||
ResourceMetadata,
|
ResourceMetadata,
|
||||||
|
ServerContext,
|
||||||
StandardSchemaWithJSON,
|
StandardSchemaWithJSON,
|
||||||
ToolAnnotations,
|
ToolAnnotations,
|
||||||
ToolCallback,
|
ToolCallback,
|
||||||
ReadResourceCallback as _ReadResourceCallback,
|
ReadResourceCallback as _ReadResourceCallback,
|
||||||
RegisteredResource,
|
RegisteredResource,
|
||||||
} from "@modelcontextprotocol/server";
|
} from "@modelcontextprotocol/server";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
// Re-exports for convenience
|
// Re-exports for convenience
|
||||||
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
|
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
|
||||||
export type { ResourceMetadata, ToolCallback };
|
export type { ResourceMetadata, ToolCallback };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plain `{ field: z.string() }` record accepted by the deprecated raw-shape
|
||||||
|
* form of {@link registerAppTool `registerAppTool`}; the SDK auto-wraps it with `z.object()`.
|
||||||
|
*
|
||||||
|
* @deprecated Wrap with `z.object({...})` instead.
|
||||||
|
*/
|
||||||
|
export type ZodRawShape = Record<string, z.ZodType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ToolCallback `ToolCallback`} variant used when `inputSchema` is a {@link ZodRawShape `ZodRawShape`}.
|
||||||
|
* Mirrors the callback type of the SDK's deprecated `registerTool` overload.
|
||||||
|
*
|
||||||
|
* @deprecated Wrap with `z.object({...})` instead and use {@link ToolCallback `ToolCallback`}.
|
||||||
|
*/
|
||||||
|
export type LegacyToolCallback<Args extends ZodRawShape | undefined> =
|
||||||
|
Args extends ZodRawShape
|
||||||
|
? (
|
||||||
|
args: z.infer<z.ZodObject<Args>>,
|
||||||
|
ctx: ServerContext,
|
||||||
|
) =>
|
||||||
|
| CallToolResult
|
||||||
|
| InputRequiredResult
|
||||||
|
| Promise<CallToolResult | InputRequiredResult>
|
||||||
|
: (
|
||||||
|
ctx: ServerContext,
|
||||||
|
) =>
|
||||||
|
| CallToolResult
|
||||||
|
| InputRequiredResult
|
||||||
|
| Promise<CallToolResult | InputRequiredResult>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base tool configuration matching the standard MCP server tool options.
|
* Base tool configuration matching the standard MCP server tool options.
|
||||||
* Extended by {@link McpUiAppToolConfig `McpUiAppToolConfig`} to add UI metadata requirements.
|
* Extended by {@link McpUiAppToolConfig `McpUiAppToolConfig`} to add UI metadata requirements.
|
||||||
|
*
|
||||||
|
* `inputSchema`/`outputSchema` accept any Standard JSON Schema (zod v4, ArkType,
|
||||||
|
* Valibot, ...). A raw zod shape (`{ field: z.string() }`) is still accepted for
|
||||||
|
* backward compatibility but deprecated.
|
||||||
*/
|
*/
|
||||||
export interface ToolConfig {
|
export interface ToolConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
inputSchema?: StandardSchemaWithJSON;
|
inputSchema?: ZodRawShape | StandardSchemaWithJSON;
|
||||||
outputSchema?: StandardSchemaWithJSON;
|
outputSchema?: ZodRawShape | StandardSchemaWithJSON;
|
||||||
annotations?: ToolAnnotations;
|
annotations?: ToolAnnotations;
|
||||||
_meta?: Record<string, unknown>;
|
_meta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +255,31 @@ export function registerAppTool<
|
||||||
outputSchema?: OutputArgs;
|
outputSchema?: OutputArgs;
|
||||||
},
|
},
|
||||||
cb: ToolCallback<InputArgs>,
|
cb: ToolCallback<InputArgs>,
|
||||||
|
): RegisteredTool;
|
||||||
|
/**
|
||||||
|
* @deprecated Wrap with `z.object({...})` instead. Raw-shape form:
|
||||||
|
* `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record;
|
||||||
|
* the SDK auto-wraps it with `z.object()`.
|
||||||
|
*/
|
||||||
|
export function registerAppTool<
|
||||||
|
InputArgs extends ZodRawShape,
|
||||||
|
OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined =
|
||||||
|
undefined,
|
||||||
|
>(
|
||||||
|
server: Pick<McpServer, "registerTool">,
|
||||||
|
name: string,
|
||||||
|
config: McpUiAppToolConfig & {
|
||||||
|
inputSchema?: InputArgs;
|
||||||
|
outputSchema?: OutputArgs;
|
||||||
|
},
|
||||||
|
cb: LegacyToolCallback<InputArgs>,
|
||||||
|
): RegisteredTool;
|
||||||
|
export function registerAppTool(
|
||||||
|
server: Pick<McpServer, "registerTool">,
|
||||||
|
name: string,
|
||||||
|
config: McpUiAppToolConfig,
|
||||||
|
// Widest callback shape accepted by either overload above.
|
||||||
|
cb: (...args: never[]) => unknown,
|
||||||
): RegisteredTool {
|
): RegisteredTool {
|
||||||
// Normalize metadata for backward compatibility:
|
// Normalize metadata for backward compatibility:
|
||||||
// - If _meta.ui.resourceUri is set, also set the legacy flat key
|
// - If _meta.ui.resourceUri is set, also set the legacy flat key
|
||||||
|
|
@ -234,7 +297,16 @@ export function registerAppTool<
|
||||||
normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } };
|
normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb);
|
// The public overloads above guarantee `config`/`cb` match one of the SDK's
|
||||||
|
// own registerTool overloads; the union-typed implementation cannot select
|
||||||
|
// between them, so widen for the forwarding call.
|
||||||
|
return server.registerTool(
|
||||||
|
name,
|
||||||
|
{ ...config, _meta: normalizedMeta } as Parameters<
|
||||||
|
McpServer["registerTool"]
|
||||||
|
>[1],
|
||||||
|
cb as ToolCallback<StandardSchemaWithJSON>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type McpUiReadResourceResult = ReadResourceResult & {
|
export type McpUiReadResourceResult = ReadResourceResult & {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue