Skip to content

Commit 40f16c2

Browse files
authored
Merge pull request #2 from modelcontextprotocol/pcarleton/tools_call_test
Add conformance test for tools/call
2 parents 3d7343a + 8bce5d1 commit 40f16c2

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env node
2+
3+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5+
6+
async function main(): Promise<void> {
7+
const serverUrl = process.argv[2];
8+
9+
if (!serverUrl) {
10+
console.error('Usage: test-client <server-url>');
11+
process.exit(1);
12+
}
13+
14+
console.log(`Connecting to MCP server at: ${serverUrl}`);
15+
16+
try {
17+
const client = new Client(
18+
{
19+
name: 'test-client',
20+
version: '1.0.0'
21+
},
22+
{
23+
capabilities: {}
24+
}
25+
);
26+
27+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
28+
29+
await client.connect(transport);
30+
console.log('✅ Successfully connected to MCP server');
31+
32+
await client.listTools();
33+
console.log('✅ Successfully listed tools');
34+
35+
await client.callTool({ name: 'add_numbers', arguments: { a: 5, b: 10 } });
36+
console.log('✅ Successfully called add_numbers tool');
37+
38+
await transport.close();
39+
console.log('✅ Connection closed successfully');
40+
41+
process.exit(0);
42+
} catch (error) {
43+
console.error('❌ Failed to connect to MCP server:', error);
44+
process.exit(1);
45+
}
46+
}
47+
48+
main().catch(error => {
49+
console.error('Unhandled error:', error);
50+
process.exit(1);
51+
});

src/scenarios/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Scenario } from '../types.js';
22
import { InitializeScenario } from './initialize.js';
3+
import { ToolsCallScenario } from './tools_call.js';
34

4-
export const scenarios = new Map<string, Scenario>([['initialize', new InitializeScenario()]]);
5+
export const scenarios = new Map<string, Scenario>([
6+
['initialize', new InitializeScenario()],
7+
['tools-call', new ToolsCallScenario()]
8+
]);
59

610
export function registerScenario(name: string, scenario: Scenario): void {
711
scenarios.set(name, scenario);

src/scenarios/tools_call.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4+
import type { Scenario, ConformanceCheck } from './types.js';
5+
import express from 'express';
6+
import { ScenarioUrls } from '../types.js';
7+
8+
function createServer(checks: ConformanceCheck[]): express.Application {
9+
const server = new Server(
10+
{
11+
name: 'add-numbers-server',
12+
version: '1.0.0'
13+
},
14+
{
15+
capabilities: {
16+
tools: {}
17+
}
18+
}
19+
);
20+
21+
server.setRequestHandler(ListToolsRequestSchema, async () => {
22+
return {
23+
tools: [
24+
{
25+
name: 'add_numbers',
26+
description: 'Add two numbers together',
27+
inputSchema: {
28+
type: 'object',
29+
properties: {
30+
a: {
31+
type: 'number',
32+
description: 'First number'
33+
},
34+
b: {
35+
type: 'number',
36+
description: 'Second number'
37+
}
38+
},
39+
required: ['a', 'b']
40+
}
41+
}
42+
]
43+
};
44+
});
45+
46+
server.setRequestHandler(CallToolRequestSchema, async request => {
47+
if (request.params.name === 'add_numbers') {
48+
const { a, b } = request.params.arguments as { a: number; b: number };
49+
const result = a + b;
50+
51+
checks.push({
52+
id: 'tool-add-numbers',
53+
name: 'ToolAddNumbers',
54+
description: 'Validates that the add_numbers tool works correctly',
55+
status: 'SUCCESS',
56+
timestamp: new Date().toISOString(),
57+
specReferences: [
58+
{
59+
id: 'MCP-Tools',
60+
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools'
61+
}
62+
],
63+
details: {
64+
a,
65+
b,
66+
result
67+
}
68+
});
69+
70+
return {
71+
content: [
72+
{
73+
type: 'text',
74+
text: `The sum of ${a} and ${b} is ${result}`
75+
}
76+
]
77+
};
78+
}
79+
80+
throw new Error(`Unknown tool: ${request.params.name}`);
81+
});
82+
83+
const app = express();
84+
app.use(express.json());
85+
86+
app.use((req, res, next) => {
87+
// Log incoming requests for debugging
88+
// console.log(`Incoming request: ${req.method} ${req.url}`);
89+
checks.push({
90+
id: 'incoming-request',
91+
name: 'IncomingRequest',
92+
description: `Received ${req.method} request for ${req.url}`,
93+
status: 'INFO',
94+
timestamp: new Date().toISOString(),
95+
details: {
96+
body: JSON.stringify(req.body)
97+
}
98+
});
99+
next();
100+
checks.push({
101+
id: 'outgoing-response',
102+
name: 'OutgoingResponse',
103+
// TODO: include MCP method?
104+
description: `Sent ${res.statusCode} response`,
105+
status: 'INFO',
106+
timestamp: new Date().toISOString(),
107+
details: {
108+
// TODO: this isn't working
109+
body: JSON.stringify(res.body)
110+
}
111+
});
112+
});
113+
114+
app.post('/mcp', async (req, res) => {
115+
const transport = new StreamableHTTPServerTransport({
116+
sessionIdGenerator: undefined
117+
});
118+
await server.connect(transport);
119+
120+
await transport.handleRequest(req, res, req.body);
121+
});
122+
123+
return app;
124+
}
125+
126+
export class ToolsCallScenario implements Scenario {
127+
name = 'tools_call';
128+
private app: express.Application | null = null;
129+
private httpServer: any = null;
130+
private checks: ConformanceCheck[] = [];
131+
132+
async start(): Promise<ScenarioUrls> {
133+
this.checks = [];
134+
this.app = createServer(this.checks);
135+
this.httpServer = this.app.listen(0);
136+
const port = this.httpServer.address().port;
137+
return { serverUrl: `http://localhost:${port}/mcp` };
138+
}
139+
140+
async stop() {
141+
if (this.httpServer) {
142+
await new Promise(resolve => this.httpServer.close(resolve));
143+
this.httpServer = null;
144+
}
145+
this.app = null;
146+
}
147+
148+
getChecks(): ConformanceCheck[] {
149+
const expectedSlugs = ['tool-add-numbers'];
150+
// add a failure if not in there already
151+
for (const slug of expectedSlugs) {
152+
if (!this.checks.find(c => c.id === slug)) {
153+
// TODO: this is duplicated from above, refactor
154+
this.checks.push({
155+
id: slug,
156+
name: `ToolAddNumbers`,
157+
description: `Validates that the add_numbers tool works correctly`,
158+
status: 'FAILURE',
159+
timestamp: new Date().toISOString(),
160+
details: { message: 'Tool was not called by client' },
161+
specReferences: [
162+
{
163+
id: 'MCP-Tools',
164+
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools'
165+
}
166+
]
167+
});
168+
}
169+
}
170+
return this.checks;
171+
}
172+
}

0 commit comments

Comments
 (0)