@foundation0/api 1.1.6 → 1.1.7

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,7 @@ 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 repoName = getArgValue('--repo-name') ?? process.env.MCP_REPO_NAME ?? process.env.F0_REPO_NAME
70
71
  const repoRoot =
71
72
  getArgValue('--repo-root') ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT
72
73
  const allowedRootEndpoints = parseListArg(
@@ -82,6 +83,8 @@ if (hasFlag('--help') || hasFlag('-h')) {
82
83
  console.log(' --server-name <name>')
83
84
  console.log(' --server-version <version>')
84
85
  console.log(' --tools-prefix <prefix>')
86
+ console.log(' --repo-name <name>')
87
+ console.log(' Default repoName used by tool calls when omitted. Env: MCP_REPO_NAME or F0_REPO_NAME.')
85
88
  console.log(' --repo-root <path>')
86
89
  console.log(' Default repo root on the server filesystem (contains /api and /projects).')
87
90
  console.log(' Env: MCP_REPO_ROOT or F0_REPO_ROOT.')
@@ -102,6 +105,7 @@ void runExampleMcpServer({
102
105
  serverVersion: serverVersion ?? undefined,
103
106
  toolsPrefix,
104
107
  repoRoot: repoRoot ?? undefined,
108
+ repoName: repoName ?? undefined,
105
109
  allowedRootEndpoints,
106
110
  disableWrite,
107
111
  enableIssues,
package/mcp/manual.md CHANGED
@@ -7,11 +7,11 @@ Assume the server is already configured in your MCP host and you can call its to
7
7
 
8
8
  1. If you are unsure about a tool name, call `mcp.listTools` first and use the exact name it returns (prefixes vary).
9
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).
10
+ 3. Prefer named keys when possible (e.g. `projectName`, `repoName`) rather than relying on positional `args`.
11
+ 4. `repoName` selects the repo workspace on the **server filesystem**. Usually omit it and let the server use its default. Legacy path aliases: `repoRoot` / `processRoot`.
12
12
  5. Tool results are returned as a **JSON text envelope**: parse the text as JSON and check `ok`.
13
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`.
14
+ Note: If you’re unsure what the server can see (cwd/default repoRoot/available repoName values), call `mcp.workspace`. Multi-repo `repoName` mappings can be configured server-side via `MCP_REPOS` (JSON object or `name=path;name=path`).
15
15
 
16
16
  ## 1) Tool naming (prefixes + aliases)
17
17
 
@@ -55,14 +55,14 @@ Tool call:
55
55
  "projectName": "<project-name>",
56
56
  "section": "spec",
57
57
  "pattern": "authentication",
58
- "repoRoot": "<repo-root>",
58
+ "repoName": "<repo-name>",
59
59
  "ignoreCase": true,
60
60
  "maxCount": 50
61
61
  }
62
62
  }
63
63
  ```
64
64
 
65
- ### D) `mcp.workspace` to debug repoRoot/cwd issues
65
+ ### D) `mcp.workspace` to debug repoName/repoRoot/cwd issues
66
66
 
67
67
  Tool call:
68
68
 
@@ -75,13 +75,13 @@ Tool call:
75
75
  Most tools accept either:
76
76
 
77
77
  ```json
78
- { "args": ["<project-name>"], "options": { "repoRoot": "<repo-root>" } }
78
+ { "args": ["<project-name>"], "options": { "repoName": "<repo-name>" } }
79
79
  ```
80
80
 
81
81
  or named keys (recommended). The server merges top-level keys into `options`:
82
82
 
83
83
  ```json
84
- { "projectName": "<project-name>", "repoRoot": "<repo-root>" }
84
+ { "projectName": "<project-name>", "repoName": "<repo-name>" }
85
85
  ```
86
86
 
87
87
  Guideline: if a tool has a natural named parameter (`projectName`, `agentName`, `target`, `taskRef`), pass it explicitly.
@@ -93,7 +93,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
93
93
  ```json
94
94
  {
95
95
  "name": "projects.listProjects",
96
- "arguments": { "repoRoot": "<repo-root>" }
96
+ "arguments": { "repoName": "<repo-name>" }
97
97
  }
98
98
  ```
99
99
 
@@ -102,7 +102,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
102
102
  ```json
103
103
  {
104
104
  "name": "agents.listAgents",
105
- "arguments": { "repoRoot": "<repo-root>" }
105
+ "arguments": { "repoName": "<repo-name>" }
106
106
  }
107
107
  ```
108
108
 
@@ -113,7 +113,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
113
113
  "name": "projects.setActive",
114
114
  "arguments": {
115
115
  "args": ["<project-name>", "/implementation-plan.v0.0.1"],
116
- "options": { "repoRoot": "<repo-root>", "latest": true }
116
+ "options": { "repoName": "<repo-name>", "latest": true }
117
117
  }
118
118
  }
119
119
  ```
@@ -128,7 +128,7 @@ Call `batch` (or `<prefix>.batch`) to run multiple tool calls:
128
128
  "arguments": {
129
129
  "calls": [
130
130
  { "tool": "projects.usage" },
131
- { "tool": "projects.listProjects", "options": { "repoRoot": "<repo-root>" } }
131
+ { "tool": "projects.listProjects", "options": { "repoName": "<repo-name>" } }
132
132
  ],
133
133
  "continueOnError": true,
134
134
  "maxConcurrency": 4
@@ -147,8 +147,8 @@ If you get:
147
147
 
148
148
  - **Unknown tool**: use the `suggestions` from the error (when present), or call `mcp.listTools` again and retry.
149
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.
150
+ - **Project folder not found: ...projects/.../projects/...**: call `mcp.workspace` and verify the server repo workspace (default `repoRoot`, and optional `repoName` mapping).
151
+ - **`projects.listProjects()` returns `[]` unexpectedly**: call `mcp.workspace` to confirm the server’s `cwd`/default `repoRoot` and whether `/projects` exists.
152
152
 
153
153
  ## 6) Tool availability (read/write/admin)
154
154
 
@@ -204,9 +204,9 @@ describe("createExampleMcpServer request handling", () => {
204
204
  }
205
205
  });
206
206
 
207
- it("accepts repoRoot for projects tools (preferred over legacy processRoot)", async () => {
207
+ it("accepts repoName for projects tools (preferred over legacy repoRoot/processRoot)", async () => {
208
208
  const tempDir = await fs.mkdtemp(
209
- path.join(os.tmpdir(), "f0-mcp-server-reporoot-"),
209
+ path.join(os.tmpdir(), "f0-mcp-server-reponame-"),
210
210
  );
211
211
  try {
212
212
  await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
@@ -217,7 +217,7 @@ describe("createExampleMcpServer request handling", () => {
217
217
  recursive: true,
218
218
  });
219
219
 
220
- const instance = createExampleMcpServer();
220
+ const instance = createExampleMcpServer({ repoRoot: tempDir, repoName: "test" });
221
221
  const handler = getToolHandler(instance);
222
222
 
223
223
  const result = await handler(
@@ -226,7 +226,7 @@ describe("createExampleMcpServer request handling", () => {
226
226
  params: {
227
227
  name: "projects.listProjects",
228
228
  arguments: {
229
- repoRoot: tempDir,
229
+ repoName: "test",
230
230
  },
231
231
  },
232
232
  },
@@ -243,7 +243,7 @@ describe("createExampleMcpServer request handling", () => {
243
243
  }
244
244
  });
245
245
 
246
- it("auto-detects repoRoot from process.cwd() when repoRoot is omitted", async () => {
246
+ it("auto-detects repoRoot from process.cwd() when repoName is omitted", async () => {
247
247
  const tempDir = await fs.mkdtemp(
248
248
  path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
249
249
  );
@@ -320,7 +320,7 @@ describe("createExampleMcpServer request handling", () => {
320
320
  }
321
321
  });
322
322
 
323
- it("normalizes repoRoot when caller accidentally passes a project root", async () => {
323
+ it("normalizes legacy repoRoot when caller accidentally passes a project root", async () => {
324
324
  const tempDir = await fs.mkdtemp(
325
325
  path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
326
326
  );
@@ -359,7 +359,7 @@ describe("createExampleMcpServer request handling", () => {
359
359
  }
360
360
  });
361
361
 
362
- it("uses server default repoRoot when repoRoot is omitted", async () => {
362
+ it("uses server default repoRoot when repoName is omitted", async () => {
363
363
  const tempDir = await fs.mkdtemp(
364
364
  path.join(os.tmpdir(), "f0-mcp-server-default-reporoot-"),
365
365
  );
@@ -424,6 +424,7 @@ describe("createExampleMcpServer request handling", () => {
424
424
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
425
425
  expect(payload.ok).toBe(true);
426
426
  expect(payload.result.repoRoot).toBe(path.resolve(tempDir));
427
+ expect(payload.result.defaultRepoRoot).toBe(path.resolve(tempDir));
427
428
  expect(payload.result.hasProjectsDir).toBe(true);
428
429
  expect(Array.isArray(payload.result.projects)).toBe(true);
429
430
  } finally {
package/mcp/server.ts CHANGED
@@ -63,9 +63,9 @@ const buildProjectsMcpUsage = (toolsPrefix: string | undefined): string => {
63
63
  const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
64
64
  return [
65
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 } }`,
66
+ `- ${tool("projects.listProjects")}: { options: { repoName: \"<repo-name>\" } }`,
67
+ `- ${tool("projects.generateSpec")}: { args: [\"<projectName>\"], options: { repoName: \"<repo-name>\" } }`,
68
+ `- ${tool("projects.setActive")}: { args: [\"<projectName>\", \"/implementation-plan.v0.0.1\"], options: { repoName: \"<repo-name>\", latest: true } }`,
69
69
  "",
70
70
  "Discovery:",
71
71
  `- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
@@ -80,9 +80,9 @@ const buildAgentsMcpUsage = (toolsPrefix: string | undefined): string => {
80
80
  const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
81
81
  return [
82
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 } }`,
83
+ `- ${tool("agents.listAgents")}: { options: { repoName: \"<repo-name>\" } }`,
84
+ `- ${tool("agents.loadAgent")}: { args: [\"<agentName>\"], options: { repoName: \"<repo-name>\" } }`,
85
+ `- ${tool("agents.setActive")}: { args: [\"<agentName>\", \"/system/boot.v0.0.1\"], options: { repoName: \"<repo-name>\", latest: true } }`,
86
86
  "",
87
87
  "Discovery:",
88
88
  `- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
@@ -226,6 +226,125 @@ const normalizeRepoRootOption = (
226
226
  return alreadyCanonical ? options : next;
227
227
  };
228
228
 
229
+ const looksLikePathish = (value: string): boolean => {
230
+ const trimmed = value.trim();
231
+ if (!trimmed) return false;
232
+ if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
233
+ return trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith(".");
234
+ };
235
+
236
+ const parseRepoMap = (raw: string | undefined): Record<string, string> => {
237
+ const input = typeof raw === "string" ? raw.trim() : "";
238
+ if (!input) return {};
239
+
240
+ if (input.startsWith("{")) {
241
+ try {
242
+ const parsed = JSON.parse(input);
243
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
244
+ return {};
245
+ }
246
+ const out: Record<string, string> = {};
247
+ for (const [name, root] of Object.entries(parsed)) {
248
+ if (!name || typeof root !== "string" || !root.trim()) continue;
249
+ out[name.trim()] = root.trim();
250
+ }
251
+ return out;
252
+ } catch {
253
+ return {};
254
+ }
255
+ }
256
+
257
+ const entries = input
258
+ .split(/[;,]/g)
259
+ .map((entry) => entry.trim())
260
+ .filter((entry) => entry.length > 0);
261
+
262
+ const out: Record<string, string> = {};
263
+ for (const entry of entries) {
264
+ const [name, root] = entry.split("=", 2);
265
+ if (!name || !root) continue;
266
+ const key = name.trim();
267
+ const value = root.trim();
268
+ if (!key || !value) continue;
269
+ out[key] = value;
270
+ }
271
+ return out;
272
+ };
273
+
274
+ type RepoResolutionContext = {
275
+ defaultRepoRoot: string;
276
+ defaultRepoName: string;
277
+ repoMapByKey: Record<string, string>;
278
+ repoNames: string[];
279
+ };
280
+
281
+ const resolveRepoSelectorOptions = (
282
+ options: Record<string, unknown>,
283
+ ctx: RepoResolutionContext,
284
+ ): Record<string, unknown> => {
285
+ const next: Record<string, unknown> = { ...options };
286
+
287
+ const explicitRoot =
288
+ (typeof next.repoRoot === "string" && next.repoRoot.trim().length > 0
289
+ ? next.repoRoot.trim()
290
+ : null) ??
291
+ (typeof next.processRoot === "string" && next.processRoot.trim().length > 0
292
+ ? next.processRoot.trim()
293
+ : null);
294
+
295
+ if (explicitRoot) {
296
+ next.repoRoot = normalizeRepoRoot(explicitRoot);
297
+ delete next.processRoot;
298
+ delete next.repoName;
299
+ return next;
300
+ }
301
+
302
+ const repoName =
303
+ typeof next.repoName === "string" && next.repoName.trim().length > 0
304
+ ? next.repoName.trim()
305
+ : null;
306
+ if (!repoName) {
307
+ delete next.processRoot;
308
+ return next;
309
+ }
310
+
311
+ if (looksLikePathish(repoName)) {
312
+ next.repoRoot = normalizeRepoRoot(repoName);
313
+ delete next.processRoot;
314
+ delete next.repoName;
315
+ return next;
316
+ }
317
+
318
+ const needle = repoName.toLowerCase();
319
+ if (needle === ctx.defaultRepoName.toLowerCase()) {
320
+ next.repoRoot = ctx.defaultRepoRoot;
321
+ delete next.processRoot;
322
+ delete next.repoName;
323
+ return next;
324
+ }
325
+
326
+ const mapped = ctx.repoMapByKey[needle];
327
+ if (mapped) {
328
+ next.repoRoot = normalizeRepoRoot(mapped);
329
+ delete next.processRoot;
330
+ delete next.repoName;
331
+ return next;
332
+ }
333
+
334
+ const suggestions = ctx.repoNames
335
+ .filter((name) => name.toLowerCase().includes(needle))
336
+ .slice(0, 8);
337
+ const hint =
338
+ suggestions.length > 0
339
+ ? ` Did you mean: ${suggestions.join(", ")}?`
340
+ : ctx.repoNames.length > 0
341
+ ? ` Available repos: ${ctx.repoNames.join(", ")}.`
342
+ : "";
343
+ throw new Error(
344
+ `Unknown repoName: ${repoName}.${hint} Tip: call mcp.workspace to see the server repo context.`,
345
+ );
346
+ };
347
+
229
348
  type NormalizedToolPayload = {
230
349
  args: unknown[];
231
350
  options: Record<string, unknown>;
@@ -451,7 +570,7 @@ const coercePayloadForTool = (
451
570
 
452
571
  switch (toolName) {
453
572
  case "projects.listProjects": {
454
- // No positional args. repoRoot is handled via options.repoRoot + buildRepoRootOnly.
573
+ // No positional args. repoRoot is resolved from repoName/repoRoot/processRoot and passed via buildRepoRootOnly.
455
574
  break;
456
575
  }
457
576
  case "projects.resolveProjectRoot":
@@ -852,7 +971,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
852
971
  description: "Unused.",
853
972
  additionalProperties: true,
854
973
  },
855
- repoRoot: {
974
+ repoName: {
856
975
  type: "string",
857
976
  description: "Unused.",
858
977
  },
@@ -862,10 +981,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
862
981
  type: "object",
863
982
  additionalProperties: true,
864
983
  properties: {
865
- repoRoot: {
984
+ repoName: {
866
985
  type: "string",
867
986
  description:
868
- 'Repo root containing /projects and /agents. If you have a project root like ".../projects/adl", omit this or pass its parent.',
987
+ "Repo selector (LLM-friendly). Omit to use server default. Tip: call mcp.workspace to see available repoName values.",
869
988
  },
870
989
  },
871
990
  required: [],
@@ -878,10 +997,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
878
997
  type: "string",
879
998
  description: 'Project name under /projects (e.g. "adl").',
880
999
  },
881
- repoRoot: {
1000
+ repoName: {
882
1001
  type: "string",
883
1002
  description:
884
- "Repo root on the server filesystem (contains /projects and /agents). If omitted, uses the server default (auto-detected from cwd).",
1003
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
885
1004
  },
886
1005
  args: {
887
1006
  type: "array",
@@ -904,9 +1023,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
904
1023
  type: "string",
905
1024
  description: 'Project name under /projects (e.g. "adl").',
906
1025
  },
907
- repoRoot: {
1026
+ repoName: {
908
1027
  type: "string",
909
- description: "Repo root containing /projects and /agents.",
1028
+ description:
1029
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
910
1030
  },
911
1031
  args: {
912
1032
  type: "array",
@@ -934,9 +1054,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
934
1054
  description:
935
1055
  'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
936
1056
  },
937
- repoRoot: {
1057
+ repoName: {
938
1058
  type: "string",
939
- description: "Repo root containing /projects and /agents.",
1059
+ description:
1060
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
940
1061
  },
941
1062
  args: {
942
1063
  type: "array",
@@ -1011,9 +1132,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1011
1132
  type: "string",
1012
1133
  description: "Remote repo override for gitea mode.",
1013
1134
  },
1014
- repoRoot: {
1135
+ repoName: {
1015
1136
  type: "string",
1016
- description: "Repo root containing /projects and /agents.",
1137
+ description:
1138
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1017
1139
  },
1018
1140
  args: {
1019
1141
  type: "array",
@@ -1088,9 +1210,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1088
1210
  type: "string",
1089
1211
  description: "Remote repo override for gitea mode.",
1090
1212
  },
1091
- repoRoot: {
1213
+ repoName: {
1092
1214
  type: "string",
1093
- description: "Repo root containing /projects and /agents.",
1215
+ description:
1216
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1094
1217
  },
1095
1218
  args: {
1096
1219
  type: "array",
@@ -1204,9 +1327,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1204
1327
  type: "boolean",
1205
1328
  description: "If true, only return issues with TASK-* IDs.",
1206
1329
  },
1207
- repoRoot: {
1330
+ repoName: {
1208
1331
  type: "string",
1209
- description: "Repo root containing /projects.",
1332
+ description:
1333
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1210
1334
  },
1211
1335
  args: {
1212
1336
  type: "array",
@@ -1245,9 +1369,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1245
1369
  type: "boolean",
1246
1370
  description: "If true, restrict to issues with TASK-* payloads.",
1247
1371
  },
1248
- repoRoot: {
1372
+ repoName: {
1249
1373
  type: "string",
1250
- description: "Repo root containing /projects.",
1374
+ description:
1375
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1251
1376
  },
1252
1377
  args: {
1253
1378
  type: "array",
@@ -1302,9 +1427,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1302
1427
  type: "string",
1303
1428
  description: "Optional task signature.",
1304
1429
  },
1305
- repoRoot: {
1430
+ repoName: {
1306
1431
  type: "string",
1307
- description: "Repo root containing /projects.",
1432
+ description:
1433
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1308
1434
  },
1309
1435
  args: {
1310
1436
  type: "array",
@@ -1336,9 +1462,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1336
1462
  items: { type: "string" },
1337
1463
  description: "Labels to set.",
1338
1464
  },
1339
- repoRoot: {
1465
+ repoName: {
1340
1466
  type: "string",
1341
- description: "Repo root containing /projects.",
1467
+ description:
1468
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1342
1469
  },
1343
1470
  args: {
1344
1471
  type: "array",
@@ -1357,10 +1484,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1357
1484
  type: "object",
1358
1485
  additionalProperties: true,
1359
1486
  properties: {
1360
- repoRoot: {
1487
+ repoName: {
1361
1488
  type: "string",
1362
1489
  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.",
1490
+ "Optional repo selector (LLM-friendly). Omit to use server default. You can also pass a server filesystem path via legacy repoRoot/processRoot.",
1364
1491
  },
1365
1492
  },
1366
1493
  required: [],
@@ -1425,10 +1552,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1425
1552
  type: "string",
1426
1553
  description: "Optional override cache directory.",
1427
1554
  },
1428
- repoRoot: {
1555
+ repoName: {
1429
1556
  type: "string",
1430
1557
  description:
1431
- "Repo root containing /projects. If you pass a project root, it will be normalized.",
1558
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1432
1559
  },
1433
1560
  },
1434
1561
  $comment: safeJsonStringify({
@@ -1460,7 +1587,7 @@ const buildArgsSchemaFromPlaceholders = (
1460
1587
  const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
1461
1588
  "projects.listProjects": {
1462
1589
  type: "array",
1463
- description: "No positional arguments. Use options.repoRoot if needed.",
1590
+ description: "No positional arguments. Use options.repoName if needed.",
1464
1591
  minItems: 0,
1465
1592
  maxItems: 0,
1466
1593
  items: {},
@@ -1594,12 +1721,12 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1594
1721
  }
1595
1722
 
1596
1723
  if (plan === "repoRootOnly") {
1597
- example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1724
+ example.options = { repoName: "<repo-name>", ...defaultOptions };
1598
1725
  return example;
1599
1726
  }
1600
1727
 
1601
1728
  if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1602
- example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1729
+ example.options = { repoName: "<repo-name>", ...defaultOptions };
1603
1730
  return example;
1604
1731
  }
1605
1732
 
@@ -1630,10 +1757,10 @@ const defaultToolInputSchema = (toolName: string) => ({
1630
1757
  additionalProperties: true,
1631
1758
  description: "Named options",
1632
1759
  },
1633
- repoRoot: {
1760
+ repoName: {
1634
1761
  type: "string",
1635
1762
  description:
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.',
1763
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1637
1764
  },
1638
1765
  },
1639
1766
  $comment: safeJsonStringify({
@@ -1868,10 +1995,14 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
1868
1995
  const invokeTool = async (
1869
1996
  tool: ToolDefinition,
1870
1997
  payload: unknown,
1871
- defaultRepoRoot: string,
1998
+ repoCtx: RepoResolutionContext,
1872
1999
  ): Promise<unknown> => {
1873
2000
  const normalized = normalizePayload(payload);
1874
- const { args, options } = coercePayloadForTool(tool.name, normalized);
2001
+ const coerced = coercePayloadForTool(tool.name, normalized);
2002
+ const { args, options } = {
2003
+ args: coerced.args,
2004
+ options: resolveRepoSelectorOptions(coerced.options, repoCtx),
2005
+ };
1875
2006
  const invoke =
1876
2007
  toolInvocationPlans[tool.name] ??
1877
2008
  ((rawArgs, rawOptions, _repoRoot) => {
@@ -1881,7 +2012,7 @@ const invokeTool = async (
1881
2012
  }
1882
2013
  return invocationArgs;
1883
2014
  });
1884
- const invocationArgs = invoke(args, options, defaultRepoRoot);
2015
+ const invocationArgs = invoke(args, options, repoCtx.defaultRepoRoot);
1885
2016
 
1886
2017
  return Promise.resolve(tool.method(...invocationArgs));
1887
2018
  };
@@ -1895,6 +2026,11 @@ export interface ExampleMcpServerOptions {
1895
2026
  * If omitted, the server attempts to auto-detect by walking up from cwd.
1896
2027
  */
1897
2028
  repoRoot?: string;
2029
+ /**
2030
+ * Optional default repo name (LLM-friendly identifier).
2031
+ * If omitted, defaults to the basename of the resolved repo root.
2032
+ */
2033
+ repoName?: string;
1898
2034
  allowedRootEndpoints?: string[];
1899
2035
  disableWrite?: boolean;
1900
2036
  enableIssues?: boolean;
@@ -2032,6 +2168,25 @@ export const createExampleMcpServer = (
2032
2168
  process.env.F0_REPO_ROOT ??
2033
2169
  process.cwd(),
2034
2170
  );
2171
+ const defaultRepoName =
2172
+ (options.repoName ?? process.env.MCP_REPO_NAME ?? process.env.F0_REPO_NAME)?.trim() ||
2173
+ path.basename(defaultRepoRoot);
2174
+ const repoMapRaw = process.env.MCP_REPOS ?? process.env.F0_REPOS;
2175
+ const repoMap = {
2176
+ ...parseRepoMap(repoMapRaw),
2177
+ [defaultRepoName]: defaultRepoRoot,
2178
+ };
2179
+ const repoNames = Object.keys(repoMap).sort((a, b) => a.localeCompare(b));
2180
+ const repoMapByKey: Record<string, string> = {};
2181
+ for (const [name, root] of Object.entries(repoMap)) {
2182
+ repoMapByKey[name.toLowerCase()] = root;
2183
+ }
2184
+ const repoCtx: RepoResolutionContext = {
2185
+ defaultRepoRoot,
2186
+ defaultRepoName,
2187
+ repoMapByKey,
2188
+ repoNames,
2189
+ };
2035
2190
 
2036
2191
  const parseString = (value: unknown): string | null => {
2037
2192
  if (typeof value !== "string") return null;
@@ -2071,7 +2226,7 @@ export const createExampleMcpServer = (
2071
2226
  "F0 MCP helper tools:",
2072
2227
  "- mcp.listTools: returns tool catalog with access + invocation hints",
2073
2228
  "- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
2074
- "- mcp.workspace: explain server filesystem context (cwd, repoRoot, projects)",
2229
+ "- mcp.workspace: explain server filesystem context (cwd, repoName/repoRoot, projects)",
2075
2230
  "- mcp.search: LLM-friendly search over project docs/spec (local-first)",
2076
2231
  "",
2077
2232
  'Tip: Prefer mcp.search for "search spec/docs" requests.',
@@ -2082,13 +2237,21 @@ export const createExampleMcpServer = (
2082
2237
  const received = isRecord(input)
2083
2238
  ? {
2084
2239
  keys: Object.keys(input),
2240
+ repoName: (input as any).repoName ?? null,
2085
2241
  repoRoot: (input as any).repoRoot ?? null,
2086
2242
  processRoot: (input as any).processRoot ?? null,
2087
2243
  }
2088
- : { keys: [], repoRoot: null, processRoot: null };
2089
- const raw =
2090
- parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2091
- const repoRoot = raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
2244
+ : { keys: [], repoName: null, repoRoot: null, processRoot: null };
2245
+ const requestedRepoName = parseString(payload.repoName);
2246
+ const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2247
+ const repoRoot =
2248
+ typeof resolved.repoRoot === "string"
2249
+ ? resolved.repoRoot
2250
+ : repoCtx.defaultRepoRoot;
2251
+ const effectiveRepoName =
2252
+ requestedRepoName && !looksLikePathish(requestedRepoName)
2253
+ ? requestedRepoName
2254
+ : repoCtx.defaultRepoName;
2092
2255
 
2093
2256
  const projectsDir = path.join(repoRoot, "projects");
2094
2257
  const apiDir = path.join(repoRoot, "api");
@@ -2102,8 +2265,8 @@ export const createExampleMcpServer = (
2102
2265
  if (!hasProjectsDir) {
2103
2266
  return [
2104
2267
  "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.",
2268
+ "Start the MCP server from the monorepo root (the folder that contains both /api and /projects), or configure the server with --repo-root / MCP_REPO_ROOT.",
2269
+ "Tool callers should usually omit repoName and let the server use its default workspace.",
2107
2270
  ].join(" ");
2108
2271
  }
2109
2272
  if (hasProjectsDir && projects.length === 0) {
@@ -2118,9 +2281,11 @@ export const createExampleMcpServer = (
2118
2281
  return {
2119
2282
  received,
2120
2283
  cwd: process.cwd(),
2121
- configuredRepoRoot: options.repoRoot ?? null,
2122
- defaultRepoRoot,
2284
+ defaultRepoName: repoCtx.defaultRepoName,
2285
+ defaultRepoRoot: repoCtx.defaultRepoRoot,
2286
+ repoName: effectiveRepoName,
2123
2287
  repoRoot,
2288
+ availableRepoNames: repoCtx.repoNames,
2124
2289
  hasProjectsDir,
2125
2290
  hasApiDir,
2126
2291
  hasAgentsDir,
@@ -2168,11 +2333,11 @@ export const createExampleMcpServer = (
2168
2333
  },
2169
2334
  search: async (input: unknown) => {
2170
2335
  const payload = isRecord(input) ? input : {};
2171
- const repoRoot = (() => {
2172
- const raw =
2173
- parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2174
- return raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
2175
- })();
2336
+ const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2337
+ const repoRoot =
2338
+ typeof resolved.repoRoot === "string"
2339
+ ? resolved.repoRoot
2340
+ : repoCtx.defaultRepoRoot;
2176
2341
 
2177
2342
  const sectionRaw = parseString(payload.section)?.toLowerCase();
2178
2343
  const section = sectionRaw === "docs" ? "docs" : "spec";
@@ -2367,7 +2532,7 @@ export const createExampleMcpServer = (
2367
2532
  tool: prefix
2368
2533
  ? `${prefix}.projects.listProjects`
2369
2534
  : "projects.listProjects",
2370
- options: { repoRoot: "<repo-root>" },
2535
+ options: { repoName: "<repo-name>" },
2371
2536
  },
2372
2537
  ],
2373
2538
  continueOnError: true,
@@ -2546,8 +2711,9 @@ export const createExampleMcpServer = (
2546
2711
  /projects[\\/].+projects[\\/]/i.test(message)
2547
2712
  ) {
2548
2713
  details.hint =
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.";
2714
+ "You likely passed a project root as repoName (or legacy repoRoot/processRoot). Repo selection should point at the monorepo root that contains /projects.";
2715
+ details.suggestion =
2716
+ "Call mcp.workspace to see the server’s cwd/default repoRoot, then omit repoName or pass the correct repoName.";
2551
2717
  }
2552
2718
 
2553
2719
  if (
@@ -2556,8 +2722,9 @@ export const createExampleMcpServer = (
2556
2722
  /projects[\\/]/i.test(message)
2557
2723
  ) {
2558
2724
  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.";
2725
+ "Repo selection might be wrong, or the server filesystem does not contain /projects for this workspace.";
2726
+ details.suggestion =
2727
+ "Call mcp.workspace to see the server’s cwd/default repoRoot and available repoName values.";
2561
2728
  details.example = {
2562
2729
  tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
2563
2730
  arguments: {},
@@ -2625,11 +2792,7 @@ export const createExampleMcpServer = (
2625
2792
  }
2626
2793
 
2627
2794
  try {
2628
- const data = await invokeTool(
2629
- toolDefinition,
2630
- { args, options },
2631
- defaultRepoRoot,
2632
- );
2795
+ const data = await invokeTool(toolDefinition, { args, options }, repoCtx);
2633
2796
  return { index, tool, isError: false, data };
2634
2797
  } catch (error) {
2635
2798
  const message =
@@ -2681,7 +2844,7 @@ export const createExampleMcpServer = (
2681
2844
  }
2682
2845
 
2683
2846
  try {
2684
- const data = await invokeTool(tool, request.params.arguments, defaultRepoRoot);
2847
+ const data = await invokeTool(tool, request.params.arguments, repoCtx);
2685
2848
  return toolOk(data);
2686
2849
  } catch (error) {
2687
2850
  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.6",
3
+ "version": "1.1.7",
4
4
  "description": "Foundation 0 API",
5
5
  "type": "module",
6
6
  "bin": {