@arhend/godot-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/cache/docs-cache.d.ts +2 -0
- package/dist/cache/docs-cache.js +24 -0
- package/dist/godot/class-reference.d.ts +47 -0
- package/dist/godot/class-reference.js +140 -0
- package/dist/godot/scene-parser.d.ts +26 -0
- package/dist/godot/scene-parser.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +12 -0
- package/dist/tools/docs.d.ts +2 -0
- package/dist/tools/docs.js +177 -0
- package/dist/tools/project.d.ts +2 -0
- package/dist/tools/project.js +225 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# godot-mcp
|
|
2
|
+
|
|
3
|
+
> https://github.com/Arhend/godot-mcp
|
|
4
|
+
|
|
5
|
+
I wanted a simple MCP server for Godot 4 to help with development — mainly searching the class reference docs and reading project files without having to manually paste things into the chat.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
**Docs & API lookup** — pulls from the Godot engine's XML class reference on GitHub and caches it locally:
|
|
10
|
+
|
|
11
|
+
- `search_godot_class` — look up any class (Node, CharacterBody2D, etc.) and get its methods, properties, and signals
|
|
12
|
+
- `get_class_method` — full details on a specific method
|
|
13
|
+
- `get_class_property` — full details on a specific property
|
|
14
|
+
- `list_godot_classes` — browse all ~570 classes, filter by name prefix or parent class
|
|
15
|
+
- `search_godot_docs` — free-text search across all class descriptions, method names, and more
|
|
16
|
+
|
|
17
|
+
**Project file analysis** — reads your local Godot project files directly:
|
|
18
|
+
|
|
19
|
+
- `read_scene` — parses a `.tscn` file into a node tree
|
|
20
|
+
- `read_resource` — parses a `.tres` resource file
|
|
21
|
+
- `read_script` — reads a `.gd` script
|
|
22
|
+
- `get_project_info` — reads `project.godot` for name, main scene, autoloads
|
|
23
|
+
- `find_project_files` — find files by extension in your project directory
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/Arhend/godot-mcp
|
|
29
|
+
cd godot-mcp
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Add to Claude Code as an MCP server:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add godot -- npx github:Arhend/godot-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The first time you use a docs tool, it fetches and caches the Godot class reference from GitHub (~30 seconds). After that it loads from the local cache in `cache/` and is instant. The cache refreshes automatically after 7 days.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const CACHE_DIR = join(__dirname, "../../cache");
|
|
6
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
7
|
+
export async function readCache(key) {
|
|
8
|
+
const filePath = join(CACHE_DIR, `${key}.json`);
|
|
9
|
+
try {
|
|
10
|
+
const s = await stat(filePath);
|
|
11
|
+
if (Date.now() - s.mtimeMs > CACHE_TTL_MS)
|
|
12
|
+
return null;
|
|
13
|
+
const raw = await readFile(filePath, "utf-8");
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function writeCache(key, data) {
|
|
21
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
22
|
+
const filePath = join(CACHE_DIR, `${key}.json`);
|
|
23
|
+
await writeFile(filePath, JSON.stringify(data), "utf-8");
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface GodotParam {
|
|
2
|
+
index: number;
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
default?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GodotMethod {
|
|
8
|
+
name: string;
|
|
9
|
+
returnType: string;
|
|
10
|
+
qualifiers?: string;
|
|
11
|
+
params: GodotParam[];
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GodotProperty {
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
default?: string;
|
|
18
|
+
description: string;
|
|
19
|
+
setter?: string;
|
|
20
|
+
getter?: string;
|
|
21
|
+
enum?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface GodotSignal {
|
|
24
|
+
name: string;
|
|
25
|
+
params: GodotParam[];
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GodotConstant {
|
|
29
|
+
name: string;
|
|
30
|
+
value: string;
|
|
31
|
+
description: string;
|
|
32
|
+
enum?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface GodotClass {
|
|
35
|
+
name: string;
|
|
36
|
+
inherits?: string;
|
|
37
|
+
briefDescription: string;
|
|
38
|
+
description: string;
|
|
39
|
+
methods: GodotMethod[];
|
|
40
|
+
properties: GodotProperty[];
|
|
41
|
+
signals: GodotSignal[];
|
|
42
|
+
constants: GodotConstant[];
|
|
43
|
+
}
|
|
44
|
+
type ClassIndex = Record<string, GodotClass>;
|
|
45
|
+
export declare function getClassIndex(): Promise<ClassIndex>;
|
|
46
|
+
export declare function getClass(name: string): Promise<GodotClass | null>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { readCache, writeCache } from "../cache/docs-cache.js";
|
|
3
|
+
const GODOT_BRANCH = "master";
|
|
4
|
+
const CLASSES_API_URL = `https://api.github.com/repos/godotengine/godot/contents/doc/classes?ref=${GODOT_BRANCH}`;
|
|
5
|
+
const RAW_BASE = `https://raw.githubusercontent.com/godotengine/godot/${GODOT_BRANCH}/doc/classes/`;
|
|
6
|
+
const CACHE_KEY = "godot-classes";
|
|
7
|
+
const BATCH_SIZE = 20;
|
|
8
|
+
let _index = null;
|
|
9
|
+
function stripBBCode(text) {
|
|
10
|
+
if (!text)
|
|
11
|
+
return "";
|
|
12
|
+
return text
|
|
13
|
+
.replace(/\[(?:b|i|u|s|code|codeblock|kbd|center|url)[^\]]*\]/g, "")
|
|
14
|
+
.replace(/\[\/(?:b|i|u|s|code|codeblock|kbd|center|url)\]/g, "")
|
|
15
|
+
.replace(/\[(?:method|member|signal|constant|enum|param|constructor|operator|theme_item) ([^\]]+)\]/g, "$1")
|
|
16
|
+
.replace(/\[([A-Z][A-Za-z0-9_]*)\]/g, "$1")
|
|
17
|
+
.replace(/\$DOCS_URL/g, "https://docs.godotengine.org/en/stable")
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
function toArray(val) {
|
|
21
|
+
if (!val)
|
|
22
|
+
return [];
|
|
23
|
+
return Array.isArray(val) ? val : [val];
|
|
24
|
+
}
|
|
25
|
+
function parseParam(p) {
|
|
26
|
+
return {
|
|
27
|
+
index: Number(p["@_index"] ?? 0),
|
|
28
|
+
name: String(p["@_name"] ?? ""),
|
|
29
|
+
type: String(p["@_type"] ?? "Variant"),
|
|
30
|
+
...(p["@_default"] !== undefined ? { default: String(p["@_default"]) } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function parseMethod(m) {
|
|
34
|
+
const ret = m["return"];
|
|
35
|
+
return {
|
|
36
|
+
name: String(m["@_name"] ?? ""),
|
|
37
|
+
returnType: ret ? String(ret["@_type"] ?? "void") : "void",
|
|
38
|
+
...(m["@_qualifiers"] ? { qualifiers: String(m["@_qualifiers"]) } : {}),
|
|
39
|
+
params: toArray(m["param"]).map(parseParam),
|
|
40
|
+
description: stripBBCode(String(m["description"] ?? "")),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseProperty(p) {
|
|
44
|
+
return {
|
|
45
|
+
name: String(p["@_name"] ?? ""),
|
|
46
|
+
type: String(p["@_type"] ?? "Variant"),
|
|
47
|
+
...(p["@_default"] !== undefined ? { default: String(p["@_default"]) } : {}),
|
|
48
|
+
...(p["@_setter"] ? { setter: String(p["@_setter"]) } : {}),
|
|
49
|
+
...(p["@_getter"] ? { getter: String(p["@_getter"]) } : {}),
|
|
50
|
+
...(p["@_enum"] ? { enum: String(p["@_enum"]) } : {}),
|
|
51
|
+
description: stripBBCode(String(p["#text"] ?? "")),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseSignal(s) {
|
|
55
|
+
return {
|
|
56
|
+
name: String(s["@_name"] ?? ""),
|
|
57
|
+
params: toArray(s["param"]).map(parseParam),
|
|
58
|
+
description: stripBBCode(String(s["description"] ?? "")),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseConstant(c) {
|
|
62
|
+
return {
|
|
63
|
+
name: String(c["@_name"] ?? ""),
|
|
64
|
+
value: String(c["@_value"] ?? ""),
|
|
65
|
+
...(c["@_enum"] ? { enum: String(c["@_enum"]) } : {}),
|
|
66
|
+
description: stripBBCode(String(c["#text"] ?? "")),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parseClassXml(xml) {
|
|
70
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text" });
|
|
71
|
+
try {
|
|
72
|
+
const doc = parser.parse(xml);
|
|
73
|
+
const cls = doc["class"];
|
|
74
|
+
if (!cls)
|
|
75
|
+
return null;
|
|
76
|
+
const methods = cls["methods"];
|
|
77
|
+
const members = cls["members"];
|
|
78
|
+
const signals = cls["signals"];
|
|
79
|
+
const constants = cls["constants"];
|
|
80
|
+
return {
|
|
81
|
+
name: String(cls["@_name"] ?? ""),
|
|
82
|
+
...(cls["@_inherits"] ? { inherits: String(cls["@_inherits"]) } : {}),
|
|
83
|
+
briefDescription: stripBBCode(String(cls["brief_description"] ?? "")),
|
|
84
|
+
description: stripBBCode(String(cls["description"] ?? "")),
|
|
85
|
+
methods: toArray(methods?.["method"]).map(parseMethod),
|
|
86
|
+
properties: toArray(members?.["member"]).map(parseProperty),
|
|
87
|
+
signals: toArray(signals?.["signal"]).map(parseSignal),
|
|
88
|
+
constants: toArray(constants?.["constant"]).map(parseConstant),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function fetchAllClasses() {
|
|
96
|
+
process.stderr.write("Fetching Godot class list from GitHub...\n");
|
|
97
|
+
const listRes = await fetch(CLASSES_API_URL, {
|
|
98
|
+
headers: { "User-Agent": "godot-mcp", "Accept": "application/vnd.github+json" },
|
|
99
|
+
});
|
|
100
|
+
if (!listRes.ok)
|
|
101
|
+
throw new Error(`GitHub API error: ${listRes.status} ${listRes.statusText}`);
|
|
102
|
+
const files = (await listRes.json());
|
|
103
|
+
const xmlFiles = files.filter((f) => f.name.endsWith(".xml"));
|
|
104
|
+
process.stderr.write(`Downloading ${xmlFiles.length} class files...\n`);
|
|
105
|
+
const index = {};
|
|
106
|
+
for (let i = 0; i < xmlFiles.length; i += BATCH_SIZE) {
|
|
107
|
+
const batch = xmlFiles.slice(i, i + BATCH_SIZE);
|
|
108
|
+
const results = await Promise.all(batch.map(async (f) => {
|
|
109
|
+
const res = await fetch(RAW_BASE + encodeURIComponent(f.name));
|
|
110
|
+
if (!res.ok)
|
|
111
|
+
return null;
|
|
112
|
+
return { name: f.name.replace(".xml", ""), xml: await res.text() };
|
|
113
|
+
}));
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
if (!r)
|
|
116
|
+
continue;
|
|
117
|
+
const cls = parseClassXml(r.xml);
|
|
118
|
+
if (cls)
|
|
119
|
+
index[cls.name] = cls;
|
|
120
|
+
}
|
|
121
|
+
process.stderr.write(` ${Math.min(i + BATCH_SIZE, xmlFiles.length)}/${xmlFiles.length}\n`);
|
|
122
|
+
}
|
|
123
|
+
return index;
|
|
124
|
+
}
|
|
125
|
+
export async function getClassIndex() {
|
|
126
|
+
if (_index)
|
|
127
|
+
return _index;
|
|
128
|
+
const cached = await readCache(CACHE_KEY);
|
|
129
|
+
if (cached) {
|
|
130
|
+
_index = cached;
|
|
131
|
+
return _index;
|
|
132
|
+
}
|
|
133
|
+
_index = await fetchAllClasses();
|
|
134
|
+
await writeCache(CACHE_KEY, _index);
|
|
135
|
+
return _index;
|
|
136
|
+
}
|
|
137
|
+
export async function getClass(name) {
|
|
138
|
+
const index = await getClassIndex();
|
|
139
|
+
return index[name] ?? index[name.toLowerCase()] ?? null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SceneNode {
|
|
2
|
+
name: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
instance?: string;
|
|
5
|
+
parent?: string;
|
|
6
|
+
script?: string;
|
|
7
|
+
properties: Record<string, string>;
|
|
8
|
+
children: SceneNode[];
|
|
9
|
+
}
|
|
10
|
+
export interface ParsedScene {
|
|
11
|
+
format: number;
|
|
12
|
+
uid?: string;
|
|
13
|
+
externalResources: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
type: string;
|
|
16
|
+
path: string;
|
|
17
|
+
}>;
|
|
18
|
+
nodes: SceneNode[];
|
|
19
|
+
rootNode?: SceneNode;
|
|
20
|
+
}
|
|
21
|
+
export interface ParsedResource {
|
|
22
|
+
type?: string;
|
|
23
|
+
properties: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseScene(content: string): ParsedScene;
|
|
26
|
+
export declare function parseResource(content: string): ParsedResource;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Parse a single header line like: [node name="Foo" type="Bar" parent="."]
|
|
2
|
+
function parseHeader(line) {
|
|
3
|
+
const attrs = {};
|
|
4
|
+
// Extract section type
|
|
5
|
+
const typeMatch = line.match(/^\[(\w+)/);
|
|
6
|
+
if (typeMatch)
|
|
7
|
+
attrs["__section__"] = typeMatch[1];
|
|
8
|
+
// Extract key="value" or key=value pairs
|
|
9
|
+
const re = /(\w+)=(?:"([^"]*?)"|([^\s\]]+))/g;
|
|
10
|
+
let m;
|
|
11
|
+
while ((m = re.exec(line)) !== null) {
|
|
12
|
+
attrs[m[1]] = m[2] !== undefined ? m[2] : m[3];
|
|
13
|
+
}
|
|
14
|
+
return attrs;
|
|
15
|
+
}
|
|
16
|
+
// Parse key=value property lines below a section header
|
|
17
|
+
function parseProperty(line) {
|
|
18
|
+
const idx = line.indexOf(" = ");
|
|
19
|
+
if (idx === -1)
|
|
20
|
+
return null;
|
|
21
|
+
return [line.slice(0, idx).trim(), line.slice(idx + 3).trim()];
|
|
22
|
+
}
|
|
23
|
+
function buildTree(nodes) {
|
|
24
|
+
const byPath = {};
|
|
25
|
+
let root;
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
const path = node.parent === undefined
|
|
28
|
+
? "."
|
|
29
|
+
: node.parent === "."
|
|
30
|
+
? node.name
|
|
31
|
+
: `${node.parent}/${node.name}`;
|
|
32
|
+
byPath[path] = node;
|
|
33
|
+
if (node.parent === undefined)
|
|
34
|
+
root = node;
|
|
35
|
+
}
|
|
36
|
+
for (const node of nodes) {
|
|
37
|
+
if (node.parent === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const parentPath = node.parent === "." ? "." : node.parent;
|
|
40
|
+
const parent = byPath[parentPath];
|
|
41
|
+
if (parent)
|
|
42
|
+
parent.children.push(node);
|
|
43
|
+
}
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
export function parseScene(content) {
|
|
47
|
+
const lines = content.split("\n");
|
|
48
|
+
const result = {
|
|
49
|
+
format: 0,
|
|
50
|
+
externalResources: [],
|
|
51
|
+
nodes: [],
|
|
52
|
+
};
|
|
53
|
+
let currentSection = null;
|
|
54
|
+
let currentNodeIndex = -1;
|
|
55
|
+
let currentProps = {};
|
|
56
|
+
const flushSection = () => {
|
|
57
|
+
if (!currentSection)
|
|
58
|
+
return;
|
|
59
|
+
const section = currentSection["__section__"];
|
|
60
|
+
if (section === "ext_resource") {
|
|
61
|
+
result.externalResources.push({
|
|
62
|
+
id: currentSection["id"] ?? "",
|
|
63
|
+
type: currentSection["type"] ?? "",
|
|
64
|
+
path: currentSection["path"] ?? "",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (section === "node") {
|
|
68
|
+
const node = {
|
|
69
|
+
name: currentSection["name"] ?? "",
|
|
70
|
+
...(currentSection["type"] ? { type: currentSection["type"] } : {}),
|
|
71
|
+
...(currentSection["instance"] ? { instance: currentSection["instance"] } : {}),
|
|
72
|
+
...(currentSection["parent"] !== undefined ? { parent: currentSection["parent"] } : {}),
|
|
73
|
+
properties: { ...currentProps },
|
|
74
|
+
children: [],
|
|
75
|
+
};
|
|
76
|
+
// Extract script from properties if present
|
|
77
|
+
const scriptProp = currentProps["script"];
|
|
78
|
+
if (scriptProp) {
|
|
79
|
+
const extRes = result.externalResources.find((r) => scriptProp.includes(`"${r.id}"`) || scriptProp.includes(`(${r.id})`));
|
|
80
|
+
if (extRes)
|
|
81
|
+
node.script = extRes.path;
|
|
82
|
+
}
|
|
83
|
+
result.nodes.push(node);
|
|
84
|
+
currentNodeIndex = result.nodes.length - 1;
|
|
85
|
+
}
|
|
86
|
+
currentProps = {};
|
|
87
|
+
};
|
|
88
|
+
for (const rawLine of lines) {
|
|
89
|
+
const line = rawLine.trim();
|
|
90
|
+
if (!line || line.startsWith(";"))
|
|
91
|
+
continue;
|
|
92
|
+
if (line.startsWith("[")) {
|
|
93
|
+
flushSection();
|
|
94
|
+
currentSection = parseHeader(line);
|
|
95
|
+
if (currentSection["__section__"] === "gd_scene" || currentSection["__section__"] === "gd_resource") {
|
|
96
|
+
result.format = Number(currentSection["format"] ?? 0);
|
|
97
|
+
result.uid = currentSection["uid"];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (currentSection) {
|
|
101
|
+
const prop = parseProperty(line);
|
|
102
|
+
if (prop) {
|
|
103
|
+
currentProps[prop[0]] = prop[1];
|
|
104
|
+
}
|
|
105
|
+
else if (currentProps && Object.keys(currentProps).length > 0) {
|
|
106
|
+
// Multi-line value continuation — append to last key
|
|
107
|
+
const lastKey = Object.keys(currentProps).at(-1);
|
|
108
|
+
if (lastKey)
|
|
109
|
+
currentProps[lastKey] += "\n" + line;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
flushSection();
|
|
114
|
+
result.rootNode = buildTree(result.nodes);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
export function parseResource(content) {
|
|
118
|
+
const lines = content.split("\n");
|
|
119
|
+
const result = { properties: {} };
|
|
120
|
+
for (const rawLine of lines) {
|
|
121
|
+
const line = rawLine.trim();
|
|
122
|
+
if (!line || line.startsWith(";"))
|
|
123
|
+
continue;
|
|
124
|
+
if (line.startsWith("[gd_resource")) {
|
|
125
|
+
const header = parseHeader(line);
|
|
126
|
+
result.type = header["type"];
|
|
127
|
+
}
|
|
128
|
+
else if (!line.startsWith("[")) {
|
|
129
|
+
const prop = parseProperty(line);
|
|
130
|
+
if (prop)
|
|
131
|
+
result.properties[prop[0]] = prop[1];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerDocsTools } from "./tools/docs.js";
|
|
3
|
+
import { registerProjectTools } from "./tools/project.js";
|
|
4
|
+
export function createServer() {
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "godot-mcp",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
});
|
|
9
|
+
registerDocsTools(server);
|
|
10
|
+
registerProjectTools(server);
|
|
11
|
+
return server;
|
|
12
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClass, getClassIndex } from "../godot/class-reference.js";
|
|
3
|
+
function formatMethod(m) {
|
|
4
|
+
const params = m.params
|
|
5
|
+
.sort((a, b) => a.index - b.index)
|
|
6
|
+
.map((p) => `${p.type} ${p.name}${p.default !== undefined ? ` = ${p.default}` : ""}`)
|
|
7
|
+
.join(", ");
|
|
8
|
+
const qualifiers = m.qualifiers ? ` ${m.qualifiers}` : "";
|
|
9
|
+
return `${m.returnType} ${m.name}(${params})${qualifiers}`;
|
|
10
|
+
}
|
|
11
|
+
function formatProperty(p) {
|
|
12
|
+
const def = p.default !== undefined ? ` [default: ${p.default}]` : "";
|
|
13
|
+
const enumVal = p.enum ? ` [enum: ${p.enum}]` : "";
|
|
14
|
+
return `${p.type} ${p.name}${def}${enumVal}`;
|
|
15
|
+
}
|
|
16
|
+
function classSummary(cls) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push(`# ${cls.name}${cls.inherits ? ` (inherits ${cls.inherits})` : ""}`);
|
|
19
|
+
lines.push("");
|
|
20
|
+
lines.push(cls.briefDescription || cls.description.slice(0, 200));
|
|
21
|
+
if (cls.methods.length > 0) {
|
|
22
|
+
lines.push("\n## Methods");
|
|
23
|
+
for (const m of cls.methods)
|
|
24
|
+
lines.push(`- ${formatMethod(m)}`);
|
|
25
|
+
}
|
|
26
|
+
if (cls.properties.length > 0) {
|
|
27
|
+
lines.push("\n## Properties");
|
|
28
|
+
for (const p of cls.properties)
|
|
29
|
+
lines.push(`- ${formatProperty(p)}`);
|
|
30
|
+
}
|
|
31
|
+
if (cls.signals.length > 0) {
|
|
32
|
+
lines.push("\n## Signals");
|
|
33
|
+
for (const s of cls.signals) {
|
|
34
|
+
const params = s.params.map((p) => `${p.type} ${p.name}`).join(", ");
|
|
35
|
+
lines.push(`- ${s.name}(${params})`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
export function registerDocsTools(server) {
|
|
41
|
+
server.registerTool("search_godot_class", {
|
|
42
|
+
description: "Look up a Godot 4 class by name. Returns its description, inheritance chain, methods, properties, and signals.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
name: z.string().describe("Exact class name, e.g. Node, CharacterBody2D, Sprite2D"),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ name }) => {
|
|
47
|
+
const cls = await getClass(name);
|
|
48
|
+
if (!cls) {
|
|
49
|
+
return { content: [{ type: "text", text: `Class "${name}" not found. Use list_godot_classes to browse available classes.` }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
return { content: [{ type: "text", text: classSummary(cls) }] };
|
|
52
|
+
});
|
|
53
|
+
server.registerTool("get_class_method", {
|
|
54
|
+
description: "Get full details on a specific method of a Godot class: parameters, return type, and description.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
className: z.string().describe("Class name, e.g. Node"),
|
|
57
|
+
methodName: z.string().describe("Method name, e.g. add_child"),
|
|
58
|
+
},
|
|
59
|
+
}, async ({ className, methodName }) => {
|
|
60
|
+
const cls = await getClass(className);
|
|
61
|
+
if (!cls)
|
|
62
|
+
return { content: [{ type: "text", text: `Class "${className}" not found.` }], isError: true };
|
|
63
|
+
const method = cls.methods.find((m) => m.name === methodName);
|
|
64
|
+
if (!method) {
|
|
65
|
+
const similar = cls.methods.filter((m) => m.name.includes(methodName.toLowerCase())).map((m) => m.name);
|
|
66
|
+
const hint = similar.length > 0 ? `\nSimilar methods: ${similar.join(", ")}` : "";
|
|
67
|
+
return { content: [{ type: "text", text: `Method "${methodName}" not found on ${className}.${hint}` }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
const lines = [
|
|
70
|
+
`## ${className}.${method.name}`,
|
|
71
|
+
`\`\`\``,
|
|
72
|
+
formatMethod(method),
|
|
73
|
+
`\`\`\``,
|
|
74
|
+
"",
|
|
75
|
+
method.description || "(no description)",
|
|
76
|
+
];
|
|
77
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
78
|
+
});
|
|
79
|
+
server.registerTool("get_class_property", {
|
|
80
|
+
description: "Get full details on a specific property of a Godot class: type, default value, and description.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
className: z.string().describe("Class name, e.g. Node"),
|
|
83
|
+
propertyName: z.string().describe("Property name, e.g. process_mode"),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ className, propertyName }) => {
|
|
86
|
+
const cls = await getClass(className);
|
|
87
|
+
if (!cls)
|
|
88
|
+
return { content: [{ type: "text", text: `Class "${className}" not found.` }], isError: true };
|
|
89
|
+
const prop = cls.properties.find((p) => p.name === propertyName);
|
|
90
|
+
if (!prop) {
|
|
91
|
+
const similar = cls.properties.filter((p) => p.name.includes(propertyName.toLowerCase())).map((p) => p.name);
|
|
92
|
+
const hint = similar.length > 0 ? `\nSimilar properties: ${similar.join(", ")}` : "";
|
|
93
|
+
return { content: [{ type: "text", text: `Property "${propertyName}" not found on ${className}.${hint}` }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
const lines = [
|
|
96
|
+
`## ${className}.${prop.name}`,
|
|
97
|
+
`Type: ${prop.type}`,
|
|
98
|
+
];
|
|
99
|
+
if (prop.default !== undefined)
|
|
100
|
+
lines.push(`Default: ${prop.default}`);
|
|
101
|
+
if (prop.enum)
|
|
102
|
+
lines.push(`Enum: ${prop.enum}`);
|
|
103
|
+
if (prop.setter || prop.getter)
|
|
104
|
+
lines.push(`Setter: ${prop.setter ?? "n/a"} | Getter: ${prop.getter ?? "n/a"}`);
|
|
105
|
+
lines.push("", prop.description || "(no description)");
|
|
106
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
107
|
+
});
|
|
108
|
+
server.registerTool("list_godot_classes", {
|
|
109
|
+
description: "List all available Godot 4 classes, optionally filtered by name prefix or parent class.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
prefix: z.string().optional().describe("Filter classes whose name starts with this string (case-insensitive)"),
|
|
112
|
+
inherits: z.string().optional().describe("Filter classes that directly inherit from this class"),
|
|
113
|
+
},
|
|
114
|
+
}, async ({ prefix, inherits }) => {
|
|
115
|
+
const index = await getClassIndex();
|
|
116
|
+
let classes = Object.values(index);
|
|
117
|
+
if (prefix) {
|
|
118
|
+
const lc = prefix.toLowerCase();
|
|
119
|
+
classes = classes.filter((c) => c.name.toLowerCase().startsWith(lc));
|
|
120
|
+
}
|
|
121
|
+
if (inherits) {
|
|
122
|
+
classes = classes.filter((c) => c.inherits === inherits);
|
|
123
|
+
}
|
|
124
|
+
classes.sort((a, b) => a.name.localeCompare(b.name));
|
|
125
|
+
const lines = classes.map((c) => `${c.name}${c.inherits ? ` (extends ${c.inherits})` : ""}: ${c.briefDescription}`);
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: `Found ${classes.length} classes:\n\n${lines.join("\n")}` }],
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("search_godot_docs", {
|
|
131
|
+
description: "Full-text search across Godot 4 class names, method names, property names, and descriptions. Use this to find relevant classes/methods when you don't know the exact name.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
query: z.string().describe("Search terms, e.g. 'physics body collision' or 'play animation'"),
|
|
134
|
+
limit: z.number().optional().describe("Max results to return (default 20)"),
|
|
135
|
+
},
|
|
136
|
+
}, async ({ query, limit = 20 }) => {
|
|
137
|
+
const index = await getClassIndex();
|
|
138
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
139
|
+
const hits = [];
|
|
140
|
+
for (const cls of Object.values(index)) {
|
|
141
|
+
const clsText = `${cls.name} ${cls.briefDescription} ${cls.description}`.toLowerCase();
|
|
142
|
+
const clsScore = terms.filter((t) => clsText.includes(t)).length;
|
|
143
|
+
if (clsScore > 0) {
|
|
144
|
+
hits.push({ score: clsScore * 2, text: `[Class] ${cls.name}: ${cls.briefDescription}` });
|
|
145
|
+
}
|
|
146
|
+
for (const m of cls.methods) {
|
|
147
|
+
const mText = `${m.name} ${m.description}`.toLowerCase();
|
|
148
|
+
const mScore = terms.filter((t) => mText.includes(t)).length;
|
|
149
|
+
if (mScore > 0) {
|
|
150
|
+
hits.push({ score: mScore, text: `[Method] ${cls.name}.${m.name}() — ${m.description.slice(0, 120)}` });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const p of cls.properties) {
|
|
154
|
+
const pText = `${p.name} ${p.description}`.toLowerCase();
|
|
155
|
+
const pScore = terms.filter((t) => pText.includes(t)).length;
|
|
156
|
+
if (pScore > 0) {
|
|
157
|
+
hits.push({ score: pScore, text: `[Property] ${cls.name}.${p.name}: ${p.type} — ${p.description.slice(0, 100)}` });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const s of cls.signals) {
|
|
161
|
+
const sText = `${s.name} ${s.description}`.toLowerCase();
|
|
162
|
+
const sScore = terms.filter((t) => sText.includes(t)).length;
|
|
163
|
+
if (sScore > 0) {
|
|
164
|
+
hits.push({ score: sScore, text: `[Signal] ${cls.name}.${s.name} — ${s.description.slice(0, 100)}` });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
hits.sort((a, b) => b.score - a.score);
|
|
169
|
+
const top = hits.slice(0, limit);
|
|
170
|
+
if (top.length === 0) {
|
|
171
|
+
return { content: [{ type: "text", text: `No results found for "${query}".` }] };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: `Top ${top.length} results for "${query}":\n\n${top.map((h) => h.text).join("\n")}` }],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join, resolve, relative, extname } from "path";
|
|
4
|
+
import { readdir } from "fs/promises";
|
|
5
|
+
import { parseScene, parseResource } from "../godot/scene-parser.js";
|
|
6
|
+
function safeResolvePath(projectRoot, filePath) {
|
|
7
|
+
// If absolute path given, use it; otherwise join with project root
|
|
8
|
+
const resolved = filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath)
|
|
9
|
+
? resolve(filePath)
|
|
10
|
+
: resolve(join(projectRoot, filePath));
|
|
11
|
+
return resolved;
|
|
12
|
+
}
|
|
13
|
+
// Convert res:// paths to absolute using project root
|
|
14
|
+
function resolveResPath(resPath, projectRoot) {
|
|
15
|
+
if (resPath.startsWith("res://"))
|
|
16
|
+
return join(projectRoot, resPath.slice(6));
|
|
17
|
+
return resPath;
|
|
18
|
+
}
|
|
19
|
+
async function walkDir(dir, exts, maxDepth, depth = 0) {
|
|
20
|
+
if (depth > maxDepth)
|
|
21
|
+
return [];
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.name.startsWith("."))
|
|
26
|
+
continue;
|
|
27
|
+
const fullPath = join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
const sub = await walkDir(fullPath, exts, maxDepth, depth + 1);
|
|
30
|
+
results.push(...sub);
|
|
31
|
+
}
|
|
32
|
+
else if (exts.length === 0 || exts.includes(extname(entry.name))) {
|
|
33
|
+
results.push(fullPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
function renderNodeTree(node, indent = 0) {
|
|
39
|
+
if (!node)
|
|
40
|
+
return "";
|
|
41
|
+
const lines = [];
|
|
42
|
+
const pad = " ".repeat(indent);
|
|
43
|
+
const typeStr = node.type ? ` [${node.type}]` : node.instance ? ` [instance: ${node.instance}]` : "";
|
|
44
|
+
const scriptStr = node.script ? ` (script: ${node.script})` : "";
|
|
45
|
+
lines.push(`${pad}${node.name}${typeStr}${scriptStr}`);
|
|
46
|
+
for (const child of node.children) {
|
|
47
|
+
lines.push(renderNodeTree(child, indent + 1));
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
export function registerProjectTools(server) {
|
|
52
|
+
server.registerTool("read_scene", {
|
|
53
|
+
description: "Parse a Godot .tscn scene file and return its node tree with types, script assignments, and external resource references. Pass an absolute path or a res:// path with the project root.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
path: z.string().describe("Absolute file path or res:// path to the .tscn file"),
|
|
56
|
+
projectRoot: z.string().optional().describe("Absolute path to the Godot project root (required for res:// paths)"),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ path, projectRoot }) => {
|
|
59
|
+
const absPath = path.startsWith("res://") && projectRoot
|
|
60
|
+
? resolveResPath(path, projectRoot)
|
|
61
|
+
: safeResolvePath(projectRoot ?? ".", path);
|
|
62
|
+
let content;
|
|
63
|
+
try {
|
|
64
|
+
content = await readFile(absPath, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return { content: [{ type: "text", text: `Could not read file: ${absPath}\n${e.message}` }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
const scene = parseScene(content);
|
|
70
|
+
const lines = [
|
|
71
|
+
`# Scene: ${absPath}`,
|
|
72
|
+
`Format: ${scene.format}${scene.uid ? ` | UID: ${scene.uid}` : ""}`,
|
|
73
|
+
`Nodes: ${scene.nodes.length} | External resources: ${scene.externalResources.length}`,
|
|
74
|
+
];
|
|
75
|
+
if (scene.externalResources.length > 0) {
|
|
76
|
+
lines.push("\n## External Resources");
|
|
77
|
+
for (const r of scene.externalResources) {
|
|
78
|
+
lines.push(` [${r.id}] ${r.type}: ${r.path}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
lines.push("\n## Node Tree");
|
|
82
|
+
lines.push(renderNodeTree(scene.rootNode));
|
|
83
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
84
|
+
});
|
|
85
|
+
server.registerTool("read_resource", {
|
|
86
|
+
description: "Parse a Godot .tres resource file and return its type and key/value properties.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
path: z.string().describe("Absolute file path or res:// path to the .tres file"),
|
|
89
|
+
projectRoot: z.string().optional().describe("Absolute path to the Godot project root (required for res:// paths)"),
|
|
90
|
+
},
|
|
91
|
+
}, async ({ path, projectRoot }) => {
|
|
92
|
+
const absPath = path.startsWith("res://") && projectRoot
|
|
93
|
+
? resolveResPath(path, projectRoot)
|
|
94
|
+
: safeResolvePath(projectRoot ?? ".", path);
|
|
95
|
+
let content;
|
|
96
|
+
try {
|
|
97
|
+
content = await readFile(absPath, "utf-8");
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return { content: [{ type: "text", text: `Could not read file: ${absPath}\n${e.message}` }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
const res = parseResource(content);
|
|
103
|
+
const lines = [
|
|
104
|
+
`# Resource: ${absPath}`,
|
|
105
|
+
`Type: ${res.type ?? "unknown"}`,
|
|
106
|
+
"\n## Properties",
|
|
107
|
+
...Object.entries(res.properties).map(([k, v]) => ` ${k} = ${v}`),
|
|
108
|
+
];
|
|
109
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
110
|
+
});
|
|
111
|
+
server.registerTool("read_script", {
|
|
112
|
+
description: "Read a GDScript (.gd) file and return its source code.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
path: z.string().describe("Absolute file path or res:// path to the .gd script"),
|
|
115
|
+
projectRoot: z.string().optional().describe("Absolute path to the Godot project root (required for res:// paths)"),
|
|
116
|
+
},
|
|
117
|
+
}, async ({ path, projectRoot }) => {
|
|
118
|
+
const absPath = path.startsWith("res://") && projectRoot
|
|
119
|
+
? resolveResPath(path, projectRoot)
|
|
120
|
+
: safeResolvePath(projectRoot ?? ".", path);
|
|
121
|
+
let content;
|
|
122
|
+
try {
|
|
123
|
+
content = await readFile(absPath, "utf-8");
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return { content: [{ type: "text", text: `Could not read file: ${absPath}\n${e.message}` }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
return { content: [{ type: "text", text: `# ${absPath}\n\n\`\`\`gdscript\n${content}\n\`\`\`` }] };
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("get_project_info", {
|
|
131
|
+
description: "Read a Godot project.godot file and return the project display name, main scene, autoloads, and key settings.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
projectRoot: z.string().describe("Absolute path to the Godot project root directory"),
|
|
134
|
+
},
|
|
135
|
+
}, async ({ projectRoot }) => {
|
|
136
|
+
const configPath = join(resolve(projectRoot), "project.godot");
|
|
137
|
+
let content;
|
|
138
|
+
try {
|
|
139
|
+
content = await readFile(configPath, "utf-8");
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return { content: [{ type: "text", text: `Could not read project.godot at: ${configPath}\n${e.message}` }], isError: true };
|
|
143
|
+
}
|
|
144
|
+
// Parse INI-style project.godot
|
|
145
|
+
const sections = {};
|
|
146
|
+
let currentSection = "global";
|
|
147
|
+
sections[currentSection] = {};
|
|
148
|
+
for (const rawLine of content.split("\n")) {
|
|
149
|
+
const line = rawLine.trim();
|
|
150
|
+
if (!line || line.startsWith(";"))
|
|
151
|
+
continue;
|
|
152
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
153
|
+
currentSection = line.slice(1, -1);
|
|
154
|
+
sections[currentSection] = {};
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const eqIdx = line.indexOf("=");
|
|
158
|
+
if (eqIdx !== -1) {
|
|
159
|
+
const key = line.slice(0, eqIdx).trim();
|
|
160
|
+
const val = line.slice(eqIdx + 1).trim();
|
|
161
|
+
sections[currentSection][key] = val;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const appSection = sections["application"] ?? {};
|
|
166
|
+
const autoloads = sections["autoload"] ?? {};
|
|
167
|
+
const rendering = sections["rendering/renderer"] ?? {};
|
|
168
|
+
const lines = [
|
|
169
|
+
`# Project: ${appSection["config/name"]?.replace(/"/g, "") ?? "(unknown)"}`,
|
|
170
|
+
`Godot version required: ${(sections[""] ?? sections["global"])?.["config_version"] ?? "unknown"}`,
|
|
171
|
+
];
|
|
172
|
+
if (appSection["run/main_scene"])
|
|
173
|
+
lines.push(`Main scene: ${appSection["run/main_scene"].replace(/"/g, "")}`);
|
|
174
|
+
if (appSection["config/description"])
|
|
175
|
+
lines.push(`Description: ${appSection["config/description"].replace(/"/g, "")}`);
|
|
176
|
+
if (Object.keys(autoloads).length > 0) {
|
|
177
|
+
lines.push("\n## Autoloads");
|
|
178
|
+
for (const [name, path] of Object.entries(autoloads)) {
|
|
179
|
+
lines.push(` ${name}: ${path.replace(/"/g, "")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const features = sections["editor_plugins"] ?? {};
|
|
183
|
+
if (Object.keys(features).length > 0) {
|
|
184
|
+
lines.push("\n## Editor Plugins");
|
|
185
|
+
for (const [k, v] of Object.entries(features))
|
|
186
|
+
lines.push(` ${k}: ${v}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push("\n## All Sections");
|
|
189
|
+
for (const [sec, props] of Object.entries(sections)) {
|
|
190
|
+
if (Object.keys(props).length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
lines.push(` [${sec}] — ${Object.keys(props).length} key(s)`);
|
|
193
|
+
}
|
|
194
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
195
|
+
});
|
|
196
|
+
server.registerTool("find_project_files", {
|
|
197
|
+
description: "Find files in a Godot project directory matching given extensions. Useful for discovering scenes, scripts, and resources.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
projectRoot: z.string().describe("Absolute path to the Godot project root directory"),
|
|
200
|
+
extensions: z
|
|
201
|
+
.array(z.string())
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("File extensions to find, e.g. [\".gd\", \".tscn\"]. Omit to list all files."),
|
|
204
|
+
maxDepth: z.number().optional().describe("Max directory depth to search (default 6)"),
|
|
205
|
+
},
|
|
206
|
+
}, async ({ projectRoot, extensions, maxDepth = 6 }) => {
|
|
207
|
+
const root = resolve(projectRoot);
|
|
208
|
+
let exts = extensions ?? [];
|
|
209
|
+
if (exts.length > 0) {
|
|
210
|
+
exts = exts.map((e) => (e.startsWith(".") ? e : `.${e}`));
|
|
211
|
+
}
|
|
212
|
+
let files;
|
|
213
|
+
try {
|
|
214
|
+
files = await walkDir(root, exts, maxDepth);
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
return { content: [{ type: "text", text: `Could not read directory: ${root}\n${e.message}` }], isError: true };
|
|
218
|
+
}
|
|
219
|
+
const relFiles = files.map((f) => relative(root, f));
|
|
220
|
+
const extSuffix = exts.length > 0 ? ` (${exts.join(", ")})` : "";
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: `Found ${relFiles.length} files${extSuffix} in ${root}:\n\n${relFiles.join("\n")}` }],
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arhend/godot-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Godot 4 — docs lookup and project file analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"godot-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && node scripts/build.js",
|
|
15
|
+
"prepare": "npm run build",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"start": "node dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["mcp", "godot", "gamedev"],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
24
|
+
"fast-xml-parser": "^5.8.0",
|
|
25
|
+
"zod": "^4.4.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.9.1",
|
|
29
|
+
"tsx": "^4.22.4",
|
|
30
|
+
"typescript": "^6.0.3"
|
|
31
|
+
}
|
|
32
|
+
}
|