threenative-engine-mcp 0.2.0 → 0.2.2

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
@@ -12,3 +12,21 @@ with that mechanically explicit request and `scope: "request"`, then once per me
12
12
  situations return up to five. A genre label alone returns no guessed recipe. Every result includes
13
13
  `matchedSituation`, so an authoring agent can verify why it was selected instead of treating a
14
14
  lexical coincidence as engine guidance.
15
+
16
+ ## Which manifest the server reads
17
+
18
+ The first that exists wins, so the manifest can never drift from the engine the project runs:
19
+
20
+ 1. `THREENATIVE_CAPABILITIES_MANIFEST` — an explicit override, resolved against the working
21
+ directory.
22
+ 2. `node_modules/@threenative/core/capabilities.json` — found by walking up from the working
23
+ directory. The installed package ships the manifest its own build generated, so it always
24
+ describes the engine the project actually links; it moves exactly when the dependency does.
25
+ 3. `packages/core/capabilities.json` — found by walking up from the running server file, which
26
+ answers from the engine repository's own generated manifest when the server is launched from
27
+ the repository rather than from an installed package.
28
+ 4. `capabilities.json` in the working directory — the original default, kept for a bare checkout
29
+ with a committed copy and no installed engine package.
30
+
31
+ A project copy committed at scaffold time is therefore inert whenever the package is installed:
32
+ the two never need re-syncing by hand.
package/dist/index.d.ts CHANGED
@@ -6,13 +6,21 @@ interface ICapabilityEntry {
6
6
  readonly signature: string;
7
7
  readonly summary: string;
8
8
  readonly situations: readonly string[];
9
+ readonly aliases: readonly string[];
9
10
  readonly example: string;
10
11
  readonly constraints: readonly string[];
11
12
  readonly overrides?: readonly string[];
13
+ readonly requires?: readonly string[];
14
+ }
15
+ interface INotOwnedCapability {
16
+ readonly id: string;
17
+ readonly situations: readonly string[];
18
+ readonly guidance: string;
12
19
  }
13
20
  interface ICapabilityManifest {
14
21
  readonly version: number;
15
22
  readonly entries: readonly ICapabilityEntry[];
23
+ readonly notOwned: readonly INotOwnedCapability[];
16
24
  }
17
25
  interface ICapabilitySearchResult {
18
26
  readonly symbol: string;
@@ -20,7 +28,14 @@ interface ICapabilitySearchResult {
20
28
  readonly summary: string;
21
29
  readonly example: string;
22
30
  readonly constraints: readonly string[];
31
+ readonly requires?: readonly string[];
23
32
  readonly matchedSituation: string;
33
+ readonly score: number;
34
+ }
35
+ interface ICapabilitySearchResponse {
36
+ readonly verdict: "matched" | "none";
37
+ readonly results: readonly ICapabilitySearchResult[];
38
+ readonly guidance: string;
24
39
  }
25
40
  interface ICapabilityDetail extends ICapabilityEntry {
26
41
  }
@@ -44,9 +59,12 @@ interface IEngineTool {
44
59
  };
45
60
  }
46
61
  declare const ENGINE_MCP_TOOL_NAMES: readonly ["engine_search_capabilities", "engine_capability_detail"];
62
+ /** Chosen by the current-manifest threshold sweep: the highest recalled candidate at 0.27. */
63
+ declare const RELEVANCE_FLOOR = 0.27;
47
64
  declare function defaultManifestPath(cwd?: string): string;
48
65
  declare function loadCapabilityManifest(file?: string): ICapabilityManifest;
49
- declare function searchCapabilities(situation: string, manifestFile?: string, scope?: "mechanic" | "request"): readonly ICapabilitySearchResult[];
66
+ declare function capabilitySituationTokens(value: string): string[];
67
+ declare function searchCapabilities(situation: string, manifestFile?: string, scope?: "mechanic" | "request"): ICapabilitySearchResponse;
50
68
  declare function capabilityDetail(symbol: string, manifestFile?: string): ICapabilityDetail;
51
69
  declare function toolDefinitions(): readonly IEngineTool[];
52
70
  /**
@@ -57,4 +75,4 @@ declare function toolDefinitions(): readonly IEngineTool[];
57
75
  declare function handleLine(line: string, manifestFile: string): string | undefined;
58
76
  declare function runServer(manifestFile?: string): void;
59
77
 
60
- export { ENGINE_MCP_TOOL_NAMES, type ICapabilityDetail, type ICapabilityEntry, type ICapabilityManifest, type ICapabilitySearchResult, type IEngineTool, capabilityDetail, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync, readFileSync } from 'fs';
2
+ import { realpathSync, readFileSync, existsSync } from 'fs';
3
3
  import path from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
@@ -12,32 +12,143 @@ var DEFAULT_MANIFEST_FILE = "capabilities.json";
12
12
  var STOP_WORDS = /* @__PURE__ */ new Set([
13
13
  "a",
14
14
  "add",
15
+ "after",
16
+ "again",
17
+ "all",
18
+ "also",
15
19
  "an",
16
20
  "and",
21
+ "any",
22
+ "are",
17
23
  "around",
24
+ "as",
25
+ "at",
26
+ "be",
27
+ "because",
28
+ "been",
29
+ "being",
30
+ "both",
18
31
  "build",
32
+ "but",
19
33
  "by",
34
+ "can",
20
35
  "create",
36
+ "do",
37
+ "does",
38
+ "each",
39
+ "either",
40
+ "else",
41
+ "every",
21
42
  "for",
22
43
  "from",
23
44
  "game",
45
+ "get",
46
+ "give",
47
+ "had",
48
+ "has",
49
+ "have",
50
+ "how",
51
+ "if",
24
52
  "in",
25
53
  "into",
54
+ "is",
26
55
  "it",
56
+ "its",
57
+ "let",
58
+ "made",
27
59
  "make",
60
+ "may",
61
+ "might",
62
+ "must",
63
+ "no",
64
+ "nor",
65
+ "not",
28
66
  "of",
29
67
  "on",
30
- "that",
31
- "the",
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",
32
96
  "to",
97
+ "too",
33
98
  "use",
34
- "with"
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"
35
124
  ]);
36
125
  var MAX_COMPLETE_REQUEST_RESULTS = 15;
37
126
  var MAX_SITUATION_RESULTS = 5;
38
- 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.`;
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.";
39
130
  function defaultManifestPath(cwd = process.cwd()) {
40
- return path.resolve(cwd, process.env.THREENATIVE_CAPABILITIES_MANIFEST ?? DEFAULT_MANIFEST_FILE);
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);
41
152
  }
42
153
  function manifestError(file, reason) {
43
154
  return new Error(`TN_ENGINE_CAPABILITIES_MANIFEST: ${file}: ${reason}`);
@@ -46,14 +157,29 @@ function isRecord(value) {
46
157
  return typeof value === "object" && value !== null && !Array.isArray(value);
47
158
  }
48
159
  function validateManifest(value, file) {
49
- if (!isRecord(value) || typeof value.version !== "number" || !Array.isArray(value.entries)) {
50
- throw manifestError(file, "root must contain a numeric version and entries array");
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
+ );
51
165
  }
52
166
  for (const [index, raw] of value.entries.entries()) {
53
- 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.constraints) || !raw.situations.every((situation) => typeof situation === "string") || !raw.constraints.every((constraint) => typeof constraint === "string")) {
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")) {
54
168
  throw manifestError(file, `entry ${index} is malformed`);
55
169
  }
56
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
+ }
57
183
  return value;
58
184
  }
59
185
  function loadCapabilityManifest(file = defaultManifestPath()) {
@@ -71,15 +197,103 @@ function loadCapabilityManifest(file = defaultManifestPath()) {
71
197
  throw manifestError(file, `cannot parse JSON: ${String(error)}`);
72
198
  }
73
199
  }
74
- function tokens(value) {
75
- return value.toLocaleLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 1 && !STOP_WORDS.has(token)).map((token) => {
76
- if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
77
- if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss"))
78
- return token.slice(0, -1);
79
- return token;
80
- });
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 };
81
278
  }
82
- function situationScore(query, situations) {
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) {
83
297
  if (query.length === 0) return { matchedSituation: "", score: 0 };
84
298
  const queryText = query.join(" ");
85
299
  let best = 0;
@@ -98,9 +312,24 @@ function situationScore(query, situations) {
98
312
  }
99
313
  return { matchedSituation, score: best };
100
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
+ }
101
331
  function capabilitySearchKey(entry) {
102
332
  return `${entry.importPath}
103
- ${entry.signature}
104
333
  ${entry.summary}
105
334
  ${entry.situations.join("\n")}`;
106
335
  }
@@ -110,7 +339,18 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco
110
339
  const manifest = loadCapabilityManifest(manifestFile);
111
340
  const query = tokens(situation);
112
341
  const limit = scope === "request" ? MAX_COMPLETE_REQUEST_RESULTS : MAX_SITUATION_RESULTS;
113
- return manifest.entries.map((entry) => ({ entry, ...situationScore(query, entry.situations) })).filter(({ score }) => score > 0).sort(
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(
114
354
  (left, right) => right.score - left.score || `${left.entry.importPath}:${left.entry.symbol}`.localeCompare(
115
355
  `${right.entry.importPath}:${right.entry.symbol}`
116
356
  )
@@ -118,14 +358,22 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco
118
358
  (candidate, index, candidates) => candidates.findIndex(
119
359
  (other) => capabilitySearchKey(other.entry) === capabilitySearchKey(candidate.entry)
120
360
  ) === index
121
- ).slice(0, limit).map(({ entry, matchedSituation }) => ({
361
+ ).slice(0, limit).map(({ entry, matchedSituation, score }) => ({
122
362
  constraints: entry.constraints,
123
363
  example: entry.example,
124
364
  importPath: entry.importPath,
125
365
  matchedSituation,
366
+ ...entry.requires === void 0 ? {} : { requires: entry.requires },
367
+ score,
126
368
  summary: entry.summary,
127
369
  symbol: entry.symbol
128
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
+ };
129
377
  }
130
378
  function capabilityDetail(symbol, manifestFile = defaultManifestPath()) {
131
379
  if (typeof symbol !== "string" || symbol.trim().length === 0)
@@ -139,7 +387,7 @@ var TOOL_DEFINITIONS = [
139
387
  {
140
388
  annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
141
389
  name: "engine_search_capabilities",
142
- 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 explains every result.',
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.',
143
391
  inputSchema: {
144
392
  additionalProperties: false,
145
393
  properties: {
@@ -216,7 +464,7 @@ function handleLine(line, manifestFile) {
216
464
  capabilities: { tools: { listChanged: false } },
217
465
  instructions: AUTHORING_INSTRUCTIONS,
218
466
  protocolVersion: "2025-06-18",
219
- serverInfo: { name: "threenative-engine-mcp", version: "0.2.0" }
467
+ serverInfo: { name: "threenative-engine-mcp", version: "0.2.2" }
220
468
  });
221
469
  }
222
470
  if (request.method === "tools/list") {
@@ -250,4 +498,4 @@ if (entryPath !== void 0 && realpathSync(path.resolve(entryPath)) === realpathSy
250
498
  }
251
499
  }
252
500
 
253
- export { ENGINE_MCP_TOOL_NAMES, capabilityDetail, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
501
+ export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
package/package.json CHANGED
@@ -1,8 +1,26 @@
1
1
  {
2
2
  "name": "threenative-engine-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "description": "Offline MCP capability discovery for ThreeNative authoring agents.",
6
+ "keywords": [
7
+ "threenative",
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "three",
11
+ "threejs",
12
+ "agents",
13
+ "capabilities"
14
+ ],
15
+ "homepage": "https://github.com/ThreeNativeHQ/threenative#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/ThreeNativeHQ/threenative/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ThreeNativeHQ/threenative.git",
22
+ "directory": "packages/engine-mcp"
23
+ },
6
24
  "type": "module",
7
25
  "files": [
8
26
  "dist",