@foundation0/api 1.1.5 → 1.1.6
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 +6 -0
- package/mcp/manual.md +12 -1
- package/mcp/server.test.ts +74 -0
- package/mcp/server.ts +123 -32
- package/package.json +2 -1
package/mcp/cli.ts
CHANGED
|
@@ -67,6 +67,8 @@ 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 repoRoot =
|
|
71
|
+
getArgValue('--repo-root') ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT
|
|
70
72
|
const allowedRootEndpoints = parseListArg(
|
|
71
73
|
getArgValue('--allowed-root-endpoints') ?? process.env.MCP_ALLOWED_ROOT_ENDPOINTS,
|
|
72
74
|
)
|
|
@@ -80,6 +82,9 @@ if (hasFlag('--help') || hasFlag('-h')) {
|
|
|
80
82
|
console.log(' --server-name <name>')
|
|
81
83
|
console.log(' --server-version <version>')
|
|
82
84
|
console.log(' --tools-prefix <prefix>')
|
|
85
|
+
console.log(' --repo-root <path>')
|
|
86
|
+
console.log(' Default repo root on the server filesystem (contains /api and /projects).')
|
|
87
|
+
console.log(' Env: MCP_REPO_ROOT or F0_REPO_ROOT.')
|
|
83
88
|
console.log(' --allowed-root-endpoints <csv>')
|
|
84
89
|
console.log(' Example: --allowed-root-endpoints projects')
|
|
85
90
|
console.log(' Example: --allowed-root-endpoints agents,projects')
|
|
@@ -96,6 +101,7 @@ void runExampleMcpServer({
|
|
|
96
101
|
serverName: serverName ?? undefined,
|
|
97
102
|
serverVersion: serverVersion ?? undefined,
|
|
98
103
|
toolsPrefix,
|
|
104
|
+
repoRoot: repoRoot ?? undefined,
|
|
99
105
|
allowedRootEndpoints,
|
|
100
106
|
disableWrite,
|
|
101
107
|
enableIssues,
|
package/mcp/manual.md
CHANGED
|
@@ -11,6 +11,8 @@ Assume the server is already configured in your MCP host and you can call its to
|
|
|
11
11
|
4. When a tool needs `repoRoot`, it must be the **repo root** containing both `api/` and `projects/` (not a single project folder). (Legacy alias: `processRoot` is still accepted.) If omitted, the server will try to auto-detect a repo root from its current working directory (best effort).
|
|
12
12
|
5. Tool results are returned as a **JSON text envelope**: parse the text as JSON and check `ok`.
|
|
13
13
|
|
|
14
|
+
Note: `repoRoot` is a path on the **server's filesystem** (not the client/LLM). If you're unsure what the server can see, call `mcp.workspace`.
|
|
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
|
|
|
@@ -60,6 +62,14 @@ Tool call:
|
|
|
60
62
|
}
|
|
61
63
|
```
|
|
62
64
|
|
|
65
|
+
### D) `mcp.workspace` to debug 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:
|
|
@@ -138,6 +148,7 @@ If you get:
|
|
|
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
150
|
- **Project folder not found: ...projects/.../projects/...**: you likely passed the wrong `repoRoot` (it must be the repo root; legacy alias `processRoot`).
|
|
151
|
+
- **`projects.listProjects()` returns `[]` unexpectedly**: call `mcp.workspace` to confirm the server’s `cwd`/`repoRoot` and whether `/projects` exists.
|
|
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
|
|
|
@@ -357,6 +359,78 @@ describe("createExampleMcpServer request handling", () => {
|
|
|
357
359
|
}
|
|
358
360
|
});
|
|
359
361
|
|
|
362
|
+
it("uses server default repoRoot when repoRoot 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.hasProjectsDir).toBe(true);
|
|
428
|
+
expect(Array.isArray(payload.result.projects)).toBe(true);
|
|
429
|
+
} finally {
|
|
430
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
360
434
|
it('parses continueOnError from string "false" (fails fast)', async () => {
|
|
361
435
|
const instance = createExampleMcpServer();
|
|
362
436
|
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}`),
|
|
@@ -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();
|
|
@@ -890,7 +881,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
890
881
|
repoRoot: {
|
|
891
882
|
type: "string",
|
|
892
883
|
description:
|
|
893
|
-
"Repo root
|
|
884
|
+
"Repo root on the server filesystem (contains /projects and /agents). If omitted, uses the server default (auto-detected from cwd).",
|
|
894
885
|
},
|
|
895
886
|
args: {
|
|
896
887
|
type: "array",
|
|
@@ -1362,6 +1353,21 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
|
|
|
1362
1353
|
},
|
|
1363
1354
|
required: ["projectName", "title"],
|
|
1364
1355
|
},
|
|
1356
|
+
"mcp.workspace": {
|
|
1357
|
+
type: "object",
|
|
1358
|
+
additionalProperties: true,
|
|
1359
|
+
properties: {
|
|
1360
|
+
repoRoot: {
|
|
1361
|
+
type: "string",
|
|
1362
|
+
description:
|
|
1363
|
+
"Optional repo root on the server filesystem. If omitted, uses the server default (auto-detected by walking up from cwd). Legacy alias: processRoot.",
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
required: [],
|
|
1367
|
+
$comment: safeJsonStringify({
|
|
1368
|
+
example: {},
|
|
1369
|
+
}),
|
|
1370
|
+
},
|
|
1365
1371
|
"mcp.search": {
|
|
1366
1372
|
type: "object",
|
|
1367
1373
|
additionalProperties: true,
|
|
@@ -1627,7 +1633,7 @@ const defaultToolInputSchema = (toolName: string) => ({
|
|
|
1627
1633
|
repoRoot: {
|
|
1628
1634
|
type: "string",
|
|
1629
1635
|
description:
|
|
1630
|
-
'Repo root
|
|
1636
|
+
'Repo root on the server filesystem (contains /projects and /agents). If omitted, uses the server default (auto-detected from cwd). If you have a project root like ".../projects/adl", omit this or pass its parent.',
|
|
1631
1637
|
},
|
|
1632
1638
|
},
|
|
1633
1639
|
$comment: safeJsonStringify({
|
|
@@ -1743,11 +1749,13 @@ const buildToolList = (
|
|
|
1743
1749
|
type ToolInvoker = (
|
|
1744
1750
|
args: unknown[],
|
|
1745
1751
|
options: Record<string, unknown>,
|
|
1752
|
+
defaultRepoRoot: string,
|
|
1746
1753
|
) => unknown[];
|
|
1747
1754
|
|
|
1748
1755
|
const buildOptionsOnly = (
|
|
1749
1756
|
args: unknown[],
|
|
1750
1757
|
options: Record<string, unknown>,
|
|
1758
|
+
_defaultRepoRoot: string,
|
|
1751
1759
|
): unknown[] => {
|
|
1752
1760
|
const invocationArgs: unknown[] = [...args];
|
|
1753
1761
|
if (Object.keys(options).length > 0) {
|
|
@@ -1759,6 +1767,7 @@ const buildOptionsOnly = (
|
|
|
1759
1767
|
const buildOptionsThenRepoRoot = (
|
|
1760
1768
|
args: unknown[],
|
|
1761
1769
|
options: Record<string, unknown>,
|
|
1770
|
+
defaultRepoRoot: string,
|
|
1762
1771
|
): unknown[] => {
|
|
1763
1772
|
const invocationArgs: unknown[] = [...args];
|
|
1764
1773
|
const remaining = { ...options };
|
|
@@ -1766,16 +1775,16 @@ const buildOptionsThenRepoRoot = (
|
|
|
1766
1775
|
if (typeof repoRoot === "string") {
|
|
1767
1776
|
delete remaining.repoRoot;
|
|
1768
1777
|
}
|
|
1778
|
+
const resolvedRepoRoot =
|
|
1779
|
+
typeof repoRoot === "string" ? repoRoot : defaultRepoRoot;
|
|
1769
1780
|
|
|
1770
1781
|
if (Object.keys(remaining).length > 0) {
|
|
1771
1782
|
invocationArgs.push(remaining);
|
|
1772
|
-
} else if (
|
|
1783
|
+
} else if (resolvedRepoRoot) {
|
|
1773
1784
|
// Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
|
|
1774
1785
|
invocationArgs.push({});
|
|
1775
1786
|
}
|
|
1776
|
-
|
|
1777
|
-
invocationArgs.push(repoRoot);
|
|
1778
|
-
}
|
|
1787
|
+
invocationArgs.push(resolvedRepoRoot);
|
|
1779
1788
|
|
|
1780
1789
|
return invocationArgs;
|
|
1781
1790
|
};
|
|
@@ -1783,6 +1792,7 @@ const buildOptionsThenRepoRoot = (
|
|
|
1783
1792
|
const buildRepoRootThenOptions = (
|
|
1784
1793
|
args: unknown[],
|
|
1785
1794
|
options: Record<string, unknown>,
|
|
1795
|
+
defaultRepoRoot: string,
|
|
1786
1796
|
): unknown[] => {
|
|
1787
1797
|
const invocationArgs: unknown[] = [...args];
|
|
1788
1798
|
const remaining = { ...options };
|
|
@@ -1791,9 +1801,7 @@ const buildRepoRootThenOptions = (
|
|
|
1791
1801
|
delete remaining.repoRoot;
|
|
1792
1802
|
}
|
|
1793
1803
|
|
|
1794
|
-
|
|
1795
|
-
invocationArgs.push(repoRoot);
|
|
1796
|
-
}
|
|
1804
|
+
invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
|
|
1797
1805
|
if (Object.keys(remaining).length > 0) {
|
|
1798
1806
|
invocationArgs.push(remaining);
|
|
1799
1807
|
}
|
|
@@ -1804,12 +1812,11 @@ const buildRepoRootThenOptions = (
|
|
|
1804
1812
|
const buildRepoRootOnly = (
|
|
1805
1813
|
args: unknown[],
|
|
1806
1814
|
options: Record<string, unknown>,
|
|
1815
|
+
defaultRepoRoot: string,
|
|
1807
1816
|
): unknown[] => {
|
|
1808
1817
|
const invocationArgs: unknown[] = [...args];
|
|
1809
1818
|
const repoRoot = options.repoRoot;
|
|
1810
|
-
|
|
1811
|
-
invocationArgs.push(repoRoot);
|
|
1812
|
-
}
|
|
1819
|
+
invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
|
|
1813
1820
|
return invocationArgs;
|
|
1814
1821
|
};
|
|
1815
1822
|
|
|
@@ -1828,7 +1835,7 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
|
|
|
1828
1835
|
"projects.resolveProjectTargetFile": buildOptionsOnly,
|
|
1829
1836
|
"agents.loadAgent": buildRepoRootOnly,
|
|
1830
1837
|
"agents.loadAgentPrompt": buildRepoRootOnly,
|
|
1831
|
-
"projects.resolveImplementationPlan": (args, options) => {
|
|
1838
|
+
"projects.resolveImplementationPlan": (args, options, _defaultRepoRoot) => {
|
|
1832
1839
|
const invocationArgs: unknown[] = [...args];
|
|
1833
1840
|
const remaining = { ...options };
|
|
1834
1841
|
const repoRoot = remaining.repoRoot;
|
|
@@ -1861,19 +1868,20 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
|
|
|
1861
1868
|
const invokeTool = async (
|
|
1862
1869
|
tool: ToolDefinition,
|
|
1863
1870
|
payload: unknown,
|
|
1871
|
+
defaultRepoRoot: string,
|
|
1864
1872
|
): Promise<unknown> => {
|
|
1865
1873
|
const normalized = normalizePayload(payload);
|
|
1866
1874
|
const { args, options } = coercePayloadForTool(tool.name, normalized);
|
|
1867
1875
|
const invoke =
|
|
1868
1876
|
toolInvocationPlans[tool.name] ??
|
|
1869
|
-
((rawArgs, rawOptions) => {
|
|
1877
|
+
((rawArgs, rawOptions, _repoRoot) => {
|
|
1870
1878
|
const invocationArgs = [...rawArgs];
|
|
1871
1879
|
if (Object.keys(rawOptions).length > 0) {
|
|
1872
1880
|
invocationArgs.push(rawOptions);
|
|
1873
1881
|
}
|
|
1874
1882
|
return invocationArgs;
|
|
1875
1883
|
});
|
|
1876
|
-
const invocationArgs = invoke(args, options);
|
|
1884
|
+
const invocationArgs = invoke(args, options, defaultRepoRoot);
|
|
1877
1885
|
|
|
1878
1886
|
return Promise.resolve(tool.method(...invocationArgs));
|
|
1879
1887
|
};
|
|
@@ -1882,6 +1890,11 @@ export interface ExampleMcpServerOptions {
|
|
|
1882
1890
|
serverName?: string;
|
|
1883
1891
|
serverVersion?: string;
|
|
1884
1892
|
toolsPrefix?: string;
|
|
1893
|
+
/**
|
|
1894
|
+
* Optional default repo root on the server filesystem.
|
|
1895
|
+
* If omitted, the server attempts to auto-detect by walking up from cwd.
|
|
1896
|
+
*/
|
|
1897
|
+
repoRoot?: string;
|
|
1885
1898
|
allowedRootEndpoints?: string[];
|
|
1886
1899
|
disableWrite?: boolean;
|
|
1887
1900
|
enableIssues?: boolean;
|
|
@@ -1922,6 +1935,7 @@ const READ_ONLY_TOOL_NAMES = new Set<string>([
|
|
|
1922
1935
|
"mcp.usage",
|
|
1923
1936
|
"mcp.listTools",
|
|
1924
1937
|
"mcp.describeTool",
|
|
1938
|
+
"mcp.workspace",
|
|
1925
1939
|
"mcp.search",
|
|
1926
1940
|
]);
|
|
1927
1941
|
|
|
@@ -2012,6 +2026,12 @@ export const createExampleMcpServer = (
|
|
|
2012
2026
|
options: ExampleMcpServerOptions = {},
|
|
2013
2027
|
): ExampleMcpServerInstance => {
|
|
2014
2028
|
let toolCatalog: unknown[] = [];
|
|
2029
|
+
const defaultRepoRoot = normalizeRepoRoot(
|
|
2030
|
+
options.repoRoot ??
|
|
2031
|
+
process.env.MCP_REPO_ROOT ??
|
|
2032
|
+
process.env.F0_REPO_ROOT ??
|
|
2033
|
+
process.cwd(),
|
|
2034
|
+
);
|
|
2015
2035
|
|
|
2016
2036
|
const parseString = (value: unknown): string | null => {
|
|
2017
2037
|
if (typeof value !== "string") return null;
|
|
@@ -2051,11 +2071,63 @@ export const createExampleMcpServer = (
|
|
|
2051
2071
|
"F0 MCP helper tools:",
|
|
2052
2072
|
"- mcp.listTools: returns tool catalog with access + invocation hints",
|
|
2053
2073
|
"- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
|
|
2074
|
+
"- mcp.workspace: explain server filesystem context (cwd, repoRoot, projects)",
|
|
2054
2075
|
"- mcp.search: LLM-friendly search over project docs/spec (local-first)",
|
|
2055
2076
|
"",
|
|
2056
2077
|
'Tip: Prefer mcp.search for "search spec/docs" requests.',
|
|
2057
2078
|
].join("\n"),
|
|
2058
2079
|
listTools: () => ({ tools: toolCatalog }),
|
|
2080
|
+
workspace: (input?: unknown) => {
|
|
2081
|
+
const payload = isRecord(input) ? input : {};
|
|
2082
|
+
const received = isRecord(input)
|
|
2083
|
+
? {
|
|
2084
|
+
keys: Object.keys(input),
|
|
2085
|
+
repoRoot: (input as any).repoRoot ?? null,
|
|
2086
|
+
processRoot: (input as any).processRoot ?? null,
|
|
2087
|
+
}
|
|
2088
|
+
: { keys: [], repoRoot: null, processRoot: null };
|
|
2089
|
+
const raw =
|
|
2090
|
+
parseString(payload.repoRoot) ?? parseString(payload.processRoot);
|
|
2091
|
+
const repoRoot = raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
|
|
2092
|
+
|
|
2093
|
+
const projectsDir = path.join(repoRoot, "projects");
|
|
2094
|
+
const apiDir = path.join(repoRoot, "api");
|
|
2095
|
+
const agentsDir = path.join(repoRoot, "agents");
|
|
2096
|
+
const hasProjectsDir = isDir(projectsDir);
|
|
2097
|
+
const hasApiDir = isDir(apiDir);
|
|
2098
|
+
const hasAgentsDir = isDir(agentsDir);
|
|
2099
|
+
|
|
2100
|
+
const projects = hasProjectsDir ? projectsApi.listProjects(repoRoot) : [];
|
|
2101
|
+
const hint = (() => {
|
|
2102
|
+
if (!hasProjectsDir) {
|
|
2103
|
+
return [
|
|
2104
|
+
"Repo does not contain /projects on the server filesystem.",
|
|
2105
|
+
"Start the MCP server from the monorepo root (the folder that contains both /api and /projects), or pass repoRoot explicitly.",
|
|
2106
|
+
"repoRoot is a server-side path, not a client/LLM path.",
|
|
2107
|
+
].join(" ");
|
|
2108
|
+
}
|
|
2109
|
+
if (hasProjectsDir && projects.length === 0) {
|
|
2110
|
+
return [
|
|
2111
|
+
"Found /projects, but no projects with docs/ were detected.",
|
|
2112
|
+
"Each project folder must contain a docs/ directory to be listed.",
|
|
2113
|
+
].join(" ");
|
|
2114
|
+
}
|
|
2115
|
+
return null;
|
|
2116
|
+
})();
|
|
2117
|
+
|
|
2118
|
+
return {
|
|
2119
|
+
received,
|
|
2120
|
+
cwd: process.cwd(),
|
|
2121
|
+
configuredRepoRoot: options.repoRoot ?? null,
|
|
2122
|
+
defaultRepoRoot,
|
|
2123
|
+
repoRoot,
|
|
2124
|
+
hasProjectsDir,
|
|
2125
|
+
hasApiDir,
|
|
2126
|
+
hasAgentsDir,
|
|
2127
|
+
projects,
|
|
2128
|
+
hint,
|
|
2129
|
+
};
|
|
2130
|
+
},
|
|
2059
2131
|
describeTool: (toolName: string) => {
|
|
2060
2132
|
const normalized = typeof toolName === "string" ? toolName.trim() : "";
|
|
2061
2133
|
if (!normalized) {
|
|
@@ -2099,7 +2171,7 @@ export const createExampleMcpServer = (
|
|
|
2099
2171
|
const repoRoot = (() => {
|
|
2100
2172
|
const raw =
|
|
2101
2173
|
parseString(payload.repoRoot) ?? parseString(payload.processRoot);
|
|
2102
|
-
return raw ? normalizeRepoRoot(raw) :
|
|
2174
|
+
return raw ? normalizeRepoRoot(raw) : defaultRepoRoot;
|
|
2103
2175
|
})();
|
|
2104
2176
|
|
|
2105
2177
|
const sectionRaw = parseString(payload.section)?.toLowerCase();
|
|
@@ -2475,6 +2547,21 @@ export const createExampleMcpServer = (
|
|
|
2475
2547
|
) {
|
|
2476
2548
|
details.hint =
|
|
2477
2549
|
"You likely passed a project root as repoRoot. repoRoot should be the repo root containing /projects. (Legacy alias: processRoot.)";
|
|
2550
|
+
details.suggestion = "Call mcp.workspace to see cwd/repoRoot on the server.";
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
if (
|
|
2554
|
+
/Project folder not found:/i.test(message) &&
|
|
2555
|
+
!details.hint &&
|
|
2556
|
+
/projects[\\/]/i.test(message)
|
|
2557
|
+
) {
|
|
2558
|
+
details.hint =
|
|
2559
|
+
"repoRoot might be wrong, or the server filesystem does not contain /projects for this repoRoot.";
|
|
2560
|
+
details.suggestion = "Call mcp.workspace to see cwd/repoRoot on the server.";
|
|
2561
|
+
details.example = {
|
|
2562
|
+
tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
|
|
2563
|
+
arguments: {},
|
|
2564
|
+
};
|
|
2478
2565
|
}
|
|
2479
2566
|
|
|
2480
2567
|
if (/Missing search pattern\./i.test(message)) {
|
|
@@ -2538,7 +2625,11 @@ export const createExampleMcpServer = (
|
|
|
2538
2625
|
}
|
|
2539
2626
|
|
|
2540
2627
|
try {
|
|
2541
|
-
const data = await invokeTool(
|
|
2628
|
+
const data = await invokeTool(
|
|
2629
|
+
toolDefinition,
|
|
2630
|
+
{ args, options },
|
|
2631
|
+
defaultRepoRoot,
|
|
2632
|
+
);
|
|
2542
2633
|
return { index, tool, isError: false, data };
|
|
2543
2634
|
} catch (error) {
|
|
2544
2635
|
const message =
|
|
@@ -2590,7 +2681,7 @@ export const createExampleMcpServer = (
|
|
|
2590
2681
|
}
|
|
2591
2682
|
|
|
2592
2683
|
try {
|
|
2593
|
-
const data = await invokeTool(tool, request.params.arguments);
|
|
2684
|
+
const data = await invokeTool(tool, request.params.arguments, defaultRepoRoot);
|
|
2594
2685
|
return toolOk(data);
|
|
2595
2686
|
} catch (error) {
|
|
2596
2687
|
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.6",
|
|
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\"",
|