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/src/server.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import type { MCPicoConfig } from "./config.js";
|
|
6
|
+
import { validateServerConfig } from "./config.js";
|
|
7
|
+
import {
|
|
8
|
+
discoverServer,
|
|
9
|
+
disconnectServer,
|
|
10
|
+
type DiscoveredServer,
|
|
11
|
+
} from "./discoverer.js";
|
|
12
|
+
import { groupTools, type ToolGroup } from "./grouper.js";
|
|
13
|
+
import { parseCommand } from "./parser.js";
|
|
14
|
+
import { generateHelpText } from "./help.js";
|
|
15
|
+
import { forwardToolCall } from "./proxy.js";
|
|
16
|
+
|
|
17
|
+
/** Helper: create a simple text content result */
|
|
18
|
+
function textResult(text: string): CallToolResult {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text" as const, text }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the MCPico proxy server.
|
|
26
|
+
*
|
|
27
|
+
* 1. Validate config
|
|
28
|
+
* 2. Connect to all upstream servers
|
|
29
|
+
* 3. Discover and group their tools
|
|
30
|
+
* 4. Register grouped tools on the MCPico server
|
|
31
|
+
* 5. Listen for client connections
|
|
32
|
+
*/
|
|
33
|
+
export async function startServer(config: MCPicoConfig): Promise<void> {
|
|
34
|
+
const separator = config.separator || "_";
|
|
35
|
+
const servers: DiscoveredServer[] = [];
|
|
36
|
+
const allGroups: ToolGroup[] = [];
|
|
37
|
+
|
|
38
|
+
// Validate server configs
|
|
39
|
+
const validationErrors: string[] = [];
|
|
40
|
+
for (const serverConfig of config.servers) {
|
|
41
|
+
const err = validateServerConfig(serverConfig);
|
|
42
|
+
if (err) {
|
|
43
|
+
validationErrors.push(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (validationErrors.length > 0) {
|
|
47
|
+
console.error("Configuration errors:");
|
|
48
|
+
for (const err of validationErrors) {
|
|
49
|
+
console.error(` - ${err}`);
|
|
50
|
+
}
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Connect to all upstream servers and discover tools
|
|
55
|
+
console.error(
|
|
56
|
+
`MCPico starting with ${config.servers.length} upstream server(s)...`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
for (const serverConfig of config.servers) {
|
|
60
|
+
try {
|
|
61
|
+
const transportLabel =
|
|
62
|
+
serverConfig.transport.type === "sse"
|
|
63
|
+
? serverConfig.transport.url
|
|
64
|
+
: serverConfig.transport.command;
|
|
65
|
+
console.error(` Connecting to "${serverConfig.name}" (${serverConfig.transport.type}: ${transportLabel})...`);
|
|
66
|
+
const discovered = await discoverServer(
|
|
67
|
+
serverConfig.name,
|
|
68
|
+
serverConfig.transport,
|
|
69
|
+
serverConfig.connectTimeoutMs
|
|
70
|
+
);
|
|
71
|
+
servers.push(discovered);
|
|
72
|
+
|
|
73
|
+
const groups = groupTools(
|
|
74
|
+
serverConfig.name,
|
|
75
|
+
discovered.tools,
|
|
76
|
+
separator,
|
|
77
|
+
config.groups
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
console.error(
|
|
81
|
+
` → ${discovered.tools.length} tools → ${groups.length} groups: ${groups.map((g) => g.groupName).join(", ")}`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
allGroups.push(...groups);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(
|
|
87
|
+
` Failed to connect to "${serverConfig.name}":`,
|
|
88
|
+
(err as Error).message
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (servers.length === 0) {
|
|
94
|
+
console.error("No upstream servers connected. Exiting.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build lookup: groupName → ToolGroup (merged across servers)
|
|
99
|
+
const mergedGroups = new Map<string, ToolGroup>();
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create the MCPico server
|
|
113
|
+
const server = new McpServer(
|
|
114
|
+
{ name: "MCPico", version: "0.1.0" },
|
|
115
|
+
{
|
|
116
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
117
|
+
instructions:
|
|
118
|
+
"MCPico bundles upstream MCP tools into hierarchical groups. " +
|
|
119
|
+
"Use 'help' on any group to discover available subcommands. " +
|
|
120
|
+
'Format: <subcommand> {"key":"value",...}',
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Register each group as a tool
|
|
125
|
+
for (const [groupName, group] of mergedGroups) {
|
|
126
|
+
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(" | ");
|
|
133
|
+
|
|
134
|
+
const helpText = generateHelpText(group);
|
|
135
|
+
|
|
136
|
+
server.registerTool(
|
|
137
|
+
groupName,
|
|
138
|
+
{
|
|
139
|
+
title: `MCPico: ${groupName}`,
|
|
140
|
+
description,
|
|
141
|
+
inputSchema: {
|
|
142
|
+
command: z
|
|
143
|
+
.string()
|
|
144
|
+
.describe(
|
|
145
|
+
`Use 'help' for full docs or '<subcommand> {"key":"value",...}' to execute. ` +
|
|
146
|
+
`${toolCount} subcommand${toolCount === 1 ? "" : "s"} available.`
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
async (args: { command: string }) => {
|
|
151
|
+
const parsed = parseCommand(args.command);
|
|
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
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
console.error(` Registered group: ${groupName} (${toolCount} tools)`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pass through resources from all upstream servers
|
|
199
|
+
let resourceCount = 0;
|
|
200
|
+
for (const upstream of servers) {
|
|
201
|
+
for (const res of upstream.resources) {
|
|
202
|
+
const namespacedUri = `mcpico://${upstream.name}/${res.uri}`;
|
|
203
|
+
const displayName = `${upstream.name}: ${res.name}`;
|
|
204
|
+
|
|
205
|
+
server.registerResource(
|
|
206
|
+
displayName,
|
|
207
|
+
namespacedUri,
|
|
208
|
+
{
|
|
209
|
+
description: `${res.description || res.name} (from ${upstream.name})`,
|
|
210
|
+
mimeType: res.mimeType,
|
|
211
|
+
},
|
|
212
|
+
async () => {
|
|
213
|
+
const result = await upstream.client.readResource({ uri: res.uri });
|
|
214
|
+
return {
|
|
215
|
+
contents: result.contents || [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
resourceCount++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Pass through prompts from all upstream servers
|
|
224
|
+
let promptCount = 0;
|
|
225
|
+
for (const upstream of servers) {
|
|
226
|
+
for (const prompt of upstream.prompts) {
|
|
227
|
+
const namespacedName = `${upstream.name}_${prompt.name}`;
|
|
228
|
+
|
|
229
|
+
const argShape: Record<string, ReturnType<typeof z.string>> = {};
|
|
230
|
+
for (const arg of prompt.arguments || []) {
|
|
231
|
+
argShape[arg.name] = z
|
|
232
|
+
.string()
|
|
233
|
+
.describe(
|
|
234
|
+
arg.description ||
|
|
235
|
+
`Argument: ${arg.name}${arg.required ? " (required)" : ""}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
server.registerPrompt(
|
|
240
|
+
namespacedName,
|
|
241
|
+
{
|
|
242
|
+
title: `${upstream.name}: ${prompt.name}`,
|
|
243
|
+
description: `${prompt.description || ""} (from ${upstream.name})`,
|
|
244
|
+
argsSchema:
|
|
245
|
+
Object.keys(argShape).length > 0 ? argShape : undefined,
|
|
246
|
+
},
|
|
247
|
+
async (args) => {
|
|
248
|
+
const promptArgs: Record<string, string> = {};
|
|
249
|
+
if (args && typeof args === "object") {
|
|
250
|
+
for (const [k, v] of Object.entries(
|
|
251
|
+
args as Record<string, unknown>
|
|
252
|
+
)) {
|
|
253
|
+
promptArgs[k] = String(v);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const result = await upstream.client.getPrompt({
|
|
257
|
+
name: prompt.name,
|
|
258
|
+
arguments: promptArgs,
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
messages: result.messages || [],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
promptCount++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (resourceCount > 0) {
|
|
270
|
+
console.error(` Registered ${resourceCount} resource(s)`);
|
|
271
|
+
}
|
|
272
|
+
if (promptCount > 0) {
|
|
273
|
+
console.error(` Registered ${promptCount} prompt(s)`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Connect to client via stdio
|
|
277
|
+
const transport = new StdioServerTransport();
|
|
278
|
+
await server.connect(transport);
|
|
279
|
+
|
|
280
|
+
console.error(
|
|
281
|
+
`MCPico ready — ${mergedGroups.size} group(s), ${servers.length} upstream server(s)`
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Handle shutdown
|
|
285
|
+
let shuttingDown = false;
|
|
286
|
+
const shutdown = async () => {
|
|
287
|
+
if (shuttingDown) return;
|
|
288
|
+
shuttingDown = true;
|
|
289
|
+
console.error("Shutting down...");
|
|
290
|
+
for (const s of servers) {
|
|
291
|
+
await disconnectServer(s);
|
|
292
|
+
}
|
|
293
|
+
await server.close();
|
|
294
|
+
process.exit(0);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
process.on("SIGINT", shutdown);
|
|
298
|
+
process.on("SIGTERM", shutdown);
|
|
299
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["src/**/*.test.ts"]
|
|
18
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ["src/**/*.test.ts"],
|
|
6
|
+
coverage: {
|
|
7
|
+
include: ["src/**/*.ts"],
|
|
8
|
+
exclude: ["src/**/*.test.ts", "src/index.ts"],
|
|
9
|
+
thresholds: {
|
|
10
|
+
statements: 85,
|
|
11
|
+
branches: 80,
|
|
12
|
+
functions: 85,
|
|
13
|
+
lines: 85,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|