@foundation0/api 1.1.4 → 1.1.6

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/cli.ts CHANGED
@@ -67,6 +67,8 @@ const parseListArg = (value: string | undefined): string[] => {
67
67
  const serverName = getArgValue('--server-name', 'f0-mcp')
68
68
  const serverVersion = getArgValue('--server-version', '1.0.0')
69
69
  const toolsPrefix = getArgValue('--tools-prefix') ?? process.env.MCP_TOOLS_PREFIX
70
+ const repoRoot =
71
+ getArgValue('--repo-root') ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT
70
72
  const allowedRootEndpoints = parseListArg(
71
73
  getArgValue('--allowed-root-endpoints') ?? process.env.MCP_ALLOWED_ROOT_ENDPOINTS,
72
74
  )
@@ -80,6 +82,9 @@ if (hasFlag('--help') || hasFlag('-h')) {
80
82
  console.log(' --server-name <name>')
81
83
  console.log(' --server-version <version>')
82
84
  console.log(' --tools-prefix <prefix>')
85
+ console.log(' --repo-root <path>')
86
+ console.log(' Default repo root on the server filesystem (contains /api and /projects).')
87
+ console.log(' Env: MCP_REPO_ROOT or F0_REPO_ROOT.')
83
88
  console.log(' --allowed-root-endpoints <csv>')
84
89
  console.log(' Example: --allowed-root-endpoints projects')
85
90
  console.log(' Example: --allowed-root-endpoints agents,projects')
@@ -96,6 +101,7 @@ void runExampleMcpServer({
96
101
  serverName: serverName ?? undefined,
97
102
  serverVersion: serverVersion ?? undefined,
98
103
  toolsPrefix,
104
+ repoRoot: repoRoot ?? undefined,
99
105
  allowedRootEndpoints,
100
106
  disableWrite,
101
107
  enableIssues,
package/mcp/manual.md ADDED
@@ -0,0 +1,161 @@
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
+ Note: `repoRoot` is a path on the **server's filesystem** (not the client/LLM). If you're unsure what the server can see, call `mcp.workspace`.
15
+
16
+ ## 1) Tool naming (prefixes + aliases)
17
+
18
+ Depending on how the server was started, tool names may be:
19
+
20
+ - unprefixed: `projects.listProjects`
21
+ - prefixed: `api.projects.listProjects`
22
+
23
+ The server usually exposes both when a prefix is set, but do not assume: **discover at runtime** via `mcp.listTools`.
24
+
25
+ Some tools also have OpenAI-safe underscore aliases (no dots). Example:
26
+
27
+ - `net.curl` may also be available as `net_curl`
28
+
29
+ ## 2) The 4 discovery calls
30
+
31
+ ### A) List all tools
32
+
33
+ Tool call:
34
+
35
+ ```json
36
+ { "name": "mcp.listTools", "arguments": {} }
37
+ ```
38
+
39
+ ### B) Describe one tool (schema + example)
40
+
41
+ Tool call (prefixed or unprefixed names both work here):
42
+
43
+ ```json
44
+ { "name": "mcp.describeTool", "arguments": { "args": ["projects.listProjects"] } }
45
+ ```
46
+
47
+ ### C) `mcp.search` for "search docs/spec" requests
48
+
49
+ Tool call:
50
+
51
+ ```json
52
+ {
53
+ "name": "mcp.search",
54
+ "arguments": {
55
+ "projectName": "<project-name>",
56
+ "section": "spec",
57
+ "pattern": "authentication",
58
+ "repoRoot": "<repo-root>",
59
+ "ignoreCase": true,
60
+ "maxCount": 50
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### D) `mcp.workspace` to debug repoRoot/cwd issues
66
+
67
+ Tool call:
68
+
69
+ ```json
70
+ { "name": "mcp.workspace", "arguments": {} }
71
+ ```
72
+
73
+ ## 3) Payload shapes (important)
74
+
75
+ Most tools accept either:
76
+
77
+ ```json
78
+ { "args": ["<project-name>"], "options": { "repoRoot": "<repo-root>" } }
79
+ ```
80
+
81
+ or named keys (recommended). The server merges top-level keys into `options`:
82
+
83
+ ```json
84
+ { "projectName": "<project-name>", "repoRoot": "<repo-root>" }
85
+ ```
86
+
87
+ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`, `target`, `taskRef`), pass it explicitly.
88
+
89
+ ## 4) Common calls (examples)
90
+
91
+ ### A) List projects
92
+
93
+ ```json
94
+ {
95
+ "name": "projects.listProjects",
96
+ "arguments": { "repoRoot": "<repo-root>" }
97
+ }
98
+ ```
99
+
100
+ ### B) List agents
101
+
102
+ ```json
103
+ {
104
+ "name": "agents.listAgents",
105
+ "arguments": { "repoRoot": "<repo-root>" }
106
+ }
107
+ ```
108
+
109
+ ### C) Set an active file (projects)
110
+
111
+ ```json
112
+ {
113
+ "name": "projects.setActive",
114
+ "arguments": {
115
+ "args": ["<project-name>", "/implementation-plan.v0.0.1"],
116
+ "options": { "repoRoot": "<repo-root>", "latest": true }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### D) Batch multiple calls
122
+
123
+ Call `batch` (or `<prefix>.batch`) to run multiple tool calls:
124
+
125
+ ```json
126
+ {
127
+ "name": "batch",
128
+ "arguments": {
129
+ "calls": [
130
+ { "tool": "projects.usage" },
131
+ { "tool": "projects.listProjects", "options": { "repoRoot": "<repo-root>" } }
132
+ ],
133
+ "continueOnError": true,
134
+ "maxConcurrency": 4
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## 5) Reading responses (envelopes + errors)
140
+
141
+ Tool results are returned as text containing JSON like:
142
+
143
+ - success: `{ "ok": true, "result": ... }`
144
+ - error: `{ "ok": false, "error": { "message": "...", "details": { ... } } }`
145
+
146
+ If you get:
147
+
148
+ - **Unknown tool**: use the `suggestions` from the error (when present), or call `mcp.listTools` again and retry.
149
+ - **Missing project name**: pass `projectName` (or set `args[0]`).
150
+ - **Project folder not found: ...projects/.../projects/...**: you likely passed the wrong `repoRoot` (it must be the repo root; legacy alias `processRoot`).
151
+ - **`projects.listProjects()` returns `[]` unexpectedly**: call `mcp.workspace` to confirm the server’s `cwd`/`repoRoot` and whether `/projects` exists.
152
+
153
+ ## 6) Tool availability (read/write/admin)
154
+
155
+ The server can be started in modes that hide tools:
156
+
157
+ - read-only mode removes write-capable tools
158
+ - admin-only tools are hidden unless the server is started in admin mode
159
+ - root namespaces can be whitelisted (so entire namespaces may be missing)
160
+
161
+ If a tool is not listed by `mcp.listTools`, you cannot call it in the current server configuration.
@@ -32,9 +32,18 @@ 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).toContain("mcp.workspace");
46
+ expect(names).not.toContain("mcp.search");
38
47
  });
39
48
 
40
49
  it("throws on unknown root endpoints", () => {
@@ -77,12 +86,21 @@ describe("createExampleMcpServer endpoint whitelist", () => {
77
86
  const names = instance.tools.map((tool) => tool.name);
78
87
 
79
88
  expect(names.length).toBeGreaterThan(0);
80
- expect(names.every((name) => name.startsWith("projects."))).toBe(true);
89
+ expect(
90
+ names.every(
91
+ (name) => name.startsWith("projects.") || name.startsWith("mcp."),
92
+ ),
93
+ ).toBe(true);
81
94
  expect(names).toContain("projects.readGitTask");
82
95
  expect(names).toContain("projects.fetchGitTasks");
83
96
  expect(names).not.toContain("projects.createGitIssue");
84
97
  expect(names).not.toContain("projects.writeGitTask");
85
98
  expect(names).not.toContain("projects.syncTasks");
99
+ expect(names).toContain("mcp.usage");
100
+ expect(names).toContain("mcp.listTools");
101
+ expect(names).toContain("mcp.describeTool");
102
+ expect(names).toContain("mcp.workspace");
103
+ expect(names).not.toContain("mcp.search");
86
104
  });
87
105
 
88
106
  it("re-enables issue write endpoints when enableIssues=true with disableWrite", () => {
@@ -186,6 +204,233 @@ describe("createExampleMcpServer request handling", () => {
186
204
  }
187
205
  });
188
206
 
207
+ it("accepts repoRoot for projects tools (preferred over legacy processRoot)", async () => {
208
+ const tempDir = await fs.mkdtemp(
209
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-"),
210
+ );
211
+ try {
212
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
213
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
214
+ recursive: true,
215
+ });
216
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
217
+ recursive: true,
218
+ });
219
+
220
+ const instance = createExampleMcpServer();
221
+ const handler = getToolHandler(instance);
222
+
223
+ const result = await handler(
224
+ {
225
+ method: "tools/call",
226
+ params: {
227
+ name: "projects.listProjects",
228
+ arguments: {
229
+ repoRoot: tempDir,
230
+ },
231
+ },
232
+ },
233
+ {},
234
+ );
235
+
236
+ expect(result.isError).toBe(false);
237
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
238
+ expect(payload.ok).toBe(true);
239
+ expect(payload.result).toContain("adl");
240
+ expect(payload.result).toContain("beta");
241
+ } finally {
242
+ await fs.rm(tempDir, { recursive: true, force: true });
243
+ }
244
+ });
245
+
246
+ it("auto-detects repoRoot from process.cwd() when repoRoot is omitted", async () => {
247
+ const tempDir = await fs.mkdtemp(
248
+ path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
249
+ );
250
+ const originalCwd = process.cwd();
251
+
252
+ try {
253
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
254
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
255
+ recursive: true,
256
+ });
257
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
258
+ recursive: true,
259
+ });
260
+
261
+ process.chdir(tempDir);
262
+
263
+ const instance = createExampleMcpServer();
264
+ const handler = getToolHandler(instance);
265
+
266
+ const result = await handler(
267
+ {
268
+ method: "tools/call",
269
+ params: {
270
+ name: "projects.listProjects",
271
+ arguments: {},
272
+ },
273
+ },
274
+ {},
275
+ );
276
+
277
+ expect(result.isError).toBe(false);
278
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
279
+ expect(payload.ok).toBe(true);
280
+ expect(payload.result).toContain("adl");
281
+ expect(payload.result).toContain("beta");
282
+ } finally {
283
+ process.chdir(originalCwd);
284
+ await fs.rm(tempDir, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ it("still accepts legacy processRoot for backwards compatibility", async () => {
289
+ const tempDir = await fs.mkdtemp(
290
+ path.join(os.tmpdir(), "f0-mcp-server-processroot-"),
291
+ );
292
+ try {
293
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
294
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
295
+ recursive: true,
296
+ });
297
+
298
+ const instance = createExampleMcpServer();
299
+ const handler = getToolHandler(instance);
300
+
301
+ const result = await handler(
302
+ {
303
+ method: "tools/call",
304
+ params: {
305
+ name: "projects.listProjects",
306
+ arguments: {
307
+ processRoot: tempDir,
308
+ },
309
+ },
310
+ },
311
+ {},
312
+ );
313
+
314
+ expect(result.isError).toBe(false);
315
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
316
+ expect(payload.ok).toBe(true);
317
+ expect(payload.result).toContain("adl");
318
+ } finally {
319
+ await fs.rm(tempDir, { recursive: true, force: true });
320
+ }
321
+ });
322
+
323
+ it("normalizes repoRoot when caller accidentally passes a project root", async () => {
324
+ const tempDir = await fs.mkdtemp(
325
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
326
+ );
327
+ try {
328
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
329
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
330
+ recursive: true,
331
+ });
332
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
333
+ recursive: true,
334
+ });
335
+
336
+ const instance = createExampleMcpServer();
337
+ const handler = getToolHandler(instance);
338
+
339
+ const result = await handler(
340
+ {
341
+ method: "tools/call",
342
+ params: {
343
+ name: "projects.listProjects",
344
+ arguments: {
345
+ repoRoot: path.join(tempDir, "projects", "adl"),
346
+ },
347
+ },
348
+ },
349
+ {},
350
+ );
351
+
352
+ expect(result.isError).toBe(false);
353
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
354
+ expect(payload.ok).toBe(true);
355
+ expect(payload.result).toContain("adl");
356
+ expect(payload.result).toContain("beta");
357
+ } finally {
358
+ await fs.rm(tempDir, { recursive: true, force: true });
359
+ }
360
+ });
361
+
362
+ it("uses server default repoRoot when repoRoot is omitted", async () => {
363
+ const tempDir = await fs.mkdtemp(
364
+ path.join(os.tmpdir(), "f0-mcp-server-default-reporoot-"),
365
+ );
366
+ try {
367
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
368
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
369
+ recursive: true,
370
+ });
371
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
372
+ recursive: true,
373
+ });
374
+
375
+ const instance = createExampleMcpServer({ repoRoot: tempDir });
376
+ const handler = getToolHandler(instance);
377
+
378
+ const result = await handler(
379
+ {
380
+ method: "tools/call",
381
+ params: {
382
+ name: "projects.listProjects",
383
+ arguments: {},
384
+ },
385
+ },
386
+ {},
387
+ );
388
+
389
+ expect(result.isError).toBe(false);
390
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
391
+ expect(payload.ok).toBe(true);
392
+ expect(payload.result).toContain("adl");
393
+ expect(payload.result).toContain("beta");
394
+ } finally {
395
+ await fs.rm(tempDir, { recursive: true, force: true });
396
+ }
397
+ });
398
+
399
+ it("exposes mcp.workspace to explain server filesystem context", async () => {
400
+ const tempDir = await fs.mkdtemp(
401
+ path.join(os.tmpdir(), "f0-mcp-server-workspace-"),
402
+ );
403
+ try {
404
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
405
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
406
+ recursive: true,
407
+ });
408
+
409
+ const instance = createExampleMcpServer({ repoRoot: tempDir });
410
+ const handler = getToolHandler(instance);
411
+
412
+ const result = await handler(
413
+ {
414
+ method: "tools/call",
415
+ params: {
416
+ name: "mcp.workspace",
417
+ arguments: {},
418
+ },
419
+ },
420
+ {},
421
+ );
422
+
423
+ expect(result.isError).toBe(false);
424
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
425
+ expect(payload.ok).toBe(true);
426
+ expect(payload.result.repoRoot).toBe(path.resolve(tempDir));
427
+ expect(payload.result.hasProjectsDir).toBe(true);
428
+ expect(Array.isArray(payload.result.projects)).toBe(true);
429
+ } finally {
430
+ await fs.rm(tempDir, { recursive: true, force: true });
431
+ }
432
+ });
433
+
189
434
  it('parses continueOnError from string "false" (fails fast)', async () => {
190
435
  const instance = createExampleMcpServer();
191
436
  const handler = getToolHandler(instance);
@@ -256,7 +501,10 @@ describe("createExampleMcpServer request handling", () => {
256
501
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
257
502
  expect(payload.ok).toBe(true);
258
503
  expect(typeof payload.result).toBe("string");
259
- expect(payload.result).toContain("Usage");
504
+ expect(payload.result).toContain("MCP");
505
+ expect(payload.result).toContain("projects.listProjects");
506
+ expect(payload.result).not.toContain("f0 projects");
507
+ expect(payload.result).not.toContain("--generate-spec");
260
508
  });
261
509
 
262
510
  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", "workspace"] 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,11 @@ 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 normalizeRepoRoot = (raw: string): string => {
135
180
  const resolved = path.resolve(raw);
136
181
  if (looksLikeRepoRoot(resolved)) return resolved;
137
182
 
138
- // Common mistake: passing a project root like ".../projects/adl" as processRoot.
183
+ // Common mistake: passing a project root like ".../projects/adl" as repoRoot.
139
184
  // Try to find the containing repo root by walking up a few levels.
140
185
  let current = resolved;
141
186
  for (let depth = 0; depth < 8; depth += 1) {
@@ -155,14 +200,30 @@ const normalizeProcessRoot = (raw: string): string => {
155
200
  return resolved;
156
201
  };
157
202
 
158
- const normalizeProcessRootOption = (
203
+ const normalizeRepoRootOption = (
159
204
  options: Record<string, unknown>,
160
205
  ): 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 };
206
+ const rawRepoRoot = typeof options.repoRoot === "string" ? options.repoRoot : null;
207
+ const rawProcessRoot =
208
+ typeof options.processRoot === "string" ? options.processRoot : null;
209
+ const raw = rawRepoRoot ?? rawProcessRoot;
210
+
211
+ if (typeof raw !== "string" || raw.trim().length === 0) {
212
+ const next = { ...options };
213
+ delete next.repoRoot;
214
+ delete next.processRoot;
215
+ return next;
216
+ }
217
+
218
+ const trimmed = raw.trim();
219
+ const normalized = normalizeRepoRoot(trimmed);
220
+
221
+ const next: Record<string, unknown> = { ...options, repoRoot: normalized };
222
+ delete next.processRoot;
223
+
224
+ const alreadyCanonical =
225
+ rawRepoRoot !== null && rawRepoRoot === normalized && !("processRoot" in options);
226
+ return alreadyCanonical ? options : next;
166
227
  };
167
228
 
168
229
  type NormalizedToolPayload = {
@@ -283,7 +344,7 @@ const normalizePayload = (payload: unknown): NormalizedToolPayload => {
283
344
 
284
345
  return {
285
346
  args,
286
- options: normalizeProcessRootOption(options),
347
+ options: normalizeRepoRootOption(options),
287
348
  };
288
349
  };
289
350
 
@@ -390,7 +451,7 @@ const coercePayloadForTool = (
390
451
 
391
452
  switch (toolName) {
392
453
  case "projects.listProjects": {
393
- // No positional args. processRoot is handled via options.processRoot + buildProcessRootOnly.
454
+ // No positional args. repoRoot is handled via options.repoRoot + buildRepoRootOnly.
394
455
  break;
395
456
  }
396
457
  case "projects.resolveProjectRoot":
@@ -480,7 +541,7 @@ const coercePayloadForTool = (
480
541
  break;
481
542
  }
482
543
 
483
- return { args, options: normalizeProcessRootOption(options) };
544
+ return { args, options: normalizeRepoRootOption(options) };
484
545
  };
485
546
 
486
547
  const normalizeBatchToolCall = (
@@ -791,7 +852,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
791
852
  description: "Unused.",
792
853
  additionalProperties: true,
793
854
  },
794
- processRoot: {
855
+ repoRoot: {
795
856
  type: "string",
796
857
  description: "Unused.",
797
858
  },
@@ -801,7 +862,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
801
862
  type: "object",
802
863
  additionalProperties: true,
803
864
  properties: {
804
- processRoot: {
865
+ repoRoot: {
805
866
  type: "string",
806
867
  description:
807
868
  'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
@@ -817,10 +878,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
817
878
  type: "string",
818
879
  description: 'Project name under /projects (e.g. "adl").',
819
880
  },
820
- processRoot: {
881
+ repoRoot: {
821
882
  type: "string",
822
883
  description:
823
- "Repo root containing /projects and /agents. If omitted, uses server cwd.",
884
+ "Repo root on the server filesystem (contains /projects and /agents). If omitted, uses the server default (auto-detected from cwd).",
824
885
  },
825
886
  args: {
826
887
  type: "array",
@@ -843,7 +904,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
843
904
  type: "string",
844
905
  description: 'Project name under /projects (e.g. "adl").',
845
906
  },
846
- processRoot: {
907
+ repoRoot: {
847
908
  type: "string",
848
909
  description: "Repo root containing /projects and /agents.",
849
910
  },
@@ -873,7 +934,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
873
934
  description:
874
935
  'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
875
936
  },
876
- processRoot: {
937
+ repoRoot: {
877
938
  type: "string",
878
939
  description: "Repo root containing /projects and /agents.",
879
940
  },
@@ -950,7 +1011,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
950
1011
  type: "string",
951
1012
  description: "Remote repo override for gitea mode.",
952
1013
  },
953
- processRoot: {
1014
+ repoRoot: {
954
1015
  type: "string",
955
1016
  description: "Repo root containing /projects and /agents.",
956
1017
  },
@@ -1027,7 +1088,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1027
1088
  type: "string",
1028
1089
  description: "Remote repo override for gitea mode.",
1029
1090
  },
1030
- processRoot: {
1091
+ repoRoot: {
1031
1092
  type: "string",
1032
1093
  description: "Repo root containing /projects and /agents.",
1033
1094
  },
@@ -1143,7 +1204,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1143
1204
  type: "boolean",
1144
1205
  description: "If true, only return issues with TASK-* IDs.",
1145
1206
  },
1146
- processRoot: {
1207
+ repoRoot: {
1147
1208
  type: "string",
1148
1209
  description: "Repo root containing /projects.",
1149
1210
  },
@@ -1184,7 +1245,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1184
1245
  type: "boolean",
1185
1246
  description: "If true, restrict to issues with TASK-* payloads.",
1186
1247
  },
1187
- processRoot: {
1248
+ repoRoot: {
1188
1249
  type: "string",
1189
1250
  description: "Repo root containing /projects.",
1190
1251
  },
@@ -1241,7 +1302,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1241
1302
  type: "string",
1242
1303
  description: "Optional task signature.",
1243
1304
  },
1244
- processRoot: {
1305
+ repoRoot: {
1245
1306
  type: "string",
1246
1307
  description: "Repo root containing /projects.",
1247
1308
  },
@@ -1275,7 +1336,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1275
1336
  items: { type: "string" },
1276
1337
  description: "Labels to set.",
1277
1338
  },
1278
- processRoot: {
1339
+ repoRoot: {
1279
1340
  type: "string",
1280
1341
  description: "Repo root containing /projects.",
1281
1342
  },
@@ -1292,6 +1353,21 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1292
1353
  },
1293
1354
  required: ["projectName", "title"],
1294
1355
  },
1356
+ "mcp.workspace": {
1357
+ type: "object",
1358
+ additionalProperties: true,
1359
+ properties: {
1360
+ repoRoot: {
1361
+ type: "string",
1362
+ description:
1363
+ "Optional repo root on the server filesystem. If omitted, uses the server default (auto-detected by walking up from cwd). Legacy alias: processRoot.",
1364
+ },
1365
+ },
1366
+ required: [],
1367
+ $comment: safeJsonStringify({
1368
+ example: {},
1369
+ }),
1370
+ },
1295
1371
  "mcp.search": {
1296
1372
  type: "object",
1297
1373
  additionalProperties: true,
@@ -1349,7 +1425,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1349
1425
  type: "string",
1350
1426
  description: "Optional override cache directory.",
1351
1427
  },
1352
- processRoot: {
1428
+ repoRoot: {
1353
1429
  type: "string",
1354
1430
  description:
1355
1431
  "Repo root containing /projects. If you pass a project root, it will be normalized.",
@@ -1384,7 +1460,7 @@ const buildArgsSchemaFromPlaceholders = (
1384
1460
  const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
1385
1461
  "projects.listProjects": {
1386
1462
  type: "array",
1387
- description: "No positional arguments. Use options.processRoot if needed.",
1463
+ description: "No positional arguments. Use options.repoRoot if needed.",
1388
1464
  minItems: 0,
1389
1465
  maxItems: 0,
1390
1466
  items: {},
@@ -1499,9 +1575,9 @@ const getInvocationPlanName = (toolName: string): string => {
1499
1575
  const plan = toolInvocationPlans[toolName];
1500
1576
  if (!plan) return "default";
1501
1577
  if (plan === buildOptionsOnly) return "optionsOnly";
1502
- if (plan === buildOptionsThenProcessRoot) return "optionsThenProcessRoot";
1503
- if (plan === buildProcessRootThenOptions) return "processRootThenOptions";
1504
- if (plan === buildProcessRootOnly) return "processRootOnly";
1578
+ if (plan === buildOptionsThenRepoRoot) return "optionsThenRepoRoot";
1579
+ if (plan === buildRepoRootThenOptions) return "repoRootThenOptions";
1580
+ if (plan === buildRepoRootOnly) return "repoRootOnly";
1505
1581
  return "custom";
1506
1582
  };
1507
1583
 
@@ -1513,17 +1589,17 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1513
1589
  const example: Record<string, unknown> = {};
1514
1590
  if (requiredArgs && requiredArgs.length > 0) {
1515
1591
  example.args = [...requiredArgs];
1516
- } else if (plan !== "processRootOnly") {
1592
+ } else if (plan !== "repoRootOnly") {
1517
1593
  example.args = ["<arg0>"];
1518
1594
  }
1519
1595
 
1520
- if (plan === "processRootOnly") {
1521
- example.options = { processRoot: "<repo-root>", ...defaultOptions };
1596
+ if (plan === "repoRootOnly") {
1597
+ example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1522
1598
  return example;
1523
1599
  }
1524
1600
 
1525
- if (plan === "optionsThenProcessRoot" || plan === "processRootThenOptions") {
1526
- example.options = { processRoot: "<repo-root>", ...defaultOptions };
1601
+ if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1602
+ example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1527
1603
  return example;
1528
1604
  }
1529
1605
 
@@ -1554,10 +1630,10 @@ const defaultToolInputSchema = (toolName: string) => ({
1554
1630
  additionalProperties: true,
1555
1631
  description: "Named options",
1556
1632
  },
1557
- processRoot: {
1633
+ repoRoot: {
1558
1634
  type: "string",
1559
1635
  description:
1560
- 'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
1636
+ 'Repo root on the server filesystem (contains /projects and /agents). If omitted, uses the server default (auto-detected from cwd). If you have a project root like ".../projects/adl", omit this or pass its parent.',
1561
1637
  },
1562
1638
  },
1563
1639
  $comment: safeJsonStringify({
@@ -1673,11 +1749,13 @@ const buildToolList = (
1673
1749
  type ToolInvoker = (
1674
1750
  args: unknown[],
1675
1751
  options: Record<string, unknown>,
1752
+ defaultRepoRoot: string,
1676
1753
  ) => unknown[];
1677
1754
 
1678
1755
  const buildOptionsOnly = (
1679
1756
  args: unknown[],
1680
1757
  options: Record<string, unknown>,
1758
+ _defaultRepoRoot: string,
1681
1759
  ): unknown[] => {
1682
1760
  const invocationArgs: unknown[] = [...args];
1683
1761
  if (Object.keys(options).length > 0) {
@@ -1686,44 +1764,44 @@ const buildOptionsOnly = (
1686
1764
  return invocationArgs;
1687
1765
  };
1688
1766
 
1689
- const buildOptionsThenProcessRoot = (
1767
+ const buildOptionsThenRepoRoot = (
1690
1768
  args: unknown[],
1691
1769
  options: Record<string, unknown>,
1770
+ defaultRepoRoot: string,
1692
1771
  ): unknown[] => {
1693
1772
  const invocationArgs: unknown[] = [...args];
1694
1773
  const remaining = { ...options };
1695
- const processRoot = remaining.processRoot;
1696
- if (typeof processRoot === "string") {
1697
- delete remaining.processRoot;
1774
+ const repoRoot = remaining.repoRoot;
1775
+ if (typeof repoRoot === "string") {
1776
+ delete remaining.repoRoot;
1698
1777
  }
1778
+ const resolvedRepoRoot =
1779
+ typeof repoRoot === "string" ? repoRoot : defaultRepoRoot;
1699
1780
 
1700
1781
  if (Object.keys(remaining).length > 0) {
1701
1782
  invocationArgs.push(remaining);
1702
- } else if (typeof processRoot === "string") {
1703
- // Preserve positional slot for signatures like fn(projectName, options?, processRoot?).
1783
+ } else if (resolvedRepoRoot) {
1784
+ // Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
1704
1785
  invocationArgs.push({});
1705
1786
  }
1706
- if (typeof processRoot === "string") {
1707
- invocationArgs.push(processRoot);
1708
- }
1787
+ invocationArgs.push(resolvedRepoRoot);
1709
1788
 
1710
1789
  return invocationArgs;
1711
1790
  };
1712
1791
 
1713
- const buildProcessRootThenOptions = (
1792
+ const buildRepoRootThenOptions = (
1714
1793
  args: unknown[],
1715
1794
  options: Record<string, unknown>,
1795
+ defaultRepoRoot: string,
1716
1796
  ): unknown[] => {
1717
1797
  const invocationArgs: unknown[] = [...args];
1718
1798
  const remaining = { ...options };
1719
- const processRoot = remaining.processRoot;
1720
- if (typeof processRoot === "string") {
1721
- delete remaining.processRoot;
1799
+ const repoRoot = remaining.repoRoot;
1800
+ if (typeof repoRoot === "string") {
1801
+ delete remaining.repoRoot;
1722
1802
  }
1723
1803
 
1724
- if (typeof processRoot === "string") {
1725
- invocationArgs.push(processRoot);
1726
- }
1804
+ invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
1727
1805
  if (Object.keys(remaining).length > 0) {
1728
1806
  invocationArgs.push(remaining);
1729
1807
  }
@@ -1731,39 +1809,38 @@ const buildProcessRootThenOptions = (
1731
1809
  return invocationArgs;
1732
1810
  };
1733
1811
 
1734
- const buildProcessRootOnly = (
1812
+ const buildRepoRootOnly = (
1735
1813
  args: unknown[],
1736
1814
  options: Record<string, unknown>,
1815
+ defaultRepoRoot: string,
1737
1816
  ): unknown[] => {
1738
1817
  const invocationArgs: unknown[] = [...args];
1739
- const processRoot = options.processRoot;
1740
- if (typeof processRoot === "string") {
1741
- invocationArgs.push(processRoot);
1742
- }
1818
+ const repoRoot = options.repoRoot;
1819
+ invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
1743
1820
  return invocationArgs;
1744
1821
  };
1745
1822
 
1746
1823
  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,
1824
+ "agents.setActive": buildRepoRootThenOptions,
1825
+ "agents.resolveAgentsRootFrom": buildRepoRootOnly,
1826
+ "projects.setActive": buildRepoRootThenOptions,
1827
+ "projects.generateSpec": buildOptionsThenRepoRoot,
1828
+ "projects.syncTasks": buildOptionsThenRepoRoot,
1829
+ "projects.clearIssues": buildOptionsThenRepoRoot,
1830
+ "projects.fetchGitTasks": buildOptionsThenRepoRoot,
1831
+ "projects.createGitIssue": buildOptionsThenRepoRoot,
1832
+ "projects.readGitTask": buildOptionsThenRepoRoot,
1833
+ "projects.writeGitTask": buildOptionsThenRepoRoot,
1757
1834
  "agents.resolveTargetFile": buildOptionsOnly,
1758
1835
  "projects.resolveProjectTargetFile": buildOptionsOnly,
1759
- "agents.loadAgent": buildProcessRootOnly,
1760
- "agents.loadAgentPrompt": buildProcessRootOnly,
1761
- "projects.resolveImplementationPlan": (args, options) => {
1836
+ "agents.loadAgent": buildRepoRootOnly,
1837
+ "agents.loadAgentPrompt": buildRepoRootOnly,
1838
+ "projects.resolveImplementationPlan": (args, options, _defaultRepoRoot) => {
1762
1839
  const invocationArgs: unknown[] = [...args];
1763
1840
  const remaining = { ...options };
1764
- const processRoot = remaining.processRoot;
1765
- if (typeof processRoot === "string") {
1766
- delete remaining.processRoot;
1841
+ const repoRoot = remaining.repoRoot;
1842
+ if (typeof repoRoot === "string") {
1843
+ delete remaining.repoRoot;
1767
1844
  }
1768
1845
 
1769
1846
  // This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
@@ -1775,35 +1852,36 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
1775
1852
  invocationArgs.push(remaining);
1776
1853
  }
1777
1854
 
1778
- // Intentionally do NOT append processRoot: projectRoot is the first positional argument.
1855
+ // Intentionally do NOT append repoRoot: projectRoot is the first positional argument.
1779
1856
  return invocationArgs;
1780
1857
  },
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,
1858
+ "agents.main": buildRepoRootOnly,
1859
+ "agents.resolveAgentsRoot": buildRepoRootOnly,
1860
+ "agents.listAgents": buildRepoRootOnly,
1861
+ "projects.resolveProjectRoot": buildRepoRootOnly,
1862
+ "projects.listProjects": buildRepoRootOnly,
1863
+ "projects.listProjectDocs": buildRepoRootOnly,
1864
+ "projects.readProjectDoc": buildRepoRootOnly,
1865
+ "projects.main": buildRepoRootOnly,
1789
1866
  };
1790
1867
 
1791
1868
  const invokeTool = async (
1792
1869
  tool: ToolDefinition,
1793
1870
  payload: unknown,
1871
+ defaultRepoRoot: string,
1794
1872
  ): Promise<unknown> => {
1795
1873
  const normalized = normalizePayload(payload);
1796
1874
  const { args, options } = coercePayloadForTool(tool.name, normalized);
1797
1875
  const invoke =
1798
1876
  toolInvocationPlans[tool.name] ??
1799
- ((rawArgs, rawOptions) => {
1877
+ ((rawArgs, rawOptions, _repoRoot) => {
1800
1878
  const invocationArgs = [...rawArgs];
1801
1879
  if (Object.keys(rawOptions).length > 0) {
1802
1880
  invocationArgs.push(rawOptions);
1803
1881
  }
1804
1882
  return invocationArgs;
1805
1883
  });
1806
- const invocationArgs = invoke(args, options);
1884
+ const invocationArgs = invoke(args, options, defaultRepoRoot);
1807
1885
 
1808
1886
  return Promise.resolve(tool.method(...invocationArgs));
1809
1887
  };
@@ -1812,6 +1890,11 @@ export interface ExampleMcpServerOptions {
1812
1890
  serverName?: string;
1813
1891
  serverVersion?: string;
1814
1892
  toolsPrefix?: string;
1893
+ /**
1894
+ * Optional default repo root on the server filesystem.
1895
+ * If omitted, the server attempts to auto-detect by walking up from cwd.
1896
+ */
1897
+ repoRoot?: string;
1815
1898
  allowedRootEndpoints?: string[];
1816
1899
  disableWrite?: boolean;
1817
1900
  enableIssues?: boolean;
@@ -1852,6 +1935,7 @@ const READ_ONLY_TOOL_NAMES = new Set<string>([
1852
1935
  "mcp.usage",
1853
1936
  "mcp.listTools",
1854
1937
  "mcp.describeTool",
1938
+ "mcp.workspace",
1855
1939
  "mcp.search",
1856
1940
  ]);
1857
1941
 
@@ -1942,6 +2026,12 @@ export const createExampleMcpServer = (
1942
2026
  options: ExampleMcpServerOptions = {},
1943
2027
  ): ExampleMcpServerInstance => {
1944
2028
  let toolCatalog: unknown[] = [];
2029
+ const defaultRepoRoot = normalizeRepoRoot(
2030
+ options.repoRoot ??
2031
+ process.env.MCP_REPO_ROOT ??
2032
+ process.env.F0_REPO_ROOT ??
2033
+ process.cwd(),
2034
+ );
1945
2035
 
1946
2036
  const parseString = (value: unknown): string | null => {
1947
2037
  if (typeof value !== "string") return null;
@@ -1981,11 +2071,63 @@ export const createExampleMcpServer = (
1981
2071
  "F0 MCP helper tools:",
1982
2072
  "- mcp.listTools: returns tool catalog with access + invocation hints",
1983
2073
  "- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
2074
+ "- mcp.workspace: explain server filesystem context (cwd, repoRoot, projects)",
1984
2075
  "- mcp.search: LLM-friendly search over project docs/spec (local-first)",
1985
2076
  "",
1986
2077
  'Tip: Prefer mcp.search for "search spec/docs" requests.',
1987
2078
  ].join("\n"),
1988
2079
  listTools: () => ({ tools: toolCatalog }),
2080
+ workspace: (input?: unknown) => {
2081
+ const payload = isRecord(input) ? input : {};
2082
+ const received = isRecord(input)
2083
+ ? {
2084
+ keys: Object.keys(input),
2085
+ repoRoot: (input as any).repoRoot ?? null,
2086
+ processRoot: (input as any).processRoot ?? null,
2087
+ }
2088
+ : { keys: [], repoRoot: null, processRoot: null };
2089
+ const raw =
2090
+ parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2091
+ const repoRoot = raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
2092
+
2093
+ const projectsDir = path.join(repoRoot, "projects");
2094
+ const apiDir = path.join(repoRoot, "api");
2095
+ const agentsDir = path.join(repoRoot, "agents");
2096
+ const hasProjectsDir = isDir(projectsDir);
2097
+ const hasApiDir = isDir(apiDir);
2098
+ const hasAgentsDir = isDir(agentsDir);
2099
+
2100
+ const projects = hasProjectsDir ? projectsApi.listProjects(repoRoot) : [];
2101
+ const hint = (() => {
2102
+ if (!hasProjectsDir) {
2103
+ return [
2104
+ "Repo does not contain /projects on the server filesystem.",
2105
+ "Start the MCP server from the monorepo root (the folder that contains both /api and /projects), or pass repoRoot explicitly.",
2106
+ "repoRoot is a server-side path, not a client/LLM path.",
2107
+ ].join(" ");
2108
+ }
2109
+ if (hasProjectsDir && projects.length === 0) {
2110
+ return [
2111
+ "Found /projects, but no projects with docs/ were detected.",
2112
+ "Each project folder must contain a docs/ directory to be listed.",
2113
+ ].join(" ");
2114
+ }
2115
+ return null;
2116
+ })();
2117
+
2118
+ return {
2119
+ received,
2120
+ cwd: process.cwd(),
2121
+ configuredRepoRoot: options.repoRoot ?? null,
2122
+ defaultRepoRoot,
2123
+ repoRoot,
2124
+ hasProjectsDir,
2125
+ hasApiDir,
2126
+ hasAgentsDir,
2127
+ projects,
2128
+ hint,
2129
+ };
2130
+ },
1989
2131
  describeTool: (toolName: string) => {
1990
2132
  const normalized = typeof toolName === "string" ? toolName.trim() : "";
1991
2133
  if (!normalized) {
@@ -2026,9 +2168,10 @@ export const createExampleMcpServer = (
2026
2168
  },
2027
2169
  search: async (input: unknown) => {
2028
2170
  const payload = isRecord(input) ? input : {};
2029
- const processRoot = (() => {
2030
- const raw = parseString(payload.processRoot);
2031
- return raw ? normalizeProcessRoot(raw) : process.cwd();
2171
+ const repoRoot = (() => {
2172
+ const raw =
2173
+ parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2174
+ return raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
2032
2175
  })();
2033
2176
 
2034
2177
  const sectionRaw = parseString(payload.section)?.toLowerCase();
@@ -2076,11 +2219,11 @@ export const createExampleMcpServer = (
2076
2219
 
2077
2220
  const projectName = ensureProjectName(
2078
2221
  parseString(payload.projectName),
2079
- processRoot,
2222
+ repoRoot,
2080
2223
  );
2081
2224
 
2082
2225
  const searchOptions: Record<string, unknown> = {
2083
- processRoot,
2226
+ processRoot: repoRoot,
2084
2227
  };
2085
2228
 
2086
2229
  const source = parseString(payload.source)?.toLowerCase();
@@ -2106,9 +2249,15 @@ export const createExampleMcpServer = (
2106
2249
  } satisfies ToolNamespace;
2107
2250
 
2108
2251
  const api: ApiEndpoint = {
2109
- agents: agentsApi,
2252
+ agents: {
2253
+ ...agentsApi,
2254
+ usage: () => buildAgentsMcpUsage(options.toolsPrefix),
2255
+ },
2110
2256
  net: netApi,
2111
- projects: projectsApi,
2257
+ projects: {
2258
+ ...projectsApi,
2259
+ usage: () => buildProjectsMcpUsage(options.toolsPrefix),
2260
+ },
2112
2261
  mcp: mcpApi,
2113
2262
  };
2114
2263
 
@@ -2140,7 +2289,18 @@ export const createExampleMcpServer = (
2140
2289
  const selectedTools = selectedRoots.flatMap((root) =>
2141
2290
  collectTools(api[root], [root]),
2142
2291
  );
2143
- const selectedToolsWithAliases = applyToolAliases(selectedTools);
2292
+ const selectedToolsWithMcpHelpers = (() => {
2293
+ if (selectedRoots.includes("mcp")) return selectedTools;
2294
+ const helperApi: Partial<Record<McpHelperToolKey, unknown>> = {};
2295
+ for (const key of MCP_HELPER_TOOL_KEYS) {
2296
+ helperApi[key] = (mcpApi as any)[key];
2297
+ }
2298
+ const helperTools = collectTools(helperApi as ToolNamespace, ["mcp"]).filter(
2299
+ (tool) => MCP_HELPER_TOOL_NAMES.has(tool.name),
2300
+ );
2301
+ return [...helperTools, ...selectedTools];
2302
+ })();
2303
+ const selectedToolsWithAliases = applyToolAliases(selectedToolsWithMcpHelpers);
2144
2304
  buildToolMeta(selectedToolsWithAliases);
2145
2305
  const adminFilteredTools = Boolean(options.admin)
2146
2306
  ? selectedToolsWithAliases
@@ -2207,7 +2367,7 @@ export const createExampleMcpServer = (
2207
2367
  tool: prefix
2208
2368
  ? `${prefix}.projects.listProjects`
2209
2369
  : "projects.listProjects",
2210
- options: { processRoot: "<repo-root>" },
2370
+ options: { repoRoot: "<repo-root>" },
2211
2371
  },
2212
2372
  ],
2213
2373
  continueOnError: true,
@@ -2386,7 +2546,22 @@ export const createExampleMcpServer = (
2386
2546
  /projects[\\/].+projects[\\/]/i.test(message)
2387
2547
  ) {
2388
2548
  details.hint =
2389
- "You likely passed a project root as processRoot. processRoot should be the repo root containing /projects.";
2549
+ "You likely passed a project root as repoRoot. repoRoot should be the repo root containing /projects. (Legacy alias: processRoot.)";
2550
+ details.suggestion = "Call mcp.workspace to see cwd/repoRoot on the server.";
2551
+ }
2552
+
2553
+ if (
2554
+ /Project folder not found:/i.test(message) &&
2555
+ !details.hint &&
2556
+ /projects[\\/]/i.test(message)
2557
+ ) {
2558
+ details.hint =
2559
+ "repoRoot might be wrong, or the server filesystem does not contain /projects for this repoRoot.";
2560
+ details.suggestion = "Call mcp.workspace to see cwd/repoRoot on the server.";
2561
+ details.example = {
2562
+ tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
2563
+ arguments: {},
2564
+ };
2390
2565
  }
2391
2566
 
2392
2567
  if (/Missing search pattern\./i.test(message)) {
@@ -2450,7 +2625,11 @@ export const createExampleMcpServer = (
2450
2625
  }
2451
2626
 
2452
2627
  try {
2453
- const data = await invokeTool(toolDefinition, { args, options });
2628
+ const data = await invokeTool(
2629
+ toolDefinition,
2630
+ { args, options },
2631
+ defaultRepoRoot,
2632
+ );
2454
2633
  return { index, tool, isError: false, data };
2455
2634
  } catch (error) {
2456
2635
  const message =
@@ -2502,7 +2681,7 @@ export const createExampleMcpServer = (
2502
2681
  }
2503
2682
 
2504
2683
  try {
2505
- const data = await invokeTool(tool, request.params.arguments);
2684
+ const data = await invokeTool(tool, request.params.arguments, defaultRepoRoot);
2506
2685
  return toolOk(data);
2507
2686
  } catch (error) {
2508
2687
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foundation0/api",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Foundation 0 API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "scripts": {
36
36
  "mcp": "bun run mcp/cli.ts",
37
37
  "test": "bun test",
38
+ "test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov",
38
39
  "deploy": "pnpm publish --access public",
39
40
  "version:patch": "pnpm version patch && git commit -am \"Bump version to %s\"",
40
41
  "version:minor": "pnpm version minor && git commit -am \"Bump version to %s\"",