@foundation0/api 1.1.5 → 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,9 @@ 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
71
+ const repoRoot =
72
+ getArgValue('--repo-root') ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT
70
73
  const allowedRootEndpoints = parseListArg(
71
74
  getArgValue('--allowed-root-endpoints') ?? process.env.MCP_ALLOWED_ROOT_ENDPOINTS,
72
75
  )
@@ -80,6 +83,11 @@ if (hasFlag('--help') || hasFlag('-h')) {
80
83
  console.log(' --server-name <name>')
81
84
  console.log(' --server-version <version>')
82
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.')
88
+ console.log(' --repo-root <path>')
89
+ console.log(' Default repo root on the server filesystem (contains /api and /projects).')
90
+ console.log(' Env: MCP_REPO_ROOT or F0_REPO_ROOT.')
83
91
  console.log(' --allowed-root-endpoints <csv>')
84
92
  console.log(' Example: --allowed-root-endpoints projects')
85
93
  console.log(' Example: --allowed-root-endpoints agents,projects')
@@ -96,6 +104,8 @@ void runExampleMcpServer({
96
104
  serverName: serverName ?? undefined,
97
105
  serverVersion: serverVersion ?? undefined,
98
106
  toolsPrefix,
107
+ repoRoot: repoRoot ?? undefined,
108
+ repoName: repoName ?? undefined,
99
109
  allowedRootEndpoints,
100
110
  disableWrite,
101
111
  enableIssues,
package/mcp/manual.md CHANGED
@@ -7,10 +7,12 @@ 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: 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
+
14
16
  ## 1) Tool naming (prefixes + aliases)
15
17
 
16
18
  Depending on how the server was started, tool names may be:
@@ -24,7 +26,7 @@ Some tools also have OpenAI-safe underscore aliases (no dots). Example:
24
26
 
25
27
  - `net.curl` may also be available as `net_curl`
26
28
 
27
- ## 2) The 3 discovery calls
29
+ ## 2) The 4 discovery calls
28
30
 
29
31
  ### A) List all tools
30
32
 
@@ -53,25 +55,33 @@ Tool call:
53
55
  "projectName": "<project-name>",
54
56
  "section": "spec",
55
57
  "pattern": "authentication",
56
- "repoRoot": "<repo-root>",
58
+ "repoName": "<repo-name>",
57
59
  "ignoreCase": true,
58
60
  "maxCount": 50
59
61
  }
60
62
  }
61
63
  ```
62
64
 
65
+ ### D) `mcp.workspace` to debug repoName/repoRoot/cwd issues
66
+
67
+ Tool call:
68
+
69
+ ```json
70
+ { "name": "mcp.workspace", "arguments": {} }
71
+ ```
72
+
63
73
  ## 3) Payload shapes (important)
64
74
 
65
75
  Most tools accept either:
66
76
 
67
77
  ```json
68
- { "args": ["<project-name>"], "options": { "repoRoot": "<repo-root>" } }
78
+ { "args": ["<project-name>"], "options": { "repoName": "<repo-name>" } }
69
79
  ```
70
80
 
71
81
  or named keys (recommended). The server merges top-level keys into `options`:
72
82
 
73
83
  ```json
74
- { "projectName": "<project-name>", "repoRoot": "<repo-root>" }
84
+ { "projectName": "<project-name>", "repoName": "<repo-name>" }
75
85
  ```
76
86
 
77
87
  Guideline: if a tool has a natural named parameter (`projectName`, `agentName`, `target`, `taskRef`), pass it explicitly.
@@ -83,7 +93,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
83
93
  ```json
84
94
  {
85
95
  "name": "projects.listProjects",
86
- "arguments": { "repoRoot": "<repo-root>" }
96
+ "arguments": { "repoName": "<repo-name>" }
87
97
  }
88
98
  ```
89
99
 
@@ -92,7 +102,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
92
102
  ```json
93
103
  {
94
104
  "name": "agents.listAgents",
95
- "arguments": { "repoRoot": "<repo-root>" }
105
+ "arguments": { "repoName": "<repo-name>" }
96
106
  }
97
107
  ```
98
108
 
@@ -103,7 +113,7 @@ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`,
103
113
  "name": "projects.setActive",
104
114
  "arguments": {
105
115
  "args": ["<project-name>", "/implementation-plan.v0.0.1"],
106
- "options": { "repoRoot": "<repo-root>", "latest": true }
116
+ "options": { "repoName": "<repo-name>", "latest": true }
107
117
  }
108
118
  }
109
119
  ```
@@ -118,7 +128,7 @@ Call `batch` (or `<prefix>.batch`) to run multiple tool calls:
118
128
  "arguments": {
119
129
  "calls": [
120
130
  { "tool": "projects.usage" },
121
- { "tool": "projects.listProjects", "options": { "repoRoot": "<repo-root>" } }
131
+ { "tool": "projects.listProjects", "options": { "repoName": "<repo-name>" } }
122
132
  ],
123
133
  "continueOnError": true,
124
134
  "maxConcurrency": 4
@@ -137,7 +147,8 @@ If you get:
137
147
 
138
148
  - **Unknown tool**: use the `suggestions` from the error (when present), or call `mcp.listTools` again and retry.
139
149
  - **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`).
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.
141
152
 
142
153
  ## 6) Tool availability (read/write/admin)
143
154
 
@@ -42,6 +42,7 @@ describe("createExampleMcpServer endpoint whitelist", () => {
42
42
  expect(names).toContain("mcp.usage");
43
43
  expect(names).toContain("mcp.listTools");
44
44
  expect(names).toContain("mcp.describeTool");
45
+ expect(names).toContain("mcp.workspace");
45
46
  expect(names).not.toContain("mcp.search");
46
47
  });
47
48
 
@@ -98,6 +99,7 @@ describe("createExampleMcpServer endpoint whitelist", () => {
98
99
  expect(names).toContain("mcp.usage");
99
100
  expect(names).toContain("mcp.listTools");
100
101
  expect(names).toContain("mcp.describeTool");
102
+ expect(names).toContain("mcp.workspace");
101
103
  expect(names).not.toContain("mcp.search");
102
104
  });
103
105
 
@@ -202,9 +204,9 @@ describe("createExampleMcpServer request handling", () => {
202
204
  }
203
205
  });
204
206
 
205
- it("accepts repoRoot for projects tools (preferred over legacy processRoot)", async () => {
207
+ it("accepts repoName for projects tools (preferred over legacy repoRoot/processRoot)", async () => {
206
208
  const tempDir = await fs.mkdtemp(
207
- path.join(os.tmpdir(), "f0-mcp-server-reporoot-"),
209
+ path.join(os.tmpdir(), "f0-mcp-server-reponame-"),
208
210
  );
209
211
  try {
210
212
  await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
@@ -215,7 +217,7 @@ describe("createExampleMcpServer request handling", () => {
215
217
  recursive: true,
216
218
  });
217
219
 
218
- const instance = createExampleMcpServer();
220
+ const instance = createExampleMcpServer({ repoRoot: tempDir, repoName: "test" });
219
221
  const handler = getToolHandler(instance);
220
222
 
221
223
  const result = await handler(
@@ -224,7 +226,7 @@ describe("createExampleMcpServer request handling", () => {
224
226
  params: {
225
227
  name: "projects.listProjects",
226
228
  arguments: {
227
- repoRoot: tempDir,
229
+ repoName: "test",
228
230
  },
229
231
  },
230
232
  },
@@ -241,7 +243,7 @@ describe("createExampleMcpServer request handling", () => {
241
243
  }
242
244
  });
243
245
 
244
- 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 () => {
245
247
  const tempDir = await fs.mkdtemp(
246
248
  path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
247
249
  );
@@ -318,7 +320,7 @@ describe("createExampleMcpServer request handling", () => {
318
320
  }
319
321
  });
320
322
 
321
- it("normalizes repoRoot when caller accidentally passes a project root", async () => {
323
+ it("normalizes legacy repoRoot when caller accidentally passes a project root", async () => {
322
324
  const tempDir = await fs.mkdtemp(
323
325
  path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
324
326
  );
@@ -357,6 +359,79 @@ describe("createExampleMcpServer request handling", () => {
357
359
  }
358
360
  });
359
361
 
362
+ it("uses server default repoRoot when repoName 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.defaultRepoRoot).toBe(path.resolve(tempDir));
428
+ expect(payload.result.hasProjectsDir).toBe(true);
429
+ expect(Array.isArray(payload.result.projects)).toBe(true);
430
+ } finally {
431
+ await fs.rm(tempDir, { recursive: true, force: true });
432
+ }
433
+ });
434
+
360
435
  it('parses continueOnError from string "false" (fails fast)', async () => {
361
436
  const instance = createExampleMcpServer();
362
437
  const handler = getToolHandler(instance);
package/mcp/server.ts CHANGED
@@ -48,7 +48,7 @@ type ApiEndpoint = {
48
48
  mcp: ToolNamespace;
49
49
  };
50
50
 
51
- const MCP_HELPER_TOOL_KEYS = ["usage", "listTools", "describeTool"] as const;
51
+ const MCP_HELPER_TOOL_KEYS = ["usage", "listTools", "describeTool", "workspace"] as const;
52
52
  type McpHelperToolKey = (typeof MCP_HELPER_TOOL_KEYS)[number];
53
53
  const MCP_HELPER_TOOL_NAMES = new Set<string>(
54
54
  MCP_HELPER_TOOL_KEYS.map((key) => `mcp.${key}`),
@@ -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)`,
@@ -176,17 +176,6 @@ const isDir = (candidate: string): boolean => {
176
176
  const looksLikeRepoRoot = (candidate: string): boolean =>
177
177
  isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
178
178
 
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
179
  const normalizeRepoRoot = (raw: string): string => {
191
180
  const resolved = path.resolve(raw);
192
181
  if (looksLikeRepoRoot(resolved)) return resolved;
@@ -220,8 +209,10 @@ const normalizeRepoRootOption = (
220
209
  const raw = rawRepoRoot ?? rawProcessRoot;
221
210
 
222
211
  if (typeof raw !== "string" || raw.trim().length === 0) {
223
- const detected = detectRepoRootFromCwd();
224
- return detected ? { ...options, repoRoot: detected } : options;
212
+ const next = { ...options };
213
+ delete next.repoRoot;
214
+ delete next.processRoot;
215
+ return next;
225
216
  }
226
217
 
227
218
  const trimmed = raw.trim();
@@ -235,6 +226,125 @@ const normalizeRepoRootOption = (
235
226
  return alreadyCanonical ? options : next;
236
227
  };
237
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
+
238
348
  type NormalizedToolPayload = {
239
349
  args: unknown[];
240
350
  options: Record<string, unknown>;
@@ -460,7 +570,7 @@ const coercePayloadForTool = (
460
570
 
461
571
  switch (toolName) {
462
572
  case "projects.listProjects": {
463
- // 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.
464
574
  break;
465
575
  }
466
576
  case "projects.resolveProjectRoot":
@@ -861,7 +971,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
861
971
  description: "Unused.",
862
972
  additionalProperties: true,
863
973
  },
864
- repoRoot: {
974
+ repoName: {
865
975
  type: "string",
866
976
  description: "Unused.",
867
977
  },
@@ -871,10 +981,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
871
981
  type: "object",
872
982
  additionalProperties: true,
873
983
  properties: {
874
- repoRoot: {
984
+ repoName: {
875
985
  type: "string",
876
986
  description:
877
- '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.",
878
988
  },
879
989
  },
880
990
  required: [],
@@ -887,10 +997,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
887
997
  type: "string",
888
998
  description: 'Project name under /projects (e.g. "adl").',
889
999
  },
890
- repoRoot: {
1000
+ repoName: {
891
1001
  type: "string",
892
1002
  description:
893
- "Repo root containing /projects and /agents. If omitted, uses server cwd.",
1003
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
894
1004
  },
895
1005
  args: {
896
1006
  type: "array",
@@ -913,9 +1023,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
913
1023
  type: "string",
914
1024
  description: 'Project name under /projects (e.g. "adl").',
915
1025
  },
916
- repoRoot: {
1026
+ repoName: {
917
1027
  type: "string",
918
- description: "Repo root containing /projects and /agents.",
1028
+ description:
1029
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
919
1030
  },
920
1031
  args: {
921
1032
  type: "array",
@@ -943,9 +1054,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
943
1054
  description:
944
1055
  'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
945
1056
  },
946
- repoRoot: {
1057
+ repoName: {
947
1058
  type: "string",
948
- description: "Repo root containing /projects and /agents.",
1059
+ description:
1060
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
949
1061
  },
950
1062
  args: {
951
1063
  type: "array",
@@ -1020,9 +1132,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1020
1132
  type: "string",
1021
1133
  description: "Remote repo override for gitea mode.",
1022
1134
  },
1023
- repoRoot: {
1135
+ repoName: {
1024
1136
  type: "string",
1025
- description: "Repo root containing /projects and /agents.",
1137
+ description:
1138
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1026
1139
  },
1027
1140
  args: {
1028
1141
  type: "array",
@@ -1097,9 +1210,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1097
1210
  type: "string",
1098
1211
  description: "Remote repo override for gitea mode.",
1099
1212
  },
1100
- repoRoot: {
1213
+ repoName: {
1101
1214
  type: "string",
1102
- description: "Repo root containing /projects and /agents.",
1215
+ description:
1216
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1103
1217
  },
1104
1218
  args: {
1105
1219
  type: "array",
@@ -1213,9 +1327,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1213
1327
  type: "boolean",
1214
1328
  description: "If true, only return issues with TASK-* IDs.",
1215
1329
  },
1216
- repoRoot: {
1330
+ repoName: {
1217
1331
  type: "string",
1218
- description: "Repo root containing /projects.",
1332
+ description:
1333
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1219
1334
  },
1220
1335
  args: {
1221
1336
  type: "array",
@@ -1254,9 +1369,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1254
1369
  type: "boolean",
1255
1370
  description: "If true, restrict to issues with TASK-* payloads.",
1256
1371
  },
1257
- repoRoot: {
1372
+ repoName: {
1258
1373
  type: "string",
1259
- description: "Repo root containing /projects.",
1374
+ description:
1375
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1260
1376
  },
1261
1377
  args: {
1262
1378
  type: "array",
@@ -1311,9 +1427,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1311
1427
  type: "string",
1312
1428
  description: "Optional task signature.",
1313
1429
  },
1314
- repoRoot: {
1430
+ repoName: {
1315
1431
  type: "string",
1316
- description: "Repo root containing /projects.",
1432
+ description:
1433
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1317
1434
  },
1318
1435
  args: {
1319
1436
  type: "array",
@@ -1345,9 +1462,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1345
1462
  items: { type: "string" },
1346
1463
  description: "Labels to set.",
1347
1464
  },
1348
- repoRoot: {
1465
+ repoName: {
1349
1466
  type: "string",
1350
- description: "Repo root containing /projects.",
1467
+ description:
1468
+ "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1351
1469
  },
1352
1470
  args: {
1353
1471
  type: "array",
@@ -1362,6 +1480,21 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1362
1480
  },
1363
1481
  required: ["projectName", "title"],
1364
1482
  },
1483
+ "mcp.workspace": {
1484
+ type: "object",
1485
+ additionalProperties: true,
1486
+ properties: {
1487
+ repoName: {
1488
+ type: "string",
1489
+ description:
1490
+ "Optional repo selector (LLM-friendly). Omit to use server default. You can also pass a server filesystem path via legacy repoRoot/processRoot.",
1491
+ },
1492
+ },
1493
+ required: [],
1494
+ $comment: safeJsonStringify({
1495
+ example: {},
1496
+ }),
1497
+ },
1365
1498
  "mcp.search": {
1366
1499
  type: "object",
1367
1500
  additionalProperties: true,
@@ -1419,10 +1552,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1419
1552
  type: "string",
1420
1553
  description: "Optional override cache directory.",
1421
1554
  },
1422
- repoRoot: {
1555
+ repoName: {
1423
1556
  type: "string",
1424
1557
  description:
1425
- "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.",
1426
1559
  },
1427
1560
  },
1428
1561
  $comment: safeJsonStringify({
@@ -1454,7 +1587,7 @@ const buildArgsSchemaFromPlaceholders = (
1454
1587
  const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
1455
1588
  "projects.listProjects": {
1456
1589
  type: "array",
1457
- description: "No positional arguments. Use options.repoRoot if needed.",
1590
+ description: "No positional arguments. Use options.repoName if needed.",
1458
1591
  minItems: 0,
1459
1592
  maxItems: 0,
1460
1593
  items: {},
@@ -1588,12 +1721,12 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1588
1721
  }
1589
1722
 
1590
1723
  if (plan === "repoRootOnly") {
1591
- example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1724
+ example.options = { repoName: "<repo-name>", ...defaultOptions };
1592
1725
  return example;
1593
1726
  }
1594
1727
 
1595
1728
  if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1596
- example.options = { repoRoot: "<repo-root>", ...defaultOptions };
1729
+ example.options = { repoName: "<repo-name>", ...defaultOptions };
1597
1730
  return example;
1598
1731
  }
1599
1732
 
@@ -1624,10 +1757,10 @@ const defaultToolInputSchema = (toolName: string) => ({
1624
1757
  additionalProperties: true,
1625
1758
  description: "Named options",
1626
1759
  },
1627
- repoRoot: {
1760
+ repoName: {
1628
1761
  type: "string",
1629
1762
  description:
1630
- 'Repo root containing /projects and /agents. 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.",
1631
1764
  },
1632
1765
  },
1633
1766
  $comment: safeJsonStringify({
@@ -1743,11 +1876,13 @@ const buildToolList = (
1743
1876
  type ToolInvoker = (
1744
1877
  args: unknown[],
1745
1878
  options: Record<string, unknown>,
1879
+ defaultRepoRoot: string,
1746
1880
  ) => unknown[];
1747
1881
 
1748
1882
  const buildOptionsOnly = (
1749
1883
  args: unknown[],
1750
1884
  options: Record<string, unknown>,
1885
+ _defaultRepoRoot: string,
1751
1886
  ): unknown[] => {
1752
1887
  const invocationArgs: unknown[] = [...args];
1753
1888
  if (Object.keys(options).length > 0) {
@@ -1759,6 +1894,7 @@ const buildOptionsOnly = (
1759
1894
  const buildOptionsThenRepoRoot = (
1760
1895
  args: unknown[],
1761
1896
  options: Record<string, unknown>,
1897
+ defaultRepoRoot: string,
1762
1898
  ): unknown[] => {
1763
1899
  const invocationArgs: unknown[] = [...args];
1764
1900
  const remaining = { ...options };
@@ -1766,16 +1902,16 @@ const buildOptionsThenRepoRoot = (
1766
1902
  if (typeof repoRoot === "string") {
1767
1903
  delete remaining.repoRoot;
1768
1904
  }
1905
+ const resolvedRepoRoot =
1906
+ typeof repoRoot === "string" ? repoRoot : defaultRepoRoot;
1769
1907
 
1770
1908
  if (Object.keys(remaining).length > 0) {
1771
1909
  invocationArgs.push(remaining);
1772
- } else if (typeof repoRoot === "string") {
1910
+ } else if (resolvedRepoRoot) {
1773
1911
  // Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
1774
1912
  invocationArgs.push({});
1775
1913
  }
1776
- if (typeof repoRoot === "string") {
1777
- invocationArgs.push(repoRoot);
1778
- }
1914
+ invocationArgs.push(resolvedRepoRoot);
1779
1915
 
1780
1916
  return invocationArgs;
1781
1917
  };
@@ -1783,6 +1919,7 @@ const buildOptionsThenRepoRoot = (
1783
1919
  const buildRepoRootThenOptions = (
1784
1920
  args: unknown[],
1785
1921
  options: Record<string, unknown>,
1922
+ defaultRepoRoot: string,
1786
1923
  ): unknown[] => {
1787
1924
  const invocationArgs: unknown[] = [...args];
1788
1925
  const remaining = { ...options };
@@ -1791,9 +1928,7 @@ const buildRepoRootThenOptions = (
1791
1928
  delete remaining.repoRoot;
1792
1929
  }
1793
1930
 
1794
- if (typeof repoRoot === "string") {
1795
- invocationArgs.push(repoRoot);
1796
- }
1931
+ invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
1797
1932
  if (Object.keys(remaining).length > 0) {
1798
1933
  invocationArgs.push(remaining);
1799
1934
  }
@@ -1804,12 +1939,11 @@ const buildRepoRootThenOptions = (
1804
1939
  const buildRepoRootOnly = (
1805
1940
  args: unknown[],
1806
1941
  options: Record<string, unknown>,
1942
+ defaultRepoRoot: string,
1807
1943
  ): unknown[] => {
1808
1944
  const invocationArgs: unknown[] = [...args];
1809
1945
  const repoRoot = options.repoRoot;
1810
- if (typeof repoRoot === "string") {
1811
- invocationArgs.push(repoRoot);
1812
- }
1946
+ invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
1813
1947
  return invocationArgs;
1814
1948
  };
1815
1949
 
@@ -1828,7 +1962,7 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
1828
1962
  "projects.resolveProjectTargetFile": buildOptionsOnly,
1829
1963
  "agents.loadAgent": buildRepoRootOnly,
1830
1964
  "agents.loadAgentPrompt": buildRepoRootOnly,
1831
- "projects.resolveImplementationPlan": (args, options) => {
1965
+ "projects.resolveImplementationPlan": (args, options, _defaultRepoRoot) => {
1832
1966
  const invocationArgs: unknown[] = [...args];
1833
1967
  const remaining = { ...options };
1834
1968
  const repoRoot = remaining.repoRoot;
@@ -1861,19 +1995,24 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
1861
1995
  const invokeTool = async (
1862
1996
  tool: ToolDefinition,
1863
1997
  payload: unknown,
1998
+ repoCtx: RepoResolutionContext,
1864
1999
  ): Promise<unknown> => {
1865
2000
  const normalized = normalizePayload(payload);
1866
- 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
+ };
1867
2006
  const invoke =
1868
2007
  toolInvocationPlans[tool.name] ??
1869
- ((rawArgs, rawOptions) => {
2008
+ ((rawArgs, rawOptions, _repoRoot) => {
1870
2009
  const invocationArgs = [...rawArgs];
1871
2010
  if (Object.keys(rawOptions).length > 0) {
1872
2011
  invocationArgs.push(rawOptions);
1873
2012
  }
1874
2013
  return invocationArgs;
1875
2014
  });
1876
- const invocationArgs = invoke(args, options);
2015
+ const invocationArgs = invoke(args, options, repoCtx.defaultRepoRoot);
1877
2016
 
1878
2017
  return Promise.resolve(tool.method(...invocationArgs));
1879
2018
  };
@@ -1882,6 +2021,16 @@ export interface ExampleMcpServerOptions {
1882
2021
  serverName?: string;
1883
2022
  serverVersion?: string;
1884
2023
  toolsPrefix?: string;
2024
+ /**
2025
+ * Optional default repo root on the server filesystem.
2026
+ * If omitted, the server attempts to auto-detect by walking up from cwd.
2027
+ */
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;
1885
2034
  allowedRootEndpoints?: string[];
1886
2035
  disableWrite?: boolean;
1887
2036
  enableIssues?: boolean;
@@ -1922,6 +2071,7 @@ const READ_ONLY_TOOL_NAMES = new Set<string>([
1922
2071
  "mcp.usage",
1923
2072
  "mcp.listTools",
1924
2073
  "mcp.describeTool",
2074
+ "mcp.workspace",
1925
2075
  "mcp.search",
1926
2076
  ]);
1927
2077
 
@@ -2012,6 +2162,31 @@ export const createExampleMcpServer = (
2012
2162
  options: ExampleMcpServerOptions = {},
2013
2163
  ): ExampleMcpServerInstance => {
2014
2164
  let toolCatalog: unknown[] = [];
2165
+ const defaultRepoRoot = normalizeRepoRoot(
2166
+ options.repoRoot ??
2167
+ process.env.MCP_REPO_ROOT ??
2168
+ process.env.F0_REPO_ROOT ??
2169
+ process.cwd(),
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
+ };
2015
2190
 
2016
2191
  const parseString = (value: unknown): string | null => {
2017
2192
  if (typeof value !== "string") return null;
@@ -2051,11 +2226,73 @@ export const createExampleMcpServer = (
2051
2226
  "F0 MCP helper tools:",
2052
2227
  "- mcp.listTools: returns tool catalog with access + invocation hints",
2053
2228
  "- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
2229
+ "- mcp.workspace: explain server filesystem context (cwd, repoName/repoRoot, projects)",
2054
2230
  "- mcp.search: LLM-friendly search over project docs/spec (local-first)",
2055
2231
  "",
2056
2232
  'Tip: Prefer mcp.search for "search spec/docs" requests.',
2057
2233
  ].join("\n"),
2058
2234
  listTools: () => ({ tools: toolCatalog }),
2235
+ workspace: (input?: unknown) => {
2236
+ const payload = isRecord(input) ? input : {};
2237
+ const received = isRecord(input)
2238
+ ? {
2239
+ keys: Object.keys(input),
2240
+ repoName: (input as any).repoName ?? null,
2241
+ repoRoot: (input as any).repoRoot ?? null,
2242
+ processRoot: (input as any).processRoot ?? null,
2243
+ }
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;
2255
+
2256
+ const projectsDir = path.join(repoRoot, "projects");
2257
+ const apiDir = path.join(repoRoot, "api");
2258
+ const agentsDir = path.join(repoRoot, "agents");
2259
+ const hasProjectsDir = isDir(projectsDir);
2260
+ const hasApiDir = isDir(apiDir);
2261
+ const hasAgentsDir = isDir(agentsDir);
2262
+
2263
+ const projects = hasProjectsDir ? projectsApi.listProjects(repoRoot) : [];
2264
+ const hint = (() => {
2265
+ if (!hasProjectsDir) {
2266
+ return [
2267
+ "Repo does not contain /projects on the server filesystem.",
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.",
2270
+ ].join(" ");
2271
+ }
2272
+ if (hasProjectsDir && projects.length === 0) {
2273
+ return [
2274
+ "Found /projects, but no projects with docs/ were detected.",
2275
+ "Each project folder must contain a docs/ directory to be listed.",
2276
+ ].join(" ");
2277
+ }
2278
+ return null;
2279
+ })();
2280
+
2281
+ return {
2282
+ received,
2283
+ cwd: process.cwd(),
2284
+ defaultRepoName: repoCtx.defaultRepoName,
2285
+ defaultRepoRoot: repoCtx.defaultRepoRoot,
2286
+ repoName: effectiveRepoName,
2287
+ repoRoot,
2288
+ availableRepoNames: repoCtx.repoNames,
2289
+ hasProjectsDir,
2290
+ hasApiDir,
2291
+ hasAgentsDir,
2292
+ projects,
2293
+ hint,
2294
+ };
2295
+ },
2059
2296
  describeTool: (toolName: string) => {
2060
2297
  const normalized = typeof toolName === "string" ? toolName.trim() : "";
2061
2298
  if (!normalized) {
@@ -2096,11 +2333,11 @@ export const createExampleMcpServer = (
2096
2333
  },
2097
2334
  search: async (input: unknown) => {
2098
2335
  const payload = isRecord(input) ? input : {};
2099
- const repoRoot = (() => {
2100
- const raw =
2101
- parseString(payload.repoRoot) ?? parseString(payload.processRoot);
2102
- return raw ? normalizeRepoRoot(raw) : process.cwd();
2103
- })();
2336
+ const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2337
+ const repoRoot =
2338
+ typeof resolved.repoRoot === "string"
2339
+ ? resolved.repoRoot
2340
+ : repoCtx.defaultRepoRoot;
2104
2341
 
2105
2342
  const sectionRaw = parseString(payload.section)?.toLowerCase();
2106
2343
  const section = sectionRaw === "docs" ? "docs" : "spec";
@@ -2295,7 +2532,7 @@ export const createExampleMcpServer = (
2295
2532
  tool: prefix
2296
2533
  ? `${prefix}.projects.listProjects`
2297
2534
  : "projects.listProjects",
2298
- options: { repoRoot: "<repo-root>" },
2535
+ options: { repoName: "<repo-name>" },
2299
2536
  },
2300
2537
  ],
2301
2538
  continueOnError: true,
@@ -2474,7 +2711,24 @@ export const createExampleMcpServer = (
2474
2711
  /projects[\\/].+projects[\\/]/i.test(message)
2475
2712
  ) {
2476
2713
  details.hint =
2477
- "You likely passed a project root as repoRoot. repoRoot should be the repo root containing /projects. (Legacy alias: processRoot.)";
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.";
2717
+ }
2718
+
2719
+ if (
2720
+ /Project folder not found:/i.test(message) &&
2721
+ !details.hint &&
2722
+ /projects[\\/]/i.test(message)
2723
+ ) {
2724
+ details.hint =
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.";
2728
+ details.example = {
2729
+ tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
2730
+ arguments: {},
2731
+ };
2478
2732
  }
2479
2733
 
2480
2734
  if (/Missing search pattern\./i.test(message)) {
@@ -2538,7 +2792,7 @@ export const createExampleMcpServer = (
2538
2792
  }
2539
2793
 
2540
2794
  try {
2541
- const data = await invokeTool(toolDefinition, { args, options });
2795
+ const data = await invokeTool(toolDefinition, { args, options }, repoCtx);
2542
2796
  return { index, tool, isError: false, data };
2543
2797
  } catch (error) {
2544
2798
  const message =
@@ -2590,7 +2844,7 @@ export const createExampleMcpServer = (
2590
2844
  }
2591
2845
 
2592
2846
  try {
2593
- const data = await invokeTool(tool, request.params.arguments);
2847
+ const data = await invokeTool(tool, request.params.arguments, repoCtx);
2594
2848
  return toolOk(data);
2595
2849
  } catch (error) {
2596
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.5",
3
+ "version": "1.1.7",
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\"",