355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import { createServer, type Server } from 'node:http';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { Client } from '../../src/client/index.js';
|
|
import { StreamableHTTPClientTransport } from '../../src/client/streamableHttp.js';
|
|
import { McpServer } from '../../src/server/mcp.js';
|
|
import { StreamableHTTPServerTransport } from '../../src/server/streamableHttp.js';
|
|
import {
|
|
CallToolResultSchema,
|
|
ListToolsResultSchema,
|
|
ListResourcesResultSchema,
|
|
ListPromptsResultSchema,
|
|
LATEST_PROTOCOL_VERSION
|
|
} from '../../src/types.js';
|
|
import { zodTestMatrix, type ZodMatrixEntry } from '../../src/__fixtures__/zodTestMatrix.js';
|
|
import { listenOnRandomPort } from '../helpers/http.js';
|
|
|
|
describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
|
|
const { z } = entry;
|
|
describe('Streamable HTTP Transport Session Management', () => {
|
|
// Function to set up the server with optional session management
|
|
async function setupServer(withSessionManagement: boolean) {
|
|
const server: Server = createServer();
|
|
const mcpServer = new McpServer(
|
|
{ name: 'test-server', version: '1.0.0' },
|
|
{
|
|
capabilities: {
|
|
logging: {},
|
|
tools: {},
|
|
resources: {},
|
|
prompts: {}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Add a simple resource
|
|
mcpServer.resource('test-resource', '/test', { description: 'A test resource' }, async () => ({
|
|
contents: [
|
|
{
|
|
uri: '/test',
|
|
text: 'This is a test resource content'
|
|
}
|
|
]
|
|
}));
|
|
|
|
mcpServer.prompt('test-prompt', 'A test prompt', async () => ({
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: {
|
|
type: 'text',
|
|
text: 'This is a test prompt'
|
|
}
|
|
}
|
|
]
|
|
}));
|
|
|
|
mcpServer.tool(
|
|
'greet',
|
|
'A simple greeting tool',
|
|
{
|
|
name: z.string().describe('Name to greet').default('World')
|
|
},
|
|
async ({ name }) => {
|
|
return {
|
|
content: [{ type: 'text', text: `Hello, ${name}!` }]
|
|
};
|
|
}
|
|
);
|
|
|
|
// Create transport with or without session management
|
|
const serverTransport = new StreamableHTTPServerTransport({
|
|
sessionIdGenerator: withSessionManagement
|
|
? () => randomUUID() // With session management, generate UUID
|
|
: undefined // Without session management, return undefined
|
|
});
|
|
|
|
await mcpServer.connect(serverTransport);
|
|
|
|
server.on('request', async (req, res) => {
|
|
await serverTransport.handleRequest(req, res);
|
|
});
|
|
|
|
// Start the server on a random port
|
|
const baseUrl = await listenOnRandomPort(server);
|
|
|
|
return { server, mcpServer, serverTransport, baseUrl };
|
|
}
|
|
|
|
describe('Stateless Mode', () => {
|
|
let server: Server;
|
|
let mcpServer: McpServer;
|
|
let serverTransport: StreamableHTTPServerTransport;
|
|
let baseUrl: URL;
|
|
|
|
beforeEach(async () => {
|
|
const setup = await setupServer(false);
|
|
server = setup.server;
|
|
mcpServer = setup.mcpServer;
|
|
serverTransport = setup.serverTransport;
|
|
baseUrl = setup.baseUrl;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up resources
|
|
await mcpServer.close().catch(() => {});
|
|
await serverTransport.close().catch(() => {});
|
|
server.close();
|
|
});
|
|
|
|
it('should support multiple client connections', async () => {
|
|
// Create and connect a client
|
|
const client1 = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport1 = new StreamableHTTPClientTransport(baseUrl);
|
|
await client1.connect(transport1);
|
|
|
|
// Verify that no session ID was set
|
|
expect(transport1.sessionId).toBeUndefined();
|
|
|
|
// List available tools
|
|
await client1.request(
|
|
{
|
|
method: 'tools/list',
|
|
params: {}
|
|
},
|
|
ListToolsResultSchema
|
|
);
|
|
|
|
const client2 = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport2 = new StreamableHTTPClientTransport(baseUrl);
|
|
await client2.connect(transport2);
|
|
|
|
// Verify that no session ID was set
|
|
expect(transport2.sessionId).toBeUndefined();
|
|
|
|
// List available tools
|
|
await client2.request(
|
|
{
|
|
method: 'tools/list',
|
|
params: {}
|
|
},
|
|
ListToolsResultSchema
|
|
);
|
|
});
|
|
it('should operate without session management', async () => {
|
|
// Create and connect a client
|
|
const client = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport = new StreamableHTTPClientTransport(baseUrl);
|
|
await client.connect(transport);
|
|
|
|
// Verify that no session ID was set
|
|
expect(transport.sessionId).toBeUndefined();
|
|
|
|
// List available tools
|
|
const toolsResult = await client.request(
|
|
{
|
|
method: 'tools/list',
|
|
params: {}
|
|
},
|
|
ListToolsResultSchema
|
|
);
|
|
|
|
// Verify tools are accessible
|
|
expect(toolsResult.tools).toContainEqual(
|
|
expect.objectContaining({
|
|
name: 'greet'
|
|
})
|
|
);
|
|
|
|
// List available resources
|
|
const resourcesResult = await client.request(
|
|
{
|
|
method: 'resources/list',
|
|
params: {}
|
|
},
|
|
ListResourcesResultSchema
|
|
);
|
|
|
|
// Verify resources result structure
|
|
expect(resourcesResult).toHaveProperty('resources');
|
|
|
|
// List available prompts
|
|
const promptsResult = await client.request(
|
|
{
|
|
method: 'prompts/list',
|
|
params: {}
|
|
},
|
|
ListPromptsResultSchema
|
|
);
|
|
|
|
// Verify prompts result structure
|
|
expect(promptsResult).toHaveProperty('prompts');
|
|
expect(promptsResult.prompts).toContainEqual(
|
|
expect.objectContaining({
|
|
name: 'test-prompt'
|
|
})
|
|
);
|
|
|
|
// Call the greeting tool
|
|
const greetingResult = await client.request(
|
|
{
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'greet',
|
|
arguments: {
|
|
name: 'Stateless Transport'
|
|
}
|
|
}
|
|
},
|
|
CallToolResultSchema
|
|
);
|
|
|
|
// Verify tool result
|
|
expect(greetingResult.content).toEqual([{ type: 'text', text: 'Hello, Stateless Transport!' }]);
|
|
|
|
// Clean up
|
|
await transport.close();
|
|
});
|
|
|
|
it('should set protocol version after connecting', async () => {
|
|
// Create and connect a client
|
|
const client = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport = new StreamableHTTPClientTransport(baseUrl);
|
|
|
|
// Verify protocol version is not set before connecting
|
|
expect(transport.protocolVersion).toBeUndefined();
|
|
|
|
await client.connect(transport);
|
|
|
|
// Verify protocol version is set after connecting
|
|
expect(transport.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
|
|
|
|
// Clean up
|
|
await transport.close();
|
|
});
|
|
});
|
|
|
|
describe('Stateful Mode', () => {
|
|
let server: Server;
|
|
let mcpServer: McpServer;
|
|
let serverTransport: StreamableHTTPServerTransport;
|
|
let baseUrl: URL;
|
|
|
|
beforeEach(async () => {
|
|
const setup = await setupServer(true);
|
|
server = setup.server;
|
|
mcpServer = setup.mcpServer;
|
|
serverTransport = setup.serverTransport;
|
|
baseUrl = setup.baseUrl;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up resources
|
|
await mcpServer.close().catch(() => {});
|
|
await serverTransport.close().catch(() => {});
|
|
server.close();
|
|
});
|
|
|
|
it('should operate with session management', async () => {
|
|
// Create and connect a client
|
|
const client = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport = new StreamableHTTPClientTransport(baseUrl);
|
|
await client.connect(transport);
|
|
|
|
// Verify that a session ID was set
|
|
expect(transport.sessionId).toBeDefined();
|
|
expect(typeof transport.sessionId).toBe('string');
|
|
|
|
// List available tools
|
|
const toolsResult = await client.request(
|
|
{
|
|
method: 'tools/list',
|
|
params: {}
|
|
},
|
|
ListToolsResultSchema
|
|
);
|
|
|
|
// Verify tools are accessible
|
|
expect(toolsResult.tools).toContainEqual(
|
|
expect.objectContaining({
|
|
name: 'greet'
|
|
})
|
|
);
|
|
|
|
// List available resources
|
|
const resourcesResult = await client.request(
|
|
{
|
|
method: 'resources/list',
|
|
params: {}
|
|
},
|
|
ListResourcesResultSchema
|
|
);
|
|
|
|
// Verify resources result structure
|
|
expect(resourcesResult).toHaveProperty('resources');
|
|
|
|
// List available prompts
|
|
const promptsResult = await client.request(
|
|
{
|
|
method: 'prompts/list',
|
|
params: {}
|
|
},
|
|
ListPromptsResultSchema
|
|
);
|
|
|
|
// Verify prompts result structure
|
|
expect(promptsResult).toHaveProperty('prompts');
|
|
expect(promptsResult.prompts).toContainEqual(
|
|
expect.objectContaining({
|
|
name: 'test-prompt'
|
|
})
|
|
);
|
|
|
|
// Call the greeting tool
|
|
const greetingResult = await client.request(
|
|
{
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'greet',
|
|
arguments: {
|
|
name: 'Stateful Transport'
|
|
}
|
|
}
|
|
},
|
|
CallToolResultSchema
|
|
);
|
|
|
|
// Verify tool result
|
|
expect(greetingResult.content).toEqual([{ type: 'text', text: 'Hello, Stateful Transport!' }]);
|
|
|
|
// Clean up
|
|
await transport.close();
|
|
});
|
|
});
|
|
});
|
|
});
|