@foundation0/api 1.1.2 → 1.1.4

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/mcp/server.ts CHANGED
@@ -1,1759 +1,2535 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
- import * as agentsApi from '../agents.ts'
5
- import * as projectsApi from '../projects.ts'
6
- import fs from 'node:fs'
7
- import path from 'node:path'
8
-
9
- type ApiMethod = (...args: unknown[]) => unknown
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";
12
+
13
+ type ApiMethod = (...args: unknown[]) => unknown;
10
14
  type ToolInvocationPayload = {
11
- args?: unknown[]
12
- options?: Record<string, unknown>
13
- [key: string]: unknown
14
- }
15
+ args?: unknown[];
16
+ options?: Record<string, unknown>;
17
+ [key: string]: unknown;
18
+ };
15
19
  type BatchToolCall = {
16
- tool: string
17
- args?: unknown[]
18
- options?: Record<string, unknown>
19
- [key: string]: unknown
20
- }
20
+ tool: string;
21
+ args?: unknown[];
22
+ options?: Record<string, unknown>;
23
+ [key: string]: unknown;
24
+ };
21
25
  type BatchToolCallPayload = {
22
- calls: BatchToolCall[]
23
- continueOnError: boolean
24
- maxConcurrency: number
25
- }
26
+ calls: BatchToolCall[];
27
+ continueOnError: boolean;
28
+ maxConcurrency: number;
29
+ };
26
30
  type BatchResult = {
27
- index: number
28
- tool: string
29
- isError: boolean
30
- data: unknown
31
- }
31
+ index: number;
32
+ tool: string;
33
+ isError: boolean;
34
+ data: unknown;
35
+ };
32
36
  type ToolDefinition = {
33
- name: string
34
- method: ApiMethod
35
- path: string[]
36
- }
37
+ name: string;
38
+ method: ApiMethod;
39
+ path: string[];
40
+ };
37
41
 
38
- type ToolNamespace = Record<string, unknown>
42
+ type ToolNamespace = Record<string, unknown>;
39
43
 
40
44
  type ApiEndpoint = {
41
- agents: ToolNamespace
42
- projects: ToolNamespace
43
- mcp: ToolNamespace
44
- }
45
+ agents: ToolNamespace;
46
+ net: ToolNamespace;
47
+ projects: ToolNamespace;
48
+ mcp: ToolNamespace;
49
+ };
45
50
 
46
51
  const isRecord = (value: unknown): value is Record<string, unknown> =>
47
- typeof value === 'object' && value !== null && !Array.isArray(value)
52
+ typeof value === "object" && value !== null && !Array.isArray(value);
48
53
 
49
54
  const parseBooleanish = (value: unknown): boolean | null => {
50
- if (value === true || value === false) return value
51
- if (value === 1 || value === 0) return Boolean(value)
52
- if (typeof value !== 'string') return null
53
- const normalized = value.trim().toLowerCase()
54
- if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
55
- if (['0', 'false', 'no', 'off'].includes(normalized)) return false
56
- return null
57
- }
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
+ };
58
63
 
59
64
  const parsePositiveInteger = (value: unknown): number | null => {
60
- const numeric = typeof value === 'string' && value.trim() !== ''
61
- ? Number(value)
62
- : (typeof value === 'number' ? value : NaN)
65
+ const numeric =
66
+ typeof value === "string" && value.trim() !== ""
67
+ ? Number(value)
68
+ : typeof value === "number"
69
+ ? value
70
+ : NaN;
63
71
 
64
- if (!Number.isInteger(numeric) || numeric <= 0) return null
65
- return numeric
66
- }
72
+ if (!Number.isInteger(numeric) || numeric <= 0) return null;
73
+ return numeric;
74
+ };
67
75
 
68
76
  const safeJsonStringify = (value: unknown): string => {
69
- try {
70
- return JSON.stringify(value, (_key, v) => (typeof v === 'bigint' ? v.toString() : v), 2)
71
- } catch (error) {
72
- const message = error instanceof Error ? error.message : String(error)
73
- return JSON.stringify({ ok: false, error: { message, note: 'Failed to JSON stringify tool result.' } }, null, 2)
74
- }
75
- }
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
+ };
76
95
 
77
96
  type ToolEnvelope =
78
- | { ok: true; result: unknown }
79
- | { ok: false; error: { message: string; details?: unknown } }
97
+ | { ok: true; result: unknown }
98
+ | { ok: false; error: { message: string; details?: unknown } };
80
99
 
81
100
  const toolOk = (result: unknown) => ({
82
- isError: false,
83
- content: [{ type: 'text', text: safeJsonStringify({ ok: true, result } satisfies ToolEnvelope) }],
84
- })
101
+ isError: false,
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: safeJsonStringify({ ok: true, result } satisfies ToolEnvelope),
106
+ },
107
+ ],
108
+ });
85
109
 
86
110
  const toolErr = (message: string, details?: unknown) => ({
87
- isError: true,
88
- content: [{ type: 'text', text: safeJsonStringify({ ok: false, error: { message, details } } satisfies ToolEnvelope) }],
89
- })
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
+ });
90
122
 
91
123
  const isDir = (candidate: string): boolean => {
92
- try {
93
- return fs.statSync(candidate).isDirectory()
94
- } catch {
95
- return false
96
- }
97
- }
124
+ try {
125
+ return fs.statSync(candidate).isDirectory();
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
98
130
 
99
131
  const looksLikeRepoRoot = (candidate: string): boolean =>
100
- isDir(path.join(candidate, 'projects')) && isDir(path.join(candidate, 'api'))
132
+ isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
101
133
 
102
134
  const normalizeProcessRoot = (raw: string): string => {
103
- const resolved = path.resolve(raw)
104
- if (looksLikeRepoRoot(resolved)) return resolved
105
-
106
- // Common mistake: passing a project root like ".../projects/adl" as processRoot.
107
- // Try to find the containing repo root by walking up a few levels.
108
- let current = resolved
109
- for (let depth = 0; depth < 8; depth += 1) {
110
- const parent = path.dirname(current)
111
- if (parent === current) break
112
- if (looksLikeRepoRoot(parent)) return parent
113
- current = parent
114
- }
115
-
116
- const parts = resolved.split(path.sep).filter((part) => part.length > 0)
117
- const projectsIndex = parts.lastIndexOf('projects')
118
- if (projectsIndex >= 0) {
119
- const candidate = parts.slice(0, projectsIndex).join(path.sep)
120
- if (candidate && looksLikeRepoRoot(candidate)) return candidate
121
- }
122
-
123
- return resolved
124
- }
125
-
126
- const normalizeProcessRootOption = (options: Record<string, unknown>): Record<string, unknown> => {
127
- const raw = options.processRoot
128
- if (typeof raw !== 'string' || raw.trim().length === 0) return options
129
- const normalized = normalizeProcessRoot(raw.trim())
130
- if (normalized === raw) return options
131
- return { ...options, processRoot: normalized }
132
- }
133
-
134
- type NormalizedToolPayload = { args: unknown[]; options: Record<string, unknown> }
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
+ };
135
172
 
136
173
  const parseStringish = (value: unknown): string | null => {
137
- if (typeof value !== 'string') return null
138
- const trimmed = value.trim()
139
- return trimmed.length > 0 ? trimmed : null
140
- }
174
+ if (typeof value !== "string") return null;
175
+ const trimmed = value.trim();
176
+ return trimmed.length > 0 ? trimmed : null;
177
+ };
141
178
 
142
179
  const parseStringArrayish = (value: unknown): string[] => {
143
- if (Array.isArray(value)) {
144
- return value.map((entry) => String(entry)).map((entry) => entry.trim()).filter((entry) => entry.length > 0)
145
- }
146
- const asString = parseStringish(value)
147
- return asString ? [asString] : []
148
- }
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
+ };
149
189
 
150
190
  const popOption = (options: Record<string, unknown>, key: string): unknown => {
151
- const value = options[key]
152
- delete options[key]
153
- return value
154
- }
155
-
156
- const popStringOption = (options: Record<string, unknown>, ...keys: string[]): string | null => {
157
- for (const key of keys) {
158
- const value = options[key]
159
- const parsed = parseStringish(value)
160
- if (parsed) {
161
- delete options[key]
162
- return parsed
163
- }
164
- }
165
- return null
166
- }
167
-
168
- const popStringArrayOption = (options: Record<string, unknown>, ...keys: string[]): string[] => {
169
- for (const key of keys) {
170
- const value = options[key]
171
- const parsed = parseStringArrayish(value)
172
- if (parsed.length > 0) {
173
- delete options[key]
174
- return parsed
175
- }
176
- }
177
- return []
178
- }
179
-
180
- const popBooleanOption = (options: Record<string, unknown>, ...keys: string[]): boolean | null => {
181
- for (const key of keys) {
182
- const value = options[key]
183
- const parsed = parseBooleanish(value)
184
- if (parsed !== null) {
185
- delete options[key]
186
- return parsed
187
- }
188
- }
189
- return null
190
- }
191
-
192
- const popIntegerOption = (options: Record<string, unknown>, ...keys: string[]): number | null => {
193
- for (const key of keys) {
194
- const value = options[key]
195
- const parsed = parsePositiveInteger(value)
196
- if (parsed !== null) {
197
- delete options[key]
198
- return parsed
199
- }
200
- }
201
- return null
202
- }
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
+ };
203
255
 
204
256
  const normalizePayload = (payload: unknown): NormalizedToolPayload => {
205
- if (!isRecord(payload)) {
206
- return {
207
- args: [],
208
- options: {},
209
- }
210
- }
211
-
212
- const explicitArgs = Array.isArray(payload.args) ? payload.args : undefined
213
- const explicitOptions = isRecord(payload.options) ? payload.options : undefined
214
-
215
- const args = explicitArgs ? [...explicitArgs] : []
216
- const options: Record<string, unknown> = explicitOptions ? { ...explicitOptions } : {}
217
-
218
- for (const [key, value] of Object.entries(payload)) {
219
- if (key === 'args' || key === 'options') {
220
- continue
221
- }
222
-
223
- if (value !== undefined) {
224
- options[key] = value
225
- }
226
- }
227
-
228
- return {
229
- args,
230
- options: normalizeProcessRootOption(options),
231
- }
232
- }
233
-
234
- const coercePayloadForTool = (toolName: string, input: NormalizedToolPayload): NormalizedToolPayload => {
235
- const args = [...input.args]
236
- const options: Record<string, unknown> = { ...input.options }
237
-
238
- const ensureArg0 = (value: unknown) => {
239
- if (args.length > 0) return
240
- if (value !== undefined) {
241
- args.push(value)
242
- }
243
- }
244
-
245
- const ensureArgs = (...values: Array<unknown>) => {
246
- while (args.length < values.length) {
247
- const next = values[args.length]
248
- if (next === undefined || next === null) break
249
- args.push(next)
250
- }
251
- }
252
-
253
- const coerceProjectName = (): void => {
254
- if (args.length > 0 && typeof args[0] === 'string' && args[0].trim().length > 0) return
255
- const name = popStringOption(options, 'projectName', 'project')
256
- if (name) {
257
- if (args.length === 0) args.push(name)
258
- else args[0] = name
259
- }
260
- }
261
-
262
- const coerceProjectSearch = (): void => {
263
- coerceProjectName()
264
-
265
- // If caller already provided [projectName, pattern, ...], keep it.
266
- if (args.length >= 2) return
267
-
268
- const pattern =
269
- popStringOption(options, 'pattern', 'query', 'q') ?? null
270
- const paths = popStringArrayOption(options, 'paths')
271
- const globs = popStringArrayOption(options, 'globs')
272
- const ref = popStringOption(options, 'ref')
273
- const source = popStringOption(options, 'source')
274
-
275
- if (source) options.source = source
276
- if (ref) options.ref = ref
277
-
278
- const refresh = popBooleanOption(options, 'refresh')
279
- if (refresh !== null) options.refresh = refresh
280
- const cacheDir = popStringOption(options, 'cacheDir')
281
- if (cacheDir) options.cacheDir = cacheDir
282
-
283
- const owner = popStringOption(options, 'owner')
284
- if (owner) options.owner = owner
285
- const repo = popStringOption(options, 'repo')
286
- if (repo) options.repo = repo
287
-
288
- if (!pattern) return
289
-
290
- const rgArgs: string[] = []
291
- const addFlag = (key: string, flag: string) => {
292
- const raw = popBooleanOption(options, key)
293
- if (raw === true) rgArgs.push(flag)
294
- }
295
-
296
- addFlag('ignoreCase', '--ignore-case')
297
- addFlag('caseSensitive', '--case-sensitive')
298
- addFlag('smartCase', '--smart-case')
299
- addFlag('fixedStrings', '--fixed-strings')
300
- addFlag('wordRegexp', '--word-regexp')
301
- addFlag('includeHidden', '--hidden')
302
- addFlag('filesWithMatches', '--files-with-matches')
303
- addFlag('filesWithoutMatch', '--files-without-match')
304
- addFlag('countOnly', '--count')
305
- addFlag('onlyMatching', '--only-matching')
306
-
307
- const maxCount = popIntegerOption(options, 'maxCount')
308
- if (maxCount !== null) {
309
- rgArgs.push('--max-count', String(maxCount))
310
- }
311
-
312
- for (const glob of globs) {
313
- rgArgs.push('--glob', glob)
314
- }
315
-
316
- rgArgs.push(pattern, ...(paths.length > 0 ? paths : ['.']))
317
-
318
- if (args.length === 0) {
319
- // Can't build without projectName; leave for underlying error.
320
- return
321
- }
322
-
323
- if (args.length === 1) {
324
- args.push(...rgArgs)
325
- }
326
- }
327
-
328
- switch (toolName) {
329
- case 'projects.listProjects': {
330
- // No positional args. processRoot is handled via options.processRoot + buildProcessRootOnly.
331
- break
332
- }
333
- case 'projects.resolveProjectRoot':
334
- case 'projects.listProjectDocs':
335
- case 'projects.fetchGitTasks': {
336
- coerceProjectName()
337
- break
338
- }
339
- case 'projects.readProjectDoc': {
340
- coerceProjectName()
341
- if (args.length >= 2) break
342
- const requestPath = popStringOption(options, 'requestPath', 'path', 'docPath')
343
- if (requestPath) {
344
- ensureArgs(args[0] ?? undefined, requestPath)
345
- }
346
- break
347
- }
348
- case 'projects.searchDocs':
349
- case 'projects.searchSpecs': {
350
- coerceProjectSearch()
351
- break
352
- }
353
- case 'projects.parseProjectTargetSpec': {
354
- if (args.length >= 1) break
355
- const spec = popStringOption(options, 'spec', 'target')
356
- if (spec) {
357
- args.push(spec)
358
- }
359
- break
360
- }
361
- case 'projects.resolveProjectTargetFile': {
362
- if (args.length >= 2) break
363
- const targetDir = popStringOption(options, 'targetDir', 'dir')
364
- const spec = options.spec
365
- if (targetDir) {
366
- if (spec !== undefined) {
367
- delete options.spec
368
- }
369
- if (args.length === 0) args.push(targetDir)
370
- if (args.length === 1 && spec !== undefined) args.push(spec)
371
- }
372
-
373
- const latest = popBooleanOption(options, 'latest')
374
- if (latest !== null) {
375
- options.latest = latest
376
- }
377
- break
378
- }
379
- case 'projects.resolveImplementationPlan': {
380
- if (args.length >= 1) break
381
- const projectRoot = popStringOption(options, 'projectRoot')
382
- const inputFile = popStringOption(options, 'inputFile')
383
- const requireActive = popBooleanOption(options, 'requireActive')
384
- if (projectRoot) {
385
- args.push(projectRoot)
386
- if (inputFile) args.push(inputFile)
387
- }
388
- if (requireActive !== null) {
389
- options.requireActive = requireActive
390
- }
391
- break
392
- }
393
- case 'projects.readGitTask':
394
- case 'projects.writeGitTask': {
395
- coerceProjectName()
396
- if (args.length >= 2) break
397
- const taskRef = popStringOption(options, 'taskRef', 'ref', 'issue', 'issueNumber', 'taskId')
398
- if (taskRef && args.length >= 1) {
399
- ensureArgs(args[0], taskRef)
400
- }
401
- break
402
- }
403
- default:
404
- break
405
- }
406
-
407
- return { args, options: normalizeProcessRootOption(options) }
408
- }
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
+ };
409
485
 
410
486
  const normalizeBatchToolCall = (
411
- call: unknown,
412
- index: number,
487
+ call: unknown,
488
+ index: number,
413
489
  ): { tool: string; payload: ToolInvocationPayload } => {
414
- if (!isRecord(call)) {
415
- throw new Error(`Invalid batch call at index ${index}: expected object`)
416
- }
417
-
418
- const tool = typeof call.tool === 'string' ? call.tool.trim() : ''
419
- if (!tool) {
420
- throw new Error(`Invalid batch call at index ${index}: missing "tool"`)
421
- }
422
-
423
- const args = Array.isArray(call.args) ? call.args : []
424
- const { options, ...extras } = call
425
- const normalized: ToolInvocationPayload = {
426
- args,
427
- options: isRecord(options) ? options : {},
428
- }
429
-
430
- for (const [key, value] of Object.entries(extras)) {
431
- if (key === 'tool' || key === 'args' || key === 'options') {
432
- continue
433
- }
434
- if (value !== undefined) {
435
- normalized.options[key] = value
436
- }
437
- }
438
-
439
- return {
440
- tool,
441
- payload: normalized,
442
- }
443
- }
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
+ };
444
520
 
445
521
  const normalizeBatchPayload = (payload: unknown): BatchToolCallPayload => {
446
- if (!isRecord(payload)) {
447
- throw new Error('Batch tool call requires an object payload')
448
- }
449
-
450
- if (!Array.isArray(payload.calls)) {
451
- throw new Error('Batch tool call requires a "calls" array')
452
- }
453
-
454
- const calls = payload.calls.map((call, index) => normalizeBatchToolCall(call, index))
455
- const continueOnError = (() => {
456
- if (payload.continueOnError === undefined) return false
457
- const parsed = parseBooleanish(payload.continueOnError)
458
- if (parsed === null) {
459
- throw new Error('"continueOnError" must be a boolean or boolean-like string (true/false, 1/0).')
460
- }
461
- return parsed
462
- })()
463
- const maxConcurrency = (() => {
464
- if (payload.maxConcurrency === undefined) return 8
465
- const parsed = parsePositiveInteger(payload.maxConcurrency)
466
- if (!parsed) {
467
- throw new Error('"maxConcurrency" must be a positive integer.')
468
- }
469
- return parsed
470
- })()
471
-
472
- return {
473
- calls: calls.map(({ tool, payload }) => ({
474
- tool,
475
- ...payload,
476
- })),
477
- continueOnError,
478
- maxConcurrency,
479
- }
480
- }
481
-
482
- const collectTools = (api: ToolNamespace, namespace: string[], path: string[] = []): ToolDefinition[] => {
483
- const tools: ToolDefinition[] = []
484
- const currentPath = [...path, ...namespace]
485
-
486
- for (const [segment, value] of Object.entries(api)) {
487
- if (typeof value !== 'function') {
488
- continue
489
- }
490
-
491
- tools.push({
492
- name: [...currentPath, segment].join('.'),
493
- method: value as ApiMethod,
494
- path: [...currentPath, segment],
495
- })
496
- }
497
-
498
- return tools
499
- }
522
+ if (!isRecord(payload)) {
523
+ throw new Error("Batch tool call requires an object payload");
524
+ }
525
+
526
+ if (!Array.isArray(payload.calls)) {
527
+ throw new Error('Batch tool call requires a "calls" array');
528
+ }
529
+
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
+ })();
551
+
552
+ return {
553
+ calls: calls.map(({ tool, payload }) => ({
554
+ tool,
555
+ ...payload,
556
+ })),
557
+ continueOnError,
558
+ maxConcurrency,
559
+ };
560
+ };
561
+
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
+ };
500
584
 
501
585
  const buildToolName = (tool: ToolDefinition, prefix?: string): string =>
502
- prefix ? `${prefix}.${tool.name}` : tool.name
586
+ prefix ? `${prefix}.${tool.name}` : tool.name;
503
587
 
504
- type ToolAccess = 'read' | 'write' | 'admin'
588
+ type ToolAccess = "read" | "write" | "admin";
505
589
  type ToolMeta = {
506
- access: ToolAccess
507
- category?: string
508
- notes?: string
509
- }
590
+ access: ToolAccess;
591
+ category?: string;
592
+ notes?: string;
593
+ };
510
594
 
511
- const TOOL_META: Record<string, ToolMeta> = {}
595
+ const TOOL_META: Record<string, ToolMeta> = {};
512
596
 
513
597
  const TOOL_REQUIRED_ARGS: Record<string, string[]> = {
514
- 'agents.createAgent': ['<agent-name>'],
515
- 'agents.setActive': ['<agent-name>', '</file-path>'],
516
- 'agents.loadAgent': ['<agent-name>'],
517
- 'agents.loadAgentPrompt': ['<agent-name>'],
518
- 'agents.runAgent': ['<agent-name>'],
519
- 'projects.resolveProjectRoot': ['<project-name>'],
520
- 'projects.listProjectDocs': ['<project-name>'],
521
- 'projects.readProjectDoc': ['<project-name>', '</doc-path>'],
522
- 'projects.searchDocs': ['<project-name>', '<pattern>', '[path...]'],
523
- 'projects.searchSpecs': ['<project-name>', '<pattern>', '[path...]'],
524
- 'projects.generateSpec': ['<project-name>'],
525
- 'projects.setActive': ['<project-name>', '</file-path>'],
526
- 'projects.fetchGitTasks': ['<project-name>'],
527
- 'projects.readGitTask': ['<project-name>', '<issue-number|task-id>'],
528
- 'projects.writeGitTask': ['<project-name>', '<issue-number|task-id>'],
529
- }
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
+ };
530
615
 
531
616
  const TOOL_DEFAULT_OPTIONS: Record<string, Record<string, unknown>> = {
532
- 'projects.searchDocs': { source: 'auto' },
533
- 'projects.searchSpecs': { source: 'auto' },
534
- }
617
+ "projects.searchDocs": { source: "auto" },
618
+ "projects.searchSpecs": { source: "auto" },
619
+ };
535
620
 
536
621
  const TOOL_USAGE_HINTS: Record<string, string> = {
537
- 'projects.searchDocs': ' Usage: args=[projectName, pattern, ...paths]. Options include source/local cache controls.',
538
- 'projects.searchSpecs': ' Usage: args=[projectName, pattern, ...paths]. Options include source/local cache controls.',
539
- 'projects.setActive': ' Usage: args=[projectName, /file-path]. Use options.latest=true to auto-pick latest version.',
540
- 'agents.setActive': ' Usage: args=[agentName, /file-path]. Use options.latest=true to auto-pick latest version.',
541
- }
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;
542
774
 
543
775
  const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
544
- 'projects.usage': {
545
- type: 'object',
546
- additionalProperties: true,
547
- properties: {
548
- args: {
549
- type: 'array',
550
- description: 'Unused. This tool takes no arguments.',
551
- minItems: 0,
552
- maxItems: 0,
553
- items: {},
554
- },
555
- options: {
556
- type: 'object',
557
- description: 'Unused.',
558
- additionalProperties: true,
559
- },
560
- processRoot: {
561
- type: 'string',
562
- description: 'Unused.',
563
- },
564
- },
565
- },
566
- 'projects.listProjects': {
567
- type: 'object',
568
- additionalProperties: true,
569
- properties: {
570
- processRoot: {
571
- type: 'string',
572
- description: 'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
573
- },
574
- },
575
- required: [],
576
- },
577
- 'projects.resolveProjectRoot': {
578
- type: 'object',
579
- additionalProperties: true,
580
- properties: {
581
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
582
- processRoot: {
583
- type: 'string',
584
- description: 'Repo root containing /projects and /agents. If omitted, uses server cwd.',
585
- },
586
- args: { type: 'array', description: 'Legacy positional args (projectName).', items: {} },
587
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
588
- },
589
- required: ['projectName'],
590
- },
591
- 'projects.listProjectDocs': {
592
- type: 'object',
593
- additionalProperties: true,
594
- properties: {
595
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
596
- processRoot: { type: 'string', description: 'Repo root containing /projects and /agents.' },
597
- args: { type: 'array', description: 'Legacy positional args (projectName).', items: {} },
598
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
599
- },
600
- required: ['projectName'],
601
- },
602
- 'projects.readProjectDoc': {
603
- type: 'object',
604
- additionalProperties: true,
605
- properties: {
606
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
607
- requestPath: { type: 'string', description: 'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).', },
608
- processRoot: { type: 'string', description: 'Repo root containing /projects and /agents.' },
609
- args: { type: 'array', description: 'Legacy positional args (projectName, requestPath).', items: {} },
610
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
611
- },
612
- required: ['projectName', 'requestPath'],
613
- },
614
- 'projects.searchDocs': {
615
- type: 'object',
616
- additionalProperties: true,
617
- properties: {
618
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
619
- pattern: { type: 'string', description: 'Search pattern (like rg PATTERN).', },
620
- query: { type: 'string', description: 'Alias of pattern.' },
621
- q: { type: 'string', description: 'Alias of pattern.' },
622
- paths: { type: 'array', items: { type: 'string' }, description: 'Paths inside docs/ to search. Defaults to [\".\"].' },
623
- globs: { type: 'array', items: { type: 'string' }, description: 'Glob filters (like rg --glob).' },
624
- ignoreCase: { type: 'boolean' },
625
- caseSensitive: { type: 'boolean' },
626
- smartCase: { type: 'boolean' },
627
- fixedStrings: { type: 'boolean' },
628
- wordRegexp: { type: 'boolean' },
629
- maxCount: { type: 'integer', minimum: 1 },
630
- includeHidden: { type: 'boolean' },
631
- filesWithMatches: { type: 'boolean' },
632
- filesWithoutMatch: { type: 'boolean' },
633
- countOnly: { type: 'boolean' },
634
- onlyMatching: { type: 'boolean' },
635
- ref: { type: 'string', description: 'Optional git ref for remote search.' },
636
- source: { type: 'string', enum: ['local', 'gitea', 'auto'], description: 'local=filesystem, gitea=remote, auto=local-first.' },
637
- refresh: { type: 'boolean', description: 'Refresh cached search corpus.' },
638
- cacheDir: { type: 'string', description: 'Optional override cache directory.' },
639
- owner: { type: 'string', description: 'Remote owner override for gitea mode.' },
640
- repo: { type: 'string', description: 'Remote repo override for gitea mode.' },
641
- processRoot: { type: 'string', description: 'Repo root containing /projects and /agents.' },
642
- args: { type: 'array', description: 'Legacy rg-like args (projectName, PATTERN, [PATH...]).', items: {} },
643
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
644
- },
645
- required: ['projectName'],
646
- },
647
- 'projects.searchSpecs': {
648
- type: 'object',
649
- additionalProperties: true,
650
- properties: {
651
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
652
- pattern: { type: 'string', description: 'Search pattern (like rg PATTERN).', },
653
- query: { type: 'string', description: 'Alias of pattern.' },
654
- q: { type: 'string', description: 'Alias of pattern.' },
655
- paths: { type: 'array', items: { type: 'string' }, description: 'Paths inside spec/ to search. Defaults to [\".\"].' },
656
- globs: { type: 'array', items: { type: 'string' }, description: 'Glob filters (like rg --glob).' },
657
- ignoreCase: { type: 'boolean' },
658
- caseSensitive: { type: 'boolean' },
659
- smartCase: { type: 'boolean' },
660
- fixedStrings: { type: 'boolean' },
661
- wordRegexp: { type: 'boolean' },
662
- maxCount: { type: 'integer', minimum: 1 },
663
- includeHidden: { type: 'boolean' },
664
- filesWithMatches: { type: 'boolean' },
665
- filesWithoutMatch: { type: 'boolean' },
666
- countOnly: { type: 'boolean' },
667
- onlyMatching: { type: 'boolean' },
668
- ref: { type: 'string', description: 'Optional git ref for remote search.' },
669
- source: { type: 'string', enum: ['local', 'gitea', 'auto'], description: 'local=filesystem, gitea=remote, auto=local-first.' },
670
- refresh: { type: 'boolean', description: 'Refresh cached search corpus.' },
671
- cacheDir: { type: 'string', description: 'Optional override cache directory.' },
672
- owner: { type: 'string', description: 'Remote owner override for gitea mode.' },
673
- repo: { type: 'string', description: 'Remote repo override for gitea mode.' },
674
- processRoot: { type: 'string', description: 'Repo root containing /projects and /agents.' },
675
- args: { type: 'array', description: 'Legacy rg-like args (projectName, PATTERN, [PATH...]).', items: {} },
676
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
677
- },
678
- required: ['projectName'],
679
- },
680
- 'projects.parseProjectTargetSpec': {
681
- type: 'object',
682
- additionalProperties: true,
683
- properties: {
684
- spec: { type: 'string', description: 'Target spec string like \"/implementation-plan.v0.0.1\" or \"spec/README.md\".' },
685
- target: { type: 'string', description: 'Alias of spec.' },
686
- args: { type: 'array', description: 'Legacy positional args (spec).', items: {} },
687
- options: { type: 'object', description: 'Legacy named options.', additionalProperties: true },
688
- },
689
- anyOf: [{ required: ['spec'] }, { required: ['target'] }],
690
- },
691
- 'projects.resolveProjectTargetFile': {
692
- type: 'object',
693
- additionalProperties: true,
694
- properties: {
695
- targetDir: { type: 'string', description: 'Directory to scan.' },
696
- spec: { type: 'object', description: 'Output of projects.parseProjectTargetSpec().', additionalProperties: true },
697
- latest: { type: 'boolean', description: 'If true and no version given, select latest versioned file.' },
698
- args: { type: 'array', description: 'Legacy positional args (targetDir, spec).', items: {} },
699
- options: { type: 'object', description: 'Legacy named options (e.g. {latest:true}).', additionalProperties: true },
700
- },
701
- required: ['targetDir', 'spec'],
702
- },
703
- 'projects.resolveImplementationPlan': {
704
- type: 'object',
705
- additionalProperties: true,
706
- properties: {
707
- projectRoot: { type: 'string', description: 'Absolute project directory (e.g. C:/.../projects/adl).' },
708
- inputFile: { type: 'string', description: 'Optional file within docs/ (or absolute) to select plan.' },
709
- requireActive: { type: 'boolean', description: 'If true, require implementation-plan.active.* to exist.' },
710
- args: { type: 'array', description: 'Legacy positional args (projectRoot, inputFile?).', items: {} },
711
- options: { type: 'object', description: 'Legacy named options (e.g. {requireActive:true}).', additionalProperties: true },
712
- },
713
- required: ['projectRoot'],
714
- },
715
- 'projects.fetchGitTasks': {
716
- type: 'object',
717
- additionalProperties: true,
718
- properties: {
719
- projectName: { type: 'string', description: 'Project name under /projects.' },
720
- owner: { type: 'string', description: 'Remote owner override.' },
721
- repo: { type: 'string', description: 'Remote repo override.' },
722
- state: { type: 'string', enum: ['open', 'closed', 'all'], description: 'Issue state filter.' },
723
- taskOnly: { type: 'boolean', description: 'If true, only return issues with TASK-* IDs.' },
724
- processRoot: { type: 'string', description: 'Repo root containing /projects.' },
725
- args: { type: 'array', description: 'Legacy positional args (projectName).', items: {} },
726
- options: { type: 'object', description: 'Legacy options (owner/repo/state/taskOnly).', additionalProperties: true },
727
- },
728
- required: ['projectName'],
729
- },
730
- 'projects.readGitTask': {
731
- type: 'object',
732
- additionalProperties: true,
733
- properties: {
734
- projectName: { type: 'string', description: 'Project name under /projects.' },
735
- taskRef: { type: 'string', description: 'Issue number (e.g. \"123\") or task ID (e.g. \"TASK-001\").' },
736
- owner: { type: 'string', description: 'Remote owner override.' },
737
- repo: { type: 'string', description: 'Remote repo override.' },
738
- state: { type: 'string', enum: ['open', 'closed', 'all'], description: 'Search scope.' },
739
- taskOnly: { type: 'boolean', description: 'If true, restrict to issues with TASK-* payloads.' },
740
- processRoot: { type: 'string', description: 'Repo root containing /projects.' },
741
- args: { type: 'array', description: 'Legacy positional args (projectName, taskRef).', items: {} },
742
- options: { type: 'object', description: 'Legacy options.', additionalProperties: true },
743
- },
744
- required: ['projectName', 'taskRef'],
745
- },
746
- 'projects.writeGitTask': {
747
- type: 'object',
748
- additionalProperties: true,
749
- properties: {
750
- projectName: { type: 'string', description: 'Project name under /projects.' },
751
- taskRef: { type: 'string', description: 'Issue number (e.g. \"123\") or task ID (e.g. \"TASK-001\").' },
752
- owner: { type: 'string', description: 'Remote owner override.' },
753
- repo: { type: 'string', description: 'Remote repo override.' },
754
- createIfMissing: { type: 'boolean', description: 'Create issue if taskRef is TASK-* and not found.' },
755
- title: { type: 'string', description: 'Issue title override.' },
756
- body: { type: 'string', description: 'Issue body override.' },
757
- labels: { type: 'array', items: { type: 'string' }, description: 'Labels to set.' },
758
- state: { type: 'string', enum: ['open', 'closed'], description: 'Issue state.' },
759
- taskDependencies: { type: 'array', items: { type: 'string' }, description: 'TASK-* dependencies.' },
760
- taskSignature: { type: 'string', description: 'Optional task signature.' },
761
- processRoot: { type: 'string', description: 'Repo root containing /projects.' },
762
- args: { type: 'array', description: 'Legacy positional args (projectName, taskRef).', items: {} },
763
- options: { type: 'object', description: 'Legacy options.', additionalProperties: true },
764
- },
765
- required: ['projectName', 'taskRef'],
766
- },
767
- 'mcp.search': {
768
- type: 'object',
769
- additionalProperties: true,
770
- properties: {
771
- projectName: { type: 'string', description: 'Project name under /projects (e.g. "adl").' },
772
- section: { type: 'string', enum: ['spec', 'docs'], description: 'Search target section.' },
773
- pattern: { type: 'string', description: 'Search pattern (like rg PATTERN).' },
774
- query: { type: 'string', description: 'Alias of pattern.' },
775
- q: { type: 'string', description: 'Alias of pattern.' },
776
- paths: { type: 'array', items: { type: 'string' }, description: 'Paths to search within the section.' },
777
- globs: { type: 'array', items: { type: 'string' }, description: 'Glob filters (like rg --glob).' },
778
- ignoreCase: { type: 'boolean' },
779
- caseSensitive: { type: 'boolean' },
780
- smartCase: { type: 'boolean' },
781
- fixedStrings: { type: 'boolean' },
782
- wordRegexp: { type: 'boolean' },
783
- maxCount: { type: 'integer', minimum: 1 },
784
- includeHidden: { type: 'boolean' },
785
- filesWithMatches: { type: 'boolean' },
786
- filesWithoutMatch: { type: 'boolean' },
787
- countOnly: { type: 'boolean' },
788
- onlyMatching: { type: 'boolean' },
789
- ref: { type: 'string', description: 'Optional git ref for remote search.' },
790
- source: { type: 'string', enum: ['local', 'gitea', 'auto'], description: 'local=filesystem, gitea=remote, auto=local-first.' },
791
- refresh: { type: 'boolean', description: 'Refresh cached search corpus.' },
792
- cacheDir: { type: 'string', description: 'Optional override cache directory.' },
793
- processRoot: {
794
- type: 'string',
795
- description: 'Repo root containing /projects. If you pass a project root, it will be normalized.',
796
- },
797
- },
798
- $comment: safeJsonStringify({
799
- example: {
800
- projectName: 'adl',
801
- section: 'spec',
802
- pattern: 'REQ-',
803
- paths: ['.'],
804
- source: 'auto',
805
- },
806
- }),
807
- },
808
- }
809
-
810
- const buildArgsSchemaFromPlaceholders = (placeholders: string[]): Record<string, unknown> => ({
811
- type: 'array',
812
- description: 'Positional arguments',
813
- minItems: placeholders.length,
814
- prefixItems: placeholders.map((placeholder) => ({
815
- type: 'string',
816
- title: placeholder,
817
- description: placeholder,
818
- })),
819
- items: {},
820
- })
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
+ });
821
1383
 
822
1384
  const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
823
- 'projects.listProjects': {
824
- type: 'array',
825
- description: 'No positional arguments. Use options.processRoot if needed.',
826
- minItems: 0,
827
- maxItems: 0,
828
- items: {},
829
- },
830
- 'projects.usage': {
831
- type: 'array',
832
- description: 'No positional arguments.',
833
- minItems: 0,
834
- maxItems: 0,
835
- items: {},
836
- },
837
- 'agents.usage': {
838
- type: 'array',
839
- description: 'No positional arguments.',
840
- minItems: 0,
841
- maxItems: 0,
842
- items: {},
843
- },
844
- 'projects.searchDocs': {
845
- type: 'array',
846
- description: 'rg-like: projectName, pattern, then optional paths.',
847
- minItems: 2,
848
- prefixItems: [
849
- { type: 'string', title: 'projectName' },
850
- { type: 'string', title: 'pattern' },
851
- ],
852
- items: { type: 'string', title: 'path' },
853
- },
854
- 'projects.searchSpecs': {
855
- type: 'array',
856
- description: 'rg-like: projectName, pattern, then optional paths.',
857
- minItems: 2,
858
- prefixItems: [
859
- { type: 'string', title: 'projectName' },
860
- { type: 'string', title: 'pattern' },
861
- ],
862
- items: { type: 'string', title: 'path' },
863
- },
864
- 'projects.resolveProjectTargetFile': {
865
- type: 'array',
866
- description: 'Low-level helper: resolve versioned file in a directory.',
867
- minItems: 2,
868
- prefixItems: [
869
- { type: 'string', title: 'targetDir', description: 'Directory to scan (absolute or relative to cwd).' },
870
- {
871
- type: 'object',
872
- title: 'spec',
873
- description: 'Output of projects.parseProjectTargetSpec().',
874
- additionalProperties: true,
875
- },
876
- ],
877
- items: {},
878
- },
879
- 'agents.resolveTargetFile': {
880
- type: 'array',
881
- description: 'Low-level helper: resolve versioned file in a directory.',
882
- minItems: 2,
883
- prefixItems: [
884
- { type: 'string', title: 'targetDir' },
885
- {
886
- type: 'object',
887
- title: 'spec',
888
- description: 'Output of agents.parseTargetSpec().',
889
- additionalProperties: true,
890
- },
891
- ],
892
- items: {},
893
- },
894
- 'projects.resolveImplementationPlan': {
895
- type: 'array',
896
- description: 'Low-level helper: resolve docs implementation plan path for a given projectRoot.',
897
- minItems: 1,
898
- prefixItems: [
899
- { type: 'string', title: 'projectRoot', description: 'Absolute path to a project folder (e.g. .../projects/adl).' },
900
- { type: 'string', title: 'inputFile', description: 'Optional path within docs/ (or absolute) to pick a plan.' },
901
- ],
902
- items: {},
903
- },
904
- }
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
+ };
905
1481
 
906
1482
  const toolArgsSchema = (toolName: string): Record<string, unknown> => {
907
- const override = TOOL_ARGS_SCHEMA_OVERRIDES[toolName]
908
- if (override) return override
909
-
910
- const requiredArgs = TOOL_REQUIRED_ARGS[toolName]
911
- if (requiredArgs && requiredArgs.length > 0) {
912
- return buildArgsSchemaFromPlaceholders(requiredArgs)
913
- }
914
-
915
- return {
916
- type: 'array',
917
- items: {},
918
- description: 'Positional arguments',
919
- }
920
- }
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
+ };
921
1497
 
922
1498
  const getInvocationPlanName = (toolName: string): string => {
923
- const plan = toolInvocationPlans[toolName]
924
- if (!plan) return 'default'
925
- if (plan === buildOptionsOnly) return 'optionsOnly'
926
- if (plan === buildOptionsThenProcessRoot) return 'optionsThenProcessRoot'
927
- if (plan === buildProcessRootThenOptions) return 'processRootThenOptions'
928
- if (plan === buildProcessRootOnly) return 'processRootOnly'
929
- return 'custom'
930
- }
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
+ };
931
1507
 
932
1508
  const buildInvocationExample = (toolName: string): Record<string, unknown> => {
933
- const requiredArgs = TOOL_REQUIRED_ARGS[toolName]
934
- const plan = getInvocationPlanName(toolName)
935
- const defaultOptions = TOOL_DEFAULT_OPTIONS[toolName] ?? {}
936
-
937
- const example: Record<string, unknown> = {}
938
- if (requiredArgs && requiredArgs.length > 0) {
939
- example.args = [...requiredArgs]
940
- } else if (plan !== 'processRootOnly') {
941
- example.args = ['<arg0>']
942
- }
943
-
944
- if (plan === 'processRootOnly') {
945
- example.options = { processRoot: '<repo-root>', ...defaultOptions }
946
- return example
947
- }
948
-
949
- if (plan === 'optionsThenProcessRoot' || plan === 'processRootThenOptions') {
950
- example.options = { processRoot: '<repo-root>', ...defaultOptions }
951
- return example
952
- }
953
-
954
- if (plan === 'optionsOnly') {
955
- example.options = Object.keys(defaultOptions).length > 0 ? { ...defaultOptions } : { example: true }
956
- return example
957
- }
958
-
959
- example.options = Object.keys(defaultOptions).length > 0 ? { ...defaultOptions } : { example: true }
960
- return example
961
- }
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
+ };
962
1544
 
963
1545
  const defaultToolInputSchema = (toolName: string) => ({
964
- type: 'object',
965
- additionalProperties: true,
966
- properties: {
967
- args: {
968
- ...toolArgsSchema(toolName),
969
- },
970
- options: {
971
- type: 'object',
972
- additionalProperties: true,
973
- description: 'Named options',
974
- },
975
- processRoot: {
976
- type: 'string',
977
- description:
978
- 'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
979
- },
980
- },
981
- $comment: safeJsonStringify({
982
- note: 'Preferred: pass args as JSON-native values. Avoid stringifying objects.',
983
- invocationPlan: getInvocationPlanName(toolName),
984
- example: buildInvocationExample(toolName),
985
- }),
986
- })
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
+ });
987
1569
 
988
1570
  const buildToolList = (
989
- tools: ToolDefinition[],
990
- batchToolName: string,
991
- prefix: string | undefined,
992
- exposeUnprefixedAliases: boolean,
1571
+ tools: ToolDefinition[],
1572
+ batchToolName: string,
1573
+ prefix: string | undefined,
1574
+ exposeUnprefixedAliases: boolean,
993
1575
  ) => {
994
- const toolEntries = tools.flatMap((tool) => {
995
- const canonicalName = buildToolName(tool, prefix)
996
- const schema = TOOL_INPUT_SCHEMA_OVERRIDES[tool.name] ?? defaultToolInputSchema(tool.name)
997
- const base = {
998
- // Some UIs only show tool descriptions. Make the fastest path be "copy schema, fill values".
999
- description: safeJsonStringify(schema),
1000
- inputSchema: schema,
1001
- }
1002
-
1003
- const entries: Array<{ name: string; description: string; inputSchema: unknown }> = [
1004
- {
1005
- name: canonicalName,
1006
- ...base,
1007
- },
1008
- ]
1009
-
1010
- if (prefix && exposeUnprefixedAliases) {
1011
- entries.push({
1012
- name: tool.name,
1013
- description: safeJsonStringify(schema),
1014
- inputSchema: base.inputSchema,
1015
- })
1016
- }
1017
-
1018
- return entries
1019
- })
1020
-
1021
- const batchTool = {
1022
- name: batchToolName,
1023
- description: '',
1024
- inputSchema: {
1025
- type: 'object',
1026
- additionalProperties: true,
1027
- properties: {
1028
- calls: {
1029
- type: 'array',
1030
- minItems: 1,
1031
- items: {
1032
- type: 'object',
1033
- additionalProperties: true,
1034
- properties: {
1035
- tool: {
1036
- type: 'string',
1037
- description: 'Full MCP tool name to execute',
1038
- },
1039
- args: {
1040
- type: 'array',
1041
- items: {},
1042
- description: 'Positional args for the tool',
1043
- },
1044
- options: {
1045
- type: 'object',
1046
- additionalProperties: true,
1047
- description: 'Tool invocation options',
1048
- },
1049
- },
1050
- required: ['tool'],
1051
- },
1052
- description: 'List of tool calls to execute',
1053
- },
1054
- continueOnError: {
1055
- type: 'boolean',
1056
- description: 'Whether to continue when a call in the batch fails',
1057
- default: false,
1058
- },
1059
- maxConcurrency: {
1060
- type: 'integer',
1061
- minimum: 1,
1062
- description: 'Max number of calls to execute concurrently when continueOnError=true.',
1063
- default: 8,
1064
- },
1065
- },
1066
- required: ['calls'],
1067
- },
1068
- }
1069
-
1070
- ;(batchTool as any).description = safeJsonStringify(batchTool.inputSchema)
1071
-
1072
- const out = [...toolEntries, batchTool]
1073
- if (prefix && exposeUnprefixedAliases) {
1074
- out.push({
1075
- ...batchTool,
1076
- name: 'batch',
1077
- description: safeJsonStringify(batchTool.inputSchema),
1078
- })
1079
- }
1080
-
1081
- return out
1082
- }
1083
-
1084
- type ToolInvoker = (args: unknown[], options: Record<string, unknown>) => unknown[]
1085
-
1086
- const buildOptionsOnly = (args: unknown[], options: Record<string, unknown>): unknown[] => {
1087
- const invocationArgs: unknown[] = [...args]
1088
- if (Object.keys(options).length > 0) {
1089
- invocationArgs.push(options)
1090
- }
1091
- return invocationArgs
1092
- }
1093
-
1094
- const buildOptionsThenProcessRoot = (args: unknown[], options: Record<string, unknown>): unknown[] => {
1095
- const invocationArgs: unknown[] = [...args]
1096
- const remaining = { ...options }
1097
- const processRoot = remaining.processRoot
1098
- if (typeof processRoot === 'string') {
1099
- delete remaining.processRoot
1100
- }
1101
-
1102
- if (Object.keys(remaining).length > 0) {
1103
- invocationArgs.push(remaining)
1104
- }
1105
- if (typeof processRoot === 'string') {
1106
- invocationArgs.push(processRoot)
1107
- }
1108
-
1109
- return invocationArgs
1110
- }
1111
-
1112
- const buildProcessRootThenOptions = (args: unknown[], options: Record<string, unknown>): unknown[] => {
1113
- const invocationArgs: unknown[] = [...args]
1114
- const remaining = { ...options }
1115
- const processRoot = remaining.processRoot
1116
- if (typeof processRoot === 'string') {
1117
- delete remaining.processRoot
1118
- }
1119
-
1120
- if (typeof processRoot === 'string') {
1121
- invocationArgs.push(processRoot)
1122
- }
1123
- if (Object.keys(remaining).length > 0) {
1124
- invocationArgs.push(remaining)
1125
- }
1126
-
1127
- return invocationArgs
1128
- }
1129
-
1130
- const buildProcessRootOnly = (args: unknown[], options: Record<string, unknown>): unknown[] => {
1131
- const invocationArgs: unknown[] = [...args]
1132
- const processRoot = options.processRoot
1133
- if (typeof processRoot === 'string') {
1134
- invocationArgs.push(processRoot)
1135
- }
1136
- return invocationArgs
1137
- }
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
+ };
1138
1745
 
1139
1746
  const toolInvocationPlans: Record<string, ToolInvoker> = {
1140
- 'agents.setActive': buildProcessRootThenOptions,
1141
- 'agents.resolveAgentsRootFrom': buildProcessRootOnly,
1142
- 'projects.setActive': buildProcessRootThenOptions,
1143
- 'projects.generateSpec': buildOptionsThenProcessRoot,
1144
- 'projects.syncTasks': buildOptionsThenProcessRoot,
1145
- 'projects.clearIssues': buildOptionsThenProcessRoot,
1146
- 'projects.fetchGitTasks': buildOptionsThenProcessRoot,
1147
- 'projects.readGitTask': buildOptionsThenProcessRoot,
1148
- 'projects.writeGitTask': buildOptionsThenProcessRoot,
1149
- 'agents.resolveTargetFile': buildOptionsOnly,
1150
- 'projects.resolveProjectTargetFile': buildOptionsOnly,
1151
- 'agents.loadAgent': buildProcessRootOnly,
1152
- 'agents.loadAgentPrompt': buildProcessRootOnly,
1153
- 'projects.resolveImplementationPlan': (args, options) => {
1154
- const invocationArgs: unknown[] = [...args]
1155
- const remaining = { ...options }
1156
- const processRoot = remaining.processRoot
1157
- if (typeof processRoot === 'string') {
1158
- delete remaining.processRoot
1159
- }
1160
-
1161
- // This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
1162
- // If the caller provides options but no inputFile, preserve the positional slot.
1163
- if (Object.keys(remaining).length > 0) {
1164
- if (invocationArgs.length === 1) {
1165
- invocationArgs.push(undefined)
1166
- }
1167
- invocationArgs.push(remaining)
1168
- }
1169
-
1170
- // Intentionally do NOT append processRoot: projectRoot is the first positional argument.
1171
- return invocationArgs
1172
- },
1173
- 'agents.main': buildProcessRootOnly,
1174
- 'agents.resolveAgentsRoot': buildProcessRootOnly,
1175
- 'agents.listAgents': buildProcessRootOnly,
1176
- 'projects.resolveProjectRoot': buildProcessRootOnly,
1177
- 'projects.listProjects': buildProcessRootOnly,
1178
- 'projects.listProjectDocs': buildProcessRootOnly,
1179
- 'projects.readProjectDoc': buildProcessRootOnly,
1180
- 'projects.main': buildProcessRootOnly,
1181
- }
1182
-
1183
- const invokeTool = async (tool: ToolDefinition, payload: unknown): Promise<unknown> => {
1184
- const normalized = normalizePayload(payload)
1185
- const { args, options } = coercePayloadForTool(tool.name, normalized)
1186
- const invoke = toolInvocationPlans[tool.name] ?? ((rawArgs, rawOptions) => {
1187
- const invocationArgs = [...rawArgs]
1188
- if (Object.keys(rawOptions).length > 0) {
1189
- invocationArgs.push(rawOptions)
1190
- }
1191
- return invocationArgs
1192
- })
1193
- const invocationArgs = invoke(args, options)
1194
-
1195
- return Promise.resolve(tool.method(...invocationArgs))
1196
- }
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
+ }
1768
+
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
+ };
1197
1810
 
1198
1811
  export interface ExampleMcpServerOptions {
1199
- serverName?: string
1200
- serverVersion?: string
1201
- toolsPrefix?: string
1202
- allowedRootEndpoints?: string[]
1203
- disableWrite?: boolean
1204
- enableIssues?: boolean
1205
- admin?: boolean
1206
- exposeUnprefixedToolAliases?: boolean
1812
+ serverName?: string;
1813
+ serverVersion?: string;
1814
+ toolsPrefix?: string;
1815
+ allowedRootEndpoints?: string[];
1816
+ disableWrite?: boolean;
1817
+ enableIssues?: boolean;
1818
+ admin?: boolean;
1819
+ exposeUnprefixedToolAliases?: boolean;
1207
1820
  }
1208
1821
 
1209
1822
  export type ExampleMcpServerInstance = {
1210
- api: ApiEndpoint
1211
- tools: ToolDefinition[]
1212
- server: Server
1213
- run: () => Promise<Server>
1214
- }
1823
+ api: ApiEndpoint;
1824
+ tools: ToolDefinition[];
1825
+ server: Server;
1826
+ run: () => Promise<Server>;
1827
+ };
1215
1828
 
1216
1829
  const READ_ONLY_TOOL_NAMES = new Set<string>([
1217
- 'agents.usage',
1218
- 'agents.resolveAgentsRoot',
1219
- 'agents.resolveAgentsRootFrom',
1220
- 'agents.listAgents',
1221
- 'agents.parseTargetSpec',
1222
- 'agents.resolveTargetFile',
1223
- 'agents.loadAgent',
1224
- 'agents.loadAgentPrompt',
1225
- 'projects.usage',
1226
- 'projects.resolveProjectRoot',
1227
- 'projects.listProjects',
1228
- 'projects.listProjectDocs',
1229
- 'projects.readProjectDoc',
1230
- 'projects.searchDocs',
1231
- 'projects.searchSpecs',
1232
- 'projects.resolveImplementationPlan',
1233
- 'projects.fetchGitTasks',
1234
- 'projects.readGitTask',
1235
- 'projects.parseProjectTargetSpec',
1236
- 'projects.resolveProjectTargetFile',
1237
- 'mcp.usage',
1238
- 'mcp.listTools',
1239
- 'mcp.describeTool',
1240
- 'mcp.search',
1241
- ])
1242
-
1243
- const isWriteCapableTool = (toolName: string): boolean => !READ_ONLY_TOOL_NAMES.has(toolName)
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
+ ]);
1857
+
1858
+ const isWriteCapableTool = (toolName: string): boolean =>
1859
+ !READ_ONLY_TOOL_NAMES.has(toolName);
1244
1860
  const ISSUE_TOOL_NAMES = new Set<string>([
1245
- 'projects.fetchGitTasks',
1246
- 'projects.readGitTask',
1247
- 'projects.writeGitTask',
1248
- 'projects.syncTasks',
1249
- 'projects.clearIssues',
1250
- ])
1251
- const isIssueTool = (toolName: string): boolean => ISSUE_TOOL_NAMES.has(toolName)
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);
1252
1870
  const ADMIN_TOOL_NAMES = new Set<string>([
1253
- 'projects.syncTasks',
1254
- 'projects.clearIssues',
1255
- 'agents.runAgent',
1256
- 'agents.main',
1257
- 'projects.main',
1258
- ])
1259
- const isAdminTool = (toolName: string): boolean => ADMIN_TOOL_NAMES.has(toolName)
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);
1260
1879
 
1261
1880
  const getToolAccess = (toolName: string): ToolAccess => {
1262
- if (isAdminTool(toolName)) return 'admin'
1263
- return isWriteCapableTool(toolName) ? 'write' : 'read'
1264
- }
1881
+ if (isAdminTool(toolName)) return "admin";
1882
+ return isWriteCapableTool(toolName) ? "write" : "read";
1883
+ };
1265
1884
 
1266
1885
  const buildToolMeta = (tools: ToolDefinition[]): void => {
1267
- for (const tool of tools) {
1268
- TOOL_META[tool.name] = {
1269
- access: getToolAccess(tool.name),
1270
- category: tool.path[0],
1271
- notes: isAdminTool(tool.name) ? ' Admin-only.' : undefined,
1272
- }
1273
- }
1274
- }
1275
-
1276
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
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
+ };
1277
1919
 
1278
1920
  const runWithConcurrency = async <T>(
1279
- tasks: Array<() => Promise<T>>,
1280
- maxConcurrency: number,
1921
+ tasks: Array<() => Promise<T>>,
1922
+ maxConcurrency: number,
1281
1923
  ): Promise<T[]> => {
1282
- const results: T[] = new Array(tasks.length)
1283
- let nextIndex = 0
1284
-
1285
- const worker = async (): Promise<void> => {
1286
- while (true) {
1287
- const index = nextIndex
1288
- nextIndex += 1
1289
- if (index >= tasks.length) return
1290
- results[index] = await tasks[index]()
1291
- }
1292
- }
1293
-
1294
- const concurrency = Math.max(1, Math.min(maxConcurrency, tasks.length))
1295
- await Promise.all(Array.from({ length: concurrency }, () => worker()))
1296
- return results
1297
- }
1298
-
1299
- export const createExampleMcpServer = (options: ExampleMcpServerOptions = {}): ExampleMcpServerInstance => {
1300
- let toolCatalog: unknown[] = []
1301
-
1302
- const parseString = (value: unknown): string | null => {
1303
- if (typeof value !== 'string') return null
1304
- const trimmed = value.trim()
1305
- return trimmed.length > 0 ? trimmed : null
1306
- }
1307
-
1308
- const parseBoolean = (value: unknown): boolean => value === true || value === 1 || value === '1' || value === 'true'
1309
-
1310
- const parseStringArray = (value: unknown): string[] => {
1311
- if (Array.isArray(value)) {
1312
- return value.map((entry) => String(entry)).map((entry) => entry.trim()).filter((entry) => entry.length > 0)
1313
- }
1314
- const asString = parseString(value)
1315
- return asString ? [asString] : []
1316
- }
1317
-
1318
- const ensureProjectName = (projectName: string | null, processRoot: string): string => {
1319
- if (projectName) return projectName
1320
- const projects = projectsApi.listProjects(processRoot)
1321
- if (projects.length === 1) return projects[0]
1322
- throw new Error(`projectName is required. Available projects: ${projects.join(', ')}`)
1323
- }
1324
-
1325
- const mcpApi = {
1326
- usage: () =>
1327
- [
1328
- 'F0 MCP helper tools:',
1329
- '- mcp.listTools: returns tool catalog with access + invocation hints',
1330
- '- mcp.describeTool: describe one tool by name (prefixed or unprefixed)',
1331
- '- mcp.search: LLM-friendly search over project docs/spec (local-first)',
1332
- '',
1333
- 'Tip: Prefer mcp.search for "search spec/docs" requests.',
1334
- ].join('\n'),
1335
- listTools: () => ({ tools: toolCatalog }),
1336
- describeTool: (toolName: string) => {
1337
- const normalized = typeof toolName === 'string' ? toolName.trim() : ''
1338
- if (!normalized) {
1339
- throw new Error('toolName is required.')
1340
- }
1341
-
1342
- const matches = toolCatalog.filter((entry) =>
1343
- isRecord(entry) && (entry.name === normalized || (entry as any).unprefixedName === normalized),
1344
- ) as Array<Record<string, unknown>>
1345
-
1346
- if (matches.length === 0) {
1347
- const needle = normalized.toLowerCase()
1348
- const candidates = toolCatalog
1349
- .filter(isRecord)
1350
- .map((entry) => String((entry as any).name ?? ''))
1351
- .filter((name) => name.length > 0)
1352
- const suggestions = candidates
1353
- .filter((name) => name.toLowerCase().includes(needle) || needle.includes(name.toLowerCase()))
1354
- .slice(0, 8)
1355
- const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(', ')}?` : ''
1356
- throw new Error(`Unknown tool: ${normalized}.${hint}`)
1357
- }
1358
-
1359
- const canonical = matches.find((entry) => (entry as any).kind === 'canonical')
1360
- return canonical ?? matches[0]
1361
- },
1362
- search: async (input: unknown) => {
1363
- const payload = isRecord(input) ? input : {}
1364
- const processRoot = (() => {
1365
- const raw = parseString(payload.processRoot)
1366
- return raw ? normalizeProcessRoot(raw) : process.cwd()
1367
- })()
1368
-
1369
- const sectionRaw = parseString(payload.section)?.toLowerCase()
1370
- const section = sectionRaw === 'docs' ? 'docs' : 'spec'
1371
-
1372
- const pattern =
1373
- parseString(payload.pattern) ??
1374
- parseString(payload.query) ??
1375
- parseString(payload.q)
1376
- if (!pattern) {
1377
- throw new Error('pattern is required (or query/q).')
1378
- }
1379
-
1380
- const rgArgs: string[] = []
1381
- if (parseBoolean(payload.ignoreCase)) rgArgs.push('--ignore-case')
1382
- if (parseBoolean(payload.caseSensitive)) rgArgs.push('--case-sensitive')
1383
- if (parseBoolean(payload.smartCase)) rgArgs.push('--smart-case')
1384
- if (parseBoolean(payload.fixedStrings)) rgArgs.push('--fixed-strings')
1385
- if (parseBoolean(payload.wordRegexp)) rgArgs.push('--word-regexp')
1386
- if (parseBoolean(payload.includeHidden)) rgArgs.push('--hidden')
1387
- if (parseBoolean(payload.filesWithMatches)) rgArgs.push('--files-with-matches')
1388
- if (parseBoolean(payload.filesWithoutMatch)) rgArgs.push('--files-without-match')
1389
- if (parseBoolean(payload.countOnly)) rgArgs.push('--count')
1390
- if (parseBoolean(payload.onlyMatching)) rgArgs.push('--only-matching')
1391
-
1392
- const maxCount = typeof payload.maxCount === 'number' ? payload.maxCount : Number(payload.maxCount)
1393
- if (Number.isFinite(maxCount) && maxCount > 0) {
1394
- rgArgs.push('--max-count', String(Math.floor(maxCount)))
1395
- }
1396
-
1397
- for (const glob of parseStringArray(payload.globs)) {
1398
- rgArgs.push('--glob', glob)
1399
- }
1400
-
1401
- rgArgs.push(pattern)
1402
- const paths = parseStringArray(payload.paths)
1403
- if (paths.length > 0) {
1404
- rgArgs.push(...paths)
1405
- }
1406
-
1407
- const projectName = ensureProjectName(parseString(payload.projectName), processRoot)
1408
-
1409
- const searchOptions: Record<string, unknown> = {
1410
- processRoot,
1411
- }
1412
-
1413
- const source = parseString(payload.source)?.toLowerCase()
1414
- if (source) {
1415
- searchOptions.source = source
1416
- }
1417
- const ref = parseString(payload.ref)
1418
- if (ref) {
1419
- searchOptions.ref = ref
1420
- }
1421
- if (parseBoolean(payload.refresh)) {
1422
- searchOptions.refresh = true
1423
- }
1424
- const cacheDir = parseString(payload.cacheDir)
1425
- if (cacheDir) {
1426
- searchOptions.cacheDir = cacheDir
1427
- }
1428
-
1429
- return section === 'docs'
1430
- ? await projectsApi.searchDocs(projectName, ...rgArgs, searchOptions)
1431
- : await projectsApi.searchSpecs(projectName, ...rgArgs, searchOptions)
1432
- },
1433
- } satisfies ToolNamespace
1434
-
1435
- const api: ApiEndpoint = {
1436
- agents: agentsApi,
1437
- projects: projectsApi,
1438
- mcp: mcpApi,
1439
- }
1440
-
1441
- const allRoots = Object.keys(api) as Array<keyof ApiEndpoint>
1442
- const selectedRoots = (() => {
1443
- if (!options.allowedRootEndpoints || options.allowedRootEndpoints.length === 0) {
1444
- return allRoots
1445
- }
1446
-
1447
- const normalized = options.allowedRootEndpoints
1448
- .map((value) => value.trim())
1449
- .filter((value) => value.length > 0)
1450
-
1451
- const unknown = normalized.filter((value) => !allRoots.includes(value as keyof ApiEndpoint))
1452
- if (unknown.length > 0) {
1453
- throw new Error(`Unknown root endpoints: ${unknown.join(', ')}. Allowed: ${allRoots.join(', ')}`)
1454
- }
1455
-
1456
- return [...new Set(normalized)] as Array<keyof ApiEndpoint>
1457
- })()
1458
-
1459
- const selectedTools = selectedRoots.flatMap((root) => collectTools(api[root], [root]))
1460
- buildToolMeta(selectedTools)
1461
- const adminFilteredTools = Boolean(options.admin)
1462
- ? selectedTools
1463
- : selectedTools.filter((tool) => !isAdminTool(tool.name))
1464
- const tools = options.disableWrite
1465
- ? adminFilteredTools.filter((tool) => !isWriteCapableTool(tool.name) || (Boolean(options.enableIssues) && isIssueTool(tool.name)))
1466
- : adminFilteredTools
1467
- const prefix = options.toolsPrefix
1468
- const exposeUnprefixedAliases = options.exposeUnprefixedToolAliases ?? true
1469
- const batchToolName = prefix ? `${prefix}.batch` : 'batch'
1470
-
1471
- toolCatalog = tools.flatMap((tool) => {
1472
- const canonicalName = buildToolName(tool, prefix)
1473
- const schema = TOOL_INPUT_SCHEMA_OVERRIDES[tool.name] ?? defaultToolInputSchema(tool.name)
1474
- const base = {
1475
- canonicalName,
1476
- unprefixedName: tool.name,
1477
- access: getToolAccess(tool.name),
1478
- category: TOOL_META[tool.name]?.category ?? tool.path[0],
1479
- invocationPlan: getInvocationPlanName(tool.name),
1480
- example: buildInvocationExample(tool.name),
1481
- description: safeJsonStringify(schema),
1482
- inputSchema: schema,
1483
- }
1484
-
1485
- const out = [
1486
- {
1487
- name: canonicalName,
1488
- kind: 'canonical' as const,
1489
- ...base,
1490
- },
1491
- ]
1492
-
1493
- if (prefix && exposeUnprefixedAliases) {
1494
- out.push({
1495
- name: tool.name,
1496
- kind: 'alias' as const,
1497
- aliasOf: canonicalName,
1498
- ...base,
1499
- })
1500
- }
1501
-
1502
- return out
1503
- })
1504
-
1505
- toolCatalog.push({
1506
- name: batchToolName,
1507
- kind: 'canonical' as const,
1508
- canonicalName: batchToolName,
1509
- unprefixedName: 'batch',
1510
- access: 'write',
1511
- category: 'mcp',
1512
- invocationPlan: 'custom',
1513
- example: {
1514
- calls: [
1515
- { tool: prefix ? `${prefix}.projects.usage` : 'projects.usage' },
1516
- { tool: prefix ? `${prefix}.projects.listProjects` : 'projects.listProjects', options: { processRoot: '<repo-root>' } },
1517
- ],
1518
- continueOnError: true,
1519
- maxConcurrency: 4,
1520
- },
1521
- description: safeJsonStringify({
1522
- type: 'object',
1523
- additionalProperties: true,
1524
- properties: {
1525
- calls: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: true } },
1526
- continueOnError: { type: 'boolean', default: false },
1527
- maxConcurrency: { type: 'integer', minimum: 1, default: 8 },
1528
- },
1529
- required: ['calls'],
1530
- }),
1531
- inputSchema: {
1532
- type: 'object',
1533
- additionalProperties: true,
1534
- properties: {
1535
- calls: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: true } },
1536
- continueOnError: { type: 'boolean', default: false },
1537
- maxConcurrency: { type: 'integer', minimum: 1, default: 8 },
1538
- },
1539
- required: ['calls'],
1540
- },
1541
- })
1542
- if (prefix && exposeUnprefixedAliases) {
1543
- toolCatalog.push({
1544
- name: 'batch',
1545
- kind: 'alias' as const,
1546
- aliasOf: batchToolName,
1547
- canonicalName: batchToolName,
1548
- unprefixedName: 'batch',
1549
- access: 'write',
1550
- category: 'mcp',
1551
- invocationPlan: 'custom',
1552
- example: {
1553
- calls: [{ tool: batchToolName }],
1554
- },
1555
- description: safeJsonStringify({
1556
- type: 'object',
1557
- additionalProperties: true,
1558
- properties: {
1559
- calls: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: true } },
1560
- continueOnError: { type: 'boolean', default: false },
1561
- maxConcurrency: { type: 'integer', minimum: 1, default: 8 },
1562
- },
1563
- required: ['calls'],
1564
- }),
1565
- inputSchema: {
1566
- type: 'object',
1567
- additionalProperties: true,
1568
- properties: {
1569
- calls: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: true } },
1570
- continueOnError: { type: 'boolean', default: false },
1571
- maxConcurrency: { type: 'integer', minimum: 1, default: 8 },
1572
- },
1573
- required: ['calls'],
1574
- },
1575
- })
1576
- }
1577
-
1578
- const server = new Server(
1579
- {
1580
- name: options.serverName ?? 'example-api',
1581
- version: options.serverVersion ?? '1.0.0',
1582
- },
1583
- {
1584
- capabilities: {
1585
- tools: {},
1586
- },
1587
- },
1588
- )
1589
-
1590
- const toolByName = new Map<string, ToolDefinition>(tools.map((tool) => [buildToolName(tool, prefix), tool]))
1591
- const toolByUnprefixedName = new Map<string, ToolDefinition>(
1592
- prefix ? tools.map((tool) => [tool.name, tool]) : [],
1593
- )
1594
-
1595
- const knownToolNames = (() => {
1596
- const names = new Set<string>()
1597
- for (const name of toolByName.keys()) names.add(name)
1598
- if (prefix && exposeUnprefixedAliases) {
1599
- for (const tool of tools) names.add(tool.name)
1600
- names.add('batch')
1601
- }
1602
- names.add(batchToolName)
1603
- return names
1604
- })()
1605
-
1606
- const suggestToolNames = (requested: string): string[] => {
1607
- const needle = requested.trim().toLowerCase()
1608
- if (!needle) return []
1609
-
1610
- const score = (candidate: string): number => {
1611
- const cand = candidate.toLowerCase()
1612
- if (cand === needle) return 0
1613
- if (cand.includes(needle)) return 1
1614
- if (needle.includes(cand)) return 2
1615
- const needleLast = needle.split('.').pop() ?? needle
1616
- const candLast = cand.split('.').pop() ?? cand
1617
- if (needleLast && candLast === needleLast) return 3
1618
- if (candLast.includes(needleLast)) return 4
1619
- return 100
1620
- }
1621
-
1622
- return Array.from(knownToolNames)
1623
- .map((name) => ({ name, s: score(name) }))
1624
- .filter((entry) => entry.s < 100)
1625
- .sort((a, b) => a.s - b.s || a.name.localeCompare(b.name))
1626
- .slice(0, 8)
1627
- .map((entry) => entry.name)
1628
- }
1629
-
1630
- const resolveTool = (name: string): ToolDefinition | null => {
1631
- const direct = toolByName.get(name)
1632
- if (direct) return direct
1633
- if (prefix) {
1634
- const unprefixed = name.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), '')
1635
- return toolByUnprefixedName.get(unprefixed) ?? toolByName.get(`${prefix}.${unprefixed}`) ?? null
1636
- }
1637
- return null
1638
- }
1639
-
1640
- const enrichToolError = (toolName: string, message: string): { message: string; details?: unknown } => {
1641
- const details: Record<string, unknown> = {}
1642
- const unprefixedToolName = prefix
1643
- ? toolName.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), '')
1644
- : toolName
1645
-
1646
- if (/Missing project name\./i.test(message)) {
1647
- details.hint = 'Provide projectName (recommended) or pass it as the first positional arg (args[0]).'
1648
- details.suggestion = 'Prefer mcp.search with { projectName, section, pattern }.'
1649
- details.example = { tool: prefix ? `${prefix}.mcp.search` : 'mcp.search', arguments: { projectName: '<project-name>', section: 'spec', pattern: '<pattern>' } }
1650
- }
1651
-
1652
- if (/Project folder not found:/i.test(message) && /projects[\\/].+projects[\\/]/i.test(message)) {
1653
- details.hint =
1654
- 'You likely passed a project root as processRoot. processRoot should be the repo root containing /projects.'
1655
- }
1656
-
1657
- if (/Missing search pattern\./i.test(message)) {
1658
- details.hint = 'Provide pattern/query (recommended) or a PATTERN as the first rg positional.'
1659
- }
1660
-
1661
- if (Object.keys(details).length === 0) {
1662
- return { message }
1663
- }
1664
-
1665
- details.tool = toolName
1666
- details.inputSchema = TOOL_INPUT_SCHEMA_OVERRIDES[unprefixedToolName] ?? defaultToolInputSchema(unprefixedToolName)
1667
- details.invocationExample = buildInvocationExample(unprefixedToolName)
1668
- return { message, details }
1669
- }
1670
-
1671
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
1672
- tools: buildToolList(tools, batchToolName, prefix, exposeUnprefixedAliases),
1673
- }))
1674
-
1675
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1676
- const requestedName = request.params.name
1677
-
1678
- const isBatchName = requestedName === batchToolName || (prefix && requestedName === 'batch')
1679
- if (isBatchName) {
1680
- try {
1681
- const { calls, continueOnError, maxConcurrency } = normalizeBatchPayload(request.params.arguments)
1682
- const executions = calls.map(({ tool, args = [], options = {} }, index) => ({ tool, args, options, index }))
1683
-
1684
- const runCall = async ({ tool, args, options, index }: typeof executions[number]): Promise<BatchResult> => {
1685
- const toolDefinition = resolveTool(tool)
1686
- if (!toolDefinition) {
1687
- const message = `Unknown tool: ${tool}`
1688
- if (!continueOnError) {
1689
- throw new Error(message)
1690
- }
1691
- const suggestions = suggestToolNames(tool)
1692
- return { index, tool, isError: true, data: { message, suggestions } }
1693
- }
1694
-
1695
- try {
1696
- const data = await invokeTool(toolDefinition, { args, options })
1697
- return { index, tool, isError: false, data }
1698
- } catch (error) {
1699
- const message = error instanceof Error ? error.message : String(error)
1700
- if (!continueOnError) {
1701
- throw new Error(message)
1702
- }
1703
- const enriched = enrichToolError(tool, message)
1704
- return { index, tool, isError: true, data: { message: enriched.message, details: enriched.details } }
1705
- }
1706
- }
1707
-
1708
- const results = continueOnError
1709
- ? await runWithConcurrency(executions.map((execution) => () => runCall(execution)), maxConcurrency)
1710
- : await (async () => {
1711
- const out: BatchResult[] = []
1712
- for (const execution of executions) {
1713
- out.push(await runCall(execution))
1714
- }
1715
- return out
1716
- })()
1717
-
1718
- const ordered = [...results].sort((a, b) => a.index - b.index)
1719
- const hasErrors = ordered.some((result) => result.isError)
1720
- return hasErrors
1721
- ? toolErr('One or more batch calls failed.', { results: ordered })
1722
- : toolOk({ results: ordered })
1723
- } catch (error) {
1724
- return toolErr(error instanceof Error ? error.message : String(error))
1725
- }
1726
- }
1727
-
1728
- const tool = resolveTool(requestedName)
1729
-
1730
- if (!tool) {
1731
- const suggestions = suggestToolNames(requestedName)
1732
- return toolErr(`Unknown tool: ${requestedName}`, suggestions.length > 0 ? { suggestions } : undefined)
1733
- }
1734
-
1735
- try {
1736
- const data = await invokeTool(tool, request.params.arguments)
1737
- return toolOk(data)
1738
- } catch (error) {
1739
- const message = error instanceof Error ? error.message : String(error)
1740
- const enriched = enrichToolError(requestedName, message)
1741
- return toolErr(enriched.message, enriched.details)
1742
- }
1743
- })
1744
-
1745
- const run = async (): Promise<Server> => {
1746
- await server.connect(new StdioServerTransport())
1747
- return server
1748
- }
1749
-
1750
- return { api, tools, server, run }
1751
- }
1752
-
1753
- export const runExampleMcpServer = async (options: ExampleMcpServerOptions = {}): Promise<Server> => {
1754
- const instance = createExampleMcpServer(options)
1755
- return instance.run()
1756
- }
1757
-
1758
- export const normalizeToolCallNameForServer = (prefix: string | undefined, toolName: string): string =>
1759
- prefix ? toolName.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), '') : toolName
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
+ };
2528
+
2529
+ export const normalizeToolCallNameForServer = (
2530
+ prefix: string | undefined,
2531
+ toolName: string,
2532
+ ): string =>
2533
+ prefix
2534
+ ? toolName.replace(new RegExp(`^${escapeRegExp(prefix)}\\.`), "")
2535
+ : toolName;