mcpico 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +172 -0
  2. package/dist/config.d.ts +46 -0
  3. package/dist/config.js +32 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/config.test.d.ts +1 -0
  6. package/dist/config.test.js +87 -0
  7. package/dist/config.test.js.map +1 -0
  8. package/dist/discoverer.d.ts +52 -0
  9. package/dist/discoverer.js +84 -0
  10. package/dist/discoverer.js.map +1 -0
  11. package/dist/discoverer.test.d.ts +1 -0
  12. package/dist/discoverer.test.js +178 -0
  13. package/dist/discoverer.test.js.map +1 -0
  14. package/dist/grouper.d.ts +22 -0
  15. package/dist/grouper.js +70 -0
  16. package/dist/grouper.js.map +1 -0
  17. package/dist/grouper.test.d.ts +1 -0
  18. package/dist/grouper.test.js +169 -0
  19. package/dist/grouper.test.js.map +1 -0
  20. package/dist/help.d.ts +5 -0
  21. package/dist/help.js +115 -0
  22. package/dist/help.js.map +1 -0
  23. package/dist/help.test.d.ts +1 -0
  24. package/dist/help.test.js +173 -0
  25. package/dist/help.test.js.map +1 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +90 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/parser.d.ts +28 -0
  30. package/dist/parser.js +60 -0
  31. package/dist/parser.js.map +1 -0
  32. package/dist/parser.test.d.ts +1 -0
  33. package/dist/parser.test.js +142 -0
  34. package/dist/parser.test.js.map +1 -0
  35. package/dist/proxy.d.ts +6 -0
  36. package/dist/proxy.js +10 -0
  37. package/dist/proxy.js.map +1 -0
  38. package/dist/proxy.test.d.ts +1 -0
  39. package/dist/proxy.test.js +61 -0
  40. package/dist/proxy.test.js.map +1 -0
  41. package/dist/server.d.ts +11 -0
  42. package/dist/server.js +212 -0
  43. package/dist/server.js.map +1 -0
  44. package/mcplico.example.json +18 -0
  45. package/package.json +36 -0
  46. package/src/config.test.ts +96 -0
  47. package/src/config.ts +76 -0
  48. package/src/discoverer.test.ts +222 -0
  49. package/src/discoverer.ts +148 -0
  50. package/src/grouper.test.ts +202 -0
  51. package/src/grouper.ts +96 -0
  52. package/src/help.test.ts +197 -0
  53. package/src/help.ts +134 -0
  54. package/src/index.ts +106 -0
  55. package/src/parser.test.ts +173 -0
  56. package/src/parser.ts +78 -0
  57. package/src/proxy.test.ts +77 -0
  58. package/src/proxy.ts +16 -0
  59. package/src/server.ts +299 -0
  60. package/tsconfig.json +18 -0
  61. package/vitest.config.ts +17 -0
package/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
+ }
@@ -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
+ });