@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 +10 -0
- package/mcp/manual.md +22 -11
- package/mcp/server.test.ts +81 -6
- package/mcp/server.ts +326 -72
- package/package.json +2 -1
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`, `
|
|
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: 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
|
|
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
|
-
"
|
|
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": { "
|
|
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>", "
|
|
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": { "
|
|
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": { "
|
|
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": { "
|
|
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": { "
|
|
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/...**:
|
|
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
|
|
package/mcp/server.test.ts
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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)`,
|
|
@@ -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
|
|
224
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
984
|
+
repoName: {
|
|
875
985
|
type: "string",
|
|
876
986
|
description:
|
|
877
|
-
|
|
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
|
-
|
|
1000
|
+
repoName: {
|
|
891
1001
|
type: "string",
|
|
892
1002
|
description:
|
|
893
|
-
"Repo
|
|
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
|
-
|
|
1026
|
+
repoName: {
|
|
917
1027
|
type: "string",
|
|
918
|
-
description:
|
|
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
|
-
|
|
1057
|
+
repoName: {
|
|
947
1058
|
type: "string",
|
|
948
|
-
description:
|
|
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
|
-
|
|
1135
|
+
repoName: {
|
|
1024
1136
|
type: "string",
|
|
1025
|
-
description:
|
|
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
|
-
|
|
1213
|
+
repoName: {
|
|
1101
1214
|
type: "string",
|
|
1102
|
-
description:
|
|
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
|
-
|
|
1330
|
+
repoName: {
|
|
1217
1331
|
type: "string",
|
|
1218
|
-
description:
|
|
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
|
-
|
|
1372
|
+
repoName: {
|
|
1258
1373
|
type: "string",
|
|
1259
|
-
description:
|
|
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
|
-
|
|
1430
|
+
repoName: {
|
|
1315
1431
|
type: "string",
|
|
1316
|
-
description:
|
|
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
|
-
|
|
1465
|
+
repoName: {
|
|
1349
1466
|
type: "string",
|
|
1350
|
-
description:
|
|
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
|
-
|
|
1555
|
+
repoName: {
|
|
1423
1556
|
type: "string",
|
|
1424
1557
|
description:
|
|
1425
|
-
"Repo
|
|
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.
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
1760
|
+
repoName: {
|
|
1628
1761
|
type: "string",
|
|
1629
1762
|
description:
|
|
1630
|
-
|
|
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 (
|
|
1910
|
+
} else if (resolvedRepoRoot) {
|
|
1773
1911
|
// Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
|
|
1774
1912
|
invocationArgs.push({});
|
|
1775
1913
|
}
|
|
1776
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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: {
|
|
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.
|
|
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.
|
|
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\"",
|