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.
- package/README.md +172 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +87 -0
- package/dist/config.test.js.map +1 -0
- package/dist/discoverer.d.ts +52 -0
- package/dist/discoverer.js +84 -0
- package/dist/discoverer.js.map +1 -0
- package/dist/discoverer.test.d.ts +1 -0
- package/dist/discoverer.test.js +178 -0
- package/dist/discoverer.test.js.map +1 -0
- package/dist/grouper.d.ts +22 -0
- package/dist/grouper.js +70 -0
- package/dist/grouper.js.map +1 -0
- package/dist/grouper.test.d.ts +1 -0
- package/dist/grouper.test.js +169 -0
- package/dist/grouper.test.js.map +1 -0
- package/dist/help.d.ts +5 -0
- package/dist/help.js +115 -0
- package/dist/help.js.map +1 -0
- package/dist/help.test.d.ts +1 -0
- package/dist/help.test.js +173 -0
- package/dist/help.test.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.js +60 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +1 -0
- package/dist/parser.test.js +142 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +10 -0
- package/dist/proxy.js.map +1 -0
- package/dist/proxy.test.d.ts +1 -0
- package/dist/proxy.test.js +61 -0
- package/dist/proxy.test.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/mcplico.example.json +18 -0
- package/package.json +36 -0
- package/src/config.test.ts +96 -0
- package/src/config.ts +76 -0
- package/src/discoverer.test.ts +222 -0
- package/src/discoverer.ts +148 -0
- package/src/grouper.test.ts +202 -0
- package/src/grouper.ts +96 -0
- package/src/help.test.ts +197 -0
- package/src/help.ts +134 -0
- package/src/index.ts +106 -0
- package/src/parser.test.ts +173 -0
- package/src/parser.ts +78 -0
- package/src/proxy.test.ts +77 -0
- package/src/proxy.ts +16 -0
- package/src/server.ts +299 -0
- package/tsconfig.json +18 -0
- 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
|
package/dist/config.d.ts
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAqB,MAAM,aAAa,CAAC;AAEtE,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QAC3B,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;YACxC,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,OAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,EAAkB,CAAC;YACpG,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE;aAC9C,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;YAC1B,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE;aAC1C,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE;oBACT,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,KAAK;oBACd,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC;oBACtB,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;oBACnB,GAAG,EAAE,MAAM;iBACZ;aACF,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;YACtB,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;aACpC,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;YAC9B,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,iBAAiB,EAAE;aACnD,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;YAChC,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,2BAA2B,EAAE;aAC7D,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,MAAM,GAAiB;gBAC3B,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,6BAA6B,EAAE;aAC/D,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;YAClD,MAAM,MAAM,GAAG;gBACb,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;aACN,CAAC;YAC7B,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,MAAM,EAAkB,CAAC;YAChD,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AA0CnG;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAY,EACZ,eAAgC,EAChC,mBAA2B,MAAM;IAEjC,IAAI,SAAS,CAAC;IAEd,IAAI,eAAe,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACrC,SAAS,GAAG,IAAI,oBAAoB,CAAC;YACnC,OAAO,EAAE,eAAe,CAAC,OAAO;YAChC,IAAI,EAAE,eAAe,CAAC,IAAI;YAC1B,GAAG,EAAE,eAAe,CAAC,GAAG;YACxB,GAAG,EAAE,eAAe,CAAC,GAAG;SACzB,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,eAAe,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC1C,SAAS,GAAG,IAAI,6BAA6B,CAC3C,IAAI,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAC7B,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,+BAAgC,eAAmC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9F,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,UAAU,IAAI,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAC5C,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAC;IAEF,uBAAuB;IACvB,MAAM,WAAW,CACf,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,gBAAgB,EAChB,kBAAkB,IAAI,qBAAqB,gBAAgB,IAAI,CAChE,CAAC;IAEF,iBAAiB;IACjB,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;IAC7C,MAAM,KAAK,GAAmB,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,WAAW,EAAG,CAAC,CAAC,WAAuC,IAAI,EAAE;KAC9D,CAAC,CAAC,CAAC;IAEJ,oCAAoC;IACpC,IAAI,SAAS,GAAuB,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC;QAC/C,SAAS,GAAG,CAAC,SAAS,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClD,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ;SACrB,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;IAED,kCAAkC;IAClC,IAAI,OAAO,GAAqB,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;QAChD,OAAO,GAAG,CAAC,YAAY,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjD,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACzC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ;aACrB,CAAC,CAAC;SACJ,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;IAChD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACrD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAwB;IAC7D,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,WAAW,CACxB,OAAmB,EACnB,SAAiB,EACjB,YAAoB;IAEpB,OAAO,OAAO,CAAC,IAAI,CAAC;QAClB,OAAO;QACP,IAAI,OAAO,CAAI,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC3B,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,SAAS,CAAC,CAC7D;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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
|