357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
/* 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, } 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");
|
|
});
|
|
//# sourceMappingURL=index.test.js.map
|