curatedmcp 2.0.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/audit/catalog.d.ts +5 -0
  4. package/dist/audit/catalog.d.ts.map +1 -0
  5. package/dist/audit/catalog.js +69 -0
  6. package/dist/audit/index.d.ts +10 -0
  7. package/dist/audit/index.d.ts.map +1 -0
  8. package/dist/audit/index.js +32 -0
  9. package/dist/audit/report.d.ts +6 -0
  10. package/dist/audit/report.d.ts.map +1 -0
  11. package/dist/audit/report.js +79 -0
  12. package/dist/audit/risk.d.ts +3 -0
  13. package/dist/audit/risk.d.ts.map +1 -0
  14. package/dist/audit/risk.js +68 -0
  15. package/dist/audit/scanner.d.ts +8 -0
  16. package/dist/audit/scanner.d.ts.map +1 -0
  17. package/dist/audit/scanner.js +69 -0
  18. package/dist/audit/types.d.ts +29 -0
  19. package/dist/audit/types.d.ts.map +1 -0
  20. package/dist/audit/types.js +2 -0
  21. package/dist/auth.d.ts +23 -0
  22. package/dist/auth.d.ts.map +1 -0
  23. package/dist/auth.js +52 -0
  24. package/dist/cli/add.d.ts +18 -0
  25. package/dist/cli/add.d.ts.map +1 -0
  26. package/dist/cli/add.js +114 -0
  27. package/dist/cli/audit.d.ts +2 -0
  28. package/dist/cli/audit.d.ts.map +1 -0
  29. package/dist/cli/audit.js +58 -0
  30. package/dist/cli/guard.d.ts +2 -0
  31. package/dist/cli/guard.d.ts.map +1 -0
  32. package/dist/cli/guard.js +58 -0
  33. package/dist/cli/init.d.ts +2 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +44 -0
  36. package/dist/cli/list.d.ts +5 -0
  37. package/dist/cli/list.d.ts.map +1 -0
  38. package/dist/cli/list.js +33 -0
  39. package/dist/cli/login.d.ts +6 -0
  40. package/dist/cli/login.d.ts.map +1 -0
  41. package/dist/cli/login.js +43 -0
  42. package/dist/cli/remove.d.ts +6 -0
  43. package/dist/cli/remove.d.ts.map +1 -0
  44. package/dist/cli/remove.js +15 -0
  45. package/dist/cli/sync.d.ts +2 -0
  46. package/dist/cli/sync.d.ts.map +1 -0
  47. package/dist/cli/sync.js +104 -0
  48. package/dist/cli.d.ts +10 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +132 -0
  51. package/dist/guard/broker.d.ts +62 -0
  52. package/dist/guard/broker.d.ts.map +1 -0
  53. package/dist/guard/broker.js +147 -0
  54. package/dist/guard/dashboard.d.ts +14 -0
  55. package/dist/guard/dashboard.d.ts.map +1 -0
  56. package/dist/guard/dashboard.js +428 -0
  57. package/dist/guard/default-policy.json +33 -0
  58. package/dist/guard/index.d.ts +20 -0
  59. package/dist/guard/index.d.ts.map +1 -0
  60. package/dist/guard/index.js +61 -0
  61. package/dist/guard/logger.d.ts +30 -0
  62. package/dist/guard/logger.d.ts.map +1 -0
  63. package/dist/guard/logger.js +118 -0
  64. package/dist/guard/policy.d.ts +19 -0
  65. package/dist/guard/policy.d.ts.map +1 -0
  66. package/dist/guard/policy.js +108 -0
  67. package/dist/guard/proxy.d.ts +29 -0
  68. package/dist/guard/proxy.d.ts.map +1 -0
  69. package/dist/guard/proxy.js +109 -0
  70. package/dist/guard/types.d.ts +70 -0
  71. package/dist/guard/types.d.ts.map +1 -0
  72. package/dist/guard/types.js +2 -0
  73. package/dist/index.d.ts +3 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +259 -0
  76. package/dist/proxy.d.ts +122 -0
  77. package/dist/proxy.d.ts.map +1 -0
  78. package/dist/proxy.js +165 -0
  79. package/dist/stack.d.ts +45 -0
  80. package/dist/stack.d.ts.map +1 -0
  81. package/dist/stack.js +93 -0
  82. package/dist/telemetry.d.ts +15 -0
  83. package/dist/telemetry.d.ts.map +1 -0
  84. package/dist/telemetry.js +71 -0
  85. package/dist/tools/get-details.d.ts +14 -0
  86. package/dist/tools/get-details.d.ts.map +1 -0
  87. package/dist/tools/get-details.js +27 -0
  88. package/dist/tools/install.d.ts +2 -0
  89. package/dist/tools/install.d.ts.map +1 -0
  90. package/dist/tools/install.js +74 -0
  91. package/dist/tools/list-categories.d.ts +2 -0
  92. package/dist/tools/list-categories.d.ts.map +1 -0
  93. package/dist/tools/list-categories.js +13 -0
  94. package/dist/tools/search.d.ts +16 -0
  95. package/dist/tools/search.d.ts.map +1 -0
  96. package/dist/tools/search.js +17 -0
  97. package/package.json +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "module";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
+ import { searchServers } from "./tools/search.js";
7
+ import { getServerDetails } from "./tools/get-details.js";
8
+ import { installServer } from "./tools/install.js";
9
+ import { listCategories } from "./tools/list-categories.js";
10
+ import { Telemetry } from "./telemetry.js";
11
+ import { runCli, isCliInvocation } from "./cli.js";
12
+ import { Proxy } from "./proxy.js";
13
+ import { readStack } from "./stack.js";
14
+ import { addToStack } from "./cli/add.js";
15
+ const require = createRequire(import.meta.url);
16
+ const VERSION = require("../package.json").version;
17
+ // ─── CLI dispatch ────────────────────────────────────────────────────────────
18
+ // If invoked with subcommand args, run the CLI and exit.
19
+ // Otherwise, fall through to MCP server mode.
20
+ if (isCliInvocation(process.argv)) {
21
+ runCli(process.argv).then((code) => process.exit(code), (err) => {
22
+ console.error(err instanceof Error ? err.message : String(err));
23
+ process.exit(1);
24
+ });
25
+ }
26
+ else {
27
+ startMcpServer().catch((err) => {
28
+ console.error("[launcher] Fatal:", err);
29
+ process.exit(1);
30
+ });
31
+ }
32
+ // ─── MCP server (default mode when no CLI args) ──────────────────────────────
33
+ async function startMcpServer() {
34
+ const telemetry = new Telemetry();
35
+ const proxy = new Proxy();
36
+ // Load user's stack and lazy-spawn child MCP servers.
37
+ // Failures here are logged but don't crash launcher — user gets discovery tools at minimum.
38
+ const stack = readStack();
39
+ if (stack.entries.length > 0) {
40
+ await proxy.loadStack(stack).catch((err) => {
41
+ console.error("[launcher] Stack load error:", err);
42
+ });
43
+ }
44
+ const server = new Server({
45
+ name: "curatedmcp-launcher",
46
+ version: VERSION,
47
+ }, {
48
+ capabilities: {
49
+ tools: {},
50
+ },
51
+ });
52
+ // ─── tools/list: discovery + proxied tools ─────────────────────────────────
53
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
54
+ const proxiedTools = await proxy.aggregateTools();
55
+ return {
56
+ tools: [...DISCOVERY_TOOLS, ...proxiedTools],
57
+ };
58
+ });
59
+ // ─── tools/call: route by name ─────────────────────────────────────────────
60
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
61
+ const { name, arguments: args } = request.params;
62
+ const argObj = (args || {});
63
+ // Proxied tools have a "<slug>__<original>" prefix
64
+ if (name.includes("__")) {
65
+ try {
66
+ return await proxy.routeCall(name, argObj);
67
+ }
68
+ catch (err) {
69
+ return errorResponse(err);
70
+ }
71
+ }
72
+ // Discovery tools
73
+ try {
74
+ await telemetry.logEvent({
75
+ event: name.replace(/_/g, "-"),
76
+ slug: argObj.slug || null,
77
+ client: argObj.client || null,
78
+ query: argObj.query || null,
79
+ });
80
+ switch (name) {
81
+ case "search_servers": {
82
+ const results = await searchServers({
83
+ query: argObj.query,
84
+ category: argObj.category,
85
+ limit: argObj.limit,
86
+ });
87
+ const text = `Found ${results.length} MCP server(s):\n\n` +
88
+ results
89
+ .map((s) => `**${s.name}** (${s.category})\n` +
90
+ `${s.tagline}\n` +
91
+ `Pricing: ${s.pricing} | Rating: ${s.rating ?? "N/A"} | Downloads: ${s.downloads}\n` +
92
+ `Slug: \`${s.slug}\``)
93
+ .join("\n\n") +
94
+ `\n\nNext steps:\n` +
95
+ `• \`get_server_details\` for full info on any server\n` +
96
+ `• \`add_to_stack\` to add a server to this Launcher (becomes available in your AI client after restart)\n` +
97
+ `• \`install_server\` for the manual install snippet`;
98
+ return { content: [{ type: "text", text }] };
99
+ }
100
+ case "get_server_details": {
101
+ const details = await getServerDetails(argObj.slug);
102
+ const text = `# ${details.name}\n\n${details.description}\n\n` +
103
+ `**Pricing:** ${details.pricingType}\n` +
104
+ `**Category:** ${details.category}\n` +
105
+ `**Rating:** ${details.rating ?? "N/A"}\n` +
106
+ `**Downloads:** ${details.downloadCount}\n\n` +
107
+ `**Repository:** ${details.repo || "N/A"}\n` +
108
+ `**Docs:** ${details.docsUrl || "N/A"}\n\n` +
109
+ `Add this server to your Launcher stack with:\n` +
110
+ `\`add_to_stack\` (slug: "${details.slug}")\n` +
111
+ `Or get the manual install snippet with \`install_server\`.`;
112
+ return { content: [{ type: "text", text }] };
113
+ }
114
+ case "install_server": {
115
+ const config = await installServer(argObj.slug, (argObj.client || "claude"));
116
+ return { content: [{ type: "text", text: config }] };
117
+ }
118
+ case "list_categories": {
119
+ const categories = listCategories();
120
+ const text = `Available MCP server categories:\n\n${categories.map((c) => `• ${c}`).join("\n")}\n\n` +
121
+ `Use the category parameter in search_servers to filter results.`;
122
+ return { content: [{ type: "text", text }] };
123
+ }
124
+ case "add_to_stack": {
125
+ const slug = argObj.slug;
126
+ if (!slug)
127
+ throw new Error("slug is required");
128
+ const result = await addToStack(slug, {
129
+ env: argObj.env || {},
130
+ nonInteractive: true,
131
+ });
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: `✅ Added \`${slug}\` to your Launcher stack.\n\n` +
137
+ `${result.summary}\n\n` +
138
+ `**Restart your AI client** for the new tools to appear. ` +
139
+ `They'll be exposed as \`${slug}__<tool>\`.`,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ default:
145
+ return {
146
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
147
+ isError: true,
148
+ };
149
+ }
150
+ }
151
+ catch (err) {
152
+ return errorResponse(err);
153
+ }
154
+ });
155
+ const transport = new StdioServerTransport();
156
+ await server.connect(transport);
157
+ console.error(`[launcher] v${VERSION} ready (stack: ${stack.entries.length} server(s))`);
158
+ // Graceful shutdown
159
+ const shutdown = async () => {
160
+ await proxy.shutdown().catch(() => { });
161
+ process.exit(0);
162
+ };
163
+ process.on("SIGINT", shutdown);
164
+ process.on("SIGTERM", shutdown);
165
+ }
166
+ // ─── Static tool definitions ─────────────────────────────────────────────────
167
+ const DISCOVERY_TOOLS = [
168
+ {
169
+ name: "search_servers",
170
+ description: "Search the CuratedMCP catalog for MCP servers by keyword, category, or use case",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ query: {
175
+ type: "string",
176
+ description: "Search query (e.g. 'GitHub', 'database', 'Stripe')",
177
+ },
178
+ category: {
179
+ type: "string",
180
+ enum: [
181
+ "DEVELOPER_TOOLS",
182
+ "WEB_AUTOMATION",
183
+ "DATABASE",
184
+ "CLOUD_SERVICES",
185
+ "AI_AGENTS",
186
+ "PRODUCTIVITY",
187
+ "COMMUNICATION",
188
+ "ANALYTICS",
189
+ ],
190
+ description: "Filter by category",
191
+ },
192
+ limit: {
193
+ type: "number",
194
+ description: "Max results (default 10, max 50)",
195
+ },
196
+ },
197
+ required: ["query"],
198
+ },
199
+ },
200
+ {
201
+ name: "get_server_details",
202
+ description: "Get full details about a specific MCP server including install instructions",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ slug: { type: "string", description: "Server slug (from search results)" },
207
+ },
208
+ required: ["slug"],
209
+ },
210
+ },
211
+ {
212
+ name: "install_server",
213
+ description: "Get the manual install configuration snippet for an MCP server (use add_to_stack instead if you want it managed by Launcher)",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ slug: { type: "string", description: "Server slug" },
218
+ client: {
219
+ type: "string",
220
+ enum: ["claude", "cursor", "windsurf"],
221
+ description: "Target client (default: claude)",
222
+ },
223
+ },
224
+ required: ["slug"],
225
+ },
226
+ },
227
+ {
228
+ name: "list_categories",
229
+ description: "List all available MCP server categories",
230
+ inputSchema: { type: "object", properties: {} },
231
+ },
232
+ {
233
+ name: "add_to_stack",
234
+ description: "Add an MCP server to your Launcher stack so its tools become available through Launcher in every AI client. The server's tools appear as `<slug>__<tool>` after the AI client is restarted.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ slug: {
239
+ type: "string",
240
+ description: "Server slug from the CuratedMCP catalog (use search_servers to find one)",
241
+ },
242
+ env: {
243
+ type: "object",
244
+ description: "Environment variables for the server (e.g. API keys). Required env vars must be supplied here.",
245
+ additionalProperties: { type: "string" },
246
+ },
247
+ },
248
+ required: ["slug"],
249
+ },
250
+ },
251
+ ];
252
+ function errorResponse(err) {
253
+ const message = err instanceof Error ? err.message : String(err);
254
+ return {
255
+ content: [{ type: "text", text: `Error: ${message}` }],
256
+ isError: true,
257
+ };
258
+ }
259
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,122 @@
1
+ import type { Stack } from "./stack.js";
2
+ /** A tool, as returned by an MCP server's tools/list. */
3
+ interface McpTool {
4
+ name: string;
5
+ description?: string;
6
+ inputSchema: unknown;
7
+ }
8
+ export declare class Proxy {
9
+ private children;
10
+ /**
11
+ * Spawn each enabled child in parallel. A single failure is logged but
12
+ * doesn't abort the whole load — partial proxying is better than none.
13
+ */
14
+ loadStack(stack: Stack): Promise<void>;
15
+ private spawnChild;
16
+ /**
17
+ * Returns all tools from all healthy children, prefixed with `<slug>__`.
18
+ * Disabled or unhealthy children contribute nothing.
19
+ */
20
+ aggregateTools(): Promise<McpTool[]>;
21
+ /**
22
+ * Route a `<slug>__<tool>` call to the right child and return its response unchanged.
23
+ * Throws if the prefix doesn't match any known child or if the child is unhealthy.
24
+ */
25
+ routeCall(prefixedName: string, args: Record<string, unknown>): Promise<{
26
+ [x: string]: unknown;
27
+ content: ({
28
+ type: "text";
29
+ text: string;
30
+ annotations?: {
31
+ audience?: ("user" | "assistant")[] | undefined;
32
+ priority?: number | undefined;
33
+ lastModified?: string | undefined;
34
+ } | undefined;
35
+ _meta?: Record<string, unknown> | undefined;
36
+ } | {
37
+ type: "image";
38
+ data: string;
39
+ mimeType: string;
40
+ annotations?: {
41
+ audience?: ("user" | "assistant")[] | undefined;
42
+ priority?: number | undefined;
43
+ lastModified?: string | undefined;
44
+ } | undefined;
45
+ _meta?: Record<string, unknown> | undefined;
46
+ } | {
47
+ type: "audio";
48
+ data: string;
49
+ mimeType: string;
50
+ annotations?: {
51
+ audience?: ("user" | "assistant")[] | undefined;
52
+ priority?: number | undefined;
53
+ lastModified?: string | undefined;
54
+ } | undefined;
55
+ _meta?: Record<string, unknown> | undefined;
56
+ } | {
57
+ type: "resource";
58
+ resource: {
59
+ uri: string;
60
+ text: string;
61
+ mimeType?: string | undefined;
62
+ _meta?: Record<string, unknown> | undefined;
63
+ } | {
64
+ uri: string;
65
+ blob: string;
66
+ mimeType?: string | undefined;
67
+ _meta?: Record<string, unknown> | undefined;
68
+ };
69
+ annotations?: {
70
+ audience?: ("user" | "assistant")[] | undefined;
71
+ priority?: number | undefined;
72
+ lastModified?: string | undefined;
73
+ } | undefined;
74
+ _meta?: Record<string, unknown> | undefined;
75
+ } | {
76
+ uri: string;
77
+ name: string;
78
+ type: "resource_link";
79
+ description?: string | undefined;
80
+ mimeType?: string | undefined;
81
+ size?: number | undefined;
82
+ annotations?: {
83
+ audience?: ("user" | "assistant")[] | undefined;
84
+ priority?: number | undefined;
85
+ lastModified?: string | undefined;
86
+ } | undefined;
87
+ _meta?: {
88
+ [x: string]: unknown;
89
+ } | undefined;
90
+ icons?: {
91
+ src: string;
92
+ mimeType?: string | undefined;
93
+ sizes?: string[] | undefined;
94
+ theme?: "light" | "dark" | undefined;
95
+ }[] | undefined;
96
+ title?: string | undefined;
97
+ })[];
98
+ _meta?: {
99
+ [x: string]: unknown;
100
+ progressToken?: string | number | undefined;
101
+ "io.modelcontextprotocol/related-task"?: {
102
+ taskId: string;
103
+ } | undefined;
104
+ } | undefined;
105
+ structuredContent?: Record<string, unknown> | undefined;
106
+ isError?: boolean | undefined;
107
+ } | {
108
+ [x: string]: unknown;
109
+ toolResult: unknown;
110
+ _meta?: {
111
+ [x: string]: unknown;
112
+ progressToken?: string | number | undefined;
113
+ "io.modelcontextprotocol/related-task"?: {
114
+ taskId: string;
115
+ } | undefined;
116
+ } | undefined;
117
+ }>;
118
+ /** Cleanly close all child clients. Best-effort; never throws. */
119
+ shutdown(): Promise<void>;
120
+ }
121
+ export {};
122
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAC;AAIpD,yDAAyD;AACzD,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;CACtB;AAWD,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAiC;IAEjD;;;OAGG;IACG,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;YAK9B,UAAU;IA6CxB;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IA0B1C;;;OAGG;IACG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;uBAsE82U,CAAC;wBAA4B,CAAC;wBAAgE,CAAC;4BAAkD,CAAC;;iBAAkE,CAAC;;;;;uBAA+J,CAAC;wBAA4B,CAAC;wBAAgE,CAAC;4BAAkD,CAAC;;iBAAkE,CAAC;;;;;uBAA+J,CAAC;wBAA4B,CAAC;wBAAgE,CAAC;4BAAkD,CAAC;;iBAAkE,CAAC;;;;;;wBAA8L,CAAC;qBAA2C,CAAC;;;;wBAA4I,CAAC;qBAA2C,CAAC;;uBAA6E,CAAC;wBAA4B,CAAC;wBAAgE,CAAC;4BAAkD,CAAC;;iBAAkE,CAAC;;;;;uBAAkK,CAAC;oBAA0C,CAAC;gBAAsC,CAAC;uBAA6C,CAAC;wBAA4B,CAAC;wBAAgE,CAAC;4BAAkD,CAAC;;iBAAkE,CAAC;;;iBAAsF,CAAC;;wBAAyD,CAAC;qBAA2C,CAAC;qBAA6C,CAAC;;iBAA8E,CAAC;;;;yBAAiH,CAAC;kDAAiF,CAAC;;;;;;;;;;;yBAAgV,CAAC;kDAAiF,CAAC;;;;;IA5C58a,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAQhC"}
package/dist/proxy.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Proxy — spawns child MCP servers from the user's stack and aggregates their tools.
3
+ *
4
+ * Architecture:
5
+ * Agent (Claude/Cursor/...) --> Launcher (parent MCP server)
6
+ * |
7
+ * +--> child Client #1 -- stdio --> server #1 process
8
+ * +--> child Client #2 -- stdio --> server #2 process
9
+ * +--> ...
10
+ *
11
+ * Tool name routing:
12
+ * Each child's tools are exposed to the agent with a `<slug>__<original_name>` prefix.
13
+ * On tools/call, we strip the prefix and forward to the right child.
14
+ *
15
+ * Failure isolation:
16
+ * - Children are loaded lazily and in parallel; one failing to connect doesn't block others
17
+ * - On a child crash mid-session, the proxy marks it unhealthy and surfaces a clean error
18
+ * to the agent rather than crashing Launcher itself
19
+ * - Children with "disabled: true" in stack.json are skipped entirely
20
+ */
21
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
23
+ const PREFIX_SEPARATOR = "__";
24
+ export class Proxy {
25
+ constructor() {
26
+ this.children = new Map();
27
+ }
28
+ /**
29
+ * Spawn each enabled child in parallel. A single failure is logged but
30
+ * doesn't abort the whole load — partial proxying is better than none.
31
+ */
32
+ async loadStack(stack) {
33
+ const enabled = stack.entries.filter((e) => !e.disabled);
34
+ await Promise.all(enabled.map((entry) => this.spawnChild(entry)));
35
+ }
36
+ async spawnChild(entry) {
37
+ const transport = new StdioClientTransport({
38
+ command: entry.command,
39
+ args: entry.args,
40
+ env: entry.env ? { ...inheritedEnv(), ...entry.env } : undefined,
41
+ // Pipe child stderr through ours so server log lines aren't lost.
42
+ stderr: "pipe",
43
+ });
44
+ const client = new Client({ name: `curatedmcp-launcher-proxy:${entry.slug}`, version: "1.0.0" }, { capabilities: {} });
45
+ const state = {
46
+ entry,
47
+ client,
48
+ toolNames: new Set(),
49
+ };
50
+ this.children.set(entry.slug, state);
51
+ try {
52
+ await client.connect(transport);
53
+ // Pre-fetch tool list so aggregateTools() is fast and we know what we route.
54
+ const list = await client.listTools();
55
+ state.toolNames = new Set(list.tools.map((t) => t.name));
56
+ // Pipe child stderr to ours (best-effort) for visibility into upstream issues.
57
+ const childStderr = transport.stderr;
58
+ if (childStderr && typeof childStderr.on === "function") {
59
+ childStderr.on("data", (chunk) => {
60
+ process.stderr.write(`[${entry.slug}] ${chunk.toString()}`);
61
+ });
62
+ }
63
+ console.error(`[launcher] Proxied ${entry.slug} (${state.toolNames.size} tools)`);
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ state.error = `Failed to connect to ${entry.slug}: ${message}`;
68
+ console.error(`[launcher] ${state.error}`);
69
+ }
70
+ }
71
+ /**
72
+ * Returns all tools from all healthy children, prefixed with `<slug>__`.
73
+ * Disabled or unhealthy children contribute nothing.
74
+ */
75
+ async aggregateTools() {
76
+ const out = [];
77
+ for (const [slug, state] of this.children) {
78
+ if (state.error)
79
+ continue;
80
+ try {
81
+ const list = await state.client.listTools();
82
+ // Refresh cached tool names in case the child added/removed tools.
83
+ state.toolNames = new Set(list.tools.map((t) => t.name));
84
+ for (const t of list.tools) {
85
+ out.push({
86
+ name: `${slug}${PREFIX_SEPARATOR}${t.name}`,
87
+ description: t.description
88
+ ? `[${slug}] ${t.description}`
89
+ : `[${slug}] (proxied)`,
90
+ inputSchema: t.inputSchema,
91
+ });
92
+ }
93
+ }
94
+ catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ state.error = `listTools failed: ${message}`;
97
+ console.error(`[launcher] [${slug}] ${state.error}`);
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+ /**
103
+ * Route a `<slug>__<tool>` call to the right child and return its response unchanged.
104
+ * Throws if the prefix doesn't match any known child or if the child is unhealthy.
105
+ */
106
+ async routeCall(prefixedName, args) {
107
+ const sepIdx = prefixedName.indexOf(PREFIX_SEPARATOR);
108
+ if (sepIdx <= 0) {
109
+ throw new Error(`Tool name "${prefixedName}" is missing the "<slug>__" prefix.`);
110
+ }
111
+ const slug = prefixedName.slice(0, sepIdx);
112
+ const toolName = prefixedName.slice(sepIdx + PREFIX_SEPARATOR.length);
113
+ const state = this.children.get(slug);
114
+ if (!state) {
115
+ throw new Error(`Server "${slug}" is not in your stack. Run \`launcher list\` to see what's loaded.`);
116
+ }
117
+ if (state.error) {
118
+ throw new Error(state.error);
119
+ }
120
+ return state.client.callTool({
121
+ name: toolName,
122
+ arguments: args,
123
+ });
124
+ }
125
+ /** Cleanly close all child clients. Best-effort; never throws. */
126
+ async shutdown() {
127
+ await Promise.all(Array.from(this.children.values()).map((s) => s.client.close().catch(() => { })));
128
+ this.children.clear();
129
+ }
130
+ }
131
+ /**
132
+ * Minimal env passthrough. We don't want to leak the parent's full env to children
133
+ * because that often includes secrets meant for other tools — but the child needs
134
+ * the basics (PATH, HOME, etc.) to even spawn.
135
+ */
136
+ function inheritedEnv() {
137
+ const keys = [
138
+ "PATH",
139
+ "HOME",
140
+ "USER",
141
+ "USERNAME",
142
+ "USERPROFILE",
143
+ "APPDATA",
144
+ "LOCALAPPDATA",
145
+ "TEMP",
146
+ "TMP",
147
+ "LANG",
148
+ "LC_ALL",
149
+ "SHELL",
150
+ "TERM",
151
+ "PWD",
152
+ // Allow npx/uvx to find their caches without forcing global install
153
+ "NPM_CONFIG_CACHE",
154
+ "UV_CACHE_DIR",
155
+ "XDG_CACHE_HOME",
156
+ ];
157
+ const out = {};
158
+ for (const k of keys) {
159
+ const v = process.env[k];
160
+ if (v !== undefined)
161
+ out[k] = v;
162
+ }
163
+ return out;
164
+ }
165
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1,45 @@
1
+ export interface StackEntry {
2
+ /** CuratedMCP catalog slug (e.g. "github", "postgres"). */
3
+ slug: string;
4
+ /** Executable to spawn (e.g. "npx", "uvx", "node", "/usr/local/bin/foo"). */
5
+ command: string;
6
+ /** Args passed to the executable. */
7
+ args: string[];
8
+ /** Optional env vars (API keys, tokens, paths). */
9
+ env?: Record<string, string>;
10
+ /** When set, the entry is in stack.json but not loaded by the proxy. */
11
+ disabled?: boolean;
12
+ /** Free-form note (e.g. "Personal account"). */
13
+ note?: string;
14
+ /** Display name for `launcher list`. Falls back to slug. */
15
+ name?: string;
16
+ /** ISO timestamp of when this was added. */
17
+ addedAt?: string;
18
+ }
19
+ export interface Stack {
20
+ /** Schema version. Bump when the schema is not backward-compatible. */
21
+ version: 1;
22
+ entries: StackEntry[];
23
+ }
24
+ /** Returns the path to stack.json (so callers can show it to users). */
25
+ export declare function stackPath(): string;
26
+ /**
27
+ * Read the stack from disk.
28
+ * Returns an empty stack if the file is missing, unreadable, or malformed —
29
+ * so a corrupt config never crashes the launcher.
30
+ */
31
+ export declare function readStack(): Stack;
32
+ /**
33
+ * Atomically write the stack to disk.
34
+ * Writes to a temp sibling file, then renames — avoids a half-written file
35
+ * if the process is killed mid-write.
36
+ */
37
+ export declare function writeStack(stack: Stack): void;
38
+ /**
39
+ * Add or replace an entry by slug. If an entry with the same slug exists,
40
+ * it's overwritten (so calling `add` twice updates env vars cleanly).
41
+ */
42
+ export declare function upsertEntry(entry: StackEntry): Stack;
43
+ /** Remove an entry by slug. No-op (returns false) if it wasn't there. */
44
+ export declare function removeEntry(slug: string): boolean;
45
+ //# sourceMappingURL=stack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../src/stack.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,uEAAuE;IACvE,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAID,wEAAwE;AACxE,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,KAAK,CAgBjC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAO7C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAcpD;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOjD"}