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.
Files changed (3) hide show
  1. package/README.md +11 -5
  2. package/dist/index.js +347 -23
  3. 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 verbose(msg) {
108
- if (verboseEnabled) console.error(`[mcpboot] ${msg}`);
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
- verbose(`Fetching ${fetchTarget}`);
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
- return realFetch(url);
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
- verbose(`Cache lookup: ${filepath}`);
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
- verbose(`Cache hit: ${filepath} (created ${parsed.createdAt})`);
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
- writeFileSync(filepath, JSON.stringify(entry, null, 2));
323
- verbose(`Cache written: ${filepath}`);
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": "What the tool does",
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": { "type": "string", "description": "Parameter description" }
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
- verbose(`Generated plan:
539
- ${JSON.stringify(parsed, null, 2)}`);
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
- verbose(`Compiling handler for tool: ${plannedTool.name}`);
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
- verbose(`Compiled handler for "${plannedTool.name}" (${code.length} chars)`);
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
- return await sandbox.runHandler(tool.handler_code, args);
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
- return executor.execute(name, args ?? {});
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
- verbose(`Whitelist: ${whitelistDomains.join(", ") || "(empty)"}`);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpboot",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Generate and serve an MCP server from a natural language prompt",
5
5
  "type": "module",
6
6
  "bin": {