blueprint-extractor-mcp 8.0.1 → 8.1.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.
package/README.md CHANGED
@@ -86,6 +86,10 @@ codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
86
86
  <tr><td><b>OpenCode</b></td></tr>
87
87
  <tr><td>
88
88
 
89
+ ```bash
90
+ npm install --prefix ~/.config/opencode --save-exact blueprint-extractor-mcp@latest
91
+ ```
92
+
89
93
  ```jsonc
90
94
  // ~/.config/opencode/opencode.json
91
95
  {
@@ -93,7 +97,7 @@ codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
93
97
  "mcp": {
94
98
  "blueprint-extractor": {
95
99
  "type": "local",
96
- "command": ["npx", "-y", "blueprint-extractor-mcp@latest"],
100
+ "command": ["/absolute/path/to/.config/opencode/node_modules/.bin/blueprint-extractor-mcp"],
97
101
  "enabled": true,
98
102
  "environment": {
99
103
  "UE_REMOTE_CONTROL_PORT": "30010"
@@ -106,7 +110,7 @@ codex mcp add --env UE_REMOTE_CONTROL_PORT=30010 \
106
110
  </td></tr>
107
111
  </table>
108
112
 
109
- > On Windows, wrap `npx` with `cmd /c` if your shell requires it.
113
+ > On Windows, point `command` at `C:\Users\you\.config\opencode\node_modules\.bin\blueprint-extractor-mcp.cmd`.
110
114
 
111
115
  <br>
112
116
 
@@ -104,7 +104,7 @@ export function extractExtraContent(result) {
104
104
  if (!isPlainObject(result) || !Array.isArray(result.content)) {
105
105
  return [];
106
106
  }
107
- return result.content.filter((candidate) => (isContentBlock(candidate) && candidate.type !== 'text'));
107
+ return result.content.filter((candidate) => (isContentBlock(candidate)));
108
108
  }
109
109
  export function maybeBoolean(...values) {
110
110
  for (const value of values) {
@@ -1,5 +1,17 @@
1
1
  import { isPlainObject } from './formatting.js';
2
2
  export function createToolResultNormalizers({ taskAwareTools, classifyRecoverableToolFailure, }) {
3
+ function serializeSuccessEnvelope(envelope) {
4
+ try {
5
+ return JSON.stringify(envelope);
6
+ }
7
+ catch {
8
+ return JSON.stringify({
9
+ success: true,
10
+ operation: typeof envelope.operation === 'string' ? envelope.operation : 'unknown_operation',
11
+ message: 'Unable to serialize tool result payload.',
12
+ });
13
+ }
14
+ }
3
15
  function extractNonTextContent(existingResult) {
4
16
  if (!existingResult || !Array.isArray(existingResult.content)) {
5
17
  return [];
@@ -132,7 +144,10 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
132
144
  ...(taskAwareTools.has(toolName) ? { execution: inferExecutionMetadata(toolName, basePayload) } : {}),
133
145
  };
134
146
  return {
135
- content: extraContent,
147
+ content: [
148
+ { type: 'text', text: serializeSuccessEnvelope(envelope) },
149
+ ...extraContent,
150
+ ],
136
151
  structuredContent: envelope,
137
152
  };
138
153
  }
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer, RegisteredPrompt } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  export type PromptCatalogEntry = {
4
4
  title: string;
@@ -105,4 +105,4 @@ export declare const designSpecSchemaExample: {
105
105
  };
106
106
  };
107
107
  export declare const promptCatalog: Record<string, PromptCatalogEntry>;
108
- export declare function registerPromptCatalog(server: Pick<McpServer, 'registerPrompt'>): void;
108
+ export declare function registerPromptCatalog(server: Pick<McpServer, 'registerPrompt'>): Map<string, RegisteredPrompt>;
@@ -342,8 +342,9 @@ export const promptCatalog = {
342
342
  },
343
343
  };
344
344
  export function registerPromptCatalog(server) {
345
+ const registeredPrompts = new Map();
345
346
  for (const [name, prompt] of Object.entries(promptCatalog)) {
346
- server.registerPrompt(name, {
347
+ const registeredPrompt = server.registerPrompt(name, {
347
348
  title: prompt.title,
348
349
  description: prompt.description,
349
350
  argsSchema: prompt.args,
@@ -357,5 +358,7 @@ export function registerPromptCatalog(server) {
357
358
  },
358
359
  }],
359
360
  }));
361
+ registeredPrompts.set(name, registeredPrompt);
360
362
  }
363
+ return registeredPrompts;
361
364
  }
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer, type RegisteredPrompt } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { AutomationControllerLike } from './automation-controller.js';
3
3
  type JsonSubsystemCaller = (method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>;
4
4
  type RegisterServerResourcesOptions = {
@@ -6,5 +6,8 @@ type RegisterServerResourcesOptions = {
6
6
  automationController: AutomationControllerLike;
7
7
  callSubsystemJson: JsonSubsystemCaller;
8
8
  };
9
- export declare function registerServerResources({ server, automationController, callSubsystemJson, }: RegisterServerResourcesOptions): void;
9
+ type RegisterServerResourcesResult = {
10
+ registeredPrompts: Map<string, RegisteredPrompt>;
11
+ };
12
+ export declare function registerServerResources({ server, automationController, callSubsystemJson, }: RegisterServerResourcesOptions): RegisterServerResourcesResult;
10
13
  export {};
@@ -8,5 +8,6 @@ export function registerServerResources({ server, automationController, callSubs
8
8
  automationController,
9
9
  callSubsystemJson,
10
10
  });
11
- registerPromptCatalog(server);
11
+ const registeredPrompts = registerPromptCatalog(server);
12
+ return { registeredPrompts };
12
13
  }
@@ -39,6 +39,31 @@ export function createBlueprintExtractorServer(client, projectController = new P
39
39
  instructions: serverInstructions,
40
40
  });
41
41
  const defaultOutputSchema = toolResultSchema.catchall(z.unknown());
42
+ let promptsEnabled = true;
43
+ function setPromptSurfaceEnabled(registeredPrompts, enabled) {
44
+ if (promptsEnabled === enabled) {
45
+ return false;
46
+ }
47
+ for (const registeredPrompt of registeredPrompts.values()) {
48
+ if (enabled) {
49
+ registeredPrompt.enable();
50
+ }
51
+ else {
52
+ registeredPrompt.disable();
53
+ }
54
+ }
55
+ promptsEnabled = enabled;
56
+ return true;
57
+ }
58
+ function isOpenCodeClient(clientVersion) {
59
+ if (!clientVersion || typeof clientVersion !== 'object') {
60
+ return false;
61
+ }
62
+ return ['name', 'title', 'description', 'websiteUrl'].some((field) => {
63
+ const value = clientVersion[field];
64
+ return typeof value === 'string' && /opencode/i.test(value);
65
+ });
66
+ }
42
67
  // Direct editor path — preserves callSubsystemJson error-checking layer and
43
68
  // avoids recursive executor routing while resolving commandlet fallback inputs.
44
69
  const directCallSubsystemJson = (method, params, options) => (callSubsystemJsonWithClient(effectiveClient, method, params, options));
@@ -105,11 +130,14 @@ export function createBlueprintExtractorServer(client, projectController = new P
105
130
  workspaceProjectPath: await activeEditorSession?.getWorkspaceProjectPath(),
106
131
  });
107
132
  }
108
- registerServerResources({
133
+ const { registeredPrompts } = registerServerResources({
109
134
  server,
110
135
  automationController,
111
136
  callSubsystemJson,
112
137
  });
138
+ // OpenCode eagerly fetches every prompt on startup and currently surfaces a
139
+ // client-side drain-listener warning against larger prompt catalogs.
140
+ setPromptSurfaceEnabled(registeredPrompts, false);
113
141
  registerServerTools({
114
142
  server,
115
143
  client: effectiveClient,
@@ -200,7 +228,12 @@ export function createBlueprintExtractorServer(client, projectController = new P
200
228
  });
201
229
  // Wire oninitialized to detect client capabilities
202
230
  server.server.oninitialized = () => {
203
- const caps = server.server.getClientCapabilities();
231
+ const lowLevelServer = server.server;
232
+ const caps = lowLevelServer.getClientCapabilities();
233
+ const promptSurfaceChanged = setPromptSurfaceEnabled(registeredPrompts, !isOpenCodeClient(lowLevelServer.getClientVersion?.()));
234
+ if (promptSurfaceChanged && caps?.prompts?.listChanged) {
235
+ server.sendPromptListChanged();
236
+ }
204
237
  if (caps?.tools?.listChanged) {
205
238
  toolSurfaceManager.activateProfile('default');
206
239
  }
@@ -47,6 +47,9 @@ export function registerProjectControlTools({ server, client, projectController,
47
47
  engineRoot: previousEditor?.engineRoot ?? fallback.engineRoot,
48
48
  target: previousEditor?.editorTarget ?? fallback.target,
49
49
  });
50
+ const matchesResolvedEditor = (entry, resolved) => (filesystemPathsEqual(entry.projectFilePath, resolved.projectPath)
51
+ && (!resolved.engineRoot || !entry.engineRoot || filesystemPathsEqual(entry.engineRoot, resolved.engineRoot))
52
+ && (!resolved.target || !entry.editorTarget || entry.editorTarget === resolved.target));
50
53
  const recoverEditorViaHostRelaunch = async (request) => {
51
54
  const recovery = {
52
55
  strategy: 'host_relaunch_after_failed_graceful_restart',
@@ -452,18 +455,51 @@ export function registerProjectControlTools({ server, client, projectController,
452
455
  if (!resolved.engineRoot || !resolved.projectPath) {
453
456
  throw explainProjectResolutionFailure('launch_editor requires engine_root and project_path', resolved);
454
457
  }
458
+ const resolvedProjectPath = resolved.projectPath;
459
+ const resolvedEngineRoot = resolved.engineRoot;
460
+ const resolvedTarget = resolved.target;
461
+ if (activeEditorSession) {
462
+ const matchingEditors = (await activeEditorSession.listRunningEditors()).filter((entry) => matchesResolvedEditor(entry, {
463
+ projectPath: resolvedProjectPath,
464
+ engineRoot: resolvedEngineRoot,
465
+ target: resolvedTarget,
466
+ }));
467
+ if (matchingEditors.length > 1) {
468
+ throw new Error(`Multiple running editors already match "${resolvedProjectPath}". `
469
+ + 'Call list_running_editors and select_editor instead of launch_editor.');
470
+ }
471
+ if (matchingEditors.length === 1) {
472
+ try {
473
+ const bound = await activeEditorSession.selectEditor({ instanceId: matchingEditors[0].instanceId });
474
+ clearProjectAutomationContext();
475
+ const activeEditor = toLabeledActiveEditor(bound);
476
+ return jsonToolSuccess({
477
+ success: true,
478
+ operation: 'launch_editor',
479
+ launched: false,
480
+ reusedExistingEditor: true,
481
+ message: 'Bound the existing matching editor instead of launching a new process.',
482
+ inputResolution: buildInputResolution(resolved),
483
+ activeEditor,
484
+ });
485
+ }
486
+ catch {
487
+ // Fall through to a real launch when the registry entry is stale or not yet responsive.
488
+ }
489
+ }
490
+ }
455
491
  const launched = await projectController.launchEditor({
456
- engineRoot: resolved.engineRoot,
457
- projectPath: resolved.projectPath,
492
+ engineRoot: resolvedEngineRoot,
493
+ projectPath: resolvedProjectPath,
458
494
  });
459
495
  clearProjectAutomationContext();
460
496
  let activeEditor;
461
497
  if (activeEditorSession) {
462
498
  const bound = await activeEditorSession.bindLaunchedEditor({
463
499
  processId: launched.processId,
464
- projectPath: resolved.projectPath,
465
- engineRoot: resolved.engineRoot,
466
- target: resolved.target,
500
+ projectPath: resolvedProjectPath,
501
+ engineRoot: resolvedEngineRoot,
502
+ target: resolvedTarget,
467
503
  timeoutMs: reconnect_timeout_seconds * 1000,
468
504
  });
469
505
  activeEditor = toLabeledActiveEditor(bound);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blueprint-extractor-mcp",
3
- "version": "8.0.1",
3
+ "version": "8.1.1",
4
4
  "description": "MCP server for the Unreal Engine BlueprintExtractor plugin over Remote Control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",