@foundation0/api 1.1.6 → 1.1.8
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 +120 -7
- package/mcp/server.ts +352 -71
- 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 {
|
|
@@ -431,6 +432,118 @@ describe("createExampleMcpServer request handling", () => {
|
|
|
431
432
|
}
|
|
432
433
|
});
|
|
433
434
|
|
|
435
|
+
it("auto-detects repo root/name from .git/config when no repoRoot/repoName are provided", async () => {
|
|
436
|
+
const originalCwd = process.cwd();
|
|
437
|
+
const tempDir = await fs.mkdtemp(
|
|
438
|
+
path.join(os.tmpdir(), "f0-mcp-server-git-detect-"),
|
|
439
|
+
);
|
|
440
|
+
try {
|
|
441
|
+
await fs.mkdir(path.join(tempDir, "api", "mcp"), { recursive: true });
|
|
442
|
+
await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
|
|
443
|
+
recursive: true,
|
|
444
|
+
});
|
|
445
|
+
await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
|
|
446
|
+
await fs.writeFile(
|
|
447
|
+
path.join(tempDir, ".git", "config"),
|
|
448
|
+
[
|
|
449
|
+
'[remote "origin"]',
|
|
450
|
+
"\turl = https://example.com/F0/adl.git",
|
|
451
|
+
"",
|
|
452
|
+
].join("\n"),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
process.chdir(path.join(tempDir, "api", "mcp"));
|
|
456
|
+
const instance = createExampleMcpServer();
|
|
457
|
+
const handler = getToolHandler(instance);
|
|
458
|
+
|
|
459
|
+
const workspace = await handler(
|
|
460
|
+
{
|
|
461
|
+
method: "tools/call",
|
|
462
|
+
params: {
|
|
463
|
+
name: "mcp.workspace",
|
|
464
|
+
arguments: {},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(workspace.isError).toBe(false);
|
|
471
|
+
const payload = JSON.parse(workspace.content?.[0]?.text ?? "{}");
|
|
472
|
+
expect(payload.ok).toBe(true);
|
|
473
|
+
expect(payload.result.defaultRepoRoot).toBe(path.resolve(tempDir));
|
|
474
|
+
expect(payload.result.repoRoot).toBe(path.resolve(tempDir));
|
|
475
|
+
expect(payload.result.defaultRepoName).toBe("adl");
|
|
476
|
+
expect(payload.result.repoName).toBe("adl");
|
|
477
|
+
expect(payload.result.availableRepoNames).toContain("adl");
|
|
478
|
+
|
|
479
|
+
const list = await handler(
|
|
480
|
+
{
|
|
481
|
+
method: "tools/call",
|
|
482
|
+
params: {
|
|
483
|
+
name: "projects.listProjects",
|
|
484
|
+
arguments: {},
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
{},
|
|
488
|
+
);
|
|
489
|
+
expect(list.isError).toBe(false);
|
|
490
|
+
const listPayload = JSON.parse(list.content?.[0]?.text ?? "{}");
|
|
491
|
+
expect(listPayload.ok).toBe(true);
|
|
492
|
+
expect(listPayload.result).toContain("adl");
|
|
493
|
+
} finally {
|
|
494
|
+
process.chdir(originalCwd);
|
|
495
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("supports worktree-style .git file (gitdir: ...) when deriving repoName from config", async () => {
|
|
500
|
+
const originalCwd = process.cwd();
|
|
501
|
+
const tempDir = await fs.mkdtemp(
|
|
502
|
+
path.join(os.tmpdir(), "f0-mcp-server-gitfile-detect-"),
|
|
503
|
+
);
|
|
504
|
+
try {
|
|
505
|
+
await fs.mkdir(path.join(tempDir, "api", "mcp"), { recursive: true });
|
|
506
|
+
await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
|
|
507
|
+
recursive: true,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const gitDir = path.join(tempDir, ".gitdir");
|
|
511
|
+
await fs.mkdir(gitDir, { recursive: true });
|
|
512
|
+
await fs.writeFile(
|
|
513
|
+
path.join(gitDir, "config"),
|
|
514
|
+
[
|
|
515
|
+
'[remote "origin"]',
|
|
516
|
+
"\turl = git@github.com:F0/adl.git",
|
|
517
|
+
"",
|
|
518
|
+
].join("\n"),
|
|
519
|
+
);
|
|
520
|
+
await fs.writeFile(path.join(tempDir, ".git"), "gitdir: .gitdir\n");
|
|
521
|
+
|
|
522
|
+
process.chdir(path.join(tempDir, "api", "mcp"));
|
|
523
|
+
const instance = createExampleMcpServer();
|
|
524
|
+
const handler = getToolHandler(instance);
|
|
525
|
+
|
|
526
|
+
const workspace = await handler(
|
|
527
|
+
{
|
|
528
|
+
method: "tools/call",
|
|
529
|
+
params: {
|
|
530
|
+
name: "mcp.workspace",
|
|
531
|
+
arguments: {},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{},
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(workspace.isError).toBe(false);
|
|
538
|
+
const payload = JSON.parse(workspace.content?.[0]?.text ?? "{}");
|
|
539
|
+
expect(payload.ok).toBe(true);
|
|
540
|
+
expect(payload.result.defaultRepoName).toBe("adl");
|
|
541
|
+
} finally {
|
|
542
|
+
process.chdir(originalCwd);
|
|
543
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
434
547
|
it('parses continueOnError from string "false" (fails fast)', async () => {
|
|
435
548
|
const instance = createExampleMcpServer();
|
|
436
549
|
const handler = getToolHandler(instance);
|
package/mcp/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as netApi from "../net.ts";
|
|
|
9
9
|
import * as projectsApi from "../projects.ts";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
|
|
13
14
|
type ApiMethod = (...args: unknown[]) => unknown;
|
|
14
15
|
type ToolInvocationPayload = {
|
|
@@ -63,9 +64,9 @@ const buildProjectsMcpUsage = (toolsPrefix: string | undefined): string => {
|
|
|
63
64
|
const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
|
|
64
65
|
return [
|
|
65
66
|
"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: {
|
|
67
|
+
`- ${tool("projects.listProjects")}: { options: { repoName: \"<repo-name>\" } }`,
|
|
68
|
+
`- ${tool("projects.generateSpec")}: { args: [\"<projectName>\"], options: { repoName: \"<repo-name>\" } }`,
|
|
69
|
+
`- ${tool("projects.setActive")}: { args: [\"<projectName>\", \"/implementation-plan.v0.0.1\"], options: { repoName: \"<repo-name>\", latest: true } }`,
|
|
69
70
|
"",
|
|
70
71
|
"Discovery:",
|
|
71
72
|
`- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
|
|
@@ -80,9 +81,9 @@ const buildAgentsMcpUsage = (toolsPrefix: string | undefined): string => {
|
|
|
80
81
|
const tool = (name: string) => formatToolNameForUsage(name, toolsPrefix);
|
|
81
82
|
return [
|
|
82
83
|
"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: {
|
|
84
|
+
`- ${tool("agents.listAgents")}: { options: { repoName: \"<repo-name>\" } }`,
|
|
85
|
+
`- ${tool("agents.loadAgent")}: { args: [\"<agentName>\"], options: { repoName: \"<repo-name>\" } }`,
|
|
86
|
+
`- ${tool("agents.setActive")}: { args: [\"<agentName>\", \"/system/boot.v0.0.1\"], options: { repoName: \"<repo-name>\", latest: true } }`,
|
|
86
87
|
"",
|
|
87
88
|
"Discovery:",
|
|
88
89
|
`- ${tool("mcp.listTools")}: list available tools (and their schemas)`,
|
|
@@ -176,6 +177,104 @@ const isDir = (candidate: string): boolean => {
|
|
|
176
177
|
const looksLikeRepoRoot = (candidate: string): boolean =>
|
|
177
178
|
isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
|
|
178
179
|
|
|
180
|
+
const fileExists = (candidate: string): boolean => {
|
|
181
|
+
try {
|
|
182
|
+
fs.statSync(candidate);
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const findGitRepoRoot = (startDir: string): string | null => {
|
|
190
|
+
let current = path.resolve(startDir);
|
|
191
|
+
for (let depth = 0; depth < 32; depth += 1) {
|
|
192
|
+
const dotGit = path.join(current, ".git");
|
|
193
|
+
if (isDir(dotGit) || fileExists(dotGit)) return current;
|
|
194
|
+
const parent = path.dirname(current);
|
|
195
|
+
if (parent === current) return null;
|
|
196
|
+
current = parent;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resolveGitDir = (repoRoot: string): string | null => {
|
|
202
|
+
const dotGit = path.join(repoRoot, ".git");
|
|
203
|
+
if (isDir(dotGit)) return dotGit;
|
|
204
|
+
if (!fileExists(dotGit)) return null;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const content = fs.readFileSync(dotGit, "utf8");
|
|
208
|
+
const match = content.match(/^\s*gitdir:\s*(.+)\s*$/im);
|
|
209
|
+
if (!match) return null;
|
|
210
|
+
const raw = match[1].trim();
|
|
211
|
+
if (!raw) return null;
|
|
212
|
+
const resolved = path.resolve(repoRoot, raw);
|
|
213
|
+
return isDir(resolved) ? resolved : null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const parseRepoNameFromRemoteUrl = (remoteUrl: string): string | null => {
|
|
220
|
+
const trimmed = remoteUrl.trim();
|
|
221
|
+
if (!trimmed) return null;
|
|
222
|
+
|
|
223
|
+
const withoutHash = trimmed.split("#")[0] ?? trimmed;
|
|
224
|
+
const withoutQuery = withoutHash.split("?")[0] ?? withoutHash;
|
|
225
|
+
const withoutGit = withoutQuery.endsWith(".git")
|
|
226
|
+
? withoutQuery.slice(0, -4)
|
|
227
|
+
: withoutQuery;
|
|
228
|
+
|
|
229
|
+
const lastSep = Math.max(withoutGit.lastIndexOf("/"), withoutGit.lastIndexOf(":"));
|
|
230
|
+
const candidate = (lastSep >= 0 ? withoutGit.slice(lastSep + 1) : withoutGit).trim();
|
|
231
|
+
if (!candidate) return null;
|
|
232
|
+
return candidate;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const readGitRemoteUrl = (gitDir: string): string | null => {
|
|
236
|
+
const configPath = path.join(gitDir, "config");
|
|
237
|
+
if (!fileExists(configPath)) return null;
|
|
238
|
+
try {
|
|
239
|
+
const config = fs.readFileSync(configPath, "utf8");
|
|
240
|
+
let currentRemote: string | null = null;
|
|
241
|
+
const remoteUrls = new Map<string, string>();
|
|
242
|
+
|
|
243
|
+
for (const rawLine of config.split(/\r?\n/)) {
|
|
244
|
+
const line = rawLine.trim();
|
|
245
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
246
|
+
|
|
247
|
+
const sectionMatch = line.match(/^\[\s*([^\s\]]+)(?:\s+"([^"]+)")?\s*\]\s*$/);
|
|
248
|
+
if (sectionMatch) {
|
|
249
|
+
const section = sectionMatch[1].toLowerCase();
|
|
250
|
+
const name = sectionMatch[2] ?? null;
|
|
251
|
+
currentRemote = section === "remote" ? name : null;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!currentRemote) continue;
|
|
256
|
+
const kvMatch = line.match(/^([A-Za-z0-9][-A-Za-z0-9]*)\s*=\s*(.*)$/);
|
|
257
|
+
if (!kvMatch) continue;
|
|
258
|
+
const key = kvMatch[1].toLowerCase();
|
|
259
|
+
if (key !== "url") continue;
|
|
260
|
+
const value = kvMatch[2].trim().replace(/^"(.*)"$/, "$1").trim();
|
|
261
|
+
if (value) remoteUrls.set(currentRemote, value);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return remoteUrls.get("origin") ?? remoteUrls.values().next().value ?? null;
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const detectRepoNameFromGitConfig = (repoRoot: string): string | null => {
|
|
271
|
+
const gitDir = resolveGitDir(repoRoot);
|
|
272
|
+
if (!gitDir) return null;
|
|
273
|
+
const remoteUrl = readGitRemoteUrl(gitDir);
|
|
274
|
+
if (!remoteUrl) return null;
|
|
275
|
+
return parseRepoNameFromRemoteUrl(remoteUrl);
|
|
276
|
+
};
|
|
277
|
+
|
|
179
278
|
const normalizeRepoRoot = (raw: string): string => {
|
|
180
279
|
const resolved = path.resolve(raw);
|
|
181
280
|
if (looksLikeRepoRoot(resolved)) return resolved;
|
|
@@ -226,6 +325,125 @@ const normalizeRepoRootOption = (
|
|
|
226
325
|
return alreadyCanonical ? options : next;
|
|
227
326
|
};
|
|
228
327
|
|
|
328
|
+
const looksLikePathish = (value: string): boolean => {
|
|
329
|
+
const trimmed = value.trim();
|
|
330
|
+
if (!trimmed) return false;
|
|
331
|
+
if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
|
|
332
|
+
return trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith(".");
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const parseRepoMap = (raw: string | undefined): Record<string, string> => {
|
|
336
|
+
const input = typeof raw === "string" ? raw.trim() : "";
|
|
337
|
+
if (!input) return {};
|
|
338
|
+
|
|
339
|
+
if (input.startsWith("{")) {
|
|
340
|
+
try {
|
|
341
|
+
const parsed = JSON.parse(input);
|
|
342
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
345
|
+
const out: Record<string, string> = {};
|
|
346
|
+
for (const [name, root] of Object.entries(parsed)) {
|
|
347
|
+
if (!name || typeof root !== "string" || !root.trim()) continue;
|
|
348
|
+
out[name.trim()] = root.trim();
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
} catch {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const entries = input
|
|
357
|
+
.split(/[;,]/g)
|
|
358
|
+
.map((entry) => entry.trim())
|
|
359
|
+
.filter((entry) => entry.length > 0);
|
|
360
|
+
|
|
361
|
+
const out: Record<string, string> = {};
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
const [name, root] = entry.split("=", 2);
|
|
364
|
+
if (!name || !root) continue;
|
|
365
|
+
const key = name.trim();
|
|
366
|
+
const value = root.trim();
|
|
367
|
+
if (!key || !value) continue;
|
|
368
|
+
out[key] = value;
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
type RepoResolutionContext = {
|
|
374
|
+
defaultRepoRoot: string;
|
|
375
|
+
defaultRepoName: string;
|
|
376
|
+
repoMapByKey: Record<string, string>;
|
|
377
|
+
repoNames: string[];
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const resolveRepoSelectorOptions = (
|
|
381
|
+
options: Record<string, unknown>,
|
|
382
|
+
ctx: RepoResolutionContext,
|
|
383
|
+
): Record<string, unknown> => {
|
|
384
|
+
const next: Record<string, unknown> = { ...options };
|
|
385
|
+
|
|
386
|
+
const explicitRoot =
|
|
387
|
+
(typeof next.repoRoot === "string" && next.repoRoot.trim().length > 0
|
|
388
|
+
? next.repoRoot.trim()
|
|
389
|
+
: null) ??
|
|
390
|
+
(typeof next.processRoot === "string" && next.processRoot.trim().length > 0
|
|
391
|
+
? next.processRoot.trim()
|
|
392
|
+
: null);
|
|
393
|
+
|
|
394
|
+
if (explicitRoot) {
|
|
395
|
+
next.repoRoot = normalizeRepoRoot(explicitRoot);
|
|
396
|
+
delete next.processRoot;
|
|
397
|
+
delete next.repoName;
|
|
398
|
+
return next;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const repoName =
|
|
402
|
+
typeof next.repoName === "string" && next.repoName.trim().length > 0
|
|
403
|
+
? next.repoName.trim()
|
|
404
|
+
: null;
|
|
405
|
+
if (!repoName) {
|
|
406
|
+
delete next.processRoot;
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (looksLikePathish(repoName)) {
|
|
411
|
+
next.repoRoot = normalizeRepoRoot(repoName);
|
|
412
|
+
delete next.processRoot;
|
|
413
|
+
delete next.repoName;
|
|
414
|
+
return next;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const needle = repoName.toLowerCase();
|
|
418
|
+
if (needle === ctx.defaultRepoName.toLowerCase()) {
|
|
419
|
+
next.repoRoot = ctx.defaultRepoRoot;
|
|
420
|
+
delete next.processRoot;
|
|
421
|
+
delete next.repoName;
|
|
422
|
+
return next;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const mapped = ctx.repoMapByKey[needle];
|
|
426
|
+
if (mapped) {
|
|
427
|
+
next.repoRoot = normalizeRepoRoot(mapped);
|
|
428
|
+
delete next.processRoot;
|
|
429
|
+
delete next.repoName;
|
|
430
|
+
return next;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const suggestions = ctx.repoNames
|
|
434
|
+
.filter((name) => name.toLowerCase().includes(needle))
|
|
435
|
+
.slice(0, 8);
|
|
436
|
+
const hint =
|
|
437
|
+
suggestions.length > 0
|
|
438
|
+
? ` Did you mean: ${suggestions.join(", ")}?`
|
|
439
|
+
: ctx.repoNames.length > 0
|
|
440
|
+
? ` Available repos: ${ctx.repoNames.join(", ")}.`
|
|
441
|
+
: "";
|
|
442
|
+
throw new Error(
|
|
443
|
+
`Unknown repoName: ${repoName}.${hint} Tip: call mcp.workspace to see the server repo context.`,
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
|
|
229
447
|
type NormalizedToolPayload = {
|
|
230
448
|
args: unknown[];
|
|
231
449
|
options: Record<string, unknown>;
|
|
@@ -451,7 +669,7 @@ const coercePayloadForTool = (
|
|
|
451
669
|
|
|
452
670
|
switch (toolName) {
|
|
453
671
|
case "projects.listProjects": {
|
|
454
|
-
// No positional args. repoRoot is
|
|
672
|
+
// No positional args. repoRoot is resolved from repoName/repoRoot/processRoot and passed via buildRepoRootOnly.
|
|
455
673
|
break;
|
|
456
674
|
}
|
|
457
675
|
case "projects.resolveProjectRoot":
|
|
@@ -852,7 +1070,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
852
1070
|
description: "Unused.",
|
|
853
1071
|
additionalProperties: true,
|
|
854
1072
|
},
|
|
855
|
-
|
|
1073
|
+
repoName: {
|
|
856
1074
|
type: "string",
|
|
857
1075
|
description: "Unused.",
|
|
858
1076
|
},
|
|
@@ -862,10 +1080,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
862
1080
|
type: "object",
|
|
863
1081
|
additionalProperties: true,
|
|
864
1082
|
properties: {
|
|
865
|
-
|
|
1083
|
+
repoName: {
|
|
866
1084
|
type: "string",
|
|
867
1085
|
description:
|
|
868
|
-
|
|
1086
|
+
"Repo selector (LLM-friendly). Omit to use server default. Tip: call mcp.workspace to see available repoName values.",
|
|
869
1087
|
},
|
|
870
1088
|
},
|
|
871
1089
|
required: [],
|
|
@@ -878,10 +1096,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
878
1096
|
type: "string",
|
|
879
1097
|
description: 'Project name under /projects (e.g. "adl").',
|
|
880
1098
|
},
|
|
881
|
-
|
|
1099
|
+
repoName: {
|
|
882
1100
|
type: "string",
|
|
883
1101
|
description:
|
|
884
|
-
"Repo
|
|
1102
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
885
1103
|
},
|
|
886
1104
|
args: {
|
|
887
1105
|
type: "array",
|
|
@@ -904,9 +1122,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
904
1122
|
type: "string",
|
|
905
1123
|
description: 'Project name under /projects (e.g. "adl").',
|
|
906
1124
|
},
|
|
907
|
-
|
|
1125
|
+
repoName: {
|
|
908
1126
|
type: "string",
|
|
909
|
-
description:
|
|
1127
|
+
description:
|
|
1128
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
910
1129
|
},
|
|
911
1130
|
args: {
|
|
912
1131
|
type: "array",
|
|
@@ -934,9 +1153,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
934
1153
|
description:
|
|
935
1154
|
'Doc path under docs/, starting with "docs/..." (or a bare filename in catalog).',
|
|
936
1155
|
},
|
|
937
|
-
|
|
1156
|
+
repoName: {
|
|
938
1157
|
type: "string",
|
|
939
|
-
description:
|
|
1158
|
+
description:
|
|
1159
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
940
1160
|
},
|
|
941
1161
|
args: {
|
|
942
1162
|
type: "array",
|
|
@@ -1011,9 +1231,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1011
1231
|
type: "string",
|
|
1012
1232
|
description: "Remote repo override for gitea mode.",
|
|
1013
1233
|
},
|
|
1014
|
-
|
|
1234
|
+
repoName: {
|
|
1015
1235
|
type: "string",
|
|
1016
|
-
description:
|
|
1236
|
+
description:
|
|
1237
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1017
1238
|
},
|
|
1018
1239
|
args: {
|
|
1019
1240
|
type: "array",
|
|
@@ -1088,9 +1309,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1088
1309
|
type: "string",
|
|
1089
1310
|
description: "Remote repo override for gitea mode.",
|
|
1090
1311
|
},
|
|
1091
|
-
|
|
1312
|
+
repoName: {
|
|
1092
1313
|
type: "string",
|
|
1093
|
-
description:
|
|
1314
|
+
description:
|
|
1315
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1094
1316
|
},
|
|
1095
1317
|
args: {
|
|
1096
1318
|
type: "array",
|
|
@@ -1204,9 +1426,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1204
1426
|
type: "boolean",
|
|
1205
1427
|
description: "If true, only return issues with TASK-* IDs.",
|
|
1206
1428
|
},
|
|
1207
|
-
|
|
1429
|
+
repoName: {
|
|
1208
1430
|
type: "string",
|
|
1209
|
-
description:
|
|
1431
|
+
description:
|
|
1432
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1210
1433
|
},
|
|
1211
1434
|
args: {
|
|
1212
1435
|
type: "array",
|
|
@@ -1245,9 +1468,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1245
1468
|
type: "boolean",
|
|
1246
1469
|
description: "If true, restrict to issues with TASK-* payloads.",
|
|
1247
1470
|
},
|
|
1248
|
-
|
|
1471
|
+
repoName: {
|
|
1249
1472
|
type: "string",
|
|
1250
|
-
description:
|
|
1473
|
+
description:
|
|
1474
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1251
1475
|
},
|
|
1252
1476
|
args: {
|
|
1253
1477
|
type: "array",
|
|
@@ -1302,9 +1526,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1302
1526
|
type: "string",
|
|
1303
1527
|
description: "Optional task signature.",
|
|
1304
1528
|
},
|
|
1305
|
-
|
|
1529
|
+
repoName: {
|
|
1306
1530
|
type: "string",
|
|
1307
|
-
description:
|
|
1531
|
+
description:
|
|
1532
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1308
1533
|
},
|
|
1309
1534
|
args: {
|
|
1310
1535
|
type: "array",
|
|
@@ -1336,9 +1561,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1336
1561
|
items: { type: "string" },
|
|
1337
1562
|
description: "Labels to set.",
|
|
1338
1563
|
},
|
|
1339
|
-
|
|
1564
|
+
repoName: {
|
|
1340
1565
|
type: "string",
|
|
1341
|
-
description:
|
|
1566
|
+
description:
|
|
1567
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1342
1568
|
},
|
|
1343
1569
|
args: {
|
|
1344
1570
|
type: "array",
|
|
@@ -1357,10 +1583,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1357
1583
|
type: "object",
|
|
1358
1584
|
additionalProperties: true,
|
|
1359
1585
|
properties: {
|
|
1360
|
-
|
|
1586
|
+
repoName: {
|
|
1361
1587
|
type: "string",
|
|
1362
1588
|
description:
|
|
1363
|
-
"Optional repo
|
|
1589
|
+
"Optional repo selector (LLM-friendly). Omit to use server default. You can also pass a server filesystem path via legacy repoRoot/processRoot.",
|
|
1364
1590
|
},
|
|
1365
1591
|
},
|
|
1366
1592
|
required: [],
|
|
@@ -1425,10 +1651,10 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1425
1651
|
type: "string",
|
|
1426
1652
|
description: "Optional override cache directory.",
|
|
1427
1653
|
},
|
|
1428
|
-
|
|
1654
|
+
repoName: {
|
|
1429
1655
|
type: "string",
|
|
1430
1656
|
description:
|
|
1431
|
-
"Repo
|
|
1657
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1432
1658
|
},
|
|
1433
1659
|
},
|
|
1434
1660
|
$comment: safeJsonStringify({
|
|
@@ -1460,7 +1686,7 @@ const buildArgsSchemaFromPlaceholders = (
|
|
|
1460
1686
|
const TOOL_ARGS_SCHEMA_OVERRIDES: Record<string, Record<string, unknown>> = {
|
|
1461
1687
|
"projects.listProjects": {
|
|
1462
1688
|
type: "array",
|
|
1463
|
-
description: "No positional arguments. Use options.
|
|
1689
|
+
description: "No positional arguments. Use options.repoName if needed.",
|
|
1464
1690
|
minItems: 0,
|
|
1465
1691
|
maxItems: 0,
|
|
1466
1692
|
items: {},
|
|
@@ -1594,12 +1820,12 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
|
|
|
1594
1820
|
}
|
|
1595
1821
|
|
|
1596
1822
|
if (plan === "repoRootOnly") {
|
|
1597
|
-
example.options = {
|
|
1823
|
+
example.options = { repoName: "<repo-name>", ...defaultOptions };
|
|
1598
1824
|
return example;
|
|
1599
1825
|
}
|
|
1600
1826
|
|
|
1601
1827
|
if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
|
|
1602
|
-
example.options = {
|
|
1828
|
+
example.options = { repoName: "<repo-name>", ...defaultOptions };
|
|
1603
1829
|
return example;
|
|
1604
1830
|
}
|
|
1605
1831
|
|
|
@@ -1630,10 +1856,10 @@ const defaultToolInputSchema = (toolName: string) => ({
|
|
|
1630
1856
|
additionalProperties: true,
|
|
1631
1857
|
description: "Named options",
|
|
1632
1858
|
},
|
|
1633
|
-
|
|
1859
|
+
repoName: {
|
|
1634
1860
|
type: "string",
|
|
1635
1861
|
description:
|
|
1636
|
-
|
|
1862
|
+
"Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
|
|
1637
1863
|
},
|
|
1638
1864
|
},
|
|
1639
1865
|
$comment: safeJsonStringify({
|
|
@@ -1868,10 +2094,14 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
|
|
|
1868
2094
|
const invokeTool = async (
|
|
1869
2095
|
tool: ToolDefinition,
|
|
1870
2096
|
payload: unknown,
|
|
1871
|
-
|
|
2097
|
+
repoCtx: RepoResolutionContext,
|
|
1872
2098
|
): Promise<unknown> => {
|
|
1873
2099
|
const normalized = normalizePayload(payload);
|
|
1874
|
-
const
|
|
2100
|
+
const coerced = coercePayloadForTool(tool.name, normalized);
|
|
2101
|
+
const { args, options } = {
|
|
2102
|
+
args: coerced.args,
|
|
2103
|
+
options: resolveRepoSelectorOptions(coerced.options, repoCtx),
|
|
2104
|
+
};
|
|
1875
2105
|
const invoke =
|
|
1876
2106
|
toolInvocationPlans[tool.name] ??
|
|
1877
2107
|
((rawArgs, rawOptions, _repoRoot) => {
|
|
@@ -1881,7 +2111,7 @@ const invokeTool = async (
|
|
|
1881
2111
|
}
|
|
1882
2112
|
return invocationArgs;
|
|
1883
2113
|
});
|
|
1884
|
-
const invocationArgs = invoke(args, options, defaultRepoRoot);
|
|
2114
|
+
const invocationArgs = invoke(args, options, repoCtx.defaultRepoRoot);
|
|
1885
2115
|
|
|
1886
2116
|
return Promise.resolve(tool.method(...invocationArgs));
|
|
1887
2117
|
};
|
|
@@ -1895,6 +2125,11 @@ export interface ExampleMcpServerOptions {
|
|
|
1895
2125
|
* If omitted, the server attempts to auto-detect by walking up from cwd.
|
|
1896
2126
|
*/
|
|
1897
2127
|
repoRoot?: string;
|
|
2128
|
+
/**
|
|
2129
|
+
* Optional default repo name (LLM-friendly identifier).
|
|
2130
|
+
* If omitted, defaults to the basename of the resolved repo root.
|
|
2131
|
+
*/
|
|
2132
|
+
repoName?: string;
|
|
1898
2133
|
allowedRootEndpoints?: string[];
|
|
1899
2134
|
disableWrite?: boolean;
|
|
1900
2135
|
enableIssues?: boolean;
|
|
@@ -2026,12 +2261,50 @@ export const createExampleMcpServer = (
|
|
|
2026
2261
|
options: ExampleMcpServerOptions = {},
|
|
2027
2262
|
): ExampleMcpServerInstance => {
|
|
2028
2263
|
let toolCatalog: unknown[] = [];
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
);
|
|
2264
|
+
|
|
2265
|
+
const configuredRepoRoot =
|
|
2266
|
+
options.repoRoot ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT;
|
|
2267
|
+
const cwd = process.cwd();
|
|
2268
|
+
const cwdNormalized = normalizeRepoRoot(cwd);
|
|
2269
|
+
const cwdLooksLikeRepoRoot = looksLikeRepoRoot(cwdNormalized);
|
|
2270
|
+
const cwdGitRoot = findGitRepoRoot(cwd);
|
|
2271
|
+
const serverFileDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2272
|
+
const serverFileGitRoot = findGitRepoRoot(serverFileDir);
|
|
2273
|
+
|
|
2274
|
+
const rawDefaultRepoRoot =
|
|
2275
|
+
configuredRepoRoot ??
|
|
2276
|
+
(cwdLooksLikeRepoRoot ? cwdNormalized : null) ??
|
|
2277
|
+
cwdGitRoot ??
|
|
2278
|
+
serverFileGitRoot ??
|
|
2279
|
+
cwd ??
|
|
2280
|
+
serverFileDir;
|
|
2281
|
+
const defaultRepoRoot = normalizeRepoRoot(rawDefaultRepoRoot);
|
|
2282
|
+
|
|
2283
|
+
const configuredRepoName =
|
|
2284
|
+
(options.repoName ?? process.env.MCP_REPO_NAME ?? process.env.F0_REPO_NAME)?.trim() ||
|
|
2285
|
+
null;
|
|
2286
|
+
const gitDerivedRepoName =
|
|
2287
|
+
detectRepoNameFromGitConfig(defaultRepoRoot) ??
|
|
2288
|
+
(cwdGitRoot ? detectRepoNameFromGitConfig(cwdGitRoot) : null) ??
|
|
2289
|
+
(serverFileGitRoot ? detectRepoNameFromGitConfig(serverFileGitRoot) : null);
|
|
2290
|
+
const defaultRepoName =
|
|
2291
|
+
configuredRepoName || gitDerivedRepoName || path.basename(defaultRepoRoot);
|
|
2292
|
+
const repoMapRaw = process.env.MCP_REPOS ?? process.env.F0_REPOS;
|
|
2293
|
+
const repoMap = {
|
|
2294
|
+
...parseRepoMap(repoMapRaw),
|
|
2295
|
+
[defaultRepoName]: defaultRepoRoot,
|
|
2296
|
+
};
|
|
2297
|
+
const repoNames = Object.keys(repoMap).sort((a, b) => a.localeCompare(b));
|
|
2298
|
+
const repoMapByKey: Record<string, string> = {};
|
|
2299
|
+
for (const [name, root] of Object.entries(repoMap)) {
|
|
2300
|
+
repoMapByKey[name.toLowerCase()] = root;
|
|
2301
|
+
}
|
|
2302
|
+
const repoCtx: RepoResolutionContext = {
|
|
2303
|
+
defaultRepoRoot,
|
|
2304
|
+
defaultRepoName,
|
|
2305
|
+
repoMapByKey,
|
|
2306
|
+
repoNames,
|
|
2307
|
+
};
|
|
2035
2308
|
|
|
2036
2309
|
const parseString = (value: unknown): string | null => {
|
|
2037
2310
|
if (typeof value !== "string") return null;
|
|
@@ -2071,7 +2344,7 @@ export const createExampleMcpServer = (
|
|
|
2071
2344
|
"F0 MCP helper tools:",
|
|
2072
2345
|
"- mcp.listTools: returns tool catalog with access + invocation hints",
|
|
2073
2346
|
"- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
|
|
2074
|
-
"- mcp.workspace: explain server filesystem context (cwd, repoRoot, projects)",
|
|
2347
|
+
"- mcp.workspace: explain server filesystem context (cwd, repoName/repoRoot, projects)",
|
|
2075
2348
|
"- mcp.search: LLM-friendly search over project docs/spec (local-first)",
|
|
2076
2349
|
"",
|
|
2077
2350
|
'Tip: Prefer mcp.search for "search spec/docs" requests.',
|
|
@@ -2082,13 +2355,21 @@ export const createExampleMcpServer = (
|
|
|
2082
2355
|
const received = isRecord(input)
|
|
2083
2356
|
? {
|
|
2084
2357
|
keys: Object.keys(input),
|
|
2358
|
+
repoName: (input as any).repoName ?? null,
|
|
2085
2359
|
repoRoot: (input as any).repoRoot ?? null,
|
|
2086
2360
|
processRoot: (input as any).processRoot ?? null,
|
|
2087
2361
|
}
|
|
2088
|
-
: { keys: [], repoRoot: null, processRoot: null };
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
const repoRoot =
|
|
2362
|
+
: { keys: [], repoName: null, repoRoot: null, processRoot: null };
|
|
2363
|
+
const requestedRepoName = parseString(payload.repoName);
|
|
2364
|
+
const resolved = resolveRepoSelectorOptions(payload, repoCtx);
|
|
2365
|
+
const repoRoot =
|
|
2366
|
+
typeof resolved.repoRoot === "string"
|
|
2367
|
+
? resolved.repoRoot
|
|
2368
|
+
: repoCtx.defaultRepoRoot;
|
|
2369
|
+
const effectiveRepoName =
|
|
2370
|
+
requestedRepoName && !looksLikePathish(requestedRepoName)
|
|
2371
|
+
? requestedRepoName
|
|
2372
|
+
: repoCtx.defaultRepoName;
|
|
2092
2373
|
|
|
2093
2374
|
const projectsDir = path.join(repoRoot, "projects");
|
|
2094
2375
|
const apiDir = path.join(repoRoot, "api");
|
|
@@ -2102,8 +2383,8 @@ export const createExampleMcpServer = (
|
|
|
2102
2383
|
if (!hasProjectsDir) {
|
|
2103
2384
|
return [
|
|
2104
2385
|
"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
|
-
"
|
|
2386
|
+
"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.",
|
|
2387
|
+
"Tool callers should usually omit repoName and let the server use its default workspace.",
|
|
2107
2388
|
].join(" ");
|
|
2108
2389
|
}
|
|
2109
2390
|
if (hasProjectsDir && projects.length === 0) {
|
|
@@ -2118,9 +2399,11 @@ export const createExampleMcpServer = (
|
|
|
2118
2399
|
return {
|
|
2119
2400
|
received,
|
|
2120
2401
|
cwd: process.cwd(),
|
|
2121
|
-
|
|
2122
|
-
defaultRepoRoot,
|
|
2402
|
+
defaultRepoName: repoCtx.defaultRepoName,
|
|
2403
|
+
defaultRepoRoot: repoCtx.defaultRepoRoot,
|
|
2404
|
+
repoName: effectiveRepoName,
|
|
2123
2405
|
repoRoot,
|
|
2406
|
+
availableRepoNames: repoCtx.repoNames,
|
|
2124
2407
|
hasProjectsDir,
|
|
2125
2408
|
hasApiDir,
|
|
2126
2409
|
hasAgentsDir,
|
|
@@ -2168,11 +2451,11 @@ export const createExampleMcpServer = (
|
|
|
2168
2451
|
},
|
|
2169
2452
|
search: async (input: unknown) => {
|
|
2170
2453
|
const payload = isRecord(input) ? input : {};
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2454
|
+
const resolved = resolveRepoSelectorOptions(payload, repoCtx);
|
|
2455
|
+
const repoRoot =
|
|
2456
|
+
typeof resolved.repoRoot === "string"
|
|
2457
|
+
? resolved.repoRoot
|
|
2458
|
+
: repoCtx.defaultRepoRoot;
|
|
2176
2459
|
|
|
2177
2460
|
const sectionRaw = parseString(payload.section)?.toLowerCase();
|
|
2178
2461
|
const section = sectionRaw === "docs" ? "docs" : "spec";
|
|
@@ -2367,7 +2650,7 @@ export const createExampleMcpServer = (
|
|
|
2367
2650
|
tool: prefix
|
|
2368
2651
|
? `${prefix}.projects.listProjects`
|
|
2369
2652
|
: "projects.listProjects",
|
|
2370
|
-
options: {
|
|
2653
|
+
options: { repoName: "<repo-name>" },
|
|
2371
2654
|
},
|
|
2372
2655
|
],
|
|
2373
2656
|
continueOnError: true,
|
|
@@ -2546,8 +2829,9 @@ export const createExampleMcpServer = (
|
|
|
2546
2829
|
/projects[\\/].+projects[\\/]/i.test(message)
|
|
2547
2830
|
) {
|
|
2548
2831
|
details.hint =
|
|
2549
|
-
"You likely passed a project root as repoRoot.
|
|
2550
|
-
details.suggestion =
|
|
2832
|
+
"You likely passed a project root as repoName (or legacy repoRoot/processRoot). Repo selection should point at the monorepo root that contains /projects.";
|
|
2833
|
+
details.suggestion =
|
|
2834
|
+
"Call mcp.workspace to see the server’s cwd/default repoRoot, then omit repoName or pass the correct repoName.";
|
|
2551
2835
|
}
|
|
2552
2836
|
|
|
2553
2837
|
if (
|
|
@@ -2556,8 +2840,9 @@ export const createExampleMcpServer = (
|
|
|
2556
2840
|
/projects[\\/]/i.test(message)
|
|
2557
2841
|
) {
|
|
2558
2842
|
details.hint =
|
|
2559
|
-
"
|
|
2560
|
-
details.suggestion =
|
|
2843
|
+
"Repo selection might be wrong, or the server filesystem does not contain /projects for this workspace.";
|
|
2844
|
+
details.suggestion =
|
|
2845
|
+
"Call mcp.workspace to see the server’s cwd/default repoRoot and available repoName values.";
|
|
2561
2846
|
details.example = {
|
|
2562
2847
|
tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
|
|
2563
2848
|
arguments: {},
|
|
@@ -2625,11 +2910,7 @@ export const createExampleMcpServer = (
|
|
|
2625
2910
|
}
|
|
2626
2911
|
|
|
2627
2912
|
try {
|
|
2628
|
-
const data = await invokeTool(
|
|
2629
|
-
toolDefinition,
|
|
2630
|
-
{ args, options },
|
|
2631
|
-
defaultRepoRoot,
|
|
2632
|
-
);
|
|
2913
|
+
const data = await invokeTool(toolDefinition, { args, options }, repoCtx);
|
|
2633
2914
|
return { index, tool, isError: false, data };
|
|
2634
2915
|
} catch (error) {
|
|
2635
2916
|
const message =
|
|
@@ -2681,7 +2962,7 @@ export const createExampleMcpServer = (
|
|
|
2681
2962
|
}
|
|
2682
2963
|
|
|
2683
2964
|
try {
|
|
2684
|
-
const data = await invokeTool(tool, request.params.arguments,
|
|
2965
|
+
const data = await invokeTool(tool, request.params.arguments, repoCtx);
|
|
2685
2966
|
return toolOk(data);
|
|
2686
2967
|
} catch (error) {
|
|
2687
2968
|
const message = error instanceof Error ? error.message : String(error);
|