blueprint-extractor-mcp 1.2.0 → 1.4.0

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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compacts Blueprint extraction JSON by stripping low-value fields and minifying.
3
+ * Reduces size by ~50-70% for LLM consumption.
4
+ */
5
+ /**
6
+ * Compacts a parsed Blueprint extraction result.
7
+ * - Removes positional data (posX, posY), GUIDs, empty fields
8
+ * - Replaces nodeGuid with sequential short IDs (n0, n1, ...)
9
+ * - Rewrites connection references to use short IDs
10
+ * - Replaces exec pin type objects with the string "exec"
11
+ */
12
+ export declare function compactBlueprint(data: unknown): unknown;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Compacts Blueprint extraction JSON by stripping low-value fields and minifying.
3
+ * Reduces size by ~50-70% for LLM consumption.
4
+ */
5
+ /**
6
+ * Compacts a parsed Blueprint extraction result.
7
+ * - Removes positional data (posX, posY), GUIDs, empty fields
8
+ * - Replaces nodeGuid with sequential short IDs (n0, n1, ...)
9
+ * - Rewrites connection references to use short IDs
10
+ * - Replaces exec pin type objects with the string "exec"
11
+ */
12
+ export function compactBlueprint(data) {
13
+ if (!data || typeof data !== 'object')
14
+ return data;
15
+ const root = data;
16
+ const bp = root.blueprint;
17
+ if (!bp)
18
+ return data;
19
+ const functions = bp.functions;
20
+ if (!Array.isArray(functions))
21
+ return data;
22
+ for (const graph of functions) {
23
+ compactGraph(graph);
24
+ }
25
+ return data;
26
+ }
27
+ function compactGraph(graph) {
28
+ // Remove graphGuid at graph level
29
+ delete graph.graphGuid;
30
+ const nodes = graph.nodes;
31
+ if (!Array.isArray(nodes))
32
+ return;
33
+ // Build guidToShortId map
34
+ const guidToShortId = new Map();
35
+ for (let i = 0; i < nodes.length; i++) {
36
+ const guid = nodes[i].nodeGuid;
37
+ if (guid) {
38
+ guidToShortId.set(guid, `n${i}`);
39
+ }
40
+ }
41
+ // Process each node
42
+ for (let i = 0; i < nodes.length; i++) {
43
+ const node = nodes[i];
44
+ // Replace nodeGuid with short ID
45
+ delete node.nodeGuid;
46
+ node.id = `n${i}`;
47
+ // Remove positional data
48
+ delete node.posX;
49
+ delete node.posY;
50
+ // Remove empty nodeComment
51
+ if (node.nodeComment === '') {
52
+ delete node.nodeComment;
53
+ }
54
+ // Process pins
55
+ const pins = node.pins;
56
+ if (Array.isArray(pins)) {
57
+ for (const pin of pins) {
58
+ compactPin(pin, guidToShortId);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ function compactPin(pin, guidToShortId) {
64
+ // Remove pinId
65
+ delete pin.pinId;
66
+ // Remove autogeneratedDefaultValue
67
+ delete pin.autogeneratedDefaultValue;
68
+ // Remove empty defaultValue
69
+ if (pin.defaultValue === '') {
70
+ delete pin.defaultValue;
71
+ }
72
+ // Process type object
73
+ const type = pin.type;
74
+ if (type && typeof type === 'object') {
75
+ // Remove empty sub_category
76
+ if (type.sub_category === '') {
77
+ delete type.sub_category;
78
+ }
79
+ // Replace exec pin type with string "exec"
80
+ if (type.category === 'exec') {
81
+ pin.type = 'exec';
82
+ }
83
+ }
84
+ // Rewrite connection nodeGuid references to short IDs
85
+ const connections = pin.connections;
86
+ if (Array.isArray(connections)) {
87
+ if (connections.length === 0) {
88
+ // Remove empty connections arrays
89
+ delete pin.connections;
90
+ }
91
+ else {
92
+ for (const conn of connections) {
93
+ const connGuid = conn.nodeGuid;
94
+ if (connGuid && guidToShortId.has(connGuid)) {
95
+ conn.nodeGuid = guidToShortId.get(connGuid);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
package/dist/index.js CHANGED
@@ -3,10 +3,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
5
  import { UEClient } from './ue-client.js';
6
+ import { compactBlueprint } from './compactor.js';
6
7
  const client = new UEClient();
7
8
  const server = new McpServer({
8
9
  name: 'blueprint-extractor',
9
- version: '1.2.0',
10
+ version: '1.4.0',
10
11
  });
11
12
  // Shared scope enum with detailed descriptions
12
13
  const scopeEnum = z.enum([
@@ -40,6 +41,9 @@ server.resource('extraction-scopes', 'blueprint://scopes', {
40
41
  '',
41
42
  'Start with the narrowest scope that answers your question.',
42
43
  'Full scope on complex Blueprints can exceed 200KB and will be truncated.',
44
+ '',
45
+ 'Note: Scopes only apply to Blueprint extraction (extract_blueprint and extract_cascade).',
46
+ 'DataAsset, DataTable, and StateTree always extract fully — no scope parameter is needed.',
43
47
  ].join('\n'),
44
48
  }],
45
49
  }));
@@ -59,11 +63,15 @@ USAGE GUIDELINES:
59
63
  * FullWithBytecode — + raw bytecode hex dump (largest, rarely needed)
60
64
  - Only escalate to Full when you need to understand graph logic (node connections, pin values, execution flow).
61
65
  - Full scope on complex Blueprints can exceed 200KB and will be truncated. If truncated, use a narrower scope or inspect specific functions via the graph names from FunctionsShallow.
66
+ - Use FunctionsShallow scope first to get graph names, then request specific graphs with graph_filter to reduce output size.
67
+ - Use compact=true to reduce JSON size by ~50-70% for LLM consumption.
62
68
 
63
69
  RETURNS: JSON object with the extracted Blueprint data at the requested scope level.`,
64
70
  inputSchema: {
65
71
  asset_path: z.string().describe('UE content path to the Blueprint asset. Must start with /Game/ (e.g. /Game/Blueprints/BP_Character). Use search_assets to find paths.'),
66
72
  scope: scopeEnum.default('Variables').describe('Extraction depth. Start with ClassLevel or Variables — only use Full when you need graph/node details.'),
73
+ graph_filter: z.array(z.string()).optional().describe('Filter to specific graphs by name. Use FunctionsShallow scope first to discover graph names, then pass the names you want here. Empty/omitted = extract all graphs. Example: ["EventGraph", "CalculateDamage"]'),
74
+ compact: z.boolean().default(false).describe('When true, strips low-value fields and minifies JSON to reduce size by ~50-70%. Removes: pinId, posX/posY, graphGuid, autogeneratedDefaultValue, nodeComment (when empty), empty connections, empty default_value, empty sub_category. Replaces full exec pin type objects with the string "exec".'),
67
75
  },
68
76
  annotations: {
69
77
  title: 'Extract Blueprint',
@@ -72,14 +80,21 @@ RETURNS: JSON object with the extracted Blueprint data at the requested scope le
72
80
  idempotentHint: true,
73
81
  openWorldHint: false,
74
82
  },
75
- }, async ({ asset_path, scope }) => {
83
+ }, async ({ asset_path, scope, graph_filter, compact }) => {
76
84
  try {
77
- const result = await client.callSubsystem('ExtractBlueprint', { AssetPath: asset_path, Scope: scope });
78
- const parsed = JSON.parse(result);
85
+ const result = await client.callSubsystem('ExtractBlueprint', {
86
+ AssetPath: asset_path,
87
+ Scope: scope,
88
+ GraphFilter: graph_filter ? graph_filter.join(',') : '',
89
+ });
90
+ let parsed = JSON.parse(result);
79
91
  if (parsed.error) {
80
92
  return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
81
93
  }
82
- const text = JSON.stringify(parsed, null, 2);
94
+ if (compact) {
95
+ parsed = compactBlueprint(parsed);
96
+ }
97
+ const text = compact ? JSON.stringify(parsed) : JSON.stringify(parsed, null, 2);
83
98
  if (text.length > 200_000) {
84
99
  return { content: [{ type: 'text', text: `Warning: Response is ${(text.length / 1024).toFixed(0)}KB — consider using a narrower scope (ClassLevel, Variables, or FunctionsShallow).\n\n${text.substring(0, 200_000)}...\n[TRUNCATED]` }] };
85
100
  }
@@ -123,10 +138,82 @@ RETURNS: JSON object with schema, state hierarchy, tasks, conditions, transition
123
138
  return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
124
139
  }
125
140
  });
126
- // Tool 3: extract_cascade
141
+ // Tool 3: extract_dataasset
142
+ server.registerTool('extract_dataasset', {
143
+ title: 'Extract DataAsset',
144
+ description: `Extract a UE5 DataAsset to structured JSON. Serializes all user-defined UPROPERTY fields using UE reflection.
145
+
146
+ USAGE GUIDELINES:
147
+ - Use search_assets first with class_filter "DataAsset" or a specific DataAsset subclass name to find the asset path.
148
+ - Returns all user-defined properties with their types and current values.
149
+ - Works with any UDataAsset or UPrimaryDataAsset subclass.
150
+
151
+ RETURNS: JSON object with the DataAsset's class info and all property values.`,
152
+ inputSchema: {
153
+ asset_path: z.string().describe('UE content path to the DataAsset (e.g. /Game/Data/DA_ItemDatabase). Use search_assets to find paths.'),
154
+ },
155
+ annotations: {
156
+ title: 'Extract DataAsset',
157
+ readOnlyHint: true,
158
+ destructiveHint: false,
159
+ idempotentHint: true,
160
+ openWorldHint: false,
161
+ },
162
+ }, async ({ asset_path }) => {
163
+ try {
164
+ const result = await client.callSubsystem('ExtractDataAsset', { AssetPath: asset_path });
165
+ const parsed = JSON.parse(result);
166
+ if (parsed.error) {
167
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
168
+ }
169
+ return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
170
+ }
171
+ catch (e) {
172
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
173
+ }
174
+ });
175
+ // Tool 4: extract_datatable
176
+ server.registerTool('extract_datatable', {
177
+ title: 'Extract DataTable',
178
+ description: `Extract a UE5 DataTable asset to structured JSON. Includes the row struct schema and all row data.
179
+
180
+ USAGE GUIDELINES:
181
+ - Use search_assets first with class_filter "DataTable" to find the asset path.
182
+ - Returns the row struct type, property schema, row count, and all row data with names and values.
183
+ - Useful for understanding game data tables (items, abilities, stats, etc.).
184
+
185
+ RETURNS: JSON object with row struct info, schema, and all rows.`,
186
+ inputSchema: {
187
+ asset_path: z.string().describe('UE content path to the DataTable (e.g. /Game/Data/DT_WeaponStats). Use search_assets to find paths.'),
188
+ },
189
+ annotations: {
190
+ title: 'Extract DataTable',
191
+ readOnlyHint: true,
192
+ destructiveHint: false,
193
+ idempotentHint: true,
194
+ openWorldHint: false,
195
+ },
196
+ }, async ({ asset_path }) => {
197
+ try {
198
+ const result = await client.callSubsystem('ExtractDataTable', { AssetPath: asset_path });
199
+ const parsed = JSON.parse(result);
200
+ if (parsed.error) {
201
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
202
+ }
203
+ const text = JSON.stringify(parsed, null, 2);
204
+ if (text.length > 200_000) {
205
+ return { content: [{ type: 'text', text: `Warning: Response is ${(text.length / 1024).toFixed(0)}KB — large DataTable.\n\n${text.substring(0, 200_000)}...\n[TRUNCATED]` }] };
206
+ }
207
+ return { content: [{ type: 'text', text }] };
208
+ }
209
+ catch (e) {
210
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
211
+ }
212
+ });
213
+ // Tool 5: extract_cascade
127
214
  server.registerTool('extract_cascade', {
128
215
  title: 'Extract Cascade',
129
- description: `Extract multiple Blueprint/StateTree assets with automatic reference following. Follows parent classes, interfaces, component classes, and other Blueprint references up to max_depth levels deep.
216
+ description: `Extract multiple assets (Blueprint, AnimBlueprint, StateTree, DataAsset, DataTable) with automatic reference following for Blueprints and StateTrees. Follows parent classes, interfaces, component classes, and other Blueprint references up to max_depth levels deep.
130
217
 
131
218
  USAGE GUIDELINES:
132
219
  - Use when you need to understand an asset AND its dependencies (parent Blueprints, referenced Blueprints, etc.).
@@ -139,6 +226,8 @@ RETURNS: Summary with extracted_count and output_directory path. Read the output
139
226
  asset_paths: z.array(z.string()).describe('Array of UE content paths to extract (e.g. ["/Game/Blueprints/BP_Character", "/Game/Blueprints/BP_Weapon"])'),
140
227
  scope: scopeEnum.default('Full').describe('Extraction depth applied to all assets. Full is the default since cascade is typically used for deep analysis.'),
141
228
  max_depth: z.number().int().min(0).max(10).default(3).describe('How many levels deep to follow references (0 = only the listed assets, 3 = default)'),
229
+ graph_filter: z.array(z.string()).optional().describe('Filter to specific graphs by name. Use FunctionsShallow scope first to discover graph names, then pass the names you want here. Empty/omitted = extract all graphs. Example: ["EventGraph", "CalculateDamage"]'),
230
+ compact: z.boolean().default(false).describe('When true, strips low-value fields and minifies JSON to reduce size by ~50-70%. Removes: pinId, posX/posY, graphGuid, autogeneratedDefaultValue, nodeComment (when empty), empty connections, empty default_value, empty sub_category. Replaces full exec pin type objects with the string "exec".'),
142
231
  },
143
232
  annotations: {
144
233
  title: 'Extract Cascade',
@@ -147,12 +236,13 @@ RETURNS: Summary with extracted_count and output_directory path. Read the output
147
236
  idempotentHint: true,
148
237
  openWorldHint: false,
149
238
  },
150
- }, async ({ asset_paths, scope, max_depth }) => {
239
+ }, async ({ asset_paths, scope, max_depth, graph_filter, compact }) => {
151
240
  try {
152
241
  const result = await client.callSubsystem('ExtractCascade', {
153
242
  AssetPathsJson: JSON.stringify(asset_paths),
154
243
  Scope: scope,
155
244
  MaxDepth: max_depth,
245
+ GraphFilter: graph_filter ? graph_filter.join(',') : '',
156
246
  });
157
247
  const parsed = JSON.parse(result);
158
248
  if (parsed.error) {
@@ -164,20 +254,20 @@ RETURNS: Summary with extracted_count and output_directory path. Read the output
164
254
  return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
165
255
  }
166
256
  });
167
- // Tool 4: search_assets
257
+ // Tool 6: search_assets
168
258
  server.registerTool('search_assets', {
169
259
  title: 'Search Assets',
170
- description: `Search for UE5 assets by name. This is a lightweight lookup — use it FIRST to find correct asset paths before calling extract_blueprint or extract_statetree.
260
+ description: `Search for UE5 assets by name. This is a lightweight lookup — use it FIRST to find correct asset paths before calling extract_blueprint, extract_statetree, extract_dataasset, or extract_datatable.
171
261
 
172
262
  USAGE GUIDELINES:
173
- - Always call this before extract_blueprint/extract_statetree if you don't already have the exact asset path.
263
+ - Always call this before any extract_* tool if you don't already have the exact asset path.
174
264
  - Searches asset names (not full paths) — partial matches work (e.g. "Character" finds "BP_Character").
175
- - Filter by class to narrow results: "Blueprint" (default), "StateTree", "WidgetBlueprint", "DataAsset", or empty string for all.
265
+ - Filter by class to narrow results: "Blueprint" (default), "AnimBlueprint", "StateTree", "DataTable", "WidgetBlueprint", "DataAsset", or empty string for all.
176
266
 
177
267
  RETURNS: JSON array of objects with path, name, and class for each matching asset.`,
178
268
  inputSchema: {
179
269
  query: z.string().describe('Search term to match against asset names. Partial matches work (e.g. "Player" finds "BP_PlayerCharacter").'),
180
- class_filter: z.string().default('Blueprint').describe('Filter by asset class. Common values: "Blueprint", "WidgetBlueprint", "StateTree", "DataAsset", or "" for all asset types.'),
270
+ class_filter: z.string().default('Blueprint').describe('Filter by asset class. Common values: "Blueprint", "AnimBlueprint", "WidgetBlueprint", "StateTree", "DataTable", "DataAsset", or "" for all asset types.'),
181
271
  },
182
272
  annotations: {
183
273
  title: 'Search Assets',
@@ -199,7 +289,7 @@ RETURNS: JSON array of objects with path, name, and class for each matching asse
199
289
  return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
200
290
  }
201
291
  });
202
- // Tool 5: list_assets
292
+ // Tool 7: list_assets
203
293
  server.registerTool('list_assets', {
204
294
  title: 'List Assets',
205
295
  description: `List UE5 assets under a package path. Use this to browse directory contents when you don't know asset names. If you know (part of) the asset name, prefer search_assets instead — it's faster and doesn't require knowing the directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blueprint-extractor-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for UE5 BlueprintExtractor plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",