Skip to content

Commit f9819a5

Browse files
feat: add integration tests with real MCP servers
- Add stdio and HTTP test fixture servers - Integration tests for stdio transport probing - Integration tests for streamable-http transport probing - Tests for normalization end-to-end - Tests for custom messages - Tests for environment variable passing - Use dynamic port allocation (port 0) to avoid conflicts - Add proper process cleanup with SIGTERM/SIGKILL - Add forceExit to Jest config for clean test exit - Use client.close() for proper connection cleanup
1 parent db4ac88 commit f9819a5

13 files changed

Lines changed: 622 additions & 16 deletions

File tree

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ inputs:
8989
description: 'Git ref to compare against (auto-detects merge-base or previous tag if not set)'
9090
required: false
9191
default: ''
92+
fail_on_error:
93+
description: 'Fail the action if probe errors occur (not just API differences)'
94+
required: false
95+
default: 'true'
9296
env_vars:
9397
description: 'Environment variables (newline-separated KEY=VALUE pairs)'
9498
required: false
@@ -156,6 +160,7 @@ runs:
156160
INPUT_START_COMMAND: ${{ inputs.start_command }}
157161
INPUT_SERVER_TIMEOUT: ${{ inputs.server_timeout }}
158162
INPUT_COMPARE_REF: ${{ inputs.compare_ref }}
163+
INPUT_FAIL_ON_ERROR: ${{ inputs.fail_on_error }}
159164
INPUT_TRANSPORT: ${{ inputs.transport }}
160165
INPUT_SERVER_URL: ${{ inputs.server_url }}
161166
INPUT_CONFIGURATIONS: ${{ inputs.configurations }}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Minimal MCP Server for integration testing (streamable-http transport)
4+
*
5+
* Run with: npx tsx http-server.ts <port>
6+
*/
7+
export {};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Minimal MCP Server for integration testing (stdio transport)
4+
*
5+
* This server exposes tools, prompts, and resources for testing the probe functionality.
6+
*/
7+
export {};

dist/index.js

Lines changed: 16 additions & 9 deletions
Large diffs are not rendered by default.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface ActionInputs {
4242
configurations: TestConfiguration[];
4343
customMessages: CustomMessage[];
4444
compareRef: string;
45+
failOnError: boolean;
4546
envVars: string;
4647
serverTimeout: number;
4748
}

jest.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export default {
33
preset: "ts-jest/presets/default-esm",
44
testEnvironment: "node",
55
extensionsToTreatAsEsm: [".ts"],
6+
// Force exit after tests complete - needed because npx tsx leaves handles open
7+
forceExit: true,
68
moduleNameMapper: {
79
"^(\\.{1,2}/.*)\\.js$": "$1",
810
},
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Minimal MCP Server for integration testing (streamable-http transport)
4+
*
5+
* Run with: npx tsx http-server.ts <port>
6+
*/
7+
8+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11+
import * as http from "http";
12+
13+
const server = new Server(
14+
{
15+
name: "test-http-server",
16+
version: "1.0.0",
17+
},
18+
{
19+
capabilities: {
20+
tools: {},
21+
},
22+
}
23+
);
24+
25+
// Define tools
26+
server.setRequestHandler(ListToolsRequestSchema, async () => {
27+
return {
28+
tools: [
29+
{
30+
name: "echo",
31+
description: "Echoes back the input",
32+
inputSchema: {
33+
type: "object" as const,
34+
properties: {
35+
message: { type: "string", description: "Message to echo" },
36+
},
37+
required: ["message"],
38+
},
39+
},
40+
],
41+
};
42+
});
43+
44+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
45+
const { name, arguments: args } = request.params;
46+
47+
if (name === "echo") {
48+
return {
49+
content: [{ type: "text", text: (args as { message: string }).message }],
50+
};
51+
}
52+
53+
throw new Error(`Unknown tool: ${name}`);
54+
});
55+
56+
// Create HTTP server with streamable transport
57+
async function main() {
58+
const httpServer = http.createServer();
59+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
60+
61+
httpServer.on("request", async (req, res) => {
62+
// Simple CORS support
63+
res.setHeader("Access-Control-Allow-Origin", "*");
64+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
65+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
66+
67+
if (req.method === "OPTIONS") {
68+
res.writeHead(200);
69+
res.end();
70+
return;
71+
}
72+
73+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
74+
await transport.handleRequest(req, res);
75+
} else {
76+
res.writeHead(404);
77+
res.end("Not found");
78+
}
79+
});
80+
81+
await server.connect(transport);
82+
83+
// Use port 0 to let the OS assign a free port, unless specific port given
84+
const requestedPort = parseInt(process.argv[2] || "0", 10);
85+
httpServer.listen(requestedPort, () => {
86+
const addr = httpServer.address();
87+
const actualPort = typeof addr === "object" && addr ? addr.port : requestedPort;
88+
// Output format that tests parse: "listening on port XXXXX"
89+
console.log(`Test HTTP MCP server listening on port ${actualPort}`);
90+
});
91+
92+
// Handle shutdown
93+
process.on("SIGTERM", () => {
94+
httpServer.close();
95+
process.exit(0);
96+
});
97+
process.on("SIGINT", () => {
98+
httpServer.close();
99+
process.exit(0);
100+
});
101+
}
102+
103+
main().catch(console.error);
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Minimal MCP Server for integration testing (stdio transport)
4+
*
5+
* This server exposes tools, prompts, and resources for testing the probe functionality.
6+
*/
7+
8+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10+
import {
11+
CallToolRequestSchema,
12+
GetPromptRequestSchema,
13+
ListPromptsRequestSchema,
14+
ListResourcesRequestSchema,
15+
ListToolsRequestSchema,
16+
ReadResourceRequestSchema,
17+
} from "@modelcontextprotocol/sdk/types.js";
18+
19+
const server = new Server(
20+
{
21+
name: "test-stdio-server",
22+
version: "1.0.0",
23+
},
24+
{
25+
capabilities: {
26+
tools: {},
27+
prompts: {},
28+
resources: {},
29+
},
30+
}
31+
);
32+
33+
// Define tools
34+
server.setRequestHandler(ListToolsRequestSchema, async () => {
35+
return {
36+
tools: [
37+
{
38+
name: "greet",
39+
description: "Greets a person by name",
40+
inputSchema: {
41+
type: "object" as const,
42+
properties: {
43+
name: { type: "string", description: "Name to greet" },
44+
},
45+
required: ["name"],
46+
},
47+
},
48+
{
49+
name: "add",
50+
description: "Adds two numbers",
51+
inputSchema: {
52+
type: "object" as const,
53+
properties: {
54+
a: { type: "number", description: "First number" },
55+
b: { type: "number", description: "Second number" },
56+
},
57+
required: ["a", "b"],
58+
},
59+
},
60+
],
61+
};
62+
});
63+
64+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
65+
const { name, arguments: args } = request.params;
66+
67+
if (name === "greet") {
68+
return {
69+
content: [{ type: "text", text: `Hello, ${(args as { name: string }).name}!` }],
70+
};
71+
}
72+
73+
if (name === "add") {
74+
const { a, b } = args as { a: number; b: number };
75+
// Return as embedded JSON to test normalization
76+
return {
77+
content: [
78+
{
79+
type: "text",
80+
text: JSON.stringify({ result: a + b, operation: "add", inputs: { b, a } }),
81+
},
82+
],
83+
};
84+
}
85+
86+
throw new Error(`Unknown tool: ${name}`);
87+
});
88+
89+
// Define prompts
90+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
91+
return {
92+
prompts: [
93+
{
94+
name: "code-review",
95+
description: "Review code for issues",
96+
arguments: [
97+
{ name: "code", description: "The code to review", required: true },
98+
{ name: "language", description: "Programming language", required: false },
99+
],
100+
},
101+
],
102+
};
103+
});
104+
105+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
106+
if (request.params.name === "code-review") {
107+
const args = request.params.arguments || {};
108+
return {
109+
messages: [
110+
{
111+
role: "user" as const,
112+
content: {
113+
type: "text" as const,
114+
text: `Please review this ${args.language || "code"}:\n\n${args.code}`,
115+
},
116+
},
117+
],
118+
};
119+
}
120+
throw new Error(`Unknown prompt: ${request.params.name}`);
121+
});
122+
123+
// Define resources
124+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
125+
return {
126+
resources: [
127+
{
128+
uri: "test://readme",
129+
name: "README",
130+
description: "Project readme file",
131+
mimeType: "text/plain",
132+
},
133+
],
134+
};
135+
});
136+
137+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
138+
if (request.params.uri === "test://readme") {
139+
return {
140+
contents: [
141+
{
142+
uri: "test://readme",
143+
mimeType: "text/plain",
144+
text: "# Test Server\n\nThis is a test MCP server.",
145+
},
146+
],
147+
};
148+
}
149+
throw new Error(`Unknown resource: ${request.params.uri}`);
150+
});
151+
152+
// Start the server
153+
async function main() {
154+
const transport = new StdioServerTransport();
155+
await server.connect(transport);
156+
}
157+
158+
main().catch(console.error);

0 commit comments

Comments
 (0)