Skip to content
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