Skip to content

Commit 8023136

Browse files
committed
add tests for internal stdio transport
1 parent be8bce9 commit 8023136

File tree

4 files changed

+1379
-4
lines changed

4 files changed

+1379
-4
lines changed

packages/mcp/bin.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Integration tests for the stdio MCP server in bin.ts
3+
*
4+
* These tests spawn the bin.ts process as a child process and communicate
5+
* with it via stdin/stdout, simulating how an MCP client would interact
6+
* with the server in production.
7+
*/
8+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
9+
import { x } from 'tinyexec';
10+
import { resolve, dirname } from 'node:path';
11+
import { fileURLToPath } from 'node:url';
12+
import type { ChildProcess } from 'node:child_process';
13+
14+
/**
15+
* Helper to send a JSON-RPC request and wait for the response
16+
*/
17+
async function sendRequest(
18+
child: ChildProcess,
19+
stdoutData: string[],
20+
request: unknown,
21+
requestId: number,
22+
timeoutMs = 10_000,
23+
): Promise<unknown> {
24+
// Send request
25+
child.stdin?.write(JSON.stringify(request) + '\n');
26+
27+
// Wait for response with timeout
28+
const { promise, resolve, reject } = Promise.withResolvers<void>();
29+
const timeout = setTimeout(() => {
30+
reject(new Error(`Timeout waiting for response to request ${requestId}`));
31+
}, timeoutMs);
32+
33+
const checkResponse = () => {
34+
const allData = stdoutData.join('');
35+
if (allData.includes(`"id":${requestId}`)) {
36+
clearTimeout(timeout);
37+
resolve();
38+
} else {
39+
setTimeout(checkResponse, 50);
40+
}
41+
};
42+
checkResponse();
43+
44+
await promise;
45+
46+
// Parse and return the response
47+
const allData = stdoutData.join('');
48+
const lines = allData.split('\n').filter((line) => line.trim());
49+
const responseLine = lines.find((line) => {
50+
try {
51+
const parsed = JSON.parse(line);
52+
return parsed.id === requestId;
53+
} catch {
54+
return false;
55+
}
56+
});
57+
58+
if (!responseLine) {
59+
throw new Error(`No response found for request ${requestId}`);
60+
}
61+
62+
return JSON.parse(responseLine);
63+
}
64+
65+
describe('bin.ts stdio MCP server', () => {
66+
let child: ChildProcess;
67+
let stdoutData: string[] = [];
68+
let stderrData: string[] = [];
69+
70+
beforeAll(() => {
71+
const currentDir = dirname(fileURLToPath(import.meta.url));
72+
const binPath = resolve(currentDir, './bin.ts');
73+
const fixturePath = resolve(
74+
currentDir,
75+
'./fixtures/full-manifest.fixture.json',
76+
);
77+
78+
const proc = x('node', [binPath, '--manifestPath', fixturePath]);
79+
80+
child = proc.process as ChildProcess;
81+
82+
// Collect stdout for later assertions
83+
child.stdout?.on('data', (chunk) => {
84+
stdoutData.push(chunk.toString());
85+
});
86+
87+
// Collect stderr for debugging
88+
child.stderr?.on('data', (chunk) => {
89+
stderrData.push(chunk.toString());
90+
});
91+
92+
child.on('error', (err) => {
93+
console.error('Process error:', err);
94+
});
95+
});
96+
97+
afterAll(() => {
98+
child.kill();
99+
});
100+
101+
it('should respond to initialize request', async () => {
102+
const request = {
103+
jsonrpc: '2.0',
104+
id: 1,
105+
method: 'initialize',
106+
params: {
107+
protocolVersion: '2024-11-05',
108+
capabilities: {},
109+
clientInfo: {
110+
name: 'test-client',
111+
version: '1.0.0',
112+
},
113+
},
114+
};
115+
116+
const response = await sendRequest(child, stdoutData, request, 1);
117+
118+
expect(response).toMatchObject({
119+
jsonrpc: '2.0',
120+
id: 1,
121+
result: {
122+
protocolVersion: '2024-11-05',
123+
capabilities: {
124+
tools: {
125+
listChanged: true,
126+
},
127+
},
128+
serverInfo: {
129+
name: '@storybook/mcp',
130+
},
131+
},
132+
});
133+
}, 15000);
134+
135+
it('should list available tools', async () => {
136+
const request = {
137+
jsonrpc: '2.0',
138+
id: 2,
139+
method: 'tools/list',
140+
params: {},
141+
};
142+
143+
const response = await sendRequest(child, stdoutData, request, 2);
144+
145+
expect(response).toMatchObject({
146+
jsonrpc: '2.0',
147+
id: 2,
148+
result: {
149+
tools: expect.arrayContaining([
150+
expect.objectContaining({
151+
name: 'list-all-components',
152+
}),
153+
expect.objectContaining({
154+
name: 'get-component-documentation',
155+
}),
156+
]),
157+
},
158+
});
159+
}, 15000);
160+
161+
it('should execute list-all-components tool', async () => {
162+
const request = {
163+
jsonrpc: '2.0',
164+
id: 3,
165+
method: 'tools/call',
166+
params: {
167+
name: 'list-all-components',
168+
arguments: {},
169+
},
170+
};
171+
172+
const response = await sendRequest(child, stdoutData, request, 3);
173+
174+
expect(response).toMatchObject({
175+
jsonrpc: '2.0',
176+
id: 3,
177+
result: {
178+
content: [
179+
{
180+
type: 'text',
181+
text: expect.stringContaining('<components>'),
182+
},
183+
],
184+
},
185+
});
186+
}, 15000);
187+
});

packages/mcp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"devDependencies": {
4242
"@tmcp/transport-stdio": "catalog:",
4343
"react-docgen": "^8.0.2",
44-
"srvx": "^0.8.16"
44+
"srvx": "^0.8.16",
45+
"tinyexec": "^1.0.2"
4546
}
4647
}

0 commit comments

Comments
 (0)