ultimate-unreal-engine-mcp 0.1.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.
- package/README.md +729 -0
- package/dist/build/error-parser.js +51 -0
- package/dist/build/fix-suggester.js +84 -0
- package/dist/build/ubt-runner.js +146 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +8 -0
- package/dist/docs/data/ue57-api.js +228 -0
- package/dist/docs/doc-index.js +110 -0
- package/dist/docs/types.js +4 -0
- package/dist/generators/class-generator.js +363 -0
- package/dist/generators/file-modifier.js +276 -0
- package/dist/generators/uht-validator.js +177 -0
- package/dist/index.js +89 -0
- package/dist/parsers/cpp-class-index.js +230 -0
- package/dist/parsers/cpp-parser.js +369 -0
- package/dist/parsers/ini-parser.js +216 -0
- package/dist/parsers/uproject-parser.js +130 -0
- package/dist/plugin-bridge/client.js +217 -0
- package/dist/plugin-bridge/protocol.js +6 -0
- package/dist/plugin-bridge/retry.js +23 -0
- package/dist/setup.js +209 -0
- package/dist/tools/ai-systems/index.js +247 -0
- package/dist/tools/ai-systems/types.js +4 -0
- package/dist/tools/animation/index.js +241 -0
- package/dist/tools/animation/types.js +4 -0
- package/dist/tools/audio/index.js +204 -0
- package/dist/tools/audio/types.js +4 -0
- package/dist/tools/blueprint/index.js +495 -0
- package/dist/tools/blueprint/types.js +4 -0
- package/dist/tools/build/index.js +163 -0
- package/dist/tools/chaos/index.js +230 -0
- package/dist/tools/chaos/types.js +4 -0
- package/dist/tools/collision-physics/index.js +211 -0
- package/dist/tools/config/index.js +288 -0
- package/dist/tools/cpp/index.js +305 -0
- package/dist/tools/docs/index.js +251 -0
- package/dist/tools/editor/index.js +242 -0
- package/dist/tools/gas/index.js +222 -0
- package/dist/tools/gas/types.js +5 -0
- package/dist/tools/import-export/index.js +218 -0
- package/dist/tools/input/index.js +146 -0
- package/dist/tools/known-issues/index.js +88 -0
- package/dist/tools/known-issues/middleware.js +55 -0
- package/dist/tools/known-issues/store.js +125 -0
- package/dist/tools/livelink/index.js +203 -0
- package/dist/tools/livelink/types.js +4 -0
- package/dist/tools/material/index.js +190 -0
- package/dist/tools/motion-design/index.js +251 -0
- package/dist/tools/motion-design/types.js +6 -0
- package/dist/tools/movie-render/index.js +220 -0
- package/dist/tools/networking/index.js +149 -0
- package/dist/tools/pcg/index.js +164 -0
- package/dist/tools/selection/index.js +180 -0
- package/dist/tools/sequencer/index.js +218 -0
- package/dist/tools/validation/index.js +183 -0
- package/dist/tools/validation/types.js +4 -0
- package/dist/tools/viewport/index.js +310 -0
- package/dist/tools/worldpartition/index.js +226 -0
- package/dist/tools/worldpartition/types.js +4 -0
- package/dist/utils/execFileNoThrow.js +40 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/path-guard.js +26 -0
- package/package.json +40 -0
- package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/tools/known-issues/middleware.ts
|
|
2
|
+
// Wraps tool handlers with a pre-execution KNOWN_ISSUES check.
|
|
3
|
+
// Matching issues are prepended as warnings to the tool response content array.
|
|
4
|
+
// Handler exceptions are caught and returned as isError responses — never propagated.
|
|
5
|
+
import { readKnownIssues } from './store.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// withKnownIssues
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a tool handler with a KNOWN_ISSUES pre-execution check.
|
|
11
|
+
*
|
|
12
|
+
* Before invoking the real handler:
|
|
13
|
+
* 1. Reads KNOWN_ISSUES.md (cached by mtime — cheap on cache hit).
|
|
14
|
+
* 2. Filters issues where affectedTools includes toolName or '*' (wildcard).
|
|
15
|
+
* 3. If matching issues exist, prepends a single warning content item
|
|
16
|
+
* (all warnings joined by '\n') before the handler's content items.
|
|
17
|
+
*
|
|
18
|
+
* Exceptions thrown by the handler are caught and returned as:
|
|
19
|
+
* { isError: true, content: [{ type: 'text', text: 'Error: <message>' }] }
|
|
20
|
+
*
|
|
21
|
+
* @param toolName The MCP tool name (e.g., 'ue_read_cpp_class').
|
|
22
|
+
* @param handler The real tool handler to wrap.
|
|
23
|
+
* @returns A new handler with the same signature.
|
|
24
|
+
*/
|
|
25
|
+
export function withKnownIssues(toolName, handler) {
|
|
26
|
+
return async (args) => {
|
|
27
|
+
const issues = await readKnownIssues();
|
|
28
|
+
const relevant = issues.filter((i) => i.affectedTools.includes(toolName) || i.affectedTools.includes('*'));
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await handler(args);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
isError: true,
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (relevant.length === 0) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
const warnings = relevant
|
|
48
|
+
.map((i) => `[KNOWN ISSUE ${i.id}] ${i.description} — Resolution: ${i.resolution}`)
|
|
49
|
+
.join('\n');
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: 'text', text: warnings }, ...result.content],
|
|
52
|
+
isError: result.isError,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/tools/known-issues/store.ts
|
|
2
|
+
// Reads, caches, and writes KNOWN_ISSUES.md at the project root.
|
|
3
|
+
// Cache is keyed by file mtime — re-reads only when the file changes.
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { PROJECT_ROOT } from '../../config.js';
|
|
7
|
+
const KNOWN_ISSUES_PATH = path.join(PROJECT_ROOT, 'KNOWN_ISSUES.md');
|
|
8
|
+
const cache = { issues: [], mtime: 0 };
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Extract the text after a bold field label on a line.
|
|
14
|
+
* e.g., "**Description:** foo" → "foo"
|
|
15
|
+
*/
|
|
16
|
+
function extractField(lines, label) {
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (trimmed.startsWith(label)) {
|
|
20
|
+
return trimmed.slice(label.length).trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a single issue block (text after "## ISSUE-" prefix).
|
|
27
|
+
* The first line of the block is the issue number suffix (e.g., "001\n...").
|
|
28
|
+
*/
|
|
29
|
+
function parseIssueBlock(block) {
|
|
30
|
+
const newlineIdx = block.indexOf('\n');
|
|
31
|
+
const idSuffix = newlineIdx === -1 ? block.trim() : block.slice(0, newlineIdx).trim();
|
|
32
|
+
if (!idSuffix)
|
|
33
|
+
return null;
|
|
34
|
+
const id = 'ISSUE-' + idSuffix;
|
|
35
|
+
const body = newlineIdx === -1 ? '' : block.slice(newlineIdx + 1);
|
|
36
|
+
const lines = body.split('\n');
|
|
37
|
+
const description = extractField(lines, '**Description:**');
|
|
38
|
+
const affectedToolsRaw = extractField(lines, '**Affected tools:**');
|
|
39
|
+
const rootCause = extractField(lines, '**Root cause:**');
|
|
40
|
+
const resolution = extractField(lines, '**Resolution:**');
|
|
41
|
+
const dateAdded = extractField(lines, '**Date added:**');
|
|
42
|
+
const statusRaw = extractField(lines, '**Status:**').toLowerCase();
|
|
43
|
+
const status = statusRaw === 'resolved' ? 'resolved' : 'active';
|
|
44
|
+
const affectedTools = affectedToolsRaw.trim() === ''
|
|
45
|
+
? []
|
|
46
|
+
: affectedToolsRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
47
|
+
return { id, description, affectedTools, rootCause, resolution, dateAdded, status };
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Public API
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Read and parse all ## ISSUE-NNN sections from KNOWN_ISSUES.md.
|
|
54
|
+
* Results are cached by mtime; a second call without file changes returns the
|
|
55
|
+
* same array reference without performing a second fs.readFile.
|
|
56
|
+
*
|
|
57
|
+
* Returns [] if the file does not exist or contains no ## ISSUE- sections.
|
|
58
|
+
*/
|
|
59
|
+
export async function readKnownIssues() {
|
|
60
|
+
let stat;
|
|
61
|
+
try {
|
|
62
|
+
stat = await fs.stat(KNOWN_ISSUES_PATH);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err !== null &&
|
|
66
|
+
typeof err === 'object' &&
|
|
67
|
+
'code' in err &&
|
|
68
|
+
err.code === 'ENOENT') {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
const currentMtime = stat.mtimeMs;
|
|
74
|
+
if (currentMtime === cache.mtime) {
|
|
75
|
+
return cache.issues;
|
|
76
|
+
}
|
|
77
|
+
const content = await fs.readFile(KNOWN_ISSUES_PATH, 'utf8');
|
|
78
|
+
// Split on '\n## ISSUE-' — the first segment is the header (discarded).
|
|
79
|
+
const segments = content.split('\n## ISSUE-');
|
|
80
|
+
const issueBlocks = segments.slice(1); // discard header segment
|
|
81
|
+
const issues = [];
|
|
82
|
+
for (const block of issueBlocks) {
|
|
83
|
+
const issue = parseIssueBlock(block);
|
|
84
|
+
if (issue !== null) {
|
|
85
|
+
issues.push(issue);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cache.issues = issues;
|
|
89
|
+
cache.mtime = currentMtime;
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Append a new ## ISSUE-NNN section to KNOWN_ISSUES.md.
|
|
94
|
+
* The issue number is auto-incremented from the highest existing issue number.
|
|
95
|
+
* Invalidates the mtime cache so the next readKnownIssues() call re-reads the file.
|
|
96
|
+
*
|
|
97
|
+
* Returns the full KnownIssue with the computed id.
|
|
98
|
+
*/
|
|
99
|
+
export async function appendKnownIssue(issue) {
|
|
100
|
+
const existing = await readKnownIssues();
|
|
101
|
+
// Compute next ID
|
|
102
|
+
let maxN = 0;
|
|
103
|
+
for (const existing_issue of existing) {
|
|
104
|
+
const n = parseInt(existing_issue.id.replace('ISSUE-', ''), 10);
|
|
105
|
+
if (!isNaN(n) && n > maxN) {
|
|
106
|
+
maxN = n;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const nextN = maxN + 1;
|
|
110
|
+
const id = 'ISSUE-' + String(nextN).padStart(3, '0');
|
|
111
|
+
// Build the markdown block matching KNOWN_ISSUES.md format
|
|
112
|
+
const affectedToolsStr = issue.affectedTools.join(', ');
|
|
113
|
+
const block = `\n## ${id}\n` +
|
|
114
|
+
`**Description:** ${issue.description}\n` +
|
|
115
|
+
`**Affected tools:** ${affectedToolsStr}\n` +
|
|
116
|
+
`**Root cause:** ${issue.rootCause}\n` +
|
|
117
|
+
`**Resolution:** ${issue.resolution}\n` +
|
|
118
|
+
`**Date added:** ${issue.dateAdded}\n` +
|
|
119
|
+
`**Status:** ${issue.status}\n` +
|
|
120
|
+
`\n---`;
|
|
121
|
+
await fs.appendFile(KNOWN_ISSUES_PATH, block, 'utf8');
|
|
122
|
+
// Invalidate cache so next read picks up the newly appended content
|
|
123
|
+
cache.mtime = 0;
|
|
124
|
+
return { ...issue, id };
|
|
125
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/tools/livelink/index.ts
|
|
2
|
+
// MCP tool implementations for Live Link source/subject management (Phase 27).
|
|
3
|
+
// All tools route commands through PluginBridgeClient to the C++ MCPBridge handlers.
|
|
4
|
+
// Returns structured plugin_not_connected errors when the plugin is absent.
|
|
5
|
+
//
|
|
6
|
+
// Tools registered (Phase 27 — LL-01 through LL-04):
|
|
7
|
+
// ue_list_livelink_sources — List all active Live Link sources with connection status
|
|
8
|
+
// ue_list_livelink_subjects — List all Live Link subjects with roles and enabled state
|
|
9
|
+
// ue_control_livelink_subject — Pause or resume an individual Live Link subject
|
|
10
|
+
// ue_preview_livelink_data — Inspect current frame data for a Live Link subject
|
|
11
|
+
//
|
|
12
|
+
// All tools require MCPBridge plugin (Phase 27 — LL-01 through LL-04).
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { withKnownIssues } from '../known-issues/middleware.js';
|
|
15
|
+
import { PluginBridgeClient, PluginNotConnectedError } from '../../plugin-bridge/client.js';
|
|
16
|
+
// Module-level bridge instance — injected in tests via exported handler signatures.
|
|
17
|
+
const bridge = new PluginBridgeClient();
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// sendOrDisconnect helper
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Sends a command to the bridge and returns a ToolResult.
|
|
23
|
+
*
|
|
24
|
+
* - On success (response.success true): returns data as JSON text.
|
|
25
|
+
* - On command-level failure (response.success false): returns isError:true with error JSON.
|
|
26
|
+
* - On PluginNotConnectedError: returns isError:true with plugin_not_connected JSON.
|
|
27
|
+
* - On other errors: rethrows (withKnownIssues catches and formats).
|
|
28
|
+
*
|
|
29
|
+
* NOTE: sendCommand() overwrites correlationId with crypto.randomUUID() internally;
|
|
30
|
+
* passing an empty string is safe and correct (per STATE.md decision).
|
|
31
|
+
*/
|
|
32
|
+
async function sendOrDisconnect(b, cmd) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await b.sendCommand({ ...cmd, correlationId: '' });
|
|
35
|
+
if (!response.success) {
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify(response.data ?? {}) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof PluginNotConnectedError) {
|
|
47
|
+
return {
|
|
48
|
+
isError: true,
|
|
49
|
+
content: [{ type: 'text', text: JSON.stringify(err.bridgeError) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw err; // withKnownIssues catches unexpected errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Exported handler functions (for direct unit testing via bridge injection)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* ue_list_livelink_sources handler — satisfies LL-01.
|
|
60
|
+
* Sends livelink.sources to the plugin; returns all active Live Link sources
|
|
61
|
+
* with connection status, source type, and machine name.
|
|
62
|
+
*
|
|
63
|
+
* @param args Tool arguments (no parameters required).
|
|
64
|
+
* @param b PluginBridgeClient instance (defaults to module-level singleton).
|
|
65
|
+
*/
|
|
66
|
+
export async function handleListLiveLinkSources(args, b = bridge) {
|
|
67
|
+
// Response data shape: LiveLinkSourcesResult
|
|
68
|
+
void args; // no parameters
|
|
69
|
+
return sendOrDisconnect(b, {
|
|
70
|
+
type: 'livelink.sources',
|
|
71
|
+
payload: {},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* ue_list_livelink_subjects handler — satisfies LL-02.
|
|
76
|
+
* Sends livelink.subjects to the plugin; returns all Live Link subjects
|
|
77
|
+
* with their roles (Animation, Transform, Camera, Light) and enabled state.
|
|
78
|
+
*
|
|
79
|
+
* @param args Tool arguments (no parameters required).
|
|
80
|
+
* @param b PluginBridgeClient instance (defaults to module-level singleton).
|
|
81
|
+
*/
|
|
82
|
+
export async function handleListLiveLinkSubjects(args, b = bridge) {
|
|
83
|
+
// Response data shape: LiveLinkSubjectsResult
|
|
84
|
+
void args; // no parameters
|
|
85
|
+
return sendOrDisconnect(b, {
|
|
86
|
+
type: 'livelink.subjects',
|
|
87
|
+
payload: {},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* ue_control_livelink_subject handler — satisfies LL-03.
|
|
92
|
+
* Sends livelink.control to the plugin; pauses or resumes an individual
|
|
93
|
+
* Live Link subject by toggling its enabled state.
|
|
94
|
+
*
|
|
95
|
+
* @param args Tool arguments: subject_name and enabled flag.
|
|
96
|
+
* @param b PluginBridgeClient instance (defaults to module-level singleton).
|
|
97
|
+
*/
|
|
98
|
+
export async function handleControlLiveLinkSubject(args, b = bridge) {
|
|
99
|
+
// Response data shape: LiveLinkControlResult
|
|
100
|
+
return sendOrDisconnect(b, {
|
|
101
|
+
type: 'livelink.control',
|
|
102
|
+
payload: { subject_name: args.subject_name, enabled: args.enabled },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* ue_preview_livelink_data handler — satisfies LL-04.
|
|
107
|
+
* Sends livelink.preview to the plugin; returns the current frame data for
|
|
108
|
+
* a Live Link subject (transform, camera, or animation bone data).
|
|
109
|
+
*
|
|
110
|
+
* @param args Tool arguments: subject_name.
|
|
111
|
+
* @param b PluginBridgeClient instance (defaults to module-level singleton).
|
|
112
|
+
*/
|
|
113
|
+
export async function handlePreviewLiveLinkData(args, b = bridge) {
|
|
114
|
+
// Response data shape: LiveLinkPreviewResult
|
|
115
|
+
return sendOrDisconnect(b, {
|
|
116
|
+
type: 'livelink.preview',
|
|
117
|
+
payload: { subject_name: args.subject_name },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// registerLiveLinkTools
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
/**
|
|
124
|
+
* Register UE Live Link source/subject management tools on the MCP server.
|
|
125
|
+
*
|
|
126
|
+
* All tools in this domain require the MCPBridge editor plugin.
|
|
127
|
+
* When the plugin is not connected, each handler returns:
|
|
128
|
+
* { isError: true, content: [{ type: 'text', text: <plugin_not_connected JSON> }] }
|
|
129
|
+
*
|
|
130
|
+
* Tools registered (Phase 27):
|
|
131
|
+
* ue_list_livelink_sources — LL-01: List Live Link sources with status
|
|
132
|
+
* ue_list_livelink_subjects — LL-02: List subjects with roles and enabled state
|
|
133
|
+
* ue_control_livelink_subject — LL-03: Pause or resume a Live Link subject
|
|
134
|
+
* ue_preview_livelink_data — LL-04: Inspect current frame data for a subject
|
|
135
|
+
*
|
|
136
|
+
* @param server The McpServer instance to register tools on.
|
|
137
|
+
* @param _bridge Optional PluginBridgeClient for testing (not used directly — handlers
|
|
138
|
+
* accept bridge injection via their exported function signatures).
|
|
139
|
+
*/
|
|
140
|
+
export function registerLiveLinkTools(server, _bridge) {
|
|
141
|
+
const b = _bridge ?? new PluginBridgeClient();
|
|
142
|
+
// --------------------------------------------------------------------------
|
|
143
|
+
// ue_list_livelink_sources (LL-01)
|
|
144
|
+
// --------------------------------------------------------------------------
|
|
145
|
+
server.registerTool('ue_list_livelink_sources', {
|
|
146
|
+
title: 'List Live Link Sources',
|
|
147
|
+
description: '[requires_plugin] List all active Live Link sources with connection status, source type, and machine name. Returns informative message if Live Link plugin is not enabled.',
|
|
148
|
+
inputSchema: z.object({}),
|
|
149
|
+
annotations: {
|
|
150
|
+
readOnlyHint: true,
|
|
151
|
+
destructiveHint: false,
|
|
152
|
+
},
|
|
153
|
+
}, withKnownIssues('ue_list_livelink_sources', async (args) => handleListLiveLinkSources(args, b)));
|
|
154
|
+
// --------------------------------------------------------------------------
|
|
155
|
+
// ue_list_livelink_subjects (LL-02)
|
|
156
|
+
// --------------------------------------------------------------------------
|
|
157
|
+
server.registerTool('ue_list_livelink_subjects', {
|
|
158
|
+
title: 'List Live Link Subjects',
|
|
159
|
+
description: '[requires_plugin] List all Live Link subjects with their roles (Animation, Transform, Camera, Light) and enabled/disabled state.',
|
|
160
|
+
inputSchema: z.object({}),
|
|
161
|
+
annotations: {
|
|
162
|
+
readOnlyHint: true,
|
|
163
|
+
destructiveHint: false,
|
|
164
|
+
},
|
|
165
|
+
}, withKnownIssues('ue_list_livelink_subjects', async (args) => handleListLiveLinkSubjects(args, b)));
|
|
166
|
+
// --------------------------------------------------------------------------
|
|
167
|
+
// ue_control_livelink_subject (LL-03)
|
|
168
|
+
// --------------------------------------------------------------------------
|
|
169
|
+
server.registerTool('ue_control_livelink_subject', {
|
|
170
|
+
title: 'Control Live Link Subject',
|
|
171
|
+
description: '[requires_plugin] Pause or resume an individual Live Link subject by toggling its enabled state.',
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
subject_name: z
|
|
174
|
+
.string()
|
|
175
|
+
.min(1)
|
|
176
|
+
.describe("Name of the Live Link subject to control, e.g. 'MyMocapActor'"),
|
|
177
|
+
enabled: z
|
|
178
|
+
.boolean()
|
|
179
|
+
.describe('true to resume/enable the subject, false to pause/disable it'),
|
|
180
|
+
}),
|
|
181
|
+
annotations: {
|
|
182
|
+
readOnlyHint: false,
|
|
183
|
+
destructiveHint: false,
|
|
184
|
+
},
|
|
185
|
+
}, withKnownIssues('ue_control_livelink_subject', async (args) => handleControlLiveLinkSubject(args, b)));
|
|
186
|
+
// --------------------------------------------------------------------------
|
|
187
|
+
// ue_preview_livelink_data (LL-04)
|
|
188
|
+
// --------------------------------------------------------------------------
|
|
189
|
+
server.registerTool('ue_preview_livelink_data', {
|
|
190
|
+
title: 'Preview Live Link Data',
|
|
191
|
+
description: '[requires_plugin] Inspect the current frame data for a Live Link subject. Returns role-specific data: transform (location/rotation/scale), camera (FOV/aperture/focus), or animation (bone transforms, capped at 10 bones).',
|
|
192
|
+
inputSchema: z.object({
|
|
193
|
+
subject_name: z
|
|
194
|
+
.string()
|
|
195
|
+
.min(1)
|
|
196
|
+
.describe("Name of the Live Link subject to preview, e.g. 'MyCamera'"),
|
|
197
|
+
}),
|
|
198
|
+
annotations: {
|
|
199
|
+
readOnlyHint: true,
|
|
200
|
+
destructiveHint: false,
|
|
201
|
+
},
|
|
202
|
+
}, withKnownIssues('ue_preview_livelink_data', async (args) => handlePreviewLiveLinkData(args, b)));
|
|
203
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/tools/material/index.ts
|
|
2
|
+
// MCP tool implementations for material parameter inspection and material instance creation (Phase 15).
|
|
3
|
+
// All tools route commands through PluginBridgeClient to the C++ MCPBridge plugin.
|
|
4
|
+
// Returns structured plugin_not_connected errors when the plugin is absent.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { withKnownIssues } from '../known-issues/middleware.js';
|
|
7
|
+
import { PluginBridgeClient, PluginNotConnectedError } from '../../plugin-bridge/client.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// sendOrDisconnect helper
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Sends a command to the bridge and returns a ToolResult.
|
|
13
|
+
*
|
|
14
|
+
* - On success (response.success true): returns data as JSON text.
|
|
15
|
+
* - On command-level failure (response.success false): returns isError:true with error JSON.
|
|
16
|
+
* - On PluginNotConnectedError: returns isError:true with plugin_not_connected JSON.
|
|
17
|
+
* - On other errors: rethrows (withKnownIssues catches and formats).
|
|
18
|
+
*
|
|
19
|
+
* NOTE: sendCommand() overwrites correlationId with crypto.randomUUID() internally;
|
|
20
|
+
* passing an empty string is safe and correct (per STATE.md decision).
|
|
21
|
+
*/
|
|
22
|
+
async function sendOrDisconnect(bridge, cmd) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await bridge.sendCommand({ ...cmd, correlationId: '' });
|
|
25
|
+
if (!response.success) {
|
|
26
|
+
return {
|
|
27
|
+
isError: true,
|
|
28
|
+
content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify(response.data ?? {}) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof PluginNotConnectedError) {
|
|
37
|
+
return {
|
|
38
|
+
isError: true,
|
|
39
|
+
content: [{ type: 'text', text: JSON.stringify(err.bridgeError) }],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
throw err; // withKnownIssues catches unexpected errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// registerMaterialTools
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Register UE material parameter inspection and material instance tools on the MCP server.
|
|
50
|
+
*
|
|
51
|
+
* All tools in this domain require the MCPBridge editor plugin.
|
|
52
|
+
* When the plugin is not connected, each handler returns:
|
|
53
|
+
* { isError: true, content: [{ type: 'text', text: <plugin_not_connected JSON> }] }
|
|
54
|
+
*
|
|
55
|
+
* Tools registered (Phase 15):
|
|
56
|
+
* ue_material_params — Read all parameters from a material or material instance
|
|
57
|
+
* ue_create_material_instance — Create a new Material Instance Constant asset from a parent material
|
|
58
|
+
* ue_set_material_param — Set a scalar, vector, or texture parameter on a material instance
|
|
59
|
+
* ue_actor_materials — List all material asset paths used by an actor or static mesh
|
|
60
|
+
*
|
|
61
|
+
* @param server The McpServer instance to register tools on.
|
|
62
|
+
* @param bridge Optional PluginBridgeClient for testing (defaults to a new instance).
|
|
63
|
+
*/
|
|
64
|
+
export function registerMaterialTools(server, bridge) {
|
|
65
|
+
const _bridge = bridge ?? new PluginBridgeClient();
|
|
66
|
+
// --------------------------------------------------------------------------
|
|
67
|
+
// ue_material_params
|
|
68
|
+
// --------------------------------------------------------------------------
|
|
69
|
+
server.registerTool('ue_material_params', {
|
|
70
|
+
title: 'Read Material Parameters',
|
|
71
|
+
description: '[requires_plugin] Read all scalar, vector, and texture parameters from a material asset or material instance. Returns parameter name, type, current value, and default value.',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
asset_path: z
|
|
74
|
+
.string()
|
|
75
|
+
.describe('UE asset path to the material or material instance, e.g. "/Game/Materials/M_Rock"'),
|
|
76
|
+
}),
|
|
77
|
+
annotations: {
|
|
78
|
+
readOnlyHint: true,
|
|
79
|
+
destructiveHint: false,
|
|
80
|
+
},
|
|
81
|
+
}, withKnownIssues('ue_material_params', async (args) => {
|
|
82
|
+
return sendOrDisconnect(_bridge, {
|
|
83
|
+
type: 'material.params',
|
|
84
|
+
payload: { asset_path: args.asset_path },
|
|
85
|
+
});
|
|
86
|
+
}));
|
|
87
|
+
// --------------------------------------------------------------------------
|
|
88
|
+
// ue_create_material_instance
|
|
89
|
+
// --------------------------------------------------------------------------
|
|
90
|
+
server.registerTool('ue_create_material_instance', {
|
|
91
|
+
title: 'Create Material Instance',
|
|
92
|
+
description: '[requires_plugin] Create a new Material Instance Constant asset from a parent material. The instance is saved to disk as a persistent UE asset.',
|
|
93
|
+
inputSchema: z.object({
|
|
94
|
+
parent_path: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe('UE asset path of the parent material, e.g. "/Game/Materials/M_Rock"'),
|
|
97
|
+
instance_path: z
|
|
98
|
+
.string()
|
|
99
|
+
.describe('Package path (directory) for the new instance, e.g. "/Game/Materials". Must start with /Game/.'),
|
|
100
|
+
instance_name: z
|
|
101
|
+
.string()
|
|
102
|
+
.describe('Asset name for the new instance, e.g. "MI_Rock_Red"'),
|
|
103
|
+
}),
|
|
104
|
+
annotations: {
|
|
105
|
+
readOnlyHint: false,
|
|
106
|
+
destructiveHint: false,
|
|
107
|
+
},
|
|
108
|
+
}, withKnownIssues('ue_create_material_instance', async (args) => {
|
|
109
|
+
return sendOrDisconnect(_bridge, {
|
|
110
|
+
type: 'material.createInstance',
|
|
111
|
+
payload: {
|
|
112
|
+
parent_path: args.parent_path,
|
|
113
|
+
instance_path: args.instance_path,
|
|
114
|
+
instance_name: args.instance_name,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}));
|
|
118
|
+
// --------------------------------------------------------------------------
|
|
119
|
+
// ue_set_material_param
|
|
120
|
+
// --------------------------------------------------------------------------
|
|
121
|
+
server.registerTool('ue_set_material_param', {
|
|
122
|
+
title: 'Set Material Instance Parameter',
|
|
123
|
+
description: '[requires_plugin] Set a scalar, vector, or texture parameter override on a Material Instance Constant. The value type must match param_type.',
|
|
124
|
+
inputSchema: z.object({
|
|
125
|
+
asset_path: z
|
|
126
|
+
.string()
|
|
127
|
+
.describe('UE asset path to the Material Instance Constant'),
|
|
128
|
+
param_name: z
|
|
129
|
+
.string()
|
|
130
|
+
.describe('Parameter name as defined in the parent material'),
|
|
131
|
+
param_type: z
|
|
132
|
+
.enum(['scalar', 'vector', 'texture'])
|
|
133
|
+
.describe('Parameter type: scalar (float), vector (RGBA), or texture (asset path string)'),
|
|
134
|
+
value: z
|
|
135
|
+
.union([
|
|
136
|
+
z.number(),
|
|
137
|
+
z.object({
|
|
138
|
+
r: z.number(),
|
|
139
|
+
g: z.number(),
|
|
140
|
+
b: z.number(),
|
|
141
|
+
a: z.number().optional(),
|
|
142
|
+
}),
|
|
143
|
+
z.string(),
|
|
144
|
+
])
|
|
145
|
+
.describe('Parameter value: number for scalar, {r,g,b,a} object for vector, asset path string for texture'),
|
|
146
|
+
}),
|
|
147
|
+
annotations: {
|
|
148
|
+
readOnlyHint: false,
|
|
149
|
+
destructiveHint: false,
|
|
150
|
+
},
|
|
151
|
+
}, withKnownIssues('ue_set_material_param', async (args) => {
|
|
152
|
+
return sendOrDisconnect(_bridge, {
|
|
153
|
+
type: 'material.setParam',
|
|
154
|
+
payload: {
|
|
155
|
+
asset_path: args.asset_path,
|
|
156
|
+
param_name: args.param_name,
|
|
157
|
+
param_type: args.param_type,
|
|
158
|
+
value: args.value,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}));
|
|
162
|
+
// --------------------------------------------------------------------------
|
|
163
|
+
// ue_actor_materials
|
|
164
|
+
// --------------------------------------------------------------------------
|
|
165
|
+
server.registerTool('ue_actor_materials', {
|
|
166
|
+
title: 'List Actor or Mesh Materials',
|
|
167
|
+
description: '[requires_plugin] List all material asset paths used by an actor in the open level or by a Static Mesh asset. Provide either actor_label or asset_path.',
|
|
168
|
+
inputSchema: z.object({
|
|
169
|
+
actor_label: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('Label of the actor in the open level (e.g. "StaticMeshActor_0"). Provide this OR asset_path.'),
|
|
173
|
+
asset_path: z
|
|
174
|
+
.string()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe('UE asset path to a Static Mesh asset (e.g. "/Game/Meshes/SM_Rock"). Provide this OR actor_label.'),
|
|
177
|
+
}),
|
|
178
|
+
annotations: {
|
|
179
|
+
readOnlyHint: true,
|
|
180
|
+
destructiveHint: false,
|
|
181
|
+
},
|
|
182
|
+
}, withKnownIssues('ue_actor_materials', async (args) => {
|
|
183
|
+
const payload = {};
|
|
184
|
+
if (args.actor_label)
|
|
185
|
+
payload['actor_label'] = args.actor_label;
|
|
186
|
+
if (args.asset_path)
|
|
187
|
+
payload['asset_path'] = args.asset_path;
|
|
188
|
+
return sendOrDisconnect(_bridge, { type: 'material.actorMaterials', payload });
|
|
189
|
+
}));
|
|
190
|
+
}
|