@foundation0/api 1.1.4 → 1.1.5

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/manual.md ADDED
@@ -0,0 +1,150 @@
1
+ # Foundation0 MCP Tool-Calling Manual (for LLMs)
2
+
3
+ This is a **tool-calling guide** for the Foundation0 MCP server in `api/mcp/*`.
4
+ Assume the server is already configured in your MCP host and you can call its tools.
5
+
6
+ ## 0) Golden rules
7
+
8
+ 1. If you are unsure about a tool name, call `mcp.listTools` first and use the exact name it returns (prefixes vary).
9
+ 2. Use `mcp.describeTool` before your first call to a tool to get the input schema + an example payload.
10
+ 3. Prefer named keys when possible (e.g. `projectName`, `repoRoot`) rather than relying on positional `args`.
11
+ 4. When a tool needs `repoRoot`, it must be the **repo root** containing both `api/` and `projects/` (not a single project folder). (Legacy alias: `processRoot` is still accepted.) If omitted, the server will try to auto-detect a repo root from its current working directory (best effort).
12
+ 5. Tool results are returned as a **JSON text envelope**: parse the text as JSON and check `ok`.
13
+
14
+ ## 1) Tool naming (prefixes + aliases)
15
+
16
+ Depending on how the server was started, tool names may be:
17
+
18
+ - unprefixed: `projects.listProjects`
19
+ - prefixed: `api.projects.listProjects`
20
+
21
+ The server usually exposes both when a prefix is set, but do not assume: **discover at runtime** via `mcp.listTools`.
22
+
23
+ Some tools also have OpenAI-safe underscore aliases (no dots). Example:
24
+
25
+ - `net.curl` may also be available as `net_curl`
26
+
27
+ ## 2) The 3 discovery calls
28
+
29
+ ### A) List all tools
30
+
31
+ Tool call:
32
+
33
+ ```json
34
+ { "name": "mcp.listTools", "arguments": {} }
35
+ ```
36
+
37
+ ### B) Describe one tool (schema + example)
38
+
39
+ Tool call (prefixed or unprefixed names both work here):
40
+
41
+ ```json
42
+ { "name": "mcp.describeTool", "arguments": { "args": ["projects.listProjects"] } }
43
+ ```
44
+
45
+ ### C) `mcp.search` for "search docs/spec" requests
46
+
47
+ Tool call:
48
+
49
+ ```json
50
+ {
51
+ "name": "mcp.search",
52
+ "arguments": {
53
+ "projectName": "<project-name>",
54
+ "section": "spec",
55
+ "pattern": "authentication",
56
+ "repoRoot": "<repo-root>",
57
+ "ignoreCase": true,
58
+ "maxCount": 50
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## 3) Payload shapes (important)
64
+
65
+ Most tools accept either:
66
+
67
+ ```json
68
+ { "args": ["<project-name>"], "options": { "repoRoot": "<repo-root>" } }
69
+ ```
70
+
71
+ or named keys (recommended). The server merges top-level keys into `options`:
72
+
73
+ ```json
74
+ { "projectName": "<project-name>", "repoRoot": "<repo-root>" }
75
+ ```
76
+
77
+ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`, `target`, `taskRef`), pass it explicitly.
78
+
79
+ ## 4) Common calls (examples)
80
+
81
+ ### A) List projects
82
+
83
+ ```json
84
+ {
85
+ "name": "projects.listProjects",
86
+ "arguments": { "repoRoot": "<repo-root>" }
87
+ }
88
+ ```
89
+
90
+ ### B) List agents
91
+
92
+ ```json
93
+ {
94
+ "name": "agents.listAgents",
95
+ "arguments": { "repoRoot": "<repo-root>" }
96
+ }
97
+ ```
98
+
99
+ ### C) Set an active file (projects)
100
+
101
+ ```json
102
+ {
103
+ "name": "projects.setActive",
104
+ "arguments": {
105
+ "args": ["<project-name>", "/implementation-plan.v0.0.1"],
106
+ "options": { "repoRoot": "<repo-root>", "latest": true }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### D) Batch multiple calls
112
+
113
+ Call `batch` (or `<prefix>.batch`) to run multiple tool calls:
114
+
115
+ ```json
116
+ {
117
+ "name": "batch",
118
+ "arguments": {
119
+ "calls": [
120
+ { "tool": "projects.usage" },
121
+ { "tool": "projects.listProjects", "options": { "repoRoot": "<repo-root>" } }
122
+ ],
123
+ "continueOnError": true,
124
+ "maxConcurrency": 4
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## 5) Reading responses (envelopes + errors)
130
+
131
+ Tool results are returned as text containing JSON like:
132
+
133
+ - success: `{ "ok": true, "result": ... }`
134
+ - error: `{ "ok": false, "error": { "message": "...", "details": { ... } } }`
135
+
136
+ If you get:
137
+
138
+ - **Unknown tool**: use the `suggestions` from the error (when present), or call `mcp.listTools` again and retry.
139
+ - **Missing project name**: pass `projectName` (or set `args[0]`).
140
+ - **Project folder not found: ...projects/.../projects/...**: you likely passed the wrong `repoRoot` (it must be the repo root; legacy alias `processRoot`).
141
+
142
+ ## 6) Tool availability (read/write/admin)
143
+
144
+ The server can be started in modes that hide tools:
145
+
146
+ - read-only mode removes write-capable tools
147
+ - admin-only tools are hidden unless the server is started in admin mode
148
+ - root namespaces can be whitelisted (so entire namespaces may be missing)
149
+
150
+ If a tool is not listed by `mcp.listTools`, you cannot call it in the current server configuration.
@@ -32,9 +32,17 @@ describe("createExampleMcpServer endpoint whitelist", () => {
32
32
  const names = instance.tools.map((tool) => tool.name);
33
33
 
34
34
  expect(names.length).toBeGreaterThan(0);
35
- expect(names.every((name) => name.startsWith("projects."))).toBe(true);
35
+ expect(
36
+ names.every(
37
+ (name) => name.startsWith("projects.") || name.startsWith("mcp."),
38
+ ),
39
+ ).toBe(true);
36
40
  expect(names.some((name) => name.startsWith("agents."))).toBe(false);
37
41
  expect(names.some((name) => name.startsWith("net."))).toBe(false);
42
+ expect(names).toContain("mcp.usage");
43
+ expect(names).toContain("mcp.listTools");
44
+ expect(names).toContain("mcp.describeTool");
45
+ expect(names).not.toContain("mcp.search");
38
46
  });
39
47
 
40
48
  it("throws on unknown root endpoints", () => {
@@ -77,12 +85,20 @@ describe("createExampleMcpServer endpoint whitelist", () => {
77
85
  const names = instance.tools.map((tool) => tool.name);
78
86
 
79
87
  expect(names.length).toBeGreaterThan(0);
80
- expect(names.every((name) => name.startsWith("projects."))).toBe(true);
88
+ expect(
89
+ names.every(
90
+ (name) => name.startsWith("projects.") || name.startsWith("mcp."),
91
+ ),
92
+ ).toBe(true);
81
93
  expect(names).toContain("projects.readGitTask");
82
94
  expect(names).toContain("projects.fetchGitTasks");
83
95
  expect(names).not.toContain("projects.createGitIssue");
84
96
  expect(names).not.toContain("projects.writeGitTask");
85
97
  expect(names).not.toContain("projects.syncTasks");
98
+ expect(names).toContain("mcp.usage");
99
+ expect(names).toContain("mcp.listTools");
100
+ expect(names).toContain("mcp.describeTool");
101
+ expect(names).not.toContain("mcp.search");
86
102
  });
87
103
 
88
104
  it("re-enables issue write endpoints when enableIssues=true with disableWrite", () => {
@@ -186,6 +202,161 @@ describe("createExampleMcpServer request handling", () => {
186
202
  }
187
203
  });
188
204
 
205
+ it("accepts repoRoot for projects tools (preferred over legacy processRoot)", async () => {
206
+ const tempDir = await fs.mkdtemp(
207
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-"),
208
+ );
209
+ try {
210
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
211
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
212
+ recursive: true,
213
+ });
214
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
215
+ recursive: true,
216
+ });
217
+
218
+ const instance = createExampleMcpServer();
219
+ const handler = getToolHandler(instance);
220
+
221
+ const result = await handler(
222
+ {
223
+ method: "tools/call",
224
+ params: {
225
+ name: "projects.listProjects",
226
+ arguments: {
227
+ repoRoot: tempDir,
228
+ },
229
+ },
230
+ },
231
+ {},
232
+ );
233
+
234
+ expect(result.isError).toBe(false);
235
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
236
+ expect(payload.ok).toBe(true);
237
+ expect(payload.result).toContain("adl");
238
+ expect(payload.result).toContain("beta");
239
+ } finally {
240
+ await fs.rm(tempDir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ it("auto-detects repoRoot from process.cwd() when repoRoot is omitted", async () => {
245
+ const tempDir = await fs.mkdtemp(
246
+ path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
247
+ );
248
+ const originalCwd = process.cwd();
249
+
250
+ try {
251
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
252
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
253
+ recursive: true,
254
+ });
255
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
256
+ recursive: true,
257
+ });
258
+
259
+ process.chdir(tempDir);
260
+
261
+ const instance = createExampleMcpServer();
262
+ const handler = getToolHandler(instance);
263
+
264
+ const result = await handler(
265
+ {
266
+ method: "tools/call",
267
+ params: {
268
+ name: "projects.listProjects",
269
+ arguments: {},
270
+ },
271
+ },
272
+ {},
273
+ );
274
+
275
+ expect(result.isError).toBe(false);
276
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
277
+ expect(payload.ok).toBe(true);
278
+ expect(payload.result).toContain("adl");
279
+ expect(payload.result).toContain("beta");
280
+ } finally {
281
+ process.chdir(originalCwd);
282
+ await fs.rm(tempDir, { recursive: true, force: true });
283
+ }
284
+ });
285
+
286
+ it("still accepts legacy processRoot for backwards compatibility", async () => {
287
+ const tempDir = await fs.mkdtemp(
288
+ path.join(os.tmpdir(), "f0-mcp-server-processroot-"),
289
+ );
290
+ try {
291
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
292
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
293
+ recursive: true,
294
+ });
295
+
296
+ const instance = createExampleMcpServer();
297
+ const handler = getToolHandler(instance);
298
+
299
+ const result = await handler(
300
+ {
301
+ method: "tools/call",
302
+ params: {
303
+ name: "projects.listProjects",
304
+ arguments: {
305
+ processRoot: tempDir,
306
+ },
307
+ },
308
+ },
309
+ {},
310
+ );
311
+
312
+ expect(result.isError).toBe(false);
313
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
314
+ expect(payload.ok).toBe(true);
315
+ expect(payload.result).toContain("adl");
316
+ } finally {
317
+ await fs.rm(tempDir, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ it("normalizes repoRoot when caller accidentally passes a project root", async () => {
322
+ const tempDir = await fs.mkdtemp(
323
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
324
+ );
325
+ try {
326
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
327
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
328
+ recursive: true,
329
+ });
330
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
331
+ recursive: true,
332
+ });
333
+
334
+ const instance = createExampleMcpServer();
335
+ const handler = getToolHandler(instance);
336
+
337
+ const result = await handler(
338
+ {
339
+ method: "tools/call",
340
+ params: {
341
+ name: "projects.listProjects",
342
+ arguments: {
343
+ repoRoot: path.join(tempDir, "projects", "adl"),
344
+ },
345
+ },
346
+ },
347
+ {},
348
+ );
349
+
350
+ expect(result.isError).toBe(false);
351
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
352
+ expect(payload.ok).toBe(true);
353
+ expect(payload.result).toContain("adl");
354
+ expect(payload.result).toContain("beta");
355
+ } finally {
356
+ await fs.rm(tempDir, { recursive: true, force: true });
357
+ }
358
+ });
359
+
189
360
  it('parses continueOnError from string "false" (fails fast)', async () => {
190
361
  const instance = createExampleMcpServer();
191
362
  const handler = getToolHandler(instance);
@@ -256,7 +427,10 @@ describe("createExampleMcpServer request handling", () => {
256
427
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
257
428
  expect(payload.ok).toBe(true);
258
429
  expect(typeof payload.result).toBe("string");
259
- expect(payload.result).toContain("Usage");
430
+ expect(payload.result).toContain("MCP");
431
+ expect(payload.result).toContain("projects.listProjects");
432
+ expect(payload.result).not.toContain("f0 projects");
433
+ expect(payload.result).not.toContain("--generate-spec");
260
434
  });
261
435
 
262
436
  it("exposes mcp.describeTool for tool discovery", async () => {
package/mcp/server.ts CHANGED
@@ -48,6 +48,51 @@ type ApiEndpoint = {
48
48
  mcp: ToolNamespace;
49
49
  };
50
50
 
51
+ const MCP_HELPER_TOOL_KEYS = ["usage", "listTools", "describeTool"] as const;
52
+ type McpHelperToolKey = (typeof MCP_HELPER_TOOL_KEYS)[number];
53
+ const MCP_HELPER_TOOL_NAMES = new Set<string>(
54
+ MCP_HELPER_TOOL_KEYS.map((key) => `mcp.${key}`),
55
+ );
56
+
57
+ const formatToolNameForUsage = (
58
+ toolName: string,
59
+ toolsPrefix: string | undefined,
60
+ ): string => (toolsPrefix ? `${toolsPrefix}.${toolName}` : toolName);
61
+
62
+ const buildProjectsMcpUsage = (toolsPrefix: string | undefined): string => {
63
+ const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
64
+ return [
65
+ "MCP Projects usage (tools/call):",
66
+ `- ${tool("projects.listProjects")}: { options: { repoRoot: \"<repo-root>\" } }`,
67
+ `- ${tool("projects.generateSpec")}: { args: [\"<projectName>\"], options: { repoRoot: \"<repo-root>\" } }`,
68
+ `- ${tool("projects.setActive")}: { args: [\"<projectName>\", \"/implementation-plan.v0.0.1\"], options: { repoRoot: \"<repo-root>\", latest: true } }`,
69
+ "",
70
+ "Discovery:",
71
+ `- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
72
+ `- ${tool("mcp.describeTool")}: { args: [\"projects.generateSpec\"] } for schema + example`,
73
+ "",
74
+ "Payload shape:",
75
+ "- Prefer { args: [...], options: {...} }. Top-level keys are also treated as options.",
76
+ ].join("\n");
77
+ };
78
+
79
+ const buildAgentsMcpUsage = (toolsPrefix: string | undefined): string => {
80
+ const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
81
+ return [
82
+ "MCP Agents usage (tools/call):",
83
+ `- ${tool("agents.listAgents")}: { options: { repoRoot: \"<repo-root>\" } }`,
84
+ `- ${tool("agents.loadAgent")}: { args: [\"<agentName>\"], options: { repoRoot: \"<repo-root>\" } }`,
85
+ `- ${tool("agents.setActive")}: { args: [\"<agentName>\", \"/system/boot.v0.0.1\"], options: { repoRoot: \"<repo-root>\", latest: true } }`,
86
+ "",
87
+ "Discovery:",
88
+ `- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
89
+ `- ${tool("mcp.describeTool")}: { args: [\"agents.setActive\"] } for schema + example`,
90
+ "",
91
+ "Payload shape:",
92
+ "- Prefer { args: [...], options: {...} }. Top-level keys are also treated as options.",
93
+ ].join("\n");
94
+ };
95
+
51
96
  const isRecord = (value: unknown): value is Record<string, unknown> =>
52
97
  typeof value === "object" && value !== null && !Array.isArray(value);
53
98
 
@@ -131,11 +176,22 @@ const isDir = (candidate: string): boolean => {
131
176
  const looksLikeRepoRoot = (candidate: string): boolean =>
132
177
  isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
133
178
 
134
- const normalizeProcessRoot = (raw: string): string => {
179
+ const detectRepoRootFromCwd = (): string | null => {
180
+ let current = process.cwd();
181
+ for (let depth = 0; depth < 12; depth += 1) {
182
+ if (looksLikeRepoRoot(current)) return current;
183
+ const parent = path.dirname(current);
184
+ if (parent === current) break;
185
+ current = parent;
186
+ }
187
+ return null;
188
+ };
189
+
190
+ const normalizeRepoRoot = (raw: string): string => {
135
191
  const resolved = path.resolve(raw);
136
192
  if (looksLikeRepoRoot(resolved)) return resolved;
137
193
 
138
- // Common mistake: passing a project root like ".../projects/adl" as processRoot.
194
+ // Common mistake: passing a project root like ".../projects/adl" as repoRoot.
139
195
  // Try to find the containing repo root by walking up a few levels.
140
196
  let current = resolved;
141
197
  for (let depth = 0; depth < 8; depth += 1) {
@@ -155,14 +211,28 @@ const normalizeProcessRoot = (raw: string): string => {
155
211
  return resolved;
156
212
  };
157
213
 
158
- const normalizeProcessRootOption = (
214
+ const normalizeRepoRootOption = (
159
215
  options: Record<string, unknown>,
160
216
  ): 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 };
217
+ const rawRepoRoot = typeof options.repoRoot === "string" ? options.repoRoot : null;
218
+ const rawProcessRoot =
219
+ typeof options.processRoot === "string" ? options.processRoot : null;
220
+ const raw = rawRepoRoot ?? rawProcessRoot;
221
+
222
+ if (typeof raw !== "string" || raw.trim().length === 0) {
223
+ const detected = detectRepoRootFromCwd();
224
+ return detected ? { ...options, repoRoot: detected } : options;
225
+ }
226
+
227
+ const trimmed = raw.trim();
228
+ const normalized = normalizeRepoRoot(trimmed);
229
+
230
+ const next: Record<string, unknown> = { ...options, repoRoot: normalized };
231
+ delete next.processRoot;
232
+
233
+ const alreadyCanonical =
234
+ rawRepoRoot !== null && rawRepoRoot === normalized && !("processRoot" in options);
235
+ return alreadyCanonical ? options : next;
166
236
  };
167
237
 
168
238
  type NormalizedToolPayload = {
@@ -283,7 +353,7 @@ const normalizePayload = (payload: unknown): NormalizedToolPayload => {
283
353
 
284
354
  return {
285
355
  args,
286
- options: normalizeProcessRootOption(options),
356
+ options: normalizeRepoRootOption(options),
287
357
  };
288
358
  };
289
359
 
@@ -390,7 +460,7 @@ const coercePayloadForTool = (
390
460
 
391
461
  switch (toolName) {
392
462
  case "projects.listProjects": {
393
- // No positional args. processRoot is handled via options.processRoot + buildProcessRootOnly.
463
+ // No positional args. repoRoot is handled via options.repoRoot + buildRepoRootOnly.
394
464
  break;
395
465
  }
396
466
  case "projects.resolveProjectRoot":
@@ -480,7 +550,7 @@ const coercePayloadForTool = (
480
550
  break;
481
551
  }
482
552
 
483
- return { args, options: normalizeProcessRootOption(options) };
553
+ return { args, options: normalizeRepoRootOption(options) };
484
554
  };
485
555
 
486
556
  const normalizeBatchToolCall = (
@@ -791,7 +861,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
791
861
  description: "Unused.",
792
862
  additionalProperties: true,
793
863
  },
794
- processRoot: {
864
+ repoRoot: {
795
865
  type: "string",
796
866
  description: "Unused.",
797
867
  },
@@ -801,7 +871,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
801
871
  type: "object",
802
872
  additionalProperties: true,
803
873
  properties: {
804
- processRoot: {
874
+ repoRoot: {
805
875
  type: "string",
806
876
  description:
807
877
  'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
@@ -817,7 +887,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
817
887
  type: "string",
818
888
  description: 'Project name under /projects (e.g. "adl").',
819
889
  },
820
- processRoot: {
890
+ repoRoot: {
821
891
  type: "string",
822
892
  description:
823
893
  "Repo root containing /projects and /agents. If omitted, uses server cwd.",
@@ -843,7 +913,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
843
913
  type: "string",
844
914
  description: 'Project name under /projects (e.g. "adl").',
845
915
  },
846
- processRoot: {
916
+ repoRoot: {
847
917
  type: "string",
848
918
  description: "Repo root containing /projects and /agents.",
849
919
  },
@@ -873,7 +943,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
873
943
  description:
874
944
  'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
875
945
  },
876
- processRoot: {
946
+ repoRoot: {
877
947
  type: "string",
878
948
  description: "Repo root containing /projects and /agents.",
879
949
  },
@@ -950,7 +1020,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
950
1020
  type: "string",
951
1021
  description: "Remote repo override for gitea mode.",
952
1022
  },
953
- processRoot: {
1023
+ repoRoot: {
954
1024
  type: "string",
955
1025
  description: "Repo root containing /projects and /agents.",
956
1026
  },
@@ -1027,7 +1097,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1027
1097
  type: "string",
1028
1098
  description: "Remote repo override for gitea mode.",
1029
1099
  },
1030
- processRoot: {
1100
+ repoRoot: {
1031
1101
  type: "string",
1032
1102
  description: "Repo root containing /projects and /agents.",
1033
1103
  },
@@ -1143,7 +1213,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1143
1213
  type: "boolean",
1144
1214
  description: "If true, only return issues with TASK-* IDs.",
1145
1215
  },
1146
- processRoot: {
1216
+ repoRoot: {
1147
1217
  type: "string",
1148
1218
  description: "Repo root containing /projects.",
1149
1219
  },
@@ -1184,7 +1254,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1184
1254
  type: "boolean",
1185
1255
  description: "If true, restrict to issues with TASK-* payloads.",
1186
1256
  },
1187
- processRoot: {
1257
+ repoRoot: {
1188
1258
  type: "string",
1189
1259
  description: "Repo root containing /projects.",
1190
1260
  },
@@ -1241,7 +1311,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1241
1311
  type: "string",
1242
1312
  description: "Optional task signature.",
1243
1313
  },
1244
- processRoot: {
1314
+ repoRoot: {
1245
1315
  type: "string",
1246
1316
  description: "Repo root containing /projects.",
1247
1317
  },
@@ -1275,7 +1345,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1275
1345
  items: { type: "string" },
1276
1346
  description: "Labels to set.",
1277
1347
  },
1278
- processRoot: {
1348
+ repoRoot: {
1279
1349
  type: "string",
1280
1350
  description: "Repo root containing /projects.",
1281
1351
  },
@@ -1349,7 +1419,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1349
1419
  type: "string",
1350
1420
  description: "Optional override cache directory.",
1351
1421
  },
1352
- processRoot: {
1422
+ repoRoot: {
1353
1423
  type: "string",
1354
1424
  description:
1355
1425
  "Repo root containing /projects. If you pass a project root, it will be normalized.",
@@ -1384,7 +1454,7 @@ const buildArgsSchemaFromPlaceholders = (
1384
1454
  const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
1385
1455
  "projects.listProjects": {
1386
1456
  type: "array",
1387
- description: "No positional arguments. Use options.processRoot if needed.",
1457
+ description: "No positional arguments. Use options.repoRoot if needed.",
1388
1458
  minItems: 0,
1389
1459
  maxItems: 0,
1390
1460
  items: {},
@@ -1499,9 +1569,9 @@ const getInvocationPlanName = (toolName: string): string => {
1499
1569
  const plan = toolInvocationPlans[toolName];
1500
1570
  if (!plan) return "default";
1501
1571
  if (plan === buildOptionsOnly) return "optionsOnly";
1502
- if (plan === buildOptionsThenProcessRoot) return "optionsThenProcessRoot";
1503
- if (plan === buildProcessRootThenOptions) return "processRootThenOptions";
1504
- if (plan === buildProcessRootOnly) return "processRootOnly";
1572
+ if (plan === buildOptionsThenRepoRoot) return "optionsThenRepoRoot";
1573
+ if (plan === buildRepoRootThenOptions) return "repoRootThenOptions";
1574
+ if (plan === buildRepoRootOnly) return "repoRootOnly";
1505
1575
  return "custom";
1506
1576
  };
1507
1577
 
@@ -1513,17 +1583,17 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1513
1583
  const example: Record<string, unknown> = {};
1514
1584
  if (requiredArgs && requiredArgs.length > 0) {
1515
1585
  example.args = [...requiredArgs];
1516
- } else if (plan !== "processRootOnly") {
1586
+ } else if (plan !== "repoRootOnly") {
1517
1587
  example.args = ["<arg0>"];
1518
1588
  }
1519
1589
 
1520
- if (plan === "processRootOnly") {
1521
- example.options = { processRoot: "<repo-root>", ...defaultOptions };
1590
+ if (plan === "repoRootOnly") {
1591
+ example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1522
1592
  return example;
1523
1593
  }
1524
1594
 
1525
- if (plan === "optionsThenProcessRoot" || plan === "processRootThenOptions") {
1526
- example.options = { processRoot: "<repo-root>", ...defaultOptions };
1595
+ if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1596
+ example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1527
1597
  return example;
1528
1598
  }
1529
1599
 
@@ -1554,7 +1624,7 @@ const defaultToolInputSchema = (toolName: string) => ({
1554
1624
  additionalProperties: true,
1555
1625
  description: "Named options",
1556
1626
  },
1557
- processRoot: {
1627
+ repoRoot: {
1558
1628
  type: "string",
1559
1629
  description:
1560
1630
  'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
@@ -1686,43 +1756,43 @@ const buildOptionsOnly = (
1686
1756
  return invocationArgs;
1687
1757
  };
1688
1758
 
1689
- const buildOptionsThenProcessRoot = (
1759
+ const buildOptionsThenRepoRoot = (
1690
1760
  args: unknown[],
1691
1761
  options: Record<string, unknown>,
1692
1762
  ): unknown[] => {
1693
1763
  const invocationArgs: unknown[] = [...args];
1694
1764
  const remaining = { ...options };
1695
- const processRoot = remaining.processRoot;
1696
- if (typeof processRoot === "string") {
1697
- delete remaining.processRoot;
1765
+ const repoRoot = remaining.repoRoot;
1766
+ if (typeof repoRoot === "string") {
1767
+ delete remaining.repoRoot;
1698
1768
  }
1699
1769
 
1700
1770
  if (Object.keys(remaining).length > 0) {
1701
1771
  invocationArgs.push(remaining);
1702
- } else if (typeof processRoot === "string") {
1703
- // Preserve positional slot for signatures like fn(projectName, options?, processRoot?).
1772
+ } else if (typeof repoRoot === "string") {
1773
+ // Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
1704
1774
  invocationArgs.push({});
1705
1775
  }
1706
- if (typeof processRoot === "string") {
1707
- invocationArgs.push(processRoot);
1776
+ if (typeof repoRoot === "string") {
1777
+ invocationArgs.push(repoRoot);
1708
1778
  }
1709
1779
 
1710
1780
  return invocationArgs;
1711
1781
  };
1712
1782
 
1713
- const buildProcessRootThenOptions = (
1783
+ const buildRepoRootThenOptions = (
1714
1784
  args: unknown[],
1715
1785
  options: Record<string, unknown>,
1716
1786
  ): unknown[] => {
1717
1787
  const invocationArgs: unknown[] = [...args];
1718
1788
  const remaining = { ...options };
1719
- const processRoot = remaining.processRoot;
1720
- if (typeof processRoot === "string") {
1721
- delete remaining.processRoot;
1789
+ const repoRoot = remaining.repoRoot;
1790
+ if (typeof repoRoot === "string") {
1791
+ delete remaining.repoRoot;
1722
1792
  }
1723
1793
 
1724
- if (typeof processRoot === "string") {
1725
- invocationArgs.push(processRoot);
1794
+ if (typeof repoRoot === "string") {
1795
+ invocationArgs.push(repoRoot);
1726
1796
  }
1727
1797
  if (Object.keys(remaining).length > 0) {
1728
1798
  invocationArgs.push(remaining);
@@ -1731,39 +1801,39 @@ const buildProcessRootThenOptions = (
1731
1801
  return invocationArgs;
1732
1802
  };
1733
1803
 
1734
- const buildProcessRootOnly = (
1804
+ const buildRepoRootOnly = (
1735
1805
  args: unknown[],
1736
1806
  options: Record<string, unknown>,
1737
1807
  ): unknown[] => {
1738
1808
  const invocationArgs: unknown[] = [...args];
1739
- const processRoot = options.processRoot;
1740
- if (typeof processRoot === "string") {
1741
- invocationArgs.push(processRoot);
1809
+ const repoRoot = options.repoRoot;
1810
+ if (typeof repoRoot === "string") {
1811
+ invocationArgs.push(repoRoot);
1742
1812
  }
1743
1813
  return invocationArgs;
1744
1814
  };
1745
1815
 
1746
1816
  const toolInvocationPlans: Record<string, ToolInvoker> = {
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,
1817
+ "agents.setActive": buildRepoRootThenOptions,
1818
+ "agents.resolveAgentsRootFrom": buildRepoRootOnly,
1819
+ "projects.setActive": buildRepoRootThenOptions,
1820
+ "projects.generateSpec": buildOptionsThenRepoRoot,
1821
+ "projects.syncTasks": buildOptionsThenRepoRoot,
1822
+ "projects.clearIssues": buildOptionsThenRepoRoot,
1823
+ "projects.fetchGitTasks": buildOptionsThenRepoRoot,
1824
+ "projects.createGitIssue": buildOptionsThenRepoRoot,
1825
+ "projects.readGitTask": buildOptionsThenRepoRoot,
1826
+ "projects.writeGitTask": buildOptionsThenRepoRoot,
1757
1827
  "agents.resolveTargetFile": buildOptionsOnly,
1758
1828
  "projects.resolveProjectTargetFile": buildOptionsOnly,
1759
- "agents.loadAgent": buildProcessRootOnly,
1760
- "agents.loadAgentPrompt": buildProcessRootOnly,
1829
+ "agents.loadAgent": buildRepoRootOnly,
1830
+ "agents.loadAgentPrompt": buildRepoRootOnly,
1761
1831
  "projects.resolveImplementationPlan": (args, options) => {
1762
1832
  const invocationArgs: unknown[] = [...args];
1763
1833
  const remaining = { ...options };
1764
- const processRoot = remaining.processRoot;
1765
- if (typeof processRoot === "string") {
1766
- delete remaining.processRoot;
1834
+ const repoRoot = remaining.repoRoot;
1835
+ if (typeof repoRoot === "string") {
1836
+ delete remaining.repoRoot;
1767
1837
  }
1768
1838
 
1769
1839
  // This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
@@ -1775,17 +1845,17 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
1775
1845
  invocationArgs.push(remaining);
1776
1846
  }
1777
1847
 
1778
- // Intentionally do NOT append processRoot: projectRoot is the first positional argument.
1848
+ // Intentionally do NOT append repoRoot: projectRoot is the first positional argument.
1779
1849
  return invocationArgs;
1780
1850
  },
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,
1851
+ "agents.main": buildRepoRootOnly,
1852
+ "agents.resolveAgentsRoot": buildRepoRootOnly,
1853
+ "agents.listAgents": buildRepoRootOnly,
1854
+ "projects.resolveProjectRoot": buildRepoRootOnly,
1855
+ "projects.listProjects": buildRepoRootOnly,
1856
+ "projects.listProjectDocs": buildRepoRootOnly,
1857
+ "projects.readProjectDoc": buildRepoRootOnly,
1858
+ "projects.main": buildRepoRootOnly,
1789
1859
  };
1790
1860
 
1791
1861
  const invokeTool = async (
@@ -2026,9 +2096,10 @@ export const createExampleMcpServer = (
2026
2096
  },
2027
2097
  search: async (input: unknown) => {
2028
2098
  const payload = isRecord(input) ? input : {};
2029
- const processRoot = (() => {
2030
- const raw = parseString(payload.processRoot);
2031
- return raw ? normalizeProcessRoot(raw) : process.cwd();
2099
+ const repoRoot = (() => {
2100
+ const raw =
2101
+ parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2102
+ return raw ? normalizeRepoRoot(raw) : process.cwd();
2032
2103
  })();
2033
2104
 
2034
2105
  const sectionRaw = parseString(payload.section)?.toLowerCase();
@@ -2076,11 +2147,11 @@ export const createExampleMcpServer = (
2076
2147
 
2077
2148
  const projectName = ensureProjectName(
2078
2149
  parseString(payload.projectName),
2079
- processRoot,
2150
+ repoRoot,
2080
2151
  );
2081
2152
 
2082
2153
  const searchOptions: Record<string, unknown> = {
2083
- processRoot,
2154
+ processRoot: repoRoot,
2084
2155
  };
2085
2156
 
2086
2157
  const source = parseString(payload.source)?.toLowerCase();
@@ -2106,9 +2177,15 @@ export const createExampleMcpServer = (
2106
2177
  } satisfies ToolNamespace;
2107
2178
 
2108
2179
  const api: ApiEndpoint = {
2109
- agents: agentsApi,
2180
+ agents: {
2181
+ ...agentsApi,
2182
+ usage: () => buildAgentsMcpUsage(options.toolsPrefix),
2183
+ },
2110
2184
  net: netApi,
2111
- projects: projectsApi,
2185
+ projects: {
2186
+ ...projectsApi,
2187
+ usage: () => buildProjectsMcpUsage(options.toolsPrefix),
2188
+ },
2112
2189
  mcp: mcpApi,
2113
2190
  };
2114
2191
 
@@ -2140,7 +2217,18 @@ export const createExampleMcpServer = (
2140
2217
  const selectedTools = selectedRoots.flatMap((root) =>
2141
2218
  collectTools(api[root], [root]),
2142
2219
  );
2143
- const selectedToolsWithAliases = applyToolAliases(selectedTools);
2220
+ const selectedToolsWithMcpHelpers = (() => {
2221
+ if (selectedRoots.includes("mcp")) return selectedTools;
2222
+ const helperApi: Partial<Record<McpHelperToolKey, unknown>> = {};
2223
+ for (const key of MCP_HELPER_TOOL_KEYS) {
2224
+ helperApi[key] = (mcpApi as any)[key];
2225
+ }
2226
+ const helperTools = collectTools(helperApi as ToolNamespace, ["mcp"]).filter(
2227
+ (tool) => MCP_HELPER_TOOL_NAMES.has(tool.name),
2228
+ );
2229
+ return [...helperTools, ...selectedTools];
2230
+ })();
2231
+ const selectedToolsWithAliases = applyToolAliases(selectedToolsWithMcpHelpers);
2144
2232
  buildToolMeta(selectedToolsWithAliases);
2145
2233
  const adminFilteredTools = Boolean(options.admin)
2146
2234
  ? selectedToolsWithAliases
@@ -2207,7 +2295,7 @@ export const createExampleMcpServer = (
2207
2295
  tool: prefix
2208
2296
  ? `${prefix}.projects.listProjects`
2209
2297
  : "projects.listProjects",
2210
- options: { processRoot: "<repo-root>" },
2298
+ options: { repoRoot: "<repo-root>" },
2211
2299
  },
2212
2300
  ],
2213
2301
  continueOnError: true,
@@ -2386,7 +2474,7 @@ export const createExampleMcpServer = (
2386
2474
  /projects[\\/].+projects[\\/]/i.test(message)
2387
2475
  ) {
2388
2476
  details.hint =
2389
- "You likely passed a project root as processRoot. processRoot should be the repo root containing /projects.";
2477
+ "You likely passed a project root as repoRoot. repoRoot should be the repo root containing /projects. (Legacy alias: processRoot.)";
2390
2478
  }
2391
2479
 
2392
2480
  if (/Missing search pattern\./i.test(message)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foundation0/api",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Foundation 0 API",
5
5
  "type": "module",
6
6
  "bin": {