@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/libs/curl.test.ts +130 -0
- package/libs/curl.ts +770 -0
- package/mcp/manual.md +150 -0
- package/mcp/server.test.ts +177 -3
- package/mcp/server.ts +173 -85
- package/net.ts +170 -0
- package/package.json +4 -2
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.
|
package/mcp/server.test.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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("
|
|
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 () => {
|