@bryan-thompson/inspector-assessment-cli 1.26.6 → 1.26.7
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.
- package/build/__tests__/assessment-runner/assessment-executor.test.js +248 -0
- package/build/__tests__/assessment-runner/config-builder.test.js +289 -0
- package/build/__tests__/assessment-runner/index.test.js +41 -0
- package/build/__tests__/assessment-runner/server-config.test.js +249 -0
- package/build/__tests__/assessment-runner/server-connection.test.js +221 -0
- package/build/__tests__/assessment-runner/source-loader.test.js +341 -0
- package/build/__tests__/assessment-runner/tool-wrapper.test.js +114 -0
- package/build/__tests__/assessment-runner-facade.test.js +118 -0
- package/build/assess-full.js +26 -1254
- package/build/lib/assessment-runner/assessment-executor.js +323 -0
- package/build/lib/assessment-runner/config-builder.js +127 -0
- package/build/lib/assessment-runner/index.js +20 -0
- package/build/lib/assessment-runner/server-config.js +78 -0
- package/build/lib/assessment-runner/server-connection.js +80 -0
- package/build/lib/assessment-runner/source-loader.js +139 -0
- package/build/lib/assessment-runner/tool-wrapper.js +40 -0
- package/build/lib/assessment-runner/types.js +8 -0
- package/build/lib/assessment-runner.js +6 -740
- package/build/lib/comparison-handler.js +84 -0
- package/build/lib/result-output.js +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Config Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for loadServerConfig() that loads MCP server configuration.
|
|
5
|
+
*/
|
|
6
|
+
import { jest, describe, it, expect, beforeEach, } from "@jest/globals";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
// Mock fs module
|
|
10
|
+
jest.unstable_mockModule("fs", () => ({
|
|
11
|
+
existsSync: jest.fn(),
|
|
12
|
+
readFileSync: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
// Import after mocking
|
|
15
|
+
const fs = await import("fs");
|
|
16
|
+
const { loadServerConfig } = await import("../../lib/assessment-runner/server-config.js");
|
|
17
|
+
describe("loadServerConfig", () => {
|
|
18
|
+
const mockExistsSync = fs.existsSync;
|
|
19
|
+
const mockReadFileSync = fs.readFileSync;
|
|
20
|
+
const homedir = os.homedir();
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
mockExistsSync.mockReturnValue(false);
|
|
24
|
+
});
|
|
25
|
+
describe("config path resolution", () => {
|
|
26
|
+
it("should search explicit configPath first when provided", () => {
|
|
27
|
+
const configPath = "/custom/path/config.json";
|
|
28
|
+
mockExistsSync.mockImplementation((p) => p === configPath);
|
|
29
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
30
|
+
mcpServers: {
|
|
31
|
+
myserver: { command: "node", args: ["server.js"] },
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
const result = loadServerConfig("myserver", configPath);
|
|
35
|
+
expect(mockExistsSync).toHaveBeenCalledWith(configPath);
|
|
36
|
+
expect(result.transport).toBe("stdio");
|
|
37
|
+
expect(result.command).toBe("node");
|
|
38
|
+
});
|
|
39
|
+
it("should search ~/.config/mcp/servers/{serverName}.json", () => {
|
|
40
|
+
const expectedPath = path.join(homedir, ".config", "mcp", "servers", "testserver.json");
|
|
41
|
+
mockExistsSync.mockImplementation((p) => p === expectedPath);
|
|
42
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
43
|
+
command: "python",
|
|
44
|
+
args: ["-m", "server"],
|
|
45
|
+
}));
|
|
46
|
+
const result = loadServerConfig("testserver");
|
|
47
|
+
expect(mockExistsSync).toHaveBeenCalledWith(expectedPath);
|
|
48
|
+
expect(result.command).toBe("python");
|
|
49
|
+
});
|
|
50
|
+
it("should search ~/.config/claude/claude_desktop_config.json", () => {
|
|
51
|
+
const claudePath = path.join(homedir, ".config", "claude", "claude_desktop_config.json");
|
|
52
|
+
mockExistsSync.mockImplementation((p) => p === claudePath);
|
|
53
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
54
|
+
mcpServers: {
|
|
55
|
+
myserver: { command: "npx", args: ["-y", "@example/server"] },
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
const result = loadServerConfig("myserver");
|
|
59
|
+
expect(mockExistsSync).toHaveBeenCalledWith(claudePath);
|
|
60
|
+
expect(result.command).toBe("npx");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("stdio transport configuration", () => {
|
|
64
|
+
it("should return stdio config from mcpServers.{name}.command", () => {
|
|
65
|
+
mockExistsSync.mockReturnValue(true);
|
|
66
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
67
|
+
mcpServers: {
|
|
68
|
+
testserver: {
|
|
69
|
+
command: "node",
|
|
70
|
+
args: ["index.js", "--port", "3000"],
|
|
71
|
+
env: { NODE_ENV: "production" },
|
|
72
|
+
cwd: "/app",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}));
|
|
76
|
+
const result = loadServerConfig("testserver", "/config.json");
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
transport: "stdio",
|
|
79
|
+
command: "node",
|
|
80
|
+
args: ["index.js", "--port", "3000"],
|
|
81
|
+
env: { NODE_ENV: "production" },
|
|
82
|
+
cwd: "/app",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it("should return stdio config from root-level command", () => {
|
|
86
|
+
mockExistsSync.mockReturnValue(true);
|
|
87
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
88
|
+
command: "python",
|
|
89
|
+
args: ["server.py"],
|
|
90
|
+
}));
|
|
91
|
+
const result = loadServerConfig("anyserver", "/server-config.json");
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
transport: "stdio",
|
|
94
|
+
command: "python",
|
|
95
|
+
args: ["server.py"],
|
|
96
|
+
env: {},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("should default args and env to empty when not provided", () => {
|
|
100
|
+
mockExistsSync.mockReturnValue(true);
|
|
101
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
102
|
+
mcpServers: {
|
|
103
|
+
simple: { command: "simple-server" },
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
const result = loadServerConfig("simple", "/config.json");
|
|
107
|
+
expect(result.args).toEqual([]);
|
|
108
|
+
expect(result.env).toEqual({});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("http transport configuration", () => {
|
|
112
|
+
it("should return http config from mcpServers with transport:http", () => {
|
|
113
|
+
mockExistsSync.mockReturnValue(true);
|
|
114
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
115
|
+
mcpServers: {
|
|
116
|
+
httpserver: {
|
|
117
|
+
transport: "http",
|
|
118
|
+
url: "http://localhost:8080",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
const result = loadServerConfig("httpserver", "/config.json");
|
|
123
|
+
expect(result).toEqual({
|
|
124
|
+
transport: "http",
|
|
125
|
+
url: "http://localhost:8080",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it("should return http config when only url is specified (infer http)", () => {
|
|
129
|
+
mockExistsSync.mockReturnValue(true);
|
|
130
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
131
|
+
mcpServers: {
|
|
132
|
+
urlserver: {
|
|
133
|
+
url: "http://api.example.com/mcp",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
const result = loadServerConfig("urlserver", "/config.json");
|
|
138
|
+
expect(result.transport).toBe("http");
|
|
139
|
+
expect(result.url).toBe("http://api.example.com/mcp");
|
|
140
|
+
});
|
|
141
|
+
it("should return http config from root-level url", () => {
|
|
142
|
+
mockExistsSync.mockReturnValue(true);
|
|
143
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
144
|
+
url: "http://root-server.com/api",
|
|
145
|
+
}));
|
|
146
|
+
const result = loadServerConfig("anyserver", "/root-config.json");
|
|
147
|
+
expect(result.transport).toBe("http");
|
|
148
|
+
expect(result.url).toBe("http://root-server.com/api");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("sse transport configuration", () => {
|
|
152
|
+
it("should return sse config from mcpServers with transport:sse", () => {
|
|
153
|
+
mockExistsSync.mockReturnValue(true);
|
|
154
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
155
|
+
mcpServers: {
|
|
156
|
+
sseserver: {
|
|
157
|
+
transport: "sse",
|
|
158
|
+
url: "http://localhost:3000/events",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}));
|
|
162
|
+
const result = loadServerConfig("sseserver", "/config.json");
|
|
163
|
+
expect(result).toEqual({
|
|
164
|
+
transport: "sse",
|
|
165
|
+
url: "http://localhost:3000/events",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
it("should return sse config from root-level transport:sse", () => {
|
|
169
|
+
mockExistsSync.mockReturnValue(true);
|
|
170
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
171
|
+
transport: "sse",
|
|
172
|
+
url: "http://sse-server.com/stream",
|
|
173
|
+
}));
|
|
174
|
+
const result = loadServerConfig("anyserver", "/sse-config.json");
|
|
175
|
+
expect(result.transport).toBe("sse");
|
|
176
|
+
expect(result.url).toBe("http://sse-server.com/stream");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe("error handling", () => {
|
|
180
|
+
it('should throw "Invalid JSON" on malformed config file', () => {
|
|
181
|
+
mockExistsSync.mockReturnValue(true);
|
|
182
|
+
mockReadFileSync.mockReturnValue("{ invalid json }");
|
|
183
|
+
expect(() => loadServerConfig("server", "/bad.json")).toThrow(/Invalid JSON in config file/);
|
|
184
|
+
});
|
|
185
|
+
it('should throw "url is missing" when transport=http but no url', () => {
|
|
186
|
+
mockExistsSync.mockReturnValue(true);
|
|
187
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
188
|
+
mcpServers: {
|
|
189
|
+
nourl: {
|
|
190
|
+
transport: "http",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
}));
|
|
194
|
+
expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/'url' is missing/);
|
|
195
|
+
});
|
|
196
|
+
it('should throw "url is missing" when transport=sse but no url', () => {
|
|
197
|
+
mockExistsSync.mockReturnValue(true);
|
|
198
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
199
|
+
mcpServers: {
|
|
200
|
+
nourl: {
|
|
201
|
+
transport: "sse",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}));
|
|
205
|
+
expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/'url' is missing/);
|
|
206
|
+
});
|
|
207
|
+
it('should throw "url is missing" for root-level transport:http without url', () => {
|
|
208
|
+
mockExistsSync.mockReturnValue(true);
|
|
209
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
210
|
+
transport: "http",
|
|
211
|
+
}));
|
|
212
|
+
expect(() => loadServerConfig("server", "/config.json")).toThrow(/'url' is missing/);
|
|
213
|
+
});
|
|
214
|
+
it('should throw "Server config not found" when server not in any path', () => {
|
|
215
|
+
mockExistsSync.mockReturnValue(false);
|
|
216
|
+
expect(() => loadServerConfig("nonexistent")).toThrow(/Server config not found for: nonexistent/);
|
|
217
|
+
});
|
|
218
|
+
it("should list all tried paths in error message", () => {
|
|
219
|
+
mockExistsSync.mockReturnValue(false);
|
|
220
|
+
try {
|
|
221
|
+
loadServerConfig("missing", "/custom/path.json");
|
|
222
|
+
fail("Should have thrown");
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
const error = e;
|
|
226
|
+
expect(error.message).toContain("/custom/path.json");
|
|
227
|
+
expect(error.message).toContain(".config/mcp/servers/missing.json");
|
|
228
|
+
expect(error.message).toContain("claude_desktop_config.json");
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe("server not found in config file", () => {
|
|
233
|
+
it("should continue searching if server name not in mcpServers", () => {
|
|
234
|
+
const firstPath = "/first.json";
|
|
235
|
+
const secondPath = path.join(homedir, ".config", "mcp", "servers", "target.json");
|
|
236
|
+
mockExistsSync.mockImplementation((p) => p === firstPath || p === secondPath);
|
|
237
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
238
|
+
if (p === firstPath) {
|
|
239
|
+
return JSON.stringify({
|
|
240
|
+
mcpServers: { other: { command: "other" } },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return JSON.stringify({ command: "found" });
|
|
244
|
+
});
|
|
245
|
+
const result = loadServerConfig("target", firstPath);
|
|
246
|
+
expect(result.command).toBe("found");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Connection Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for connectToServer() that establishes MCP server connections.
|
|
5
|
+
*/
|
|
6
|
+
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
|
7
|
+
// Create mock transport classes
|
|
8
|
+
const mockStdioTransport = {
|
|
9
|
+
stderr: {
|
|
10
|
+
on: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const mockSSETransport = {};
|
|
14
|
+
const mockHTTPTransport = {};
|
|
15
|
+
const mockConnect = jest.fn();
|
|
16
|
+
const mockClient = {
|
|
17
|
+
connect: mockConnect,
|
|
18
|
+
};
|
|
19
|
+
// Mock MCP SDK
|
|
20
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
21
|
+
Client: jest.fn().mockImplementation(() => mockClient),
|
|
22
|
+
}));
|
|
23
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/client/stdio.js", () => ({
|
|
24
|
+
StdioClientTransport: jest.fn().mockImplementation(() => mockStdioTransport),
|
|
25
|
+
}));
|
|
26
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
|
27
|
+
SSEClientTransport: jest.fn().mockImplementation(() => mockSSETransport),
|
|
28
|
+
}));
|
|
29
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
30
|
+
StreamableHTTPClientTransport: jest
|
|
31
|
+
.fn()
|
|
32
|
+
.mockImplementation(() => mockHTTPTransport),
|
|
33
|
+
}));
|
|
34
|
+
// Import after mocking
|
|
35
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
36
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
37
|
+
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
38
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
39
|
+
const { connectToServer } = await import("../../lib/assessment-runner/server-connection.js");
|
|
40
|
+
describe("connectToServer", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
mockConnect.mockResolvedValue(undefined);
|
|
44
|
+
mockStdioTransport.stderr.on.mockClear();
|
|
45
|
+
});
|
|
46
|
+
describe("HTTP transport", () => {
|
|
47
|
+
it("should create StreamableHTTPClientTransport for transport:http", async () => {
|
|
48
|
+
const config = {
|
|
49
|
+
transport: "http",
|
|
50
|
+
url: "http://localhost:8080",
|
|
51
|
+
};
|
|
52
|
+
await connectToServer(config);
|
|
53
|
+
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(expect.any(URL));
|
|
54
|
+
const urlArg = StreamableHTTPClientTransport.mock
|
|
55
|
+
.calls[0][0];
|
|
56
|
+
expect(urlArg.toString()).toBe("http://localhost:8080/");
|
|
57
|
+
});
|
|
58
|
+
it('should throw "URL required for HTTP transport" when url missing', async () => {
|
|
59
|
+
const config = {
|
|
60
|
+
transport: "http",
|
|
61
|
+
url: undefined,
|
|
62
|
+
};
|
|
63
|
+
await expect(connectToServer(config)).rejects.toThrow("URL required for HTTP transport");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("SSE transport", () => {
|
|
67
|
+
it("should create SSEClientTransport for transport:sse", async () => {
|
|
68
|
+
const config = {
|
|
69
|
+
transport: "sse",
|
|
70
|
+
url: "http://localhost:3000/events",
|
|
71
|
+
};
|
|
72
|
+
await connectToServer(config);
|
|
73
|
+
expect(SSEClientTransport).toHaveBeenCalledWith(expect.any(URL));
|
|
74
|
+
const urlArg = SSEClientTransport.mock.calls[0][0];
|
|
75
|
+
expect(urlArg.toString()).toBe("http://localhost:3000/events");
|
|
76
|
+
});
|
|
77
|
+
it('should throw "URL required for SSE transport" when url missing', async () => {
|
|
78
|
+
const config = {
|
|
79
|
+
transport: "sse",
|
|
80
|
+
url: undefined,
|
|
81
|
+
};
|
|
82
|
+
await expect(connectToServer(config)).rejects.toThrow("URL required for SSE transport");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("stdio transport", () => {
|
|
86
|
+
it("should create StdioClientTransport for transport:stdio", async () => {
|
|
87
|
+
const config = {
|
|
88
|
+
transport: "stdio",
|
|
89
|
+
command: "node",
|
|
90
|
+
args: ["server.js"],
|
|
91
|
+
env: { NODE_ENV: "test" },
|
|
92
|
+
cwd: "/app",
|
|
93
|
+
};
|
|
94
|
+
await connectToServer(config);
|
|
95
|
+
expect(StdioClientTransport).toHaveBeenCalledWith(expect.objectContaining({
|
|
96
|
+
command: "node",
|
|
97
|
+
args: ["server.js"],
|
|
98
|
+
cwd: "/app",
|
|
99
|
+
stderr: "pipe",
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
it("should create StdioClientTransport when transport is undefined (default)", async () => {
|
|
103
|
+
const config = {
|
|
104
|
+
transport: "stdio",
|
|
105
|
+
command: "python",
|
|
106
|
+
args: ["-m", "server"],
|
|
107
|
+
};
|
|
108
|
+
await connectToServer(config);
|
|
109
|
+
expect(StdioClientTransport).toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it('should throw "Command required for stdio transport" when command missing', async () => {
|
|
112
|
+
const config = {
|
|
113
|
+
transport: "stdio",
|
|
114
|
+
command: undefined,
|
|
115
|
+
};
|
|
116
|
+
await expect(connectToServer(config)).rejects.toThrow("Command required for stdio transport");
|
|
117
|
+
});
|
|
118
|
+
it("should merge process.env with config.env", async () => {
|
|
119
|
+
const originalEnv = process.env;
|
|
120
|
+
process.env = { PATH: "/usr/bin", HOME: "/home/user" };
|
|
121
|
+
const config = {
|
|
122
|
+
transport: "stdio",
|
|
123
|
+
command: "node",
|
|
124
|
+
args: [],
|
|
125
|
+
env: { CUSTOM_VAR: "value" },
|
|
126
|
+
};
|
|
127
|
+
await connectToServer(config);
|
|
128
|
+
const callArg = StdioClientTransport.mock.calls[0][0];
|
|
129
|
+
expect(callArg.env).toEqual(expect.objectContaining({
|
|
130
|
+
PATH: "/usr/bin",
|
|
131
|
+
HOME: "/home/user",
|
|
132
|
+
CUSTOM_VAR: "value",
|
|
133
|
+
}));
|
|
134
|
+
process.env = originalEnv;
|
|
135
|
+
});
|
|
136
|
+
it("should setup stderr listener before connecting", async () => {
|
|
137
|
+
const config = {
|
|
138
|
+
transport: "stdio",
|
|
139
|
+
command: "node",
|
|
140
|
+
args: [],
|
|
141
|
+
};
|
|
142
|
+
await connectToServer(config);
|
|
143
|
+
// stderr.on should be called to capture error output
|
|
144
|
+
expect(mockStdioTransport.stderr.on).toHaveBeenCalledWith("data", expect.any(Function));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("Client creation", () => {
|
|
148
|
+
it("should create Client with correct info", async () => {
|
|
149
|
+
const config = {
|
|
150
|
+
transport: "http",
|
|
151
|
+
url: "http://localhost:8080",
|
|
152
|
+
};
|
|
153
|
+
await connectToServer(config);
|
|
154
|
+
expect(Client).toHaveBeenCalledWith({ name: "mcp-assess-full", version: "1.0.0" }, { capabilities: {} });
|
|
155
|
+
});
|
|
156
|
+
it("should call client.connect with transport", async () => {
|
|
157
|
+
const config = {
|
|
158
|
+
transport: "http",
|
|
159
|
+
url: "http://localhost:8080",
|
|
160
|
+
};
|
|
161
|
+
await connectToServer(config);
|
|
162
|
+
expect(mockConnect).toHaveBeenCalledWith(mockHTTPTransport);
|
|
163
|
+
});
|
|
164
|
+
it("should return connected client", async () => {
|
|
165
|
+
const config = {
|
|
166
|
+
transport: "http",
|
|
167
|
+
url: "http://localhost:8080",
|
|
168
|
+
};
|
|
169
|
+
const result = await connectToServer(config);
|
|
170
|
+
expect(result).toBe(mockClient);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("connection error handling", () => {
|
|
174
|
+
it("should include stderr in error message on connection failure", async () => {
|
|
175
|
+
// Setup stderr capture
|
|
176
|
+
let stderrCallback = () => { };
|
|
177
|
+
mockStdioTransport.stderr.on.mockImplementation((event, cb) => {
|
|
178
|
+
if (event === "data") {
|
|
179
|
+
stderrCallback = cb;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
mockConnect.mockImplementation(async () => {
|
|
183
|
+
// Simulate stderr output before connection failure
|
|
184
|
+
stderrCallback(Buffer.from("Error: Module not found\n"));
|
|
185
|
+
throw new Error("Connection refused");
|
|
186
|
+
});
|
|
187
|
+
const config = {
|
|
188
|
+
transport: "stdio",
|
|
189
|
+
command: "node",
|
|
190
|
+
args: ["server.js"],
|
|
191
|
+
};
|
|
192
|
+
await expect(connectToServer(config)).rejects.toThrow(/Failed to connect.*Module not found/s);
|
|
193
|
+
});
|
|
194
|
+
it("should provide helpful context in error message", async () => {
|
|
195
|
+
let stderrCallback = () => { };
|
|
196
|
+
mockStdioTransport.stderr.on.mockImplementation((event, cb) => {
|
|
197
|
+
if (event === "data") {
|
|
198
|
+
stderrCallback = cb;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
mockConnect.mockImplementation(async () => {
|
|
202
|
+
stderrCallback(Buffer.from("Missing API key"));
|
|
203
|
+
throw new Error("Process exited");
|
|
204
|
+
});
|
|
205
|
+
const config = {
|
|
206
|
+
transport: "stdio",
|
|
207
|
+
command: "node",
|
|
208
|
+
args: [],
|
|
209
|
+
};
|
|
210
|
+
await expect(connectToServer(config)).rejects.toThrow(/Common causes/);
|
|
211
|
+
});
|
|
212
|
+
it("should throw simple error when no stderr captured", async () => {
|
|
213
|
+
mockConnect.mockRejectedValue(new Error("Connection timeout"));
|
|
214
|
+
const config = {
|
|
215
|
+
transport: "http",
|
|
216
|
+
url: "http://localhost:8080",
|
|
217
|
+
};
|
|
218
|
+
await expect(connectToServer(config)).rejects.toThrow("Failed to connect to MCP server: Connection timeout");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|