mcpico 0.1.0 → 0.1.2
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 +22 -2
- package/dist/config.d.ts +12 -0
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +31 -0
- package/dist/server.js +113 -55
- package/dist/server.js.map +1 -1
- package/package.json +10 -1
- package/src/config.ts +9 -0
- package/src/help.test.ts +40 -0
- package/src/server.test.ts +324 -0
- package/src/server.ts +142 -73
- package/vitest.config.ts +7 -4
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ Group related tools under a single entry point. The model sees 9 groups instead
|
|
|
24
24
|
- **Tool bundling** — Groups tools by prefix (configurable separator), collapsing flat tool lists
|
|
25
25
|
- **Auto-generated help** — Each group's `help` subcommand is built from upstream tool schemas
|
|
26
26
|
- **Multi-server aggregation** — Proxy multiple upstream MCP servers through one interface
|
|
27
|
-
- **Dual transport** — Supports both stdio and Streamable HTTP (SSE) upstream servers
|
|
27
|
+
- **Dual upstream transport** — Supports both stdio and Streamable HTTP (SSE) upstream servers
|
|
28
|
+
- **Dual listen transport** — MCPico itself listens via stdio or HTTP/SSE (configurable port)
|
|
28
29
|
- **Configurable timeouts** — Per-server connection timeout with sensible default (30s)
|
|
29
30
|
- **Resource & prompt passthrough** — Namespaced to avoid collisions across servers
|
|
30
31
|
|
|
@@ -132,6 +133,25 @@ Groups from different servers are merged if they share a prefix. Otherwise each
|
|
|
132
133
|
| `servers` | `ServerConfig[]` | **required** | Upstream MCP servers to proxy |
|
|
133
134
|
| `separator` | `string` | `"_"` | Separator for prefix-based tool grouping |
|
|
134
135
|
| `groups` | `object` | `{}` | Explicit group overrides (`{ "group": ["tool1","tool2"] }`) |
|
|
136
|
+
| `listen` | `ListenConfig` | `{"type":"stdio"}` | How MCPico exposes itself to MCP clients |
|
|
137
|
+
|
|
138
|
+
### ListenConfig
|
|
139
|
+
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
|-------|------|----------|-------------|
|
|
142
|
+
| `type` | `"stdio"` | yes | Standard stdio transport |
|
|
143
|
+
| `type` | `"sse"` | yes | HTTP/SSE — specify `port` and optional `host` |
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
// SSE listen mode — MCPico as an HTTP endpoint
|
|
147
|
+
{
|
|
148
|
+
"servers": [...],
|
|
149
|
+
"listen": {
|
|
150
|
+
"type": "sse",
|
|
151
|
+
"port": 3000
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
135
155
|
|
|
136
156
|
### ServerConfig
|
|
137
157
|
|
|
@@ -163,7 +183,7 @@ Groups from different servers are merged if they share a prefix. Otherwise each
|
|
|
163
183
|
```bash
|
|
164
184
|
npm install
|
|
165
185
|
npm run build # TypeScript compilation
|
|
166
|
-
npm test # Run tests (
|
|
186
|
+
npm test # Run tests (105 tests, vitest)
|
|
167
187
|
npm run dev # Run directly with tsx
|
|
168
188
|
```
|
|
169
189
|
|
package/dist/config.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export interface ServerConfig {
|
|
|
29
29
|
export interface GroupOverrides {
|
|
30
30
|
[groupName: string]: string[];
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Transport to expose MCPico itself to clients.
|
|
34
|
+
*/
|
|
35
|
+
export type ListenConfig = {
|
|
36
|
+
type: "stdio";
|
|
37
|
+
} | {
|
|
38
|
+
type: "sse";
|
|
39
|
+
port: number;
|
|
40
|
+
host?: string;
|
|
41
|
+
};
|
|
32
42
|
/**
|
|
33
43
|
* Full MCPico configuration.
|
|
34
44
|
*/
|
|
@@ -39,6 +49,8 @@ export interface MCPicoConfig {
|
|
|
39
49
|
separator?: string;
|
|
40
50
|
/** Explicit group overrides — tools not listed here are auto-grouped */
|
|
41
51
|
groups?: GroupOverrides;
|
|
52
|
+
/** How MCPico exposes itself to clients (default: stdio) */
|
|
53
|
+
listen?: ListenConfig;
|
|
42
54
|
}
|
|
43
55
|
/**
|
|
44
56
|
* Validate server config and return a user-friendly error message, or null if valid.
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAyDA;;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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
1
2
|
import type { MCPicoConfig } from "./config.js";
|
|
3
|
+
import { type DiscoveredServer } from "./discoverer.js";
|
|
4
|
+
import { type ToolGroup } from "./grouper.js";
|
|
5
|
+
/** Helper: create a simple text content result */
|
|
6
|
+
export declare function textResult(text: string): CallToolResult;
|
|
7
|
+
/**
|
|
8
|
+
* Merge tool groups from multiple upstream servers.
|
|
9
|
+
*
|
|
10
|
+
* Groups with the same name are merged:
|
|
11
|
+
* - Tools are concatenated
|
|
12
|
+
* - serverName is joined with " + "
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeGroups(allGroups: ToolGroup[]): Map<string, ToolGroup>;
|
|
15
|
+
/**
|
|
16
|
+
* Handle a tool call command for a given group.
|
|
17
|
+
*
|
|
18
|
+
* Dispatches: help → error → unknown subcommand → forward to upstream server.
|
|
19
|
+
*
|
|
20
|
+
* Exported for testing; allows injecting a mock forwardFn to avoid
|
|
21
|
+
* connecting to real upstream servers.
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleToolCall(command: string, group: ToolGroup, servers: DiscoveredServer[], helpText: string, forwardFn?: (server: DiscoveredServer, toolName: string, args: Record<string, unknown>) => Promise<CallToolResult>): Promise<CallToolResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Build the description string for a tool group.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildGroupDescription(group: ToolGroup): string;
|
|
28
|
+
/**
|
|
29
|
+
* Validate all server configs, returning an array of error messages.
|
|
30
|
+
* Returns empty array if all configs are valid.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateAllServerConfigs(servers: MCPicoConfig["servers"]): string[];
|
|
2
33
|
/**
|
|
3
34
|
* Start the MCPico proxy server.
|
|
4
35
|
*
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createServer } from "node:http";
|
|
3
5
|
import { z } from "zod/v4";
|
|
4
6
|
import { validateServerConfig } from "./config.js";
|
|
5
7
|
import { discoverServer, disconnectServer, } from "./discoverer.js";
|
|
@@ -8,11 +10,94 @@ import { parseCommand } from "./parser.js";
|
|
|
8
10
|
import { generateHelpText } from "./help.js";
|
|
9
11
|
import { forwardToolCall } from "./proxy.js";
|
|
10
12
|
/** Helper: create a simple text content result */
|
|
11
|
-
function textResult(text) {
|
|
13
|
+
export function textResult(text) {
|
|
12
14
|
return {
|
|
13
15
|
content: [{ type: "text", text }],
|
|
14
16
|
};
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Merge tool groups from multiple upstream servers.
|
|
20
|
+
*
|
|
21
|
+
* Groups with the same name are merged:
|
|
22
|
+
* - Tools are concatenated
|
|
23
|
+
* - serverName is joined with " + "
|
|
24
|
+
*/
|
|
25
|
+
export function mergeGroups(allGroups) {
|
|
26
|
+
const mergedGroups = new Map();
|
|
27
|
+
for (const group of allGroups) {
|
|
28
|
+
const existing = mergedGroups.get(group.groupName);
|
|
29
|
+
if (existing) {
|
|
30
|
+
existing.tools.push(...group.tools);
|
|
31
|
+
if (!existing.serverName.includes(group.serverName)) {
|
|
32
|
+
existing.serverName += ` + ${group.serverName}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
mergedGroups.set(group.groupName, { ...group });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return mergedGroups;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Handle a tool call command for a given group.
|
|
43
|
+
*
|
|
44
|
+
* Dispatches: help → error → unknown subcommand → forward to upstream server.
|
|
45
|
+
*
|
|
46
|
+
* Exported for testing; allows injecting a mock forwardFn to avoid
|
|
47
|
+
* connecting to real upstream servers.
|
|
48
|
+
*/
|
|
49
|
+
export async function handleToolCall(command, group, servers, helpText, forwardFn = forwardToolCall) {
|
|
50
|
+
const parsed = parseCommand(command);
|
|
51
|
+
if (parsed.isHelp) {
|
|
52
|
+
return textResult(helpText);
|
|
53
|
+
}
|
|
54
|
+
if (parsed.error) {
|
|
55
|
+
return textResult(parsed.error);
|
|
56
|
+
}
|
|
57
|
+
const toolName = parsed.subcommand;
|
|
58
|
+
const upstreamTool = group.tools.find((t) => t.name === toolName);
|
|
59
|
+
if (!upstreamTool) {
|
|
60
|
+
const available = group.tools.map((t) => t.name).join(", ");
|
|
61
|
+
return textResult(`Unknown subcommand: "${toolName}"\n\nAvailable in ${group.groupName}: ${available}\n\nUse 'help' for full documentation.`);
|
|
62
|
+
}
|
|
63
|
+
const serverForTool = servers.find((s) => s.tools.some((t) => t.name === toolName));
|
|
64
|
+
if (!serverForTool) {
|
|
65
|
+
return textResult(`Internal error: could not find upstream server for tool "${toolName}"`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const result = await forwardFn(serverForTool, toolName, parsed.args);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return textResult(`Error calling "${toolName}": ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the description string for a tool group.
|
|
77
|
+
*/
|
|
78
|
+
export function buildGroupDescription(group) {
|
|
79
|
+
const toolCount = group.tools.length;
|
|
80
|
+
return [
|
|
81
|
+
`MCPico ${group.groupName} — ${toolCount} tool${toolCount === 1 ? "" : "s"}`,
|
|
82
|
+
`Source: ${group.serverName}`,
|
|
83
|
+
`Use 'help' to see all subcommands and their parameters.`,
|
|
84
|
+
`Format: <subcommand> {"key":"value",...}`,
|
|
85
|
+
].join(" | ");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate all server configs, returning an array of error messages.
|
|
89
|
+
* Returns empty array if all configs are valid.
|
|
90
|
+
*/
|
|
91
|
+
export function validateAllServerConfigs(servers) {
|
|
92
|
+
const errors = [];
|
|
93
|
+
for (const serverConfig of servers) {
|
|
94
|
+
const err = validateServerConfig(serverConfig);
|
|
95
|
+
if (err) {
|
|
96
|
+
errors.push(err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return errors;
|
|
100
|
+
}
|
|
16
101
|
/**
|
|
17
102
|
* Start the MCPico proxy server.
|
|
18
103
|
*
|
|
@@ -27,13 +112,7 @@ export async function startServer(config) {
|
|
|
27
112
|
const servers = [];
|
|
28
113
|
const allGroups = [];
|
|
29
114
|
// 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
|
-
}
|
|
115
|
+
const validationErrors = validateAllServerConfigs(config.servers);
|
|
37
116
|
if (validationErrors.length > 0) {
|
|
38
117
|
console.error("Configuration errors:");
|
|
39
118
|
for (const err of validationErrors) {
|
|
@@ -64,19 +143,7 @@ export async function startServer(config) {
|
|
|
64
143
|
process.exit(1);
|
|
65
144
|
}
|
|
66
145
|
// Build lookup: groupName → ToolGroup (merged across servers)
|
|
67
|
-
const mergedGroups =
|
|
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
|
-
}
|
|
146
|
+
const mergedGroups = mergeGroups(allGroups);
|
|
80
147
|
// Create the MCPico server
|
|
81
148
|
const server = new McpServer({ name: "MCPico", version: "0.1.0" }, {
|
|
82
149
|
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
@@ -87,12 +154,7 @@ export async function startServer(config) {
|
|
|
87
154
|
// Register each group as a tool
|
|
88
155
|
for (const [groupName, group] of mergedGroups) {
|
|
89
156
|
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(" | ");
|
|
157
|
+
const description = buildGroupDescription(group);
|
|
96
158
|
const helpText = generateHelpText(group);
|
|
97
159
|
server.registerTool(groupName, {
|
|
98
160
|
title: `MCPico: ${groupName}`,
|
|
@@ -104,30 +166,7 @@ export async function startServer(config) {
|
|
|
104
166
|
`${toolCount} subcommand${toolCount === 1 ? "" : "s"} available.`),
|
|
105
167
|
},
|
|
106
168
|
}, async (args) => {
|
|
107
|
-
|
|
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
|
-
}
|
|
169
|
+
return handleToolCall(args.command, group, servers, helpText);
|
|
131
170
|
});
|
|
132
171
|
console.error(` Registered group: ${groupName} (${toolCount} tools)`);
|
|
133
172
|
}
|
|
@@ -189,10 +228,29 @@ export async function startServer(config) {
|
|
|
189
228
|
if (promptCount > 0) {
|
|
190
229
|
console.error(` Registered ${promptCount} prompt(s)`);
|
|
191
230
|
}
|
|
192
|
-
// Connect to client via
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
// Connect to client via configured transport
|
|
232
|
+
const listenConfig = config.listen || { type: "stdio" };
|
|
233
|
+
if (listenConfig.type === "sse") {
|
|
234
|
+
const transport = new StreamableHTTPServerTransport();
|
|
235
|
+
const httpServer = createServer(async (req, res) => {
|
|
236
|
+
// Collect body for handleRequest
|
|
237
|
+
const chunks = [];
|
|
238
|
+
for await (const chunk of req) {
|
|
239
|
+
chunks.push(chunk);
|
|
240
|
+
}
|
|
241
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : undefined;
|
|
242
|
+
await transport.handleRequest(req, res, body);
|
|
243
|
+
});
|
|
244
|
+
await server.connect(transport);
|
|
245
|
+
const host = listenConfig.host || "localhost";
|
|
246
|
+
httpServer.listen(listenConfig.port, host);
|
|
247
|
+
console.error(`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s) — listening on http://${host}:${listenConfig.port}`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const transport = new StdioServerTransport();
|
|
251
|
+
await server.connect(transport);
|
|
252
|
+
console.error(`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s)`);
|
|
253
|
+
}
|
|
196
254
|
// Handle shutdown
|
|
197
255
|
let shuttingDown = false;
|
|
198
256
|
const shutdown = async () => {
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpico",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCPico — MCP proxy that bundles flat tool lists into hierarchical subcommand groups. Pico footprint, maximum discovery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,11 +24,20 @@
|
|
|
24
24
|
"subcommand"
|
|
25
25
|
],
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/lxg2it/mcpico"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/lxg2it/mcpico#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/lxg2it/mcpico/issues"
|
|
34
|
+
},
|
|
27
35
|
"dependencies": {
|
|
28
36
|
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
29
37
|
},
|
|
30
38
|
"devDependencies": {
|
|
31
39
|
"@types/node": "^22.0.0",
|
|
40
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
32
41
|
"tsx": "^4.19.0",
|
|
33
42
|
"typescript": "^5.8.0",
|
|
34
43
|
"vitest": "^4.1.9"
|
package/src/config.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface GroupOverrides {
|
|
|
34
34
|
[groupName: string]: string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Transport to expose MCPico itself to clients.
|
|
39
|
+
*/
|
|
40
|
+
export type ListenConfig =
|
|
41
|
+
| { type: "stdio" }
|
|
42
|
+
| { type: "sse"; port: number; host?: string };
|
|
43
|
+
|
|
37
44
|
/**
|
|
38
45
|
* Full MCPico configuration.
|
|
39
46
|
*/
|
|
@@ -44,6 +51,8 @@ export interface MCPicoConfig {
|
|
|
44
51
|
separator?: string;
|
|
45
52
|
/** Explicit group overrides — tools not listed here are auto-grouped */
|
|
46
53
|
groups?: GroupOverrides;
|
|
54
|
+
/** How MCPico exposes itself to clients (default: stdio) */
|
|
55
|
+
listen?: ListenConfig;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
package/src/help.test.ts
CHANGED
|
@@ -194,4 +194,44 @@ describe("generateHelpText", () => {
|
|
|
194
194
|
expect(text).toContain(tool.name);
|
|
195
195
|
// Should not crash
|
|
196
196
|
});
|
|
197
|
+
|
|
198
|
+
it("handles non-standard property types gracefully", () => {
|
|
199
|
+
const tool = makeTool({
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
weird: { type: null, description: "Unknown type" },
|
|
204
|
+
},
|
|
205
|
+
required: ["weird"],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
const text = generateHelpText(makeGroup({ tools: [tool] }));
|
|
209
|
+
// Should not crash — fallback to "any"
|
|
210
|
+
expect(text).toContain("(any");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("falls back to bare subcommand for very long examples", () => {
|
|
214
|
+
// Three required params with long names → JSON.stringify > 80 chars
|
|
215
|
+
const tool = makeTool({
|
|
216
|
+
name: "do_something",
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
configuration_file_path: { type: "string", description: "Long param" },
|
|
221
|
+
output_destination_directory: { type: "string", description: "Another long one" },
|
|
222
|
+
encryption_algorithm_identifier: { type: "string", description: "Third long one" },
|
|
223
|
+
},
|
|
224
|
+
required: [
|
|
225
|
+
"configuration_file_path",
|
|
226
|
+
"output_destination_directory",
|
|
227
|
+
"encryption_algorithm_identifier",
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const text = generateHelpText(makeGroup({ tools: [tool] }));
|
|
232
|
+
// Example should just be the bare subcommand name (long args overflow)
|
|
233
|
+
// (param names appear in Parameters section too, so just check the Example line)
|
|
234
|
+
const exampleLine = text.split("\n").find((l) => l.trim().startsWith("Example:"));
|
|
235
|
+
expect(exampleLine).toBe(" Example: do_something");
|
|
236
|
+
});
|
|
197
237
|
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { textResult, mergeGroups, handleToolCall, validateAllServerConfigs, buildGroupDescription } from "./server.js";
|
|
3
|
+
import type { ToolGroup } from "./grouper.js";
|
|
4
|
+
import type { DiscoveredServer, UpstreamTool } from "./discoverer.js";
|
|
5
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
|
|
7
|
+
function makeTool(overrides: Partial<UpstreamTool> = {}): UpstreamTool {
|
|
8
|
+
return {
|
|
9
|
+
name: "test_tool",
|
|
10
|
+
description: "A test tool",
|
|
11
|
+
inputSchema: { type: "object", properties: {} },
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeGroup(overrides: Partial<ToolGroup> = {}): ToolGroup {
|
|
17
|
+
return {
|
|
18
|
+
groupName: "test_group",
|
|
19
|
+
serverName: "test-server",
|
|
20
|
+
tools: [makeTool()],
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeServer(overrides: Partial<DiscoveredServer> = {}): DiscoveredServer {
|
|
26
|
+
return {
|
|
27
|
+
name: "test-server",
|
|
28
|
+
tools: [makeTool()],
|
|
29
|
+
resources: [],
|
|
30
|
+
prompts: [],
|
|
31
|
+
client: {} as any,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeTextResult(text: string): CallToolResult {
|
|
37
|
+
return { content: [{ type: "text", text }] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("textResult", () => {
|
|
41
|
+
it("wraps text in content array", () => {
|
|
42
|
+
const result = textResult("hello");
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
content: [{ type: "text", text: "hello" }],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles empty string", () => {
|
|
49
|
+
const result = textResult("");
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
content: [{ type: "text", text: "" }],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("mergeGroups", () => {
|
|
57
|
+
it("returns empty map for empty input", () => {
|
|
58
|
+
const result = mergeGroups([]);
|
|
59
|
+
expect(result.size).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("preserves single group", () => {
|
|
63
|
+
const group = makeGroup({ groupName: "filesystem" });
|
|
64
|
+
const result = mergeGroups([group]);
|
|
65
|
+
expect(result.size).toBe(1);
|
|
66
|
+
const entry = result.get("filesystem")!;
|
|
67
|
+
expect(entry.groupName).toBe("filesystem");
|
|
68
|
+
expect(entry.tools).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps different groups separate", () => {
|
|
72
|
+
const groups = [
|
|
73
|
+
makeGroup({ groupName: "filesystem" }),
|
|
74
|
+
makeGroup({ groupName: "database" }),
|
|
75
|
+
];
|
|
76
|
+
const result = mergeGroups(groups);
|
|
77
|
+
expect(result.size).toBe(2);
|
|
78
|
+
expect(result.has("filesystem")).toBe(true);
|
|
79
|
+
expect(result.has("database")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("merges groups with same name across servers", () => {
|
|
83
|
+
const groups = [
|
|
84
|
+
makeGroup({
|
|
85
|
+
groupName: "filesystem",
|
|
86
|
+
serverName: "server-a",
|
|
87
|
+
tools: [makeTool({ name: "read" })],
|
|
88
|
+
}),
|
|
89
|
+
makeGroup({
|
|
90
|
+
groupName: "filesystem",
|
|
91
|
+
serverName: "server-b",
|
|
92
|
+
tools: [makeTool({ name: "write" })],
|
|
93
|
+
}),
|
|
94
|
+
];
|
|
95
|
+
const result = mergeGroups(groups);
|
|
96
|
+
expect(result.size).toBe(1);
|
|
97
|
+
const merged = result.get("filesystem")!;
|
|
98
|
+
expect(merged.tools).toHaveLength(2);
|
|
99
|
+
expect(merged.tools.map((t) => t.name)).toEqual(["read", "write"]);
|
|
100
|
+
expect(merged.serverName).toContain("server-a");
|
|
101
|
+
expect(merged.serverName).toContain("server-b");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not duplicate server name when already present", () => {
|
|
105
|
+
const groups = [
|
|
106
|
+
makeGroup({
|
|
107
|
+
groupName: "filesystem",
|
|
108
|
+
serverName: "server-a + server-b",
|
|
109
|
+
tools: [makeTool({ name: "read" })],
|
|
110
|
+
}),
|
|
111
|
+
makeGroup({
|
|
112
|
+
groupName: "filesystem",
|
|
113
|
+
serverName: "server-b",
|
|
114
|
+
tools: [makeTool({ name: "write" })],
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
const result = mergeGroups(groups);
|
|
118
|
+
const merged = result.get("filesystem")!;
|
|
119
|
+
// server-b is already in "server-a + server-b", should not be added again
|
|
120
|
+
expect(merged.serverName).toBe("server-a + server-b");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("validateAllServerConfigs", () => {
|
|
125
|
+
it("returns empty for valid configs", () => {
|
|
126
|
+
const errors = validateAllServerConfigs([
|
|
127
|
+
{ name: "server-a", transport: { type: "stdio" as const, command: "echo" } },
|
|
128
|
+
]);
|
|
129
|
+
expect(errors).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns errors for invalid configs", () => {
|
|
133
|
+
const errors = validateAllServerConfigs([
|
|
134
|
+
{ name: "", transport: { type: "stdio" as const, command: "" } },
|
|
135
|
+
]);
|
|
136
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("validates multiple configs and collects all errors", () => {
|
|
140
|
+
const errors = validateAllServerConfigs([
|
|
141
|
+
{ name: "", transport: { type: "stdio" as const, command: "" } },
|
|
142
|
+
{ name: "missing-transport" } as any,
|
|
143
|
+
]);
|
|
144
|
+
expect(errors.length).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles empty servers array", () => {
|
|
148
|
+
const errors = validateAllServerConfigs([]);
|
|
149
|
+
expect(errors).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("buildGroupDescription", () => {
|
|
154
|
+
it("includes group name", () => {
|
|
155
|
+
const desc = buildGroupDescription(makeGroup({ groupName: "filesystem" }));
|
|
156
|
+
expect(desc).toContain("MCPico filesystem");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("shows singular for 1 tool", () => {
|
|
160
|
+
const desc = buildGroupDescription(makeGroup());
|
|
161
|
+
expect(desc).toContain("1 tool");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("shows plural for multiple tools", () => {
|
|
165
|
+
const group = makeGroup({
|
|
166
|
+
tools: [makeTool({ name: "a" }), makeTool({ name: "b" })],
|
|
167
|
+
});
|
|
168
|
+
const desc = buildGroupDescription(group);
|
|
169
|
+
expect(desc).toContain("2 tools");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("includes server name", () => {
|
|
173
|
+
const desc = buildGroupDescription(makeGroup({ serverName: "my-upstream" }));
|
|
174
|
+
expect(desc).toContain("Source: my-upstream");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("includes usage hints", () => {
|
|
178
|
+
const desc = buildGroupDescription(makeGroup());
|
|
179
|
+
expect(desc).toContain("Use 'help'");
|
|
180
|
+
expect(desc).toContain("<subcommand>");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("handleToolCall", () => {
|
|
185
|
+
const servers = [makeServer()];
|
|
186
|
+
const group = makeGroup();
|
|
187
|
+
const helpText = "Help text here";
|
|
188
|
+
|
|
189
|
+
it("returns help for 'help' command", async () => {
|
|
190
|
+
const result = await handleToolCall("help", group, servers, helpText);
|
|
191
|
+
expect(result).toEqual(makeTextResult(helpText));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("returns help for empty command", async () => {
|
|
195
|
+
const result = await handleToolCall("", group, servers, helpText);
|
|
196
|
+
expect(result).toEqual(makeTextResult(helpText));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns help for whitespace command", async () => {
|
|
200
|
+
const result = await handleToolCall(" ", group, servers, helpText);
|
|
201
|
+
expect(result).toEqual(makeTextResult(helpText));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns parse error for invalid JSON args", async () => {
|
|
205
|
+
const result = await handleToolCall(
|
|
206
|
+
"test_tool {bad json}",
|
|
207
|
+
group,
|
|
208
|
+
servers,
|
|
209
|
+
helpText
|
|
210
|
+
);
|
|
211
|
+
expect(result.content?.[0]).toBeDefined();
|
|
212
|
+
if (result.content?.[0] && "text" in result.content[0]) {
|
|
213
|
+
expect(result.content[0].text).toContain("Could not parse arguments as JSON");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns error for unknown subcommand", async () => {
|
|
218
|
+
const result = await handleToolCall(
|
|
219
|
+
"nonexistent",
|
|
220
|
+
group,
|
|
221
|
+
servers,
|
|
222
|
+
helpText
|
|
223
|
+
);
|
|
224
|
+
expect(result.content?.[0]).toBeDefined();
|
|
225
|
+
if (result.content?.[0] && "text" in result.content[0]) {
|
|
226
|
+
expect(result.content[0].text).toContain('Unknown subcommand: "nonexistent"');
|
|
227
|
+
expect(result.content[0].text).toContain("test_tool");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("returns internal error when server not found for valid tool", async () => {
|
|
232
|
+
// Tool exists in group but no server has it
|
|
233
|
+
const orphanTool = makeTool({ name: "orphan_tool" });
|
|
234
|
+
const orphanGroup = makeGroup({ tools: [orphanTool] });
|
|
235
|
+
const emptyServers: DiscoveredServer[] = [];
|
|
236
|
+
|
|
237
|
+
const result = await handleToolCall(
|
|
238
|
+
"orphan_tool",
|
|
239
|
+
orphanGroup,
|
|
240
|
+
emptyServers,
|
|
241
|
+
helpText
|
|
242
|
+
);
|
|
243
|
+
expect(result.content?.[0]).toBeDefined();
|
|
244
|
+
if (result.content?.[0] && "text" in result.content[0]) {
|
|
245
|
+
expect(result.content[0].text).toContain("Internal error");
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("dispatches to forwardFn for valid tool", async () => {
|
|
250
|
+
const forwardFn = vi.fn().mockResolvedValue(makeTextResult("forwarded!"));
|
|
251
|
+
|
|
252
|
+
const result = await handleToolCall(
|
|
253
|
+
"test_tool",
|
|
254
|
+
group,
|
|
255
|
+
servers,
|
|
256
|
+
helpText,
|
|
257
|
+
forwardFn
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(forwardFn).toHaveBeenCalledTimes(1);
|
|
261
|
+
expect(forwardFn).toHaveBeenCalledWith(servers[0], "test_tool", {});
|
|
262
|
+
expect(result).toEqual(makeTextResult("forwarded!"));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("passes parsed args to forwardFn", async () => {
|
|
266
|
+
const forwardFn = vi.fn().mockResolvedValue(makeTextResult("ok"));
|
|
267
|
+
|
|
268
|
+
await handleToolCall(
|
|
269
|
+
'test_tool {"key":"value","count":42}',
|
|
270
|
+
group,
|
|
271
|
+
servers,
|
|
272
|
+
helpText,
|
|
273
|
+
forwardFn
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(forwardFn).toHaveBeenCalledWith(servers[0], "test_tool", {
|
|
277
|
+
key: "value",
|
|
278
|
+
count: 42,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("returns error when forwardFn throws", async () => {
|
|
283
|
+
const forwardFn = vi.fn().mockRejectedValue(new Error("Connection lost"));
|
|
284
|
+
|
|
285
|
+
const result = await handleToolCall(
|
|
286
|
+
"test_tool",
|
|
287
|
+
group,
|
|
288
|
+
servers,
|
|
289
|
+
helpText,
|
|
290
|
+
forwardFn
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(result.content?.[0]).toBeDefined();
|
|
294
|
+
if (result.content?.[0] && "text" in result.content[0]) {
|
|
295
|
+
expect(result.content[0].text).toContain('Error calling "test_tool"');
|
|
296
|
+
expect(result.content[0].text).toContain("Connection lost");
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("finds correct server when multiple servers exist", async () => {
|
|
301
|
+
const serverA = makeServer({
|
|
302
|
+
name: "server-a",
|
|
303
|
+
tools: [makeTool({ name: "tool_a" })],
|
|
304
|
+
});
|
|
305
|
+
const serverB = makeServer({
|
|
306
|
+
name: "server-b",
|
|
307
|
+
tools: [makeTool({ name: "tool_b" })],
|
|
308
|
+
});
|
|
309
|
+
const multiGroup = makeGroup({
|
|
310
|
+
tools: [makeTool({ name: "tool_a" }), makeTool({ name: "tool_b" })],
|
|
311
|
+
});
|
|
312
|
+
const forwardFn = vi.fn().mockResolvedValue(makeTextResult("ok"));
|
|
313
|
+
|
|
314
|
+
await handleToolCall(
|
|
315
|
+
"tool_b",
|
|
316
|
+
multiGroup,
|
|
317
|
+
[serverA, serverB],
|
|
318
|
+
helpText,
|
|
319
|
+
forwardFn
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(forwardFn).toHaveBeenCalledWith(serverB, "tool_b", {});
|
|
323
|
+
});
|
|
324
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createServer } from "node:http";
|
|
3
5
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
6
|
import { z } from "zod/v4";
|
|
5
7
|
import type { MCPicoConfig } from "./config.js";
|
|
@@ -15,12 +17,121 @@ import { generateHelpText } from "./help.js";
|
|
|
15
17
|
import { forwardToolCall } from "./proxy.js";
|
|
16
18
|
|
|
17
19
|
/** Helper: create a simple text content result */
|
|
18
|
-
function textResult(text: string): CallToolResult {
|
|
20
|
+
export function textResult(text: string): CallToolResult {
|
|
19
21
|
return {
|
|
20
22
|
content: [{ type: "text" as const, text }],
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Merge tool groups from multiple upstream servers.
|
|
28
|
+
*
|
|
29
|
+
* Groups with the same name are merged:
|
|
30
|
+
* - Tools are concatenated
|
|
31
|
+
* - serverName is joined with " + "
|
|
32
|
+
*/
|
|
33
|
+
export function mergeGroups(allGroups: ToolGroup[]): Map<string, ToolGroup> {
|
|
34
|
+
const mergedGroups = new Map<string, ToolGroup>();
|
|
35
|
+
for (const group of allGroups) {
|
|
36
|
+
const existing = mergedGroups.get(group.groupName);
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.tools.push(...group.tools);
|
|
39
|
+
if (!existing.serverName.includes(group.serverName)) {
|
|
40
|
+
existing.serverName += ` + ${group.serverName}`;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
mergedGroups.set(group.groupName, { ...group });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return mergedGroups;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle a tool call command for a given group.
|
|
51
|
+
*
|
|
52
|
+
* Dispatches: help → error → unknown subcommand → forward to upstream server.
|
|
53
|
+
*
|
|
54
|
+
* Exported for testing; allows injecting a mock forwardFn to avoid
|
|
55
|
+
* connecting to real upstream servers.
|
|
56
|
+
*/
|
|
57
|
+
export async function handleToolCall(
|
|
58
|
+
command: string,
|
|
59
|
+
group: ToolGroup,
|
|
60
|
+
servers: DiscoveredServer[],
|
|
61
|
+
helpText: string,
|
|
62
|
+
forwardFn: (
|
|
63
|
+
server: DiscoveredServer,
|
|
64
|
+
toolName: string,
|
|
65
|
+
args: Record<string, unknown>
|
|
66
|
+
) => Promise<CallToolResult> = forwardToolCall
|
|
67
|
+
): Promise<CallToolResult> {
|
|
68
|
+
const parsed = parseCommand(command);
|
|
69
|
+
|
|
70
|
+
if (parsed.isHelp) {
|
|
71
|
+
return textResult(helpText);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (parsed.error) {
|
|
75
|
+
return textResult(parsed.error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toolName = parsed.subcommand!;
|
|
79
|
+
const upstreamTool = group.tools.find((t) => t.name === toolName);
|
|
80
|
+
if (!upstreamTool) {
|
|
81
|
+
const available = group.tools.map((t) => t.name).join(", ");
|
|
82
|
+
return textResult(
|
|
83
|
+
`Unknown subcommand: "${toolName}"\n\nAvailable in ${group.groupName}: ${available}\n\nUse 'help' for full documentation.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const serverForTool = servers.find((s) =>
|
|
88
|
+
s.tools.some((t) => t.name === toolName)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!serverForTool) {
|
|
92
|
+
return textResult(
|
|
93
|
+
`Internal error: could not find upstream server for tool "${toolName}"`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await forwardFn(serverForTool, toolName, parsed.args);
|
|
99
|
+
return result;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return textResult(
|
|
102
|
+
`Error calling "${toolName}": ${(err as Error).message}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build the description string for a tool group.
|
|
109
|
+
*/
|
|
110
|
+
export function buildGroupDescription(group: ToolGroup): string {
|
|
111
|
+
const toolCount = group.tools.length;
|
|
112
|
+
return [
|
|
113
|
+
`MCPico ${group.groupName} — ${toolCount} tool${toolCount === 1 ? "" : "s"}`,
|
|
114
|
+
`Source: ${group.serverName}`,
|
|
115
|
+
`Use 'help' to see all subcommands and their parameters.`,
|
|
116
|
+
`Format: <subcommand> {"key":"value",...}`,
|
|
117
|
+
].join(" | ");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate all server configs, returning an array of error messages.
|
|
122
|
+
* Returns empty array if all configs are valid.
|
|
123
|
+
*/
|
|
124
|
+
export function validateAllServerConfigs(servers: MCPicoConfig["servers"]): string[] {
|
|
125
|
+
const errors: string[] = [];
|
|
126
|
+
for (const serverConfig of servers) {
|
|
127
|
+
const err = validateServerConfig(serverConfig);
|
|
128
|
+
if (err) {
|
|
129
|
+
errors.push(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return errors;
|
|
133
|
+
}
|
|
134
|
+
|
|
24
135
|
/**
|
|
25
136
|
* Start the MCPico proxy server.
|
|
26
137
|
*
|
|
@@ -36,13 +147,7 @@ export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
|
36
147
|
const allGroups: ToolGroup[] = [];
|
|
37
148
|
|
|
38
149
|
// Validate server configs
|
|
39
|
-
const validationErrors
|
|
40
|
-
for (const serverConfig of config.servers) {
|
|
41
|
-
const err = validateServerConfig(serverConfig);
|
|
42
|
-
if (err) {
|
|
43
|
-
validationErrors.push(err);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
150
|
+
const validationErrors = validateAllServerConfigs(config.servers);
|
|
46
151
|
if (validationErrors.length > 0) {
|
|
47
152
|
console.error("Configuration errors:");
|
|
48
153
|
for (const err of validationErrors) {
|
|
@@ -96,18 +201,7 @@ export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
|
96
201
|
}
|
|
97
202
|
|
|
98
203
|
// Build lookup: groupName → ToolGroup (merged across servers)
|
|
99
|
-
const mergedGroups =
|
|
100
|
-
for (const group of allGroups) {
|
|
101
|
-
const existing = mergedGroups.get(group.groupName);
|
|
102
|
-
if (existing) {
|
|
103
|
-
existing.tools.push(...group.tools);
|
|
104
|
-
if (!existing.serverName.includes(group.serverName)) {
|
|
105
|
-
existing.serverName += ` + ${group.serverName}`;
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
mergedGroups.set(group.groupName, { ...group });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
204
|
+
const mergedGroups = mergeGroups(allGroups);
|
|
111
205
|
|
|
112
206
|
// Create the MCPico server
|
|
113
207
|
const server = new McpServer(
|
|
@@ -124,12 +218,7 @@ export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
|
124
218
|
// Register each group as a tool
|
|
125
219
|
for (const [groupName, group] of mergedGroups) {
|
|
126
220
|
const toolCount = group.tools.length;
|
|
127
|
-
const description =
|
|
128
|
-
`MCPico ${groupName} — ${toolCount} tool${toolCount === 1 ? "" : "s"}`,
|
|
129
|
-
`Source: ${group.serverName}`,
|
|
130
|
-
`Use 'help' to see all subcommands and their parameters.`,
|
|
131
|
-
`Format: <subcommand> {"key":"value",...}`,
|
|
132
|
-
].join(" | ");
|
|
221
|
+
const description = buildGroupDescription(group);
|
|
133
222
|
|
|
134
223
|
const helpText = generateHelpText(group);
|
|
135
224
|
|
|
@@ -148,47 +237,7 @@ export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
|
148
237
|
},
|
|
149
238
|
},
|
|
150
239
|
async (args: { command: string }) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (parsed.isHelp) {
|
|
154
|
-
return textResult(helpText);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (parsed.error) {
|
|
158
|
-
return textResult(parsed.error);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const toolName = parsed.subcommand!;
|
|
162
|
-
const upstreamTool = group.tools.find((t) => t.name === toolName);
|
|
163
|
-
if (!upstreamTool) {
|
|
164
|
-
const available = group.tools.map((t) => t.name).join(", ");
|
|
165
|
-
return textResult(
|
|
166
|
-
`Unknown subcommand: "${toolName}"\n\nAvailable in ${groupName}: ${available}\n\nUse 'help' for full documentation.`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const serverForTool = servers.find((s) =>
|
|
171
|
-
s.tools.some((t) => t.name === toolName)
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
if (!serverForTool) {
|
|
175
|
-
return textResult(
|
|
176
|
-
`Internal error: could not find upstream server for tool "${toolName}"`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const result = await forwardToolCall(
|
|
182
|
-
serverForTool,
|
|
183
|
-
toolName,
|
|
184
|
-
parsed.args
|
|
185
|
-
);
|
|
186
|
-
return result;
|
|
187
|
-
} catch (err) {
|
|
188
|
-
return textResult(
|
|
189
|
-
`Error calling "${toolName}": ${(err as Error).message}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
240
|
+
return handleToolCall(args.command, group, servers, helpText);
|
|
192
241
|
}
|
|
193
242
|
);
|
|
194
243
|
|
|
@@ -273,13 +322,33 @@ export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
|
273
322
|
console.error(` Registered ${promptCount} prompt(s)`);
|
|
274
323
|
}
|
|
275
324
|
|
|
276
|
-
// Connect to client via
|
|
277
|
-
const
|
|
278
|
-
await server.connect(transport);
|
|
325
|
+
// Connect to client via configured transport
|
|
326
|
+
const listenConfig = config.listen || { type: "stdio" };
|
|
279
327
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
328
|
+
if (listenConfig.type === "sse") {
|
|
329
|
+
const transport = new StreamableHTTPServerTransport();
|
|
330
|
+
const httpServer = createServer(async (req, res) => {
|
|
331
|
+
// Collect body for handleRequest
|
|
332
|
+
const chunks: Buffer[] = [];
|
|
333
|
+
for await (const chunk of req) {
|
|
334
|
+
chunks.push(chunk);
|
|
335
|
+
}
|
|
336
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : undefined;
|
|
337
|
+
await transport.handleRequest(req, res, body);
|
|
338
|
+
});
|
|
339
|
+
await server.connect(transport);
|
|
340
|
+
const host = listenConfig.host || "localhost";
|
|
341
|
+
httpServer.listen(listenConfig.port, host);
|
|
342
|
+
console.error(
|
|
343
|
+
`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s) — listening on http://${host}:${listenConfig.port}`
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
const transport = new StdioServerTransport();
|
|
347
|
+
await server.connect(transport);
|
|
348
|
+
console.error(
|
|
349
|
+
`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s)`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
283
352
|
|
|
284
353
|
// Handle shutdown
|
|
285
354
|
let shuttingDown = false;
|
package/vitest.config.ts
CHANGED
|
@@ -7,10 +7,13 @@ export default defineConfig({
|
|
|
7
7
|
include: ["src/**/*.ts"],
|
|
8
8
|
exclude: ["src/**/*.test.ts", "src/index.ts"],
|
|
9
9
|
thresholds: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// startServer() is I/O orchestration (connect/discover/register/transport)
|
|
11
|
+
// — requires integration tests, not unit tests. Pure logic functions are
|
|
12
|
+
// extracted and fully tested. SSE/HTTP transport code is also I/O.
|
|
13
|
+
statements: 67,
|
|
14
|
+
branches: 68,
|
|
15
|
+
functions: 80,
|
|
16
|
+
lines: 66,
|
|
14
17
|
},
|
|
15
18
|
},
|
|
16
19
|
},
|