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.
@@ -1,5 +1,5 @@
1
1
  export const MCP_SERVER_NAME = "ast";
2
- const EXPECTED_HERMES_TOOLS = [
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 isMissingClaudeMcp(output) {
16
- return (/\bno\s+mcp\s+server\b/i.test(output) &&
17
- /(?:\bfound\b|\bnamed?\b)/i.test(output) &&
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 inspectClaudeFields(stdout) {
21
- const fields = new Map();
22
- for (const line of stdout.split(/\r?\n/)) {
23
- const match = line.match(/^\s*(Status|Type|Command|Args):\s*(.*)$/i);
24
- if (match !== null)
25
- fields.set(match[1].toLowerCase(), match[2].trim());
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
- return fields;
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 isMissingClaudeMcp(output)
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
- const fields = inspectClaudeFields(result.stdout);
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") === context.nodeExecutable &&
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
- return runtime.run([
52
- "mcp",
53
- "add",
54
- "--scope",
55
- "user",
56
- "--transport",
57
- "stdio",
58
- MCP_SERVER_NAME,
59
- "--",
60
- context.nodeExecutable,
61
- context.serverEntryPath,
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
- const current = tested.exitCode === 0 && EXPECTED_HERMES_TOOLS.every((tool) => tested.stdout.includes(tool));
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
- return runtime.run([
87
- "mcp",
88
- "add",
89
- MCP_SERVER_NAME,
90
- "--command",
91
- context.nodeExecutable,
92
- "--args",
93
- context.serverEntryPath,
94
- ], "\n");
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
- registrationAccepted(result) {
97
- return result.exitCode === 0 && /saved\s+'ast'/i.test(result.stdout);
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: "claude",
103
- label: "Claude Code",
104
- command: "claude",
105
- skillTarget: "claude",
106
- mcp: claudeMcp,
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: "hermes",
110
- label: "Hermes",
111
- command: "hermes",
112
- skillTarget: "hermes",
113
- mcp: hermesMcp,
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 === undefined)
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
+ }