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:
2026-07-05 22:53:27 -04:00
parent 695e4db5cd
commit 91b878f347
155 changed files with 20335 additions and 972 deletions
+38
View File
@@ -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
View File
@@ -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"}
+124
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"autoload-ini.js","sourceRoot":"","sources":["../../src/utils/autoload-ini.ts"],"names":[],"mappings":"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"}
+46
View File
@@ -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
View File
@@ -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"}
+186
View File
@@ -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
File diff suppressed because one or more lines are too long
+37
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"bridge-protocol.js","sourceRoot":"","sources":["../../src/utils/bridge-protocol.ts"],"names":[],"mappings":"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"}
+248
View File
@@ -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
File diff suppressed because one or more lines are too long
+1119
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+36
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"}
+4
View File
@@ -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
+1
View File
@@ -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"}
+11
View File
@@ -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
+1
View File
@@ -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"}