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/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # MCPico
2
+
3
+ **MCP proxy that bundles flat tool lists into hierarchical subcommand groups.**
4
+
5
+ MCPico (MCP + "ico" = tiny) wraps upstream MCP servers, grouping their tools into discoverable subcommand-based tools. One tool per group, not one per tool. The `help` subcommand auto-generates rich documentation from upstream schemas.
6
+
7
+ ## The Problem
8
+
9
+ MCP servers expose tools as a flat list. Every tool costs context tokens. A filesystem server exposes 14+ separate tools — the model sees all of them, all the time, even when it only needs one.
10
+
11
+ Some clients add "tool search" as a workaround. But searching requires the model to proactively look for tools it doesn't know exist. No structural signal about which tools relate to each other.
12
+
13
+ ## MCPico's Solution
14
+
15
+ Group related tools under a single entry point. The model sees 9 groups instead of 14 tools. Discovery is built-in via `help`:
16
+
17
+ ```
18
+ → filesystem tools:
19
+ 14 tools → 9 groups: read, write, edit, create, list, directory, move, search, get
20
+ ```
21
+
22
+ ## Features
23
+
24
+ - **Tool bundling** — Groups tools by prefix (configurable separator), collapsing flat tool lists
25
+ - **Auto-generated help** — Each group's `help` subcommand is built from upstream tool schemas
26
+ - **Multi-server aggregation** — Proxy multiple upstream MCP servers through one interface
27
+ - **Dual transport** — Supports both stdio and Streamable HTTP (SSE) upstream servers
28
+ - **Configurable timeouts** — Per-server connection timeout with sensible default (30s)
29
+ - **Resource & prompt passthrough** — Namespaced to avoid collisions across servers
30
+
31
+ ## Usage
32
+
33
+ ### Install
34
+
35
+ ```bash
36
+ npm install -g mcpico
37
+ ```
38
+
39
+ ### Configure
40
+
41
+ Create `mcpico.json`:
42
+
43
+ ```json
44
+ {
45
+ "servers": [
46
+ {
47
+ "name": "filesystem",
48
+ "transport": {
49
+ "type": "stdio",
50
+ "command": "npx",
51
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
52
+ }
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ ### Run
59
+
60
+ ```bash
61
+ mcpico
62
+ ```
63
+
64
+ ### Connect your MCP client
65
+
66
+ Add MCPico as a server in your MCP client config:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "mcpico": {
72
+ "command": "mcpico",
73
+ "args": ["--config", "/path/to/mcpico.json"]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## How it works
80
+
81
+ 1. **Connect** to upstream MCP servers
82
+ 2. **Discover** their tools (`tools/list`)
83
+ 3. **Group** tools by prefix (configurable separator, default `_`)
84
+ - `filesystem_read_file`, `filesystem_write_file` → group `filesystem`
85
+ 4. **Register** each group as a single MCP tool with a `command` string argument
86
+ 5. **Forward** tool calls by parsing `<subcommand> {"key":"value"}` and proxying to upstream
87
+ 6. **Generate help** dynamically from original tool schemas
88
+
89
+ ### Command format
90
+
91
+ ```
92
+ <subcommand> {"arg1":"val1","arg2":"val2"}
93
+ ```
94
+
95
+ Examples:
96
+ - `help` — see all subcommands and their parameters
97
+ - `read_file {"path":"/tmp/hello.txt"}` — call a specific tool
98
+ - `write_file {"path":"/tmp/out.txt","content":"hello"}` — with arguments
99
+
100
+ ### Multi-server aggregation
101
+
102
+ MCPico can proxy multiple upstream servers simultaneously:
103
+
104
+ ```json
105
+ {
106
+ "servers": [
107
+ {
108
+ "name": "filesystem",
109
+ "transport": {
110
+ "type": "stdio",
111
+ "command": "npx",
112
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
113
+ }
114
+ },
115
+ {
116
+ "name": "github",
117
+ "transport": {
118
+ "type": "sse",
119
+ "url": "https://mcp-github.example.com/mcp"
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ Groups from different servers are merged if they share a prefix. Otherwise each server's tools appear as separate groups.
127
+
128
+ ## Configuration
129
+
130
+ | Field | Type | Default | Description |
131
+ |-------|------|---------|-------------|
132
+ | `servers` | `ServerConfig[]` | **required** | Upstream MCP servers to proxy |
133
+ | `separator` | `string` | `"_"` | Separator for prefix-based tool grouping |
134
+ | `groups` | `object` | `{}` | Explicit group overrides (`{ "group": ["tool1","tool2"] }`) |
135
+
136
+ ### ServerConfig
137
+
138
+ | Field | Type | Required | Description |
139
+ |-------|------|----------|-------------|
140
+ | `name` | `string` | yes | Friendly name / group namespace |
141
+ | `transport` | `TransportConfig` | yes | How to connect to the upstream server |
142
+ | `connectTimeoutMs` | `number` | no | Connection timeout in ms (default: 30000) |
143
+
144
+ ### TransportConfig (stdio)
145
+
146
+ | Field | Type | Required | Description |
147
+ |-------|------|----------|-------------|
148
+ | `type` | `"stdio"` | yes | Transport type |
149
+ | `command` | `string` | yes | Executable to spawn |
150
+ | `args` | `string[]` | no | Command-line arguments |
151
+ | `env` | `object` | no | Environment variables |
152
+ | `cwd` | `string` | no | Working directory |
153
+
154
+ ### TransportConfig (SSE / Streamable HTTP)
155
+
156
+ | Field | Type | Required | Description |
157
+ |-------|------|----------|-------------|
158
+ | `type` | `"sse"` | yes | Transport type |
159
+ | `url` | `string` | yes | Full URL to MCP Streamable HTTP endpoint |
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ npm install
165
+ npm run build # TypeScript compilation
166
+ npm test # Run tests (77 tests, vitest)
167
+ npm run dev # Run directly with tsx
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Transport configuration for connecting to an upstream MCP server.
3
+ */
4
+ export type TransportConfig = {
5
+ type: "stdio";
6
+ command: string;
7
+ args?: string[];
8
+ env?: Record<string, string>;
9
+ cwd?: string;
10
+ } | {
11
+ type: "sse";
12
+ /** Full URL to the MCP Streamable HTTP endpoint (e.g. "https://mcp.example.com/mcp") */
13
+ url: string;
14
+ };
15
+ /**
16
+ * Server configuration entry.
17
+ */
18
+ export interface ServerConfig {
19
+ /** Friendly name used as the tool group namespace */
20
+ name: string;
21
+ /** Transport to connect to this upstream server */
22
+ transport: TransportConfig;
23
+ /** Connection timeout in milliseconds (default: 30000) */
24
+ connectTimeoutMs?: number;
25
+ }
26
+ /**
27
+ * Tool grouping override — map group names to explicit tool lists.
28
+ */
29
+ export interface GroupOverrides {
30
+ [groupName: string]: string[];
31
+ }
32
+ /**
33
+ * Full MCPico configuration.
34
+ */
35
+ export interface MCPicoConfig {
36
+ /** Upstream MCP servers to proxy */
37
+ servers: ServerConfig[];
38
+ /** Separator used for prefix-based tool grouping (default: "_") */
39
+ separator?: string;
40
+ /** Explicit group overrides — tools not listed here are auto-grouped */
41
+ groups?: GroupOverrides;
42
+ }
43
+ /**
44
+ * Validate server config and return a user-friendly error message, or null if valid.
45
+ */
46
+ export declare function validateServerConfig(server: ServerConfig): string | null;
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Validate server config and return a user-friendly error message, or null if valid.
3
+ */
4
+ export function validateServerConfig(server) {
5
+ if (!server.name || typeof server.name !== "string") {
6
+ return 'Each server must have a non-empty "name" (string)';
7
+ }
8
+ if (!server.transport) {
9
+ return `Server "${server.name}": missing "transport"`;
10
+ }
11
+ if (server.transport.type === "stdio") {
12
+ if (!server.transport.command) {
13
+ return `Server "${server.name}": stdio transport requires "command"`;
14
+ }
15
+ }
16
+ else if (server.transport.type === "sse") {
17
+ if (!server.transport.url) {
18
+ return `Server "${server.name}": sse transport requires "url"`;
19
+ }
20
+ try {
21
+ new URL(server.transport.url);
22
+ }
23
+ catch {
24
+ return `Server "${server.name}": sse transport "url" is not a valid URL: "${server.transport.url}"`;
25
+ }
26
+ }
27
+ else {
28
+ return `Server "${server.name}": unknown transport type "${server.transport.type}". Supported: stdio, sse`;
29
+ }
30
+ return null;
31
+ }
32
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAgDA;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAoB;IACvD,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpD,OAAO,mDAAmD,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,WAAW,MAAM,CAAC,IAAI,wBAAwB,CAAC;IACxD,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YAC9B,OAAO,WAAW,MAAM,CAAC,IAAI,uCAAuC,CAAC;QACvE,CAAC;IACH,CAAC;SAAM,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;YAC1B,OAAO,WAAW,MAAM,CAAC,IAAI,iCAAiC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,WAAW,MAAM,CAAC,IAAI,+CAA+C,MAAM,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC;QACtG,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,WAAW,MAAM,CAAC,IAAI,8BAA+B,MAAM,CAAC,SAAoC,CAAC,IAAI,0BAA0B,CAAC;IACzI,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateServerConfig } from "./config.js";
3
+ describe("validateServerConfig", () => {
4
+ describe("server name", () => {
5
+ it("returns error for missing name", () => {
6
+ const server = { name: "", transport: { type: "stdio", command: "echo" } };
7
+ expect(validateServerConfig(server)).toContain('"name"');
8
+ });
9
+ it("returns null for valid stdio config", () => {
10
+ const server = {
11
+ name: "test-server",
12
+ transport: { type: "stdio", command: "echo" },
13
+ };
14
+ expect(validateServerConfig(server)).toBeNull();
15
+ });
16
+ });
17
+ describe("stdio transport", () => {
18
+ it("requires command", () => {
19
+ const server = {
20
+ name: "test",
21
+ transport: { type: "stdio", command: "" },
22
+ };
23
+ expect(validateServerConfig(server)).toContain('"command"');
24
+ });
25
+ it("accepts stdio with optional fields", () => {
26
+ const server = {
27
+ name: "test",
28
+ transport: {
29
+ type: "stdio",
30
+ command: "npx",
31
+ args: ["-y", "server"],
32
+ env: { DEBUG: "1" },
33
+ cwd: "/tmp",
34
+ },
35
+ };
36
+ expect(validateServerConfig(server)).toBeNull();
37
+ });
38
+ });
39
+ describe("sse transport", () => {
40
+ it("requires url", () => {
41
+ const server = {
42
+ name: "test",
43
+ transport: { type: "sse", url: "" },
44
+ };
45
+ expect(validateServerConfig(server)).toContain('"url"');
46
+ });
47
+ it("validates URL format", () => {
48
+ const server = {
49
+ name: "test",
50
+ transport: { type: "sse", url: "not-a-valid-url" },
51
+ };
52
+ expect(validateServerConfig(server)).toContain("not a valid URL");
53
+ });
54
+ it("accepts valid HTTP URL", () => {
55
+ const server = {
56
+ name: "test",
57
+ transport: { type: "sse", url: "http://localhost:8080/mcp" },
58
+ };
59
+ expect(validateServerConfig(server)).toBeNull();
60
+ });
61
+ it("accepts valid HTTPS URL", () => {
62
+ const server = {
63
+ name: "test",
64
+ transport: { type: "sse", url: "https://mcp.example.com/api" },
65
+ };
66
+ expect(validateServerConfig(server)).toBeNull();
67
+ });
68
+ });
69
+ describe("unknown transport", () => {
70
+ it("returns error for unknown transport type", () => {
71
+ const server = {
72
+ name: "test",
73
+ transport: { type: "websocket" },
74
+ };
75
+ const err = validateServerConfig(server);
76
+ expect(err).toContain("unknown transport type");
77
+ expect(err).toContain("websocket");
78
+ });
79
+ });
80
+ describe("missing transport", () => {
81
+ it("returns error for missing transport", () => {
82
+ const server = { name: "test" };
83
+ expect(validateServerConfig(server)).toContain('"transport"');
84
+ });
85
+ });
86
+ });
87
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { TransportConfig } from "./config.js";
3
+ /**
4
+ * Tool metadata from an upstream MCP server.
5
+ */
6
+ export interface UpstreamTool {
7
+ name: string;
8
+ description?: string;
9
+ inputSchema: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Resource metadata from an upstream MCP server.
13
+ */
14
+ export interface UpstreamResource {
15
+ uri: string;
16
+ name: string;
17
+ description?: string;
18
+ mimeType?: string;
19
+ }
20
+ /**
21
+ * Prompt metadata from an upstream MCP server.
22
+ */
23
+ export interface UpstreamPrompt {
24
+ name: string;
25
+ description?: string;
26
+ arguments?: Array<{
27
+ name: string;
28
+ description?: string;
29
+ required?: boolean;
30
+ }>;
31
+ }
32
+ /**
33
+ * Discovered upstream server with its tools, resources, and prompts.
34
+ */
35
+ export interface DiscoveredServer {
36
+ name: string;
37
+ tools: UpstreamTool[];
38
+ resources: UpstreamResource[];
39
+ prompts: UpstreamPrompt[];
40
+ client: Client;
41
+ }
42
+ /**
43
+ * Connect to an upstream MCP server and discover its tools.
44
+ *
45
+ * Supports stdio and streamable HTTP transports.
46
+ * Default connection timeout is 30 seconds.
47
+ */
48
+ export declare function discoverServer(name: string, transportConfig: TransportConfig, connectTimeoutMs?: number): Promise<DiscoveredServer>;
49
+ /**
50
+ * Disconnect from an upstream server.
51
+ */
52
+ export declare function disconnectServer(server: DiscoveredServer): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ /**
5
+ * Connect to an upstream MCP server and discover its tools.
6
+ *
7
+ * Supports stdio and streamable HTTP transports.
8
+ * Default connection timeout is 30 seconds.
9
+ */
10
+ export async function discoverServer(name, transportConfig, connectTimeoutMs = 30_000) {
11
+ let transport;
12
+ if (transportConfig.type === "stdio") {
13
+ transport = new StdioClientTransport({
14
+ command: transportConfig.command,
15
+ args: transportConfig.args,
16
+ env: transportConfig.env,
17
+ cwd: transportConfig.cwd,
18
+ });
19
+ }
20
+ else if (transportConfig.type === "sse") {
21
+ transport = new StreamableHTTPClientTransport(new URL(transportConfig.url));
22
+ }
23
+ else {
24
+ throw new Error(`Unsupported transport type: ${transportConfig.type}`);
25
+ }
26
+ const client = new Client({ name: `MCPico-${name}`, version: "0.1.0" }, { capabilities: {} });
27
+ // Connect with timeout
28
+ await withTimeout(client.connect(transport), connectTimeoutMs, `Connection to "${name}" timed out after ${connectTimeoutMs}ms`);
29
+ // Discover tools
30
+ const toolsResult = await client.listTools();
31
+ const tools = (toolsResult.tools || []).map((t) => ({
32
+ name: t.name,
33
+ description: t.description,
34
+ inputSchema: t.inputSchema || {},
35
+ }));
36
+ // Discover resources (if supported)
37
+ let resources = [];
38
+ try {
39
+ const resResult = await client.listResources();
40
+ resources = (resResult.resources || []).map((r) => ({
41
+ uri: r.uri,
42
+ name: r.name,
43
+ description: r.description,
44
+ mimeType: r.mimeType,
45
+ }));
46
+ }
47
+ catch {
48
+ // Resources may not be supported by this server
49
+ }
50
+ // Discover prompts (if supported)
51
+ let prompts = [];
52
+ try {
53
+ const promptResult = await client.listPrompts();
54
+ prompts = (promptResult.prompts || []).map((p) => ({
55
+ name: p.name,
56
+ description: p.description,
57
+ arguments: (p.arguments || []).map((a) => ({
58
+ name: a.name,
59
+ description: a.description,
60
+ required: a.required,
61
+ })),
62
+ }));
63
+ }
64
+ catch {
65
+ // Prompts may not be supported by this server
66
+ }
67
+ return { name, tools, resources, prompts, client };
68
+ }
69
+ /**
70
+ * Disconnect from an upstream server.
71
+ */
72
+ export async function disconnectServer(server) {
73
+ await server.client.close();
74
+ }
75
+ /**
76
+ * Race a promise against a timeout.
77
+ */
78
+ async function withTimeout(promise, timeoutMs, errorMessage) {
79
+ return Promise.race([
80
+ promise,
81
+ new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs)),
82
+ ]);
83
+ }
84
+ //# sourceMappingURL=discoverer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discoverer.js","sourceRoot":"","sources":["../src/discoverer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // Create mock instances for the Client class
3
+ const mockConnect = vi.fn();
4
+ const mockClose = vi.fn();
5
+ let listToolsResult = { tools: [] };
6
+ let listResourcesRejects = true;
7
+ let listResourcesResult = { resources: [] };
8
+ let listPromptsRejects = true;
9
+ let listPromptsResult = { prompts: [] };
10
+ // Mock the MCP SDK modules
11
+ vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
12
+ Client: class {
13
+ connect = mockConnect;
14
+ close = mockClose;
15
+ listTools = () => Promise.resolve(listToolsResult);
16
+ listResources = () => listResourcesRejects
17
+ ? Promise.reject(new Error("Not supported"))
18
+ : Promise.resolve(listResourcesResult);
19
+ listPrompts = () => listPromptsRejects
20
+ ? Promise.reject(new Error("Not supported"))
21
+ : Promise.resolve(listPromptsResult);
22
+ callTool = vi.fn().mockResolvedValue({
23
+ content: [{ type: "text", text: "result" }],
24
+ });
25
+ readResource = vi.fn().mockResolvedValue({ contents: [] });
26
+ getPrompt = vi.fn().mockResolvedValue({ messages: [] });
27
+ },
28
+ }));
29
+ vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
30
+ StdioClientTransport: class {
31
+ constructor(..._args) { }
32
+ },
33
+ }));
34
+ vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
35
+ StreamableHTTPClientTransport: class {
36
+ constructor(..._args) { }
37
+ },
38
+ }));
39
+ import { discoverServer, disconnectServer, } from "./discoverer.js";
40
+ describe("discoverServer", () => {
41
+ const stdioTransport = {
42
+ type: "stdio",
43
+ command: "npx",
44
+ args: ["-y", "test-server"],
45
+ };
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ listToolsResult = { tools: [] };
49
+ listResourcesRejects = true;
50
+ listResourcesResult = { resources: [] };
51
+ listPromptsRejects = true;
52
+ listPromptsResult = { prompts: [] };
53
+ });
54
+ it("connects to upstream and discovers tools", async () => {
55
+ listToolsResult = {
56
+ tools: [
57
+ {
58
+ name: "read_file",
59
+ description: "Read a file",
60
+ inputSchema: { type: "object" },
61
+ },
62
+ {
63
+ name: "write_file",
64
+ description: "Write a file",
65
+ inputSchema: { type: "object" },
66
+ },
67
+ ],
68
+ };
69
+ const server = await discoverServer("test-server", stdioTransport);
70
+ expect(server.name).toBe("test-server");
71
+ expect(server.tools).toHaveLength(2);
72
+ expect(server.tools[0].name).toBe("read_file");
73
+ expect(server.tools[0].description).toBe("Read a file");
74
+ expect(server.tools[0].inputSchema).toEqual({ type: "object" });
75
+ expect(mockConnect).toHaveBeenCalled();
76
+ });
77
+ it("handles tools without descriptions or schemas", async () => {
78
+ listToolsResult = {
79
+ tools: [{ name: "simple_tool" }],
80
+ };
81
+ const server = await discoverServer("test-server", stdioTransport);
82
+ expect(server.tools).toHaveLength(1);
83
+ expect(server.tools[0].name).toBe("simple_tool");
84
+ expect(server.tools[0].inputSchema).toEqual({});
85
+ });
86
+ it("discovers resources when supported", async () => {
87
+ listToolsResult = { tools: [] };
88
+ listResourcesRejects = false;
89
+ listResourcesResult = {
90
+ resources: [
91
+ {
92
+ uri: "file:///tmp/test.txt",
93
+ name: "test.txt",
94
+ description: "A test file",
95
+ mimeType: "text/plain",
96
+ },
97
+ ],
98
+ };
99
+ const server = await discoverServer("test-server", stdioTransport);
100
+ expect(server.resources).toHaveLength(1);
101
+ expect(server.resources[0].uri).toBe("file:///tmp/test.txt");
102
+ expect(server.resources[0].name).toBe("test.txt");
103
+ expect(server.resources[0].mimeType).toBe("text/plain");
104
+ });
105
+ it("discovers prompts when supported", async () => {
106
+ listToolsResult = { tools: [] };
107
+ listPromptsRejects = false;
108
+ listPromptsResult = {
109
+ prompts: [
110
+ {
111
+ name: "greeting",
112
+ description: "Generate a greeting",
113
+ arguments: [
114
+ { name: "name", description: "Who to greet", required: true },
115
+ ],
116
+ },
117
+ ],
118
+ };
119
+ const server = await discoverServer("test-server", stdioTransport);
120
+ expect(server.prompts).toHaveLength(1);
121
+ expect(server.prompts[0].name).toBe("greeting");
122
+ expect(server.prompts[0].arguments).toHaveLength(1);
123
+ expect(server.prompts[0].arguments[0].name).toBe("name");
124
+ expect(server.prompts[0].arguments[0].required).toBe(true);
125
+ });
126
+ it("returns empty arrays when resources/prompts are not supported", async () => {
127
+ listToolsResult = { tools: [] };
128
+ listResourcesRejects = true;
129
+ listPromptsRejects = true;
130
+ const server = await discoverServer("test-server", stdioTransport);
131
+ expect(server.resources).toEqual([]);
132
+ expect(server.prompts).toEqual([]);
133
+ });
134
+ it("handles empty tools response gracefully", async () => {
135
+ listToolsResult = {};
136
+ const server = await discoverServer("test-server", stdioTransport);
137
+ expect(server.tools).toEqual([]);
138
+ });
139
+ describe("sse transport", () => {
140
+ const sseTransport = {
141
+ type: "sse",
142
+ url: "https://mcp.example.com/api",
143
+ };
144
+ it("connects via SSE transport", async () => {
145
+ listToolsResult = {
146
+ tools: [{ name: "remote_tool", description: "Remote tool", inputSchema: {} }],
147
+ };
148
+ const server = await discoverServer("sse-server", sseTransport);
149
+ expect(server.name).toBe("sse-server");
150
+ expect(server.tools).toHaveLength(1);
151
+ expect(server.tools[0].name).toBe("remote_tool");
152
+ expect(mockConnect).toHaveBeenCalled();
153
+ });
154
+ });
155
+ describe("timeout", () => {
156
+ it("rejects after timeout when connection hangs", async () => {
157
+ // Simulate a connection that never resolves
158
+ mockConnect.mockImplementationOnce(() => new Promise(() => { }));
159
+ await expect(discoverServer("slow-server", stdioTransport, 100)).rejects.toThrow("timed out");
160
+ });
161
+ it("succeeds when connection is fast enough", async () => {
162
+ listToolsResult = { tools: [{ name: "fast", inputSchema: {} }] };
163
+ const server = await discoverServer("fast-server", stdioTransport, 5000);
164
+ expect(server.tools).toHaveLength(1);
165
+ });
166
+ });
167
+ });
168
+ describe("disconnectServer", () => {
169
+ it("calls client.close()", async () => {
170
+ const close = vi.fn().mockResolvedValue(undefined);
171
+ const server = {
172
+ client: { close },
173
+ };
174
+ await disconnectServer(server);
175
+ expect(close).toHaveBeenCalled();
176
+ });
177
+ });
178
+ //# sourceMappingURL=discoverer.test.js.map