@foundation0/api 1.1.1 → 1.1.3
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/agents.ts +7 -6
- package/git.ts +2 -2
- package/mcp/AGENTS.md +130 -0
- package/mcp/cli.mjs +1 -1
- package/mcp/cli.ts +3 -3
- package/mcp/client.test.ts +13 -0
- package/mcp/client.ts +12 -4
- package/mcp/server.test.ts +464 -117
- package/mcp/server.ts +2497 -484
- package/package.json +2 -2
- package/projects.ts +1791 -99
package/mcp/server.ts
CHANGED
|
@@ -1,522 +1,2535 @@
|
|
|
1
|
-
import { Server } from
|
|
2
|
-
import { StdioServerTransport } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import * as agentsApi from "../agents.ts";
|
|
8
|
+
import * as netApi from "../net.ts";
|
|
9
|
+
import * as projectsApi from "../projects.ts";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
6
12
|
|
|
7
|
-
type ApiMethod = (...args: unknown[]) => unknown
|
|
13
|
+
type ApiMethod = (...args: unknown[]) => unknown;
|
|
8
14
|
type ToolInvocationPayload = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
15
|
+
args?: unknown[];
|
|
16
|
+
options?: Record<string, unknown>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
13
19
|
type BatchToolCall = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
20
|
+
tool: string;
|
|
21
|
+
args?: unknown[];
|
|
22
|
+
options?: Record<string, unknown>;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
19
25
|
type BatchToolCallPayload = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
calls: BatchToolCall[];
|
|
27
|
+
continueOnError: boolean;
|
|
28
|
+
maxConcurrency: number;
|
|
29
|
+
};
|
|
23
30
|
type BatchResult = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
31
|
+
index: number;
|
|
32
|
+
tool: string;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
data: unknown;
|
|
35
|
+
};
|
|
29
36
|
type ToolDefinition = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
37
|
+
name: string;
|
|
38
|
+
method: ApiMethod;
|
|
39
|
+
path: string[];
|
|
40
|
+
};
|
|
34
41
|
|
|
35
|
-
type ToolNamespace = Record<string, unknown
|
|
42
|
+
type ToolNamespace = Record<string, unknown>;
|
|
36
43
|
|
|
37
44
|
type ApiEndpoint = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
agents: ToolNamespace;
|
|
46
|
+
net: ToolNamespace;
|
|
47
|
+
projects: ToolNamespace;
|
|
48
|
+
mcp: ToolNamespace;
|
|
49
|
+
};
|
|
41
50
|
|
|
42
51
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
52
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
|
|
54
|
+
const parseBooleanish = (value: unknown): boolean | null => {
|
|
55
|
+
if (value === true || value === false) return value;
|
|
56
|
+
if (value === 1 || value === 0) return Boolean(value);
|
|
57
|
+
if (typeof value !== "string") return null;
|
|
58
|
+
const normalized = value.trim().toLowerCase();
|
|
59
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
60
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const parsePositiveInteger = (value: unknown): number | null => {
|
|
65
|
+
const numeric =
|
|
66
|
+
typeof value === "string" && value.trim() !== ""
|
|
67
|
+
? Number(value)
|
|
68
|
+
: typeof value === "number"
|
|
69
|
+
? value
|
|
70
|
+
: NaN;
|
|
71
|
+
|
|
72
|
+
if (!Number.isInteger(numeric) || numeric <= 0) return null;
|
|
73
|
+
return numeric;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const safeJsonStringify = (value: unknown): string => {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.stringify(
|
|
79
|
+
value,
|
|
80
|
+
(_key, v) => (typeof v === "bigint" ? v.toString() : v),
|
|
81
|
+
2,
|
|
82
|
+
);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
return JSON.stringify(
|
|
86
|
+
{
|
|
87
|
+
ok: false,
|
|
88
|
+
error: { message, note: "Failed to JSON stringify tool result." },
|
|
89
|
+
},
|
|
90
|
+
null,
|
|
91
|
+
2,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type ToolEnvelope =
|
|
97
|
+
| { ok: true; result: unknown }
|
|
98
|
+
| { ok: false; error: { message: string; details?: unknown } };
|
|
99
|
+
|
|
100
|
+
const toolOk = (result: unknown) => ({
|
|
101
|
+
isError: false,
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: safeJsonStringify({ ok: true, result } satisfies ToolEnvelope),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const toolErr = (message: string, details?: unknown) => ({
|
|
111
|
+
isError: true,
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: safeJsonStringify({
|
|
116
|
+
ok: false,
|
|
117
|
+
error: { message, details },
|
|
118
|
+
} satisfies ToolEnvelope),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const isDir = (candidate: string): boolean => {
|
|
124
|
+
try {
|
|
125
|
+
return fs.statSync(candidate).isDirectory();
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const looksLikeRepoRoot = (candidate: string): boolean =>
|
|
132
|
+
isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
|
|
133
|
+
|
|
134
|
+
const normalizeProcessRoot = (raw: string): string => {
|
|
135
|
+
const resolved = path.resolve(raw);
|
|
136
|
+
if (looksLikeRepoRoot(resolved)) return resolved;
|
|
137
|
+
|
|
138
|
+
// Common mistake: passing a project root like ".../projects/adl" as processRoot.
|
|
139
|
+
// Try to find the containing repo root by walking up a few levels.
|
|
140
|
+
let current = resolved;
|
|
141
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
142
|
+
const parent = path.dirname(current);
|
|
143
|
+
if (parent === current) break;
|
|
144
|
+
if (looksLikeRepoRoot(parent)) return parent;
|
|
145
|
+
current = parent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parts = resolved.split(path.sep).filter((part) => part.length > 0);
|
|
149
|
+
const projectsIndex = parts.lastIndexOf("projects");
|
|
150
|
+
if (projectsIndex >= 0) {
|
|
151
|
+
const candidate = parts.slice(0, projectsIndex).join(path.sep);
|
|
152
|
+
if (candidate && looksLikeRepoRoot(candidate)) return candidate;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return resolved;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const normalizeProcessRootOption = (
|
|
159
|
+
options: Record<string, unknown>,
|
|
160
|
+
): Record<string, unknown> => {
|
|
161
|
+
const raw = options.processRoot;
|
|
162
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return options;
|
|
163
|
+
const normalized = normalizeProcessRoot(raw.trim());
|
|
164
|
+
if (normalized === raw) return options;
|
|
165
|
+
return { ...options, processRoot: normalized };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
type NormalizedToolPayload = {
|
|
169
|
+
args: unknown[];
|
|
170
|
+
options: Record<string, unknown>;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const parseStringish = (value: unknown): string | null => {
|
|
174
|
+
if (typeof value !== "string") return null;
|
|
175
|
+
const trimmed = value.trim();
|
|
176
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const parseStringArrayish = (value: unknown): string[] => {
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
return value
|
|
182
|
+
.map((entry) => String(entry))
|
|
183
|
+
.map((entry) => entry.trim())
|
|
184
|
+
.filter((entry) => entry.length > 0);
|
|
185
|
+
}
|
|
186
|
+
const asString = parseStringish(value);
|
|
187
|
+
return asString ? [asString] : [];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const popOption = (options: Record<string, unknown>, key: string): unknown => {
|
|
191
|
+
const value = options[key];
|
|
192
|
+
delete options[key];
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const popStringOption = (
|
|
197
|
+
options: Record<string, unknown>,
|
|
198
|
+
...keys: string[]
|
|
199
|
+
): string | null => {
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
const value = options[key];
|
|
202
|
+
const parsed = parseStringish(value);
|
|
203
|
+
if (parsed) {
|
|
204
|
+
delete options[key];
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const popStringArrayOption = (
|
|
212
|
+
options: Record<string, unknown>,
|
|
213
|
+
...keys: string[]
|
|
214
|
+
): string[] => {
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
const value = options[key];
|
|
217
|
+
const parsed = parseStringArrayish(value);
|
|
218
|
+
if (parsed.length > 0) {
|
|
219
|
+
delete options[key];
|
|
220
|
+
return parsed;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return [];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const popBooleanOption = (
|
|
227
|
+
options: Record<string, unknown>,
|
|
228
|
+
...keys: string[]
|
|
229
|
+
): boolean | null => {
|
|
230
|
+
for (const key of keys) {
|
|
231
|
+
const value = options[key];
|
|
232
|
+
const parsed = parseBooleanish(value);
|
|
233
|
+
if (parsed !== null) {
|
|
234
|
+
delete options[key];
|
|
235
|
+
return parsed;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const popIntegerOption = (
|
|
242
|
+
options: Record<string, unknown>,
|
|
243
|
+
...keys: string[]
|
|
244
|
+
): number | null => {
|
|
245
|
+
for (const key of keys) {
|
|
246
|
+
const value = options[key];
|
|
247
|
+
const parsed = parsePositiveInteger(value);
|
|
248
|
+
if (parsed !== null) {
|
|
249
|
+
delete options[key];
|
|
250
|
+
return parsed;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const normalizePayload = (payload: unknown): NormalizedToolPayload => {
|
|
257
|
+
if (!isRecord(payload)) {
|
|
258
|
+
return {
|
|
259
|
+
args: [],
|
|
260
|
+
options: {},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const explicitArgs = Array.isArray(payload.args) ? payload.args : undefined;
|
|
265
|
+
const explicitOptions = isRecord(payload.options)
|
|
266
|
+
? payload.options
|
|
267
|
+
: undefined;
|
|
268
|
+
|
|
269
|
+
const args = explicitArgs ? [...explicitArgs] : [];
|
|
270
|
+
const options: Record<string, unknown> = explicitOptions
|
|
271
|
+
? { ...explicitOptions }
|
|
272
|
+
: {};
|
|
273
|
+
|
|
274
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
275
|
+
if (key === "args" || key === "options") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (value !== undefined) {
|
|
280
|
+
options[key] = value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
args,
|
|
286
|
+
options: normalizeProcessRootOption(options),
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const coercePayloadForTool = (
|
|
291
|
+
toolName: string,
|
|
292
|
+
input: NormalizedToolPayload,
|
|
293
|
+
): NormalizedToolPayload => {
|
|
294
|
+
const args = [...input.args];
|
|
295
|
+
const options: Record<string, unknown> = { ...input.options };
|
|
296
|
+
|
|
297
|
+
const ensureArg0 = (value: unknown) => {
|
|
298
|
+
if (args.length > 0) return;
|
|
299
|
+
if (value !== undefined) {
|
|
300
|
+
args.push(value);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const ensureArgs = (...values: Array<unknown>) => {
|
|
305
|
+
while (args.length < values.length) {
|
|
306
|
+
const next = values[args.length];
|
|
307
|
+
if (next === undefined || next === null) break;
|
|
308
|
+
args.push(next);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const coerceProjectName = (): void => {
|
|
313
|
+
if (
|
|
314
|
+
args.length > 0 &&
|
|
315
|
+
typeof args[0] === "string" &&
|
|
316
|
+
args[0].trim().length > 0
|
|
317
|
+
)
|
|
318
|
+
return;
|
|
319
|
+
const name = popStringOption(options, "projectName", "project");
|
|
320
|
+
if (name) {
|
|
321
|
+
if (args.length === 0) args.push(name);
|
|
322
|
+
else args[0] = name;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const coerceProjectSearch = (): void => {
|
|
327
|
+
coerceProjectName();
|
|
328
|
+
|
|
329
|
+
// If caller already provided [projectName, pattern, ...], keep it.
|
|
330
|
+
if (args.length >= 2) return;
|
|
331
|
+
|
|
332
|
+
const pattern = popStringOption(options, "pattern", "query", "q") ?? null;
|
|
333
|
+
const paths = popStringArrayOption(options, "paths");
|
|
334
|
+
const globs = popStringArrayOption(options, "globs");
|
|
335
|
+
const ref = popStringOption(options, "ref");
|
|
336
|
+
const source = popStringOption(options, "source");
|
|
337
|
+
|
|
338
|
+
if (source) options.source = source;
|
|
339
|
+
if (ref) options.ref = ref;
|
|
340
|
+
|
|
341
|
+
const refresh = popBooleanOption(options, "refresh");
|
|
342
|
+
if (refresh !== null) options.refresh = refresh;
|
|
343
|
+
const cacheDir = popStringOption(options, "cacheDir");
|
|
344
|
+
if (cacheDir) options.cacheDir = cacheDir;
|
|
345
|
+
|
|
346
|
+
const owner = popStringOption(options, "owner");
|
|
347
|
+
if (owner) options.owner = owner;
|
|
348
|
+
const repo = popStringOption(options, "repo");
|
|
349
|
+
if (repo) options.repo = repo;
|
|
350
|
+
|
|
351
|
+
if (!pattern) return;
|
|
352
|
+
|
|
353
|
+
const rgArgs: string[] = [];
|
|
354
|
+
const addFlag = (key: string, flag: string) => {
|
|
355
|
+
const raw = popBooleanOption(options, key);
|
|
356
|
+
if (raw === true) rgArgs.push(flag);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
addFlag("ignoreCase", "--ignore-case");
|
|
360
|
+
addFlag("caseSensitive", "--case-sensitive");
|
|
361
|
+
addFlag("smartCase", "--smart-case");
|
|
362
|
+
addFlag("fixedStrings", "--fixed-strings");
|
|
363
|
+
addFlag("wordRegexp", "--word-regexp");
|
|
364
|
+
addFlag("includeHidden", "--hidden");
|
|
365
|
+
addFlag("filesWithMatches", "--files-with-matches");
|
|
366
|
+
addFlag("filesWithoutMatch", "--files-without-match");
|
|
367
|
+
addFlag("countOnly", "--count");
|
|
368
|
+
addFlag("onlyMatching", "--only-matching");
|
|
369
|
+
|
|
370
|
+
const maxCount = popIntegerOption(options, "maxCount");
|
|
371
|
+
if (maxCount !== null) {
|
|
372
|
+
rgArgs.push("--max-count", String(maxCount));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const glob of globs) {
|
|
376
|
+
rgArgs.push("--glob", glob);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
rgArgs.push(pattern, ...(paths.length > 0 ? paths : ["."]));
|
|
380
|
+
|
|
381
|
+
if (args.length === 0) {
|
|
382
|
+
// Can't build without projectName; leave for underlying error.
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (args.length === 1) {
|
|
387
|
+
args.push(...rgArgs);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
switch (toolName) {
|
|
392
|
+
case "projects.listProjects": {
|
|
393
|
+
// No positional args. processRoot is handled via options.processRoot + buildProcessRootOnly.
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "projects.resolveProjectRoot":
|
|
397
|
+
case "projects.listProjectDocs":
|
|
398
|
+
case "projects.fetchGitTasks":
|
|
399
|
+
case "projects.createGitIssue": {
|
|
400
|
+
coerceProjectName();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case "projects.readProjectDoc": {
|
|
404
|
+
coerceProjectName();
|
|
405
|
+
if (args.length >= 2) break;
|
|
406
|
+
const requestPath = popStringOption(
|
|
407
|
+
options,
|
|
408
|
+
"requestPath",
|
|
409
|
+
"path",
|
|
410
|
+
"docPath",
|
|
411
|
+
);
|
|
412
|
+
if (requestPath) {
|
|
413
|
+
ensureArgs(args[0] ?? undefined, requestPath);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "projects.searchDocs":
|
|
418
|
+
case "projects.searchSpecs": {
|
|
419
|
+
coerceProjectSearch();
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "projects.parseProjectTargetSpec": {
|
|
423
|
+
if (args.length >= 1) break;
|
|
424
|
+
const spec = popStringOption(options, "spec", "target");
|
|
425
|
+
if (spec) {
|
|
426
|
+
args.push(spec);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "projects.resolveProjectTargetFile": {
|
|
431
|
+
if (args.length >= 2) break;
|
|
432
|
+
const targetDir = popStringOption(options, "targetDir", "dir");
|
|
433
|
+
const spec = options.spec;
|
|
434
|
+
if (targetDir) {
|
|
435
|
+
if (spec !== undefined) {
|
|
436
|
+
delete options.spec;
|
|
437
|
+
}
|
|
438
|
+
if (args.length === 0) args.push(targetDir);
|
|
439
|
+
if (args.length === 1 && spec !== undefined) args.push(spec);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const latest = popBooleanOption(options, "latest");
|
|
443
|
+
if (latest !== null) {
|
|
444
|
+
options.latest = latest;
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "projects.resolveImplementationPlan": {
|
|
449
|
+
if (args.length >= 1) break;
|
|
450
|
+
const projectRoot = popStringOption(options, "projectRoot");
|
|
451
|
+
const inputFile = popStringOption(options, "inputFile");
|
|
452
|
+
const requireActive = popBooleanOption(options, "requireActive");
|
|
453
|
+
if (projectRoot) {
|
|
454
|
+
args.push(projectRoot);
|
|
455
|
+
if (inputFile) args.push(inputFile);
|
|
456
|
+
}
|
|
457
|
+
if (requireActive !== null) {
|
|
458
|
+
options.requireActive = requireActive;
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "projects.readGitTask":
|
|
463
|
+
case "projects.writeGitTask": {
|
|
464
|
+
coerceProjectName();
|
|
465
|
+
if (args.length >= 2) break;
|
|
466
|
+
const taskRef = popStringOption(
|
|
467
|
+
options,
|
|
468
|
+
"taskRef",
|
|
469
|
+
"ref",
|
|
470
|
+
"issue",
|
|
471
|
+
"issueNumber",
|
|
472
|
+
"taskId",
|
|
473
|
+
);
|
|
474
|
+
if (taskRef && args.length >= 1) {
|
|
475
|
+
ensureArgs(args[0], taskRef);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
default:
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { args, options: normalizeProcessRootOption(options) };
|
|
484
|
+
};
|
|
74
485
|
|
|
75
486
|
const normalizeBatchToolCall = (
|
|
76
|
-
|
|
77
|
-
|
|
487
|
+
call: unknown,
|
|
488
|
+
index: number,
|
|
78
489
|
): { tool: string; payload: ToolInvocationPayload } => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
490
|
+
if (!isRecord(call)) {
|
|
491
|
+
throw new Error(`Invalid batch call at index ${index}: expected object`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const tool = typeof call.tool === "string" ? call.tool.trim() : "";
|
|
495
|
+
if (!tool) {
|
|
496
|
+
throw new Error(`Invalid batch call at index ${index}: missing "tool"`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const args = Array.isArray(call.args) ? call.args : [];
|
|
500
|
+
const { options, ...extras } = call;
|
|
501
|
+
const normalized: ToolInvocationPayload = {
|
|
502
|
+
args,
|
|
503
|
+
options: isRecord(options) ? options : {},
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
507
|
+
if (key === "tool" || key === "args" || key === "options") {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (value !== undefined) {
|
|
511
|
+
normalized.options[key] = value;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
tool,
|
|
517
|
+
payload: normalized,
|
|
518
|
+
};
|
|
519
|
+
};
|
|
106
520
|
|
|
107
521
|
const normalizeBatchPayload = (payload: unknown): BatchToolCallPayload => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!Array.isArray(payload.calls)) {
|
|
113
|
-
throw new Error('Batch tool call requires a "calls" array')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const calls = payload.calls.map((call, index) => normalizeBatchToolCall(call, index))
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
calls: calls.map(({ tool, payload }) => ({
|
|
120
|
-
tool,
|
|
121
|
-
...payload,
|
|
122
|
-
})),
|
|
123
|
-
continueOnError: Boolean(payload.continueOnError),
|
|
124
|
-
}
|
|
125
|
-
}
|
|
522
|
+
if (!isRecord(payload)) {
|
|
523
|
+
throw new Error("Batch tool call requires an object payload");
|
|
524
|
+
}
|
|
126
525
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
526
|
+
if (!Array.isArray(payload.calls)) {
|
|
527
|
+
throw new Error('Batch tool call requires a "calls" array');
|
|
528
|
+
}
|
|
130
529
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
530
|
+
const calls = payload.calls.map((call, index) =>
|
|
531
|
+
normalizeBatchToolCall(call, index),
|
|
532
|
+
);
|
|
533
|
+
const continueOnError = (() => {
|
|
534
|
+
if (payload.continueOnError === undefined) return false;
|
|
535
|
+
const parsed = parseBooleanish(payload.continueOnError);
|
|
536
|
+
if (parsed === null) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
'"continueOnError" must be a boolean or boolean-like string (true/false, 1/0).',
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return parsed;
|
|
542
|
+
})();
|
|
543
|
+
const maxConcurrency = (() => {
|
|
544
|
+
if (payload.maxConcurrency === undefined) return 8;
|
|
545
|
+
const parsed = parsePositiveInteger(payload.maxConcurrency);
|
|
546
|
+
if (!parsed) {
|
|
547
|
+
throw new Error('"maxConcurrency" must be a positive integer.');
|
|
548
|
+
}
|
|
549
|
+
return parsed;
|
|
550
|
+
})();
|
|
135
551
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
552
|
+
return {
|
|
553
|
+
calls: calls.map(({ tool, payload }) => ({
|
|
554
|
+
tool,
|
|
555
|
+
...payload,
|
|
556
|
+
})),
|
|
557
|
+
continueOnError,
|
|
558
|
+
maxConcurrency,
|
|
559
|
+
};
|
|
560
|
+
};
|
|
142
561
|
|
|
143
|
-
|
|
144
|
-
|
|
562
|
+
const collectTools = (
|
|
563
|
+
api: ToolNamespace,
|
|
564
|
+
namespace: string[],
|
|
565
|
+
path: string[] = [],
|
|
566
|
+
): ToolDefinition[] => {
|
|
567
|
+
const tools: ToolDefinition[] = [];
|
|
568
|
+
const currentPath = [...path, ...namespace];
|
|
569
|
+
|
|
570
|
+
for (const [segment, value] of Object.entries(api)) {
|
|
571
|
+
if (typeof value !== "function") {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
tools.push({
|
|
576
|
+
name: [...currentPath, segment].join("."),
|
|
577
|
+
method: value as ApiMethod,
|
|
578
|
+
path: [...currentPath, segment],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return tools;
|
|
583
|
+
};
|
|
145
584
|
|
|
146
585
|
const buildToolName = (tool: ToolDefinition, prefix?: string): string =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const buildToolList = (tools: ToolDefinition[], batchToolName: string, prefix?: string) => {
|
|
150
|
-
const toolEntries = tools.map((tool) => ({
|
|
151
|
-
name: buildToolName(tool, prefix),
|
|
152
|
-
description: `Call API method ${tool.path.join('.')}`,
|
|
153
|
-
inputSchema: {
|
|
154
|
-
type: 'object',
|
|
155
|
-
additionalProperties: true,
|
|
156
|
-
properties: {
|
|
157
|
-
args: {
|
|
158
|
-
type: 'array',
|
|
159
|
-
items: { type: 'string' },
|
|
160
|
-
description: 'Positional arguments',
|
|
161
|
-
},
|
|
162
|
-
options: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
additionalProperties: true,
|
|
165
|
-
description: 'Named options',
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
}))
|
|
170
|
-
|
|
171
|
-
const batchTool = {
|
|
172
|
-
name: batchToolName,
|
|
173
|
-
description:
|
|
174
|
-
'Preferred mode for client calls: execute multiple MCP tool calls in one request with parallel execution.',
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: 'object',
|
|
177
|
-
additionalProperties: true,
|
|
178
|
-
properties: {
|
|
179
|
-
calls: {
|
|
180
|
-
type: 'array',
|
|
181
|
-
minItems: 1,
|
|
182
|
-
items: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
additionalProperties: true,
|
|
185
|
-
properties: {
|
|
186
|
-
tool: {
|
|
187
|
-
type: 'string',
|
|
188
|
-
description: 'Full MCP tool name to execute',
|
|
189
|
-
},
|
|
190
|
-
args: {
|
|
191
|
-
type: 'array',
|
|
192
|
-
items: { type: 'string' },
|
|
193
|
-
description: 'Positional args for the tool',
|
|
194
|
-
},
|
|
195
|
-
options: {
|
|
196
|
-
type: 'object',
|
|
197
|
-
additionalProperties: true,
|
|
198
|
-
description: 'Tool invocation options',
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
required: ['tool'],
|
|
202
|
-
},
|
|
203
|
-
description: 'List of tool calls to execute',
|
|
204
|
-
},
|
|
205
|
-
continueOnError: {
|
|
206
|
-
type: 'boolean',
|
|
207
|
-
description: 'Whether to continue when a call in the batch fails',
|
|
208
|
-
default: false,
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
required: ['calls'],
|
|
212
|
-
},
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return [...toolEntries, batchTool]
|
|
216
|
-
}
|
|
586
|
+
prefix ? `${prefix}.${tool.name}` : tool.name;
|
|
217
587
|
|
|
218
|
-
type
|
|
588
|
+
type ToolAccess = "read" | "write" | "admin";
|
|
589
|
+
type ToolMeta = {
|
|
590
|
+
access: ToolAccess;
|
|
591
|
+
category?: string;
|
|
592
|
+
notes?: string;
|
|
593
|
+
};
|
|
219
594
|
|
|
220
|
-
const
|
|
221
|
-
const invocationArgs: unknown[] = [...args]
|
|
222
|
-
if (Object.keys(options).length > 0) {
|
|
223
|
-
invocationArgs.push(options)
|
|
224
|
-
}
|
|
225
|
-
return invocationArgs
|
|
226
|
-
}
|
|
595
|
+
const TOOL_META: Record<string, ToolMeta> = {};
|
|
227
596
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
597
|
+
const TOOL_REQUIRED_ARGS: Record<string, string[]> = {
|
|
598
|
+
"agents.createAgent": ["<agent-name>"],
|
|
599
|
+
"agents.setActive": ["<agent-name>", "</file-path>"],
|
|
600
|
+
"agents.loadAgent": ["<agent-name>"],
|
|
601
|
+
"agents.loadAgentPrompt": ["<agent-name>"],
|
|
602
|
+
"agents.runAgent": ["<agent-name>"],
|
|
603
|
+
"projects.resolveProjectRoot": ["<project-name>"],
|
|
604
|
+
"projects.listProjectDocs": ["<project-name>"],
|
|
605
|
+
"projects.readProjectDoc": ["<project-name>", "</doc-path>"],
|
|
606
|
+
"projects.searchDocs": ["<project-name>", "<pattern>", "[path...]"],
|
|
607
|
+
"projects.searchSpecs": ["<project-name>", "<pattern>", "[path...]"],
|
|
608
|
+
"projects.generateSpec": ["<project-name>"],
|
|
609
|
+
"projects.setActive": ["<project-name>", "</file-path>"],
|
|
610
|
+
"projects.fetchGitTasks": ["<project-name>"],
|
|
611
|
+
"projects.createGitIssue": ["<project-name>"],
|
|
612
|
+
"projects.readGitTask": ["<project-name>", "<issue-number|task-id>"],
|
|
613
|
+
"projects.writeGitTask": ["<project-name>", "<issue-number|task-id>"],
|
|
614
|
+
};
|
|
245
615
|
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (typeof processRoot === 'string') {
|
|
251
|
-
delete remaining.processRoot
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (typeof processRoot === 'string') {
|
|
255
|
-
invocationArgs.push(processRoot)
|
|
256
|
-
}
|
|
257
|
-
if (Object.keys(remaining).length > 0) {
|
|
258
|
-
invocationArgs.push(remaining)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return invocationArgs
|
|
262
|
-
}
|
|
616
|
+
const TOOL_DEFAULT_OPTIONS: Record<string, Record<string, unknown>> = {
|
|
617
|
+
"projects.searchDocs": { source: "auto" },
|
|
618
|
+
"projects.searchSpecs": { source: "auto" },
|
|
619
|
+
};
|
|
263
620
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
621
|
+
const TOOL_USAGE_HINTS: Record<string, string> = {
|
|
622
|
+
"projects.searchDocs":
|
|
623
|
+
" Usage: args=[projectName, pattern, ...paths]. Options include source/local cache controls.",
|
|
624
|
+
"projects.searchSpecs":
|
|
625
|
+
" Usage: args=[projectName, pattern, ...paths]. Options include source/local cache controls.",
|
|
626
|
+
"projects.setActive":
|
|
627
|
+
" Usage: args=[projectName, /file-path]. Use options.latest=true to auto-pick latest version.",
|
|
628
|
+
"agents.setActive":
|
|
629
|
+
" Usage: args=[agentName, /file-path]. Use options.latest=true to auto-pick latest version.",
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const NET_CURL_SCHEMA = {
|
|
633
|
+
type: "object",
|
|
634
|
+
additionalProperties: true,
|
|
635
|
+
properties: {
|
|
636
|
+
args: {
|
|
637
|
+
type: "array",
|
|
638
|
+
items: { type: "string" },
|
|
639
|
+
description:
|
|
640
|
+
'Curl-style CLI arguments (recommended when you already know curl). Excludes the leading "curl". Example: { "args": ["-L", "https://example.com"] }',
|
|
641
|
+
},
|
|
642
|
+
url: {
|
|
643
|
+
description:
|
|
644
|
+
'Structured mode: URL to fetch (string or array). Example: { "url": "https://example.com", "method": "GET" }',
|
|
645
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
646
|
+
},
|
|
647
|
+
urls: {
|
|
648
|
+
type: "array",
|
|
649
|
+
items: { type: "string" },
|
|
650
|
+
description:
|
|
651
|
+
"Structured mode: multiple URLs to fetch (like passing multiple URLs to curl).",
|
|
652
|
+
},
|
|
653
|
+
method: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description:
|
|
656
|
+
"Structured mode: HTTP method (GET/POST/PUT/PATCH/DELETE/HEAD).",
|
|
657
|
+
},
|
|
658
|
+
headers: {
|
|
659
|
+
type: "object",
|
|
660
|
+
additionalProperties: { type: "string" },
|
|
661
|
+
description: "Structured mode: request headers.",
|
|
662
|
+
},
|
|
663
|
+
body: {
|
|
664
|
+
type: "string",
|
|
665
|
+
description: "Structured mode: raw request body (maps to --data-raw).",
|
|
666
|
+
},
|
|
667
|
+
data: {
|
|
668
|
+
description: "Structured mode: request body data (maps to -d).",
|
|
669
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
670
|
+
},
|
|
671
|
+
json: {
|
|
672
|
+
description:
|
|
673
|
+
"Structured mode: JSON payload (maps to --json). Can be object/array/string.",
|
|
674
|
+
},
|
|
675
|
+
form: {
|
|
676
|
+
type: "object",
|
|
677
|
+
additionalProperties: { type: "string" },
|
|
678
|
+
description:
|
|
679
|
+
"Structured mode: multipart form fields (maps to -F key=value).",
|
|
680
|
+
},
|
|
681
|
+
followRedirects: {
|
|
682
|
+
type: "boolean",
|
|
683
|
+
description: "Structured mode: follow redirects (maps to -L).",
|
|
684
|
+
},
|
|
685
|
+
includeHeaders: {
|
|
686
|
+
type: "boolean",
|
|
687
|
+
description: "Structured mode: include response headers (maps to -i).",
|
|
688
|
+
},
|
|
689
|
+
head: {
|
|
690
|
+
type: "boolean",
|
|
691
|
+
description: "Structured mode: HEAD request (maps to -I).",
|
|
692
|
+
},
|
|
693
|
+
user: {
|
|
694
|
+
type: "string",
|
|
695
|
+
description:
|
|
696
|
+
'Structured mode: basic auth in "user:pass" form (maps to -u).',
|
|
697
|
+
},
|
|
698
|
+
output: {
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "Structured mode: write response to file (maps to -o).",
|
|
701
|
+
},
|
|
702
|
+
remoteName: {
|
|
703
|
+
type: "boolean",
|
|
704
|
+
description:
|
|
705
|
+
"Structured mode: write response to remote filename (maps to -O).",
|
|
706
|
+
},
|
|
707
|
+
fail: {
|
|
708
|
+
type: "boolean",
|
|
709
|
+
description:
|
|
710
|
+
"Structured mode: fail on HTTP >= 400 and suppress body (maps to -f).",
|
|
711
|
+
},
|
|
712
|
+
silent: {
|
|
713
|
+
type: "boolean",
|
|
714
|
+
description: "Structured mode: silent (maps to -s).",
|
|
715
|
+
},
|
|
716
|
+
showError: {
|
|
717
|
+
type: "boolean",
|
|
718
|
+
description:
|
|
719
|
+
"Structured mode: show errors even when silent (maps to -S).",
|
|
720
|
+
},
|
|
721
|
+
verbose: {
|
|
722
|
+
type: "boolean",
|
|
723
|
+
description: "Structured mode: verbose (maps to -v).",
|
|
724
|
+
},
|
|
725
|
+
writeOut: {
|
|
726
|
+
type: "string",
|
|
727
|
+
description: "Structured mode: write-out template (maps to -w).",
|
|
728
|
+
},
|
|
729
|
+
maxRedirects: {
|
|
730
|
+
type: "integer",
|
|
731
|
+
minimum: 0,
|
|
732
|
+
description: "Structured mode: max redirects (maps to --max-redirs).",
|
|
733
|
+
},
|
|
734
|
+
maxTimeSeconds: {
|
|
735
|
+
type: "number",
|
|
736
|
+
minimum: 0,
|
|
737
|
+
description: "Structured mode: max time in seconds (maps to --max-time).",
|
|
738
|
+
},
|
|
739
|
+
get: {
|
|
740
|
+
type: "boolean",
|
|
741
|
+
description: "Structured mode: send data as query params (maps to -G).",
|
|
742
|
+
},
|
|
743
|
+
timeoutMs: {
|
|
744
|
+
type: "integer",
|
|
745
|
+
minimum: 1,
|
|
746
|
+
description: "Optional timeout in milliseconds.",
|
|
747
|
+
},
|
|
748
|
+
cwd: {
|
|
749
|
+
type: "string",
|
|
750
|
+
description: "Optional working directory for the curl process.",
|
|
751
|
+
},
|
|
752
|
+
stdin: {
|
|
753
|
+
description: "Optional stdin content for @- (string or byte array).",
|
|
754
|
+
anyOf: [
|
|
755
|
+
{ type: "string" },
|
|
756
|
+
{ type: "array", items: { type: "integer", minimum: 0, maximum: 255 } },
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
env: {
|
|
760
|
+
type: "object",
|
|
761
|
+
additionalProperties: { type: "string" },
|
|
762
|
+
description:
|
|
763
|
+
"Optional environment variable overrides for the curl process.",
|
|
764
|
+
},
|
|
765
|
+
options: {
|
|
766
|
+
type: "object",
|
|
767
|
+
additionalProperties: true,
|
|
768
|
+
description:
|
|
769
|
+
"Legacy tool options bag. Prefer top-level keys instead of nesting under options.",
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
required: [],
|
|
773
|
+
} as const;
|
|
774
|
+
|
|
775
|
+
const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
776
|
+
"net.curl": NET_CURL_SCHEMA,
|
|
777
|
+
net_curl: NET_CURL_SCHEMA,
|
|
778
|
+
"projects.usage": {
|
|
779
|
+
type: "object",
|
|
780
|
+
additionalProperties: true,
|
|
781
|
+
properties: {
|
|
782
|
+
args: {
|
|
783
|
+
type: "array",
|
|
784
|
+
description: "Unused. This tool takes no arguments.",
|
|
785
|
+
minItems: 0,
|
|
786
|
+
maxItems: 0,
|
|
787
|
+
items: {},
|
|
788
|
+
},
|
|
789
|
+
options: {
|
|
790
|
+
type: "object",
|
|
791
|
+
description: "Unused.",
|
|
792
|
+
additionalProperties: true,
|
|
793
|
+
},
|
|
794
|
+
processRoot: {
|
|
795
|
+
type: "string",
|
|
796
|
+
description: "Unused.",
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
"projects.listProjects": {
|
|
801
|
+
type: "object",
|
|
802
|
+
additionalProperties: true,
|
|
803
|
+
properties: {
|
|
804
|
+
processRoot: {
|
|
805
|
+
type: "string",
|
|
806
|
+
description:
|
|
807
|
+
'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
required: [],
|
|
811
|
+
},
|
|
812
|
+
"projects.resolveProjectRoot": {
|
|
813
|
+
type: "object",
|
|
814
|
+
additionalProperties: true,
|
|
815
|
+
properties: {
|
|
816
|
+
projectName: {
|
|
817
|
+
type: "string",
|
|
818
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
819
|
+
},
|
|
820
|
+
processRoot: {
|
|
821
|
+
type: "string",
|
|
822
|
+
description:
|
|
823
|
+
"Repo root containing /projects and /agents. If omitted, uses server cwd.",
|
|
824
|
+
},
|
|
825
|
+
args: {
|
|
826
|
+
type: "array",
|
|
827
|
+
description: "Legacy positional args (projectName).",
|
|
828
|
+
items: {},
|
|
829
|
+
},
|
|
830
|
+
options: {
|
|
831
|
+
type: "object",
|
|
832
|
+
description: "Legacy named options.",
|
|
833
|
+
additionalProperties: true,
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
required: ["projectName"],
|
|
837
|
+
},
|
|
838
|
+
"projects.listProjectDocs": {
|
|
839
|
+
type: "object",
|
|
840
|
+
additionalProperties: true,
|
|
841
|
+
properties: {
|
|
842
|
+
projectName: {
|
|
843
|
+
type: "string",
|
|
844
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
845
|
+
},
|
|
846
|
+
processRoot: {
|
|
847
|
+
type: "string",
|
|
848
|
+
description: "Repo root containing /projects and /agents.",
|
|
849
|
+
},
|
|
850
|
+
args: {
|
|
851
|
+
type: "array",
|
|
852
|
+
description: "Legacy positional args (projectName).",
|
|
853
|
+
items: {},
|
|
854
|
+
},
|
|
855
|
+
options: {
|
|
856
|
+
type: "object",
|
|
857
|
+
description: "Legacy named options.",
|
|
858
|
+
additionalProperties: true,
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
required: ["projectName"],
|
|
862
|
+
},
|
|
863
|
+
"projects.readProjectDoc": {
|
|
864
|
+
type: "object",
|
|
865
|
+
additionalProperties: true,
|
|
866
|
+
properties: {
|
|
867
|
+
projectName: {
|
|
868
|
+
type: "string",
|
|
869
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
870
|
+
},
|
|
871
|
+
requestPath: {
|
|
872
|
+
type: "string",
|
|
873
|
+
description:
|
|
874
|
+
'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
|
|
875
|
+
},
|
|
876
|
+
processRoot: {
|
|
877
|
+
type: "string",
|
|
878
|
+
description: "Repo root containing /projects and /agents.",
|
|
879
|
+
},
|
|
880
|
+
args: {
|
|
881
|
+
type: "array",
|
|
882
|
+
description: "Legacy positional args (projectName, requestPath).",
|
|
883
|
+
items: {},
|
|
884
|
+
},
|
|
885
|
+
options: {
|
|
886
|
+
type: "object",
|
|
887
|
+
description: "Legacy named options.",
|
|
888
|
+
additionalProperties: true,
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
required: ["projectName", "requestPath"],
|
|
892
|
+
},
|
|
893
|
+
"projects.searchDocs": {
|
|
894
|
+
type: "object",
|
|
895
|
+
additionalProperties: true,
|
|
896
|
+
properties: {
|
|
897
|
+
projectName: {
|
|
898
|
+
type: "string",
|
|
899
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
900
|
+
},
|
|
901
|
+
pattern: {
|
|
902
|
+
type: "string",
|
|
903
|
+
description: "Search pattern (like rg PATTERN).",
|
|
904
|
+
},
|
|
905
|
+
query: { type: "string", description: "Alias of pattern." },
|
|
906
|
+
q: { type: "string", description: "Alias of pattern." },
|
|
907
|
+
paths: {
|
|
908
|
+
type: "array",
|
|
909
|
+
items: { type: "string" },
|
|
910
|
+
description: 'Paths inside docs/ to search. Defaults to [\".\"].',
|
|
911
|
+
},
|
|
912
|
+
globs: {
|
|
913
|
+
type: "array",
|
|
914
|
+
items: { type: "string" },
|
|
915
|
+
description: "Glob filters (like rg --glob).",
|
|
916
|
+
},
|
|
917
|
+
ignoreCase: { type: "boolean" },
|
|
918
|
+
caseSensitive: { type: "boolean" },
|
|
919
|
+
smartCase: { type: "boolean" },
|
|
920
|
+
fixedStrings: { type: "boolean" },
|
|
921
|
+
wordRegexp: { type: "boolean" },
|
|
922
|
+
maxCount: { type: "integer", minimum: 1 },
|
|
923
|
+
includeHidden: { type: "boolean" },
|
|
924
|
+
filesWithMatches: { type: "boolean" },
|
|
925
|
+
filesWithoutMatch: { type: "boolean" },
|
|
926
|
+
countOnly: { type: "boolean" },
|
|
927
|
+
onlyMatching: { type: "boolean" },
|
|
928
|
+
ref: {
|
|
929
|
+
type: "string",
|
|
930
|
+
description: "Optional git ref for remote search.",
|
|
931
|
+
},
|
|
932
|
+
source: {
|
|
933
|
+
type: "string",
|
|
934
|
+
enum: ["local", "gitea", "auto"],
|
|
935
|
+
description: "local=filesystem, gitea=remote, auto=local-first.",
|
|
936
|
+
},
|
|
937
|
+
refresh: {
|
|
938
|
+
type: "boolean",
|
|
939
|
+
description: "Refresh cached search corpus.",
|
|
940
|
+
},
|
|
941
|
+
cacheDir: {
|
|
942
|
+
type: "string",
|
|
943
|
+
description: "Optional override cache directory.",
|
|
944
|
+
},
|
|
945
|
+
owner: {
|
|
946
|
+
type: "string",
|
|
947
|
+
description: "Remote owner override for gitea mode.",
|
|
948
|
+
},
|
|
949
|
+
repo: {
|
|
950
|
+
type: "string",
|
|
951
|
+
description: "Remote repo override for gitea mode.",
|
|
952
|
+
},
|
|
953
|
+
processRoot: {
|
|
954
|
+
type: "string",
|
|
955
|
+
description: "Repo root containing /projects and /agents.",
|
|
956
|
+
},
|
|
957
|
+
args: {
|
|
958
|
+
type: "array",
|
|
959
|
+
description: "Legacy rg-like args (projectName, PATTERN, [PATH...]).",
|
|
960
|
+
items: {},
|
|
961
|
+
},
|
|
962
|
+
options: {
|
|
963
|
+
type: "object",
|
|
964
|
+
description: "Legacy named options.",
|
|
965
|
+
additionalProperties: true,
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
required: ["projectName"],
|
|
969
|
+
},
|
|
970
|
+
"projects.searchSpecs": {
|
|
971
|
+
type: "object",
|
|
972
|
+
additionalProperties: true,
|
|
973
|
+
properties: {
|
|
974
|
+
projectName: {
|
|
975
|
+
type: "string",
|
|
976
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
977
|
+
},
|
|
978
|
+
pattern: {
|
|
979
|
+
type: "string",
|
|
980
|
+
description: "Search pattern (like rg PATTERN).",
|
|
981
|
+
},
|
|
982
|
+
query: { type: "string", description: "Alias of pattern." },
|
|
983
|
+
q: { type: "string", description: "Alias of pattern." },
|
|
984
|
+
paths: {
|
|
985
|
+
type: "array",
|
|
986
|
+
items: { type: "string" },
|
|
987
|
+
description: 'Paths inside spec/ to search. Defaults to [\".\"].',
|
|
988
|
+
},
|
|
989
|
+
globs: {
|
|
990
|
+
type: "array",
|
|
991
|
+
items: { type: "string" },
|
|
992
|
+
description: "Glob filters (like rg --glob).",
|
|
993
|
+
},
|
|
994
|
+
ignoreCase: { type: "boolean" },
|
|
995
|
+
caseSensitive: { type: "boolean" },
|
|
996
|
+
smartCase: { type: "boolean" },
|
|
997
|
+
fixedStrings: { type: "boolean" },
|
|
998
|
+
wordRegexp: { type: "boolean" },
|
|
999
|
+
maxCount: { type: "integer", minimum: 1 },
|
|
1000
|
+
includeHidden: { type: "boolean" },
|
|
1001
|
+
filesWithMatches: { type: "boolean" },
|
|
1002
|
+
filesWithoutMatch: { type: "boolean" },
|
|
1003
|
+
countOnly: { type: "boolean" },
|
|
1004
|
+
onlyMatching: { type: "boolean" },
|
|
1005
|
+
ref: {
|
|
1006
|
+
type: "string",
|
|
1007
|
+
description: "Optional git ref for remote search.",
|
|
1008
|
+
},
|
|
1009
|
+
source: {
|
|
1010
|
+
type: "string",
|
|
1011
|
+
enum: ["local", "gitea", "auto"],
|
|
1012
|
+
description: "local=filesystem, gitea=remote, auto=local-first.",
|
|
1013
|
+
},
|
|
1014
|
+
refresh: {
|
|
1015
|
+
type: "boolean",
|
|
1016
|
+
description: "Refresh cached search corpus.",
|
|
1017
|
+
},
|
|
1018
|
+
cacheDir: {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
description: "Optional override cache directory.",
|
|
1021
|
+
},
|
|
1022
|
+
owner: {
|
|
1023
|
+
type: "string",
|
|
1024
|
+
description: "Remote owner override for gitea mode.",
|
|
1025
|
+
},
|
|
1026
|
+
repo: {
|
|
1027
|
+
type: "string",
|
|
1028
|
+
description: "Remote repo override for gitea mode.",
|
|
1029
|
+
},
|
|
1030
|
+
processRoot: {
|
|
1031
|
+
type: "string",
|
|
1032
|
+
description: "Repo root containing /projects and /agents.",
|
|
1033
|
+
},
|
|
1034
|
+
args: {
|
|
1035
|
+
type: "array",
|
|
1036
|
+
description: "Legacy rg-like args (projectName, PATTERN, [PATH...]).",
|
|
1037
|
+
items: {},
|
|
1038
|
+
},
|
|
1039
|
+
options: {
|
|
1040
|
+
type: "object",
|
|
1041
|
+
description: "Legacy named options.",
|
|
1042
|
+
additionalProperties: true,
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
required: ["projectName"],
|
|
1046
|
+
},
|
|
1047
|
+
"projects.parseProjectTargetSpec": {
|
|
1048
|
+
type: "object",
|
|
1049
|
+
additionalProperties: true,
|
|
1050
|
+
properties: {
|
|
1051
|
+
spec: {
|
|
1052
|
+
type: "string",
|
|
1053
|
+
description:
|
|
1054
|
+
'Target spec string like \"/implementation-plan.v0.0.1\" or \"spec/README.md\".',
|
|
1055
|
+
},
|
|
1056
|
+
target: { type: "string", description: "Alias of spec." },
|
|
1057
|
+
args: {
|
|
1058
|
+
type: "array",
|
|
1059
|
+
description: "Legacy positional args (spec).",
|
|
1060
|
+
items: {},
|
|
1061
|
+
},
|
|
1062
|
+
options: {
|
|
1063
|
+
type: "object",
|
|
1064
|
+
description: "Legacy named options.",
|
|
1065
|
+
additionalProperties: true,
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
anyOf: [{ required: ["spec"] }, { required: ["target"] }],
|
|
1069
|
+
},
|
|
1070
|
+
"projects.resolveProjectTargetFile": {
|
|
1071
|
+
type: "object",
|
|
1072
|
+
additionalProperties: true,
|
|
1073
|
+
properties: {
|
|
1074
|
+
targetDir: { type: "string", description: "Directory to scan." },
|
|
1075
|
+
spec: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
description: "Output of projects.parseProjectTargetSpec().",
|
|
1078
|
+
additionalProperties: true,
|
|
1079
|
+
},
|
|
1080
|
+
latest: {
|
|
1081
|
+
type: "boolean",
|
|
1082
|
+
description:
|
|
1083
|
+
"If true and no version given, select latest versioned file.",
|
|
1084
|
+
},
|
|
1085
|
+
args: {
|
|
1086
|
+
type: "array",
|
|
1087
|
+
description: "Legacy positional args (targetDir, spec).",
|
|
1088
|
+
items: {},
|
|
1089
|
+
},
|
|
1090
|
+
options: {
|
|
1091
|
+
type: "object",
|
|
1092
|
+
description: "Legacy named options (e.g. {latest:true}).",
|
|
1093
|
+
additionalProperties: true,
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
required: ["targetDir", "spec"],
|
|
1097
|
+
},
|
|
1098
|
+
"projects.resolveImplementationPlan": {
|
|
1099
|
+
type: "object",
|
|
1100
|
+
additionalProperties: true,
|
|
1101
|
+
properties: {
|
|
1102
|
+
projectRoot: {
|
|
1103
|
+
type: "string",
|
|
1104
|
+
description: "Absolute project directory (e.g. C:/.../projects/adl).",
|
|
1105
|
+
},
|
|
1106
|
+
inputFile: {
|
|
1107
|
+
type: "string",
|
|
1108
|
+
description: "Optional file within docs/ (or absolute) to select plan.",
|
|
1109
|
+
},
|
|
1110
|
+
requireActive: {
|
|
1111
|
+
type: "boolean",
|
|
1112
|
+
description: "If true, require implementation-plan.active.* to exist.",
|
|
1113
|
+
},
|
|
1114
|
+
args: {
|
|
1115
|
+
type: "array",
|
|
1116
|
+
description: "Legacy positional args (projectRoot, inputFile?).",
|
|
1117
|
+
items: {},
|
|
1118
|
+
},
|
|
1119
|
+
options: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
description: "Legacy named options (e.g. {requireActive:true}).",
|
|
1122
|
+
additionalProperties: true,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
required: ["projectRoot"],
|
|
1126
|
+
},
|
|
1127
|
+
"projects.fetchGitTasks": {
|
|
1128
|
+
type: "object",
|
|
1129
|
+
additionalProperties: true,
|
|
1130
|
+
properties: {
|
|
1131
|
+
projectName: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
description: "Project name under /projects.",
|
|
1134
|
+
},
|
|
1135
|
+
owner: { type: "string", description: "Remote owner override." },
|
|
1136
|
+
repo: { type: "string", description: "Remote repo override." },
|
|
1137
|
+
state: {
|
|
1138
|
+
type: "string",
|
|
1139
|
+
enum: ["open", "closed", "all"],
|
|
1140
|
+
description: "Issue state filter.",
|
|
1141
|
+
},
|
|
1142
|
+
taskOnly: {
|
|
1143
|
+
type: "boolean",
|
|
1144
|
+
description: "If true, only return issues with TASK-* IDs.",
|
|
1145
|
+
},
|
|
1146
|
+
processRoot: {
|
|
1147
|
+
type: "string",
|
|
1148
|
+
description: "Repo root containing /projects.",
|
|
1149
|
+
},
|
|
1150
|
+
args: {
|
|
1151
|
+
type: "array",
|
|
1152
|
+
description: "Legacy positional args (projectName).",
|
|
1153
|
+
items: {},
|
|
1154
|
+
},
|
|
1155
|
+
options: {
|
|
1156
|
+
type: "object",
|
|
1157
|
+
description: "Legacy options (owner/repo/state/taskOnly).",
|
|
1158
|
+
additionalProperties: true,
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
required: ["projectName"],
|
|
1162
|
+
},
|
|
1163
|
+
"projects.readGitTask": {
|
|
1164
|
+
type: "object",
|
|
1165
|
+
additionalProperties: true,
|
|
1166
|
+
properties: {
|
|
1167
|
+
projectName: {
|
|
1168
|
+
type: "string",
|
|
1169
|
+
description: "Project name under /projects.",
|
|
1170
|
+
},
|
|
1171
|
+
taskRef: {
|
|
1172
|
+
type: "string",
|
|
1173
|
+
description:
|
|
1174
|
+
'Issue number (e.g. \"123\") or task ID (e.g. \"TASK-001\").',
|
|
1175
|
+
},
|
|
1176
|
+
owner: { type: "string", description: "Remote owner override." },
|
|
1177
|
+
repo: { type: "string", description: "Remote repo override." },
|
|
1178
|
+
state: {
|
|
1179
|
+
type: "string",
|
|
1180
|
+
enum: ["open", "closed", "all"],
|
|
1181
|
+
description: "Search scope.",
|
|
1182
|
+
},
|
|
1183
|
+
taskOnly: {
|
|
1184
|
+
type: "boolean",
|
|
1185
|
+
description: "If true, restrict to issues with TASK-* payloads.",
|
|
1186
|
+
},
|
|
1187
|
+
processRoot: {
|
|
1188
|
+
type: "string",
|
|
1189
|
+
description: "Repo root containing /projects.",
|
|
1190
|
+
},
|
|
1191
|
+
args: {
|
|
1192
|
+
type: "array",
|
|
1193
|
+
description: "Legacy positional args (projectName, taskRef).",
|
|
1194
|
+
items: {},
|
|
1195
|
+
},
|
|
1196
|
+
options: {
|
|
1197
|
+
type: "object",
|
|
1198
|
+
description: "Legacy options.",
|
|
1199
|
+
additionalProperties: true,
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
required: ["projectName", "taskRef"],
|
|
1203
|
+
},
|
|
1204
|
+
"projects.writeGitTask": {
|
|
1205
|
+
type: "object",
|
|
1206
|
+
additionalProperties: true,
|
|
1207
|
+
properties: {
|
|
1208
|
+
projectName: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
description: "Project name under /projects.",
|
|
1211
|
+
},
|
|
1212
|
+
taskRef: {
|
|
1213
|
+
type: "string",
|
|
1214
|
+
description:
|
|
1215
|
+
'Issue number (e.g. \"123\") or task ID (e.g. \"TASK-001\").',
|
|
1216
|
+
},
|
|
1217
|
+
owner: { type: "string", description: "Remote owner override." },
|
|
1218
|
+
repo: { type: "string", description: "Remote repo override." },
|
|
1219
|
+
createIfMissing: {
|
|
1220
|
+
type: "boolean",
|
|
1221
|
+
description: "Create issue if taskRef is TASK-* and not found.",
|
|
1222
|
+
},
|
|
1223
|
+
title: { type: "string", description: "Issue title override." },
|
|
1224
|
+
body: { type: "string", description: "Issue body override." },
|
|
1225
|
+
labels: {
|
|
1226
|
+
type: "array",
|
|
1227
|
+
items: { type: "string" },
|
|
1228
|
+
description: "Labels to set.",
|
|
1229
|
+
},
|
|
1230
|
+
state: {
|
|
1231
|
+
type: "string",
|
|
1232
|
+
enum: ["open", "closed"],
|
|
1233
|
+
description: "Issue state.",
|
|
1234
|
+
},
|
|
1235
|
+
taskDependencies: {
|
|
1236
|
+
type: "array",
|
|
1237
|
+
items: { type: "string" },
|
|
1238
|
+
description: "TASK-* dependencies.",
|
|
1239
|
+
},
|
|
1240
|
+
taskSignature: {
|
|
1241
|
+
type: "string",
|
|
1242
|
+
description: "Optional task signature.",
|
|
1243
|
+
},
|
|
1244
|
+
processRoot: {
|
|
1245
|
+
type: "string",
|
|
1246
|
+
description: "Repo root containing /projects.",
|
|
1247
|
+
},
|
|
1248
|
+
args: {
|
|
1249
|
+
type: "array",
|
|
1250
|
+
description: "Legacy positional args (projectName, taskRef).",
|
|
1251
|
+
items: {},
|
|
1252
|
+
},
|
|
1253
|
+
options: {
|
|
1254
|
+
type: "object",
|
|
1255
|
+
description: "Legacy options.",
|
|
1256
|
+
additionalProperties: true,
|
|
1257
|
+
},
|
|
1258
|
+
},
|
|
1259
|
+
required: ["projectName", "taskRef"],
|
|
1260
|
+
},
|
|
1261
|
+
"projects.createGitIssue": {
|
|
1262
|
+
type: "object",
|
|
1263
|
+
additionalProperties: true,
|
|
1264
|
+
properties: {
|
|
1265
|
+
projectName: {
|
|
1266
|
+
type: "string",
|
|
1267
|
+
description: "Project name under /projects.",
|
|
1268
|
+
},
|
|
1269
|
+
owner: { type: "string", description: "Remote owner override." },
|
|
1270
|
+
repo: { type: "string", description: "Remote repo override." },
|
|
1271
|
+
title: { type: "string", description: "Issue title." },
|
|
1272
|
+
body: { type: "string", description: "Issue body." },
|
|
1273
|
+
labels: {
|
|
1274
|
+
type: "array",
|
|
1275
|
+
items: { type: "string" },
|
|
1276
|
+
description: "Labels to set.",
|
|
1277
|
+
},
|
|
1278
|
+
processRoot: {
|
|
1279
|
+
type: "string",
|
|
1280
|
+
description: "Repo root containing /projects.",
|
|
1281
|
+
},
|
|
1282
|
+
args: {
|
|
1283
|
+
type: "array",
|
|
1284
|
+
description: "Legacy positional args (projectName).",
|
|
1285
|
+
items: {},
|
|
1286
|
+
},
|
|
1287
|
+
options: {
|
|
1288
|
+
type: "object",
|
|
1289
|
+
description: "Legacy options.",
|
|
1290
|
+
additionalProperties: true,
|
|
1291
|
+
},
|
|
1292
|
+
},
|
|
1293
|
+
required: ["projectName", "title"],
|
|
1294
|
+
},
|
|
1295
|
+
"mcp.search": {
|
|
1296
|
+
type: "object",
|
|
1297
|
+
additionalProperties: true,
|
|
1298
|
+
properties: {
|
|
1299
|
+
projectName: {
|
|
1300
|
+
type: "string",
|
|
1301
|
+
description: 'Project name under /projects (e.g. "adl").',
|
|
1302
|
+
},
|
|
1303
|
+
section: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
enum: ["spec", "docs"],
|
|
1306
|
+
description: "Search target section.",
|
|
1307
|
+
},
|
|
1308
|
+
pattern: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Search pattern (like rg PATTERN).",
|
|
1311
|
+
},
|
|
1312
|
+
query: { type: "string", description: "Alias of pattern." },
|
|
1313
|
+
q: { type: "string", description: "Alias of pattern." },
|
|
1314
|
+
paths: {
|
|
1315
|
+
type: "array",
|
|
1316
|
+
items: { type: "string" },
|
|
1317
|
+
description: "Paths to search within the section.",
|
|
1318
|
+
},
|
|
1319
|
+
globs: {
|
|
1320
|
+
type: "array",
|
|
1321
|
+
items: { type: "string" },
|
|
1322
|
+
description: "Glob filters (like rg --glob).",
|
|
1323
|
+
},
|
|
1324
|
+
ignoreCase: { type: "boolean" },
|
|
1325
|
+
caseSensitive: { type: "boolean" },
|
|
1326
|
+
smartCase: { type: "boolean" },
|
|
1327
|
+
fixedStrings: { type: "boolean" },
|
|
1328
|
+
wordRegexp: { type: "boolean" },
|
|
1329
|
+
maxCount: { type: "integer", minimum: 1 },
|
|
1330
|
+
includeHidden: { type: "boolean" },
|
|
1331
|
+
filesWithMatches: { type: "boolean" },
|
|
1332
|
+
filesWithoutMatch: { type: "boolean" },
|
|
1333
|
+
countOnly: { type: "boolean" },
|
|
1334
|
+
onlyMatching: { type: "boolean" },
|
|
1335
|
+
ref: {
|
|
1336
|
+
type: "string",
|
|
1337
|
+
description: "Optional git ref for remote search.",
|
|
1338
|
+
},
|
|
1339
|
+
source: {
|
|
1340
|
+
type: "string",
|
|
1341
|
+
enum: ["local", "gitea", "auto"],
|
|
1342
|
+
description: "local=filesystem, gitea=remote, auto=local-first.",
|
|
1343
|
+
},
|
|
1344
|
+
refresh: {
|
|
1345
|
+
type: "boolean",
|
|
1346
|
+
description: "Refresh cached search corpus.",
|
|
1347
|
+
},
|
|
1348
|
+
cacheDir: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "Optional override cache directory.",
|
|
1351
|
+
},
|
|
1352
|
+
processRoot: {
|
|
1353
|
+
type: "string",
|
|
1354
|
+
description:
|
|
1355
|
+
"Repo root containing /projects. If you pass a project root, it will be normalized.",
|
|
1356
|
+
},
|
|
1357
|
+
},
|
|
1358
|
+
$comment: safeJsonStringify({
|
|
1359
|
+
example: {
|
|
1360
|
+
projectName: "adl",
|
|
1361
|
+
section: "spec",
|
|
1362
|
+
pattern: "REQ-",
|
|
1363
|
+
paths: ["."],
|
|
1364
|
+
source: "auto",
|
|
1365
|
+
},
|
|
1366
|
+
}),
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const buildArgsSchemaFromPlaceholders = (
|
|
1371
|
+
placeholders: string[],
|
|
1372
|
+
): Record<string, unknown> => ({
|
|
1373
|
+
type: "array",
|
|
1374
|
+
description: "Positional arguments",
|
|
1375
|
+
minItems: placeholders.length,
|
|
1376
|
+
prefixItems: placeholders.map((placeholder) => ({
|
|
1377
|
+
type: "string",
|
|
1378
|
+
title: placeholder,
|
|
1379
|
+
description: placeholder,
|
|
1380
|
+
})),
|
|
1381
|
+
items: {},
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
|
|
1385
|
+
"projects.listProjects": {
|
|
1386
|
+
type: "array",
|
|
1387
|
+
description: "No positional arguments. Use options.processRoot if needed.",
|
|
1388
|
+
minItems: 0,
|
|
1389
|
+
maxItems: 0,
|
|
1390
|
+
items: {},
|
|
1391
|
+
},
|
|
1392
|
+
"projects.usage": {
|
|
1393
|
+
type: "array",
|
|
1394
|
+
description: "No positional arguments.",
|
|
1395
|
+
minItems: 0,
|
|
1396
|
+
maxItems: 0,
|
|
1397
|
+
items: {},
|
|
1398
|
+
},
|
|
1399
|
+
"agents.usage": {
|
|
1400
|
+
type: "array",
|
|
1401
|
+
description: "No positional arguments.",
|
|
1402
|
+
minItems: 0,
|
|
1403
|
+
maxItems: 0,
|
|
1404
|
+
items: {},
|
|
1405
|
+
},
|
|
1406
|
+
"projects.searchDocs": {
|
|
1407
|
+
type: "array",
|
|
1408
|
+
description: "rg-like: projectName, pattern, then optional paths.",
|
|
1409
|
+
minItems: 2,
|
|
1410
|
+
prefixItems: [
|
|
1411
|
+
{ type: "string", title: "projectName" },
|
|
1412
|
+
{ type: "string", title: "pattern" },
|
|
1413
|
+
],
|
|
1414
|
+
items: { type: "string", title: "path" },
|
|
1415
|
+
},
|
|
1416
|
+
"projects.searchSpecs": {
|
|
1417
|
+
type: "array",
|
|
1418
|
+
description: "rg-like: projectName, pattern, then optional paths.",
|
|
1419
|
+
minItems: 2,
|
|
1420
|
+
prefixItems: [
|
|
1421
|
+
{ type: "string", title: "projectName" },
|
|
1422
|
+
{ type: "string", title: "pattern" },
|
|
1423
|
+
],
|
|
1424
|
+
items: { type: "string", title: "path" },
|
|
1425
|
+
},
|
|
1426
|
+
"projects.resolveProjectTargetFile": {
|
|
1427
|
+
type: "array",
|
|
1428
|
+
description: "Low-level helper: resolve versioned file in a directory.",
|
|
1429
|
+
minItems: 2,
|
|
1430
|
+
prefixItems: [
|
|
1431
|
+
{
|
|
1432
|
+
type: "string",
|
|
1433
|
+
title: "targetDir",
|
|
1434
|
+
description: "Directory to scan (absolute or relative to cwd).",
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
type: "object",
|
|
1438
|
+
title: "spec",
|
|
1439
|
+
description: "Output of projects.parseProjectTargetSpec().",
|
|
1440
|
+
additionalProperties: true,
|
|
1441
|
+
},
|
|
1442
|
+
],
|
|
1443
|
+
items: {},
|
|
1444
|
+
},
|
|
1445
|
+
"agents.resolveTargetFile": {
|
|
1446
|
+
type: "array",
|
|
1447
|
+
description: "Low-level helper: resolve versioned file in a directory.",
|
|
1448
|
+
minItems: 2,
|
|
1449
|
+
prefixItems: [
|
|
1450
|
+
{ type: "string", title: "targetDir" },
|
|
1451
|
+
{
|
|
1452
|
+
type: "object",
|
|
1453
|
+
title: "spec",
|
|
1454
|
+
description: "Output of agents.parseTargetSpec().",
|
|
1455
|
+
additionalProperties: true,
|
|
1456
|
+
},
|
|
1457
|
+
],
|
|
1458
|
+
items: {},
|
|
1459
|
+
},
|
|
1460
|
+
"projects.resolveImplementationPlan": {
|
|
1461
|
+
type: "array",
|
|
1462
|
+
description:
|
|
1463
|
+
"Low-level helper: resolve docs implementation plan path for a given projectRoot.",
|
|
1464
|
+
minItems: 1,
|
|
1465
|
+
prefixItems: [
|
|
1466
|
+
{
|
|
1467
|
+
type: "string",
|
|
1468
|
+
title: "projectRoot",
|
|
1469
|
+
description:
|
|
1470
|
+
"Absolute path to a project folder (e.g. .../projects/adl).",
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
type: "string",
|
|
1474
|
+
title: "inputFile",
|
|
1475
|
+
description: "Optional path within docs/ (or absolute) to pick a plan.",
|
|
1476
|
+
},
|
|
1477
|
+
],
|
|
1478
|
+
items: {},
|
|
1479
|
+
},
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
const toolArgsSchema = (toolName: string): Record<string, unknown> => {
|
|
1483
|
+
const override = TOOL_ARGS_SCHEMA_OVERRIDES[toolName];
|
|
1484
|
+
if (override) return override;
|
|
1485
|
+
|
|
1486
|
+
const requiredArgs = TOOL_REQUIRED_ARGS[toolName];
|
|
1487
|
+
if (requiredArgs && requiredArgs.length > 0) {
|
|
1488
|
+
return buildArgsSchemaFromPlaceholders(requiredArgs);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return {
|
|
1492
|
+
type: "array",
|
|
1493
|
+
items: {},
|
|
1494
|
+
description: "Positional arguments",
|
|
1495
|
+
};
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
const getInvocationPlanName = (toolName: string): string => {
|
|
1499
|
+
const plan = toolInvocationPlans[toolName];
|
|
1500
|
+
if (!plan) return "default";
|
|
1501
|
+
if (plan === buildOptionsOnly) return "optionsOnly";
|
|
1502
|
+
if (plan === buildOptionsThenProcessRoot) return "optionsThenProcessRoot";
|
|
1503
|
+
if (plan === buildProcessRootThenOptions) return "processRootThenOptions";
|
|
1504
|
+
if (plan === buildProcessRootOnly) return "processRootOnly";
|
|
1505
|
+
return "custom";
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
const buildInvocationExample = (toolName: string): Record<string, unknown> => {
|
|
1509
|
+
const requiredArgs = TOOL_REQUIRED_ARGS[toolName];
|
|
1510
|
+
const plan = getInvocationPlanName(toolName);
|
|
1511
|
+
const defaultOptions = TOOL_DEFAULT_OPTIONS[toolName] ?? {};
|
|
1512
|
+
|
|
1513
|
+
const example: Record<string, unknown> = {};
|
|
1514
|
+
if (requiredArgs && requiredArgs.length > 0) {
|
|
1515
|
+
example.args = [...requiredArgs];
|
|
1516
|
+
} else if (plan !== "processRootOnly") {
|
|
1517
|
+
example.args = ["<arg0>"];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (plan === "processRootOnly") {
|
|
1521
|
+
example.options = { processRoot: "<repo-root>", ...defaultOptions };
|
|
1522
|
+
return example;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (plan === "optionsThenProcessRoot" || plan === "processRootThenOptions") {
|
|
1526
|
+
example.options = { processRoot: "<repo-root>", ...defaultOptions };
|
|
1527
|
+
return example;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (plan === "optionsOnly") {
|
|
1531
|
+
example.options =
|
|
1532
|
+
Object.keys(defaultOptions).length > 0
|
|
1533
|
+
? { ...defaultOptions }
|
|
1534
|
+
: { example: true };
|
|
1535
|
+
return example;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
example.options =
|
|
1539
|
+
Object.keys(defaultOptions).length > 0
|
|
1540
|
+
? { ...defaultOptions }
|
|
1541
|
+
: { example: true };
|
|
1542
|
+
return example;
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
const defaultToolInputSchema = (toolName: string) => ({
|
|
1546
|
+
type: "object",
|
|
1547
|
+
additionalProperties: true,
|
|
1548
|
+
properties: {
|
|
1549
|
+
args: {
|
|
1550
|
+
...toolArgsSchema(toolName),
|
|
1551
|
+
},
|
|
1552
|
+
options: {
|
|
1553
|
+
type: "object",
|
|
1554
|
+
additionalProperties: true,
|
|
1555
|
+
description: "Named options",
|
|
1556
|
+
},
|
|
1557
|
+
processRoot: {
|
|
1558
|
+
type: "string",
|
|
1559
|
+
description:
|
|
1560
|
+
'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
$comment: safeJsonStringify({
|
|
1564
|
+
note: "Preferred: pass args as JSON-native values. Avoid stringifying objects.",
|
|
1565
|
+
invocationPlan: getInvocationPlanName(toolName),
|
|
1566
|
+
example: buildInvocationExample(toolName),
|
|
1567
|
+
}),
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
const buildToolList = (
|
|
1571
|
+
tools: ToolDefinition[],
|
|
1572
|
+
batchToolName: string,
|
|
1573
|
+
prefix: string | undefined,
|
|
1574
|
+
exposeUnprefixedAliases: boolean,
|
|
1575
|
+
) => {
|
|
1576
|
+
const toolEntries = tools.flatMap((tool) => {
|
|
1577
|
+
const canonicalName = buildToolName(tool, prefix);
|
|
1578
|
+
const schema =
|
|
1579
|
+
TOOL_INPUT_SCHEMA_OVERRIDES[tool.name] ??
|
|
1580
|
+
defaultToolInputSchema(tool.name);
|
|
1581
|
+
const base = {
|
|
1582
|
+
// Some UIs only show tool descriptions. Make the fastest path be "copy schema, fill values".
|
|
1583
|
+
description: safeJsonStringify(schema),
|
|
1584
|
+
inputSchema: schema,
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
const entries: Array<{
|
|
1588
|
+
name: string;
|
|
1589
|
+
description: string;
|
|
1590
|
+
inputSchema: unknown;
|
|
1591
|
+
}> = [
|
|
1592
|
+
{
|
|
1593
|
+
name: canonicalName,
|
|
1594
|
+
...base,
|
|
1595
|
+
},
|
|
1596
|
+
];
|
|
1597
|
+
|
|
1598
|
+
if (prefix && exposeUnprefixedAliases) {
|
|
1599
|
+
entries.push({
|
|
1600
|
+
name: tool.name,
|
|
1601
|
+
description: safeJsonStringify(schema),
|
|
1602
|
+
inputSchema: base.inputSchema,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
return entries;
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
const batchTool = {
|
|
1610
|
+
name: batchToolName,
|
|
1611
|
+
description: "",
|
|
1612
|
+
inputSchema: {
|
|
1613
|
+
type: "object",
|
|
1614
|
+
additionalProperties: true,
|
|
1615
|
+
properties: {
|
|
1616
|
+
calls: {
|
|
1617
|
+
type: "array",
|
|
1618
|
+
minItems: 1,
|
|
1619
|
+
items: {
|
|
1620
|
+
type: "object",
|
|
1621
|
+
additionalProperties: true,
|
|
1622
|
+
properties: {
|
|
1623
|
+
tool: {
|
|
1624
|
+
type: "string",
|
|
1625
|
+
description: "Full MCP tool name to execute",
|
|
1626
|
+
},
|
|
1627
|
+
args: {
|
|
1628
|
+
type: "array",
|
|
1629
|
+
items: {},
|
|
1630
|
+
description: "Positional args for the tool",
|
|
1631
|
+
},
|
|
1632
|
+
options: {
|
|
1633
|
+
type: "object",
|
|
1634
|
+
additionalProperties: true,
|
|
1635
|
+
description: "Tool invocation options",
|
|
1636
|
+
},
|
|
1637
|
+
},
|
|
1638
|
+
required: ["tool"],
|
|
1639
|
+
},
|
|
1640
|
+
description: "List of tool calls to execute",
|
|
1641
|
+
},
|
|
1642
|
+
continueOnError: {
|
|
1643
|
+
type: "boolean",
|
|
1644
|
+
description: "Whether to continue when a call in the batch fails",
|
|
1645
|
+
default: false,
|
|
1646
|
+
},
|
|
1647
|
+
maxConcurrency: {
|
|
1648
|
+
type: "integer",
|
|
1649
|
+
minimum: 1,
|
|
1650
|
+
description:
|
|
1651
|
+
"Max number of calls to execute concurrently when continueOnError=true.",
|
|
1652
|
+
default: 8,
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
required: ["calls"],
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
(batchTool as any).description = safeJsonStringify(batchTool.inputSchema);
|
|
1660
|
+
|
|
1661
|
+
const out = [...toolEntries, batchTool];
|
|
1662
|
+
if (prefix && exposeUnprefixedAliases) {
|
|
1663
|
+
out.push({
|
|
1664
|
+
...batchTool,
|
|
1665
|
+
name: "batch",
|
|
1666
|
+
description: safeJsonStringify(batchTool.inputSchema),
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return out;
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
type ToolInvoker = (
|
|
1674
|
+
args: unknown[],
|
|
1675
|
+
options: Record<string, unknown>,
|
|
1676
|
+
) => unknown[];
|
|
1677
|
+
|
|
1678
|
+
const buildOptionsOnly = (
|
|
1679
|
+
args: unknown[],
|
|
1680
|
+
options: Record<string, unknown>,
|
|
1681
|
+
): unknown[] => {
|
|
1682
|
+
const invocationArgs: unknown[] = [...args];
|
|
1683
|
+
if (Object.keys(options).length > 0) {
|
|
1684
|
+
invocationArgs.push(options);
|
|
1685
|
+
}
|
|
1686
|
+
return invocationArgs;
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
const buildOptionsThenProcessRoot = (
|
|
1690
|
+
args: unknown[],
|
|
1691
|
+
options: Record<string, unknown>,
|
|
1692
|
+
): unknown[] => {
|
|
1693
|
+
const invocationArgs: unknown[] = [...args];
|
|
1694
|
+
const remaining = { ...options };
|
|
1695
|
+
const processRoot = remaining.processRoot;
|
|
1696
|
+
if (typeof processRoot === "string") {
|
|
1697
|
+
delete remaining.processRoot;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (Object.keys(remaining).length > 0) {
|
|
1701
|
+
invocationArgs.push(remaining);
|
|
1702
|
+
} else if (typeof processRoot === "string") {
|
|
1703
|
+
// Preserve positional slot for signatures like fn(projectName, options?, processRoot?).
|
|
1704
|
+
invocationArgs.push({});
|
|
1705
|
+
}
|
|
1706
|
+
if (typeof processRoot === "string") {
|
|
1707
|
+
invocationArgs.push(processRoot);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return invocationArgs;
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
const buildProcessRootThenOptions = (
|
|
1714
|
+
args: unknown[],
|
|
1715
|
+
options: Record<string, unknown>,
|
|
1716
|
+
): unknown[] => {
|
|
1717
|
+
const invocationArgs: unknown[] = [...args];
|
|
1718
|
+
const remaining = { ...options };
|
|
1719
|
+
const processRoot = remaining.processRoot;
|
|
1720
|
+
if (typeof processRoot === "string") {
|
|
1721
|
+
delete remaining.processRoot;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (typeof processRoot === "string") {
|
|
1725
|
+
invocationArgs.push(processRoot);
|
|
1726
|
+
}
|
|
1727
|
+
if (Object.keys(remaining).length > 0) {
|
|
1728
|
+
invocationArgs.push(remaining);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return invocationArgs;
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
const buildProcessRootOnly = (
|
|
1735
|
+
args: unknown[],
|
|
1736
|
+
options: Record<string, unknown>,
|
|
1737
|
+
): unknown[] => {
|
|
1738
|
+
const invocationArgs: unknown[] = [...args];
|
|
1739
|
+
const processRoot = options.processRoot;
|
|
1740
|
+
if (typeof processRoot === "string") {
|
|
1741
|
+
invocationArgs.push(processRoot);
|
|
1742
|
+
}
|
|
1743
|
+
return invocationArgs;
|
|
1744
|
+
};
|
|
272
1745
|
|
|
273
1746
|
const toolInvocationPlans: Record<string, ToolInvoker> = {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
1747
|
+
"agents.setActive": buildProcessRootThenOptions,
|
|
1748
|
+
"agents.resolveAgentsRootFrom": buildProcessRootOnly,
|
|
1749
|
+
"projects.setActive": buildProcessRootThenOptions,
|
|
1750
|
+
"projects.generateSpec": buildOptionsThenProcessRoot,
|
|
1751
|
+
"projects.syncTasks": buildOptionsThenProcessRoot,
|
|
1752
|
+
"projects.clearIssues": buildOptionsThenProcessRoot,
|
|
1753
|
+
"projects.fetchGitTasks": buildOptionsThenProcessRoot,
|
|
1754
|
+
"projects.createGitIssue": buildOptionsThenProcessRoot,
|
|
1755
|
+
"projects.readGitTask": buildOptionsThenProcessRoot,
|
|
1756
|
+
"projects.writeGitTask": buildOptionsThenProcessRoot,
|
|
1757
|
+
"agents.resolveTargetFile": buildOptionsOnly,
|
|
1758
|
+
"projects.resolveProjectTargetFile": buildOptionsOnly,
|
|
1759
|
+
"agents.loadAgent": buildProcessRootOnly,
|
|
1760
|
+
"agents.loadAgentPrompt": buildProcessRootOnly,
|
|
1761
|
+
"projects.resolveImplementationPlan": (args, options) => {
|
|
1762
|
+
const invocationArgs: unknown[] = [...args];
|
|
1763
|
+
const remaining = { ...options };
|
|
1764
|
+
const processRoot = remaining.processRoot;
|
|
1765
|
+
if (typeof processRoot === "string") {
|
|
1766
|
+
delete remaining.processRoot;
|
|
1767
|
+
}
|
|
296
1768
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1769
|
+
// This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
|
|
1770
|
+
// If the caller provides options but no inputFile, preserve the positional slot.
|
|
1771
|
+
if (Object.keys(remaining).length > 0) {
|
|
1772
|
+
if (invocationArgs.length === 1) {
|
|
1773
|
+
invocationArgs.push(undefined);
|
|
1774
|
+
}
|
|
1775
|
+
invocationArgs.push(remaining);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Intentionally do NOT append processRoot: projectRoot is the first positional argument.
|
|
1779
|
+
return invocationArgs;
|
|
1780
|
+
},
|
|
1781
|
+
"agents.main": buildProcessRootOnly,
|
|
1782
|
+
"agents.resolveAgentsRoot": buildProcessRootOnly,
|
|
1783
|
+
"agents.listAgents": buildProcessRootOnly,
|
|
1784
|
+
"projects.resolveProjectRoot": buildProcessRootOnly,
|
|
1785
|
+
"projects.listProjects": buildProcessRootOnly,
|
|
1786
|
+
"projects.listProjectDocs": buildProcessRootOnly,
|
|
1787
|
+
"projects.readProjectDoc": buildProcessRootOnly,
|
|
1788
|
+
"projects.main": buildProcessRootOnly,
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
const invokeTool = async (
|
|
1792
|
+
tool: ToolDefinition,
|
|
1793
|
+
payload: unknown,
|
|
1794
|
+
): Promise<unknown> => {
|
|
1795
|
+
const normalized = normalizePayload(payload);
|
|
1796
|
+
const { args, options } = coercePayloadForTool(tool.name, normalized);
|
|
1797
|
+
const invoke =
|
|
1798
|
+
toolInvocationPlans[tool.name] ??
|
|
1799
|
+
((rawArgs, rawOptions) => {
|
|
1800
|
+
const invocationArgs = [...rawArgs];
|
|
1801
|
+
if (Object.keys(rawOptions).length > 0) {
|
|
1802
|
+
invocationArgs.push(rawOptions);
|
|
1803
|
+
}
|
|
1804
|
+
return invocationArgs;
|
|
1805
|
+
});
|
|
1806
|
+
const invocationArgs = invoke(args, options);
|
|
1807
|
+
|
|
1808
|
+
return Promise.resolve(tool.method(...invocationArgs));
|
|
1809
|
+
};
|
|
310
1810
|
|
|
311
1811
|
export interface ExampleMcpServerOptions {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
1812
|
+
serverName?: string;
|
|
1813
|
+
serverVersion?: string;
|
|
1814
|
+
toolsPrefix?: string;
|
|
1815
|
+
allowedRootEndpoints?: string[];
|
|
1816
|
+
disableWrite?: boolean;
|
|
1817
|
+
enableIssues?: boolean;
|
|
1818
|
+
admin?: boolean;
|
|
1819
|
+
exposeUnprefixedToolAliases?: boolean;
|
|
319
1820
|
}
|
|
320
1821
|
|
|
321
1822
|
export type ExampleMcpServerInstance = {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
1823
|
+
api: ApiEndpoint;
|
|
1824
|
+
tools: ToolDefinition[];
|
|
1825
|
+
server: Server;
|
|
1826
|
+
run: () => Promise<Server>;
|
|
1827
|
+
};
|
|
327
1828
|
|
|
328
|
-
const READ_ONLY_TOOL_NAMES = new Set([
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
])
|
|
357
|
-
const isIssueTool = (toolName: string): boolean => ISSUE_TOOL_NAMES.has(toolName)
|
|
358
|
-
const ADMIN_TOOL_NAMES = new Set([
|
|
359
|
-
'projects.syncTasks',
|
|
360
|
-
'projects.clearIssues',
|
|
361
|
-
])
|
|
362
|
-
const isAdminTool = (toolName: string): boolean => ADMIN_TOOL_NAMES.has(toolName)
|
|
363
|
-
|
|
364
|
-
export const createExampleMcpServer = (options: ExampleMcpServerOptions = {}): ExampleMcpServerInstance => {
|
|
365
|
-
const api: ApiEndpoint = {
|
|
366
|
-
agents: agentsApi,
|
|
367
|
-
projects: projectsApi,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const allRoots = Object.keys(api) as Array<keyof ApiEndpoint>
|
|
371
|
-
const selectedRoots = (() => {
|
|
372
|
-
if (!options.allowedRootEndpoints || options.allowedRootEndpoints.length === 0) {
|
|
373
|
-
return allRoots
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const normalized = options.allowedRootEndpoints
|
|
377
|
-
.map((value) => value.trim())
|
|
378
|
-
.filter((value) => value.length > 0)
|
|
379
|
-
|
|
380
|
-
const unknown = normalized.filter((value) => !allRoots.includes(value as keyof ApiEndpoint))
|
|
381
|
-
if (unknown.length > 0) {
|
|
382
|
-
throw new Error(`Unknown root endpoints: ${unknown.join(', ')}. Allowed: ${allRoots.join(', ')}`)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return [...new Set(normalized)] as Array<keyof ApiEndpoint>
|
|
386
|
-
})()
|
|
387
|
-
|
|
388
|
-
const selectedTools = selectedRoots.flatMap((root) => collectTools(api[root], [root]))
|
|
389
|
-
const adminFilteredTools = Boolean(options.admin)
|
|
390
|
-
? selectedTools
|
|
391
|
-
: selectedTools.filter((tool) => !isAdminTool(tool.name))
|
|
392
|
-
const tools = options.disableWrite
|
|
393
|
-
? adminFilteredTools.filter((tool) => !isWriteCapableTool(tool.name) || (Boolean(options.enableIssues) && isIssueTool(tool.name)))
|
|
394
|
-
: adminFilteredTools
|
|
395
|
-
const prefix = options.toolsPrefix
|
|
396
|
-
const batchToolName = prefix ? `${prefix}.batch` : 'batch'
|
|
397
|
-
|
|
398
|
-
const server = new Server(
|
|
399
|
-
{
|
|
400
|
-
name: options.serverName ?? 'example-api',
|
|
401
|
-
version: options.serverVersion ?? '1.0.0',
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
capabilities: {
|
|
405
|
-
tools: {},
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
const toolByName = new Map<string, ToolDefinition>(tools.map((tool) => [buildToolName(tool, prefix), tool]))
|
|
411
|
-
|
|
412
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
413
|
-
tools: buildToolList(tools, batchToolName, prefix),
|
|
414
|
-
}))
|
|
415
|
-
|
|
416
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
417
|
-
const requestedName = request.params.name
|
|
418
|
-
|
|
419
|
-
if (requestedName === batchToolName) {
|
|
420
|
-
try {
|
|
421
|
-
const { calls, continueOnError } = normalizeBatchPayload(request.params.arguments)
|
|
422
|
-
const executions = calls.map(({ tool, args = [], options = {} }, index) => ({ tool, args, options, index }))
|
|
423
|
-
const results = await Promise.all(
|
|
424
|
-
executions.map(async ({ tool, args, options, index }) => {
|
|
425
|
-
const toolDefinition = toolByName.get(tool)
|
|
426
|
-
if (!toolDefinition) {
|
|
427
|
-
return {
|
|
428
|
-
index,
|
|
429
|
-
tool,
|
|
430
|
-
isError: true,
|
|
431
|
-
data: `Unknown tool: ${tool}`,
|
|
432
|
-
} as BatchResult
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
const data = await invokeTool(toolDefinition, { args, options })
|
|
437
|
-
return {
|
|
438
|
-
index,
|
|
439
|
-
tool,
|
|
440
|
-
isError: false,
|
|
441
|
-
data,
|
|
442
|
-
} as BatchResult
|
|
443
|
-
} catch (error) {
|
|
444
|
-
if (continueOnError) {
|
|
445
|
-
return {
|
|
446
|
-
index,
|
|
447
|
-
tool,
|
|
448
|
-
isError: true,
|
|
449
|
-
data: error instanceof Error ? error.message : String(error),
|
|
450
|
-
} as BatchResult
|
|
451
|
-
}
|
|
452
|
-
throw error
|
|
453
|
-
}
|
|
454
|
-
}),
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
return {
|
|
458
|
-
isError: results.some((result) => result.isError),
|
|
459
|
-
content: [
|
|
460
|
-
{
|
|
461
|
-
type: 'text',
|
|
462
|
-
text: JSON.stringify(results, null, 2),
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
}
|
|
466
|
-
} catch (error) {
|
|
467
|
-
return {
|
|
468
|
-
isError: true,
|
|
469
|
-
content: [
|
|
470
|
-
{
|
|
471
|
-
type: 'text',
|
|
472
|
-
text: error instanceof Error ? error.message : String(error),
|
|
473
|
-
},
|
|
474
|
-
],
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const tool = toolByName.get(requestedName)
|
|
480
|
-
|
|
481
|
-
if (!tool) {
|
|
482
|
-
throw new Error(`Unknown tool: ${requestedName}`)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
const data = await invokeTool(tool, request.params.arguments)
|
|
487
|
-
return {
|
|
488
|
-
content: [
|
|
489
|
-
{
|
|
490
|
-
type: 'text',
|
|
491
|
-
text: JSON.stringify(data, null, 2),
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
}
|
|
495
|
-
} catch (error) {
|
|
496
|
-
return {
|
|
497
|
-
isError: true,
|
|
498
|
-
content: [
|
|
499
|
-
{
|
|
500
|
-
type: 'text',
|
|
501
|
-
text: error instanceof Error ? error.message : String(error),
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
const run = async (): Promise<Server> => {
|
|
509
|
-
await server.connect(new StdioServerTransport())
|
|
510
|
-
return server
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return { api, tools, server, run }
|
|
514
|
-
}
|
|
1829
|
+
const READ_ONLY_TOOL_NAMES = new Set<string>([
|
|
1830
|
+
"agents.usage",
|
|
1831
|
+
"agents.resolveAgentsRoot",
|
|
1832
|
+
"agents.resolveAgentsRootFrom",
|
|
1833
|
+
"agents.listAgents",
|
|
1834
|
+
"agents.parseTargetSpec",
|
|
1835
|
+
"agents.resolveTargetFile",
|
|
1836
|
+
"agents.loadAgent",
|
|
1837
|
+
"agents.loadAgentPrompt",
|
|
1838
|
+
"net.curl",
|
|
1839
|
+
"net_curl",
|
|
1840
|
+
"projects.usage",
|
|
1841
|
+
"projects.resolveProjectRoot",
|
|
1842
|
+
"projects.listProjects",
|
|
1843
|
+
"projects.listProjectDocs",
|
|
1844
|
+
"projects.readProjectDoc",
|
|
1845
|
+
"projects.searchDocs",
|
|
1846
|
+
"projects.searchSpecs",
|
|
1847
|
+
"projects.resolveImplementationPlan",
|
|
1848
|
+
"projects.fetchGitTasks",
|
|
1849
|
+
"projects.readGitTask",
|
|
1850
|
+
"projects.parseProjectTargetSpec",
|
|
1851
|
+
"projects.resolveProjectTargetFile",
|
|
1852
|
+
"mcp.usage",
|
|
1853
|
+
"mcp.listTools",
|
|
1854
|
+
"mcp.describeTool",
|
|
1855
|
+
"mcp.search",
|
|
1856
|
+
]);
|
|
515
1857
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1858
|
+
const isWriteCapableTool = (toolName: string): boolean =>
|
|
1859
|
+
!READ_ONLY_TOOL_NAMES.has(toolName);
|
|
1860
|
+
const ISSUE_TOOL_NAMES = new Set<string>([
|
|
1861
|
+
"projects.fetchGitTasks",
|
|
1862
|
+
"projects.createGitIssue",
|
|
1863
|
+
"projects.readGitTask",
|
|
1864
|
+
"projects.writeGitTask",
|
|
1865
|
+
"projects.syncTasks",
|
|
1866
|
+
"projects.clearIssues",
|
|
1867
|
+
]);
|
|
1868
|
+
const isIssueTool = (toolName: string): boolean =>
|
|
1869
|
+
ISSUE_TOOL_NAMES.has(toolName);
|
|
1870
|
+
const ADMIN_TOOL_NAMES = new Set<string>([
|
|
1871
|
+
"projects.syncTasks",
|
|
1872
|
+
"projects.clearIssues",
|
|
1873
|
+
"agents.runAgent",
|
|
1874
|
+
"agents.main",
|
|
1875
|
+
"projects.main",
|
|
1876
|
+
]);
|
|
1877
|
+
const isAdminTool = (toolName: string): boolean =>
|
|
1878
|
+
ADMIN_TOOL_NAMES.has(toolName);
|
|
1879
|
+
|
|
1880
|
+
const getToolAccess = (toolName: string): ToolAccess => {
|
|
1881
|
+
if (isAdminTool(toolName)) return "admin";
|
|
1882
|
+
return isWriteCapableTool(toolName) ? "write" : "read";
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
const buildToolMeta = (tools: ToolDefinition[]): void => {
|
|
1886
|
+
for (const tool of tools) {
|
|
1887
|
+
TOOL_META[tool.name] = {
|
|
1888
|
+
access: getToolAccess(tool.name),
|
|
1889
|
+
category: tool.path[0],
|
|
1890
|
+
notes: isAdminTool(tool.name) ? " Admin-only." : undefined,
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
const escapeRegExp = (value: string): string =>
|
|
1896
|
+
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1897
|
+
|
|
1898
|
+
const OPENAI_SAFE_TOOL_ALIASES: Record<string, string> = {
|
|
1899
|
+
"net.curl": "net_curl",
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
const applyToolAliases = (tools: ToolDefinition[]): ToolDefinition[] => {
|
|
1903
|
+
const existing = new Set(tools.map((tool) => tool.name));
|
|
1904
|
+
const out: ToolDefinition[] = [...tools];
|
|
1905
|
+
|
|
1906
|
+
for (const tool of tools) {
|
|
1907
|
+
const aliasName = OPENAI_SAFE_TOOL_ALIASES[tool.name];
|
|
1908
|
+
if (!aliasName) continue;
|
|
1909
|
+
if (existing.has(aliasName)) continue;
|
|
1910
|
+
existing.add(aliasName);
|
|
1911
|
+
out.push({
|
|
1912
|
+
...tool,
|
|
1913
|
+
name: aliasName,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
return out;
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
const runWithConcurrency = async <T>(
|
|
1921
|
+
tasks: Array<() => Promise<T>>,
|
|
1922
|
+
maxConcurrency: number,
|
|
1923
|
+
): Promise<T[]> => {
|
|
1924
|
+
const results: T[] = new Array(tasks.length);
|
|
1925
|
+
let nextIndex = 0;
|
|
1926
|
+
|
|
1927
|
+
const worker = async (): Promise<void> => {
|
|
1928
|
+
while (true) {
|
|
1929
|
+
const index = nextIndex;
|
|
1930
|
+
nextIndex += 1;
|
|
1931
|
+
if (index >= tasks.length) return;
|
|
1932
|
+
results[index] = await tasks[index]();
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1936
|
+
const concurrency = Math.max(1, Math.min(maxConcurrency, tasks.length));
|
|
1937
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
1938
|
+
return results;
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
export const createExampleMcpServer = (
|
|
1942
|
+
options: ExampleMcpServerOptions = {},
|
|
1943
|
+
): ExampleMcpServerInstance => {
|
|
1944
|
+
let toolCatalog: unknown[] = [];
|
|
1945
|
+
|
|
1946
|
+
const parseString = (value: unknown): string | null => {
|
|
1947
|
+
if (typeof value !== "string") return null;
|
|
1948
|
+
const trimmed = value.trim();
|
|
1949
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
const parseBoolean = (value: unknown): boolean =>
|
|
1953
|
+
value === true || value === 1 || value === "1" || value === "true";
|
|
1954
|
+
|
|
1955
|
+
const parseStringArray = (value: unknown): string[] => {
|
|
1956
|
+
if (Array.isArray(value)) {
|
|
1957
|
+
return value
|
|
1958
|
+
.map((entry) => String(entry))
|
|
1959
|
+
.map((entry) => entry.trim())
|
|
1960
|
+
.filter((entry) => entry.length > 0);
|
|
1961
|
+
}
|
|
1962
|
+
const asString = parseString(value);
|
|
1963
|
+
return asString ? [asString] : [];
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
const ensureProjectName = (
|
|
1967
|
+
projectName: string | null,
|
|
1968
|
+
processRoot: string,
|
|
1969
|
+
): string => {
|
|
1970
|
+
if (projectName) return projectName;
|
|
1971
|
+
const projects = projectsApi.listProjects(processRoot);
|
|
1972
|
+
if (projects.length === 1) return projects[0];
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
`projectName is required. Available projects: ${projects.join(", ")}`,
|
|
1975
|
+
);
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
const mcpApi = {
|
|
1979
|
+
usage: () =>
|
|
1980
|
+
[
|
|
1981
|
+
"F0 MCP helper tools:",
|
|
1982
|
+
"- mcp.listTools: returns tool catalog with access + invocation hints",
|
|
1983
|
+
"- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
|
|
1984
|
+
"- mcp.search: LLM-friendly search over project docs/spec (local-first)",
|
|
1985
|
+
"",
|
|
1986
|
+
'Tip: Prefer mcp.search for "search spec/docs" requests.',
|
|
1987
|
+
].join("\n"),
|
|
1988
|
+
listTools: () => ({ tools: toolCatalog }),
|
|
1989
|
+
describeTool: (toolName: string) => {
|
|
1990
|
+
const normalized = typeof toolName === "string" ? toolName.trim() : "";
|
|
1991
|
+
if (!normalized) {
|
|
1992
|
+
throw new Error("toolName is required.");
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const matches = toolCatalog.filter(
|
|
1996
|
+
(entry) =>
|
|
1997
|
+
isRecord(entry) &&
|
|
1998
|
+
(entry.name === normalized ||
|
|
1999
|
+
(entry as any).unprefixedName === normalized),
|
|
2000
|
+
) as Array<Record<string, unknown>>;
|
|
2001
|
+
|
|
2002
|
+
if (matches.length === 0) {
|
|
2003
|
+
const needle = normalized.toLowerCase();
|
|
2004
|
+
const candidates = toolCatalog
|
|
2005
|
+
.filter(isRecord)
|
|
2006
|
+
.map((entry) => String((entry as any).name ?? ""))
|
|
2007
|
+
.filter((name) => name.length > 0);
|
|
2008
|
+
const suggestions = candidates
|
|
2009
|
+
.filter(
|
|
2010
|
+
(name) =>
|
|
2011
|
+
name.toLowerCase().includes(needle) ||
|
|
2012
|
+
needle.includes(name.toLowerCase()),
|
|
2013
|
+
)
|
|
2014
|
+
.slice(0, 8);
|
|
2015
|
+
const hint =
|
|
2016
|
+
suggestions.length > 0
|
|
2017
|
+
? ` Did you mean: ${suggestions.join(", ")}?`
|
|
2018
|
+
: "";
|
|
2019
|
+
throw new Error(`Unknown tool: ${normalized}.${hint}`);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const canonical = matches.find(
|
|
2023
|
+
(entry) => (entry as any).kind === "canonical",
|
|
2024
|
+
);
|
|
2025
|
+
return canonical ?? matches[0];
|
|
2026
|
+
},
|
|
2027
|
+
search: async (input: unknown) => {
|
|
2028
|
+
const payload = isRecord(input) ? input : {};
|
|
2029
|
+
const processRoot = (() => {
|
|
2030
|
+
const raw = parseString(payload.processRoot);
|
|
2031
|
+
return raw ? normalizeProcessRoot(raw) : process.cwd();
|
|
2032
|
+
})();
|
|
2033
|
+
|
|
2034
|
+
const sectionRaw = parseString(payload.section)?.toLowerCase();
|
|
2035
|
+
const section = sectionRaw === "docs" ? "docs" : "spec";
|
|
2036
|
+
|
|
2037
|
+
const pattern =
|
|
2038
|
+
parseString(payload.pattern) ??
|
|
2039
|
+
parseString(payload.query) ??
|
|
2040
|
+
parseString(payload.q);
|
|
2041
|
+
if (!pattern) {
|
|
2042
|
+
throw new Error("pattern is required (or query/q).");
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const rgArgs: string[] = [];
|
|
2046
|
+
if (parseBoolean(payload.ignoreCase)) rgArgs.push("--ignore-case");
|
|
2047
|
+
if (parseBoolean(payload.caseSensitive)) rgArgs.push("--case-sensitive");
|
|
2048
|
+
if (parseBoolean(payload.smartCase)) rgArgs.push("--smart-case");
|
|
2049
|
+
if (parseBoolean(payload.fixedStrings)) rgArgs.push("--fixed-strings");
|
|
2050
|
+
if (parseBoolean(payload.wordRegexp)) rgArgs.push("--word-regexp");
|
|
2051
|
+
if (parseBoolean(payload.includeHidden)) rgArgs.push("--hidden");
|
|
2052
|
+
if (parseBoolean(payload.filesWithMatches))
|
|
2053
|
+
rgArgs.push("--files-with-matches");
|
|
2054
|
+
if (parseBoolean(payload.filesWithoutMatch))
|
|
2055
|
+
rgArgs.push("--files-without-match");
|
|
2056
|
+
if (parseBoolean(payload.countOnly)) rgArgs.push("--count");
|
|
2057
|
+
if (parseBoolean(payload.onlyMatching)) rgArgs.push("--only-matching");
|
|
2058
|
+
|
|
2059
|
+
const maxCount =
|
|
2060
|
+
typeof payload.maxCount === "number"
|
|
2061
|
+
? payload.maxCount
|
|
2062
|
+
: Number(payload.maxCount);
|
|
2063
|
+
if (Number.isFinite(maxCount) && maxCount > 0) {
|
|
2064
|
+
rgArgs.push("--max-count", String(Math.floor(maxCount)));
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
for (const glob of parseStringArray(payload.globs)) {
|
|
2068
|
+
rgArgs.push("--glob", glob);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
rgArgs.push(pattern);
|
|
2072
|
+
const paths = parseStringArray(payload.paths);
|
|
2073
|
+
if (paths.length > 0) {
|
|
2074
|
+
rgArgs.push(...paths);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const projectName = ensureProjectName(
|
|
2078
|
+
parseString(payload.projectName),
|
|
2079
|
+
processRoot,
|
|
2080
|
+
);
|
|
2081
|
+
|
|
2082
|
+
const searchOptions: Record<string, unknown> = {
|
|
2083
|
+
processRoot,
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
const source = parseString(payload.source)?.toLowerCase();
|
|
2087
|
+
if (source) {
|
|
2088
|
+
searchOptions.source = source;
|
|
2089
|
+
}
|
|
2090
|
+
const ref = parseString(payload.ref);
|
|
2091
|
+
if (ref) {
|
|
2092
|
+
searchOptions.ref = ref;
|
|
2093
|
+
}
|
|
2094
|
+
if (parseBoolean(payload.refresh)) {
|
|
2095
|
+
searchOptions.refresh = true;
|
|
2096
|
+
}
|
|
2097
|
+
const cacheDir = parseString(payload.cacheDir);
|
|
2098
|
+
if (cacheDir) {
|
|
2099
|
+
searchOptions.cacheDir = cacheDir;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
return section === "docs"
|
|
2103
|
+
? await projectsApi.searchDocs(projectName, ...rgArgs, searchOptions)
|
|
2104
|
+
: await projectsApi.searchSpecs(projectName, ...rgArgs, searchOptions);
|
|
2105
|
+
},
|
|
2106
|
+
} satisfies ToolNamespace;
|
|
2107
|
+
|
|
2108
|
+
const api: ApiEndpoint = {
|
|
2109
|
+
agents: agentsApi,
|
|
2110
|
+
net: netApi,
|
|
2111
|
+
projects: projectsApi,
|
|
2112
|
+
mcp: mcpApi,
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
const allRoots = Object.keys(api) as Array<keyof ApiEndpoint>;
|
|
2116
|
+
const selectedRoots = (() => {
|
|
2117
|
+
if (
|
|
2118
|
+
!options.allowedRootEndpoints ||
|
|
2119
|
+
options.allowedRootEndpoints.length === 0
|
|
2120
|
+
) {
|
|
2121
|
+
return allRoots;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
const normalized = options.allowedRootEndpoints
|
|
2125
|
+
.map((value) => value.trim())
|
|
2126
|
+
.filter((value) => value.length > 0);
|
|
2127
|
+
|
|
2128
|
+
const unknown = normalized.filter(
|
|
2129
|
+
(value) => !allRoots.includes(value as keyof ApiEndpoint),
|
|
2130
|
+
);
|
|
2131
|
+
if (unknown.length > 0) {
|
|
2132
|
+
throw new Error(
|
|
2133
|
+
`Unknown root endpoints: ${unknown.join(", ")}. Allowed: ${allRoots.join(", ")}`,
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
return [...new Set(normalized)] as Array<keyof ApiEndpoint>;
|
|
2138
|
+
})();
|
|
2139
|
+
|
|
2140
|
+
const selectedTools = selectedRoots.flatMap((root) =>
|
|
2141
|
+
collectTools(api[root], [root]),
|
|
2142
|
+
);
|
|
2143
|
+
const selectedToolsWithAliases = applyToolAliases(selectedTools);
|
|
2144
|
+
buildToolMeta(selectedToolsWithAliases);
|
|
2145
|
+
const adminFilteredTools = Boolean(options.admin)
|
|
2146
|
+
? selectedToolsWithAliases
|
|
2147
|
+
: selectedToolsWithAliases.filter((tool) => !isAdminTool(tool.name));
|
|
2148
|
+
const tools = options.disableWrite
|
|
2149
|
+
? adminFilteredTools.filter(
|
|
2150
|
+
(tool) =>
|
|
2151
|
+
!isWriteCapableTool(tool.name) ||
|
|
2152
|
+
(Boolean(options.enableIssues) && isIssueTool(tool.name)),
|
|
2153
|
+
)
|
|
2154
|
+
: adminFilteredTools;
|
|
2155
|
+
const prefix = options.toolsPrefix;
|
|
2156
|
+
const exposeUnprefixedAliases = options.exposeUnprefixedToolAliases ?? true;
|
|
2157
|
+
const batchToolName = prefix ? `${prefix}.batch` : "batch";
|
|
2158
|
+
|
|
2159
|
+
toolCatalog = tools.flatMap((tool) => {
|
|
2160
|
+
const canonicalName = buildToolName(tool, prefix);
|
|
2161
|
+
const schema =
|
|
2162
|
+
TOOL_INPUT_SCHEMA_OVERRIDES[tool.name] ??
|
|
2163
|
+
defaultToolInputSchema(tool.name);
|
|
2164
|
+
const base = {
|
|
2165
|
+
canonicalName,
|
|
2166
|
+
unprefixedName: tool.name,
|
|
2167
|
+
access: getToolAccess(tool.name),
|
|
2168
|
+
category: TOOL_META[tool.name]?.category ?? tool.path[0],
|
|
2169
|
+
invocationPlan: getInvocationPlanName(tool.name),
|
|
2170
|
+
example: buildInvocationExample(tool.name),
|
|
2171
|
+
description: safeJsonStringify(schema),
|
|
2172
|
+
inputSchema: schema,
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
const out = [
|
|
2176
|
+
{
|
|
2177
|
+
name: canonicalName,
|
|
2178
|
+
kind: "canonical" as const,
|
|
2179
|
+
...base,
|
|
2180
|
+
},
|
|
2181
|
+
];
|
|
2182
|
+
|
|
2183
|
+
if (prefix && exposeUnprefixedAliases) {
|
|
2184
|
+
out.push({
|
|
2185
|
+
name: tool.name,
|
|
2186
|
+
kind: "alias" as const,
|
|
2187
|
+
aliasOf: canonicalName,
|
|
2188
|
+
...base,
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
return out;
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
toolCatalog.push({
|
|
2196
|
+
name: batchToolName,
|
|
2197
|
+
kind: "canonical" as const,
|
|
2198
|
+
canonicalName: batchToolName,
|
|
2199
|
+
unprefixedName: "batch",
|
|
2200
|
+
access: "write",
|
|
2201
|
+
category: "mcp",
|
|
2202
|
+
invocationPlan: "custom",
|
|
2203
|
+
example: {
|
|
2204
|
+
calls: [
|
|
2205
|
+
{ tool: prefix ? `${prefix}.projects.usage` : "projects.usage" },
|
|
2206
|
+
{
|
|
2207
|
+
tool: prefix
|
|
2208
|
+
? `${prefix}.projects.listProjects`
|
|
2209
|
+
: "projects.listProjects",
|
|
2210
|
+
options: { processRoot: "<repo-root>" },
|
|
2211
|
+
},
|
|
2212
|
+
],
|
|
2213
|
+
continueOnError: true,
|
|
2214
|
+
maxConcurrency: 4,
|
|
2215
|
+
},
|
|
2216
|
+
description: safeJsonStringify({
|
|
2217
|
+
type: "object",
|
|
2218
|
+
additionalProperties: true,
|
|
2219
|
+
properties: {
|
|
2220
|
+
calls: {
|
|
2221
|
+
type: "array",
|
|
2222
|
+
minItems: 1,
|
|
2223
|
+
items: { type: "object", additionalProperties: true },
|
|
2224
|
+
},
|
|
2225
|
+
continueOnError: { type: "boolean", default: false },
|
|
2226
|
+
maxConcurrency: { type: "integer", minimum: 1, default: 8 },
|
|
2227
|
+
},
|
|
2228
|
+
required: ["calls"],
|
|
2229
|
+
}),
|
|
2230
|
+
inputSchema: {
|
|
2231
|
+
type: "object",
|
|
2232
|
+
additionalProperties: true,
|
|
2233
|
+
properties: {
|
|
2234
|
+
calls: {
|
|
2235
|
+
type: "array",
|
|
2236
|
+
minItems: 1,
|
|
2237
|
+
items: { type: "object", additionalProperties: true },
|
|
2238
|
+
},
|
|
2239
|
+
continueOnError: { type: "boolean", default: false },
|
|
2240
|
+
maxConcurrency: { type: "integer", minimum: 1, default: 8 },
|
|
2241
|
+
},
|
|
2242
|
+
required: ["calls"],
|
|
2243
|
+
},
|
|
2244
|
+
});
|
|
2245
|
+
if (prefix && exposeUnprefixedAliases) {
|
|
2246
|
+
toolCatalog.push({
|
|
2247
|
+
name: "batch",
|
|
2248
|
+
kind: "alias" as const,
|
|
2249
|
+
aliasOf: batchToolName,
|
|
2250
|
+
canonicalName: batchToolName,
|
|
2251
|
+
unprefixedName: "batch",
|
|
2252
|
+
access: "write",
|
|
2253
|
+
category: "mcp",
|
|
2254
|
+
invocationPlan: "custom",
|
|
2255
|
+
example: {
|
|
2256
|
+
calls: [{ tool: batchToolName }],
|
|
2257
|
+
},
|
|
2258
|
+
description: safeJsonStringify({
|
|
2259
|
+
type: "object",
|
|
2260
|
+
additionalProperties: true,
|
|
2261
|
+
properties: {
|
|
2262
|
+
calls: {
|
|
2263
|
+
type: "array",
|
|
2264
|
+
minItems: 1,
|
|
2265
|
+
items: { type: "object", additionalProperties: true },
|
|
2266
|
+
},
|
|
2267
|
+
continueOnError: { type: "boolean", default: false },
|
|
2268
|
+
maxConcurrency: { type: "integer", minimum: 1, default: 8 },
|
|
2269
|
+
},
|
|
2270
|
+
required: ["calls"],
|
|
2271
|
+
}),
|
|
2272
|
+
inputSchema: {
|
|
2273
|
+
type: "object",
|
|
2274
|
+
additionalProperties: true,
|
|
2275
|
+
properties: {
|
|
2276
|
+
calls: {
|
|
2277
|
+
type: "array",
|
|
2278
|
+
minItems: 1,
|
|
2279
|
+
items: { type: "object", additionalProperties: true },
|
|
2280
|
+
},
|
|
2281
|
+
continueOnError: { type: "boolean", default: false },
|
|
2282
|
+
maxConcurrency: { type: "integer", minimum: 1, default: 8 },
|
|
2283
|
+
},
|
|
2284
|
+
required: ["calls"],
|
|
2285
|
+
},
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const server = new Server(
|
|
2290
|
+
{
|
|
2291
|
+
name: options.serverName ?? "example-api",
|
|
2292
|
+
version: options.serverVersion ?? "1.0.0",
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
capabilities: {
|
|
2296
|
+
tools: {},
|
|
2297
|
+
},
|
|
2298
|
+
},
|
|
2299
|
+
);
|
|
2300
|
+
|
|
2301
|
+
const toolByName = new Map<string, ToolDefinition>(
|
|
2302
|
+
tools.map((tool) => [buildToolName(tool, prefix), tool]),
|
|
2303
|
+
);
|
|
2304
|
+
const toolByUnprefixedName = new Map<string, ToolDefinition>(
|
|
2305
|
+
prefix ? tools.map((tool) => [tool.name, tool]) : [],
|
|
2306
|
+
);
|
|
2307
|
+
|
|
2308
|
+
const knownToolNames = (() => {
|
|
2309
|
+
const names = new Set<string>();
|
|
2310
|
+
for (const name of toolByName.keys()) names.add(name);
|
|
2311
|
+
if (prefix && exposeUnprefixedAliases) {
|
|
2312
|
+
for (const tool of tools) names.add(tool.name);
|
|
2313
|
+
names.add("batch");
|
|
2314
|
+
}
|
|
2315
|
+
names.add(batchToolName);
|
|
2316
|
+
return names;
|
|
2317
|
+
})();
|
|
2318
|
+
|
|
2319
|
+
const suggestToolNames = (requested: string): string[] => {
|
|
2320
|
+
const needle = requested.trim().toLowerCase();
|
|
2321
|
+
if (!needle) return [];
|
|
2322
|
+
|
|
2323
|
+
const score = (candidate: string): number => {
|
|
2324
|
+
const cand = candidate.toLowerCase();
|
|
2325
|
+
if (cand === needle) return 0;
|
|
2326
|
+
if (cand.includes(needle)) return 1;
|
|
2327
|
+
if (needle.includes(cand)) return 2;
|
|
2328
|
+
const needleLast = needle.split(".").pop() ?? needle;
|
|
2329
|
+
const candLast = cand.split(".").pop() ?? cand;
|
|
2330
|
+
if (needleLast && candLast === needleLast) return 3;
|
|
2331
|
+
if (candLast.includes(needleLast)) return 4;
|
|
2332
|
+
return 100;
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
return Array.from(knownToolNames)
|
|
2336
|
+
.map((name) => ({ name, s: score(name) }))
|
|
2337
|
+
.filter((entry) => entry.s < 100)
|
|
2338
|
+
.sort((a, b) => a.s - b.s || a.name.localeCompare(b.name))
|
|
2339
|
+
.slice(0, 8)
|
|
2340
|
+
.map((entry) => entry.name);
|
|
2341
|
+
};
|
|
2342
|
+
|
|
2343
|
+
const resolveTool = (name: string): ToolDefinition | null => {
|
|
2344
|
+
const direct = toolByName.get(name);
|
|
2345
|
+
if (direct) return direct;
|
|
2346
|
+
if (prefix) {
|
|
2347
|
+
const unprefixed = name.replace(
|
|
2348
|
+
new RegExp(`^${escapeRegExp(prefix)}\\.`),
|
|
2349
|
+
"",
|
|
2350
|
+
);
|
|
2351
|
+
return (
|
|
2352
|
+
toolByUnprefixedName.get(unprefixed) ??
|
|
2353
|
+
toolByName.get(`${prefix}.${unprefixed}`) ??
|
|
2354
|
+
null
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
return null;
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
const enrichToolError = (
|
|
2361
|
+
toolName: string,
|
|
2362
|
+
message: string,
|
|
2363
|
+
): { message: string; details?: unknown } => {
|
|
2364
|
+
const details: Record<string, unknown> = {};
|
|
2365
|
+
const unprefixedToolName = prefix
|
|
2366
|
+
? toolName.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), "")
|
|
2367
|
+
: toolName;
|
|
2368
|
+
|
|
2369
|
+
if (/Missing project name\./i.test(message)) {
|
|
2370
|
+
details.hint =
|
|
2371
|
+
"Provide projectName (recommended) or pass it as the first positional arg (args[0]).";
|
|
2372
|
+
details.suggestion =
|
|
2373
|
+
"Prefer mcp.search with { projectName, section, pattern }.";
|
|
2374
|
+
details.example = {
|
|
2375
|
+
tool: prefix ? `${prefix}.mcp.search` : "mcp.search",
|
|
2376
|
+
arguments: {
|
|
2377
|
+
projectName: "<project-name>",
|
|
2378
|
+
section: "spec",
|
|
2379
|
+
pattern: "<pattern>",
|
|
2380
|
+
},
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
if (
|
|
2385
|
+
/Project folder not found:/i.test(message) &&
|
|
2386
|
+
/projects[\\/].+projects[\\/]/i.test(message)
|
|
2387
|
+
) {
|
|
2388
|
+
details.hint =
|
|
2389
|
+
"You likely passed a project root as processRoot. processRoot should be the repo root containing /projects.";
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (/Missing search pattern\./i.test(message)) {
|
|
2393
|
+
details.hint =
|
|
2394
|
+
"Provide pattern/query (recommended) or a PATTERN as the first rg positional.";
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
if (Object.keys(details).length === 0) {
|
|
2398
|
+
return { message };
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
details.tool = toolName;
|
|
2402
|
+
details.inputSchema =
|
|
2403
|
+
TOOL_INPUT_SCHEMA_OVERRIDES[unprefixedToolName] ??
|
|
2404
|
+
defaultToolInputSchema(unprefixedToolName);
|
|
2405
|
+
details.invocationExample = buildInvocationExample(unprefixedToolName);
|
|
2406
|
+
return { message, details };
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2410
|
+
tools: buildToolList(tools, batchToolName, prefix, exposeUnprefixedAliases),
|
|
2411
|
+
}));
|
|
2412
|
+
|
|
2413
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2414
|
+
const requestedName = request.params.name;
|
|
2415
|
+
|
|
2416
|
+
const isBatchName =
|
|
2417
|
+
requestedName === batchToolName || (prefix && requestedName === "batch");
|
|
2418
|
+
if (isBatchName) {
|
|
2419
|
+
try {
|
|
2420
|
+
const { calls, continueOnError, maxConcurrency } =
|
|
2421
|
+
normalizeBatchPayload(request.params.arguments);
|
|
2422
|
+
const executions = calls.map(
|
|
2423
|
+
({ tool, args = [], options = {} }, index) => ({
|
|
2424
|
+
tool,
|
|
2425
|
+
args,
|
|
2426
|
+
options,
|
|
2427
|
+
index,
|
|
2428
|
+
}),
|
|
2429
|
+
);
|
|
2430
|
+
|
|
2431
|
+
const runCall = async ({
|
|
2432
|
+
tool,
|
|
2433
|
+
args,
|
|
2434
|
+
options,
|
|
2435
|
+
index,
|
|
2436
|
+
}: (typeof executions)[number]): Promise<BatchResult> => {
|
|
2437
|
+
const toolDefinition = resolveTool(tool);
|
|
2438
|
+
if (!toolDefinition) {
|
|
2439
|
+
const message = `Unknown tool: ${tool}`;
|
|
2440
|
+
if (!continueOnError) {
|
|
2441
|
+
throw new Error(message);
|
|
2442
|
+
}
|
|
2443
|
+
const suggestions = suggestToolNames(tool);
|
|
2444
|
+
return {
|
|
2445
|
+
index,
|
|
2446
|
+
tool,
|
|
2447
|
+
isError: true,
|
|
2448
|
+
data: { message, suggestions },
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
try {
|
|
2453
|
+
const data = await invokeTool(toolDefinition, { args, options });
|
|
2454
|
+
return { index, tool, isError: false, data };
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
const message =
|
|
2457
|
+
error instanceof Error ? error.message : String(error);
|
|
2458
|
+
if (!continueOnError) {
|
|
2459
|
+
throw new Error(message);
|
|
2460
|
+
}
|
|
2461
|
+
const enriched = enrichToolError(tool, message);
|
|
2462
|
+
return {
|
|
2463
|
+
index,
|
|
2464
|
+
tool,
|
|
2465
|
+
isError: true,
|
|
2466
|
+
data: { message: enriched.message, details: enriched.details },
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
const results = continueOnError
|
|
2472
|
+
? await runWithConcurrency(
|
|
2473
|
+
executions.map((execution) => () => runCall(execution)),
|
|
2474
|
+
maxConcurrency,
|
|
2475
|
+
)
|
|
2476
|
+
: await (async () => {
|
|
2477
|
+
const out: BatchResult[] = [];
|
|
2478
|
+
for (const execution of executions) {
|
|
2479
|
+
out.push(await runCall(execution));
|
|
2480
|
+
}
|
|
2481
|
+
return out;
|
|
2482
|
+
})();
|
|
2483
|
+
|
|
2484
|
+
const ordered = [...results].sort((a, b) => a.index - b.index);
|
|
2485
|
+
const hasErrors = ordered.some((result) => result.isError);
|
|
2486
|
+
return hasErrors
|
|
2487
|
+
? toolErr("One or more batch calls failed.", { results: ordered })
|
|
2488
|
+
: toolOk({ results: ordered });
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
return toolErr(error instanceof Error ? error.message : String(error));
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const tool = resolveTool(requestedName);
|
|
2495
|
+
|
|
2496
|
+
if (!tool) {
|
|
2497
|
+
const suggestions = suggestToolNames(requestedName);
|
|
2498
|
+
return toolErr(
|
|
2499
|
+
`Unknown tool: ${requestedName}`,
|
|
2500
|
+
suggestions.length > 0 ? { suggestions } : undefined,
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
try {
|
|
2505
|
+
const data = await invokeTool(tool, request.params.arguments);
|
|
2506
|
+
return toolOk(data);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2509
|
+
const enriched = enrichToolError(requestedName, message);
|
|
2510
|
+
return toolErr(enriched.message, enriched.details);
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
const run = async (): Promise<Server> => {
|
|
2515
|
+
await server.connect(new StdioServerTransport());
|
|
2516
|
+
return server;
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
return { api, tools, server, run };
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
export const runExampleMcpServer = async (
|
|
2523
|
+
options: ExampleMcpServerOptions = {},
|
|
2524
|
+
): Promise<Server> => {
|
|
2525
|
+
const instance = createExampleMcpServer(options);
|
|
2526
|
+
return instance.run();
|
|
2527
|
+
};
|
|
520
2528
|
|
|
521
|
-
export const normalizeToolCallNameForServer = (
|
|
522
|
-
|
|
2529
|
+
export const normalizeToolCallNameForServer = (
|
|
2530
|
+
prefix: string | undefined,
|
|
2531
|
+
toolName: string,
|
|
2532
|
+
): string =>
|
|
2533
|
+
prefix
|
|
2534
|
+
? toolName.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), "")
|
|
2535
|
+
: toolName;
|