Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that pr

## Features

- Tools to run, stop, and capture screenshots from Godot projects
- Resources to monitor running projects, access output streams, and check exit status
- **Tools**: Run/stop Godot projects, search scenes by node type/name/properties, capture screenshots
- **Resources**: Browse scenes and .tres files, query by resource type, monitor running projects with output streams

## Prerequisites

Expand Down
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export default [
'@typescript-eslint': tseslint,
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
'no-unused-vars': 'off', // Using TypeScript version instead
'@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': 'error',
'no-var': 'error',
Expand Down
6 changes: 6 additions & 0 deletions godot-test-project/animation.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_resource type="Animation" format=3]

[resource]
length = 2.0
loop_mode = 1
step = 0.1
6 changes: 6 additions & 0 deletions godot-test-project/test_material.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_resource type="StandardMaterial3D" format=3]

[resource]
albedo_color = Color(0.8, 0.2, 0.2, 1)
metallic = 0.5
roughness = 0.3
4 changes: 4 additions & 0 deletions godot-test-project/theme.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[gd_resource type="Theme" format=3]

[resource]
default_font_size = 16
17 changes: 15 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fernforestgames/mcp-server-godot",
"version": "0.2.2",
"version": "0.3.0",
"description": "MCP server for Godot integration",
"author": "Justin Spahr-Summers <[email protected]> (https://fernforestgames.com)",
"license": "MIT",
Expand Down Expand Up @@ -32,6 +32,7 @@
"node": ">=22.0.0"
},
"dependencies": {
"@fernforestgames/godot-resource-parser": "^0.1.1",
"@modelcontextprotocol/sdk": "^1.0.0",
"node-screenshots": "^0.2.1"
},
Expand Down
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Configuration validation and setup

// Get Godot project path from command line arguments, default to cwd
const projectPath = process.argv[2] || process.cwd();

// Get Godot executable path from environment variable
const godotPath = process.env['GODOT_PATH'];
if (!godotPath) {
console.error("Error: GODOT_PATH environment variable must be set");
process.exit(1);
}

export { projectPath, godotPath };
156 changes: 156 additions & 0 deletions src/handlers/resources/godot-resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { isGodotResource } from "@fernforestgames/godot-resource-parser";
import { projectPath } from "../../config.js";
import { findGodotFiles, parseGodotFile } from "../../utils/files.js";

// Resource introspection resources
export const resourcesList = async (uri: URL) => {
const resources = findGodotFiles(projectPath, '.tres');

return {
contents: [{
uri: uri.href,
text: JSON.stringify(resources, null, 2)
}]
};
};

// List callback for resource data template
export const resourceDataList = async () => {
const resources = findGodotFiles(projectPath, '.tres');
const resourceList = resources.map(resourcePath => ({
uri: `godot://project/resources/${resourcePath}`,
name: `resource-${resourcePath}`,
mimeType: "application/json"
}));
return { resources: resourceList };
};

export const resourceData = async (uri: URL, params: any) => {
const resourcePath = params['resourcePath...'];
if (!resourcePath) {
throw new Error(`resourcePath parameter is required, got: ${JSON.stringify(params)}`);
}
const resourcePathStr = resourcePath;
const parsed = parseGodotFile(projectPath, resourcePathStr);

if (!isGodotResource(parsed)) {
throw new Error(`File ${resourcePathStr} is not a resource file`);
}

return {
contents: [{
uri: uri.href,
text: JSON.stringify(parsed, null, 2)
}]
};
};

// Resource type query resources
export const resourceTypesList = async (uri: URL) => {
const resources = findGodotFiles(projectPath, '.tres');
const typeSet = new Set<string>();

for (const resourcePath of resources) {
try {
const parsed = parseGodotFile(projectPath, resourcePath);
if (isGodotResource(parsed)) {
typeSet.add(parsed.header.resourceType);
}
} catch (error) {
// Skip files we can't parse
console.error(`Warning: Failed to parse resource ${resourcePath}:`, error);
}
}

const types = Array.from(typeSet).sort();

return {
contents: [{
uri: uri.href,
text: JSON.stringify(types, null, 2)
}]
};
};

// List callback for resources by type template
export const resourcesByTypeList = async () => {
const resources = findGodotFiles(projectPath, '.tres');
const typeSet = new Set<string>();

for (const resourcePath of resources) {
try {
const parsed = parseGodotFile(projectPath, resourcePath);
if (isGodotResource(parsed)) {
typeSet.add(parsed.header.resourceType);
}
} catch (error) {
// Skip files we can't parse
console.error(`Warning: Failed to parse resource ${resourcePath}:`, error);
}
}

const resourceList = Array.from(typeSet).map(type => ({
uri: `godot://project/resourceTypes/${type}`,
name: `type-${type}`,
mimeType: "application/json"
}));

return { resources: resourceList };
};

export const resourcesByType = async (uri: URL, { type }: any) => {
const typeStr = type as string;
const resources = findGodotFiles(projectPath, '.tres');
const matchingResources: string[] = [];

for (const resourcePath of resources) {
try {
const parsed = parseGodotFile(projectPath, resourcePath);
if (isGodotResource(parsed) && parsed.header.resourceType === typeStr) {
matchingResources.push(resourcePath);
}
} catch (error) {
// Skip files we can't parse
console.error(`Warning: Failed to parse resource ${resourcePath}:`, error);
}
}

return {
contents: [{
uri: uri.href,
text: JSON.stringify(matchingResources, null, 2)
}]
};
};

export const resourcePropertyByType = async (uri: URL, { type, property }: any) => {
const typeStr = type as string;
const propertyStr = property as string;
const resources = findGodotFiles(projectPath, '.tres');
const results: Array<{ path: string; value: unknown }> = [];

for (const resourcePath of resources) {
try {
const parsed = parseGodotFile(projectPath, resourcePath);
if (isGodotResource(parsed) && parsed.header.resourceType === typeStr) {
// Check if property exists in resource section
if (parsed.resource?.properties[propertyStr] !== undefined) {
results.push({
path: resourcePath,
value: parsed.resource.properties[propertyStr]
});
}
}
} catch (error) {
// Skip files we can't parse
console.error(`Warning: Failed to parse resource ${resourcePath}:`, error);
}
}

return {
contents: [{
uri: uri.href,
text: JSON.stringify(results, null, 2)
}]
};
};
104 changes: 104 additions & 0 deletions src/handlers/resources/runs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { type ProjectRun } from "../../types.js";

// Resources for project management
export const runsList = async (uri: URL, runningProjects: Map<string, ProjectRun>) => {
const runs = Array.from(runningProjects.values()).map(run => ({
id: run.id,
projectPath: run.projectPath,
status: run.status,
startTime: run.startTime.toISOString(),
exitCode: run.exitCode,
args: run.args
}));

return {
contents: [{
uri: uri.href,
text: JSON.stringify(runs, null, 2)
}]
};
};

// List callback for project stdout template
export const projectStdoutList = (runningProjects: Map<string, ProjectRun>) => async () => {
const resources = Array.from(runningProjects.keys()).map(runId => ({
uri: `godot://runs/${runId}/stdout`,
name: `stdout-${runId}`,
mimeType: "text/plain"
}));
return { resources };
};

export const projectStdout = async (uri: URL, { runId }: any, runningProjects: Map<string, ProjectRun>) => {
const projectRun = runningProjects.get(runId as string);

if (!projectRun) {
throw new Error(`No project found with run ID: ${runId}`);
}

return {
contents: [{
uri: uri.href,
text: projectRun.stdout.join('')
}]
};
};

// List callback for project stderr template
export const projectStderrList = (runningProjects: Map<string, ProjectRun>) => async () => {
const resources = Array.from(runningProjects.keys()).map(runId => ({
uri: `godot://runs/${runId}/stderr`,
name: `stderr-${runId}`,
mimeType: "text/plain"
}));
return { resources };
};

export const projectStderr = async (uri: URL, { runId }: any, runningProjects: Map<string, ProjectRun>) => {
const projectRun = runningProjects.get(runId as string);

if (!projectRun) {
throw new Error(`No project found with run ID: ${runId}`);
}

return {
contents: [{
uri: uri.href,
text: projectRun.stderr.join('')
}]
};
};

// List callback for project status template
export const projectStatusList = (runningProjects: Map<string, ProjectRun>) => async () => {
const resources = Array.from(runningProjects.keys()).map(runId => ({
uri: `godot://runs/${runId}/status`,
name: `status-${runId}`,
mimeType: "application/json"
}));
return { resources };
};

export const projectStatus = async (uri: URL, { runId }: any, runningProjects: Map<string, ProjectRun>) => {
const projectRun = runningProjects.get(runId as string);

if (!projectRun) {
throw new Error(`No project found with run ID: ${runId}`);
}

const status = {
id: projectRun.id,
status: projectRun.status,
projectPath: projectRun.projectPath,
startTime: projectRun.startTime.toISOString(),
exitCode: projectRun.exitCode,
args: projectRun.args
};

return {
contents: [{
uri: uri.href,
text: JSON.stringify(status, null, 2)
}]
};
};
Loading