ast-mcp-server 0.7.2 → 0.8.1
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/CHANGELOG.md +43 -1
- package/README.md +36 -14
- package/SECURITY.md +8 -8
- package/dist/cli.js +18 -14
- package/dist/services/agent-setup.js +248 -27
- package/dist/services/agent-targets.js +209 -63
- package/dist/services/checkbox-state.js +31 -0
- package/dist/services/managed-file.js +709 -0
- package/dist/services/managed-guidance.js +229 -0
- package/dist/services/opencode-config.js +135 -0
- package/dist/services/raw-tty.js +75 -0
- package/dist/services/setup-wizard.js +45 -93
- package/dist/services/skill-installer.js +145 -84
- package/docs/support.md +13 -13
- package/package.json +6 -3
- package/skills/structural-code-editing/SKILL.md +9 -3
- package/skills/structural-code-editing/guidance.md +3 -0
- package/skills/structural-code-editing/releases.json +50 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const MCP_SERVER_NAME = "ast";
|
|
2
|
-
const
|
|
2
|
+
const EXPECTED_TOOLS = [
|
|
3
3
|
"ast_list_files",
|
|
4
4
|
"ast_get_outline",
|
|
5
5
|
"ast_get_symbol_source",
|
|
@@ -12,111 +12,257 @@ const EXPECTED_HERMES_TOOLS = [
|
|
|
12
12
|
"ast_get_operation_preview",
|
|
13
13
|
"ast_apply_operation",
|
|
14
14
|
];
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/\bast\b/i.test(output));
|
|
15
|
+
function semver(value) {
|
|
16
|
+
const match = value.match(/(?:^|\s|v)(\d+)\.(\d+)\.(\d+)(?=\s|$|[-+()]|\.(?:\s|$))/);
|
|
17
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
19
18
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
function atLeast(value, minimum) {
|
|
20
|
+
return (value[0] > minimum[0] ||
|
|
21
|
+
(value[0] === minimum[0] &&
|
|
22
|
+
(value[1] > minimum[1] || (value[1] === minimum[1] && value[2] >= minimum[2]))));
|
|
23
|
+
}
|
|
24
|
+
export function classifyAgentVersion(id, output) {
|
|
25
|
+
const version = semver(output);
|
|
26
|
+
if (!version)
|
|
27
|
+
return {
|
|
28
|
+
status: "incompatible",
|
|
29
|
+
reason: "Version output does not match an admitted contract.",
|
|
30
|
+
};
|
|
31
|
+
if (id === "opencode" && !atLeast(version, [1, 18, 18])) {
|
|
32
|
+
return { status: "incompatible", reason: "OpenCode 1.18.18 or newer is required." };
|
|
33
|
+
}
|
|
34
|
+
return { status: "compatible", contract: `${id}-mcp-v1`, version: version.join(".") };
|
|
35
|
+
}
|
|
36
|
+
function expected(context, command, args) {
|
|
37
|
+
return (command === context.nodeExecutable &&
|
|
38
|
+
Array.isArray(args) &&
|
|
39
|
+
args.length === 1 &&
|
|
40
|
+
args[0] === context.serverEntryPath);
|
|
41
|
+
}
|
|
42
|
+
function parseJson(result) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(result.stdout);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function structuredInspection(result, context) {
|
|
51
|
+
if (result.exitCode !== 0) {
|
|
52
|
+
return /(?:not found|no mcp server)/i.test(`${result.stdout}\n${result.stderr}`)
|
|
53
|
+
? { status: "missing" }
|
|
54
|
+
: { status: "error", operation: "MCP inspection", result };
|
|
55
|
+
}
|
|
56
|
+
const value = parseJson(result);
|
|
57
|
+
if (!value || value.name !== MCP_SERVER_NAME)
|
|
58
|
+
return { status: "error", operation: "MCP inspection", result };
|
|
59
|
+
const transport = (value.transport ?? value);
|
|
60
|
+
return expected(context, transport.command, transport.args)
|
|
61
|
+
? { status: "current" }
|
|
62
|
+
: {
|
|
63
|
+
status: "conflict",
|
|
64
|
+
detail: "The existing 'ast' registration does not match this package.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function copilotStructuredInspection(result, context) {
|
|
68
|
+
if (result.exitCode !== 0) {
|
|
69
|
+
return /(?:not found|no mcp server)/i.test(`${result.stdout}\n${result.stderr}`)
|
|
70
|
+
? { status: "missing" }
|
|
71
|
+
: { status: "error", operation: "MCP inspection", result };
|
|
72
|
+
}
|
|
73
|
+
const value = parseJson(result);
|
|
74
|
+
const server = value?.[MCP_SERVER_NAME];
|
|
75
|
+
if (server === null || typeof server !== "object" || Array.isArray(server)) {
|
|
76
|
+
return { status: "error", operation: "MCP inspection", result };
|
|
26
77
|
}
|
|
27
|
-
|
|
78
|
+
const configuration = server;
|
|
79
|
+
return configuration.type === "local" &&
|
|
80
|
+
configuration.source === "user" &&
|
|
81
|
+
configuration.enabled === true &&
|
|
82
|
+
Array.isArray(configuration.tools) &&
|
|
83
|
+
configuration.tools.length === 1 &&
|
|
84
|
+
configuration.tools[0] === "*" &&
|
|
85
|
+
expected(context, configuration.command, configuration.args)
|
|
86
|
+
? { status: "current" }
|
|
87
|
+
: {
|
|
88
|
+
status: "conflict",
|
|
89
|
+
detail: "The existing user-scoped 'ast' registration does not match this package.",
|
|
90
|
+
};
|
|
28
91
|
}
|
|
29
92
|
const claudeMcp = {
|
|
30
93
|
async inspect(runtime, context) {
|
|
31
94
|
const result = await runtime.run(["mcp", "get", MCP_SERVER_NAME]);
|
|
32
95
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
33
|
-
if (result.exitCode !== 0)
|
|
34
|
-
return
|
|
96
|
+
if (result.exitCode !== 0)
|
|
97
|
+
return /no\s+mcp\s+server.*ast/i.test(output)
|
|
35
98
|
? { status: "missing" }
|
|
36
99
|
: { status: "error", operation: "MCP inspection", result };
|
|
100
|
+
const fields = new Map();
|
|
101
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
102
|
+
const match = line.match(/^\s*(Status|Type|Command|Args):\s*(.*)$/i);
|
|
103
|
+
if (match)
|
|
104
|
+
fields.set(match[1].toLowerCase(), match[2].trim());
|
|
37
105
|
}
|
|
38
|
-
|
|
39
|
-
const current = /connected/i.test(fields.get("status") ?? "") &&
|
|
106
|
+
return /connected/i.test(fields.get("status") ?? "") &&
|
|
40
107
|
fields.get("type")?.toLowerCase() === "stdio" &&
|
|
41
|
-
fields.get("command")
|
|
42
|
-
fields.get("args") === context.serverEntryPath;
|
|
43
|
-
return current
|
|
108
|
+
expected(context, fields.get("command"), [fields.get("args")])
|
|
44
109
|
? { status: "current" }
|
|
45
110
|
: {
|
|
46
111
|
status: "conflict",
|
|
47
112
|
detail: "The existing user-scoped 'ast' registration does not match this package.",
|
|
48
113
|
};
|
|
49
114
|
},
|
|
50
|
-
register(runtime, context)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
registrationAccepted(result) {
|
|
65
|
-
return result.exitCode === 0;
|
|
66
|
-
},
|
|
115
|
+
register: (runtime, context) => runtime.run([
|
|
116
|
+
"mcp",
|
|
117
|
+
"add",
|
|
118
|
+
"--scope",
|
|
119
|
+
"user",
|
|
120
|
+
"--transport",
|
|
121
|
+
"stdio",
|
|
122
|
+
MCP_SERVER_NAME,
|
|
123
|
+
"--",
|
|
124
|
+
context.nodeExecutable,
|
|
125
|
+
context.serverEntryPath,
|
|
126
|
+
]),
|
|
127
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
67
128
|
};
|
|
68
129
|
const hermesMcp = {
|
|
69
130
|
async inspect(runtime) {
|
|
70
131
|
const listed = await runtime.run(["mcp", "list"]);
|
|
71
|
-
if (listed.exitCode !== 0)
|
|
132
|
+
if (listed.exitCode !== 0)
|
|
72
133
|
return { status: "error", operation: "MCP list", result: listed };
|
|
73
|
-
}
|
|
74
134
|
if (!/^\s*ast(?:\s|$)/im.test(listed.stdout))
|
|
75
135
|
return { status: "missing" };
|
|
76
136
|
const tested = await runtime.run(["mcp", "test", MCP_SERVER_NAME]);
|
|
77
|
-
|
|
78
|
-
return current
|
|
137
|
+
return tested.exitCode === 0 && EXPECTED_TOOLS.every((tool) => tested.stdout.includes(tool))
|
|
79
138
|
? { status: "current" }
|
|
80
139
|
: {
|
|
81
140
|
status: "conflict",
|
|
82
141
|
detail: "The existing 'ast' registration does not expose the expected structural tools.",
|
|
83
142
|
};
|
|
84
143
|
},
|
|
85
|
-
register(runtime, context)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
register: (runtime, context) => runtime.run([
|
|
145
|
+
"mcp",
|
|
146
|
+
"add",
|
|
147
|
+
MCP_SERVER_NAME,
|
|
148
|
+
"--command",
|
|
149
|
+
context.nodeExecutable,
|
|
150
|
+
"--args",
|
|
151
|
+
context.serverEntryPath,
|
|
152
|
+
], "\n"),
|
|
153
|
+
registrationAccepted: (result) => result.exitCode === 0 && /saved\s+'ast'/i.test(result.stdout),
|
|
154
|
+
};
|
|
155
|
+
const codexMcp = {
|
|
156
|
+
async inspect(runtime, context) {
|
|
157
|
+
return structuredInspection(await runtime.run(["mcp", "get", MCP_SERVER_NAME, "--json"]), context);
|
|
158
|
+
},
|
|
159
|
+
register: (runtime, context) => runtime.run([
|
|
160
|
+
"mcp",
|
|
161
|
+
"add",
|
|
162
|
+
MCP_SERVER_NAME,
|
|
163
|
+
"--",
|
|
164
|
+
context.nodeExecutable,
|
|
165
|
+
context.serverEntryPath,
|
|
166
|
+
]),
|
|
167
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
168
|
+
};
|
|
169
|
+
const copilotMcp = {
|
|
170
|
+
async inspect(runtime, context) {
|
|
171
|
+
return copilotStructuredInspection(await runtime.run(["mcp", "get", MCP_SERVER_NAME, "--json"]), context);
|
|
172
|
+
},
|
|
173
|
+
register: (runtime, context) => runtime.run([
|
|
174
|
+
"mcp",
|
|
175
|
+
"add",
|
|
176
|
+
MCP_SERVER_NAME,
|
|
177
|
+
"--",
|
|
178
|
+
context.nodeExecutable,
|
|
179
|
+
context.serverEntryPath,
|
|
180
|
+
]),
|
|
181
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
182
|
+
};
|
|
183
|
+
const geminiMcp = {
|
|
184
|
+
async inspect(runtime, context) {
|
|
185
|
+
const result = await runtime.run(["mcp", "list"]);
|
|
186
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
187
|
+
if (/MCP servers are disabled in untrusted folders\. Trust this folder to continue\./i.test(output)) {
|
|
188
|
+
return {
|
|
189
|
+
status: "blocked_untrusted_folder",
|
|
190
|
+
detail: "Trust the working folder in Gemini CLI and retry.",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (result.exitCode !== 0)
|
|
194
|
+
return { status: "error", operation: "MCP list", result };
|
|
195
|
+
if (/No MCP servers configured\./i.test(result.stdout))
|
|
196
|
+
return { status: "missing" };
|
|
197
|
+
const pattern = /ast[^\n]*Connected[^\n]*command:\s*([^,\n]+),\s*args:\s*\[([^\]]*)\]/i;
|
|
198
|
+
const match = result.stdout.match(pattern);
|
|
199
|
+
if (!match)
|
|
200
|
+
return { status: "error", operation: "MCP inspection", result };
|
|
201
|
+
return expected(context, match[1]?.trim(), match[2]?.split(",").map((item) => item.trim()))
|
|
202
|
+
? { status: "current" }
|
|
203
|
+
: {
|
|
204
|
+
status: "conflict",
|
|
205
|
+
detail: "The existing Gemini 'ast' registration does not match this package.",
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
register: (runtime, context) => runtime.run([
|
|
209
|
+
"mcp",
|
|
210
|
+
"add",
|
|
211
|
+
MCP_SERVER_NAME,
|
|
212
|
+
context.nodeExecutable,
|
|
213
|
+
context.serverEntryPath,
|
|
214
|
+
"--scope",
|
|
215
|
+
"user",
|
|
216
|
+
]),
|
|
217
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
218
|
+
};
|
|
219
|
+
const opencodeMcp = {
|
|
220
|
+
async inspect() {
|
|
221
|
+
return {
|
|
222
|
+
status: "error",
|
|
223
|
+
operation: "OpenCode configuration inspection",
|
|
224
|
+
result: {
|
|
225
|
+
exitCode: 1,
|
|
226
|
+
stdout: "",
|
|
227
|
+
stderr: "OpenCode uses the routed configuration adapter.",
|
|
228
|
+
},
|
|
229
|
+
};
|
|
95
230
|
},
|
|
96
|
-
|
|
97
|
-
return
|
|
231
|
+
async register() {
|
|
232
|
+
return { exitCode: 1, stdout: "", stderr: "OpenCode CLI registration is disabled." };
|
|
98
233
|
},
|
|
234
|
+
registrationAccepted: () => false,
|
|
99
235
|
};
|
|
100
236
|
export const AGENT_TARGETS = [
|
|
237
|
+
{ id: "claude", label: "Claude Code", command: "claude", skillTarget: "claude", mcp: claudeMcp },
|
|
238
|
+
{ id: "hermes", label: "Hermes", command: "hermes", skillTarget: "hermes", mcp: hermesMcp },
|
|
101
239
|
{
|
|
102
|
-
id: "
|
|
103
|
-
label: "
|
|
104
|
-
command: "
|
|
105
|
-
skillTarget: "
|
|
106
|
-
mcp:
|
|
240
|
+
id: "opencode",
|
|
241
|
+
label: "OpenCode",
|
|
242
|
+
command: "opencode",
|
|
243
|
+
skillTarget: "agents",
|
|
244
|
+
mcp: opencodeMcp,
|
|
107
245
|
},
|
|
246
|
+
{ id: "codex", label: "Codex CLI", command: "codex", skillTarget: "agents", mcp: codexMcp },
|
|
247
|
+
{ id: "gemini", label: "Gemini CLI", command: "gemini", skillTarget: "agents", mcp: geminiMcp },
|
|
108
248
|
{
|
|
109
|
-
id: "
|
|
110
|
-
label: "
|
|
111
|
-
command: "
|
|
112
|
-
skillTarget: "
|
|
113
|
-
mcp:
|
|
249
|
+
id: "copilot",
|
|
250
|
+
label: "GitHub Copilot CLI",
|
|
251
|
+
command: "copilot",
|
|
252
|
+
skillTarget: "agents",
|
|
253
|
+
mcp: copilotMcp,
|
|
114
254
|
},
|
|
115
255
|
];
|
|
116
256
|
export const AGENT_IDS = AGENT_TARGETS.map((target) => target.id);
|
|
117
257
|
export function getAgentTarget(id) {
|
|
118
258
|
const target = AGENT_TARGETS.find((candidate) => candidate.id === id);
|
|
119
|
-
if (target
|
|
259
|
+
if (!target)
|
|
120
260
|
throw new Error(`Unsupported agent target: ${id}`);
|
|
121
261
|
return target;
|
|
122
262
|
}
|
|
263
|
+
export async function inspectAgentFixture(id, results, context) {
|
|
264
|
+
let index = 0;
|
|
265
|
+
return getAgentTarget(id).mcp.inspect({
|
|
266
|
+
run: async () => results[index++] ?? { exitCode: 1, stdout: "", stderr: "Missing fixture result." },
|
|
267
|
+
}, context);
|
|
268
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createCheckboxState(choices) {
|
|
2
|
+
return {
|
|
3
|
+
choices: choices.map((choice) => ({ ...choice })),
|
|
4
|
+
focus: Math.max(0, choices.findIndex((choice) => choice.enabled)),
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function reduceCheckboxState(state, action) {
|
|
8
|
+
if (action === "cancel")
|
|
9
|
+
return { ...state, cancelled: true };
|
|
10
|
+
if (action === "up" || action === "down") {
|
|
11
|
+
const delta = action === "up" ? -1 : 1;
|
|
12
|
+
return {
|
|
13
|
+
...state,
|
|
14
|
+
focus: Math.max(0, Math.min(state.choices.length - 1, state.focus + delta)),
|
|
15
|
+
validation: undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (action === "toggle") {
|
|
19
|
+
return {
|
|
20
|
+
...state,
|
|
21
|
+
validation: undefined,
|
|
22
|
+
choices: state.choices.map((choice, index) => index === state.focus && choice.enabled ? { ...choice, checked: !choice.checked } : choice),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const selected = state.choices
|
|
26
|
+
.filter((choice) => choice.enabled && choice.checked)
|
|
27
|
+
.map((choice) => choice.id);
|
|
28
|
+
return selected.length === 0
|
|
29
|
+
? { ...state, validation: "Select at least one agent." }
|
|
30
|
+
: { ...state, submitted: selected };
|
|
31
|
+
}
|