From 32ef943f075b39fa5b174afe99a1952f795aa43f Mon Sep 17 00:00:00 2001 From: Naseem AlNaji Date: Thu, 4 Dec 2025 16:39:02 -0500 Subject: [PATCH] feat: add MCP SDK 1.24+ compatibility Support both (SDK 1.23-) and (SDK 1.24+) property names in RegisteredTool to maintain backwards compatibility. Changes: - Update RegisteredTool type to accept either callback or handler - Add utility functions to abstract property name detection - Extract MCP SDK compat helpers into dedicated module - Preserve original property name when wrapping tools - Remove 1.24 version restriction from CI compatibility workflow Tested with MCP SDK versions 1.23.0 and 1.24.2. --- .github/workflows/mcp-compatibility.yml | 8 +- package.json | 2 +- pnpm-lock.yaml | 90 ++++---------- src/modules/mcp-sdk-compat.ts | 155 ++++++++++++++++++++++++ src/modules/tracingV2.ts | 132 +++++--------------- src/tests/handler-property.test.ts | 105 ++++++++++++++++ src/types.ts | 7 +- 7 files changed, 322 insertions(+), 177 deletions(-) create mode 100644 src/modules/mcp-sdk-compat.ts create mode 100644 src/tests/handler-property.test.ts diff --git a/.github/workflows/mcp-compatibility.yml b/.github/workflows/mcp-compatibility.yml index fa92749..8cd0b36 100644 --- a/.github/workflows/mcp-compatibility.yml +++ b/.github/workflows/mcp-compatibility.yml @@ -32,17 +32,15 @@ jobs: with: version: 9 - - name: Fetch all available MCP SDK versions (≥1.11, <1.24) + - name: Fetch all available MCP SDK versions (≥1.11) id: get-versions run: | - # Get all versions >= 1.11 and < 1.24, filter to get latest patch for each minor version - # Note: Versions 1.24+ have known compatibility issues + # Get all versions >= 1.11, filter to get latest patch for each minor version VERSIONS=$(pnpm view @modelcontextprotocol/sdk versions --json | jq -c ' map(select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))) | map(select( (split(".")[0] | tonumber == 1) and - (split(".")[1] | tonumber >= 11) and - (split(".")[1] | tonumber < 24) + (split(".")[1] | tonumber >= 11) )) | group_by(split(".")[0:2] | join(".")) | map(max_by(split(".") | map(tonumber))) | diff --git a/package.json b/package.json index e720a6b..f610464 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "packageManager": "pnpm@10.11.0", "devDependencies": { "@changesets/cli": "^2.29.8", - "@modelcontextprotocol/sdk": "~1.23.0", + "@modelcontextprotocol/sdk": "~1.24.2", "@types/node": "^22.15.21", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c4615b..fec1097 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: ^2.29.8 version: 2.29.8(@types/node@22.15.21) "@modelcontextprotocol/sdk": - specifier: ~1.23.0 - version: 1.23.0(zod@3.25.30) + specifier: ~1.24.2 + version: 1.24.2(zod@3.25.30) "@types/node": specifier: ^22.15.21 version: 22.15.21 @@ -630,10 +630,10 @@ packages: integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, } - "@modelcontextprotocol/sdk@1.23.0": + "@modelcontextprotocol/sdk@1.24.2": resolution: { - integrity: sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA==, + integrity: sha512-hS/kzSfchqzvUeJUsdiDHi84/kNhLIZaZ6coGQVwbYIelOBbcAwUohUfaQTLa1MvFOK/jbTnGFzraHSFwB7pjQ==, } engines: { node: ">=18" } peerDependencies: @@ -2251,13 +2251,6 @@ packages: integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, } - http-errors@2.0.0: - resolution: - { - integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, - } - engines: { node: ">= 0.8" } - http-errors@2.0.1: resolution: { @@ -2280,13 +2273,6 @@ packages: engines: { node: ">=18" } hasBin: true - iconv-lite@0.6.3: - resolution: - { - integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, - } - engines: { node: ">=0.10.0" } - iconv-lite@0.7.0: resolution: { @@ -2437,6 +2423,12 @@ packages: integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, } + jose@6.1.3: + resolution: + { + integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==, + } + joycon@3.1.1: resolution: { @@ -3082,13 +3074,6 @@ packages: } engines: { node: ">= 0.6" } - raw-body@3.0.0: - resolution: - { - integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==, - } - engines: { node: ">= 0.8" } - raw-body@3.0.2: resolution: { @@ -3329,13 +3314,6 @@ packages: integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, } - statuses@2.0.1: - resolution: - { - integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==, - } - engines: { node: ">= 0.8" } - statuses@2.0.2: resolution: { @@ -4133,7 +4111,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - "@modelcontextprotocol/sdk@1.23.0(zod@3.25.30)": + "@modelcontextprotocol/sdk@1.24.2(zod@3.25.30)": dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -4144,8 +4122,9 @@ snapshots: eventsource-parser: 3.0.3 express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.0) + jose: 6.1.3 pkce-challenge: 5.0.0 - raw-body: 3.0.0 + raw-body: 3.0.2 zod: 3.25.30 zod-to-json-schema: 3.25.0(zod@3.25.30) transitivePeerDependencies: @@ -4604,7 +4583,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 @@ -4886,13 +4865,13 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 finalhandler: 2.1.0 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.1 on-finished: 2.4.1 @@ -4904,7 +4883,7 @@ snapshots: router: 2.2.0 send: 1.2.0 serve-static: 2.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -4952,12 +4931,12 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -5074,14 +5053,6 @@ snapshots: html-escaper@2.0.2: {} - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -5094,10 +5065,6 @@ snapshots: husky@9.1.7: {} - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -5170,6 +5137,8 @@ snapshots: optionalDependencies: "@pkgjs/parseargs": 0.11.0 + jose@6.1.3: {} + joycon@3.1.1: {} js-tokens@9.0.1: {} @@ -5486,13 +5455,6 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.0: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -5580,7 +5542,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -5600,17 +5562,17 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -5694,8 +5656,6 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.1: {} - statuses@2.0.2: {} std-env@3.10.0: {} diff --git a/src/modules/mcp-sdk-compat.ts b/src/modules/mcp-sdk-compat.ts new file mode 100644 index 0000000..737bb94 --- /dev/null +++ b/src/modules/mcp-sdk-compat.ts @@ -0,0 +1,155 @@ +/** + * MCP SDK Compatibility Helpers + * + * Internal utilities for handling differences between MCP SDK versions. + * These helpers abstract away SDK-internal details like: + * - Tool callback/handler property names (changed in SDK 1.24) + * - Zod schema internal structures (v3 vs v4) + */ + +import { RegisteredTool, ToolCallback } from "../types.js"; + +// --- Tool function property utilities for MCP SDK version compatibility --- +// MCP SDK 1.23 and earlier use "callback", 1.24+ uses "handler" + +export type ToolFunctionKey = "callback" | "handler"; + +/** + * Returns the tool function (callback/handler) from a RegisteredTool. + * Supports both MCP SDK 1.23- (callback) and 1.24+ (handler). + */ +export function getToolFunction(tool: RegisteredTool): ToolCallback { + if ("handler" in tool && typeof tool.handler === "function") { + return tool.handler; + } + if ("callback" in tool && typeof tool.callback === "function") { + return tool.callback; + } + throw new Error("Tool has neither callback nor handler property"); +} + +/** + * Returns the property key name used for the tool function ("callback" or "handler"). + * This preserves the original property name when wrapping tools. + */ +export function getToolFunctionKey(tool: RegisteredTool): ToolFunctionKey { + if ("handler" in tool && typeof tool.handler === "function") { + return "handler"; + } + return "callback"; +} + +/** + * Returns true if the tool has a callback or handler property. + */ +export function hasToolFunction(tool: unknown): tool is RegisteredTool { + if (!tool || typeof tool !== "object") return false; + const t = tool as Record; + return ( + ("handler" in t && typeof t.handler === "function") || + ("callback" in t && typeof t.callback === "function") + ); +} + +/** + * Creates a new tool object with the wrapped function, preserving the original property name. + * This ensures MCP SDK 1.24+ gets back a tool with "handler" and 1.23- gets "callback". + */ +export function createWrappedTool( + originalTool: RegisteredTool, + wrappedFunction: ToolCallback, +): RegisteredTool { + const key = getToolFunctionKey(originalTool); + return { + ...originalTool, + [key]: wrappedFunction, + } as RegisteredTool; +} + +// --- Zod schema internal property helpers --- +// These access internal properties to extract method names from MCP SDK schemas +// No Zod import needed - we introspect the internal structure directly + +interface ZodV3Internal { + _def?: { + value?: unknown; + values?: unknown[]; // For enums - some Zod versions store literal values here + shape?: Record | (() => Record); + }; + shape?: Record | (() => Record); +} + +interface ZodV4Internal { + _zod?: { + def?: { + value?: unknown; + values?: unknown[]; // For enums - some Zod versions store literal values here + shape?: Record | (() => Record); + }; + }; +} + +export function isZ4Schema(schema: unknown): boolean { + if (!schema || typeof schema !== "object") return false; + return !!(schema as ZodV4Internal)._zod; +} + +export function getObjectShape( + schema: unknown, +): Record | undefined { + if (!schema || typeof schema !== "object") return undefined; + + let rawShape: + | Record + | (() => Record) + | undefined; + + if (isZ4Schema(schema)) { + const v4Schema = schema as ZodV4Internal; + rawShape = v4Schema._zod?.def?.shape; + } else { + const v3Schema = schema as ZodV3Internal; + // Try .shape first, then fall back to _def.shape (some v3 schema types store it there) + rawShape = v3Schema.shape ?? v3Schema._def?.shape; + } + + if (!rawShape) return undefined; + + if (typeof rawShape === "function") { + try { + return rawShape(); + } catch { + return undefined; + } + } + + return rawShape; +} + +export function getLiteralValue(schema: unknown): unknown { + if (!schema || typeof schema !== "object") return undefined; + + if (isZ4Schema(schema)) { + const v4Schema = schema as ZodV4Internal; + const def = v4Schema._zod?.def; + if (def?.value !== undefined) return def.value; + // Fallback: values array (for enums) + if (Array.isArray(def?.values) && def.values.length > 0) { + return def.values[0]; + } + } else { + const v3Schema = schema as ZodV3Internal; + const def = v3Schema._def; + if (def?.value !== undefined) return def.value; + // Fallback: values array (for enums) + if (Array.isArray(def?.values) && def.values.length > 0) { + return def.values[0]; + } + } + + // Final fallback: direct .value property (some Zod versions) + const directValue = (schema as { value?: unknown }).value; + if (directValue !== undefined) return directValue; + + return undefined; +} diff --git a/src/modules/tracingV2.ts b/src/modules/tracingV2.ts index 0f4323f..733443a 100644 --- a/src/modules/tracingV2.ts +++ b/src/modules/tracingV2.ts @@ -14,7 +14,13 @@ import { publishEvent } from "./eventQueue.js"; import { handleReportMissing } from "./tools.js"; import { setupInitializeTracing, setupListToolsTracing } from "./tracing.js"; import { captureException } from "./exceptions.js"; -// Note: No Zod import; we use JSON Schema directly for tool registration. +import { + getToolFunction, + hasToolFunction, + createWrappedTool, + getObjectShape, + getLiteralValue, +} from "./mcp-sdk-compat.js"; // WeakMap to track which callbacks have already been wrapped const wrappedCallbacks = new WeakMap(); @@ -26,93 +32,6 @@ function isToolResultError(result: any): boolean { return result && typeof result === "object" && result.isError === true; } -// --- Minimal Zod internal property helpers (no zod import needed) --- -// These access internal properties to extract method names from MCP SDK schemas - -interface ZodV3Internal { - _def?: { - value?: unknown; - values?: unknown[]; // For enums - some Zod versions store literal values here - shape?: Record | (() => Record); - }; - shape?: Record | (() => Record); -} - -interface ZodV4Internal { - _zod?: { - def?: { - value?: unknown; - values?: unknown[]; // For enums - some Zod versions store literal values here - shape?: Record | (() => Record); - }; - }; -} - -function isZ4Schema(schema: unknown): boolean { - if (!schema || typeof schema !== "object") return false; - return !!(schema as ZodV4Internal)._zod; -} - -function getObjectShape(schema: unknown): Record | undefined { - if (!schema || typeof schema !== "object") return undefined; - - let rawShape: - | Record - | (() => Record) - | undefined; - - if (isZ4Schema(schema)) { - const v4Schema = schema as ZodV4Internal; - rawShape = v4Schema._zod?.def?.shape; - } else { - const v3Schema = schema as ZodV3Internal; - // Try .shape first, then fall back to _def.shape (some v3 schema types store it there) - rawShape = v3Schema.shape ?? v3Schema._def?.shape; - } - - if (!rawShape) return undefined; - - if (typeof rawShape === "function") { - try { - return rawShape(); - } catch { - return undefined; - } - } - - return rawShape; -} - -function getLiteralValue(schema: unknown): unknown { - if (!schema || typeof schema !== "object") return undefined; - - if (isZ4Schema(schema)) { - const v4Schema = schema as ZodV4Internal; - const def = v4Schema._zod?.def; - if (def?.value !== undefined) return def.value; - // Fallback: values array (for enums) - if (Array.isArray(def?.values) && def.values.length > 0) { - return def.values[0]; - } - } else { - const v3Schema = schema as ZodV3Internal; - const def = v3Schema._def; - if (def?.value !== undefined) return def.value; - // Fallback: values array (for enums) - if (Array.isArray(def?.values) && def.values.length > 0) { - return def.values[0]; - } - } - - // Final fallback: direct .value property (some Zod versions) - const directValue = (schema as { value?: unknown }).value; - if (directValue !== undefined) return directValue; - - return undefined; -} - -// --- End of Zod helpers --- - function addTracingToToolRegistry( tools: Record, server: HighLevelMCPServerLike, @@ -141,12 +60,12 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void { value: RegisteredTool, ): boolean { try { - // Check if this is a tool being registered (has callback property) + // Check if this is a tool being registered (has callback or handler property) if ( typeof property === "string" && value && typeof value === "object" && - "callback" in value + hasToolFunction(value) ) { // Check if tool has already been processed if ((value as any)[MCPCAT_PROCESSED]) { @@ -157,8 +76,8 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void { return Reflect.set(target, property, value); } - // Check if callback is already wrapped - if (wrappedCallbacks.has(value.callback)) { + // Check if callback/handler is already wrapped + if (wrappedCallbacks.has(getToolFunction(value))) { writeToLog( `Tool ${String(property)} callback already wrapped, skipping proxy wrapping`, ); @@ -178,12 +97,20 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void { const originalUpdate = value.update; value.update = function (...updateArgs: any[]) { // If callback is being updated, wrap the new callback - if (updateArgs[0] && updateArgs[0].callback) { - updateArgs[0].callback = addTracingToToolCallbackInternal( - { callback: updateArgs[0].callback }, - property, - server, - ).callback; + // Note: MCP SDK's update() method API uses "callback" property in its interface + if (updateArgs[0]) { + const updateObj = updateArgs[0]; + if ( + updateObj.callback && + typeof updateObj.callback === "function" + ) { + const wrappedTool = addTracingToToolCallbackInternal( + { callback: updateObj.callback } as RegisteredTool, + property, + server, + ); + updateObj.callback = getToolFunction(wrappedTool); + } } return originalUpdate.apply(this, updateArgs); }; @@ -240,7 +167,7 @@ function addTracingToToolCallbackInternal( toolName: string, _server: HighLevelMCPServerLike, ): RegisteredTool { - const originalCallback = tool.callback; + const originalCallback = getToolFunction(tool); if (wrappedCallbacks.has(originalCallback)) { writeToLog(`Tool ${toolName} callback already wrapped, skipping re-wrap`); @@ -304,11 +231,8 @@ function addTracingToToolCallbackInternal( // Mark the wrapped callback as well (in case it gets re-wrapped) wrappedCallbacks.set(wrappedCallback, true); - // Create a new tool object with the wrapped callback - const wrappedTool = { - ...tool, - callback: wrappedCallback as RegisteredTool["callback"], - }; + // Create a new tool object with the wrapped callback, preserving the property name + const wrappedTool = createWrappedTool(tool, wrappedCallback); // Mark the tool as processed (wrappedTool as any)[MCPCAT_PROCESSED] = true; diff --git a/src/tests/handler-property.test.ts b/src/tests/handler-property.test.ts new file mode 100644 index 0000000..0433193 --- /dev/null +++ b/src/tests/handler-property.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { track } from "../index.js"; + +// Helper to get the tool function property name for the current MCP SDK version +function getToolFunctionPropertyName(tool: any): "callback" | "handler" { + if ("handler" in tool && typeof tool.handler === "function") { + return "handler"; + } + if ("callback" in tool && typeof tool.callback === "function") { + return "callback"; + } + throw new Error("Tool has neither callback nor handler"); +} + +describe("MCP SDK callback/handler compatibility", () => { + it("should have either callback or handler property (SDK version dependent)", () => { + const server = new McpServer({ name: "test", version: "1.0.0" }); + + server.tool("test_tool", { a: z.number() }, async ({ a }) => { + return { content: [{ type: "text", text: String(a) }] }; + }); + + const tools = (server as any)._registeredTools; + const tool = tools["test_tool"]; + const propName = getToolFunctionPropertyName(tool); + + console.log("\n=== MCP SDK Tool Structure ==="); + console.log("Tool properties:", Object.keys(tool)); + console.log("Has 'callback':", "callback" in tool); + console.log("Has 'handler':", "handler" in tool); + console.log("Tool function property:", propName); + + // Tool should have either 'handler' (1.24+) or 'callback' (1.23-) + const hasToolFunction = + ("handler" in tool && typeof tool.handler === "function") || + ("callback" in tool && typeof tool.callback === "function"); + expect(hasToolFunction).toBe(true); + }); + + it("should preserve the original property name after track() is called", () => { + const server = new McpServer({ name: "test", version: "1.0.0" }); + + server.tool("test_tool", { a: z.number() }, async ({ a }) => { + return { content: [{ type: "text", text: String(a) }] }; + }); + + // Get the property name BEFORE track() + const toolsBefore = (server as any)._registeredTools; + const toolBefore = toolsBefore["test_tool"]; + const originalPropName = getToolFunctionPropertyName(toolBefore); + + // Call track() to apply MCPCat's tracing + track(server, "test-project-id"); + + const toolsAfter = (server as any)._registeredTools; + const toolAfter = toolsAfter["test_tool"]; + const afterPropName = getToolFunctionPropertyName(toolAfter); + + console.log("\n=== After track() ==="); + console.log("Original property name:", originalPropName); + console.log("Property name after track():", afterPropName); + console.log("Has 'callback':", "callback" in toolAfter); + console.log("Has 'handler':", "handler" in toolAfter); + + // MCPCat should preserve the original property name + expect(afterPropName).toBe(originalPropName); + expect(typeof toolAfter[afterPropName]).toBe("function"); + }); + + it("should preserve property name for tools registered after track()", () => { + const server = new McpServer({ name: "test", version: "1.0.0" }); + + // Register a tool first to determine SDK's property name + server.tool("initial_tool", { a: z.number() }, async ({ a }) => { + return { content: [{ type: "text", text: String(a) }] }; + }); + const expectedPropName = getToolFunctionPropertyName( + (server as any)._registeredTools["initial_tool"], + ); + + // Call track() first + track(server, "test-project-id"); + + // Then register a tool after track() + server.tool("late_tool", { b: z.string() }, async ({ b }) => { + return { content: [{ type: "text", text: b }] }; + }); + + const tools = (server as any)._registeredTools; + const tool = tools["late_tool"]; + const propName = getToolFunctionPropertyName(tool); + + console.log("\n=== Tool registered after track() ==="); + console.log("Expected property name:", expectedPropName); + console.log("Actual property name:", propName); + console.log("Has 'callback':", "callback" in tool); + console.log("Has 'handler':", "handler" in tool); + + // Tools registered after track() should also preserve the SDK's property name + expect(propName).toBe(expectedPropName); + expect(typeof tool[propName]).toBe("function"); + }); +}); diff --git a/src/types.ts b/src/types.ts index 5ffa393..b960a98 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,12 +22,15 @@ export type ToolCallback = extra: CompatibleRequestHandlerExtra, ) => CallToolResult | Promise); +// RegisteredTool type that supports both MCP SDK 1.23- (callback) and 1.24+ (handler) export type RegisteredTool = { description?: string; inputSchema?: any; - callback: ToolCallback; update?: (...args: any[]) => any; -}; +} & ( + | { callback: ToolCallback; handler?: never } + | { handler: ToolCallback; callback?: never } +); export type RedactFunction = (text: string) => Promise;