@utdk/mcp 0.1.0-dev.646adf4
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +373 -0
- package/README.md +81 -0
- package/dist/__tests__/auth.test.js +63 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/__tests__/loader.test.js +205 -0
- package/dist/__tests__/loader.test.js.map +1 -0
- package/dist/__tests__/search.test.js +204 -0
- package/dist/__tests__/search.test.js.map +1 -0
- package/dist/auth.js +82 -0
- package/dist/auth.js.map +1 -0
- package/dist/loader.js +211 -0
- package/dist/loader.js.map +1 -0
- package/dist/search.js +143 -0
- package/dist/search.js.map +1 -0
- package/dist/server.js +294 -0
- package/dist/server.js.map +1 -0
- package/package.json +60 -0
- package/src/__tests__/auth.test.ts +78 -0
- package/src/__tests__/loader.test.ts +264 -0
- package/src/__tests__/search.test.ts +243 -0
- package/src/auth.ts +111 -0
- package/src/loader.ts +309 -0
- package/src/search.ts +175 -0
- package/src/server.ts +362 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +9 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @utdk/mcp-server — Unified MCP server with dynamic tool loading.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* UTDK_PROVIDERS=github,slack npx @utdk/mcp-server
|
|
7
|
+
*
|
|
8
|
+
* MCP config:
|
|
9
|
+
* { "command": "npx", "args": ["@utdk/mcp-server"], "env": { "UTDK_PROVIDERS": "github" } }
|
|
10
|
+
*
|
|
11
|
+
* Environment variables:
|
|
12
|
+
* UTDK_PROVIDERS Comma-separated list of @utdk providers to load (required)
|
|
13
|
+
* UTDK_OTEL_EXPORTER Telemetry exporter: "otlp" | "console" | unset (noop)
|
|
14
|
+
* <PROVIDER>_TOKEN Bearer token for a provider (e.g. GITHUB_TOKEN)
|
|
15
|
+
* <PROVIDER>_SECRET_KEY API key for a provider (e.g. STRIPE_SECRET_KEY)
|
|
16
|
+
*
|
|
17
|
+
* Hot-reload:
|
|
18
|
+
* Send SIGHUP to reload provider list from the current UTDK_PROVIDERS env value.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
CallToolRequestSchema,
|
|
23
|
+
ListToolsRequestSchema,
|
|
24
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
26
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
executeTool,
|
|
30
|
+
initTelemetry,
|
|
31
|
+
loadProviders,
|
|
32
|
+
parseProviderNames,
|
|
33
|
+
} from "./loader.js";
|
|
34
|
+
import type { ProviderTool } from "./loader.js";
|
|
35
|
+
import { searchTools, groupTools } from "./search.js";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Schema normalization
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ensure the input schema is a valid MCP tool inputSchema (type: "object").
|
|
43
|
+
*/
|
|
44
|
+
function normalizeInputSchema(schema: Record<string, unknown>): {
|
|
45
|
+
type: "object";
|
|
46
|
+
properties?: Record<string, object>;
|
|
47
|
+
required?: string[];
|
|
48
|
+
} {
|
|
49
|
+
if (schema["type"] === "object") {
|
|
50
|
+
return {
|
|
51
|
+
type: "object",
|
|
52
|
+
...(schema["properties"]
|
|
53
|
+
? { properties: schema["properties"] as Record<string, object> }
|
|
54
|
+
: {}),
|
|
55
|
+
...(schema["required"] ? { required: schema["required"] as string[] } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
type: "object",
|
|
61
|
+
...(schema["properties"]
|
|
62
|
+
? { properties: schema["properties"] as Record<string, object> }
|
|
63
|
+
: {}),
|
|
64
|
+
...(schema["required"] ? { required: schema["required"] as string[] } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// search_tools and groupTools are imported from ./search.js above.
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// MCP Server factory
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a low-level MCP Server that serves tools from the given ProviderTool list.
|
|
76
|
+
* Uses raw request handlers for tools/list and tools/call to support dynamic,
|
|
77
|
+
* JSON-schema-based tool definitions without requiring Zod schemas.
|
|
78
|
+
*/
|
|
79
|
+
function createServer(tools: ProviderTool[]): Server {
|
|
80
|
+
const server = new Server(
|
|
81
|
+
{ name: "@utdk/mcp-server", version: "0.1.0" },
|
|
82
|
+
{ capabilities: { tools: {} } },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Build a lookup map for fast dispatch
|
|
86
|
+
const toolMap = new Map<string, ProviderTool>(tools.map((t) => [t.mcpName, t]));
|
|
87
|
+
|
|
88
|
+
// tools/list — return exactly the 4 meta-tools (no direct provider tool exposure)
|
|
89
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
90
|
+
tools: [
|
|
91
|
+
{
|
|
92
|
+
name: "list_tools",
|
|
93
|
+
description:
|
|
94
|
+
"List available tool names without full schemas. Pass an optional provider to filter results, or group_by to organize by category.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object" as const,
|
|
97
|
+
properties: {
|
|
98
|
+
provider: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Filter results to tools from this provider name (e.g. 'github')",
|
|
101
|
+
},
|
|
102
|
+
group_by: {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ["provider", "tag"],
|
|
105
|
+
description:
|
|
106
|
+
"Group results by 'provider' or by OpenAPI 'tag'. When omitted, returns a flat list of names.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "search_tools",
|
|
113
|
+
description:
|
|
114
|
+
"Find tools by keyword. Searches tool names, OpenAPI tags, and descriptions using TF-IDF relevance ranking so the LLM can locate relevant tools without browsing the full catalog.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object" as const,
|
|
117
|
+
properties: {
|
|
118
|
+
query: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: "Natural language or keyword search (all words must appear in name, tags, or description)",
|
|
121
|
+
},
|
|
122
|
+
provider: {
|
|
123
|
+
type: "string",
|
|
124
|
+
description: "Restrict search to tools from this provider name (e.g. 'github')",
|
|
125
|
+
},
|
|
126
|
+
limit: {
|
|
127
|
+
type: "number",
|
|
128
|
+
description: "Maximum number of results to return (default 10)",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["query"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "tool_info",
|
|
136
|
+
description:
|
|
137
|
+
"Get the full schema and metadata for a specific tool by name. Use this after list_tools or search_tools to retrieve the complete input schema before calling the tool.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object" as const,
|
|
140
|
+
properties: {
|
|
141
|
+
tool_name: {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "The MCP tool name (e.g. 'github__repos_list')",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
required: ["tool_name"],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "call_tool",
|
|
151
|
+
description:
|
|
152
|
+
"Execute any registered tool by name with the provided arguments. Use tool_info first to get the correct argument schema.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object" as const,
|
|
155
|
+
properties: {
|
|
156
|
+
tool_name: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "The MCP tool name to execute (e.g. 'github__repos_list')",
|
|
159
|
+
},
|
|
160
|
+
arguments: {
|
|
161
|
+
type: "object",
|
|
162
|
+
description: "Arguments to pass to the tool",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
required: ["tool_name", "arguments"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// tools/call — dispatch to the 4 meta-tools only
|
|
172
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
173
|
+
const toolName = request.params.name;
|
|
174
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
175
|
+
|
|
176
|
+
// list_tools meta-tool: enumerate provider tool names without schemas
|
|
177
|
+
if (toolName === "list_tools") {
|
|
178
|
+
const providerFilter =
|
|
179
|
+
typeof args["provider"] === "string" ? args["provider"] : undefined;
|
|
180
|
+
const groupBy =
|
|
181
|
+
args["group_by"] === "provider" || args["group_by"] === "tag"
|
|
182
|
+
? (args["group_by"] as "provider" | "tag")
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
const filtered = providerFilter
|
|
186
|
+
? tools.filter((t) => t.providerName === providerFilter)
|
|
187
|
+
: tools;
|
|
188
|
+
|
|
189
|
+
let result: unknown;
|
|
190
|
+
|
|
191
|
+
if (groupBy === "provider" || groupBy === "tag") {
|
|
192
|
+
result = groupTools(filtered, groupBy);
|
|
193
|
+
} else {
|
|
194
|
+
result = filtered.map((t) => t.mcpName);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// search_tools meta-tool: keyword search across tool names and descriptions
|
|
203
|
+
if (toolName === "search_tools") {
|
|
204
|
+
const query = typeof args["query"] === "string" ? args["query"] : "";
|
|
205
|
+
const providerFilter =
|
|
206
|
+
typeof args["provider"] === "string" ? args["provider"] : undefined;
|
|
207
|
+
const limit =
|
|
208
|
+
typeof args["limit"] === "number" && args["limit"] > 0 ? Math.floor(args["limit"]) : 10;
|
|
209
|
+
const results = searchTools(tools, query, providerFilter, limit);
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// tool_info meta-tool: return full schema and metadata for a specific tool
|
|
216
|
+
if (toolName === "tool_info") {
|
|
217
|
+
const requestedName = typeof args["tool_name"] === "string" ? args["tool_name"] : "";
|
|
218
|
+
const tool = toolMap.get(requestedName);
|
|
219
|
+
if (!tool) {
|
|
220
|
+
return {
|
|
221
|
+
isError: true,
|
|
222
|
+
content: [{ type: "text" as const, text: `Unknown tool: ${requestedName}` }],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const info = {
|
|
226
|
+
name: tool.mcpName,
|
|
227
|
+
description: tool.description,
|
|
228
|
+
provider: tool.providerName,
|
|
229
|
+
inputSchema: normalizeInputSchema(tool.inputSchema),
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text" as const, text: JSON.stringify(info, null, 2) }],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// call_tool meta-tool: execute a registered tool by name
|
|
237
|
+
if (toolName === "call_tool") {
|
|
238
|
+
const requestedName = typeof args["tool_name"] === "string" ? args["tool_name"] : "";
|
|
239
|
+
const toolArgs =
|
|
240
|
+
typeof args["arguments"] === "object" && args["arguments"] !== null
|
|
241
|
+
? (args["arguments"] as Record<string, unknown>)
|
|
242
|
+
: {};
|
|
243
|
+
|
|
244
|
+
const tool = toolMap.get(requestedName);
|
|
245
|
+
if (!tool) {
|
|
246
|
+
return {
|
|
247
|
+
isError: true,
|
|
248
|
+
content: [{ type: "text" as const, text: `Unknown tool: ${requestedName}` }],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result = await executeTool(tool, toolArgs);
|
|
254
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
255
|
+
return { content: [{ type: "text" as const, text }] };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
return {
|
|
259
|
+
isError: true,
|
|
260
|
+
content: [{ type: "text" as const, text: `Error calling ${requestedName}: ${message}` }],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
isError: true,
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text" as const,
|
|
270
|
+
text: `Unknown tool: ${toolName}. Use list_tools or search_tools to discover tools, then call_tool to execute them.`,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return server;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Hot-reload support
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Reload providers from the current UTDK_PROVIDERS env value.
|
|
285
|
+
* Returns the new tool list (the caller is responsible for reconnecting if needed).
|
|
286
|
+
*/
|
|
287
|
+
async function reloadProviders(): Promise<ProviderTool[]> {
|
|
288
|
+
const providerNames = parseProviderNames(process.env["UTDK_PROVIDERS"]);
|
|
289
|
+
process.stderr.write(
|
|
290
|
+
`[mcp-server] (Re)loading providers: ${providerNames.join(", ") || "(none)"}\n`,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const tools = await loadProviders(providerNames);
|
|
294
|
+
process.stderr.write(
|
|
295
|
+
`[mcp-server] Loaded ${tools.length} total tool(s) from ${providerNames.length} provider(s)\n`,
|
|
296
|
+
);
|
|
297
|
+
return tools;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Entry point
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
async function main(): Promise<void> {
|
|
305
|
+
await initTelemetry();
|
|
306
|
+
|
|
307
|
+
const providerNames = parseProviderNames(process.env["UTDK_PROVIDERS"]);
|
|
308
|
+
|
|
309
|
+
if (providerNames.length === 0) {
|
|
310
|
+
process.stderr.write(
|
|
311
|
+
"[mcp-server] Warning: UTDK_PROVIDERS is not set — starting with no tools.\n" +
|
|
312
|
+
"[mcp-server] Set UTDK_PROVIDERS=github,slack,stripe to load providers.\n",
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
`[mcp-server] Loading providers: ${providerNames.join(", ")}\n`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let tools = await loadProviders(providerNames);
|
|
321
|
+
process.stderr.write(
|
|
322
|
+
`[mcp-server] Ready with ${tools.length} tool(s). Starting MCP stdio server...\n`,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
let server = createServer(tools);
|
|
326
|
+
const transport = new StdioServerTransport();
|
|
327
|
+
|
|
328
|
+
// SIGHUP → hot-reload provider list.
|
|
329
|
+
// Because the MCP protocol is stateful (initialized per connection), we rebuild
|
|
330
|
+
// the internal request handler registry. The client sees updated tools on the
|
|
331
|
+
// next tools/list request without needing to reconnect.
|
|
332
|
+
process.on("SIGHUP", () => {
|
|
333
|
+
process.stderr.write("[mcp-server] SIGHUP received — reloading providers...\n");
|
|
334
|
+
reloadProviders()
|
|
335
|
+
.then((newTools) => {
|
|
336
|
+
tools = newTools;
|
|
337
|
+
// Rebuild server handlers in-place by re-registering on the existing server.
|
|
338
|
+
// The transport connection is preserved; only the handler registry changes.
|
|
339
|
+
server = createServer(newTools);
|
|
340
|
+
// Reconnect the new server to the existing transport so future messages
|
|
341
|
+
// are routed to the updated handler set.
|
|
342
|
+
server.connect(transport).catch((err) => {
|
|
343
|
+
process.stderr.write(`[mcp-server] Reconnect error after SIGHUP: ${err}\n`);
|
|
344
|
+
});
|
|
345
|
+
})
|
|
346
|
+
.catch((err) => {
|
|
347
|
+
process.stderr.write(`[mcp-server] Reload error: ${err}\n`);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
process.on("SIGTERM", () => {
|
|
352
|
+
process.stderr.write("[mcp-server] SIGTERM received — shutting down.\n");
|
|
353
|
+
process.exit(0);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
await server.connect(transport);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((err) => {
|
|
360
|
+
process.stderr.write(`[mcp-server] Fatal: ${err}\n`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"moduleResolution": "Bundler",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"declaration": false,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"resolveJsonModule": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts"],
|
|
12
|
+
"exclude": ["node_modules", "dist"]
|
|
13
|
+
}
|