Quaternius weapons, input fixes, sound & path fixes
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle) - Created weapon resource files with balanced stats - Wired Pistol + Rifle into player.tscn replacing Kenney blasters - Fixed WASD input (matched input map action names) - Fixed Escape key toggles mouse capture - Added Audio autoload singleton - Fixed broken sound asset paths across all scripts - Fixed all scene ext_resource text paths (nested under assets/kenney-fps/) - Fixed all .import source_file paths - Deleted stale .import files (will be regenerated by editor on open)
This commit is contained in:
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Parsing and editing primitives for the `[autoload]` section of project.godot.
|
||||
*
|
||||
* Used by:
|
||||
* - tools/autoload-tools.ts — list/add/remove/update_autoload handlers
|
||||
* - utils/bridge-manager.ts — McpBridge inject/cleanup/repair
|
||||
*
|
||||
* Pure functions: each takes the absolute path to project.godot and returns
|
||||
* either parsed data or a boolean indicating whether the file was mutated.
|
||||
*/
|
||||
export interface AutoloadEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
singleton: boolean;
|
||||
}
|
||||
/**
|
||||
* Matches an empty `[autoload]` section (the header followed by only blank
|
||||
* lines, up to the next section header or end-of-file). Used by cleanup paths
|
||||
* to drop the section after the last entry is removed.
|
||||
*/
|
||||
export declare const EMPTY_AUTOLOAD_SECTION_REGEX: RegExp;
|
||||
/**
|
||||
* Mirrors the parser's `\w+` assumption (parseAutoloads / removeAutoloadEntry).
|
||||
* Enforced on write paths to prevent a name with newlines or INI section
|
||||
* delimiters from corrupting project.godot.
|
||||
*/
|
||||
export declare const VALID_AUTOLOAD_NAME_REGEX: RegExp;
|
||||
export declare function normalizeAutoloadPath(p: string): string;
|
||||
export declare function parseAutoloads(projectFilePath: string, existingContent?: string): AutoloadEntry[];
|
||||
export declare function addAutoloadEntry(projectFilePath: string, name: string, path: string, singleton: boolean, existingContent?: string): void;
|
||||
/**
|
||||
* Remove the named autoload entry. Also drops the `[autoload]` section header
|
||||
* if the removed entry was the last one in it. Returns true when the file was
|
||||
* mutated.
|
||||
*/
|
||||
export declare function removeAutoloadEntry(projectFilePath: string, name: string): boolean;
|
||||
export declare function updateAutoloadEntry(projectFilePath: string, name: string, newPath?: string, singleton?: boolean): boolean;
|
||||
//# sourceMappingURL=autoload-ini.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"autoload-ini.d.ts","sourceRoot":"","sources":["../../src/utils/autoload-ini.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,QAAkC,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAU,CAAC;AAUjD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAsBjG;AAED,wBAAgB,gBAAgB,CAC9B,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,OAAO,EAClB,eAAe,CAAC,EAAE,MAAM,GACvB,IAAI,CAkBN;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA6BlF;AAED,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,CA8BT"}
|
||||
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
/**
|
||||
* Matches an empty `[autoload]` section (the header followed by only blank
|
||||
* lines, up to the next section header or end-of-file). Used by cleanup paths
|
||||
* to drop the section after the last entry is removed.
|
||||
*/
|
||||
export const EMPTY_AUTOLOAD_SECTION_REGEX = /\[autoload\]\s*(?=\n\[|\n*$)/g;
|
||||
/**
|
||||
* Mirrors the parser's `\w+` assumption (parseAutoloads / removeAutoloadEntry).
|
||||
* Enforced on write paths to prevent a name with newlines or INI section
|
||||
* delimiters from corrupting project.godot.
|
||||
*/
|
||||
export const VALID_AUTOLOAD_NAME_REGEX = /^\w+$/;
|
||||
function assertValidName(name) {
|
||||
if (!VALID_AUTOLOAD_NAME_REGEX.test(name)) {
|
||||
throw new Error(`Invalid autoload name '${name}': must contain only word characters (letters, digits, underscore)`);
|
||||
}
|
||||
}
|
||||
export function normalizeAutoloadPath(p) {
|
||||
return p.startsWith('res://') ? p : `res://${p}`;
|
||||
}
|
||||
export function parseAutoloads(projectFilePath, existingContent) {
|
||||
const content = existingContent ?? readFileSync(projectFilePath, 'utf8');
|
||||
const autoloads = [];
|
||||
let inAutoloadSection = false;
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('[')) {
|
||||
inAutoloadSection = trimmed === '[autoload]';
|
||||
continue;
|
||||
}
|
||||
if (!inAutoloadSection || trimmed === '' || trimmed.startsWith(';') || trimmed.startsWith('#'))
|
||||
continue;
|
||||
// The surrounding `"?` are intentional: Godot always writes quotes, but
|
||||
// hand-edited project.godot files sometimes omit them. Tolerating both
|
||||
// shapes means a missing quote pair doesn't silently drop the entry.
|
||||
const match = trimmed.match(/^(\w+)="?(\*?)([^"]*?)"?$/);
|
||||
if (match) {
|
||||
autoloads.push({ name: match[1], singleton: match[2] === '*', path: match[3] });
|
||||
}
|
||||
}
|
||||
return autoloads;
|
||||
}
|
||||
export function addAutoloadEntry(projectFilePath, name, path, singleton, existingContent) {
|
||||
assertValidName(name);
|
||||
const content = existingContent ?? readFileSync(projectFilePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`;
|
||||
const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]');
|
||||
if (sectionIdx === -1) {
|
||||
writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8');
|
||||
return;
|
||||
}
|
||||
let insertIdx = sectionIdx + 1;
|
||||
while (insertIdx < lines.length && !lines[insertIdx].trim().startsWith('[')) {
|
||||
insertIdx++;
|
||||
}
|
||||
lines.splice(insertIdx, 0, entry);
|
||||
writeFileSync(projectFilePath, lines.join('\n'), 'utf8');
|
||||
}
|
||||
/**
|
||||
* Remove the named autoload entry. Also drops the `[autoload]` section header
|
||||
* if the removed entry was the last one in it. Returns true when the file was
|
||||
* mutated.
|
||||
*/
|
||||
export function removeAutoloadEntry(projectFilePath, name) {
|
||||
const content = readFileSync(projectFilePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let inAutoloadSection = false;
|
||||
let removed = false;
|
||||
const filtered = lines.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('[')) {
|
||||
inAutoloadSection = trimmed === '[autoload]';
|
||||
return true;
|
||||
}
|
||||
if (inAutoloadSection) {
|
||||
const match = trimmed.match(/^(\w+)=/);
|
||||
if (match && match[1] === name) {
|
||||
removed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!removed)
|
||||
return false;
|
||||
let newContent = filtered.join('\n');
|
||||
newContent = newContent.replace(EMPTY_AUTOLOAD_SECTION_REGEX, '');
|
||||
newContent = newContent.trimEnd() + '\n';
|
||||
writeFileSync(projectFilePath, newContent, 'utf8');
|
||||
return true;
|
||||
}
|
||||
export function updateAutoloadEntry(projectFilePath, name, newPath, singleton) {
|
||||
assertValidName(name);
|
||||
const content = readFileSync(projectFilePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let inAutoloadSection = false;
|
||||
let updated = false;
|
||||
const newLines = lines.map((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('[')) {
|
||||
inAutoloadSection = trimmed === '[autoload]';
|
||||
return line;
|
||||
}
|
||||
if (inAutoloadSection) {
|
||||
// The surrounding `"?` are intentional: Godot always writes quotes, but
|
||||
// hand-edited project.godot files sometimes omit them. Tolerating both
|
||||
// shapes means a missing quote pair doesn't silently drop the entry.
|
||||
const match = trimmed.match(/^(\w+)="?(\*?)([^"]*?)"?$/);
|
||||
if (match && match[1] === name) {
|
||||
const effectiveSingleton = singleton !== undefined ? singleton : match[2] === '*';
|
||||
const effectivePath = newPath !== undefined ? normalizeAutoloadPath(newPath) : match[3];
|
||||
updated = true;
|
||||
return `${name}="${effectiveSingleton ? '*' : ''}${effectivePath}"`;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
if (updated)
|
||||
writeFileSync(projectFilePath, newLines.join('\n'), 'utf8');
|
||||
return updated;
|
||||
}
|
||||
//# sourceMappingURL=autoload-ini.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"autoload-ini.js","sourceRoot":"","sources":["../../src/utils/autoload-ini.ts"],"names":[],"mappings":"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"}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Owns the McpBridge autoload artifact: the script copy in the target project,
|
||||
* the `[autoload]` entry in project.godot, the `.mcp/.gdignore` marker, and the
|
||||
* `.gitignore` augmentation. GodotRunner delegates to this for inject/cleanup
|
||||
* during run_project / attach_project / stop_project flows.
|
||||
*
|
||||
* The project-root bridge script is runtime-owned and refreshed on first
|
||||
* injection for a manager session so a rebuilt server cannot talk to stale
|
||||
* GDScript from an earlier run. Idempotent within a session via
|
||||
* `injectedProjects`: a second `inject()` call for the same path short-circuits
|
||||
* without rewriting project.godot.
|
||||
*/
|
||||
export declare class BridgeManager {
|
||||
private bridgeScriptPath;
|
||||
private injectedProjects;
|
||||
private repairedProjects;
|
||||
/**
|
||||
* Last port baked into the on-disk script per project. Used by the
|
||||
* race-detection helper in runtime-tools to spot a concurrent re-inject
|
||||
* after a bridge-wait timeout.
|
||||
*/
|
||||
private lastInjectedPort;
|
||||
constructor(bridgeScriptPath: string);
|
||||
inject(projectPath: string, port: number): void;
|
||||
cleanup(projectPath: string): void;
|
||||
/**
|
||||
* Read the port currently baked into the project's bridge script. Returns
|
||||
* the integer on success, or null if the file is missing, unreadable, or
|
||||
* lacks the marker. Used to detect concurrent re-inject after a bridge-
|
||||
* wait timeout (another MCP client may have rewritten the port).
|
||||
*/
|
||||
readBakedPort(projectPath: string): number | null;
|
||||
/**
|
||||
* If project.godot still has an `McpBridge=` line but the script file is
|
||||
* missing, the autoload would crash every subsequent headless op. Detect and
|
||||
* clean the orphan before running an operation.
|
||||
*
|
||||
* Cached per project: once a path has been checked clean, skip the file
|
||||
* reads on subsequent ops in the same session.
|
||||
*/
|
||||
repairOrphaned(projectPath: string): void;
|
||||
private removeBridgeArtifacts;
|
||||
private ensureMcpGdignore;
|
||||
private ensureGitignored;
|
||||
}
|
||||
//# sourceMappingURL=bridge-manager.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge-manager.d.ts","sourceRoot":"","sources":["../../src/utils/bridge-manager.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;IAUZ,OAAO,CAAC,gBAAgB;IATpC,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,gBAAgB,CAA0B;IAClD;;;;OAIG;IACH,OAAO,CAAC,gBAAgB,CAAkC;gBAEtC,gBAAgB,EAAE,MAAM;IAE5C,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAuC/C,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAOlC;;;;;OAKG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAejD;;;;;;;OAOG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAqBzC,OAAO,CAAC,qBAAqB;IAkC7B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;CAkBzB"}
|
||||
Vendored
+186
@@ -0,0 +1,186 @@
|
||||
import { join } from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
||||
import { logDebug } from './logger.js';
|
||||
import { addAutoloadEntry, parseAutoloads, removeAutoloadEntry } from './autoload-ini.js';
|
||||
const BRIDGE_AUTOLOAD_NAME = 'McpBridge';
|
||||
const BRIDGE_SCRIPT_FILENAME = 'mcp_bridge.gd';
|
||||
const MCP_GITIGNORE_ENTRY = '.mcp/';
|
||||
// Matches the baked-port marker line inserted in src/scripts/mcp_bridge.gd —
|
||||
// `const PORT := <int>` — so inject() can rewrite the integer per project.
|
||||
const BAKED_PORT_REGEX = /const PORT := \d+/;
|
||||
/**
|
||||
* Owns the McpBridge autoload artifact: the script copy in the target project,
|
||||
* the `[autoload]` entry in project.godot, the `.mcp/.gdignore` marker, and the
|
||||
* `.gitignore` augmentation. GodotRunner delegates to this for inject/cleanup
|
||||
* during run_project / attach_project / stop_project flows.
|
||||
*
|
||||
* The project-root bridge script is runtime-owned and refreshed on first
|
||||
* injection for a manager session so a rebuilt server cannot talk to stale
|
||||
* GDScript from an earlier run. Idempotent within a session via
|
||||
* `injectedProjects`: a second `inject()` call for the same path short-circuits
|
||||
* without rewriting project.godot.
|
||||
*/
|
||||
export class BridgeManager {
|
||||
bridgeScriptPath;
|
||||
injectedProjects = new Set();
|
||||
repairedProjects = new Set();
|
||||
/**
|
||||
* Last port baked into the on-disk script per project. Used by the
|
||||
* race-detection helper in runtime-tools to spot a concurrent re-inject
|
||||
* after a bridge-wait timeout.
|
||||
*/
|
||||
lastInjectedPort = new Map();
|
||||
constructor(bridgeScriptPath) {
|
||||
this.bridgeScriptPath = bridgeScriptPath;
|
||||
}
|
||||
inject(projectPath, port) {
|
||||
// Always rewrite the destination — the per-project bridge script may
|
||||
// differ from the template by exactly the baked integer, so a size/mtime
|
||||
// shortcut no longer maps to "up-to-date." Bake the resolved port into
|
||||
// the const PORT line so the running game listens on the exact port the
|
||||
// Node side will connect to.
|
||||
const template = readFileSync(this.bridgeScriptPath, 'utf8');
|
||||
if (!BAKED_PORT_REGEX.test(template)) {
|
||||
throw new Error(`Bridge script template at ${this.bridgeScriptPath} is missing the 'const PORT := <int>' marker`);
|
||||
}
|
||||
const baked = template.replace(BAKED_PORT_REGEX, `const PORT := ${port}`);
|
||||
const destScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
writeFileSync(destScript, baked, 'utf8');
|
||||
this.lastInjectedPort.set(projectPath, port);
|
||||
logDebug(`Wrote bridge autoload at ${destScript} (baked port ${port})`);
|
||||
if (this.injectedProjects.has(projectPath)) {
|
||||
logDebug('Bridge already injected for this project; refreshed script only.');
|
||||
return;
|
||||
}
|
||||
this.ensureMcpGdignore(projectPath);
|
||||
this.ensureGitignored(projectPath);
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
const existing = parseAutoloads(projectFile);
|
||||
const alreadyRegistered = existing.some((a) => a.name === BRIDGE_AUTOLOAD_NAME);
|
||||
if (alreadyRegistered) {
|
||||
logDebug('Bridge autoload already present, skipping injection');
|
||||
}
|
||||
else {
|
||||
addAutoloadEntry(projectFile, BRIDGE_AUTOLOAD_NAME, BRIDGE_SCRIPT_FILENAME, true);
|
||||
logDebug('Injected bridge autoload into project.godot');
|
||||
}
|
||||
this.injectedProjects.add(projectPath);
|
||||
}
|
||||
cleanup(projectPath) {
|
||||
this.removeBridgeArtifacts(projectPath);
|
||||
this.injectedProjects.delete(projectPath);
|
||||
this.repairedProjects.delete(projectPath);
|
||||
this.lastInjectedPort.delete(projectPath);
|
||||
}
|
||||
/**
|
||||
* Read the port currently baked into the project's bridge script. Returns
|
||||
* the integer on success, or null if the file is missing, unreadable, or
|
||||
* lacks the marker. Used to detect concurrent re-inject after a bridge-
|
||||
* wait timeout (another MCP client may have rewritten the port).
|
||||
*/
|
||||
readBakedPort(projectPath) {
|
||||
const destScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (!existsSync(destScript))
|
||||
return null;
|
||||
try {
|
||||
const content = readFileSync(destScript, 'utf8');
|
||||
const match = content.match(BAKED_PORT_REGEX);
|
||||
if (!match)
|
||||
return null;
|
||||
const parsed = Number.parseInt(match[0].replace(/^const PORT := /, ''), 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535)
|
||||
return null;
|
||||
return parsed;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If project.godot still has an `McpBridge=` line but the script file is
|
||||
* missing, the autoload would crash every subsequent headless op. Detect and
|
||||
* clean the orphan before running an operation.
|
||||
*
|
||||
* Cached per project: once a path has been checked clean, skip the file
|
||||
* reads on subsequent ops in the same session.
|
||||
*/
|
||||
repairOrphaned(projectPath) {
|
||||
if (this.repairedProjects.has(projectPath))
|
||||
return;
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
const bridgeScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (!existsSync(projectFile))
|
||||
return;
|
||||
if (existsSync(bridgeScript)) {
|
||||
this.repairedProjects.add(projectPath);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(projectFile, 'utf8');
|
||||
if (content.includes(`${BRIDGE_AUTOLOAD_NAME}=`)) {
|
||||
this.removeBridgeArtifacts(projectPath);
|
||||
logDebug('Cleaned up orphaned McpBridge autoload entry');
|
||||
}
|
||||
this.repairedProjects.add(projectPath);
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to check/repair orphaned bridge: ${err}`);
|
||||
}
|
||||
}
|
||||
removeBridgeArtifacts(projectPath) {
|
||||
try {
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
if (existsSync(projectFile)) {
|
||||
const removed = removeAutoloadEntry(projectFile, BRIDGE_AUTOLOAD_NAME);
|
||||
if (removed) {
|
||||
logDebug(`Removed ${BRIDGE_AUTOLOAD_NAME} autoload from project.godot`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to clean ${BRIDGE_AUTOLOAD_NAME} from project.godot: ${err}`);
|
||||
}
|
||||
try {
|
||||
const scriptFile = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (existsSync(scriptFile)) {
|
||||
unlinkSync(scriptFile);
|
||||
logDebug(`Removed ${BRIDGE_SCRIPT_FILENAME} from project`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to remove ${BRIDGE_SCRIPT_FILENAME}: ${err}`);
|
||||
}
|
||||
try {
|
||||
const uidFile = join(projectPath, `${BRIDGE_SCRIPT_FILENAME}.uid`);
|
||||
if (existsSync(uidFile)) {
|
||||
unlinkSync(uidFile);
|
||||
logDebug(`Removed ${BRIDGE_SCRIPT_FILENAME}.uid from project`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to remove ${BRIDGE_SCRIPT_FILENAME}.uid: ${err}`);
|
||||
}
|
||||
}
|
||||
ensureMcpGdignore(projectPath) {
|
||||
const mcpDir = join(projectPath, '.mcp');
|
||||
mkdirSync(mcpDir, { recursive: true });
|
||||
writeFileSync(join(mcpDir, '.gdignore'), '', 'utf8');
|
||||
logDebug('Created .mcp/.gdignore');
|
||||
}
|
||||
ensureGitignored(projectPath) {
|
||||
const gitignorePath = join(projectPath, '.gitignore');
|
||||
if (existsSync(gitignorePath)) {
|
||||
const gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
||||
if (!gitignoreContent.includes(MCP_GITIGNORE_ENTRY)) {
|
||||
const newline = gitignoreContent.endsWith('\n') ? '' : '\n';
|
||||
writeFileSync(gitignorePath, gitignoreContent + newline + MCP_GITIGNORE_ENTRY + '\n', 'utf8');
|
||||
logDebug('Added .mcp/ to existing .gitignore');
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeFileSync(gitignorePath, MCP_GITIGNORE_ENTRY + '\n', 'utf8');
|
||||
logDebug('Created .gitignore with .mcp/ entry');
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=bridge-manager.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Wire format shared between the Node-side `GodotRunner.sendCommand` and the
|
||||
* GDScript-side `McpBridge` autoload.
|
||||
*
|
||||
* KEEP IN SYNC: src/scripts/mcp_bridge.gd implements the same framing on the
|
||||
* Godot side. Any change here MUST be mirrored there (and vice versa).
|
||||
*
|
||||
* Frame: 4-byte big-endian length prefix + UTF-8 JSON payload.
|
||||
* Max frame size is 16 MiB; oversize frames are rejected on receive.
|
||||
*/
|
||||
export declare const DEFAULT_BRIDGE_PORT = 9900;
|
||||
export declare const MAX_FRAME_BYTES: number;
|
||||
export declare const FRAME_HEADER_BYTES = 4;
|
||||
/**
|
||||
* Find an available TCP port by binding to port 0 (OS-assigned ephemeral port),
|
||||
* reading the assigned port, and closing the listener. The brief TOCTOU window
|
||||
* between close and the consumer's listen is acceptable — if a collision occurs,
|
||||
* the bridge readiness check will surface the failure.
|
||||
*/
|
||||
export declare function findFreePort(): Promise<number>;
|
||||
/**
|
||||
* Encode a JSON string as a length-prefixed frame.
|
||||
*/
|
||||
export declare function encodeFrame(payload: string): Buffer;
|
||||
export interface ParseFramesResult {
|
||||
frames: Buffer[];
|
||||
remainder: Buffer;
|
||||
}
|
||||
/**
|
||||
* Pull as many complete frames as possible from a streaming buffer. Any
|
||||
* partial frame at the tail is returned as `remainder` for the next call.
|
||||
*
|
||||
* Throws if a header advertises a payload larger than {@link MAX_FRAME_BYTES} —
|
||||
* the caller should treat this as a fatal protocol error and close the socket.
|
||||
*/
|
||||
export declare function parseFrames(buffer: Buffer): ParseFramesResult;
|
||||
//# sourceMappingURL=bridge-protocol.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge-protocol.d.ts","sourceRoot":"","sources":["../../src/utils/bridge-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,eAAe,QAAmB,CAAC;AAChD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAkB9C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAoB7D"}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Wire format shared between the Node-side `GodotRunner.sendCommand` and the
|
||||
* GDScript-side `McpBridge` autoload.
|
||||
*
|
||||
* KEEP IN SYNC: src/scripts/mcp_bridge.gd implements the same framing on the
|
||||
* Godot side. Any change here MUST be mirrored there (and vice versa).
|
||||
*
|
||||
* Frame: 4-byte big-endian length prefix + UTF-8 JSON payload.
|
||||
* Max frame size is 16 MiB; oversize frames are rejected on receive.
|
||||
*/
|
||||
import * as net from 'net';
|
||||
export const DEFAULT_BRIDGE_PORT = 9900;
|
||||
export const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
||||
export const FRAME_HEADER_BYTES = 4;
|
||||
/**
|
||||
* Find an available TCP port by binding to port 0 (OS-assigned ephemeral port),
|
||||
* reading the assigned port, and closing the listener. The brief TOCTOU window
|
||||
* between close and the consumer's listen is acceptable — if a collision occurs,
|
||||
* the bridge readiness check will surface the failure.
|
||||
*/
|
||||
export function findFreePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, '127.0.0.1', () => {
|
||||
const addr = srv.address();
|
||||
if (!addr || typeof addr === 'string') {
|
||||
srv.close();
|
||||
reject(new Error('Failed to determine assigned port'));
|
||||
return;
|
||||
}
|
||||
const port = addr.port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
// One-shot: only fires before listen() succeeds. If listen succeeded,
|
||||
// we proceed to srv.close() in the listening callback — a later error
|
||||
// is not possible from this server, so the listener stays safely dormant.
|
||||
srv.on('error', reject);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Encode a JSON string as a length-prefixed frame.
|
||||
*/
|
||||
export function encodeFrame(payload) {
|
||||
const body = Buffer.from(payload, 'utf8');
|
||||
if (body.length > MAX_FRAME_BYTES) {
|
||||
throw new Error(`Bridge frame too large: ${body.length} bytes (limit ${MAX_FRAME_BYTES})`);
|
||||
}
|
||||
const frame = Buffer.allocUnsafe(FRAME_HEADER_BYTES + body.length);
|
||||
frame.writeUInt32BE(body.length, 0);
|
||||
body.copy(frame, FRAME_HEADER_BYTES);
|
||||
return frame;
|
||||
}
|
||||
/**
|
||||
* Pull as many complete frames as possible from a streaming buffer. Any
|
||||
* partial frame at the tail is returned as `remainder` for the next call.
|
||||
*
|
||||
* Throws if a header advertises a payload larger than {@link MAX_FRAME_BYTES} —
|
||||
* the caller should treat this as a fatal protocol error and close the socket.
|
||||
*/
|
||||
export function parseFrames(buffer) {
|
||||
const frames = [];
|
||||
let offset = 0;
|
||||
while (buffer.length - offset >= FRAME_HEADER_BYTES) {
|
||||
const len = buffer.readUInt32BE(offset);
|
||||
if (len > MAX_FRAME_BYTES) {
|
||||
throw new Error(`Bridge frame header advertises ${len} bytes, exceeds limit ${MAX_FRAME_BYTES}`);
|
||||
}
|
||||
const frameStart = offset + FRAME_HEADER_BYTES;
|
||||
const frameEnd = frameStart + len;
|
||||
if (buffer.length < frameEnd)
|
||||
break;
|
||||
frames.push(buffer.subarray(frameStart, frameEnd));
|
||||
offset = frameEnd;
|
||||
}
|
||||
const remainder = offset === 0 ? buffer : buffer.subarray(offset);
|
||||
return { frames, remainder };
|
||||
}
|
||||
//# sourceMappingURL=bridge-protocol.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge-protocol.js","sourceRoot":"","sources":["../../src/utils/bridge-protocol.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+248
@@ -0,0 +1,248 @@
|
||||
import type { ChildProcess } from 'child_process';
|
||||
/**
|
||||
* Thrown when the bridge socket closes (Godot exited, port closed, or peer
|
||||
* dropped the connection mid-flight). Lets callers distinguish
|
||||
* "session ended" from generic transport errors.
|
||||
*/
|
||||
export declare class BridgeDisconnectedError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
export declare const BRIDGE_WAIT_SPAWNED_TIMEOUT_MS = 8000;
|
||||
/**
|
||||
* Normalize a path for cross-platform comparison.
|
||||
* Folds Windows backslashes to forward slashes and strips trailing slashes,
|
||||
* so Node's `path.normalize` output matches Godot's `globalize_path("res://")`.
|
||||
*/
|
||||
export declare function normalizeForCompare(p: string): string;
|
||||
/**
|
||||
* Extract JSON from Godot output by finding the first { or [ and matching to the end.
|
||||
* This strips debug logs, version banners, and other noise.
|
||||
*/
|
||||
export declare function extractJson(output: string): string;
|
||||
/**
|
||||
* Strip Godot banner and debug lines from output, keeping only meaningful content.
|
||||
*/
|
||||
export declare function cleanOutput(output: string): string;
|
||||
export declare function cleanStdout(stdout: string): string;
|
||||
export interface GodotProcess {
|
||||
process: ChildProcess;
|
||||
output: string[];
|
||||
errors: string[];
|
||||
totalErrorsWritten: number;
|
||||
exitCode: number | null;
|
||||
hasExited: boolean;
|
||||
sessionToken: string;
|
||||
}
|
||||
export type RuntimeSessionMode = 'spawned' | 'attached';
|
||||
export interface RuntimeStopResult {
|
||||
mode: RuntimeSessionMode;
|
||||
output: string[];
|
||||
errors: string[];
|
||||
externalProcessPreserved?: boolean;
|
||||
}
|
||||
export interface GodotServerConfig {
|
||||
godotPath?: string;
|
||||
debugMode?: boolean;
|
||||
}
|
||||
export interface OperationParams {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface OperationResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
export interface ToolAnnotations {
|
||||
readOnlyHint?: boolean;
|
||||
destructiveHint?: boolean;
|
||||
idempotentHint?: boolean;
|
||||
openWorldHint?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: string;
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
};
|
||||
outputSchema?: {
|
||||
type: string;
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
annotations?: ToolAnnotations;
|
||||
}
|
||||
export interface ToolResponse {
|
||||
content: Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
[k: string]: unknown;
|
||||
}>;
|
||||
isError?: boolean;
|
||||
structuredContent?: Record<string, unknown>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export type ToolHandler = (runner: GodotRunner, args: OperationParams) => Promise<ToolResponse> | ToolResponse;
|
||||
export declare function normalizeParameters(params: OperationParams): OperationParams;
|
||||
export declare function convertCamelToSnakeCase(params: OperationParams): OperationParams;
|
||||
/**
|
||||
* Check whether a display server (X11 / Wayland) is available on the current
|
||||
* platform. On macOS and Windows the display subsystem is always present;
|
||||
* on Linux we probe the standard environment variables.
|
||||
*/
|
||||
export declare function checkDisplayAvailable(): boolean;
|
||||
export declare function validatePath(path: string): boolean;
|
||||
/**
|
||||
* Stricter check for paths that must stay inside `projectPath`. Rejects `..`
|
||||
* (via `validatePath`) and absolute paths that escape the project root.
|
||||
* `path.join('/project', '/etc/passwd')` resolves to `/etc/passwd`, so the
|
||||
* basic `..`-substring check alone permits absolute-path traversal.
|
||||
*
|
||||
* Tolerates a leading `res://` (Godot's project-root URI) by stripping it
|
||||
* before resolving — autoload entries and resource paths use this prefix.
|
||||
*/
|
||||
export declare function validateSubPath(projectPath: string, userPath: string): boolean;
|
||||
/**
|
||||
* Validate a Godot scene-tree path (NodePath). Scene-tree paths are a
|
||||
* separate namespace from filesystem paths — they address nodes inside
|
||||
* a scene, not files on disk, so the project-root containment check
|
||||
* in `validateSubPath` does not apply.
|
||||
*
|
||||
* Rejects empty strings and `..` segments. Accepts both relative
|
||||
* (`root/Player`) and absolute (`/root/Player`) Godot forms; the
|
||||
* codebase convention is the relative form.
|
||||
*/
|
||||
export declare function validateNodePath(path: string): boolean;
|
||||
/**
|
||||
* True when `child` resolves to `parent` or a path beneath it. Used by
|
||||
* defense-in-depth checks on bridge-returned paths (e.g. screenshot files
|
||||
* that must live under `.mcp/screenshots/`).
|
||||
*/
|
||||
export declare function isUnderDir(parent: string, child: string): boolean;
|
||||
/**
|
||||
* Return `error.message` when `error` is an `Error`, otherwise `'Unknown error'`.
|
||||
* Centralizes the catch-block boilerplate so handlers can build error responses
|
||||
* without repeating the `instanceof Error` ternary.
|
||||
*/
|
||||
export declare function getErrorMessage(error: unknown): string;
|
||||
/**
|
||||
* Build the absolute path to a project's `project.godot` manifest. Use this
|
||||
* instead of `join(dir, 'project.godot')` ad hoc.
|
||||
*/
|
||||
export declare function projectGodotPath(projectDir: string): string;
|
||||
/**
|
||||
* Extract the first [ERROR] message from GDScript stderr output.
|
||||
* Falls back to a generic message if no [ERROR] line is found.
|
||||
*/
|
||||
export declare function extractGdError(stderr: string): string;
|
||||
export declare function createErrorResponse(message: string, possibleSolutions?: string[]): {
|
||||
content: Array<{
|
||||
type: 'text';
|
||||
text: string;
|
||||
}>;
|
||||
isError: boolean;
|
||||
};
|
||||
/**
|
||||
* Wrap a structured payload as a ToolResponse that satisfies the MCP
|
||||
* outputSchema contract. The same payload is emitted both as a JSON text
|
||||
* content block (for lenient clients) and as `structuredContent` (required
|
||||
* by strict clients per MCP spec revision 2025-06-18).
|
||||
*
|
||||
* `extraContent` is prepended for handlers that also return non-text blocks
|
||||
* (e.g. `take_screenshot`'s inline image).
|
||||
*/
|
||||
export declare function createStructuredResponse<T extends Record<string, unknown>>(payload: T, extraContent?: Array<{
|
||||
type: string;
|
||||
[k: string]: unknown;
|
||||
}>): ToolResponse;
|
||||
interface ValidatedProjectArgs {
|
||||
projectPath: string;
|
||||
}
|
||||
interface ValidatedSceneArgs {
|
||||
projectPath: string;
|
||||
scenePath: string;
|
||||
}
|
||||
type ValidationErrorResult = ReturnType<typeof createErrorResponse>;
|
||||
export declare function validateProjectArgs(args: OperationParams): ValidatedProjectArgs | ValidationErrorResult;
|
||||
export declare function validateSceneArgs(args: OperationParams, opts?: {
|
||||
sceneRequired?: boolean;
|
||||
}): ValidatedSceneArgs | ValidationErrorResult;
|
||||
export declare class GodotRunner {
|
||||
private godotPath;
|
||||
private operationsScriptPath;
|
||||
private bridge;
|
||||
private validatedPaths;
|
||||
private cachedVersion;
|
||||
activeProcess: GodotProcess | null;
|
||||
activeProjectPath: string | null;
|
||||
activeSessionMode: RuntimeSessionMode | null;
|
||||
activeBridgePort: number | null;
|
||||
private socket;
|
||||
private rxChunks;
|
||||
private rxTotal;
|
||||
private inFlight;
|
||||
constructor(config?: GodotServerConfig);
|
||||
private isValidGodotPathSync;
|
||||
private spawnAsync;
|
||||
private isValidGodotPath;
|
||||
detectGodotPath(): Promise<void>;
|
||||
getGodotPath(): string | null;
|
||||
/**
|
||||
* Read the port currently baked into the project's bridge script. Returns
|
||||
* null if the file is missing or malformed. Thin pass-through to
|
||||
* BridgeManager — used by bridge-wait-timeout race detection.
|
||||
*/
|
||||
readBakedBridgePort(projectPath: string): number | null;
|
||||
getVersion(): Promise<string>;
|
||||
executeOperation(operation: string, params: OperationParams, projectPath: string, timeoutMs?: number): Promise<OperationResult>;
|
||||
launchEditor(projectPath: string): ChildProcess;
|
||||
runProject(projectPath: string, scene?: string, background?: boolean, bridgePort?: number): Promise<GodotProcess>;
|
||||
attachProject(projectPath: string, bridgePort?: number): Promise<void>;
|
||||
stopProject(): Promise<RuntimeStopResult | null>;
|
||||
hasActiveRuntimeSession(): boolean;
|
||||
/**
|
||||
* Send a JSON command to the McpBridge over a long-lived TCP connection.
|
||||
*
|
||||
* MCP serializes tool calls so we hold one in-flight command at a time. The
|
||||
* socket is lazy-connected on first call and persists across commands until
|
||||
* `closeConnection` (or a peer-side close). A close mid-flight rejects with
|
||||
* `BridgeDisconnectedError`; a per-command timeout rejects but does NOT
|
||||
* close the socket — a slow command does not invalidate the session.
|
||||
*/
|
||||
sendCommand(command: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<string>;
|
||||
/**
|
||||
* Tear down the bridge socket. Idempotent. Any in-flight command is
|
||||
* rejected with a session-ended error.
|
||||
*/
|
||||
closeConnection(): void;
|
||||
private resetRxBuffer;
|
||||
getErrorCount(): number;
|
||||
getErrorsSince(marker: number): string[];
|
||||
private static readonly SCRIPT_ERROR_PATTERNS;
|
||||
private static readonly RETRYABLE_BRIDGE_COMMANDS;
|
||||
extractRuntimeErrors(lines: string[]): string[];
|
||||
private sendCommandWithReconnect;
|
||||
sendCommandWithErrors(command: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<{
|
||||
response: string;
|
||||
runtimeErrors: string[];
|
||||
}>;
|
||||
/**
|
||||
* Shared poll loop for `waitForBridge` (spawned) and `waitForBridgeAttached`.
|
||||
* Sends `ping` payloads until the bridge replies with a pong that
|
||||
* `validatePong` accepts, the deadline passes, or `shouldAbort` reports
|
||||
* the spawned process has exited.
|
||||
*/
|
||||
private pollBridge;
|
||||
waitForBridgeAttached(timeoutMs?: number, intervalMs?: number): Promise<{
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
waitForBridge(timeoutMs?: number, intervalMs?: number): Promise<{
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
getRecentErrors(count?: number): string[];
|
||||
}
|
||||
export {};
|
||||
//# sourceMappingURL=godot-runner.d.ts.map
|
||||
+1
File diff suppressed because one or more lines are too long
Vendored
+1119
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+36
@@ -0,0 +1,36 @@
|
||||
import type { GodotRunner, OperationParams, ToolResponse } from './godot-runner.js';
|
||||
export declare const MAX_RUNTIME_ERROR_CONTEXT_LINES = 30;
|
||||
/**
|
||||
* Wraps the execute + empty-stdout-check + try/catch around a headless GDScript
|
||||
* operation. Used by the 15 scene/node mutation handlers in tools/scene-tools.ts
|
||||
* and tools/node-tools.ts to eliminate identical error-handling duplication.
|
||||
*
|
||||
* Handlers retain control of: parameter normalization, project/scene validation,
|
||||
* field validation, and constructing the `params` object — those run before the
|
||||
* call. Success-shape construction (the JSON wrapping the GDScript stdout) is
|
||||
* also unchanged: this helper just returns `{ content: [{ type: 'text', text: stdout }] }`,
|
||||
* which is the exact shape every handler produced previously.
|
||||
*/
|
||||
export declare function executeSceneOp(runner: GodotRunner, operation: string, params: OperationParams, projectPath: string, failurePrefix: string, emptyStdoutSolutions: string[], exceptionSolutions?: string[], options?: {
|
||||
parseStdoutAsJson?: boolean;
|
||||
}): Promise<ToolResponse>;
|
||||
export type ParseResult<T> = {
|
||||
ok: true;
|
||||
data: T;
|
||||
} | {
|
||||
ok: false;
|
||||
response: ToolResponse;
|
||||
};
|
||||
/**
|
||||
* Parse a JSON frame returned by the McpBridge. On failure, returns a
|
||||
* structured error response so handlers can short-circuit with one branch.
|
||||
* `context` should describe which bridge command produced the frame.
|
||||
*/
|
||||
export declare function parseBridgeJson<T = unknown>(responseStr: string, context: string): ParseResult<T>;
|
||||
/**
|
||||
* Attach captured runtime errors as a `warnings` array on a tool response
|
||||
* payload. No-op when there are no runtime errors. Truncates to
|
||||
* `MAX_RUNTIME_ERROR_CONTEXT_LINES` to keep payloads bounded.
|
||||
*/
|
||||
export declare function attachRuntimeWarnings(target: Record<string, unknown>, runtimeErrors: string[]): void;
|
||||
//# sourceMappingURL=handler-helpers.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"handler-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/handler-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAQpF,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,oBAAoB,EAAE,MAAM,EAAE,EAC9B,kBAAkB,GAAE,MAAM,EAA4C,EACtE,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5C,OAAO,CAAC,YAAY,CAAC,CA2BvB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3F;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAejG;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,aAAa,EAAE,MAAM,EAAE,GACtB,IAAI,CAIN"}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { createErrorResponse, createStructuredResponse, extractGdError, getErrorMessage, } from './godot-runner.js';
|
||||
export const MAX_RUNTIME_ERROR_CONTEXT_LINES = 30;
|
||||
/**
|
||||
* Wraps the execute + empty-stdout-check + try/catch around a headless GDScript
|
||||
* operation. Used by the 15 scene/node mutation handlers in tools/scene-tools.ts
|
||||
* and tools/node-tools.ts to eliminate identical error-handling duplication.
|
||||
*
|
||||
* Handlers retain control of: parameter normalization, project/scene validation,
|
||||
* field validation, and constructing the `params` object — those run before the
|
||||
* call. Success-shape construction (the JSON wrapping the GDScript stdout) is
|
||||
* also unchanged: this helper just returns `{ content: [{ type: 'text', text: stdout }] }`,
|
||||
* which is the exact shape every handler produced previously.
|
||||
*/
|
||||
export async function executeSceneOp(runner, operation, params, projectPath, failurePrefix, emptyStdoutSolutions, exceptionSolutions = ['Ensure Godot is installed correctly'], options = {}) {
|
||||
try {
|
||||
const { stdout, stderr } = await runner.executeOperation(operation, params, projectPath);
|
||||
if (!stdout.trim()) {
|
||||
return createErrorResponse(`${failurePrefix}: ${extractGdError(stderr)}`, emptyStdoutSolutions);
|
||||
}
|
||||
if (options.parseStdoutAsJson) {
|
||||
try {
|
||||
const payload = JSON.parse(stdout.trim());
|
||||
return createStructuredResponse(payload);
|
||||
}
|
||||
catch (parseErr) {
|
||||
return createErrorResponse(`${failurePrefix}: GDScript returned invalid JSON (${getErrorMessage(parseErr)})`, [
|
||||
'This indicates a bug in godot_operations.gd — the operation should emit a JSON payload matching its outputSchema',
|
||||
'Check get_debug_output for the raw stdout and stderr',
|
||||
]);
|
||||
}
|
||||
}
|
||||
return { content: [{ type: 'text', text: stdout }] };
|
||||
}
|
||||
catch (error) {
|
||||
return createErrorResponse(`${failurePrefix}: ${getErrorMessage(error)}`, exceptionSolutions);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse a JSON frame returned by the McpBridge. On failure, returns a
|
||||
* structured error response so handlers can short-circuit with one branch.
|
||||
* `context` should describe which bridge command produced the frame.
|
||||
*/
|
||||
export function parseBridgeJson(responseStr, context) {
|
||||
try {
|
||||
return { ok: true, data: JSON.parse(responseStr) };
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
response: createErrorResponse(`Invalid response from bridge (${context}): ${getErrorMessage(error)}`, [
|
||||
'The bridge returned non-JSON data — check Godot stderr via get_debug_output',
|
||||
'Restart the project with stop_project followed by run_project',
|
||||
]),
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Attach captured runtime errors as a `warnings` array on a tool response
|
||||
* payload. No-op when there are no runtime errors. Truncates to
|
||||
* `MAX_RUNTIME_ERROR_CONTEXT_LINES` to keep payloads bounded.
|
||||
*/
|
||||
export function attachRuntimeWarnings(target, runtimeErrors) {
|
||||
if (runtimeErrors.length > 0) {
|
||||
target.warnings = runtimeErrors.slice(0, MAX_RUNTIME_ERROR_CONTEXT_LINES);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=handler-helpers.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"handler-helpers.js","sourceRoot":"","sources":["../../src/utils/handler-helpers.ts"],"names":[],"mappings":"AACA,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAElD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAmB,EACnB,SAAiB,EACjB,MAAuB,EACvB,WAAmB,EACnB,aAAqB,EACrB,oBAA8B,EAC9B,qBAA+B,CAAC,qCAAqC,CAAC,EACtE,UAA2C,EAAE;IAE7C,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QACzF,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,mBAAmB,CACxB,GAAG,aAAa,KAAK,cAAc,CAAC,MAAM,CAAC,EAAE,EAC7C,oBAAoB,CACrB,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAA4B,CAAC;gBACrE,OAAO,wBAAwB,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,QAAQ,EAAE,CAAC;gBAClB,OAAO,mBAAmB,CACxB,GAAG,aAAa,qCAAqC,eAAe,CAAC,QAAQ,CAAC,GAAG,EACjF;oBACE,kHAAkH;oBAClH,sDAAsD;iBACvD,CACF,CAAC;YACJ,CAAC;QACH,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,OAAO,mBAAmB,CAAC,GAAG,aAAa,KAAK,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAc,WAAmB,EAAE,OAAe;IAC/E,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAM,EAAE,CAAC;IAC1D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,mBAAmB,CAC3B,iCAAiC,OAAO,MAAM,eAAe,CAAC,KAAK,CAAC,EAAE,EACtE;gBACE,6EAA6E;gBAC7E,+DAA+D;aAChE,CACF;SACF,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA+B,EAC/B,aAAuB;IAEvB,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,+BAA+B,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare const DEBUG_MODE: boolean;
|
||||
export declare function logDebug(message: string): void;
|
||||
export declare function logError(message: string): void;
|
||||
//# sourceMappingURL=logger.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9C;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE9C"}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
// Debug mode from environment
|
||||
export const DEBUG_MODE = process.env.DEBUG === 'true';
|
||||
export function logDebug(message) {
|
||||
if (DEBUG_MODE) {
|
||||
console.error(`[DEBUG] ${message}`);
|
||||
}
|
||||
}
|
||||
export function logError(message) {
|
||||
console.error(`[SERVER] ${message}`);
|
||||
}
|
||||
//# sourceMappingURL=logger.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,MAAM,CAAC;AAEvD,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,CAAC,KAAK,CAAC,YAAY,OAAO,EAAE,CAAC,CAAC;AACvC,CAAC"}
|
||||
Reference in New Issue
Block a user