mcpico 0.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 (61) hide show
  1. package/README.md +172 -0
  2. package/dist/config.d.ts +46 -0
  3. package/dist/config.js +32 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/config.test.d.ts +1 -0
  6. package/dist/config.test.js +87 -0
  7. package/dist/config.test.js.map +1 -0
  8. package/dist/discoverer.d.ts +52 -0
  9. package/dist/discoverer.js +84 -0
  10. package/dist/discoverer.js.map +1 -0
  11. package/dist/discoverer.test.d.ts +1 -0
  12. package/dist/discoverer.test.js +178 -0
  13. package/dist/discoverer.test.js.map +1 -0
  14. package/dist/grouper.d.ts +22 -0
  15. package/dist/grouper.js +70 -0
  16. package/dist/grouper.js.map +1 -0
  17. package/dist/grouper.test.d.ts +1 -0
  18. package/dist/grouper.test.js +169 -0
  19. package/dist/grouper.test.js.map +1 -0
  20. package/dist/help.d.ts +5 -0
  21. package/dist/help.js +115 -0
  22. package/dist/help.js.map +1 -0
  23. package/dist/help.test.d.ts +1 -0
  24. package/dist/help.test.js +173 -0
  25. package/dist/help.test.js.map +1 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +90 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/parser.d.ts +28 -0
  30. package/dist/parser.js +60 -0
  31. package/dist/parser.js.map +1 -0
  32. package/dist/parser.test.d.ts +1 -0
  33. package/dist/parser.test.js +142 -0
  34. package/dist/parser.test.js.map +1 -0
  35. package/dist/proxy.d.ts +6 -0
  36. package/dist/proxy.js +10 -0
  37. package/dist/proxy.js.map +1 -0
  38. package/dist/proxy.test.d.ts +1 -0
  39. package/dist/proxy.test.js +61 -0
  40. package/dist/proxy.test.js.map +1 -0
  41. package/dist/server.d.ts +11 -0
  42. package/dist/server.js +212 -0
  43. package/dist/server.js.map +1 -0
  44. package/mcplico.example.json +18 -0
  45. package/package.json +36 -0
  46. package/src/config.test.ts +96 -0
  47. package/src/config.ts +76 -0
  48. package/src/discoverer.test.ts +222 -0
  49. package/src/discoverer.ts +148 -0
  50. package/src/grouper.test.ts +202 -0
  51. package/src/grouper.ts +96 -0
  52. package/src/help.test.ts +197 -0
  53. package/src/help.ts +134 -0
  54. package/src/index.ts +106 -0
  55. package/src/parser.test.ts +173 -0
  56. package/src/parser.ts +78 -0
  57. package/src/proxy.test.ts +77 -0
  58. package/src/proxy.ts +16 -0
  59. package/src/server.ts +299 -0
  60. package/tsconfig.json +18 -0
  61. package/vitest.config.ts +17 -0
package/dist/server.js ADDED
@@ -0,0 +1,212 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod/v4";
4
+ import { validateServerConfig } from "./config.js";
5
+ import { discoverServer, disconnectServer, } from "./discoverer.js";
6
+ import { groupTools } from "./grouper.js";
7
+ import { parseCommand } from "./parser.js";
8
+ import { generateHelpText } from "./help.js";
9
+ import { forwardToolCall } from "./proxy.js";
10
+ /** Helper: create a simple text content result */
11
+ function textResult(text) {
12
+ return {
13
+ content: [{ type: "text", text }],
14
+ };
15
+ }
16
+ /**
17
+ * Start the MCPico proxy server.
18
+ *
19
+ * 1. Validate config
20
+ * 2. Connect to all upstream servers
21
+ * 3. Discover and group their tools
22
+ * 4. Register grouped tools on the MCPico server
23
+ * 5. Listen for client connections
24
+ */
25
+ export async function startServer(config) {
26
+ const separator = config.separator || "_";
27
+ const servers = [];
28
+ const allGroups = [];
29
+ // Validate server configs
30
+ const validationErrors = [];
31
+ for (const serverConfig of config.servers) {
32
+ const err = validateServerConfig(serverConfig);
33
+ if (err) {
34
+ validationErrors.push(err);
35
+ }
36
+ }
37
+ if (validationErrors.length > 0) {
38
+ console.error("Configuration errors:");
39
+ for (const err of validationErrors) {
40
+ console.error(` - ${err}`);
41
+ }
42
+ process.exit(1);
43
+ }
44
+ // Connect to all upstream servers and discover tools
45
+ console.error(`MCPico starting with ${config.servers.length} upstream server(s)...`);
46
+ for (const serverConfig of config.servers) {
47
+ try {
48
+ const transportLabel = serverConfig.transport.type === "sse"
49
+ ? serverConfig.transport.url
50
+ : serverConfig.transport.command;
51
+ console.error(` Connecting to "${serverConfig.name}" (${serverConfig.transport.type}: ${transportLabel})...`);
52
+ const discovered = await discoverServer(serverConfig.name, serverConfig.transport, serverConfig.connectTimeoutMs);
53
+ servers.push(discovered);
54
+ const groups = groupTools(serverConfig.name, discovered.tools, separator, config.groups);
55
+ console.error(` → ${discovered.tools.length} tools → ${groups.length} groups: ${groups.map((g) => g.groupName).join(", ")}`);
56
+ allGroups.push(...groups);
57
+ }
58
+ catch (err) {
59
+ console.error(` Failed to connect to "${serverConfig.name}":`, err.message);
60
+ }
61
+ }
62
+ if (servers.length === 0) {
63
+ console.error("No upstream servers connected. Exiting.");
64
+ process.exit(1);
65
+ }
66
+ // Build lookup: groupName → ToolGroup (merged across servers)
67
+ const mergedGroups = new Map();
68
+ for (const group of allGroups) {
69
+ const existing = mergedGroups.get(group.groupName);
70
+ if (existing) {
71
+ existing.tools.push(...group.tools);
72
+ if (!existing.serverName.includes(group.serverName)) {
73
+ existing.serverName += ` + ${group.serverName}`;
74
+ }
75
+ }
76
+ else {
77
+ mergedGroups.set(group.groupName, { ...group });
78
+ }
79
+ }
80
+ // Create the MCPico server
81
+ const server = new McpServer({ name: "MCPico", version: "0.1.0" }, {
82
+ capabilities: { tools: {}, resources: {}, prompts: {} },
83
+ instructions: "MCPico bundles upstream MCP tools into hierarchical groups. " +
84
+ "Use 'help' on any group to discover available subcommands. " +
85
+ 'Format: <subcommand> {"key":"value",...}',
86
+ });
87
+ // Register each group as a tool
88
+ for (const [groupName, group] of mergedGroups) {
89
+ const toolCount = group.tools.length;
90
+ const description = [
91
+ `MCPico ${groupName} — ${toolCount} tool${toolCount === 1 ? "" : "s"}`,
92
+ `Source: ${group.serverName}`,
93
+ `Use 'help' to see all subcommands and their parameters.`,
94
+ `Format: <subcommand> {"key":"value",...}`,
95
+ ].join(" | ");
96
+ const helpText = generateHelpText(group);
97
+ server.registerTool(groupName, {
98
+ title: `MCPico: ${groupName}`,
99
+ description,
100
+ inputSchema: {
101
+ command: z
102
+ .string()
103
+ .describe(`Use 'help' for full docs or '<subcommand> {"key":"value",...}' to execute. ` +
104
+ `${toolCount} subcommand${toolCount === 1 ? "" : "s"} available.`),
105
+ },
106
+ }, async (args) => {
107
+ const parsed = parseCommand(args.command);
108
+ if (parsed.isHelp) {
109
+ return textResult(helpText);
110
+ }
111
+ if (parsed.error) {
112
+ return textResult(parsed.error);
113
+ }
114
+ const toolName = parsed.subcommand;
115
+ const upstreamTool = group.tools.find((t) => t.name === toolName);
116
+ if (!upstreamTool) {
117
+ const available = group.tools.map((t) => t.name).join(", ");
118
+ return textResult(`Unknown subcommand: "${toolName}"\n\nAvailable in ${groupName}: ${available}\n\nUse 'help' for full documentation.`);
119
+ }
120
+ const serverForTool = servers.find((s) => s.tools.some((t) => t.name === toolName));
121
+ if (!serverForTool) {
122
+ return textResult(`Internal error: could not find upstream server for tool "${toolName}"`);
123
+ }
124
+ try {
125
+ const result = await forwardToolCall(serverForTool, toolName, parsed.args);
126
+ return result;
127
+ }
128
+ catch (err) {
129
+ return textResult(`Error calling "${toolName}": ${err.message}`);
130
+ }
131
+ });
132
+ console.error(` Registered group: ${groupName} (${toolCount} tools)`);
133
+ }
134
+ // Pass through resources from all upstream servers
135
+ let resourceCount = 0;
136
+ for (const upstream of servers) {
137
+ for (const res of upstream.resources) {
138
+ const namespacedUri = `mcpico://${upstream.name}/${res.uri}`;
139
+ const displayName = `${upstream.name}: ${res.name}`;
140
+ server.registerResource(displayName, namespacedUri, {
141
+ description: `${res.description || res.name} (from ${upstream.name})`,
142
+ mimeType: res.mimeType,
143
+ }, async () => {
144
+ const result = await upstream.client.readResource({ uri: res.uri });
145
+ return {
146
+ contents: result.contents || [],
147
+ };
148
+ });
149
+ resourceCount++;
150
+ }
151
+ }
152
+ // Pass through prompts from all upstream servers
153
+ let promptCount = 0;
154
+ for (const upstream of servers) {
155
+ for (const prompt of upstream.prompts) {
156
+ const namespacedName = `${upstream.name}_${prompt.name}`;
157
+ const argShape = {};
158
+ for (const arg of prompt.arguments || []) {
159
+ argShape[arg.name] = z
160
+ .string()
161
+ .describe(arg.description ||
162
+ `Argument: ${arg.name}${arg.required ? " (required)" : ""}`);
163
+ }
164
+ server.registerPrompt(namespacedName, {
165
+ title: `${upstream.name}: ${prompt.name}`,
166
+ description: `${prompt.description || ""} (from ${upstream.name})`,
167
+ argsSchema: Object.keys(argShape).length > 0 ? argShape : undefined,
168
+ }, async (args) => {
169
+ const promptArgs = {};
170
+ if (args && typeof args === "object") {
171
+ for (const [k, v] of Object.entries(args)) {
172
+ promptArgs[k] = String(v);
173
+ }
174
+ }
175
+ const result = await upstream.client.getPrompt({
176
+ name: prompt.name,
177
+ arguments: promptArgs,
178
+ });
179
+ return {
180
+ messages: result.messages || [],
181
+ };
182
+ });
183
+ promptCount++;
184
+ }
185
+ }
186
+ if (resourceCount > 0) {
187
+ console.error(` Registered ${resourceCount} resource(s)`);
188
+ }
189
+ if (promptCount > 0) {
190
+ console.error(` Registered ${promptCount} prompt(s)`);
191
+ }
192
+ // Connect to client via stdio
193
+ const transport = new StdioServerTransport();
194
+ await server.connect(transport);
195
+ console.error(`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s)`);
196
+ // Handle shutdown
197
+ let shuttingDown = false;
198
+ const shutdown = async () => {
199
+ if (shuttingDown)
200
+ return;
201
+ shuttingDown = true;
202
+ console.error("Shutting down...");
203
+ for (const s of servers) {
204
+ await disconnectServer(s);
205
+ }
206
+ await server.close();
207
+ process.exit(0);
208
+ };
209
+ process.on("SIGINT", shutdown);
210
+ process.on("SIGTERM", shutdown);
211
+ }
212
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ {
2
+ "servers": [
3
+ {
4
+ "name": "filesystem",
5
+ "transport": {
6
+ "type": "stdio",
7
+ "command": "npx",
8
+ "args": [
9
+ "-y",
10
+ "@modelcontextprotocol/server-filesystem",
11
+ "/tmp"
12
+ ]
13
+ }
14
+ }
15
+ ],
16
+ "separator": "_",
17
+ "groups": {}
18
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "mcpico",
3
+ "version": "0.1.0",
4
+ "description": "MCPico — MCP proxy that bundles flat tool lists into hierarchical subcommand groups. Pico footprint, maximum discovery.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcpico": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "proxy",
21
+ "tools",
22
+ "context",
23
+ "hierarchical",
24
+ "subcommand"
25
+ ],
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.25.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "tsx": "^4.19.0",
33
+ "typescript": "^5.8.0",
34
+ "vitest": "^4.1.9"
35
+ }
36
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateServerConfig, type ServerConfig } from "./config.js";
3
+
4
+ describe("validateServerConfig", () => {
5
+ describe("server name", () => {
6
+ it("returns error for missing name", () => {
7
+ const server = { name: "", transport: { type: "stdio" as const, command: "echo" } } as ServerConfig;
8
+ expect(validateServerConfig(server)).toContain('"name"');
9
+ });
10
+
11
+ it("returns null for valid stdio config", () => {
12
+ const server: ServerConfig = {
13
+ name: "test-server",
14
+ transport: { type: "stdio", command: "echo" },
15
+ };
16
+ expect(validateServerConfig(server)).toBeNull();
17
+ });
18
+ });
19
+
20
+ describe("stdio transport", () => {
21
+ it("requires command", () => {
22
+ const server: ServerConfig = {
23
+ name: "test",
24
+ transport: { type: "stdio", command: "" },
25
+ };
26
+ expect(validateServerConfig(server)).toContain('"command"');
27
+ });
28
+
29
+ it("accepts stdio with optional fields", () => {
30
+ const server: ServerConfig = {
31
+ name: "test",
32
+ transport: {
33
+ type: "stdio",
34
+ command: "npx",
35
+ args: ["-y", "server"],
36
+ env: { DEBUG: "1" },
37
+ cwd: "/tmp",
38
+ },
39
+ };
40
+ expect(validateServerConfig(server)).toBeNull();
41
+ });
42
+ });
43
+
44
+ describe("sse transport", () => {
45
+ it("requires url", () => {
46
+ const server: ServerConfig = {
47
+ name: "test",
48
+ transport: { type: "sse", url: "" },
49
+ };
50
+ expect(validateServerConfig(server)).toContain('"url"');
51
+ });
52
+
53
+ it("validates URL format", () => {
54
+ const server: ServerConfig = {
55
+ name: "test",
56
+ transport: { type: "sse", url: "not-a-valid-url" },
57
+ };
58
+ expect(validateServerConfig(server)).toContain("not a valid URL");
59
+ });
60
+
61
+ it("accepts valid HTTP URL", () => {
62
+ const server: ServerConfig = {
63
+ name: "test",
64
+ transport: { type: "sse", url: "http://localhost:8080/mcp" },
65
+ };
66
+ expect(validateServerConfig(server)).toBeNull();
67
+ });
68
+
69
+ it("accepts valid HTTPS URL", () => {
70
+ const server: ServerConfig = {
71
+ name: "test",
72
+ transport: { type: "sse", url: "https://mcp.example.com/api" },
73
+ };
74
+ expect(validateServerConfig(server)).toBeNull();
75
+ });
76
+ });
77
+
78
+ describe("unknown transport", () => {
79
+ it("returns error for unknown transport type", () => {
80
+ const server = {
81
+ name: "test",
82
+ transport: { type: "websocket" },
83
+ } as unknown as ServerConfig;
84
+ const err = validateServerConfig(server);
85
+ expect(err).toContain("unknown transport type");
86
+ expect(err).toContain("websocket");
87
+ });
88
+ });
89
+
90
+ describe("missing transport", () => {
91
+ it("returns error for missing transport", () => {
92
+ const server = { name: "test" } as ServerConfig;
93
+ expect(validateServerConfig(server)).toContain('"transport"');
94
+ });
95
+ });
96
+ });
package/src/config.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Transport configuration for connecting to an upstream MCP server.
3
+ */
4
+ export type TransportConfig =
5
+ | {
6
+ type: "stdio";
7
+ command: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ cwd?: string;
11
+ }
12
+ | {
13
+ type: "sse";
14
+ /** Full URL to the MCP Streamable HTTP endpoint (e.g. "https://mcp.example.com/mcp") */
15
+ url: string;
16
+ };
17
+
18
+ /**
19
+ * Server configuration entry.
20
+ */
21
+ export interface ServerConfig {
22
+ /** Friendly name used as the tool group namespace */
23
+ name: string;
24
+ /** Transport to connect to this upstream server */
25
+ transport: TransportConfig;
26
+ /** Connection timeout in milliseconds (default: 30000) */
27
+ connectTimeoutMs?: number;
28
+ }
29
+
30
+ /**
31
+ * Tool grouping override — map group names to explicit tool lists.
32
+ */
33
+ export interface GroupOverrides {
34
+ [groupName: string]: string[];
35
+ }
36
+
37
+ /**
38
+ * Full MCPico configuration.
39
+ */
40
+ export interface MCPicoConfig {
41
+ /** Upstream MCP servers to proxy */
42
+ servers: ServerConfig[];
43
+ /** Separator used for prefix-based tool grouping (default: "_") */
44
+ separator?: string;
45
+ /** Explicit group overrides — tools not listed here are auto-grouped */
46
+ groups?: GroupOverrides;
47
+ }
48
+
49
+ /**
50
+ * Validate server config and return a user-friendly error message, or null if valid.
51
+ */
52
+ export function validateServerConfig(server: ServerConfig): string | null {
53
+ if (!server.name || typeof server.name !== "string") {
54
+ return 'Each server must have a non-empty "name" (string)';
55
+ }
56
+ if (!server.transport) {
57
+ return `Server "${server.name}": missing "transport"`;
58
+ }
59
+ if (server.transport.type === "stdio") {
60
+ if (!server.transport.command) {
61
+ return `Server "${server.name}": stdio transport requires "command"`;
62
+ }
63
+ } else if (server.transport.type === "sse") {
64
+ if (!server.transport.url) {
65
+ return `Server "${server.name}": sse transport requires "url"`;
66
+ }
67
+ try {
68
+ new URL(server.transport.url);
69
+ } catch {
70
+ return `Server "${server.name}": sse transport "url" is not a valid URL: "${server.transport.url}"`;
71
+ }
72
+ } else {
73
+ return `Server "${server.name}": unknown transport type "${(server.transport as Record<string, string>).type}". Supported: stdio, sse`;
74
+ }
75
+ return null;
76
+ }
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type { TransportConfig } from "./config.js";
3
+
4
+ // Create mock instances for the Client class
5
+ const mockConnect = vi.fn();
6
+ const mockClose = vi.fn();
7
+ let listToolsResult = { tools: [] };
8
+ let listResourcesRejects = true;
9
+ let listResourcesResult = { resources: [] };
10
+ let listPromptsRejects = true;
11
+ let listPromptsResult = { prompts: [] };
12
+
13
+ // Mock the MCP SDK modules
14
+ vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
15
+ Client: class {
16
+ connect = mockConnect;
17
+ close = mockClose;
18
+ listTools = () => Promise.resolve(listToolsResult);
19
+ listResources = () =>
20
+ listResourcesRejects
21
+ ? Promise.reject(new Error("Not supported"))
22
+ : Promise.resolve(listResourcesResult);
23
+ listPrompts = () =>
24
+ listPromptsRejects
25
+ ? Promise.reject(new Error("Not supported"))
26
+ : Promise.resolve(listPromptsResult);
27
+ callTool = vi.fn().mockResolvedValue({
28
+ content: [{ type: "text", text: "result" }],
29
+ });
30
+ readResource = vi.fn().mockResolvedValue({ contents: [] });
31
+ getPrompt = vi.fn().mockResolvedValue({ messages: [] });
32
+ },
33
+ }));
34
+
35
+ vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
36
+ StdioClientTransport: class {
37
+ constructor(..._args: unknown[]) {}
38
+ },
39
+ }));
40
+
41
+ vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
42
+ StreamableHTTPClientTransport: class {
43
+ constructor(..._args: unknown[]) {}
44
+ },
45
+ }));
46
+
47
+ import {
48
+ discoverServer,
49
+ disconnectServer,
50
+ type DiscoveredServer,
51
+ } from "./discoverer.js";
52
+
53
+ describe("discoverServer", () => {
54
+ const stdioTransport: TransportConfig = {
55
+ type: "stdio",
56
+ command: "npx",
57
+ args: ["-y", "test-server"],
58
+ };
59
+
60
+ beforeEach(() => {
61
+ vi.clearAllMocks();
62
+ listToolsResult = { tools: [] };
63
+ listResourcesRejects = true;
64
+ listResourcesResult = { resources: [] };
65
+ listPromptsRejects = true;
66
+ listPromptsResult = { prompts: [] };
67
+ });
68
+
69
+ it("connects to upstream and discovers tools", async () => {
70
+ listToolsResult = {
71
+ tools: [
72
+ {
73
+ name: "read_file",
74
+ description: "Read a file",
75
+ inputSchema: { type: "object" },
76
+ },
77
+ {
78
+ name: "write_file",
79
+ description: "Write a file",
80
+ inputSchema: { type: "object" },
81
+ },
82
+ ],
83
+ };
84
+
85
+ const server = await discoverServer("test-server", stdioTransport);
86
+
87
+ expect(server.name).toBe("test-server");
88
+ expect(server.tools).toHaveLength(2);
89
+ expect(server.tools[0].name).toBe("read_file");
90
+ expect(server.tools[0].description).toBe("Read a file");
91
+ expect(server.tools[0].inputSchema).toEqual({ type: "object" });
92
+ expect(mockConnect).toHaveBeenCalled();
93
+ });
94
+
95
+ it("handles tools without descriptions or schemas", async () => {
96
+ listToolsResult = {
97
+ tools: [{ name: "simple_tool" }],
98
+ };
99
+
100
+ const server = await discoverServer("test-server", stdioTransport);
101
+
102
+ expect(server.tools).toHaveLength(1);
103
+ expect(server.tools[0].name).toBe("simple_tool");
104
+ expect(server.tools[0].inputSchema).toEqual({});
105
+ });
106
+
107
+ it("discovers resources when supported", async () => {
108
+ listToolsResult = { tools: [] };
109
+ listResourcesRejects = false;
110
+ listResourcesResult = {
111
+ resources: [
112
+ {
113
+ uri: "file:///tmp/test.txt",
114
+ name: "test.txt",
115
+ description: "A test file",
116
+ mimeType: "text/plain",
117
+ },
118
+ ],
119
+ };
120
+
121
+ const server = await discoverServer("test-server", stdioTransport);
122
+
123
+ expect(server.resources).toHaveLength(1);
124
+ expect(server.resources[0].uri).toBe("file:///tmp/test.txt");
125
+ expect(server.resources[0].name).toBe("test.txt");
126
+ expect(server.resources[0].mimeType).toBe("text/plain");
127
+ });
128
+
129
+ it("discovers prompts when supported", async () => {
130
+ listToolsResult = { tools: [] };
131
+ listPromptsRejects = false;
132
+ listPromptsResult = {
133
+ prompts: [
134
+ {
135
+ name: "greeting",
136
+ description: "Generate a greeting",
137
+ arguments: [
138
+ { name: "name", description: "Who to greet", required: true },
139
+ ],
140
+ },
141
+ ],
142
+ };
143
+
144
+ const server = await discoverServer("test-server", stdioTransport);
145
+
146
+ expect(server.prompts).toHaveLength(1);
147
+ expect(server.prompts[0].name).toBe("greeting");
148
+ expect(server.prompts[0].arguments).toHaveLength(1);
149
+ expect(server.prompts[0].arguments![0].name).toBe("name");
150
+ expect(server.prompts[0].arguments![0].required).toBe(true);
151
+ });
152
+
153
+ it("returns empty arrays when resources/prompts are not supported", async () => {
154
+ listToolsResult = { tools: [] };
155
+ listResourcesRejects = true;
156
+ listPromptsRejects = true;
157
+
158
+ const server = await discoverServer("test-server", stdioTransport);
159
+
160
+ expect(server.resources).toEqual([]);
161
+ expect(server.prompts).toEqual([]);
162
+ });
163
+
164
+ it("handles empty tools response gracefully", async () => {
165
+ listToolsResult = {};
166
+
167
+ const server = await discoverServer("test-server", stdioTransport);
168
+
169
+ expect(server.tools).toEqual([]);
170
+ });
171
+
172
+ describe("sse transport", () => {
173
+ const sseTransport: TransportConfig = {
174
+ type: "sse",
175
+ url: "https://mcp.example.com/api",
176
+ };
177
+
178
+ it("connects via SSE transport", async () => {
179
+ listToolsResult = {
180
+ tools: [{ name: "remote_tool", description: "Remote tool", inputSchema: {} }],
181
+ };
182
+
183
+ const server = await discoverServer("sse-server", sseTransport);
184
+
185
+ expect(server.name).toBe("sse-server");
186
+ expect(server.tools).toHaveLength(1);
187
+ expect(server.tools[0].name).toBe("remote_tool");
188
+ expect(mockConnect).toHaveBeenCalled();
189
+ });
190
+ });
191
+
192
+ describe("timeout", () => {
193
+ it("rejects after timeout when connection hangs", async () => {
194
+ // Simulate a connection that never resolves
195
+ mockConnect.mockImplementationOnce(() => new Promise(() => {}));
196
+
197
+ await expect(
198
+ discoverServer("slow-server", stdioTransport, 100)
199
+ ).rejects.toThrow("timed out");
200
+ });
201
+
202
+ it("succeeds when connection is fast enough", async () => {
203
+ listToolsResult = { tools: [{ name: "fast", inputSchema: {} }] };
204
+
205
+ const server = await discoverServer("fast-server", stdioTransport, 5000);
206
+
207
+ expect(server.tools).toHaveLength(1);
208
+ });
209
+ });
210
+ });
211
+
212
+ describe("disconnectServer", () => {
213
+ it("calls client.close()", async () => {
214
+ const close = vi.fn().mockResolvedValue(undefined);
215
+ const server = {
216
+ client: { close },
217
+ } as unknown as DiscoveredServer;
218
+
219
+ await disconnectServer(server);
220
+ expect(close).toHaveBeenCalled();
221
+ });
222
+ });