Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test capabilities, use explicit specifiers for now #52

Merged
46 changes: 34 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ import { StdioServerTransport } from "./server/stdio.js";
import { ListResourcesResultSchema } from "./types.js";

async function runClient(url_or_command: string, args: string[]) {
const client = new Client({
name: "mcp-typescript test client",
version: "0.1.0",
});
const client = new Client(
{
name: "mcp-typescript test client",
version: "0.1.0",
},
{
capabilities: {
sampling: {},
},
},
);

let clientTransport;

Expand Down Expand Up @@ -63,10 +70,15 @@ async function runServer(port: number | null) {
console.log("Got new SSE connection");

const transport = new SSEServerTransport("/message", res);
const server = new Server({
name: "mcp-typescript test server",
version: "0.1.0",
});
const server = new Server(
{
name: "mcp-typescript test server",
version: "0.1.0",
},
{
capabilities: {},
},
);

servers.push(server);

Expand Down Expand Up @@ -97,10 +109,20 @@ async function runServer(port: number | null) {
console.log(`Server running on http://localhost:${port}/sse`);
});
} else {
const server = new Server({
name: "mcp-typescript test server",
version: "0.1.0",
});
const server = new Server(
{
name: "mcp-typescript test server",
version: "0.1.0",
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
},
);

const transport = new StdioServerTransport();
await server.connect(transport);
Expand Down
271 changes: 255 additions & 16 deletions src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import {
ResultSchema,
LATEST_PROTOCOL_VERSION,
SUPPORTED_PROTOCOL_VERSIONS,
InitializeRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
CreateMessageRequestSchema,
ListRootsRequestSchema,
} from "../types.js";
import { Transport } from "../shared/transport.js";
import { Server } from "../server/index.js";
import { InMemoryTransport } from "../inMemory.js";

test("should initialize with matching protocol version", async () => {
const clientTransport: Transport = {
Expand All @@ -35,10 +42,17 @@ test("should initialize with matching protocol version", async () => {
}),
};

const client = new Client({
name: "test client",
version: "1.0",
});
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {
sampling: {},
},
},
);

await client.connect(clientTransport);

Expand Down Expand Up @@ -77,10 +91,17 @@ test("should initialize with supported older protocol version", async () => {
}),
};

const client = new Client({
name: "test client",
version: "1.0",
});
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {
sampling: {},
},
},
);

await client.connect(clientTransport);

Expand Down Expand Up @@ -114,10 +135,17 @@ test("should reject unsupported protocol version", async () => {
}),
};

const client = new Client({
name: "test client",
version: "1.0",
});
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",
Expand All @@ -126,6 +154,210 @@ test("should reject unsupported protocol version", async () => {
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.
*/
Expand Down Expand Up @@ -171,10 +403,17 @@ test("should typecheck", () => {
WeatherRequest,
WeatherNotification,
WeatherResult
>({
name: "WeatherClient",
version: "1.0.0",
});
>(
{
name: "WeatherClient",
version: "1.0.0",
},
{
capabilities: {
sampling: {},
},
},
);

// Typecheck that only valid weather requests/notifications/results are allowed
false &&
Expand Down
Loading