mcp-server-diff 2.1.0 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,158 +0,0 @@
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);
@@ -1,306 +0,0 @@
1
- /**
2
- * Integration tests for MCP Conformance Action
3
- *
4
- * These tests actually spin up MCP servers and probe them.
5
- */
6
-
7
- import { probeServer, probeResultToFiles } from "../probe.js";
8
- import { spawn, ChildProcess } from "child_process";
9
- import * as path from "path";
10
- import { fileURLToPath } from "url";
11
- import { jest } from "@jest/globals";
12
-
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const FIXTURES_DIR = path.join(__dirname, "fixtures");
15
-
16
- // Increase timeout for integration tests
17
- jest.setTimeout(30000);
18
-
19
- describe("Integration: stdio transport", () => {
20
- it("probes a real stdio MCP server", async () => {
21
- const result = await probeServer({
22
- transport: "stdio",
23
- command: "npx",
24
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
25
- workingDir: FIXTURES_DIR,
26
- });
27
-
28
- // Should not have errors
29
- expect(result.error).toBeUndefined();
30
-
31
- // Should have initialize info
32
- expect(result.initialize).not.toBeNull();
33
- expect(result.initialize?.serverInfo?.name).toBe("test-stdio-server");
34
- expect(result.initialize?.serverInfo?.version).toBe("1.0.0");
35
-
36
- // Should have tools
37
- expect(result.tools).not.toBeNull();
38
- expect(result.tools?.tools).toHaveLength(2);
39
- const toolNames = result.tools?.tools.map((t) => t.name).sort();
40
- expect(toolNames).toEqual(["add", "greet"]);
41
-
42
- // Should have prompts
43
- expect(result.prompts).not.toBeNull();
44
- expect(result.prompts?.prompts).toHaveLength(1);
45
- expect(result.prompts?.prompts[0].name).toBe("code-review");
46
-
47
- // Should have resources
48
- expect(result.resources).not.toBeNull();
49
- expect(result.resources?.resources).toHaveLength(1);
50
- expect(result.resources?.resources[0].uri).toBe("test://readme");
51
- });
52
-
53
- it("handles probe errors gracefully", async () => {
54
- const result = await probeServer({
55
- transport: "stdio",
56
- command: "node",
57
- args: ["-e", "console.log('not an mcp server'); process.exit(1)"],
58
- workingDir: FIXTURES_DIR,
59
- });
60
-
61
- // Should have an error
62
- expect(result.error).toBeDefined();
63
- });
64
-
65
- it("returns error for non-existent command", async () => {
66
- const result = await probeServer({
67
- transport: "stdio",
68
- command: "non-existent-command-12345",
69
- args: [],
70
- workingDir: FIXTURES_DIR,
71
- });
72
-
73
- // Should have an error
74
- expect(result.error).toBeDefined();
75
- });
76
- });
77
-
78
- describe("Integration: streamable-http transport", () => {
79
- let serverProcess: ChildProcess | null = null;
80
- let serverUrl: string = "";
81
-
82
- async function startHttpServer(): Promise<void> {
83
- return new Promise((resolve, reject) => {
84
- // Use port 0 to let OS assign free port
85
- serverProcess = spawn("npx", ["tsx", path.join(FIXTURES_DIR, "http-server.ts"), "0"], {
86
- cwd: FIXTURES_DIR,
87
- stdio: ["ignore", "pipe", "pipe"],
88
- detached: false, // Ensure child process is attached to parent
89
- });
90
-
91
- let resolved = false;
92
-
93
- serverProcess.stdout?.on("data", (data) => {
94
- const output = data.toString();
95
- // Parse the actual port from server output
96
- const portMatch = output.match(/listening on port (\d+)/);
97
- if (portMatch && !resolved) {
98
- resolved = true;
99
- const port = portMatch[1];
100
- serverUrl = `http://localhost:${port}/mcp`;
101
- // Give it a moment to be fully ready
102
- setTimeout(resolve, 500);
103
- }
104
- });
105
-
106
- serverProcess.stderr?.on("data", (data) => {
107
- // Log errors during startup to help debug
108
- if (!resolved) {
109
- const msg = data.toString();
110
- if (msg.includes("EADDRINUSE")) {
111
- reject(new Error("Port conflict - this should not happen with port 0"));
112
- }
113
- }
114
- });
115
-
116
- serverProcess.on("error", (err) => {
117
- if (!resolved) {
118
- resolved = true;
119
- reject(err);
120
- }
121
- });
122
-
123
- serverProcess.on("exit", (code) => {
124
- if (!resolved && code !== 0) {
125
- resolved = true;
126
- reject(new Error(`Server exited with code ${code}`));
127
- }
128
- });
129
-
130
- // Timeout after 10s
131
- setTimeout(() => {
132
- if (!resolved) {
133
- resolved = true;
134
- reject(new Error("Server startup timeout"));
135
- }
136
- }, 10000);
137
- });
138
- }
139
-
140
- async function stopHttpServer(): Promise<void> {
141
- if (serverProcess) {
142
- const proc = serverProcess;
143
- serverProcess = null;
144
-
145
- // Try graceful shutdown first
146
- proc.kill("SIGTERM");
147
-
148
- // Wait for exit with timeout, then force kill
149
- await new Promise<void>((resolve) => {
150
- const forceKillTimeout = setTimeout(() => {
151
- proc.kill("SIGKILL");
152
- }, 1000);
153
-
154
- proc.on("exit", () => {
155
- clearTimeout(forceKillTimeout);
156
- resolve();
157
- });
158
-
159
- // Final safety timeout
160
- setTimeout(resolve, 2000);
161
- });
162
- }
163
- }
164
-
165
- beforeAll(async () => {
166
- await startHttpServer();
167
- });
168
-
169
- afterAll(async () => {
170
- await stopHttpServer();
171
- });
172
-
173
- it("probes a real HTTP MCP server", async () => {
174
- const result = await probeServer({
175
- transport: "streamable-http",
176
- url: serverUrl,
177
- });
178
-
179
- // Should not have errors
180
- expect(result.error).toBeUndefined();
181
-
182
- // Should have initialize info
183
- expect(result.initialize).not.toBeNull();
184
- expect(result.initialize?.serverInfo?.name).toBe("test-http-server");
185
- expect(result.initialize?.serverInfo?.version).toBe("1.0.0");
186
-
187
- // Should have tools
188
- expect(result.tools).not.toBeNull();
189
- expect(result.tools?.tools).toHaveLength(1);
190
- expect(result.tools?.tools[0].name).toBe("echo");
191
- });
192
-
193
- it("returns error for unavailable HTTP server", async () => {
194
- const result = await probeServer({
195
- transport: "streamable-http",
196
- url: "http://localhost:59999/mcp", // Port that's not listening
197
- });
198
-
199
- // Should have an error
200
- expect(result.error).toBeDefined();
201
- });
202
- });
203
-
204
- describe("Integration: normalization end-to-end", () => {
205
- it("normalizes probe results consistently", async () => {
206
- const result = await probeServer({
207
- transport: "stdio",
208
- command: "npx",
209
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
210
- workingDir: FIXTURES_DIR,
211
- });
212
-
213
- expect(result.error).toBeUndefined();
214
-
215
- // Convert to files
216
- const files = probeResultToFiles(result);
217
-
218
- // Should have expected files
219
- expect(files.has("initialize")).toBe(true);
220
- expect(files.has("tools")).toBe(true);
221
- expect(files.has("prompts")).toBe(true);
222
- expect(files.has("resources")).toBe(true);
223
-
224
- // Parse and verify tools are normalized (sorted)
225
- const tools = JSON.parse(files.get("tools")!);
226
- const toolNames = tools.tools.map((t: { name: string }) => t.name);
227
- expect(toolNames).toEqual(["add", "greet"]); // Sorted alphabetically
228
-
229
- // Verify the JSON is deterministic
230
- const files2 = probeResultToFiles(result);
231
- expect(files.get("tools")).toBe(files2.get("tools"));
232
- expect(files.get("initialize")).toBe(files2.get("initialize"));
233
- });
234
-
235
- it("produces identical output for semantically identical servers", async () => {
236
- // Probe the server twice
237
- const result1 = await probeServer({
238
- transport: "stdio",
239
- command: "npx",
240
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
241
- workingDir: FIXTURES_DIR,
242
- });
243
-
244
- const result2 = await probeServer({
245
- transport: "stdio",
246
- command: "npx",
247
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
248
- workingDir: FIXTURES_DIR,
249
- });
250
-
251
- expect(result1.error).toBeUndefined();
252
- expect(result2.error).toBeUndefined();
253
-
254
- const files1 = probeResultToFiles(result1);
255
- const files2 = probeResultToFiles(result2);
256
-
257
- // All files should be identical
258
- for (const [name, content] of files1) {
259
- expect(files2.get(name)).toBe(content);
260
- }
261
- });
262
- });
263
-
264
- describe("Integration: custom messages", () => {
265
- it("sends custom messages and captures responses", async () => {
266
- // The stdio server supports tools/list which we can call as a custom message
267
- const result = await probeServer({
268
- transport: "stdio",
269
- command: "npx",
270
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
271
- workingDir: FIXTURES_DIR,
272
- customMessages: [
273
- {
274
- id: 1,
275
- name: "list-tools-custom",
276
- message: { method: "tools/list", params: {} },
277
- },
278
- ],
279
- });
280
-
281
- expect(result.error).toBeUndefined();
282
-
283
- // Should have custom response
284
- expect(result.customResponses.has("list-tools-custom")).toBe(true);
285
- const customResponse = result.customResponses.get("list-tools-custom") as { tools: unknown[] };
286
- expect(customResponse.tools).toHaveLength(2);
287
- });
288
- });
289
-
290
- describe("Integration: environment variables", () => {
291
- it("passes environment variables to stdio server", async () => {
292
- const result = await probeServer({
293
- transport: "stdio",
294
- command: "npx",
295
- args: ["tsx", path.join(FIXTURES_DIR, "stdio-server.ts")],
296
- workingDir: FIXTURES_DIR,
297
- envVars: {
298
- TEST_VAR: "test_value",
299
- },
300
- });
301
-
302
- // Server should start successfully (it doesn't use the env var, but it should be passed)
303
- expect(result.error).toBeUndefined();
304
- expect(result.initialize?.serverInfo?.name).toBe("test-stdio-server");
305
- });
306
- });