index.test.js•13.6 kB
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Client } from "./index.js";
import { z } from "zod";
import { RequestSchema, NotificationSchema, ResultSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, InitializeRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, CreateMessageRequestSchema, ListRootsRequestSchema, ErrorCode, } from "../types.js";
import { Server } from "../server/index.js";
import { InMemoryTransport } from "../inMemory.js";
test("should initialize with matching protocol version", async () => {
    const clientTransport = {
        start: jest.fn().mockResolvedValue(undefined),
        close: jest.fn().mockResolvedValue(undefined),
        send: jest.fn().mockImplementation((message) => {
            var _a;
            if (message.method === "initialize") {
                (_a = clientTransport.onmessage) === null || _a === void 0 ? void 0 : _a.call(clientTransport, {
                    jsonrpc: "2.0",
                    id: message.id,
                    result: {
                        protocolVersion: LATEST_PROTOCOL_VERSION,
                        capabilities: {},
                        serverInfo: {
                            name: "test",
                            version: "1.0",
                        },
                    },
                });
            }
            return Promise.resolve();
        }),
    };
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            sampling: {},
        },
    });
    await client.connect(clientTransport);
    // Should have sent initialize with latest version
    expect(clientTransport.send).toHaveBeenCalledWith(expect.objectContaining({
        method: "initialize",
        params: expect.objectContaining({
            protocolVersion: LATEST_PROTOCOL_VERSION,
        }),
    }));
});
test("should initialize with supported older protocol version", async () => {
    const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
    const clientTransport = {
        start: jest.fn().mockResolvedValue(undefined),
        close: jest.fn().mockResolvedValue(undefined),
        send: jest.fn().mockImplementation((message) => {
            var _a;
            if (message.method === "initialize") {
                (_a = clientTransport.onmessage) === null || _a === void 0 ? void 0 : _a.call(clientTransport, {
                    jsonrpc: "2.0",
                    id: message.id,
                    result: {
                        protocolVersion: OLD_VERSION,
                        capabilities: {},
                        serverInfo: {
                            name: "test",
                            version: "1.0",
                        },
                    },
                });
            }
            return Promise.resolve();
        }),
    };
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            sampling: {},
        },
    });
    await client.connect(clientTransport);
    // Connection should succeed with the older version
    expect(client.getServerVersion()).toEqual({
        name: "test",
        version: "1.0",
    });
});
test("should reject unsupported protocol version", async () => {
    const clientTransport = {
        start: jest.fn().mockResolvedValue(undefined),
        close: jest.fn().mockResolvedValue(undefined),
        send: jest.fn().mockImplementation((message) => {
            var _a;
            if (message.method === "initialize") {
                (_a = clientTransport.onmessage) === null || _a === void 0 ? void 0 : _a.call(clientTransport, {
                    jsonrpc: "2.0",
                    id: message.id,
                    result: {
                        protocolVersion: "invalid-version",
                        capabilities: {},
                        serverInfo: {
                            name: "test",
                            version: "1.0",
                        },
                    },
                });
            }
            return Promise.resolve();
        }),
    };
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            sampling: {},
        },
    });
    await expect(client.connect(clientTransport)).rejects.toThrow("Server's protocol version is not supported: invalid-version");
    expect(clientTransport.close).toHaveBeenCalled();
});
test("should respect server capabilities", async () => {
    const server = new Server({
        name: "test server",
        version: "1.0",
    }, {
        capabilities: {
            resources: {},
            tools: {},
        },
    });
    server.setRequestHandler(InitializeRequestSchema, (_request) => ({
        protocolVersion: LATEST_PROTOCOL_VERSION,
        capabilities: {
            resources: {},
            tools: {},
        },
        serverInfo: {
            name: "test",
            version: "1.0",
        },
    }));
    server.setRequestHandler(ListResourcesRequestSchema, () => ({
        resources: [],
    }));
    server.setRequestHandler(ListToolsRequestSchema, () => ({
        tools: [],
    }));
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            sampling: {},
        },
        enforceStrictCapabilities: true,
    });
    await Promise.all([
        client.connect(clientTransport),
        server.connect(serverTransport),
    ]);
    // Server supports resources and tools, but not prompts
    expect(client.getServerCapabilities()).toEqual({
        resources: {},
        tools: {},
    });
    // These should work
    await expect(client.listResources()).resolves.not.toThrow();
    await expect(client.listTools()).resolves.not.toThrow();
    // This should throw because prompts are not supported
    await expect(client.listPrompts()).rejects.toThrow("Server does not support prompts");
});
test("should respect client notification capabilities", async () => {
    const server = new Server({
        name: "test server",
        version: "1.0",
    }, {
        capabilities: {},
    });
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            roots: {
                listChanged: true,
            },
        },
    });
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    await Promise.all([
        client.connect(clientTransport),
        server.connect(serverTransport),
    ]);
    // This should work because the client has the roots.listChanged capability
    await expect(client.sendRootsListChanged()).resolves.not.toThrow();
    // Create a new client without the roots.listChanged capability
    const clientWithoutCapability = new Client({
        name: "test client without capability",
        version: "1.0",
    }, {
        capabilities: {},
        enforceStrictCapabilities: true,
    });
    await clientWithoutCapability.connect(clientTransport);
    // This should throw because the client doesn't have the roots.listChanged capability
    await expect(clientWithoutCapability.sendRootsListChanged()).rejects.toThrow(/^Client does not support/);
});
test("should respect server notification capabilities", async () => {
    const server = new Server({
        name: "test server",
        version: "1.0",
    }, {
        capabilities: {
            logging: {},
            resources: {
                listChanged: true,
            },
        },
    });
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {},
    });
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    await Promise.all([
        client.connect(clientTransport),
        server.connect(serverTransport),
    ]);
    // These should work because the server has the corresponding capabilities
    await expect(server.sendLoggingMessage({ level: "info", data: "Test" })).resolves.not.toThrow();
    await expect(server.sendResourceListChanged()).resolves.not.toThrow();
    // This should throw because the server doesn't have the tools capability
    await expect(server.sendToolListChanged()).rejects.toThrow("Server does not support notifying of tool list changes");
});
test("should only allow setRequestHandler for declared capabilities", () => {
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {
            sampling: {},
        },
    });
    // This should work because sampling is a declared capability
    expect(() => {
        client.setRequestHandler(CreateMessageRequestSchema, () => ({
            model: "test-model",
            role: "assistant",
            content: {
                type: "text",
                text: "Test response",
            },
        }));
    }).not.toThrow();
    // This should throw because roots listing is not a declared capability
    expect(() => {
        client.setRequestHandler(ListRootsRequestSchema, () => ({}));
    }).toThrow("Client does not support roots capability");
});
/*
  Test that custom request/notification/result schemas can be used with the Client class.
  */
test("should typecheck", () => {
    const GetWeatherRequestSchema = RequestSchema.extend({
        method: z.literal("weather/get"),
        params: z.object({
            city: z.string(),
        }),
    });
    const GetForecastRequestSchema = RequestSchema.extend({
        method: z.literal("weather/forecast"),
        params: z.object({
            city: z.string(),
            days: z.number(),
        }),
    });
    const WeatherForecastNotificationSchema = NotificationSchema.extend({
        method: z.literal("weather/alert"),
        params: z.object({
            severity: z.enum(["warning", "watch"]),
            message: z.string(),
        }),
    });
    const WeatherRequestSchema = GetWeatherRequestSchema.or(GetForecastRequestSchema);
    const WeatherNotificationSchema = WeatherForecastNotificationSchema;
    const WeatherResultSchema = ResultSchema.extend({
        temperature: z.number(),
        conditions: z.string(),
    });
    // Create a typed Client for weather data
    const weatherClient = new Client({
        name: "WeatherClient",
        version: "1.0.0",
    }, {
        capabilities: {
            sampling: {},
        },
    });
    // Typecheck that only valid weather requests/notifications/results are allowed
    false &&
        weatherClient.request({
            method: "weather/get",
            params: {
                city: "Seattle",
            },
        }, WeatherResultSchema);
    false &&
        weatherClient.notification({
            method: "weather/alert",
            params: {
                severity: "warning",
                message: "Storm approaching",
            },
        });
});
test("should handle client cancelling a request", async () => {
    const server = new Server({
        name: "test server",
        version: "1.0",
    }, {
        capabilities: {
            resources: {},
        },
    });
    // Set up server to delay responding to listResources
    server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return {
            resources: [],
        };
    });
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {},
    });
    await Promise.all([
        client.connect(clientTransport),
        server.connect(serverTransport),
    ]);
    // Set up abort controller
    const controller = new AbortController();
    // Issue request but cancel it immediately
    const listResourcesPromise = client.listResources(undefined, {
        signal: controller.signal,
    });
    controller.abort("Cancelled by test");
    // Request should be rejected
    await expect(listResourcesPromise).rejects.toBe("Cancelled by test");
});
test("should handle request timeout", async () => {
    const server = new Server({
        name: "test server",
        version: "1.0",
    }, {
        capabilities: {
            resources: {},
        },
    });
    // Set up server with a delayed response
    server.setRequestHandler(ListResourcesRequestSchema, async (_request, extra) => {
        const timer = new Promise((resolve) => {
            const timeout = setTimeout(resolve, 100);
            extra.signal.addEventListener("abort", () => clearTimeout(timeout));
        });
        await timer;
        return {
            resources: [],
        };
    });
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    const client = new Client({
        name: "test client",
        version: "1.0",
    }, {
        capabilities: {},
    });
    await Promise.all([
        client.connect(clientTransport),
        server.connect(serverTransport),
    ]);
    // Request with 0 msec timeout should fail immediately
    await expect(client.listResources(undefined, { timeout: 0 })).rejects.toMatchObject({
        code: ErrorCode.RequestTimeout,
    });
});
//# sourceMappingURL=index.test.js.map