@threenative/core 0.2.0 → 0.3.1

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/capabilities.json +5292 -0
  4. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  5. package/dist/audio-BFiGneTL.d.ts +156 -0
  6. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  7. package/dist/game-XGrTzapq.d.ts +1166 -0
  8. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  9. package/dist/hot.d.ts +20 -2
  10. package/dist/hot.js +14 -2
  11. package/dist/index.d.ts +2108 -55
  12. package/dist/index.js +15645 -2222
  13. package/dist/net.d.ts +65 -0
  14. package/dist/net.js +643 -0
  15. package/dist/playtest.d.ts +37 -4
  16. package/dist/playtest.js +246 -546
  17. package/dist/react.d.ts +177 -0
  18. package/dist/react.js +635 -0
  19. package/dist/renderer-C6hqZpoG.d.ts +770 -0
  20. package/dist/ui-layer.d.ts +306 -0
  21. package/dist/ui-layer.js +425 -0
  22. package/dist/world.d.ts +254 -0
  23. package/dist/world.js +2686 -0
  24. package/gpl/LICENSE.GPL +117 -0
  25. package/gpl/convert.py +192 -0
  26. package/gpl/recipes/_common.py +169 -0
  27. package/gpl/recipes/bake_ao.py +111 -0
  28. package/gpl/recipes/decimate.py +64 -0
  29. package/gpl/recipes/retarget.py +131 -0
  30. package/gpl/recipes/unwrap.py +71 -0
  31. package/mcp/assets.mjs +5 -0
  32. package/mcp/blender-server.mjs +632 -0
  33. package/mcp/blender.mjs +27 -0
  34. package/mcp/engine-server.mjs +501 -0
  35. package/mcp/engine.mjs +31 -0
  36. package/mcp/install.d.mts +37 -0
  37. package/mcp/install.mjs +145 -0
  38. package/mcp/launch.mjs +72 -0
  39. package/mcp/sculpt.mjs +5 -0
  40. package/mcp/servers.d.mts +34 -0
  41. package/mcp/servers.mjs +160 -0
  42. package/package.json +76 -6
  43. package/patches/three@0.185.1.patch +522 -0
  44. package/scripts/apply-three-patch.mjs +297 -0
  45. package/scripts/ensure-mcp.mjs +43 -0
  46. package/scripts/postinstall.mjs +6 -0
  47. package/dist/audio-CEAw0w5y.d.ts +0 -35
  48. package/dist/game-DRt1Qhq3.d.ts +0 -429
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync, readFileSync, existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { createInterface } from 'readline';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ var ENGINE_MCP_TOOL_NAMES = [
8
+ "engine_search_capabilities",
9
+ "engine_capability_detail"
10
+ ];
11
+ var DEFAULT_MANIFEST_FILE = "capabilities.json";
12
+ var STOP_WORDS = /* @__PURE__ */ new Set([
13
+ "a",
14
+ "add",
15
+ "after",
16
+ "again",
17
+ "all",
18
+ "also",
19
+ "an",
20
+ "and",
21
+ "any",
22
+ "are",
23
+ "around",
24
+ "as",
25
+ "at",
26
+ "be",
27
+ "because",
28
+ "been",
29
+ "being",
30
+ "both",
31
+ "build",
32
+ "but",
33
+ "by",
34
+ "can",
35
+ "create",
36
+ "do",
37
+ "does",
38
+ "each",
39
+ "either",
40
+ "else",
41
+ "every",
42
+ "for",
43
+ "from",
44
+ "game",
45
+ "get",
46
+ "give",
47
+ "had",
48
+ "has",
49
+ "have",
50
+ "how",
51
+ "if",
52
+ "in",
53
+ "into",
54
+ "is",
55
+ "it",
56
+ "its",
57
+ "let",
58
+ "made",
59
+ "make",
60
+ "may",
61
+ "might",
62
+ "must",
63
+ "no",
64
+ "nor",
65
+ "not",
66
+ "of",
67
+ "on",
68
+ "once",
69
+ "one",
70
+ "only",
71
+ "or",
72
+ "other",
73
+ "our",
74
+ "own",
75
+ "put",
76
+ "rather",
77
+ "same",
78
+ "shall",
79
+ "should",
80
+ "since",
81
+ "so",
82
+ "some",
83
+ "such",
84
+ "take",
85
+ "than",
86
+ "their",
87
+ "them",
88
+ "then",
89
+ "there",
90
+ "these",
91
+ "they",
92
+ "this",
93
+ "those",
94
+ "though",
95
+ "thus",
96
+ "to",
97
+ "too",
98
+ "use",
99
+ "very",
100
+ "was",
101
+ "want",
102
+ "way",
103
+ "we",
104
+ "well",
105
+ "were",
106
+ "what",
107
+ "when",
108
+ "whenever",
109
+ "where",
110
+ "whether",
111
+ "which",
112
+ "while",
113
+ "who",
114
+ "whose",
115
+ "why",
116
+ "will",
117
+ "with",
118
+ "within",
119
+ "without",
120
+ "would",
121
+ "yet",
122
+ "you",
123
+ "your"
124
+ ]);
125
+ var MAX_COMPLETE_REQUEST_RESULTS = 15;
126
+ var MAX_SITUATION_RESULTS = 5;
127
+ var RELEVANCE_FLOOR = 0.27;
128
+ var AUTHORING_INSTRUCTIONS = `Before authoring, infer concrete gameplay mechanics. Preserve the request's distinctive fantasy: choose the smallest loop that uses its characteristic setting, traversal medium, or simulation instead of a generic character game with themed props, and search those implied mechanics even when the user did not name engine terms. Search the mechanically explicit complete request with scope "request", then each mechanic with scope "mechanic". A genre label alone is not a capability query: clarify or decompose it; do not assume a preset. Inspect capability detail and obey constraints before implementing. Capability detail is authoritative on platform support: never invent a platform limitation it does not state. A response with verdict "none" is an actionable answer: follow its guidance and write game-owned behavior in src/ instead of rephrasing the same request.`;
129
+ var GENERIC_GUIDANCE = "No installed engine capability matches this situation. Decompose it into concrete mechanics and write the game-owned behavior in your project's src/; inspect the relevant template AGENTS.md before adding a package.";
130
+ function defaultManifestPath(cwd = process.cwd()) {
131
+ const override = process.env.THREENATIVE_CAPABILITIES_MANIFEST;
132
+ if (override !== void 0 && override.trim().length > 0) return path.resolve(cwd, override);
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;
142
+ const parent = path.dirname(directory);
143
+ if (parent === directory) break;
144
+ }
145
+ for (let directory = path.dirname(fileURLToPath(import.meta.url)); ; directory = path.dirname(directory)) {
146
+ const repository = path.join(directory, "packages", "core", "capabilities.json");
147
+ if (existsSync(repository)) return repository;
148
+ const parent = path.dirname(directory);
149
+ if (parent === directory) break;
150
+ }
151
+ return path.resolve(cwd, DEFAULT_MANIFEST_FILE);
152
+ }
153
+ function manifestError(file, reason) {
154
+ return new Error(`TN_ENGINE_CAPABILITIES_MANIFEST: ${file}: ${reason}`);
155
+ }
156
+ function isRecord(value) {
157
+ return typeof value === "object" && value !== null && !Array.isArray(value);
158
+ }
159
+ function validateManifest(value, file) {
160
+ if (!isRecord(value) || value.version !== 2 || !Array.isArray(value.entries) || !Array.isArray(value.notOwned)) {
161
+ throw manifestError(
162
+ file,
163
+ "root must contain manifest version 2, entries array, and notOwned array"
164
+ );
165
+ }
166
+ 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")) {
168
+ throw manifestError(file, `entry ${index} is malformed`);
169
+ }
170
+ }
171
+ const notOwnedIds = /* @__PURE__ */ new Set();
172
+ for (const [index, raw] of value.notOwned.entries()) {
173
+ if (!isRecord(raw) || typeof raw.id !== "string" || raw.id.trim().length === 0 || !Array.isArray(raw.situations) || raw.situations.length === 0 || !raw.situations.every(
174
+ (situation) => typeof situation === "string" && situation.trim().length > 0
175
+ ) || typeof raw.guidance !== "string" || raw.guidance.trim().length === 0) {
176
+ throw manifestError(file, `notOwned ${index} is malformed`);
177
+ }
178
+ if (notOwnedIds.has(raw.id)) {
179
+ throw manifestError(file, `notOwned contains duplicate id '${raw.id}'`);
180
+ }
181
+ notOwnedIds.add(raw.id);
182
+ }
183
+ return value;
184
+ }
185
+ function loadCapabilityManifest(file = defaultManifestPath()) {
186
+ let raw;
187
+ try {
188
+ raw = readFileSync(file, "utf8");
189
+ } catch (error) {
190
+ throw manifestError(file, `cannot read committed manifest: ${String(error)}`);
191
+ }
192
+ try {
193
+ return validateManifest(JSON.parse(raw), file);
194
+ } catch (error) {
195
+ if (error instanceof Error && error.message.startsWith("TN_ENGINE_CAPABILITIES_MANIFEST:"))
196
+ throw error;
197
+ throw manifestError(file, `cannot parse JSON: ${String(error)}`);
198
+ }
199
+ }
200
+ function undouble(stem2) {
201
+ const last = stem2.at(-1);
202
+ return stem2.length > 3 && last !== void 0 && last === stem2.at(-2) && !"aeiou".includes(last) ? stem2.slice(0, -1) : stem2;
203
+ }
204
+ function stem(token) {
205
+ const stripped = suffixStripped(token);
206
+ return stripped.length > 3 && stripped.endsWith("e") ? stripped.slice(0, -1) : stripped;
207
+ }
208
+ function suffixStripped(token) {
209
+ if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
210
+ if (token.length > 4 && token.endsWith("ing")) return undouble(token.slice(0, -3));
211
+ if (token.length > 4 && token.endsWith("ed")) return undouble(token.slice(0, -2));
212
+ if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
213
+ return token;
214
+ }
215
+ function capabilitySituationTokens(value) {
216
+ return value.toLocaleLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 1 && !STOP_WORDS.has(token)).map(stem);
217
+ }
218
+ var tokens = capabilitySituationTokens;
219
+ function tokenWeights(entries) {
220
+ const frequency = /* @__PURE__ */ new Map();
221
+ let situations = 0;
222
+ for (const entry of entries) {
223
+ for (const situation of [...entry.situations, ...entry.aliases]) {
224
+ situations += 1;
225
+ for (const token of new Set(tokens(situation)))
226
+ frequency.set(token, (frequency.get(token) ?? 0) + 1);
227
+ }
228
+ }
229
+ const weights = /* @__PURE__ */ new Map();
230
+ for (const [token, count] of frequency) weights.set(token, Math.log(1 + situations / count));
231
+ return weights;
232
+ }
233
+ function weightOf(weights, token) {
234
+ return weights.get(token) ?? 0;
235
+ }
236
+ var DISTINCTIVE_SITUATION_SHARE = 0.02;
237
+ var DISTINCTIVE_FLOOR = Math.log(1 + 1 / DISTINCTIVE_SITUATION_SHARE);
238
+ var AGREEMENT_WEIGHT = 0.4;
239
+ var LONE_WORD_COVERAGE = 0.22;
240
+ function agreement(situation, query) {
241
+ const matched = situation.filter((token) => query.has(token)).length;
242
+ return 1 + AGREEMENT_WEIGHT * Math.max(0, matched - 1);
243
+ }
244
+ function scorePhrase(value, queryTokens, queryText, weights) {
245
+ const phrase = tokens(value);
246
+ const unique = [...new Set(phrase)];
247
+ const total = unique.reduce((sum, token) => sum + weightOf(weights, token), 0);
248
+ const matched = unique.filter((token) => queryTokens.has(token)).reduce((sum, token) => sum + weightOf(weights, token), 0);
249
+ const phraseBonus = phrase.join(" ").includes(queryText) || queryText.includes(phrase.join(" ")) ? 1 : 0;
250
+ if (total === 0 || matched === 0 && phraseBonus === 0) return void 0;
251
+ const coverage = matched / total;
252
+ const agreed = agreement(unique, queryTokens);
253
+ return { agreed, coverage, matched, phraseBonus, score: coverage * agreed + phraseBonus };
254
+ }
255
+ function isDistinctivePhrase(candidate) {
256
+ if (candidate.phraseBonus > 0) return true;
257
+ if (candidate.matched < DISTINCTIVE_FLOOR) return false;
258
+ return !(candidate.agreed === 1 && candidate.coverage < LONE_WORD_COVERAGE);
259
+ }
260
+ function scoreReadableSituations(situations, queryTokens, queryText, weights) {
261
+ let best = 0;
262
+ let matchedSituation = "";
263
+ let bestReadableScore = 0;
264
+ let bestReadableSituation = situations[0] ?? "";
265
+ for (const situation of situations) {
266
+ const candidate = scorePhrase(situation, queryTokens, queryText, weights);
267
+ if (candidate === void 0) continue;
268
+ if (candidate.score > bestReadableScore) {
269
+ bestReadableScore = candidate.score;
270
+ bestReadableSituation = situation;
271
+ }
272
+ if (isDistinctivePhrase(candidate) && candidate.score > best) {
273
+ best = candidate.score;
274
+ matchedSituation = situation;
275
+ }
276
+ }
277
+ return { best, bestReadableSituation, matchedSituation };
278
+ }
279
+ function scoreAliases(aliases, queryTokens, queryText, weights) {
280
+ let best = 0;
281
+ for (const alias of aliases) {
282
+ const candidate = scorePhrase(alias, queryTokens, queryText, weights);
283
+ if (candidate !== void 0) best = Math.max(best, candidate.score);
284
+ }
285
+ return best;
286
+ }
287
+ function situationScore(query, situations, aliases, weights) {
288
+ if (query.length === 0) return { matchedSituation: "", score: 0 };
289
+ const queryTokens = new Set(query);
290
+ const queryText = query.join(" ");
291
+ const readable = scoreReadableSituations(situations, queryTokens, queryText, weights);
292
+ const best = Math.max(readable.best, scoreAliases(aliases, queryTokens, queryText, weights));
293
+ const matchedSituation = readable.matchedSituation.length > 0 ? readable.matchedSituation : best >= RELEVANCE_FLOOR ? readable.bestReadableSituation : "";
294
+ return matchedSituation.length > 0 ? { matchedSituation, score: best } : { matchedSituation: "", score: 0 };
295
+ }
296
+ function notOwnedSituationScore(query, situations) {
297
+ if (query.length === 0) return { matchedSituation: "", score: 0 };
298
+ const queryText = query.join(" ");
299
+ let best = 0;
300
+ let matchedSituation = "";
301
+ for (const situation of situations) {
302
+ const phrase = tokens(situation);
303
+ const overlap = new Set(phrase.filter((token) => query.includes(token))).size;
304
+ const score = overlap / Math.max(query.length, phrase.length);
305
+ const phraseBonus = phrase.join(" ").includes(queryText) || queryText.includes(phrase.join(" ")) ? 1 : 0;
306
+ if (overlap < (query.length >= 4 ? 2 : 1) && phraseBonus === 0) continue;
307
+ const candidate = score + phraseBonus;
308
+ if (candidate > best) {
309
+ best = candidate;
310
+ matchedSituation = situation;
311
+ }
312
+ }
313
+ return { matchedSituation, score: best };
314
+ }
315
+ function notOwnedMatch(manifest, query) {
316
+ return manifest.notOwned.map((entry) => ({ entry, ...notOwnedSituationScore(query, entry.situations) })).filter(
317
+ ({ matchedSituation, score }) => matchedSituation.length > 0 && score >= RELEVANCE_FLOOR
318
+ ).sort(
319
+ (left, right) => right.score - left.score || left.entry.id.localeCompare(right.entry.id)
320
+ )[0];
321
+ }
322
+ function isSpecificNotOwnedMatch(query, matchedSituation) {
323
+ const queryText = query.join(" ");
324
+ const phrase = tokens(matchedSituation);
325
+ const phraseText = phrase.join(" ");
326
+ if (queryText === phraseText) return true;
327
+ if (query.length === 1 && phrase.length <= 2 && phrase.includes(query[0] ?? "")) return true;
328
+ if (query.length > phrase.length + 1) return false;
329
+ return new Set(phrase.filter((token) => query.includes(token))).size >= 2;
330
+ }
331
+ function capabilitySearchKey(entry) {
332
+ return `${entry.importPath}
333
+ ${entry.summary}
334
+ ${entry.situations.join("\n")}`;
335
+ }
336
+ function searchCapabilities(situation, manifestFile = defaultManifestPath(), scope = "mechanic") {
337
+ if (typeof situation !== "string" || situation.trim().length === 0)
338
+ throw new Error("engine_search_capabilities requires a non-empty situation string.");
339
+ const manifest = loadCapabilityManifest(manifestFile);
340
+ const query = tokens(situation);
341
+ const limit = scope === "request" ? MAX_COMPLETE_REQUEST_RESULTS : MAX_SITUATION_RESULTS;
342
+ const weights = tokenWeights(manifest.entries);
343
+ const notOwned = notOwnedMatch(manifest, query);
344
+ if (notOwned !== void 0 && isSpecificNotOwnedMatch(query, notOwned.matchedSituation)) {
345
+ return {
346
+ guidance: notOwned.entry.guidance,
347
+ results: [],
348
+ verdict: "none"
349
+ };
350
+ }
351
+ const results = manifest.entries.map((entry) => ({ entry, ...situationScore(query, entry.situations, entry.aliases, weights) })).filter(
352
+ ({ matchedSituation, score }) => matchedSituation.length > 0 && score >= RELEVANCE_FLOOR
353
+ ).sort(
354
+ (left, right) => right.score - left.score || `${left.entry.importPath}:${left.entry.symbol}`.localeCompare(
355
+ `${right.entry.importPath}:${right.entry.symbol}`
356
+ )
357
+ ).filter(
358
+ (candidate, index, candidates) => candidates.findIndex(
359
+ (other) => capabilitySearchKey(other.entry) === capabilitySearchKey(candidate.entry)
360
+ ) === index
361
+ ).slice(0, limit).map(({ entry, matchedSituation, score }) => ({
362
+ constraints: entry.constraints,
363
+ example: entry.example,
364
+ importPath: entry.importPath,
365
+ matchedSituation,
366
+ ...entry.requires === void 0 ? {} : { requires: entry.requires },
367
+ score,
368
+ summary: entry.summary,
369
+ symbol: entry.symbol
370
+ }));
371
+ if (results.length > 0) return { guidance: "", results, verdict: "matched" };
372
+ return {
373
+ guidance: notOwned?.entry.guidance ?? GENERIC_GUIDANCE,
374
+ results: [],
375
+ verdict: "none"
376
+ };
377
+ }
378
+ function capabilityDetail(symbol, manifestFile = defaultManifestPath()) {
379
+ if (typeof symbol !== "string" || symbol.trim().length === 0)
380
+ 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;
385
+ }
386
+ var TOOL_DEFINITIONS = [
387
+ {
388
+ annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
389
+ name: "engine_search_capabilities",
390
+ description: 'Search the installed engine by concrete gameplay mechanic. Decompose genres first. Use scope "request" for the mechanically explicit full request and "mechanic" for each focused search; matchedSituation and score explain every result. The response verdict "none" is an actionable answer with guidance, not a failed search.',
391
+ inputSchema: {
392
+ additionalProperties: false,
393
+ properties: {
394
+ scope: {
395
+ description: "Use request for the complete cross-system game request; use mechanic or omit it for one mechanic.",
396
+ enum: ["request", "mechanic"],
397
+ type: "string"
398
+ },
399
+ situation: { type: "string" }
400
+ },
401
+ required: ["situation"],
402
+ type: "object"
403
+ }
404
+ },
405
+ {
406
+ annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
407
+ name: "engine_capability_detail",
408
+ description: "Inspect one engine capability's import, signature, example, constraints, and overrides.",
409
+ inputSchema: {
410
+ additionalProperties: false,
411
+ properties: { symbol: { type: "string" } },
412
+ required: ["symbol"],
413
+ type: "object"
414
+ }
415
+ }
416
+ ];
417
+ function toolDefinitions() {
418
+ return TOOL_DEFINITIONS;
419
+ }
420
+ function jsonRpcError(id, code, message) {
421
+ return JSON.stringify({ id, jsonrpc: "2.0", error: { code, message } });
422
+ }
423
+ function jsonRpcResult(id, result) {
424
+ return JSON.stringify({ id, jsonrpc: "2.0", result });
425
+ }
426
+ function writeLine(value) {
427
+ process.stdout.write(`${value}
428
+ `);
429
+ }
430
+ function toolText(value) {
431
+ return { content: [{ text: JSON.stringify(value, null, 2), type: "text" }] };
432
+ }
433
+ function handleToolCall(params, manifestFile) {
434
+ const name = params.name;
435
+ const argumentsValue = isRecord(params.arguments) ? params.arguments : {};
436
+ if (name === "engine_search_capabilities") {
437
+ if (typeof argumentsValue.situation !== "string")
438
+ throw new Error("engine_search_capabilities requires a string 'situation' argument.");
439
+ const scope = argumentsValue.scope ?? "mechanic";
440
+ if (scope !== "request" && scope !== "mechanic")
441
+ throw new Error("engine_search_capabilities scope must be 'request' or 'mechanic'.");
442
+ return toolText(searchCapabilities(argumentsValue.situation, manifestFile, scope));
443
+ }
444
+ if (name === "engine_capability_detail") {
445
+ if (typeof argumentsValue.symbol !== "string")
446
+ throw new Error("engine_capability_detail requires a string 'symbol' argument.");
447
+ return toolText(capabilityDetail(argumentsValue.symbol, manifestFile));
448
+ }
449
+ throw new Error(`Unknown engine MCP tool '${String(name)}'.`);
450
+ }
451
+ function handleLine(line, manifestFile) {
452
+ if (line.trim().length === 0) return void 0;
453
+ let request;
454
+ try {
455
+ request = JSON.parse(line);
456
+ } catch (error) {
457
+ return jsonRpcError(null, -32700, `Invalid JSON: ${String(error)}`);
458
+ }
459
+ if (!isRecord(request) || request.id === void 0 || typeof request.method !== "string")
460
+ return void 0;
461
+ try {
462
+ if (request.method === "initialize") {
463
+ return jsonRpcResult(request.id, {
464
+ capabilities: { tools: { listChanged: false } },
465
+ instructions: AUTHORING_INSTRUCTIONS,
466
+ protocolVersion: "2025-06-18",
467
+ serverInfo: { name: "threenative-engine-mcp", version: "0.2.1" }
468
+ });
469
+ }
470
+ if (request.method === "tools/list") {
471
+ return jsonRpcResult(request.id, { tools: TOOL_DEFINITIONS });
472
+ }
473
+ if (request.method === "tools/call") {
474
+ if (!isRecord(request.params)) throw new Error("tools/call requires an object params value.");
475
+ return jsonRpcResult(request.id, handleToolCall(request.params, manifestFile));
476
+ }
477
+ return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
478
+ } catch (error) {
479
+ return jsonRpcError(request.id, -32e3, error instanceof Error ? error.message : String(error));
480
+ }
481
+ }
482
+ function runServer(manifestFile = defaultManifestPath()) {
483
+ loadCapabilityManifest(manifestFile);
484
+ const input = createInterface({ input: process.stdin });
485
+ input.on("line", (line) => {
486
+ const response = handleLine(line, manifestFile);
487
+ if (response !== void 0) writeLine(response);
488
+ });
489
+ }
490
+ var entryPath = process.argv[1];
491
+ if (entryPath !== void 0 && realpathSync(path.resolve(entryPath)) === realpathSync(fileURLToPath(import.meta.url))) {
492
+ try {
493
+ runServer();
494
+ } catch (error) {
495
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
496
+ `);
497
+ process.exitCode = 1;
498
+ }
499
+ }
500
+
501
+ export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
package/mcp/engine.mjs ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { launchMcpServer } from "./launch.mjs";
6
+ import { MCP_PACKAGES } from "./servers.mjs";
7
+
8
+ // The capability server must answer with the manifest of the engine the game actually runs, and
9
+ // the copy this package ships is generated by the same build as that engine. Prefer it over any
10
+ // copy the project committed: a committed snapshot drifts the moment the engine dependency moves.
11
+ // With no bundled copy (a stripped install), fall through to the server's own defaults.
12
+ const bundled = path.resolve(fileURLToPath(import.meta.url), "..", "..", "capabilities.json");
13
+ const env = existsSync(bundled)
14
+ ? Object.fromEntries([["THREENATIVE_CAPABILITIES_MANIFEST", bundled]])
15
+ : {};
16
+
17
+ const localServer = path.resolve(fileURLToPath(import.meta.url), "..", "engine-server.mjs");
18
+ if (existsSync(localServer)) {
19
+ const { runServer } = await import("./engine-server.mjs");
20
+ runServer(env.THREENATIVE_CAPABILITIES_MANIFEST);
21
+ } else {
22
+ // Say so. This branch fetches a package over the network, and when the network is slow, absent
23
+ // or the version unpublished it produces no output at all — the host simply waits. A CI run
24
+ // spent 30 s here twice and reported `threenative-engine initialize timed out after 30000ms`
25
+ // with empty stderr, which named neither the fallback nor the reason for it. The bundled server
26
+ // is missing here for a reason worth reporting even when the fetch then succeeds.
27
+ process.stderr.write(
28
+ `TN_MCP_ENGINE_FALLBACK: no bundled capability server at ${localServer}; fetching ${MCP_PACKAGES.engine.name}@${MCP_PACKAGES.engine.version} over the network instead. A packaged @threenative/core should always carry the bundle.\n`,
29
+ );
30
+ await launchMcpServer({ ...MCP_PACKAGES.engine, env });
31
+ }
@@ -0,0 +1,37 @@
1
+ // Types for `install.mjs`, the same contract `servers.d.mts` provides for `servers.mjs`. That file
2
+ // is plain JavaScript by design — `@threenative/core`'s postinstall runs it before anything is
3
+ // built — so it can never be a `.ts`. Without this declaration every consumer reached for
4
+ // `@ts-expect-error` and then retyped the host table locally.
5
+
6
+ /** One project-scoped host config the installer writes. */
7
+ export interface IMcpHost {
8
+ readonly file: string;
9
+ readonly format: string;
10
+ readonly id: string;
11
+ readonly label: string;
12
+ readonly seed?: Readonly<Record<string, unknown>>;
13
+ }
14
+
15
+ /** What a single config write did, or why it was left alone. */
16
+ export type McpConfigOutcome =
17
+ | "conflict"
18
+ | "created"
19
+ | "unchanged"
20
+ | "unreadable"
21
+ | "unwritable"
22
+ | "updated";
23
+
24
+ export declare const MCP_HOSTS: readonly IMcpHost[];
25
+ export declare function installTarget(
26
+ environment?: NodeJS.ProcessEnv,
27
+ cwd?: string,
28
+ ): string | undefined;
29
+ export declare function ensureJsonMcpConfig(
30
+ target: string,
31
+ file: string,
32
+ format: string,
33
+ seed?: Readonly<Record<string, unknown>>,
34
+ ): McpConfigOutcome;
35
+ export declare function ensureMcpConfig(target: string): McpConfigOutcome;
36
+ export declare function ensureCodexMcpConfig(target: string): McpConfigOutcome;
37
+ export declare function ensureHostMcpConfigs(target: string): Map<string, McpConfigOutcome>;