blueprint-extractor-mcp 1.0.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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { UEClient } from './ue-client.js';
6
+ const client = new UEClient();
7
+ const server = new McpServer({
8
+ name: 'blueprint-extractor',
9
+ version: '1.0.0',
10
+ });
11
+ // Tool 1: extract_blueprint
12
+ server.tool('extract_blueprint', 'Extract a UE5 Blueprint asset to JSON. Returns the full Blueprint structure including class info, variables, components, and graph data.', {
13
+ asset_path: z.string().describe('UE asset path, e.g. /Game/Blueprints/BP_Character'),
14
+ scope: z.enum(['ClassLevel', 'Variables', 'Components', 'FunctionsShallow', 'Full', 'FullWithBytecode']).default('Full').describe('Extraction depth'),
15
+ }, async ({ asset_path, scope }) => {
16
+ try {
17
+ const result = await client.callSubsystem('ExtractBlueprint', { AssetPath: asset_path, Scope: scope });
18
+ const parsed = JSON.parse(result);
19
+ if (parsed.error) {
20
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
21
+ }
22
+ const text = JSON.stringify(parsed, null, 2);
23
+ if (text.length > 200_000) {
24
+ return { content: [{ type: 'text', text: `Warning: Response is ${(text.length / 1024).toFixed(0)}KB. Consider using a narrower scope.\n\n${text.substring(0, 200_000)}...\n[TRUNCATED]` }] };
25
+ }
26
+ return { content: [{ type: 'text', text }] };
27
+ }
28
+ catch (e) {
29
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
30
+ }
31
+ });
32
+ // Tool 2: extract_statetree
33
+ server.tool('extract_statetree', 'Extract a UE5 StateTree asset to JSON. Returns the full state hierarchy, tasks, conditions, and transitions.', {
34
+ asset_path: z.string().describe('UE asset path to a StateTree, e.g. /Game/AI/ST_BotBehavior'),
35
+ }, async ({ asset_path }) => {
36
+ try {
37
+ const result = await client.callSubsystem('ExtractStateTree', { AssetPath: asset_path });
38
+ const parsed = JSON.parse(result);
39
+ if (parsed.error) {
40
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
41
+ }
42
+ return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
43
+ }
44
+ catch (e) {
45
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
46
+ }
47
+ });
48
+ // Tool 3: extract_cascade
49
+ server.tool('extract_cascade', 'Extract multiple Blueprint/StateTree assets with cascade reference following. Writes results to files on disk.', {
50
+ asset_paths: z.array(z.string()).describe('Array of UE asset paths to extract'),
51
+ scope: z.enum(['ClassLevel', 'Variables', 'Components', 'FunctionsShallow', 'Full', 'FullWithBytecode']).default('Full').describe('Extraction depth'),
52
+ max_depth: z.number().int().min(0).max(10).default(3).describe('How many levels deep to follow references'),
53
+ }, async ({ asset_paths, scope, max_depth }) => {
54
+ try {
55
+ const result = await client.callSubsystem('ExtractCascade', {
56
+ AssetPathsJson: JSON.stringify(asset_paths),
57
+ Scope: scope,
58
+ MaxDepth: max_depth,
59
+ });
60
+ const parsed = JSON.parse(result);
61
+ if (parsed.error) {
62
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
63
+ }
64
+ return { content: [{ type: 'text', text: `Extracted ${parsed.extracted_count} assets to ${parsed.output_directory}` }] };
65
+ }
66
+ catch (e) {
67
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
68
+ }
69
+ });
70
+ // Tool 4: search_assets
71
+ server.tool('search_assets', 'Search for UE5 assets by name. Returns matching asset paths, names, and classes.', {
72
+ query: z.string().describe('Search query to match against asset names'),
73
+ class_filter: z.string().default('Blueprint').describe('Filter by asset class (e.g. Blueprint, StateTree, or empty for all)'),
74
+ }, async ({ query, class_filter }) => {
75
+ try {
76
+ const result = await client.callSubsystem('SearchAssets', { Query: query, ClassFilter: class_filter });
77
+ const parsed = JSON.parse(result);
78
+ if (parsed.error) {
79
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
80
+ }
81
+ return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
82
+ }
83
+ catch (e) {
84
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
85
+ }
86
+ });
87
+ // Tool 5: list_assets
88
+ server.tool('list_assets', 'List UE5 assets in a directory. Returns asset paths, names, and classes.', {
89
+ package_path: z.string().describe('UE package path, e.g. /Game/Blueprints'),
90
+ recursive: z.boolean().default(true).describe('Search subdirectories'),
91
+ class_filter: z.string().default('').describe('Filter by asset class (empty for all)'),
92
+ }, async ({ package_path, recursive, class_filter }) => {
93
+ try {
94
+ const result = await client.callSubsystem('ListAssets', {
95
+ PackagePath: package_path,
96
+ bRecursive: recursive,
97
+ ClassFilter: class_filter,
98
+ });
99
+ const parsed = JSON.parse(result);
100
+ if (parsed.error) {
101
+ return { content: [{ type: 'text', text: `Error: ${parsed.error}` }], isError: true };
102
+ }
103
+ return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
104
+ }
105
+ catch (e) {
106
+ return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
107
+ }
108
+ });
109
+ // Start server
110
+ async function main() {
111
+ const transport = new StdioServerTransport();
112
+ await server.connect(transport);
113
+ }
114
+ main().catch((e) => {
115
+ console.error('Fatal:', e);
116
+ process.exit(1);
117
+ });
@@ -0,0 +1,15 @@
1
+ export interface RemoteCallRequest {
2
+ objectPath: string;
3
+ functionName: string;
4
+ parameters: Record<string, unknown>;
5
+ generateTransaction: boolean;
6
+ }
7
+ export interface RemoteCallResponse {
8
+ ReturnValue?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface AssetInfo {
12
+ path: string;
13
+ name: string;
14
+ class: string;
15
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare class UEClient {
2
+ private host;
3
+ private port;
4
+ private subsystemPath;
5
+ constructor();
6
+ private get baseUrl();
7
+ checkConnection(): Promise<boolean>;
8
+ discoverSubsystem(): Promise<string>;
9
+ private rawCall;
10
+ callSubsystem(method: string, params: Record<string, unknown>): Promise<string>;
11
+ }
@@ -0,0 +1,98 @@
1
+ const DEFAULT_HOST = '127.0.0.1';
2
+ const DEFAULT_PORT = 30010;
3
+ const TIMEOUT_MS = 60_000;
4
+ export class UEClient {
5
+ host;
6
+ port;
7
+ subsystemPath = null;
8
+ constructor() {
9
+ this.host = process.env.UE_REMOTE_CONTROL_HOST ?? DEFAULT_HOST;
10
+ this.port = parseInt(process.env.UE_REMOTE_CONTROL_PORT ?? String(DEFAULT_PORT), 10);
11
+ }
12
+ get baseUrl() {
13
+ return `http://${this.host}:${this.port}`;
14
+ }
15
+ async checkConnection() {
16
+ // GET /remote/info — if it responds, UE is running with Remote Control
17
+ try {
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), 5000);
20
+ const res = await fetch(`${this.baseUrl}/remote/info`, { signal: controller.signal });
21
+ clearTimeout(timeout);
22
+ return res.ok;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ async discoverSubsystem() {
29
+ if (this.subsystemPath)
30
+ return this.subsystemPath;
31
+ // Use /remote/object/call to find the subsystem via GEditor
32
+ // EditorSubsystems are singletons accessible via GEditor->GetEditorSubsystem<T>()
33
+ // The object path for editor subsystems follows the pattern: /Engine/Transient.UnrealEditorSubsystem_0
34
+ // We try to call a method directly — if it works, the path is valid
35
+ // For UEditorSubsystem, the default object path is:
36
+ // /Engine/Transient.BlueprintExtractorSubsystem
37
+ // But the exact path depends on the engine. Let's try the standard pattern.
38
+ const candidatePaths = [
39
+ '/Script/BlueprintExtractor.Default__BlueprintExtractorSubsystem',
40
+ '/Engine/Transient.BlueprintExtractorSubsystem',
41
+ '/Engine/Transient.BlueprintExtractorSubsystem_0',
42
+ ];
43
+ for (const path of candidatePaths) {
44
+ try {
45
+ // Try calling ListAssets as a health check
46
+ const res = await this.rawCall(path, 'ListAssets', { PackagePath: '/Game', bRecursive: false, ClassFilter: '' });
47
+ if (res !== null) {
48
+ this.subsystemPath = path;
49
+ return path;
50
+ }
51
+ }
52
+ catch {
53
+ // Try next candidate
54
+ }
55
+ }
56
+ throw new Error('BlueprintExtractor subsystem not found. Ensure the plugin is loaded in the editor.');
57
+ }
58
+ async rawCall(objectPath, functionName, parameters) {
59
+ const body = {
60
+ objectPath,
61
+ functionName,
62
+ parameters,
63
+ generateTransaction: false,
64
+ };
65
+ const controller = new AbortController();
66
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
67
+ try {
68
+ const res = await fetch(`${this.baseUrl}/remote/object/call`, {
69
+ method: 'PUT',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify(body),
72
+ signal: controller.signal,
73
+ });
74
+ clearTimeout(timeout);
75
+ if (!res.ok)
76
+ return null;
77
+ return await res.json();
78
+ }
79
+ catch {
80
+ clearTimeout(timeout);
81
+ return null;
82
+ }
83
+ }
84
+ async callSubsystem(method, params) {
85
+ const connected = await this.checkConnection();
86
+ if (!connected) {
87
+ throw new Error(`UE Editor not running or Remote Control not available on ${this.host}:${this.port}`);
88
+ }
89
+ const objectPath = await this.discoverSubsystem();
90
+ const res = await this.rawCall(objectPath, method, params);
91
+ if (res === null) {
92
+ throw new Error(`Failed to call ${method} on BlueprintExtractorSubsystem`);
93
+ }
94
+ // The subsystem methods return FString, which comes back as ReturnValue
95
+ const returnValue = res.ReturnValue ?? JSON.stringify(res);
96
+ return typeof returnValue === 'string' ? returnValue : JSON.stringify(returnValue);
97
+ }
98
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "blueprint-extractor-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for UE5 BlueprintExtractor plugin",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "blueprint-extractor-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.12.1",
20
+ "zod": "^3.24.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.8.0",
24
+ "@types/node": "^22.0.0"
25
+ }
26
+ }