@drumcode/runner 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.
@@ -0,0 +1,632 @@
1
+ // src/runner.ts
2
+ import { CORE_TOOLSET, redactSecrets } from "@drumcode/core";
3
+ var DrumcodeRunner = class {
4
+ options;
5
+ toolCache = /* @__PURE__ */ new Map();
6
+ manifestCache = null;
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.log("info", "Drumcode Runner initialized", {
10
+ registryUrl: options.registryUrl,
11
+ cacheEnabled: options.cacheEnabled
12
+ });
13
+ }
14
+ // ---------------------------------------------------------------------------
15
+ // Tool Discovery
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Get the list of tools to expose to the client
19
+ * Uses Polyfill strategy for non-Anthropic clients
20
+ */
21
+ async getToolList(capabilities) {
22
+ if (capabilities.supportsAdvancedToolUse && capabilities.supportsDeferredLoading) {
23
+ this.log("debug", "Using Strategy A: Full manifest with deferred loading");
24
+ return this.getFullManifest();
25
+ }
26
+ this.log("debug", "Using Strategy B: Polyfill mode with core toolset");
27
+ return CORE_TOOLSET;
28
+ }
29
+ /**
30
+ * Fetch the full tool manifest from the Registry
31
+ */
32
+ async getFullManifest() {
33
+ if (this.manifestCache && this.options.cacheEnabled) {
34
+ return this.manifestCache;
35
+ }
36
+ try {
37
+ const response = await fetch(`${this.options.registryUrl}/v1/manifest`, {
38
+ headers: this.getAuthHeaders()
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`Registry returned ${response.status}`);
42
+ }
43
+ const data = await response.json();
44
+ this.manifestCache = data.tools;
45
+ return this.manifestCache;
46
+ } catch (error) {
47
+ this.log("warn", "Failed to fetch manifest, using cached or fallback", { error });
48
+ return this.manifestCache || CORE_TOOLSET;
49
+ }
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Tool Execution
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * Execute a tool call
56
+ */
57
+ async executeTool(request) {
58
+ const startTime = Date.now();
59
+ try {
60
+ if (request.name === "drumcode_search_tools") {
61
+ return this.handleSearchTools(request.arguments);
62
+ }
63
+ if (request.name === "drumcode_get_tool_schema") {
64
+ return this.handleGetToolSchema(request.arguments);
65
+ }
66
+ return this.executeRegularTool(request);
67
+ } catch (error) {
68
+ const latency = Date.now() - startTime;
69
+ this.log("error", "Tool execution failed", {
70
+ tool: request.name,
71
+ error,
72
+ latencyMs: latency
73
+ });
74
+ return {
75
+ content: [{
76
+ type: "text",
77
+ text: `Error executing tool: ${error instanceof Error ? error.message : "Unknown error"}`
78
+ }],
79
+ isError: true
80
+ };
81
+ }
82
+ }
83
+ /**
84
+ * Handle drumcode_search_tools meta-tool
85
+ */
86
+ async handleSearchTools(args) {
87
+ this.log("debug", "Searching tools", { query: args.query });
88
+ try {
89
+ const response = await fetch(`${this.options.registryUrl}/v1/tools/search`, {
90
+ method: "POST",
91
+ headers: {
92
+ ...this.getAuthHeaders(),
93
+ "Content-Type": "application/json"
94
+ },
95
+ body: JSON.stringify({
96
+ query: args.query,
97
+ project_token: this.options.token,
98
+ limit: 5
99
+ })
100
+ });
101
+ if (!response.ok) {
102
+ throw new Error(`Search failed with status ${response.status}`);
103
+ }
104
+ const data = await response.json();
105
+ const resultText = data.matches.length > 0 ? data.matches.map(
106
+ (m, i) => `${i + 1}. **${m.name}** (relevance: ${(m.relevance * 100).toFixed(0)}%)
107
+ ${m.description}`
108
+ ).join("\n\n") : "No matching tools found. Try a different search query.";
109
+ return {
110
+ content: [{
111
+ type: "text",
112
+ text: `Found ${data.matches.length} matching tools:
113
+
114
+ ${resultText}
115
+
116
+ Use \`drumcode_get_tool_schema\` to get detailed arguments for any of these tools.`
117
+ }]
118
+ };
119
+ } catch (error) {
120
+ this.log("error", "Search failed", { error });
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: `Search failed: ${error instanceof Error ? error.message : "Unknown error"}`
125
+ }],
126
+ isError: true
127
+ };
128
+ }
129
+ }
130
+ /**
131
+ * Handle drumcode_get_tool_schema meta-tool
132
+ */
133
+ async handleGetToolSchema(args) {
134
+ this.log("debug", "Fetching tool schema", { toolName: args.tool_name });
135
+ if (this.toolCache.has(args.tool_name)) {
136
+ const cached = this.toolCache.get(args.tool_name);
137
+ return {
138
+ content: [{
139
+ type: "text",
140
+ text: this.formatToolSchema(cached)
141
+ }]
142
+ };
143
+ }
144
+ try {
145
+ const response = await fetch(
146
+ `${this.options.registryUrl}/v1/tools/${encodeURIComponent(args.tool_name)}`,
147
+ { headers: this.getAuthHeaders() }
148
+ );
149
+ if (!response.ok) {
150
+ if (response.status === 404) {
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: `Tool "${args.tool_name}" not found. Use drumcode_search_tools to find available tools.`
155
+ }],
156
+ isError: true
157
+ };
158
+ }
159
+ throw new Error(`Failed to fetch schema: ${response.status}`);
160
+ }
161
+ const schema = await response.json();
162
+ const tool = {
163
+ name: schema.name,
164
+ description: schema.description,
165
+ input_schema: schema.input_schema
166
+ };
167
+ this.toolCache.set(args.tool_name, tool);
168
+ return {
169
+ content: [{
170
+ type: "text",
171
+ text: this.formatToolSchema(tool)
172
+ }]
173
+ };
174
+ } catch (error) {
175
+ this.log("error", "Schema fetch failed", { error });
176
+ return {
177
+ content: [{
178
+ type: "text",
179
+ text: `Failed to fetch schema: ${error instanceof Error ? error.message : "Unknown error"}`
180
+ }],
181
+ isError: true
182
+ };
183
+ }
184
+ }
185
+ /**
186
+ * Execute a regular (non-meta) tool
187
+ */
188
+ async executeRegularTool(request) {
189
+ if (request.name === "drumcode_echo") {
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: JSON.stringify(request.arguments, null, 2)
194
+ }]
195
+ };
196
+ }
197
+ if (request.name === "drumcode_http_get") {
198
+ const url = request.arguments.url;
199
+ try {
200
+ const response = await fetch(url);
201
+ const text = await response.text();
202
+ return {
203
+ content: [{
204
+ type: "text",
205
+ text: `Status: ${response.status}
206
+
207
+ ${text.slice(0, 5e3)}${text.length > 5e3 ? "...(truncated)" : ""}`
208
+ }]
209
+ };
210
+ } catch (error) {
211
+ return {
212
+ content: [{
213
+ type: "text",
214
+ text: `HTTP request failed: ${error instanceof Error ? error.message : "Unknown error"}`
215
+ }],
216
+ isError: true
217
+ };
218
+ }
219
+ }
220
+ if (request.name.startsWith("arxiv_")) {
221
+ return this.executeArxivTool(request);
222
+ }
223
+ return {
224
+ content: [{
225
+ type: "text",
226
+ text: `Tool "${request.name}" is not yet implemented. This tool needs to be fetched and executed via the Registry.`
227
+ }],
228
+ isError: true
229
+ };
230
+ }
231
+ /**
232
+ * Execute arXiv tools via the arXiv API
233
+ */
234
+ async executeArxivTool(request) {
235
+ const baseUrl = "http://export.arxiv.org/api/query";
236
+ try {
237
+ switch (request.name) {
238
+ case "arxiv_search_papers": {
239
+ const { query, max_results = 10, categories } = request.arguments;
240
+ let searchQuery = query;
241
+ if (categories && categories.length > 0) {
242
+ const catFilter = categories.map((c) => `cat:${c}`).join(" OR ");
243
+ searchQuery = `(${query}) AND (${catFilter})`;
244
+ }
245
+ const params = new URLSearchParams({
246
+ search_query: `all:${searchQuery}`,
247
+ start: "0",
248
+ max_results: String(Math.min(max_results, 20)),
249
+ sortBy: "submittedDate",
250
+ sortOrder: "descending"
251
+ });
252
+ const response = await fetch(`${baseUrl}?${params}`);
253
+ const xml = await response.text();
254
+ const papers = this.parseArxivResponse(xml);
255
+ return {
256
+ content: [{
257
+ type: "text",
258
+ text: `Found ${papers.length} papers:
259
+
260
+ ${papers.map(
261
+ (p, i) => `${i + 1}. **${p.title}**
262
+ ID: ${p.id}
263
+ Authors: ${p.authors}
264
+ Published: ${p.published}
265
+ ${p.summary.slice(0, 200)}...`
266
+ ).join("\n\n")}`
267
+ }]
268
+ };
269
+ }
270
+ case "arxiv_get_paper":
271
+ case "arxiv_get_abstract": {
272
+ const { paper_id } = request.arguments;
273
+ const params = new URLSearchParams({ id_list: paper_id });
274
+ const response = await fetch(`${baseUrl}?${params}`);
275
+ const xml = await response.text();
276
+ const papers = this.parseArxivResponse(xml);
277
+ if (papers.length === 0) {
278
+ return {
279
+ content: [{ type: "text", text: `Paper ${paper_id} not found.` }],
280
+ isError: true
281
+ };
282
+ }
283
+ const p = papers[0];
284
+ return {
285
+ content: [{
286
+ type: "text",
287
+ text: `**${p.title}**
288
+
289
+ ID: ${p.id}
290
+ Authors: ${p.authors}
291
+ Published: ${p.published}
292
+ Categories: ${p.categories}
293
+ PDF: ${p.pdf_url}
294
+
295
+ **Abstract:**
296
+ ${p.summary}`
297
+ }]
298
+ };
299
+ }
300
+ case "arxiv_search_by_author": {
301
+ const { author, max_results = 10 } = request.arguments;
302
+ const params = new URLSearchParams({
303
+ search_query: `au:${author}`,
304
+ start: "0",
305
+ max_results: String(Math.min(max_results, 20)),
306
+ sortBy: "submittedDate",
307
+ sortOrder: "descending"
308
+ });
309
+ const response = await fetch(`${baseUrl}?${params}`);
310
+ const xml = await response.text();
311
+ const papers = this.parseArxivResponse(xml);
312
+ return {
313
+ content: [{
314
+ type: "text",
315
+ text: `Found ${papers.length} papers by "${author}":
316
+
317
+ ${papers.map(
318
+ (p, i) => `${i + 1}. **${p.title}** (${p.id})
319
+ ${p.published}`
320
+ ).join("\n\n")}`
321
+ }]
322
+ };
323
+ }
324
+ case "arxiv_search_by_category": {
325
+ const { category, max_results = 10 } = request.arguments;
326
+ const params = new URLSearchParams({
327
+ search_query: `cat:${category}`,
328
+ start: "0",
329
+ max_results: String(Math.min(max_results, 20)),
330
+ sortBy: "submittedDate",
331
+ sortOrder: "descending"
332
+ });
333
+ const response = await fetch(`${baseUrl}?${params}`);
334
+ const xml = await response.text();
335
+ const papers = this.parseArxivResponse(xml);
336
+ return {
337
+ content: [{
338
+ type: "text",
339
+ text: `Found ${papers.length} recent papers in ${category}:
340
+
341
+ ${papers.map(
342
+ (p, i) => `${i + 1}. **${p.title}** (${p.id})
343
+ Authors: ${p.authors}
344
+ ${p.published}`
345
+ ).join("\n\n")}`
346
+ }]
347
+ };
348
+ }
349
+ default:
350
+ return {
351
+ content: [{
352
+ type: "text",
353
+ text: `arXiv tool "${request.name}" is not yet implemented.`
354
+ }],
355
+ isError: true
356
+ };
357
+ }
358
+ } catch (error) {
359
+ this.log("error", "arXiv API call failed", { error });
360
+ return {
361
+ content: [{
362
+ type: "text",
363
+ text: `arXiv API error: ${error instanceof Error ? error.message : "Unknown error"}`
364
+ }],
365
+ isError: true
366
+ };
367
+ }
368
+ }
369
+ /**
370
+ * Parse arXiv Atom XML response
371
+ */
372
+ parseArxivResponse(xml) {
373
+ const papers = [];
374
+ const entryRegex = /<entry>([\s\S]*?)<\/entry>/g;
375
+ let match;
376
+ while ((match = entryRegex.exec(xml)) !== null) {
377
+ const entry = match[1];
378
+ const getId = (s) => {
379
+ const m = s.match(/<id>(.*?)<\/id>/);
380
+ return m ? m[1].replace("http://arxiv.org/abs/", "") : "";
381
+ };
382
+ const getTitle = (s) => {
383
+ const m = s.match(/<title>([\s\S]*?)<\/title>/);
384
+ return m ? m[1].replace(/\s+/g, " ").trim() : "";
385
+ };
386
+ const getSummary = (s) => {
387
+ const m = s.match(/<summary>([\s\S]*?)<\/summary>/);
388
+ return m ? m[1].replace(/\s+/g, " ").trim() : "";
389
+ };
390
+ const getAuthors = (s) => {
391
+ const authors = [];
392
+ const authorRegex = /<author>\s*<name>(.*?)<\/name>/g;
393
+ let authorMatch;
394
+ while ((authorMatch = authorRegex.exec(s)) !== null) {
395
+ authors.push(authorMatch[1]);
396
+ }
397
+ return authors.join(", ");
398
+ };
399
+ const getPublished = (s) => {
400
+ const m = s.match(/<published>(.*?)<\/published>/);
401
+ return m ? m[1].split("T")[0] : "";
402
+ };
403
+ const getCategories = (s) => {
404
+ const cats = [];
405
+ const catRegex = /<category[^>]*term="([^"]+)"/g;
406
+ let catMatch;
407
+ while ((catMatch = catRegex.exec(s)) !== null) {
408
+ cats.push(catMatch[1]);
409
+ }
410
+ return cats.join(", ");
411
+ };
412
+ const getPdfUrl = (s) => {
413
+ const m = s.match(/<link[^>]*title="pdf"[^>]*href="([^"]+)"/);
414
+ return m ? m[1] : "";
415
+ };
416
+ papers.push({
417
+ id: getId(entry),
418
+ title: getTitle(entry),
419
+ summary: getSummary(entry),
420
+ authors: getAuthors(entry),
421
+ published: getPublished(entry),
422
+ categories: getCategories(entry),
423
+ pdf_url: getPdfUrl(entry)
424
+ });
425
+ }
426
+ return papers;
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Helpers
430
+ // ---------------------------------------------------------------------------
431
+ getAuthHeaders() {
432
+ const headers = {
433
+ "User-Agent": "drumcode-runner/0.1.0"
434
+ };
435
+ if (this.options.token) {
436
+ headers["Authorization"] = `Bearer ${this.options.token}`;
437
+ }
438
+ return headers;
439
+ }
440
+ formatToolSchema(tool) {
441
+ const props = tool.input_schema.properties;
442
+ const required = tool.input_schema.required || [];
443
+ const argsDescription = Object.entries(props).map(([name, prop]) => {
444
+ const req = required.includes(name) ? " (required)" : " (optional)";
445
+ return ` - ${name}${req}: ${prop.type}${prop.description ? ` - ${prop.description}` : ""}`;
446
+ }).join("\n");
447
+ return `## ${tool.name}
448
+
449
+ ${tool.description}
450
+
451
+ ### Arguments:
452
+ ${argsDescription || " No arguments required."}
453
+
454
+ ### Usage:
455
+ Call this tool with the arguments above to execute it.`;
456
+ }
457
+ log(level, message, data) {
458
+ const levels = ["debug", "info", "warn", "error"];
459
+ const currentLevel = levels.indexOf(this.options.logLevel);
460
+ const messageLevel = levels.indexOf(level);
461
+ if (messageLevel >= currentLevel) {
462
+ const safeData = data ? JSON.parse(redactSecrets(JSON.stringify(data))) : void 0;
463
+ console.error(`[drumcode] ${message}`, safeData ? safeData : "");
464
+ }
465
+ }
466
+ };
467
+
468
+ // src/server.ts
469
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
470
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
471
+ import {
472
+ CallToolRequestSchema,
473
+ ListToolsRequestSchema
474
+ } from "@modelcontextprotocol/sdk/types.js";
475
+ async function createServer(options) {
476
+ const runner = new DrumcodeRunner(options);
477
+ const server = new Server(
478
+ {
479
+ name: "drumcode",
480
+ version: "0.1.0"
481
+ },
482
+ {
483
+ capabilities: {
484
+ tools: {}
485
+ }
486
+ }
487
+ );
488
+ server.setRequestHandler(ListToolsRequestSchema, async (request) => {
489
+ const capabilities = detectClientCapabilities(request);
490
+ const tools = await runner.getToolList(capabilities);
491
+ const demoTools = [
492
+ {
493
+ name: "drumcode_echo",
494
+ description: "Echo back the input arguments. Useful for testing.",
495
+ input_schema: {
496
+ type: "object",
497
+ properties: {
498
+ message: {
499
+ type: "string",
500
+ description: "The message to echo back"
501
+ }
502
+ },
503
+ required: ["message"]
504
+ }
505
+ },
506
+ {
507
+ name: "drumcode_http_get",
508
+ description: "Make an HTTP GET request to a URL and return the response.",
509
+ input_schema: {
510
+ type: "object",
511
+ properties: {
512
+ url: {
513
+ type: "string",
514
+ description: "The URL to fetch"
515
+ }
516
+ },
517
+ required: ["url"]
518
+ }
519
+ }
520
+ ];
521
+ const arxivTools = [
522
+ {
523
+ name: "arxiv_search_papers",
524
+ description: "Search for academic papers on arXiv. Returns papers matching the query with optional filters.",
525
+ input_schema: {
526
+ type: "object",
527
+ properties: {
528
+ query: {
529
+ type: "string",
530
+ description: 'Search query keywords (e.g., "machine learning", "quantum computing")'
531
+ },
532
+ max_results: {
533
+ type: "number",
534
+ description: "Maximum number of results to return (1-20)"
535
+ },
536
+ categories: {
537
+ type: "string",
538
+ description: 'Comma-separated arXiv categories to filter by (e.g., "cs.AI,cs.LG")'
539
+ }
540
+ },
541
+ required: ["query"]
542
+ }
543
+ },
544
+ {
545
+ name: "arxiv_get_paper",
546
+ description: "Get detailed metadata and abstract for a specific paper by its arXiv ID.",
547
+ input_schema: {
548
+ type: "object",
549
+ properties: {
550
+ paper_id: {
551
+ type: "string",
552
+ description: 'The arXiv paper ID (e.g., "2301.07041" or "1706.03762")'
553
+ }
554
+ },
555
+ required: ["paper_id"]
556
+ }
557
+ },
558
+ {
559
+ name: "arxiv_search_by_author",
560
+ description: "Search for papers by a specific author on arXiv.",
561
+ input_schema: {
562
+ type: "object",
563
+ properties: {
564
+ author: {
565
+ type: "string",
566
+ description: "Author name to search for"
567
+ },
568
+ max_results: {
569
+ type: "number",
570
+ description: "Maximum number of results (1-20)"
571
+ }
572
+ },
573
+ required: ["author"]
574
+ }
575
+ },
576
+ {
577
+ name: "arxiv_search_by_category",
578
+ description: "Search for recent papers in a specific arXiv category.",
579
+ input_schema: {
580
+ type: "object",
581
+ properties: {
582
+ category: {
583
+ type: "string",
584
+ description: 'arXiv category (e.g., "cs.AI", "math.CO", "physics.hep-th")'
585
+ },
586
+ max_results: {
587
+ type: "number",
588
+ description: "Maximum number of results (1-20)"
589
+ }
590
+ },
591
+ required: ["category"]
592
+ }
593
+ }
594
+ ];
595
+ const allTools = [...tools, ...demoTools, ...arxivTools].map((t) => ({
596
+ name: t.name,
597
+ description: t.description,
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: t.input_schema.properties,
601
+ required: t.input_schema.required
602
+ }
603
+ }));
604
+ return { tools: allTools };
605
+ });
606
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
607
+ const { name, arguments: args } = request.params;
608
+ const result = await runner.executeTool({
609
+ name,
610
+ arguments: args ?? {}
611
+ });
612
+ return {
613
+ content: result.content,
614
+ isError: result.isError
615
+ };
616
+ });
617
+ const transport = new StdioServerTransport();
618
+ await server.connect(transport);
619
+ console.error("[drumcode] MCP Server started");
620
+ }
621
+ function detectClientCapabilities(_request) {
622
+ return {
623
+ type: "unknown",
624
+ supportsAdvancedToolUse: false,
625
+ supportsDeferredLoading: false
626
+ };
627
+ }
628
+
629
+ export {
630
+ DrumcodeRunner,
631
+ createServer
632
+ };