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,
|
||||
EXTENSION_ID,
|
||||
} 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", () => {
|
||||
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", () => {
|
||||
it("should register a resource with default MIME type", () => {
|
||||
let capturedName: string | undefined;
|
||||
|
|
|
|||
|
|
@ -39,31 +39,69 @@ import {
|
|||
} from "../spec.types.js";
|
||||
import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ClientCapabilities,
|
||||
InputRequiredResult,
|
||||
McpServer,
|
||||
ReadResourceResult,
|
||||
RegisteredTool,
|
||||
ResourceMetadata,
|
||||
ServerContext,
|
||||
StandardSchemaWithJSON,
|
||||
ToolAnnotations,
|
||||
ToolCallback,
|
||||
ReadResourceCallback as _ReadResourceCallback,
|
||||
RegisteredResource,
|
||||
} from "@modelcontextprotocol/server";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// Re-exports for convenience
|
||||
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
|
||||
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.
|
||||
* 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 {
|
||||
title?: string;
|
||||
description?: string;
|
||||
inputSchema?: StandardSchemaWithJSON;
|
||||
outputSchema?: StandardSchemaWithJSON;
|
||||
inputSchema?: ZodRawShape | StandardSchemaWithJSON;
|
||||
outputSchema?: ZodRawShape | StandardSchemaWithJSON;
|
||||
annotations?: ToolAnnotations;
|
||||
_meta?: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -217,6 +255,31 @@ export function registerAppTool<
|
|||
outputSchema?: OutputArgs;
|
||||
},
|
||||
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 {
|
||||
// Normalize metadata for backward compatibility:
|
||||
// - 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 } };
|
||||
}
|
||||
|
||||
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 & {
|
||||
|
|
|
|||
Loading…
Reference in a new issue