Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/sdk/src/__tests__/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,3 +892,83 @@ describe("createProxy — session reuse", () => {
expect(firstSessionId).toMatch(/^ses_/);
});
});

describe("createProxy — Skybridge registerTool(config, cb)", () => {
beforeEach(() => {
_resetGlobalState();
mockedMint.mockResolvedValue(null as never);
});

// Skybridge's McpServer uses `registerTool(config, cb)` (the name lives on
// `config.name`) and returns `this` for chaining — unlike the MCP SDK's
// `registerTool(name, config, cb)`. The proxy must instrument both.
function createSkybridgeMock() {
const mock: Record<string, unknown> = {
stored: null,
tool() {
return mock;
},
registerTool(...args: unknown[]) {
mock.stored = args.find((a) => typeof a === "function");
return mock; // fluent — returns the server itself
},
connect: async () => {},
server: { getClientVersion: () => ({ name: "test-client" }) },
};
return mock;
}

it("derives the tool name from config.name (not 'unknown')", () => {
const mock = createSkybridgeMock();
const transport = createMockTransport();
const proxy = createProxy(mock as never, testConfig, transport, "0.0.1");

proxy.registerTool(
{ name: "compare_prices", description: "Compare prices", inputSchema: { q: {} } } as never,
(() => ({ content: [{ type: "text", text: "ok" }] })) as never,
);

const discovery = transport.sent
.flat()
.find((e) => (e as Record<string, unknown>).event_type === "tool_discovery") as
| Record<string, unknown>
| undefined;
expect(discovery?.tool_name).toBe("compare_prices");
});

it("wraps the handler so invoking it emits a tool_call event", async () => {
const mock = createSkybridgeMock();
const transport = createMockTransport();
const proxy = createProxy(mock as never, testConfig, transport, "0.0.1");

proxy.registerTool(
{ name: "compare_prices", description: "d" } as never,
(async () => ({ content: [{ type: "text", text: "ok" }] })) as never,
);

const handler = mock.stored as (...a: unknown[]) => Promise<unknown>;
expect(typeof handler).toBe("function");
await handler({ q: "x" }, { sessionId: "s1", _meta: {} });

const call = transport.sent
.flat()
.find((e) => (e as Record<string, unknown>).event_type === "tool_call") as
| Record<string, unknown>
| undefined;
expect(call?.event_name).toBe("compare_prices");
});

it("keeps the fluent chain intercepted across registrations", () => {
const mock = createSkybridgeMock();
const transport = createMockTransport();
const proxy = createProxy(mock as never, testConfig, transport, "0.0.1");

const ret = proxy.registerTool(
{ name: "a" } as never,
(() => ({ content: [] })) as never,
);
// Skybridge returns `this`; the proxy must hand back the proxy so the next
// chained registration stays instrumented.
expect(ret).toBe(proxy);
});
});
37 changes: 24 additions & 13 deletions packages/sdk/src/server/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,25 @@ export function createProxy<T extends McpServer>(

if (prop === "registerTool") {
return (...args: unknown[]) => {
// registerTool(name: string, config: object, cb: Function)
// Callback is always the 3rd argument (index 2)
const toolName = typeof args[0] === "string" ? args[0] : "unknown";
if (args.length >= 3 && typeof args[2] === "function") {
const originalCb = args[2] as (...cbArgs: unknown[]) => unknown;
args[2] = wrapToolCallback(
// Two registerTool signatures are supported:
// - MCP SDK: registerTool(name: string, config: object, cb)
// - Skybridge: registerTool(config: object, cb) // name is config.name
// Locate the callback by type rather than a fixed index, and derive
// the tool name from the string arg or the config object's `name`.
const cbIndex = args.findIndex((a) => typeof a === "function");
const configObj = args.find(
(a, i) => i !== cbIndex && a !== null && typeof a === "object",
) as Record<string, unknown> | undefined;
const toolName =
typeof args[0] === "string"
? args[0]
: typeof configObj?.name === "string"
? configObj.name
: "unknown";

if (cbIndex !== -1) {
const originalCb = args[cbIndex] as (...cbArgs: unknown[]) => unknown;
args[cbIndex] = wrapToolCallback(
originalCb,
toolName,
resolveSession,
Expand All @@ -384,14 +397,12 @@ export function createProxy<T extends McpServer>(
emittedToolDiscoveries.add(toolName);
let description: string | undefined;
let inputSchema: Record<string, unknown> | undefined;
const configArg = args[1];
if (configArg && typeof configArg === "object") {
const obj = configArg as Record<string, unknown>;
if (typeof obj.description === "string") {
description = obj.description;
if (configObj) {
if (typeof configObj.description === "string") {
description = configObj.description;
}
if (obj.inputSchema && typeof obj.inputSchema === "object") {
inputSchema = obj.inputSchema as Record<string, unknown>;
if (configObj.inputSchema && typeof configObj.inputSchema === "object") {
inputSchema = configObj.inputSchema as Record<string, unknown>;
}
}
emitDiscovery(toolName, description, inputSchema);
Expand Down
Loading