threenative-engine-mcp 0.2.2 → 0.2.4

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 CHANGED
@@ -30,3 +30,16 @@ The first that exists wins, so the manifest can never drift from the engine the
30
30
 
31
31
  A project copy committed at scaffold time is therefore inert whenever the package is installed:
32
32
  the two never need re-syncing by hand.
33
+
34
+ ## The call log
35
+
36
+ Every tool call appends one JSON line to `.threenative/engine-mcp.log` in the launch directory, so
37
+ a finished session's searches can be read back:
38
+
39
+ ```json
40
+ {"time":"2026-09-14T20:20:00.000Z","results":["MultiMesh3D"],"scope":"mechanic","situation":"spawn many identical props","tool":"engine_search_capabilities","verdict":"matched"}
41
+ ```
42
+
43
+ A rejected call is logged with its `error`, which is where an agent asked for something the engine
44
+ could not answer. `THREENATIVE_ENGINE_MCP_LOG` names another file, or `off` to disable the log. A
45
+ log that cannot be written reports once on stderr and never fails a tool call.
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ interface ICapabilityEntry {
11
11
  readonly constraints: readonly string[];
12
12
  readonly overrides?: readonly string[];
13
13
  readonly requires?: readonly string[];
14
+ /** Absent unless something on this entry is deprecated; each string names what, and what to use. */
15
+ readonly deprecated?: readonly string[];
14
16
  }
15
17
  interface INotOwnedCapability {
16
18
  readonly id: string;
@@ -65,7 +67,14 @@ declare function defaultManifestPath(cwd?: string): string;
65
67
  declare function loadCapabilityManifest(file?: string): ICapabilityManifest;
66
68
  declare function capabilitySituationTokens(value: string): string[];
67
69
  declare function searchCapabilities(situation: string, manifestFile?: string, scope?: "mechanic" | "request"): ICapabilitySearchResponse;
68
- declare function capabilityDetail(symbol: string, manifestFile?: string): ICapabilityDetail;
70
+ /**
71
+ * Every manifest entry named `symbol` (optionally in one `importPath`); throws when there is none.
72
+ * Two packages can export one name (`createThreeObject` from raw-unreal and ueformat), so a lookup
73
+ * by name alone must return both — answering with the first hides the second from detail.
74
+ */
75
+ declare function capabilityDetails(symbol: string, manifestFile?: string, importPath?: string): readonly ICapabilityDetail[];
76
+ /** One capability's detail; code callers must name `importPath` when the symbol is shared. */
77
+ declare function capabilityDetail(symbol: string, manifestFile?: string, importPath?: string): ICapabilityDetail;
69
78
  declare function toolDefinitions(): readonly IEngineTool[];
70
79
  /**
71
80
  * Handles one raw stdin line and returns the JSON-RPC response to write, or `undefined` when the
@@ -75,4 +84,4 @@ declare function toolDefinitions(): readonly IEngineTool[];
75
84
  declare function handleLine(line: string, manifestFile: string): string | undefined;
76
85
  declare function runServer(manifestFile?: string): void;
77
86
 
78
- export { ENGINE_MCP_TOOL_NAMES, type ICapabilityDetail, type ICapabilityEntry, type ICapabilityManifest, type ICapabilitySearchResponse, type ICapabilitySearchResult, type IEngineTool, type INotOwnedCapability, RELEVANCE_FLOOR, capabilityDetail, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
87
+ export { ENGINE_MCP_TOOL_NAMES, type ICapabilityDetail, type ICapabilityEntry, type ICapabilityManifest, type ICapabilitySearchResponse, type ICapabilitySearchResult, type IEngineTool, type INotOwnedCapability, RELEVANCE_FLOOR, capabilityDetail, capabilityDetails, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync, readFileSync, existsSync } from 'fs';
2
+ import { realpathSync, readFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
3
3
  import path from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
@@ -131,14 +131,13 @@ function defaultManifestPath(cwd = process.cwd()) {
131
131
  const override = process.env.THREENATIVE_CAPABILITIES_MANIFEST;
132
132
  if (override !== void 0 && override.trim().length > 0) return path.resolve(cwd, override);
133
133
  for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
134
- const installed = path.join(
135
- directory,
136
- "node_modules",
137
- "@threenative",
138
- "core",
139
- "capabilities.json"
140
- );
141
- if (existsSync(installed)) return installed;
134
+ const nodeModules = path.join(directory, "node_modules");
135
+ const installed = path.join(nodeModules, "@threenative", "core", "capabilities.json");
136
+ if (existsSync(installed)) {
137
+ const relative = path.relative(realpathSync(directory), realpathSync(nodeModules));
138
+ if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
139
+ return installed;
140
+ }
142
141
  const parent = path.dirname(directory);
143
142
  if (parent === directory) break;
144
143
  }
@@ -156,6 +155,9 @@ function manifestError(file, reason) {
156
155
  function isRecord(value) {
157
156
  return typeof value === "object" && value !== null && !Array.isArray(value);
158
157
  }
158
+ function isStringArray(value) {
159
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
160
+ }
159
161
  function validateManifest(value, file) {
160
162
  if (!isRecord(value) || value.version !== 2 || !Array.isArray(value.entries) || !Array.isArray(value.notOwned)) {
161
163
  throw manifestError(
@@ -164,7 +166,7 @@ function validateManifest(value, file) {
164
166
  );
165
167
  }
166
168
  for (const [index, raw] of value.entries.entries()) {
167
- if (!isRecord(raw) || typeof raw.symbol !== "string" || typeof raw.package !== "string" || typeof raw.importPath !== "string" || typeof raw.kind !== "string" || typeof raw.signature !== "string" || typeof raw.summary !== "string" || typeof raw.example !== "string" || !Array.isArray(raw.situations) || !Array.isArray(raw.aliases) || !Array.isArray(raw.constraints) || raw.requires !== void 0 && !Array.isArray(raw.requires) || !raw.situations.every((situation) => typeof situation === "string") || !raw.aliases.every((alias) => typeof alias === "string") || !raw.constraints.every((constraint) => typeof constraint === "string") || Array.isArray(raw.requires) && !raw.requires.every((requirement) => typeof requirement === "string")) {
169
+ if (!isRecord(raw) || typeof raw.symbol !== "string" || typeof raw.package !== "string" || typeof raw.importPath !== "string" || typeof raw.kind !== "string" || typeof raw.signature !== "string" || typeof raw.summary !== "string" || typeof raw.example !== "string" || !Array.isArray(raw.situations) || !Array.isArray(raw.aliases) || !Array.isArray(raw.constraints) || raw.requires !== void 0 && !isStringArray(raw.requires) || raw.deprecated !== void 0 && !isStringArray(raw.deprecated) || !raw.situations.every((situation) => typeof situation === "string") || !raw.aliases.every((alias) => typeof alias === "string") || !raw.constraints.every((constraint) => typeof constraint === "string")) {
168
170
  throw manifestError(file, `entry ${index} is malformed`);
169
171
  }
170
172
  }
@@ -375,13 +377,25 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco
375
377
  verdict: "none"
376
378
  };
377
379
  }
378
- function capabilityDetail(symbol, manifestFile = defaultManifestPath()) {
380
+ function capabilityDetails(symbol, manifestFile = defaultManifestPath(), importPath) {
379
381
  if (typeof symbol !== "string" || symbol.trim().length === 0)
380
382
  throw new Error("engine_capability_detail requires a non-empty symbol string.");
381
- const manifest = loadCapabilityManifest(manifestFile);
382
- const entry = manifest.entries.find((candidate) => candidate.symbol === symbol);
383
- if (entry === void 0) throw new Error(`Unknown engine capability '${symbol}'.`);
384
- return entry;
383
+ const matches = loadCapabilityManifest(manifestFile).entries.filter(
384
+ (candidate) => candidate.symbol === symbol && (importPath === void 0 || candidate.importPath === importPath)
385
+ );
386
+ if (matches.length === 0)
387
+ throw new Error(
388
+ `Unknown engine capability '${symbol}'${importPath === void 0 ? "" : ` in '${importPath}'`}.`
389
+ );
390
+ return matches;
391
+ }
392
+ function capabilityDetail(symbol, manifestFile = defaultManifestPath(), importPath) {
393
+ const matches = capabilityDetails(symbol, manifestFile, importPath);
394
+ if (matches.length > 1)
395
+ throw new Error(
396
+ `Ambiguous engine capability '${symbol}': pass importPath, one of ${matches.map((match) => `'${match.importPath}'`).join(", ")}.`
397
+ );
398
+ return matches[0];
385
399
  }
386
400
  var TOOL_DEFINITIONS = [
387
401
  {
@@ -405,10 +419,16 @@ var TOOL_DEFINITIONS = [
405
419
  {
406
420
  annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
407
421
  name: "engine_capability_detail",
408
- description: "Inspect one engine capability's import, signature, example, constraints, and overrides.",
422
+ description: "Inspect one engine capability's import, signature, example, constraints, and overrides. When several packages export the symbol, every match is returned under `matches`.",
409
423
  inputSchema: {
410
424
  additionalProperties: false,
411
- properties: { symbol: { type: "string" } },
425
+ properties: {
426
+ importPath: {
427
+ description: "The search result's importPath; required when two packages export the same symbol.",
428
+ type: "string"
429
+ },
430
+ symbol: { type: "string" }
431
+ },
412
432
  required: ["symbol"],
413
433
  type: "object"
414
434
  }
@@ -417,6 +437,22 @@ var TOOL_DEFINITIONS = [
417
437
  function toolDefinitions() {
418
438
  return TOOL_DEFINITIONS;
419
439
  }
440
+ var DEFAULT_LOG_FILE = path.join(".threenative", "engine-mcp.log");
441
+ var logBroken = false;
442
+ function logToolCall(entry) {
443
+ if (logBroken) return;
444
+ const target = process.env.THREENATIVE_ENGINE_MCP_LOG ?? DEFAULT_LOG_FILE;
445
+ if (target === "off") return;
446
+ try {
447
+ mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
448
+ appendFileSync(target, `${JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
449
+ `);
450
+ } catch (error) {
451
+ logBroken = true;
452
+ process.stderr.write(`engine-mcp log disabled (${target}): ${String(error)}
453
+ `);
454
+ }
455
+ }
420
456
  function jsonRpcError(id, code, message) {
421
457
  return JSON.stringify({ id, jsonrpc: "2.0", error: { code, message } });
422
458
  }
@@ -439,12 +475,36 @@ function handleToolCall(params, manifestFile) {
439
475
  const scope = argumentsValue.scope ?? "mechanic";
440
476
  if (scope !== "request" && scope !== "mechanic")
441
477
  throw new Error("engine_search_capabilities scope must be 'request' or 'mechanic'.");
442
- return toolText(searchCapabilities(argumentsValue.situation, manifestFile, scope));
478
+ const response = searchCapabilities(argumentsValue.situation, manifestFile, scope);
479
+ logToolCall({
480
+ results: response.results.map((result) => result.symbol),
481
+ scope,
482
+ situation: argumentsValue.situation,
483
+ tool: name,
484
+ verdict: response.verdict
485
+ });
486
+ return toolText(response);
443
487
  }
444
488
  if (name === "engine_capability_detail") {
445
489
  if (typeof argumentsValue.symbol !== "string")
446
490
  throw new Error("engine_capability_detail requires a string 'symbol' argument.");
447
- return toolText(capabilityDetail(argumentsValue.symbol, manifestFile));
491
+ if (argumentsValue.importPath !== void 0 && typeof argumentsValue.importPath !== "string")
492
+ throw new Error("engine_capability_detail 'importPath' must be a string when given.");
493
+ const matches = capabilityDetails(
494
+ argumentsValue.symbol,
495
+ manifestFile,
496
+ argumentsValue.importPath
497
+ );
498
+ const detail = matches.length === 1 ? matches[0] : {
499
+ guidance: `${matches.length} packages export '${argumentsValue.symbol}'. Import the match whose summary fits; pass importPath to get one.`,
500
+ matches
501
+ };
502
+ logToolCall({
503
+ importPath: argumentsValue.importPath,
504
+ symbol: argumentsValue.symbol,
505
+ tool: name
506
+ });
507
+ return toolText(detail);
448
508
  }
449
509
  throw new Error(`Unknown engine MCP tool '${String(name)}'.`);
450
510
  }
@@ -464,7 +524,7 @@ function handleLine(line, manifestFile) {
464
524
  capabilities: { tools: { listChanged: false } },
465
525
  instructions: AUTHORING_INSTRUCTIONS,
466
526
  protocolVersion: "2025-06-18",
467
- serverInfo: { name: "threenative-engine-mcp", version: "0.2.2" }
527
+ serverInfo: { name: "threenative-engine-mcp", version: "0.2.4" }
468
528
  });
469
529
  }
470
530
  if (request.method === "tools/list") {
@@ -476,7 +536,9 @@ function handleLine(line, manifestFile) {
476
536
  }
477
537
  return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
478
538
  } catch (error) {
479
- return jsonRpcError(request.id, -32e3, error instanceof Error ? error.message : String(error));
539
+ const message = error instanceof Error ? error.message : String(error);
540
+ if (request.method === "tools/call") logToolCall({ error: message, request: request.params });
541
+ return jsonRpcError(request.id, -32e3, message);
480
542
  }
481
543
  }
482
544
  function runServer(manifestFile = defaultManifestPath()) {
@@ -498,4 +560,4 @@ if (entryPath !== void 0 && realpathSync(path.resolve(entryPath)) === realpathSy
498
560
  }
499
561
  }
500
562
 
501
- export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
563
+ export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilityDetails, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threenative-engine-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "license": "MIT",
5
5
  "description": "Offline MCP capability discovery for ThreeNative authoring agents.",
6
6
  "keywords": [