@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 +4 -0
- package/mcp/manual.md +13 -13
- package/mcp/server.test.ts +8 -7
- package/mcp/server.ts +228 -65
- package/package.json +1 -1
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`, `
|
|
11
|
-
4.
|
|
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:
|
|
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
|
-
"
|
|
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": { "
|
|
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>", "
|
|
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": { "
|
|
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": { "
|
|
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": { "
|
|
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": { "
|
|
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/...**:
|
|
151
|
-
- **`projects.listProjects()` returns `[]` unexpectedly**: call `mcp.workspace` to confirm the server’s `cwd
|
|
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
|
|
package/mcp/server.test.ts
CHANGED
|
@@ -204,9 +204,9 @@ describe("createExampleMcpServer request handling", () => {
|
|
|
204
204
|
}
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
it("accepts
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: {
|
|
67
|
-
`- ${tool("projects.generateSpec")}: { args: [\"<projectName>\"], options: {
|
|
68
|
-
`- ${tool("projects.setActive")}: { args: [\"<projectName>\", \"/implementation-plan.v0.0.1\"], options: {
|
|
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: {
|
|
84
|
-
`- ${tool("agents.loadAgent")}: { args: [\"<agentName>\"], options: {
|
|
85
|
-
`- ${tool("agents.setActive")}: { args: [\"<agentName>\", \"/system/boot.v0.0.1\"], options: {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
984
|
+
repoName: {
|
|
866
985
|
type: "string",
|
|
867
986
|
description:
|
|
868
|
-
|
|
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
|
-
|
|
1000
|
+
repoName: {
|
|
882
1001
|
type: "string",
|
|
883
1002
|
description:
|
|
884
|
-
"Repo
|
|
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
|
-
|
|
1026
|
+
repoName: {
|
|
908
1027
|
type: "string",
|
|
909
|
-
description:
|
|
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
|
-
|
|
1057
|
+
repoName: {
|
|
938
1058
|
type: "string",
|
|
939
|
-
description:
|
|
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
|
-
|
|
1135
|
+
repoName: {
|
|
1015
1136
|
type: "string",
|
|
1016
|
-
description:
|
|
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
|
-
|
|
1213
|
+
repoName: {
|
|
1092
1214
|
type: "string",
|
|
1093
|
-
description:
|
|
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
|
-
|
|
1330
|
+
repoName: {
|
|
1208
1331
|
type: "string",
|
|
1209
|
-
description:
|
|
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
|
-
|
|
1372
|
+
repoName: {
|
|
1249
1373
|
type: "string",
|
|
1250
|
-
description:
|
|
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
|
-
|
|
1430
|
+
repoName: {
|
|
1306
1431
|
type: "string",
|
|
1307
|
-
description:
|
|
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
|
-
|
|
1465
|
+
repoName: {
|
|
1340
1466
|
type: "string",
|
|
1341
|
-
description:
|
|
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
|
-
|
|
1487
|
+
repoName: {
|
|
1361
1488
|
type: "string",
|
|
1362
1489
|
description:
|
|
1363
|
-
"Optional repo
|
|
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
|
-
|
|
1555
|
+
repoName: {
|
|
1429
1556
|
type: "string",
|
|
1430
1557
|
description:
|
|
1431
|
-
"Repo
|
|
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.
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
1760
|
+
repoName: {
|
|
1634
1761
|
type: "string",
|
|
1635
1762
|
description:
|
|
1636
|
-
|
|
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
|
-
|
|
1998
|
+
repoCtx: RepoResolutionContext,
|
|
1872
1999
|
): Promise<unknown> => {
|
|
1873
2000
|
const normalized = normalizePayload(payload);
|
|
1874
|
-
const
|
|
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
|
|
2090
|
-
|
|
2091
|
-
const repoRoot =
|
|
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
|
|
2106
|
-
"
|
|
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
|
-
|
|
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
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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: {
|
|
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.
|
|
2550
|
-
details.suggestion =
|
|
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
|
-
"
|
|
2560
|
-
details.suggestion =
|
|
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,
|
|
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);
|