@foundation0/api 1.1.3 → 1.1.5

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/manual.md ADDED
@@ -0,0 +1,150 @@
1
+ # Foundation0 MCP Tool-Calling Manual (for LLMs)
2
+
3
+ This is a **tool-calling guide** for the Foundation0 MCP server in `api/mcp/*`.
4
+ Assume the server is already configured in your MCP host and you can call its tools.
5
+
6
+ ## 0) Golden rules
7
+
8
+ 1. If you are unsure about a tool name, call `mcp.listTools` first and use the exact name it returns (prefixes vary).
9
+ 2. Use `mcp.describeTool` before your first call to a tool to get the input schema + an example payload.
10
+ 3. Prefer named keys when possible (e.g. `projectName`, `repoRoot`) rather than relying on positional `args`.
11
+ 4. When a tool needs `repoRoot`, it must be the **repo root** containing both `api/` and `projects/` (not a single project folder). (Legacy alias: `processRoot` is still accepted.) If omitted, the server will try to auto-detect a repo root from its current working directory (best effort).
12
+ 5. Tool results are returned as a **JSON text envelope**: parse the text as JSON and check `ok`.
13
+
14
+ ## 1) Tool naming (prefixes + aliases)
15
+
16
+ Depending on how the server was started, tool names may be:
17
+
18
+ - unprefixed: `projects.listProjects`
19
+ - prefixed: `api.projects.listProjects`
20
+
21
+ The server usually exposes both when a prefix is set, but do not assume: **discover at runtime** via `mcp.listTools`.
22
+
23
+ Some tools also have OpenAI-safe underscore aliases (no dots). Example:
24
+
25
+ - `net.curl` may also be available as `net_curl`
26
+
27
+ ## 2) The 3 discovery calls
28
+
29
+ ### A) List all tools
30
+
31
+ Tool call:
32
+
33
+ ```json
34
+ { "name": "mcp.listTools", "arguments": {} }
35
+ ```
36
+
37
+ ### B) Describe one tool (schema + example)
38
+
39
+ Tool call (prefixed or unprefixed names both work here):
40
+
41
+ ```json
42
+ { "name": "mcp.describeTool", "arguments": { "args": ["projects.listProjects"] } }
43
+ ```
44
+
45
+ ### C) `mcp.search` for "search docs/spec" requests
46
+
47
+ Tool call:
48
+
49
+ ```json
50
+ {
51
+ "name": "mcp.search",
52
+ "arguments": {
53
+ "projectName": "<project-name>",
54
+ "section": "spec",
55
+ "pattern": "authentication",
56
+ "repoRoot": "<repo-root>",
57
+ "ignoreCase": true,
58
+ "maxCount": 50
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## 3) Payload shapes (important)
64
+
65
+ Most tools accept either:
66
+
67
+ ```json
68
+ { "args": ["<project-name>"], "options": { "repoRoot": "<repo-root>" } }
69
+ ```
70
+
71
+ or named keys (recommended). The server merges top-level keys into `options`:
72
+
73
+ ```json
74
+ { "projectName": "<project-name>", "repoRoot": "<repo-root>" }
75
+ ```
76
+
77
+ Guideline: if a tool has a natural named parameter (`projectName`, `agentName`, `target`, `taskRef`), pass it explicitly.
78
+
79
+ ## 4) Common calls (examples)
80
+
81
+ ### A) List projects
82
+
83
+ ```json
84
+ {
85
+ "name": "projects.listProjects",
86
+ "arguments": { "repoRoot": "<repo-root>" }
87
+ }
88
+ ```
89
+
90
+ ### B) List agents
91
+
92
+ ```json
93
+ {
94
+ "name": "agents.listAgents",
95
+ "arguments": { "repoRoot": "<repo-root>" }
96
+ }
97
+ ```
98
+
99
+ ### C) Set an active file (projects)
100
+
101
+ ```json
102
+ {
103
+ "name": "projects.setActive",
104
+ "arguments": {
105
+ "args": ["<project-name>", "/implementation-plan.v0.0.1"],
106
+ "options": { "repoRoot": "<repo-root>", "latest": true }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### D) Batch multiple calls
112
+
113
+ Call `batch` (or `<prefix>.batch`) to run multiple tool calls:
114
+
115
+ ```json
116
+ {
117
+ "name": "batch",
118
+ "arguments": {
119
+ "calls": [
120
+ { "tool": "projects.usage" },
121
+ { "tool": "projects.listProjects", "options": { "repoRoot": "<repo-root>" } }
122
+ ],
123
+ "continueOnError": true,
124
+ "maxConcurrency": 4
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## 5) Reading responses (envelopes + errors)
130
+
131
+ Tool results are returned as text containing JSON like:
132
+
133
+ - success: `{ "ok": true, "result": ... }`
134
+ - error: `{ "ok": false, "error": { "message": "...", "details": { ... } } }`
135
+
136
+ If you get:
137
+
138
+ - **Unknown tool**: use the `suggestions` from the error (when present), or call `mcp.listTools` again and retry.
139
+ - **Missing project name**: pass `projectName` (or set `args[0]`).
140
+ - **Project folder not found: ...projects/.../projects/...**: you likely passed the wrong `repoRoot` (it must be the repo root; legacy alias `processRoot`).
141
+
142
+ ## 6) Tool availability (read/write/admin)
143
+
144
+ The server can be started in modes that hide tools:
145
+
146
+ - read-only mode removes write-capable tools
147
+ - admin-only tools are hidden unless the server is started in admin mode
148
+ - root namespaces can be whitelisted (so entire namespaces may be missing)
149
+
150
+ If a tool is not listed by `mcp.listTools`, you cannot call it in the current server configuration.
@@ -32,9 +32,17 @@ describe("createExampleMcpServer endpoint whitelist", () => {
32
32
  const names = instance.tools.map((tool) => tool.name);
33
33
 
34
34
  expect(names.length).toBeGreaterThan(0);
35
- expect(names.every((name) => name.startsWith("projects."))).toBe(true);
35
+ expect(
36
+ names.every(
37
+ (name) => name.startsWith("projects.") || name.startsWith("mcp."),
38
+ ),
39
+ ).toBe(true);
36
40
  expect(names.some((name) => name.startsWith("agents."))).toBe(false);
37
41
  expect(names.some((name) => name.startsWith("net."))).toBe(false);
42
+ expect(names).toContain("mcp.usage");
43
+ expect(names).toContain("mcp.listTools");
44
+ expect(names).toContain("mcp.describeTool");
45
+ expect(names).not.toContain("mcp.search");
38
46
  });
39
47
 
40
48
  it("throws on unknown root endpoints", () => {
@@ -77,12 +85,20 @@ describe("createExampleMcpServer endpoint whitelist", () => {
77
85
  const names = instance.tools.map((tool) => tool.name);
78
86
 
79
87
  expect(names.length).toBeGreaterThan(0);
80
- expect(names.every((name) => name.startsWith("projects."))).toBe(true);
88
+ expect(
89
+ names.every(
90
+ (name) => name.startsWith("projects.") || name.startsWith("mcp."),
91
+ ),
92
+ ).toBe(true);
81
93
  expect(names).toContain("projects.readGitTask");
82
94
  expect(names).toContain("projects.fetchGitTasks");
83
95
  expect(names).not.toContain("projects.createGitIssue");
84
96
  expect(names).not.toContain("projects.writeGitTask");
85
97
  expect(names).not.toContain("projects.syncTasks");
98
+ expect(names).toContain("mcp.usage");
99
+ expect(names).toContain("mcp.listTools");
100
+ expect(names).toContain("mcp.describeTool");
101
+ expect(names).not.toContain("mcp.search");
86
102
  });
87
103
 
88
104
  it("re-enables issue write endpoints when enableIssues=true with disableWrite", () => {
@@ -186,6 +202,161 @@ describe("createExampleMcpServer request handling", () => {
186
202
  }
187
203
  });
188
204
 
205
+ it("accepts repoRoot for projects tools (preferred over legacy processRoot)", async () => {
206
+ const tempDir = await fs.mkdtemp(
207
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-"),
208
+ );
209
+ try {
210
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
211
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
212
+ recursive: true,
213
+ });
214
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
215
+ recursive: true,
216
+ });
217
+
218
+ const instance = createExampleMcpServer();
219
+ const handler = getToolHandler(instance);
220
+
221
+ const result = await handler(
222
+ {
223
+ method: "tools/call",
224
+ params: {
225
+ name: "projects.listProjects",
226
+ arguments: {
227
+ repoRoot: tempDir,
228
+ },
229
+ },
230
+ },
231
+ {},
232
+ );
233
+
234
+ expect(result.isError).toBe(false);
235
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
236
+ expect(payload.ok).toBe(true);
237
+ expect(payload.result).toContain("adl");
238
+ expect(payload.result).toContain("beta");
239
+ } finally {
240
+ await fs.rm(tempDir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ it("auto-detects repoRoot from process.cwd() when repoRoot is omitted", async () => {
245
+ const tempDir = await fs.mkdtemp(
246
+ path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
247
+ );
248
+ const originalCwd = process.cwd();
249
+
250
+ try {
251
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
252
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
253
+ recursive: true,
254
+ });
255
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
256
+ recursive: true,
257
+ });
258
+
259
+ process.chdir(tempDir);
260
+
261
+ const instance = createExampleMcpServer();
262
+ const handler = getToolHandler(instance);
263
+
264
+ const result = await handler(
265
+ {
266
+ method: "tools/call",
267
+ params: {
268
+ name: "projects.listProjects",
269
+ arguments: {},
270
+ },
271
+ },
272
+ {},
273
+ );
274
+
275
+ expect(result.isError).toBe(false);
276
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
277
+ expect(payload.ok).toBe(true);
278
+ expect(payload.result).toContain("adl");
279
+ expect(payload.result).toContain("beta");
280
+ } finally {
281
+ process.chdir(originalCwd);
282
+ await fs.rm(tempDir, { recursive: true, force: true });
283
+ }
284
+ });
285
+
286
+ it("still accepts legacy processRoot for backwards compatibility", async () => {
287
+ const tempDir = await fs.mkdtemp(
288
+ path.join(os.tmpdir(), "f0-mcp-server-processroot-"),
289
+ );
290
+ try {
291
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
292
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
293
+ recursive: true,
294
+ });
295
+
296
+ const instance = createExampleMcpServer();
297
+ const handler = getToolHandler(instance);
298
+
299
+ const result = await handler(
300
+ {
301
+ method: "tools/call",
302
+ params: {
303
+ name: "projects.listProjects",
304
+ arguments: {
305
+ processRoot: tempDir,
306
+ },
307
+ },
308
+ },
309
+ {},
310
+ );
311
+
312
+ expect(result.isError).toBe(false);
313
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
314
+ expect(payload.ok).toBe(true);
315
+ expect(payload.result).toContain("adl");
316
+ } finally {
317
+ await fs.rm(tempDir, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ it("normalizes repoRoot when caller accidentally passes a project root", async () => {
322
+ const tempDir = await fs.mkdtemp(
323
+ path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
324
+ );
325
+ try {
326
+ await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
327
+ await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
328
+ recursive: true,
329
+ });
330
+ await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
331
+ recursive: true,
332
+ });
333
+
334
+ const instance = createExampleMcpServer();
335
+ const handler = getToolHandler(instance);
336
+
337
+ const result = await handler(
338
+ {
339
+ method: "tools/call",
340
+ params: {
341
+ name: "projects.listProjects",
342
+ arguments: {
343
+ repoRoot: path.join(tempDir, "projects", "adl"),
344
+ },
345
+ },
346
+ },
347
+ {},
348
+ );
349
+
350
+ expect(result.isError).toBe(false);
351
+ const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
352
+ expect(payload.ok).toBe(true);
353
+ expect(payload.result).toContain("adl");
354
+ expect(payload.result).toContain("beta");
355
+ } finally {
356
+ await fs.rm(tempDir, { recursive: true, force: true });
357
+ }
358
+ });
359
+
189
360
  it('parses continueOnError from string "false" (fails fast)', async () => {
190
361
  const instance = createExampleMcpServer();
191
362
  const handler = getToolHandler(instance);
@@ -256,7 +427,10 @@ describe("createExampleMcpServer request handling", () => {
256
427
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
257
428
  expect(payload.ok).toBe(true);
258
429
  expect(typeof payload.result).toBe("string");
259
- expect(payload.result).toContain("Usage");
430
+ expect(payload.result).toContain("MCP");
431
+ expect(payload.result).toContain("projects.listProjects");
432
+ expect(payload.result).not.toContain("f0 projects");
433
+ expect(payload.result).not.toContain("--generate-spec");
260
434
  });
261
435
 
262
436
  it("exposes mcp.describeTool for tool discovery", async () => {