mcpboot 0.1.1 → 0.1.3
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 +11 -5
- package/dist/index.js +347 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@ Options:
|
|
|
42
42
|
--port <number> HTTP server port (default: 8000)
|
|
43
43
|
--cache-dir <path> Cache directory (default: .mcpboot-cache)
|
|
44
44
|
--no-cache Disable caching, regenerate on every startup
|
|
45
|
-
--verbose Verbose logging
|
|
45
|
+
--verbose Verbose logging (structured JSON to stderr)
|
|
46
|
+
--log-file <path> Write full verbose log to file (JSON lines, untruncated)
|
|
46
47
|
--dry-run Show generation plan without starting server
|
|
47
48
|
```
|
|
48
49
|
|
|
@@ -87,18 +88,23 @@ mcpboot fetches the API docs from GitHub, uses the LLM to plan and compile 10 to
|
|
|
87
88
|
|
|
88
89
|
```
|
|
89
90
|
[mcpboot] Found 1 URL(s) in prompt
|
|
90
|
-
[mcpboot] Fetching https://raw.githubusercontent.com/HackerNews/API/HEAD/README.md
|
|
91
91
|
[mcpboot] Fetched 1 page(s)
|
|
92
|
-
[mcpboot] Whitelist: github.com, firebase.google.com, hacker-news.firebaseio.com, ...
|
|
93
92
|
[mcpboot] Cache miss — generating tools via LLM
|
|
94
93
|
[mcpboot] Plan: 10 tool(s)
|
|
95
|
-
[mcpboot] Compiling handler for tool: get_top_stories
|
|
96
|
-
...
|
|
97
94
|
[mcpboot] Compiled 10 handler(s)
|
|
98
95
|
[mcpboot] Listening on http://localhost:8100/mcp
|
|
99
96
|
[mcpboot] Serving 10 tool(s)
|
|
100
97
|
```
|
|
101
98
|
|
|
99
|
+
With `--verbose`, each step also emits structured JSON events to stderr — one JSON object per line — with timestamps, request correlation IDs, and detailed payloads:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{"ts":"...","event":"llm_call_start","req_id":"startup","call_id":1,"provider":"anthropic","model":"claude-haiku-4-5",...}
|
|
103
|
+
{"ts":"...","event":"llm_call_end","req_id":"startup","call_id":1,"elapsed_ms":2100,"prompt_tokens":1240,"completion_tokens":892,...}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use `--log-file mcpboot.log` to capture the full untruncated output (stderr truncates long strings to 500 chars).
|
|
107
|
+
|
|
102
108
|
**2. Test with the MCP Inspector:**
|
|
103
109
|
|
|
104
110
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function buildConfig(argv, pipeOverride) {
|
|
|
10
10
|
"--provider <name>",
|
|
11
11
|
"LLM provider: anthropic | openai",
|
|
12
12
|
"anthropic"
|
|
13
|
-
).option("--model <id>", "LLM model ID").option("--api-key <key>", "LLM API key").option("--port <number>", "HTTP server port", "8000").option("--cache-dir <path>", "Cache directory", ".mcpboot-cache").option("--no-cache", "Disable caching").option("--verbose", "Verbose logging", false).option(
|
|
13
|
+
).option("--model <id>", "LLM model ID").option("--api-key <key>", "LLM API key").option("--port <number>", "HTTP server port", "8000").option("--cache-dir <path>", "Cache directory", ".mcpboot-cache").option("--no-cache", "Disable caching").option("--verbose", "Verbose logging", false).option("--log-file <path>", "Write full verbose log to file (JSON lines)").option(
|
|
14
14
|
"--dry-run",
|
|
15
15
|
"Show generation plan without starting server",
|
|
16
16
|
false
|
|
@@ -89,23 +89,103 @@ function buildConfig(argv, pipeOverride) {
|
|
|
89
89
|
},
|
|
90
90
|
pipe,
|
|
91
91
|
dryRun: opts.dryRun ?? false,
|
|
92
|
-
verbose: opts.verbose ?? false
|
|
92
|
+
verbose: opts.verbose ?? false,
|
|
93
|
+
logFile: opts.logFile
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
// src/log.ts
|
|
98
|
+
import { appendFileSync, writeFileSync } from "node:fs";
|
|
97
99
|
var verboseEnabled = false;
|
|
100
|
+
var logFilePath;
|
|
101
|
+
var currentRequestId;
|
|
102
|
+
var stats = {
|
|
103
|
+
llmCalls: 0,
|
|
104
|
+
llmTotalMs: 0,
|
|
105
|
+
promptTokens: 0,
|
|
106
|
+
completionTokens: 0,
|
|
107
|
+
fetchCalls: 0,
|
|
108
|
+
fetchTotalMs: 0,
|
|
109
|
+
sandboxCalls: 0,
|
|
110
|
+
sandboxTotalMs: 0
|
|
111
|
+
};
|
|
98
112
|
function setVerbose(enabled) {
|
|
99
113
|
verboseEnabled = enabled;
|
|
100
114
|
}
|
|
115
|
+
function setLogFile(path) {
|
|
116
|
+
logFilePath = path;
|
|
117
|
+
writeFileSync(path, "");
|
|
118
|
+
}
|
|
119
|
+
function setRequestId(id) {
|
|
120
|
+
currentRequestId = id;
|
|
121
|
+
}
|
|
101
122
|
function log(msg) {
|
|
102
123
|
console.error(`[mcpboot] ${msg}`);
|
|
103
124
|
}
|
|
104
125
|
function warn(msg) {
|
|
105
126
|
console.error(`[mcpboot] WARN: ${msg}`);
|
|
106
127
|
}
|
|
107
|
-
function
|
|
108
|
-
if (
|
|
128
|
+
function truncateValue(val, limit) {
|
|
129
|
+
if (typeof val === "string" && val.length > limit) {
|
|
130
|
+
return val.slice(0, limit) + `...(${val.length} total)`;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(val)) {
|
|
133
|
+
return val.map((v) => truncateValue(v, limit));
|
|
134
|
+
}
|
|
135
|
+
if (val && typeof val === "object") {
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [k, v] of Object.entries(val)) {
|
|
138
|
+
out[k] = truncateValue(v, limit);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
return val;
|
|
143
|
+
}
|
|
144
|
+
function logEvent(event, data = {}) {
|
|
145
|
+
if (!verboseEnabled && !logFilePath) return;
|
|
146
|
+
const entry = {
|
|
147
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
event,
|
|
149
|
+
...currentRequestId ? { req_id: currentRequestId } : {},
|
|
150
|
+
...data
|
|
151
|
+
};
|
|
152
|
+
if (logFilePath) {
|
|
153
|
+
try {
|
|
154
|
+
appendFileSync(logFilePath, JSON.stringify(entry) + "\n");
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (verboseEnabled) {
|
|
159
|
+
const truncated = truncateValue(entry, 500);
|
|
160
|
+
console.error(JSON.stringify(truncated));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function trackLLM(elapsed_ms, prompt_tokens, completion_tokens) {
|
|
164
|
+
stats.llmCalls++;
|
|
165
|
+
stats.llmTotalMs += elapsed_ms;
|
|
166
|
+
if (prompt_tokens != null) stats.promptTokens += prompt_tokens;
|
|
167
|
+
if (completion_tokens != null) stats.completionTokens += completion_tokens;
|
|
168
|
+
}
|
|
169
|
+
function trackFetch(elapsed_ms) {
|
|
170
|
+
stats.fetchCalls++;
|
|
171
|
+
stats.fetchTotalMs += elapsed_ms;
|
|
172
|
+
}
|
|
173
|
+
function trackSandbox(elapsed_ms) {
|
|
174
|
+
stats.sandboxCalls++;
|
|
175
|
+
stats.sandboxTotalMs += elapsed_ms;
|
|
176
|
+
}
|
|
177
|
+
function logSummary() {
|
|
178
|
+
logEvent("session_summary", {
|
|
179
|
+
llm_calls: stats.llmCalls,
|
|
180
|
+
llm_total_ms: Math.round(stats.llmTotalMs),
|
|
181
|
+
llm_prompt_tokens: stats.promptTokens,
|
|
182
|
+
llm_completion_tokens: stats.completionTokens,
|
|
183
|
+
llm_total_tokens: stats.promptTokens + stats.completionTokens,
|
|
184
|
+
fetch_calls: stats.fetchCalls,
|
|
185
|
+
fetch_total_ms: Math.round(stats.fetchTotalMs),
|
|
186
|
+
sandbox_calls: stats.sandboxCalls,
|
|
187
|
+
sandbox_total_ms: Math.round(stats.sandboxTotalMs)
|
|
188
|
+
});
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
// src/fetcher.ts
|
|
@@ -145,7 +225,12 @@ function truncateContent(content, limit = MAX_CONTENT_LENGTH) {
|
|
|
145
225
|
}
|
|
146
226
|
async function fetchUrl(url) {
|
|
147
227
|
const fetchTarget = rewriteGitHubUrl(url) ?? url;
|
|
148
|
-
|
|
228
|
+
logEvent("fetch_start", {
|
|
229
|
+
url,
|
|
230
|
+
target_url: fetchTarget,
|
|
231
|
+
rewritten: fetchTarget !== url
|
|
232
|
+
});
|
|
233
|
+
const start = performance.now();
|
|
149
234
|
const response = await fetch(fetchTarget, {
|
|
150
235
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
151
236
|
headers: {
|
|
@@ -153,6 +238,15 @@ async function fetchUrl(url) {
|
|
|
153
238
|
}
|
|
154
239
|
});
|
|
155
240
|
if (!response.ok) {
|
|
241
|
+
const elapsed_ms2 = Math.round(performance.now() - start);
|
|
242
|
+
trackFetch(elapsed_ms2);
|
|
243
|
+
logEvent("fetch_error", {
|
|
244
|
+
url,
|
|
245
|
+
target_url: fetchTarget,
|
|
246
|
+
status: response.status,
|
|
247
|
+
status_text: response.statusText,
|
|
248
|
+
elapsed_ms: elapsed_ms2
|
|
249
|
+
});
|
|
156
250
|
throw new Error(
|
|
157
251
|
`Fetch failed for ${url}: ${response.status} ${response.statusText}`
|
|
158
252
|
);
|
|
@@ -160,11 +254,24 @@ async function fetchUrl(url) {
|
|
|
160
254
|
const rawContentType = response.headers.get("content-type") ?? "text/plain";
|
|
161
255
|
const contentType = rawContentType.split(";")[0].trim();
|
|
162
256
|
let text = await response.text();
|
|
257
|
+
const rawLength = text.length;
|
|
163
258
|
if (contentType === "text/html") {
|
|
164
259
|
text = stripHtml(text);
|
|
165
260
|
}
|
|
166
261
|
text = truncateContent(text);
|
|
167
262
|
const discovered = discoverUrls(text);
|
|
263
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
264
|
+
trackFetch(elapsed_ms);
|
|
265
|
+
logEvent("fetch_end", {
|
|
266
|
+
url,
|
|
267
|
+
target_url: fetchTarget,
|
|
268
|
+
status: response.status,
|
|
269
|
+
content_type: contentType,
|
|
270
|
+
raw_length: rawLength,
|
|
271
|
+
final_length: text.length,
|
|
272
|
+
discovered_urls: discovered.length,
|
|
273
|
+
elapsed_ms
|
|
274
|
+
});
|
|
168
275
|
return {
|
|
169
276
|
url,
|
|
170
277
|
content: text,
|
|
@@ -223,7 +330,7 @@ function buildWhitelist(promptUrls, contents) {
|
|
|
223
330
|
};
|
|
224
331
|
}
|
|
225
332
|
function createWhitelistedFetch(whitelist, realFetch = globalThis.fetch) {
|
|
226
|
-
return (url) => {
|
|
333
|
+
return async (url) => {
|
|
227
334
|
const hostname = extractDomain(url);
|
|
228
335
|
if (!hostname) {
|
|
229
336
|
return Promise.reject(
|
|
@@ -237,7 +344,29 @@ function createWhitelistedFetch(whitelist, realFetch = globalThis.fetch) {
|
|
|
237
344
|
)
|
|
238
345
|
);
|
|
239
346
|
}
|
|
240
|
-
|
|
347
|
+
logEvent("sandbox_fetch_start", { url, domain: hostname });
|
|
348
|
+
const start = performance.now();
|
|
349
|
+
try {
|
|
350
|
+
const response = await realFetch(url);
|
|
351
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
352
|
+
logEvent("sandbox_fetch_end", {
|
|
353
|
+
url,
|
|
354
|
+
domain: hostname,
|
|
355
|
+
status: response.status,
|
|
356
|
+
elapsed_ms
|
|
357
|
+
});
|
|
358
|
+
return response;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
361
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
362
|
+
logEvent("sandbox_fetch_error", {
|
|
363
|
+
url,
|
|
364
|
+
domain: hostname,
|
|
365
|
+
error: message,
|
|
366
|
+
elapsed_ms
|
|
367
|
+
});
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
241
370
|
};
|
|
242
371
|
}
|
|
243
372
|
|
|
@@ -247,7 +376,7 @@ import {
|
|
|
247
376
|
existsSync as existsSync2,
|
|
248
377
|
mkdirSync,
|
|
249
378
|
readFileSync as readFileSync2,
|
|
250
|
-
writeFileSync,
|
|
379
|
+
writeFileSync as writeFileSync2,
|
|
251
380
|
unlinkSync
|
|
252
381
|
} from "node:fs";
|
|
253
382
|
import { join } from "node:path";
|
|
@@ -292,7 +421,7 @@ function createCache(config) {
|
|
|
292
421
|
if (!config.enabled) return null;
|
|
293
422
|
const filepath = join(config.dir, cacheFilename(promptHash, contentHash));
|
|
294
423
|
if (!existsSync2(filepath)) return null;
|
|
295
|
-
|
|
424
|
+
logEvent("cache_lookup", { filepath });
|
|
296
425
|
try {
|
|
297
426
|
const raw = readFileSync2(filepath, "utf-8");
|
|
298
427
|
const parsed = JSON.parse(raw);
|
|
@@ -301,7 +430,7 @@ function createCache(config) {
|
|
|
301
430
|
unlinkSync(filepath);
|
|
302
431
|
return null;
|
|
303
432
|
}
|
|
304
|
-
|
|
433
|
+
logEvent("cache_hit", { filepath, created_at: parsed.createdAt });
|
|
305
434
|
return parsed;
|
|
306
435
|
} catch {
|
|
307
436
|
warn(`Failed to read cache file ${filepath}, removing`);
|
|
@@ -319,8 +448,8 @@ function createCache(config) {
|
|
|
319
448
|
config.dir,
|
|
320
449
|
cacheFilename(entry.promptHash, entry.contentHash)
|
|
321
450
|
);
|
|
322
|
-
|
|
323
|
-
|
|
451
|
+
writeFileSync2(filepath, JSON.stringify(entry, null, 2));
|
|
452
|
+
logEvent("cache_written", { filepath });
|
|
324
453
|
}
|
|
325
454
|
};
|
|
326
455
|
}
|
|
@@ -333,6 +462,7 @@ var DEFAULT_MODELS = {
|
|
|
333
462
|
anthropic: "claude-sonnet-4-20250514",
|
|
334
463
|
openai: "gpt-4o"
|
|
335
464
|
};
|
|
465
|
+
var callCount = 0;
|
|
336
466
|
function createLLMClient(config) {
|
|
337
467
|
const modelId = config.model ?? DEFAULT_MODELS[config.provider];
|
|
338
468
|
let model;
|
|
@@ -343,8 +473,20 @@ function createLLMClient(config) {
|
|
|
343
473
|
const openai = createOpenAI({ apiKey: config.apiKey });
|
|
344
474
|
model = openai(modelId);
|
|
345
475
|
}
|
|
476
|
+
logEvent("llm_init", { provider: config.provider, model: modelId });
|
|
346
477
|
return {
|
|
347
478
|
async generate(system, user) {
|
|
479
|
+
const callId = ++callCount;
|
|
480
|
+
logEvent("llm_call_start", {
|
|
481
|
+
call_id: callId,
|
|
482
|
+
provider: config.provider,
|
|
483
|
+
model: modelId,
|
|
484
|
+
max_tokens: 8192,
|
|
485
|
+
temperature: 0.2,
|
|
486
|
+
system_prompt: system,
|
|
487
|
+
user_prompt: user
|
|
488
|
+
});
|
|
489
|
+
const start = performance.now();
|
|
348
490
|
try {
|
|
349
491
|
const result = await generateText({
|
|
350
492
|
model,
|
|
@@ -353,9 +495,29 @@ function createLLMClient(config) {
|
|
|
353
495
|
maxTokens: 8192,
|
|
354
496
|
temperature: 0.2
|
|
355
497
|
});
|
|
498
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
499
|
+
const prompt_tokens = result.usage?.promptTokens;
|
|
500
|
+
const completion_tokens = result.usage?.completionTokens;
|
|
501
|
+
trackLLM(elapsed_ms, prompt_tokens, completion_tokens);
|
|
502
|
+
logEvent("llm_call_end", {
|
|
503
|
+
call_id: callId,
|
|
504
|
+
elapsed_ms,
|
|
505
|
+
prompt_tokens,
|
|
506
|
+
completion_tokens,
|
|
507
|
+
total_tokens: (prompt_tokens ?? 0) + (completion_tokens ?? 0),
|
|
508
|
+
finish_reason: result.finishReason,
|
|
509
|
+
response: result.text
|
|
510
|
+
});
|
|
356
511
|
return result.text;
|
|
357
512
|
} catch (error) {
|
|
513
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
358
514
|
const err = error;
|
|
515
|
+
logEvent("llm_call_error", {
|
|
516
|
+
call_id: callId,
|
|
517
|
+
elapsed_ms,
|
|
518
|
+
error: err.message ?? String(error),
|
|
519
|
+
status_code: err.statusCode
|
|
520
|
+
});
|
|
359
521
|
if (err.statusCode === 404) {
|
|
360
522
|
throw new Error(
|
|
361
523
|
`Model "${modelId}" not found. Check the model ID \u2014 e.g. "claude-sonnet-4-20250514" or "claude-haiku-4-5" (note: dated variants like "claude-haiku-4-5-20241022" don't exist; use "claude-3-5-haiku-20241022" for the dated form)`
|
|
@@ -371,6 +533,8 @@ function createLLMClient(config) {
|
|
|
371
533
|
var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
372
534
|
var SYSTEM_PROMPT = `You are an MCP tool planner. You receive a natural language description of desired tools, optionally with API documentation content, and you produce a STRUCTURED PLAN (as JSON) describing the tools to generate.
|
|
373
535
|
|
|
536
|
+
IMPORTANT: The tool descriptions and schemas you produce will be consumed by downstream AI/LLM systems to understand each tool's behavior, generate web UIs, and produce correct tool calls. Descriptions and schemas must therefore be thorough, self-contained, and richly annotated.
|
|
537
|
+
|
|
374
538
|
OUTPUT FORMAT:
|
|
375
539
|
Return ONLY valid JSON matching this exact schema (no markdown, no explanation):
|
|
376
540
|
|
|
@@ -378,11 +542,16 @@ Return ONLY valid JSON matching this exact schema (no markdown, no explanation):
|
|
|
378
542
|
"tools": [
|
|
379
543
|
{
|
|
380
544
|
"name": "tool_name",
|
|
381
|
-
"description": "
|
|
545
|
+
"description": "A comprehensive, multi-paragraph description (see DESCRIPTION GUIDELINES below)",
|
|
382
546
|
"input_schema": {
|
|
383
547
|
"type": "object",
|
|
384
548
|
"properties": {
|
|
385
|
-
"param_name": {
|
|
549
|
+
"param_name": {
|
|
550
|
+
"type": "string",
|
|
551
|
+
"description": "Detailed parameter description including purpose, format, and constraints",
|
|
552
|
+
"default": "optional default value if applicable",
|
|
553
|
+
"examples": ["example_value_1", "example_value_2"]
|
|
554
|
+
}
|
|
386
555
|
},
|
|
387
556
|
"required": ["param_name"]
|
|
388
557
|
},
|
|
@@ -393,6 +562,51 @@ Return ONLY valid JSON matching this exact schema (no markdown, no explanation):
|
|
|
393
562
|
]
|
|
394
563
|
}
|
|
395
564
|
|
|
565
|
+
DESCRIPTION GUIDELINES:
|
|
566
|
+
Each tool's "description" field must be a rich, self-contained documentation string. Include ALL of the following sections, separated by blank lines:
|
|
567
|
+
|
|
568
|
+
1. SUMMARY: A clear one-sentence summary of what the tool does.
|
|
569
|
+
2. DETAILS: When to use this tool, how it relates to other tools, any important behavioral notes or caveats.
|
|
570
|
+
3. RESPONSE FORMAT: Describe the structure and fields of a successful response. Include a concrete JSON example.
|
|
571
|
+
4. EXAMPLE: Show at least one realistic usage example with sample parameter values and a corresponding example response.
|
|
572
|
+
5. ERRORS: List common error scenarios (e.g., invalid input, resource not found, rate limits).
|
|
573
|
+
|
|
574
|
+
Example of a good description:
|
|
575
|
+
|
|
576
|
+
"Retrieves the full details of a specific Hacker News item (story, comment, job, poll, or pollopt) by its unique numeric ID.\\n\\nUse this tool when you have an item ID (e.g. from get_top_stories) and need its title, author, score, URL, or child comments. Each item type returns slightly different fields.\\n\\nResponse format:\\nReturns a JSON object with the item details. Fields vary by type. Stories include: id, type, by, title, url, score, time, descendants, kids. Comments include: id, type, by, text, parent, time, kids.\\n\\nExample response:\\n{\\n \\"id\\": 8863,\\n \\"type\\": \\"story\\",\\n \\"by\\": \\"dhouston\\",\\n \\"title\\": \\"My YC app: Dropbox\\",\\n \\"url\\": \\"http://www.getdropbox.com/u/2/screencast.html\\",\\n \\"score\\": 111,\\n \\"time\\": 1175714200,\\n \\"descendants\\": 71\\n}\\n\\nExample usage:\\n- Input: { \\"id\\": 8863 }\\n- Returns the full item object for story 8863\\n\\nErrors:\\n- Returns an error if the item ID does not exist or the API is unreachable."
|
|
577
|
+
|
|
578
|
+
INPUT SCHEMA GUIDELINES:
|
|
579
|
+
Use the full power of JSON Schema to describe each parameter precisely:
|
|
580
|
+
- Every property MUST have a "description" explaining its purpose, expected format, and constraints.
|
|
581
|
+
- Use "default" to document the default value when a parameter is optional.
|
|
582
|
+
- Use "examples" (array) to provide 2-3 realistic example values.
|
|
583
|
+
- Use "minimum" / "maximum" for numeric bounds when applicable.
|
|
584
|
+
- Use "enum" for parameters with a fixed set of allowed values.
|
|
585
|
+
- Use "pattern" for string parameters with a specific format (e.g., date patterns).
|
|
586
|
+
- Use "minLength" / "maxLength" for string length constraints when applicable.
|
|
587
|
+
|
|
588
|
+
Example of a good input_schema:
|
|
589
|
+
{
|
|
590
|
+
"type": "object",
|
|
591
|
+
"properties": {
|
|
592
|
+
"limit": {
|
|
593
|
+
"type": "number",
|
|
594
|
+
"description": "Maximum number of story IDs to return. The API provides up to 500 stories; this parameter caps the result to the specified count.",
|
|
595
|
+
"default": 10,
|
|
596
|
+
"minimum": 1,
|
|
597
|
+
"maximum": 500,
|
|
598
|
+
"examples": [5, 10, 50]
|
|
599
|
+
},
|
|
600
|
+
"id": {
|
|
601
|
+
"type": "number",
|
|
602
|
+
"description": "The unique numeric Hacker News item ID. Item IDs are positive integers assigned sequentially. Obtain item IDs from tools like get_top_stories or get_new_stories.",
|
|
603
|
+
"minimum": 1,
|
|
604
|
+
"examples": [8863, 37052586]
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
"required": ["id"]
|
|
608
|
+
}
|
|
609
|
+
|
|
396
610
|
RULES:
|
|
397
611
|
1. Tool names must be lowercase with underscores only (a-z, 0-9, _). Must start with a letter.
|
|
398
612
|
2. Each tool must have a unique name.
|
|
@@ -491,7 +705,14 @@ ${domains.join("\n")}
|
|
|
491
705
|
async function generatePlan(llm, prompt, contents, whitelist) {
|
|
492
706
|
const userPrompt = buildUserPrompt(prompt, contents, whitelist);
|
|
493
707
|
let lastError = null;
|
|
708
|
+
logEvent("plan_start", {
|
|
709
|
+
doc_count: contents.length,
|
|
710
|
+
whitelist: [...whitelist.domains]
|
|
711
|
+
});
|
|
494
712
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
713
|
+
if (attempt > 0) {
|
|
714
|
+
logEvent("plan_retry", { attempt: attempt + 1 });
|
|
715
|
+
}
|
|
495
716
|
let response;
|
|
496
717
|
try {
|
|
497
718
|
response = await llm.generate(SYSTEM_PROMPT, userPrompt);
|
|
@@ -499,6 +720,7 @@ async function generatePlan(llm, prompt, contents, whitelist) {
|
|
|
499
720
|
const message = error instanceof Error ? error.message : String(error);
|
|
500
721
|
throw new Error(`LLM error during planning: ${message}`);
|
|
501
722
|
}
|
|
723
|
+
logEvent("plan_llm_response", { response, attempt: attempt + 1 });
|
|
502
724
|
const jsonText = extractJSON(response);
|
|
503
725
|
let parsed;
|
|
504
726
|
try {
|
|
@@ -535,8 +757,11 @@ async function generatePlan(llm, prompt, contents, whitelist) {
|
|
|
535
757
|
}
|
|
536
758
|
throw lastError;
|
|
537
759
|
}
|
|
538
|
-
|
|
539
|
-
|
|
760
|
+
logEvent("plan_end", {
|
|
761
|
+
tool_count: parsed.tools.length,
|
|
762
|
+
tool_names: parsed.tools.map((t) => t.name),
|
|
763
|
+
plan: parsed
|
|
764
|
+
});
|
|
540
765
|
return parsed;
|
|
541
766
|
}
|
|
542
767
|
throw lastError ?? new Error("Plan generation failed");
|
|
@@ -653,12 +878,22 @@ ${content.content}
|
|
|
653
878
|
}
|
|
654
879
|
async function compilePlan(llm, plan, contents) {
|
|
655
880
|
const tools = /* @__PURE__ */ new Map();
|
|
881
|
+
logEvent("compile_start", { tool_count: plan.tools.length });
|
|
656
882
|
for (const plannedTool of plan.tools) {
|
|
657
|
-
|
|
883
|
+
logEvent("compile_tool_start", {
|
|
884
|
+
tool_name: plannedTool.name,
|
|
885
|
+
needs_network: plannedTool.needs_network
|
|
886
|
+
});
|
|
658
887
|
const systemPrompt = plannedTool.needs_network ? SYSTEM_PROMPT_NETWORK : SYSTEM_PROMPT_PURE;
|
|
659
888
|
const userPrompt = buildHandlerPrompt("", plannedTool, contents);
|
|
660
889
|
let lastError = null;
|
|
661
890
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
891
|
+
if (attempt > 0) {
|
|
892
|
+
logEvent("compile_tool_retry", {
|
|
893
|
+
tool_name: plannedTool.name,
|
|
894
|
+
attempt: attempt + 1
|
|
895
|
+
});
|
|
896
|
+
}
|
|
662
897
|
let response;
|
|
663
898
|
try {
|
|
664
899
|
response = await llm.generate(systemPrompt, userPrompt);
|
|
@@ -668,6 +903,11 @@ async function compilePlan(llm, plan, contents) {
|
|
|
668
903
|
`LLM error while compiling "${plannedTool.name}": ${message}`
|
|
669
904
|
);
|
|
670
905
|
}
|
|
906
|
+
logEvent("compile_tool_llm_response", {
|
|
907
|
+
tool_name: plannedTool.name,
|
|
908
|
+
response,
|
|
909
|
+
attempt: attempt + 1
|
|
910
|
+
});
|
|
671
911
|
const code = extractCode(response);
|
|
672
912
|
try {
|
|
673
913
|
validateCode(code);
|
|
@@ -690,7 +930,11 @@ async function compilePlan(llm, plan, contents) {
|
|
|
690
930
|
handler_code: code,
|
|
691
931
|
needs_network: plannedTool.needs_network
|
|
692
932
|
});
|
|
693
|
-
|
|
933
|
+
logEvent("compile_tool_end", {
|
|
934
|
+
tool_name: plannedTool.name,
|
|
935
|
+
code_length: code.length,
|
|
936
|
+
handler_code: code
|
|
937
|
+
});
|
|
694
938
|
break;
|
|
695
939
|
}
|
|
696
940
|
}
|
|
@@ -703,15 +947,34 @@ function createExecutor(compiled, sandbox) {
|
|
|
703
947
|
async execute(toolName, args) {
|
|
704
948
|
const tool = compiled.tools.get(toolName);
|
|
705
949
|
if (!tool) {
|
|
950
|
+
logEvent("executor_unknown_tool", { tool_name: toolName });
|
|
706
951
|
return {
|
|
707
952
|
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
|
|
708
953
|
isError: true
|
|
709
954
|
};
|
|
710
955
|
}
|
|
956
|
+
logEvent("executor_start", { tool_name: toolName, args });
|
|
957
|
+
const start = performance.now();
|
|
711
958
|
try {
|
|
712
|
-
|
|
959
|
+
const result = await sandbox.runHandler(tool.handler_code, args);
|
|
960
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
961
|
+
trackSandbox(elapsed_ms);
|
|
962
|
+
logEvent("executor_end", {
|
|
963
|
+
tool_name: toolName,
|
|
964
|
+
result,
|
|
965
|
+
elapsed_ms,
|
|
966
|
+
is_error: false
|
|
967
|
+
});
|
|
968
|
+
return result;
|
|
713
969
|
} catch (error) {
|
|
970
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
971
|
+
trackSandbox(elapsed_ms);
|
|
714
972
|
const message = error instanceof Error ? error.message : String(error);
|
|
973
|
+
logEvent("executor_error", {
|
|
974
|
+
tool_name: toolName,
|
|
975
|
+
error: message,
|
|
976
|
+
elapsed_ms
|
|
977
|
+
});
|
|
715
978
|
return {
|
|
716
979
|
content: [{ type: "text", text: `Handler error: ${message}` }],
|
|
717
980
|
isError: true
|
|
@@ -798,6 +1061,10 @@ import {
|
|
|
798
1061
|
CallToolRequestSchema
|
|
799
1062
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
800
1063
|
import http from "node:http";
|
|
1064
|
+
import { randomBytes } from "node:crypto";
|
|
1065
|
+
import { createRequire } from "node:module";
|
|
1066
|
+
var require2 = createRequire(import.meta.url);
|
|
1067
|
+
var { version } = require2("../package.json");
|
|
801
1068
|
function readBody(req) {
|
|
802
1069
|
return new Promise((resolve, reject) => {
|
|
803
1070
|
const chunks = [];
|
|
@@ -813,8 +1080,11 @@ function readBody(req) {
|
|
|
813
1080
|
req.on("error", reject);
|
|
814
1081
|
});
|
|
815
1082
|
}
|
|
816
|
-
function createExposedServer(config, executor) {
|
|
1083
|
+
function createExposedServer(config, executor, compiled) {
|
|
817
1084
|
const httpServer = http.createServer(async (req, res) => {
|
|
1085
|
+
const reqId = randomBytes(6).toString("hex");
|
|
1086
|
+
setRequestId(reqId);
|
|
1087
|
+
logEvent("http_request", { method: req.method, url: req.url });
|
|
818
1088
|
if (req.method === "POST" && req.url === "/mcp") {
|
|
819
1089
|
const mcpServer = new Server(
|
|
820
1090
|
{ name: "mcpboot", version: "0.1.0" },
|
|
@@ -822,6 +1092,7 @@ function createExposedServer(config, executor) {
|
|
|
822
1092
|
);
|
|
823
1093
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
824
1094
|
const tools = executor.getExposedTools();
|
|
1095
|
+
logEvent("mcp_list_tools", { tool_count: tools.length });
|
|
825
1096
|
return {
|
|
826
1097
|
tools: tools.map((t) => ({
|
|
827
1098
|
name: t.name,
|
|
@@ -832,16 +1103,42 @@ function createExposedServer(config, executor) {
|
|
|
832
1103
|
});
|
|
833
1104
|
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
834
1105
|
const { name, arguments: args } = request.params;
|
|
835
|
-
|
|
1106
|
+
if (name === "_mcp_metadata") {
|
|
1107
|
+
return {
|
|
1108
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1109
|
+
stage: "boot",
|
|
1110
|
+
version,
|
|
1111
|
+
upstream_url: null,
|
|
1112
|
+
whitelist_domains: compiled.whitelist_domains,
|
|
1113
|
+
tools: Array.from(compiled.tools.values())
|
|
1114
|
+
}) }]
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
logEvent("mcp_call_tool_start", {
|
|
1118
|
+
tool_name: name,
|
|
1119
|
+
args: args ?? {}
|
|
1120
|
+
});
|
|
1121
|
+
const start = performance.now();
|
|
1122
|
+
const result = await executor.execute(name, args ?? {});
|
|
1123
|
+
const elapsed_ms = Math.round(performance.now() - start);
|
|
1124
|
+
logEvent("mcp_call_tool_end", {
|
|
1125
|
+
tool_name: name,
|
|
1126
|
+
result,
|
|
1127
|
+
elapsed_ms,
|
|
1128
|
+
is_error: result.isError ?? false
|
|
1129
|
+
});
|
|
1130
|
+
return result;
|
|
836
1131
|
});
|
|
837
1132
|
const transport = new StreamableHTTPServerTransport({
|
|
838
1133
|
sessionIdGenerator: void 0
|
|
839
1134
|
});
|
|
840
1135
|
try {
|
|
841
1136
|
const body = await readBody(req);
|
|
1137
|
+
logEvent("mcp_request_body", { body });
|
|
842
1138
|
await mcpServer.connect(transport);
|
|
843
1139
|
await transport.handleRequest(req, res, body);
|
|
844
1140
|
res.on("close", () => {
|
|
1141
|
+
setRequestId(void 0);
|
|
845
1142
|
transport.close();
|
|
846
1143
|
mcpServer.close();
|
|
847
1144
|
});
|
|
@@ -860,6 +1157,9 @@ function createExposedServer(config, executor) {
|
|
|
860
1157
|
}
|
|
861
1158
|
}
|
|
862
1159
|
} else if (req.method === "GET" && req.url === "/health") {
|
|
1160
|
+
logEvent("health_check", {
|
|
1161
|
+
tool_count: executor.getExposedTools().length
|
|
1162
|
+
});
|
|
863
1163
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
864
1164
|
res.end(
|
|
865
1165
|
JSON.stringify({
|
|
@@ -868,9 +1168,11 @@ function createExposedServer(config, executor) {
|
|
|
868
1168
|
})
|
|
869
1169
|
);
|
|
870
1170
|
} else {
|
|
1171
|
+
logEvent("unknown_route", { method: req.method, url: req.url });
|
|
871
1172
|
res.writeHead(404);
|
|
872
1173
|
res.end("Not found");
|
|
873
1174
|
}
|
|
1175
|
+
setRequestId(void 0);
|
|
874
1176
|
});
|
|
875
1177
|
return {
|
|
876
1178
|
start() {
|
|
@@ -927,6 +1229,17 @@ async function main(argv = process.argv) {
|
|
|
927
1229
|
const config = buildConfig(argv);
|
|
928
1230
|
if (!config) return;
|
|
929
1231
|
setVerbose(config.verbose);
|
|
1232
|
+
if (config.logFile) {
|
|
1233
|
+
setLogFile(config.logFile);
|
|
1234
|
+
}
|
|
1235
|
+
setRequestId("startup");
|
|
1236
|
+
logEvent("session_start", {
|
|
1237
|
+
prompt_length: config.prompt.length,
|
|
1238
|
+
provider: config.llm.provider,
|
|
1239
|
+
model: config.llm.model,
|
|
1240
|
+
cache_enabled: config.cache.enabled,
|
|
1241
|
+
dry_run: config.dryRun
|
|
1242
|
+
});
|
|
930
1243
|
const urls = extractUrls(config.prompt);
|
|
931
1244
|
log(`Found ${urls.length} URL(s) in prompt`);
|
|
932
1245
|
const contents = await fetchUrls(urls);
|
|
@@ -938,7 +1251,7 @@ async function main(argv = process.argv) {
|
|
|
938
1251
|
}
|
|
939
1252
|
const whitelist = buildWhitelist(urls, contents);
|
|
940
1253
|
const whitelistDomains = [...whitelist.domains];
|
|
941
|
-
|
|
1254
|
+
logEvent("whitelist_built", { domains: whitelistDomains });
|
|
942
1255
|
const cache = createCache(config.cache);
|
|
943
1256
|
const promptHash = hash(config.prompt);
|
|
944
1257
|
const contentHash = buildContentHash(contents);
|
|
@@ -947,15 +1260,18 @@ async function main(argv = process.argv) {
|
|
|
947
1260
|
const cached = cache.get(promptHash, contentHash);
|
|
948
1261
|
if (cached) {
|
|
949
1262
|
log("Cache hit \u2014 loading generated tools");
|
|
1263
|
+
logEvent("cache_hit", { prompt_hash: promptHash, content_hash: contentHash });
|
|
950
1264
|
compiled = deserializeCompiled(cached);
|
|
951
1265
|
activeWhitelist = reconstructWhitelist(cached.whitelist_domains);
|
|
952
1266
|
} else {
|
|
953
1267
|
log("Cache miss \u2014 generating tools via LLM");
|
|
1268
|
+
logEvent("cache_miss", { prompt_hash: promptHash, content_hash: contentHash });
|
|
954
1269
|
const llm = createLLMClient(config.llm);
|
|
955
1270
|
const plan = await generatePlan(llm, config.prompt, contents, whitelist);
|
|
956
1271
|
log(`Plan: ${plan.tools.length} tool(s)`);
|
|
957
1272
|
if (config.dryRun) {
|
|
958
1273
|
console.log(JSON.stringify(plan, null, 2));
|
|
1274
|
+
logSummary();
|
|
959
1275
|
return;
|
|
960
1276
|
}
|
|
961
1277
|
compiled = await compilePlan(llm, plan, contents);
|
|
@@ -976,20 +1292,28 @@ async function main(argv = process.argv) {
|
|
|
976
1292
|
log(`${compiled.tools.size} cached tool(s) available`);
|
|
977
1293
|
const toolNames = Array.from(compiled.tools.keys());
|
|
978
1294
|
console.log(JSON.stringify({ cached: true, tools: toolNames }, null, 2));
|
|
1295
|
+
logSummary();
|
|
979
1296
|
return;
|
|
980
1297
|
}
|
|
1298
|
+
setRequestId(void 0);
|
|
981
1299
|
const whitelistedFetch = createWhitelistedFetch(activeWhitelist);
|
|
982
1300
|
const sandbox = createSandbox(whitelistedFetch);
|
|
983
1301
|
const executor = createExecutor(compiled, sandbox);
|
|
984
|
-
const server = createExposedServer(config.server, executor);
|
|
1302
|
+
const server = createExposedServer(config.server, executor, compiled);
|
|
985
1303
|
const port = await server.start();
|
|
986
1304
|
log(`Listening on http://localhost:${port}/mcp`);
|
|
987
1305
|
log(`Serving ${executor.getExposedTools().length} tool(s)`);
|
|
1306
|
+
logEvent("server_ready", {
|
|
1307
|
+
port,
|
|
1308
|
+
tool_count: executor.getExposedTools().length,
|
|
1309
|
+
tool_names: executor.getExposedTools().map((t) => t.name)
|
|
1310
|
+
});
|
|
988
1311
|
if (config.pipe.stdoutIsPipe) {
|
|
989
1312
|
writeOwnUrl(`http://localhost:${port}/mcp`);
|
|
990
1313
|
}
|
|
991
1314
|
const shutdown = async () => {
|
|
992
1315
|
log("Shutting down...");
|
|
1316
|
+
logSummary();
|
|
993
1317
|
await server.stop();
|
|
994
1318
|
process.exit(0);
|
|
995
1319
|
};
|