mcp-server-diff 2.1.0

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.
Files changed (55) hide show
  1. package/.github/dependabot.yml +21 -0
  2. package/.github/workflows/ci.yml +51 -0
  3. package/.github/workflows/publish.yml +36 -0
  4. package/.github/workflows/release.yml +51 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc +8 -0
  7. package/CONTRIBUTING.md +81 -0
  8. package/LICENSE +21 -0
  9. package/README.md +526 -0
  10. package/action.yml +250 -0
  11. package/dist/__tests__/fixtures/http-server.d.ts +7 -0
  12. package/dist/__tests__/fixtures/stdio-server.d.ts +7 -0
  13. package/dist/cli/__tests__/fixtures/http-server.d.ts +7 -0
  14. package/dist/cli/__tests__/fixtures/stdio-server.d.ts +7 -0
  15. package/dist/cli/cli.d.ts +7 -0
  16. package/dist/cli/diff.d.ts +44 -0
  17. package/dist/cli/git.d.ts +37 -0
  18. package/dist/cli/index.d.ts +7 -0
  19. package/dist/cli/index.js +57182 -0
  20. package/dist/cli/licenses.txt +466 -0
  21. package/dist/cli/logger.d.ts +46 -0
  22. package/dist/cli/package.json +3 -0
  23. package/dist/cli/probe.d.ts +35 -0
  24. package/dist/cli/reporter.d.ts +20 -0
  25. package/dist/cli/runner.d.ts +30 -0
  26. package/dist/cli/types.d.ts +134 -0
  27. package/dist/cli.d.ts +7 -0
  28. package/dist/diff.d.ts +44 -0
  29. package/dist/git.d.ts +37 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +58032 -0
  32. package/dist/licenses.txt +466 -0
  33. package/dist/logger.d.ts +46 -0
  34. package/dist/package.json +3 -0
  35. package/dist/probe.d.ts +35 -0
  36. package/dist/reporter.d.ts +20 -0
  37. package/dist/runner.d.ts +30 -0
  38. package/dist/types.d.ts +134 -0
  39. package/eslint.config.mjs +47 -0
  40. package/jest.config.mjs +26 -0
  41. package/package.json +64 -0
  42. package/src/__tests__/fixtures/http-server.ts +103 -0
  43. package/src/__tests__/fixtures/stdio-server.ts +158 -0
  44. package/src/__tests__/integration.test.ts +306 -0
  45. package/src/__tests__/runner.test.ts +430 -0
  46. package/src/cli.ts +421 -0
  47. package/src/diff.ts +252 -0
  48. package/src/git.ts +262 -0
  49. package/src/index.ts +284 -0
  50. package/src/logger.ts +93 -0
  51. package/src/probe.ts +327 -0
  52. package/src/reporter.ts +214 -0
  53. package/src/runner.ts +902 -0
  54. package/src/types.ts +155 -0
  55. package/tsconfig.json +30 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Type definitions for MCP Conformance Action
3
+ */
4
+ export interface TestConfiguration {
5
+ name: string;
6
+ transport: "stdio" | "streamable-http";
7
+ start_command?: string;
8
+ args?: string;
9
+ server_url?: string;
10
+ headers?: Record<string, string>;
11
+ env_vars?: string;
12
+ custom_messages?: CustomMessage[];
13
+ /** Command to run before starting the MCP server for this config */
14
+ pre_test_command?: string;
15
+ /** Milliseconds to wait after pre_test_command before starting the server */
16
+ pre_test_wait_ms?: number;
17
+ /** Milliseconds to wait for HTTP server to start (when using start_command with HTTP transport) */
18
+ startup_wait_ms?: number;
19
+ /** Command to run after stopping the MCP server for this config (cleanup) */
20
+ post_test_command?: string;
21
+ }
22
+ export interface CustomMessage {
23
+ id: number;
24
+ name: string;
25
+ message: Record<string, unknown>;
26
+ }
27
+ export interface ActionInputs {
28
+ setupNode: boolean;
29
+ nodeVersion: string;
30
+ setupPython: boolean;
31
+ pythonVersion: string;
32
+ setupGo: boolean;
33
+ goVersion: string;
34
+ setupRust: boolean;
35
+ rustToolchain: string;
36
+ setupDotnet: boolean;
37
+ dotnetVersion: string;
38
+ installCommand: string;
39
+ buildCommand: string;
40
+ startCommand: string;
41
+ transport: "stdio" | "streamable-http";
42
+ serverUrl: string;
43
+ headers: Record<string, string>;
44
+ configurations: TestConfiguration[];
45
+ customMessages: CustomMessage[];
46
+ compareRef: string;
47
+ failOnError: boolean;
48
+ failOnDiff: boolean;
49
+ envVars: string;
50
+ serverTimeout: number;
51
+ httpStartCommand: string;
52
+ httpStartupWaitMs: number;
53
+ }
54
+ export interface ProbeResult {
55
+ initialize: InitializeInfo | null;
56
+ tools: ToolsResult | null;
57
+ prompts: PromptsResult | null;
58
+ resources: ResourcesResult | null;
59
+ resourceTemplates: ResourceTemplatesResult | null;
60
+ customResponses: Map<string, unknown>;
61
+ error?: string;
62
+ }
63
+ export interface InitializeInfo {
64
+ serverInfo?: {
65
+ name: string;
66
+ version: string;
67
+ };
68
+ capabilities?: Record<string, unknown>;
69
+ }
70
+ export interface ToolsResult {
71
+ tools: Array<{
72
+ name: string;
73
+ description?: string;
74
+ inputSchema?: Record<string, unknown>;
75
+ }>;
76
+ }
77
+ export interface PromptsResult {
78
+ prompts: Array<{
79
+ name: string;
80
+ description?: string;
81
+ arguments?: Array<{
82
+ name: string;
83
+ description?: string;
84
+ required?: boolean;
85
+ }>;
86
+ }>;
87
+ }
88
+ export interface ResourcesResult {
89
+ resources: Array<{
90
+ uri: string;
91
+ name: string;
92
+ description?: string;
93
+ mimeType?: string;
94
+ }>;
95
+ }
96
+ export interface ResourceTemplatesResult {
97
+ resourceTemplates: Array<{
98
+ uriTemplate: string;
99
+ name: string;
100
+ description?: string;
101
+ mimeType?: string;
102
+ }>;
103
+ }
104
+ /** Counts of MCP primitives discovered */
105
+ export interface PrimitiveCounts {
106
+ tools: number;
107
+ prompts: number;
108
+ resources: number;
109
+ resourceTemplates: number;
110
+ }
111
+ export interface TestResult {
112
+ configName: string;
113
+ transport: string;
114
+ branchTime: number;
115
+ baseTime: number;
116
+ hasDifferences: boolean;
117
+ diffs: Map<string, string>;
118
+ /** Primitive counts from current branch */
119
+ branchCounts?: PrimitiveCounts;
120
+ /** Primitive counts from base ref */
121
+ baseCounts?: PrimitiveCounts;
122
+ /** Error message if probing failed */
123
+ error?: string;
124
+ }
125
+ export interface ConformanceReport {
126
+ generatedAt: string;
127
+ currentBranch: string;
128
+ compareRef: string;
129
+ results: TestResult[];
130
+ totalBranchTime: number;
131
+ totalBaseTime: number;
132
+ passedCount: number;
133
+ diffCount: number;
134
+ }
@@ -0,0 +1,47 @@
1
+ import eslint from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import prettier from "eslint-config-prettier";
4
+
5
+ export default tseslint.config(
6
+ eslint.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ prettier,
9
+ {
10
+ languageOptions: {
11
+ parserOptions: {
12
+ project: "./tsconfig.json",
13
+ },
14
+ },
15
+ rules: {
16
+ "@typescript-eslint/no-unused-vars": [
17
+ "error",
18
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
19
+ ],
20
+ "@typescript-eslint/explicit-function-return-type": "off",
21
+ "@typescript-eslint/no-explicit-any": "warn",
22
+ "no-console": "warn",
23
+ },
24
+ },
25
+ {
26
+ // Test files don't need the project reference and need Jest globals
27
+ files: ["**/__tests__/**/*.ts", "**/*.test.ts"],
28
+ languageOptions: {
29
+ parserOptions: {
30
+ project: null,
31
+ },
32
+ globals: {
33
+ describe: "readonly",
34
+ it: "readonly",
35
+ expect: "readonly",
36
+ beforeEach: "readonly",
37
+ afterEach: "readonly",
38
+ beforeAll: "readonly",
39
+ afterAll: "readonly",
40
+ jest: "readonly",
41
+ },
42
+ },
43
+ },
44
+ {
45
+ ignores: ["dist/**", "node_modules/**", "*.js", "*.mjs"],
46
+ }
47
+ );
@@ -0,0 +1,26 @@
1
+ /** @type {import('jest').Config} */
2
+ export default {
3
+ preset: "ts-jest/presets/default-esm",
4
+ testEnvironment: "node",
5
+ extensionsToTreatAsEsm: [".ts"],
6
+ // Force exit after tests complete - needed because npx tsx leaves handles open
7
+ forceExit: true,
8
+ moduleNameMapper: {
9
+ "^(\\.{1,2}/.*)\\.js$": "$1",
10
+ },
11
+ transform: {
12
+ "^.+\\.tsx?$": [
13
+ "ts-jest",
14
+ {
15
+ useESM: true,
16
+ diagnostics: {
17
+ ignoreCodes: [151002],
18
+ },
19
+ },
20
+ ],
21
+ },
22
+ testMatch: ["**/__tests__/**/*.test.ts", "**/*.test.ts"],
23
+ collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
24
+ coverageDirectory: "coverage",
25
+ coverageReporters: ["text", "lcov", "html"],
26
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "mcp-server-diff",
3
+ "version": "2.1.0",
4
+ "description": "Diff MCP server public interfaces - CLI tool and GitHub Action",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-server-diff": "dist/cli/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "npm run build:action && npm run build:cli",
12
+ "build:action": "ncc build src/index.ts -o dist --license licenses.txt --no-source-map-register",
13
+ "build:cli": "ncc build src/cli.ts -o dist/cli --license licenses.txt --no-source-map-register && node -e \"const fs=require('fs'); const p='dist/cli/index.js'; fs.writeFileSync(p, '#!/usr/bin/env node\\n' + fs.readFileSync(p))\"",
14
+ "lint": "eslint src/",
15
+ "lint:fix": "eslint src/ --fix",
16
+ "format": "prettier --write src/",
17
+ "format:check": "prettier --check src/",
18
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
19
+ "test:coverage": "npm test -- --coverage",
20
+ "typecheck": "tsc --noEmit",
21
+ "check": "npm run typecheck && npm run lint && npm run format:check && npm test",
22
+ "prepublishOnly": "npm run check && npm run build"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/SamMorrowDrums/mcp-server-diff.git"
27
+ },
28
+ "keywords": [
29
+ "cli",
30
+ "github-action",
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "diff",
34
+ "api-changes"
35
+ ],
36
+ "author": "Sam Morrow",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@actions/core": "^1.11.1",
40
+ "@actions/exec": "^1.1.1",
41
+ "@actions/io": "^1.1.3",
42
+ "@modelcontextprotocol/sdk": "^1.13.2",
43
+ "diff": "^8.0.3",
44
+ "undici": "^6.23.0",
45
+ "zod": "^3.24.5"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^9.17.0",
49
+ "@types/diff": "^7.0.2",
50
+ "@types/jest": "^29.5.14",
51
+ "@types/node": "^22.0.0",
52
+ "@vercel/ncc": "^0.38.3",
53
+ "eslint": "^9.17.0",
54
+ "eslint-config-prettier": "^10.0.1",
55
+ "jest": "^29.7.0",
56
+ "prettier": "^3.4.2",
57
+ "ts-jest": "^29.2.5",
58
+ "typescript": "^5.6.0",
59
+ "typescript-eslint": "^8.19.1"
60
+ },
61
+ "overrides": {
62
+ "undici": "^6.23.0"
63
+ }
64
+ }
@@ -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);
@@ -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);