@foundation0/api 1.1.1 → 1.1.3

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/agents.ts CHANGED
@@ -39,7 +39,7 @@ interface ActiveConfigInput {
39
39
  required: boolean
40
40
  }
41
41
 
42
- const CLI_NAME = 'example'
42
+ const CLI_NAME = 'f0'
43
43
  const AGENT_INITIAL_VERSION = 'v0.0.1'
44
44
  const DEFAULT_SKILL_NAME = 'coding-standards'
45
45
  const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
@@ -58,7 +58,7 @@ export function usage(): string {
58
58
  `Use --latest to resolve /file-name to the latest version.\n` +
59
59
  `The active file created is [file].active.<ext>.\n` +
60
60
  `Use "run" to start codex with developer_instructions from system/prompt.ts.\n` +
61
- `Set EXAMPLE_CODEX_BIN to pin a specific codex binary.\n`
61
+ `Set F0_CODEX_BIN (or EXAMPLE_CODEX_BIN) to pin a specific codex binary.\n`
62
62
  }
63
63
 
64
64
  export function resolveAgentsRoot(processRoot: string = process.cwd()): string {
@@ -460,9 +460,9 @@ export function setActiveLink(sourceFile: string, activeFile: string): 'symlink'
460
460
 
461
461
  fs.mkdirSync(path.dirname(activeFile), { recursive: true })
462
462
 
463
- if (fs.existsSync(activeFile)) {
464
- fs.rmSync(activeFile, { force: true })
465
- }
463
+ // Remove the active pointer first. Using rmSync directly (instead of existsSync)
464
+ // ensures we also clear broken symlinks (existsSync returns false for them).
465
+ fs.rmSync(activeFile, { force: true })
466
466
 
467
467
  const linkTarget = path.relative(path.dirname(activeFile), sourceFile)
468
468
 
@@ -643,7 +643,7 @@ type SpawnCodexResult = {
643
643
  type SpawnCodexFn = (args: string[]) => SpawnCodexResult
644
644
 
645
645
  function getCodexCommand(): string {
646
- const override = process.env.EXAMPLE_CODEX_BIN?.trim()
646
+ const override = process.env.F0_CODEX_BIN?.trim() ?? process.env.EXAMPLE_CODEX_BIN?.trim()
647
647
  if (override) {
648
648
  return override
649
649
  }
@@ -666,6 +666,7 @@ const defaultSpawnCodex: SpawnCodexFn = (args) => {
666
666
  const err = primary.error as NodeJS.ErrnoException
667
667
  if (
668
668
  process.platform === 'win32'
669
+ && !process.env.F0_CODEX_BIN
669
670
  && !process.env.EXAMPLE_CODEX_BIN
670
671
  && primaryCommand === 'codex.cmd'
671
672
  && err.code === 'ENOENT'
package/git.ts CHANGED
@@ -5,7 +5,7 @@ export type {
5
5
  GitServiceApi,
6
6
  GitServiceApiExecutionResult,
7
7
  GitServiceApiMethod,
8
- } from '../git/packages/git/src/index.ts'
8
+ } from '@foundation0/git'
9
9
 
10
10
  export {
11
11
  attachGitLabelManagementApi,
@@ -17,4 +17,4 @@ export {
17
17
  extractDependencyIssueNumbers,
18
18
  resolveProjectRepoIdentity,
19
19
  syncIssueDependencies,
20
- } from '../git/packages/git/src/index.ts'
20
+ } from '@foundation0/git'
package/mcp/AGENTS.md ADDED
@@ -0,0 +1,130 @@
1
+ # MCP Endpoint Design (LLM-Friendly)
2
+
3
+ This folder contains the Foundation0 MCP server (`api/mcp/server.ts`) and CLI (`api/mcp/cli.ts`).
4
+
5
+ These notes exist because the most common “LLM friction” issues are predictable:
6
+ - tool name/prefix confusion (duplicate names, “tool not found”),
7
+ - payload-shape confusion (positional `args` vs object options, or proxy tools that require JSON strings),
8
+ - oversized responses (truncation → extra tool calls),
9
+ - weak errors (not actionable → retries and wasted calls).
10
+
11
+ Use this doc when adding or modifying MCP endpoints so models can succeed in **one call**.
12
+
13
+ ## 1) Avoid Tool Prefix Double-Wrapping
14
+
15
+ If your agent uses `pi-mcp-adapter` (or any MCP proxy that already prefixes tools), do **not** also run the server with `--tools-prefix`.
16
+
17
+ Bad (creates confusing duplicates like `f0_f0_net_curl` and `f0_net_curl`):
18
+ - Agent tool prefixing: `toolPrefix: "server"` (adds `f0_...`)
19
+ - Server tool prefixing: `--tools-prefix f0` (adds `f0....`)
20
+
21
+ Good (single predictable name):
22
+ - Agent: `toolPrefix: "server"`
23
+ - Server: no `--tools-prefix`
24
+
25
+ ## 2) Prefer Direct Tools for Hot Paths (Fewer Calls, Fewer Errors)
26
+
27
+ Proxy-based MCP gateways often require passing `args` as a **JSON string** (not an object), which LLMs frequently get wrong.
28
+
29
+ If a tool is used often (HTTP, search, file ops), expose it as a **direct tool** in the agent config:
30
+
31
+ ```json
32
+ {
33
+ "mcp": {
34
+ "mcpServers": {
35
+ "f0": {
36
+ "directTools": ["net_curl", "batch"]
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Note: Direct tool names must be OpenAI-tool-name-safe (no dots). Use the underscore aliases (e.g., `net_curl`) when exposing direct tools.
44
+
45
+ Benefits:
46
+ - The model calls `f0_net_curl` directly (no gateway wrapper).
47
+ - The model passes a normal JSON object (no “args must be a JSON string” confusion).
48
+ - Tool discoverability improves (tool appears in the system prompt).
49
+
50
+ Guideline: keep direct tools to a focused set (5–20). Use proxy for large catalogs.
51
+
52
+ ## 3) Always Support a “Structured Mode” (Don’t Force Curl-CLI Literacy)
53
+
54
+ LLMs naturally try:
55
+ ```json
56
+ { "url": "https://…", "method": "GET", "headers": { "accept": "application/json" } }
57
+ ```
58
+
59
+ If your tool only accepts positional CLI-like args (e.g. `{ "args": ["-L", "…"] }`), the model will often:
60
+ - guess the wrong shape,
61
+ - call `describe`,
62
+ - retry,
63
+ - and burn multiple tool calls.
64
+
65
+ Best practice:
66
+ - support **both** shapes:
67
+ - Curl-style: `{ "args": ["-L", "https://…"] }`
68
+ - Structured: `{ "url": "https://…", "method": "GET", ... }`
69
+
70
+ For `net.curl` (alias `net_curl`), structured mode should accept the common fields:
71
+ - `url`/`urls`, `method`, `headers`, `body`/`data`/`json`, `followRedirects`, `includeHeaders`,
72
+ - `fail`, `silent`, `verbose`, `writeOut`,
73
+ - `timeoutMs`, `maxTimeSeconds`, `maxRedirects`,
74
+ - `output`/`remoteName`, `user`.
75
+
76
+ Also: keep field names intuitive (`url`, `method`, `headers`, `body`) even if internally you translate to an `args[]` array.
77
+
78
+ ## 4) Write a Tight Schema + Examples (Make the First Call Succeed)
79
+
80
+ Tool schemas are often the only thing an LLM sees.
81
+
82
+ Do:
83
+ - Provide a schema that shows **both** modes (positional `args[]` + structured keys).
84
+ - Add short descriptions that include **an example payload**.
85
+ - Prefer `additionalProperties: true` to allow forwards-compatible additions.
86
+
87
+ Avoid:
88
+ - schemas that only describe `args` but not recommended “structured mode”.
89
+ - schema-required fields that don’t exist for the other mode.
90
+
91
+ ## 5) Make Errors Actionable (Self-Correct in One Retry)
92
+
93
+ When rejecting input:
94
+ - Use clear “what you passed / what I expected” wording.
95
+ - Include a copy/paste example.
96
+ - Provide likely tool-name suggestions when tool lookup fails.
97
+
98
+ If you control the gateway layer:
99
+ - Treat argument-shape failures as `isError: true` so the LLM knows to correct (not continue).
100
+
101
+ ## 6) Keep Outputs Small by Default (Prevent Truncation)
102
+
103
+ Large outputs cause:
104
+ - truncation (the model misses the answer),
105
+ - follow-up calls,
106
+ - and sometimes tool-call loops.
107
+
108
+ Do:
109
+ - omit base64-encoded blobs by default (make them opt-in),
110
+ - provide `-o/--output` style file outputs for large bodies,
111
+ - include lightweight metadata (status code, headers, elapsed time).
112
+
113
+ ## 7) Add Tests that Simulate “LLM Mistakes”
114
+
115
+ Every new endpoint should include tests that cover:
116
+ - structured payload success (`{ url, method }`),
117
+ - curl-style args success (`{ args: [...] }`),
118
+ - invalid shape failure (missing URL),
119
+ - “tool not found” suggestion behavior (server-side).
120
+
121
+ Tests belong next to the server/tool code (Bun tests under `api/`).
122
+
123
+ ## 8) Endpoint Implementation Checklist
124
+
125
+ When adding `root.namespace.tool`:
126
+ - Add it under an explicit namespace (`net.*`, `projects.*`, etc.), not the root.
127
+ - Add/adjust `TOOL_INPUT_SCHEMA_OVERRIDES[toolName]` with examples.
128
+ - Ensure tool access is correct (read vs write vs admin).
129
+ - Ensure agent configs include the root endpoint in `--allowed-root-endpoints` when needed.
130
+ - Consider `directTools` for high-frequency tools.
package/mcp/cli.mjs CHANGED
@@ -32,6 +32,6 @@ try {
32
32
  process.exit(1)
33
33
  }
34
34
 
35
- console.error('Failed to launch example-org MCP server', error)
35
+ console.error('Failed to launch f0-mcp server', error)
36
36
  process.exit(1)
37
37
  }
package/mcp/cli.ts CHANGED
@@ -101,6 +101,6 @@ void runExampleMcpServer({
101
101
  enableIssues,
102
102
  admin,
103
103
  }).catch((error) => {
104
- console.error('Failed to start example-org MCP server', error)
105
- process.exit(1)
106
- })
104
+ console.error('Failed to start f0-mcp server', error)
105
+ process.exit(1)
106
+ })
@@ -126,4 +126,17 @@ describe('ExampleMcpClient git file guard', () => {
126
126
  expect(response.isError).toBe(false)
127
127
  expect(response.data).toEqual({ ok: true, body: { result: 'pass-through' } })
128
128
  })
129
+
130
+ it('keeps plain-text tool responses as data when JSON parsing fails', async () => {
131
+ stubRequests(() => ({
132
+ isError: false,
133
+ content: [{ type: 'text', text: 'not-json' }],
134
+ }))
135
+
136
+ const response = await client.call('projects.listProjects')
137
+
138
+ expect(response.isError).toBe(false)
139
+ expect(response.text).toBe('not-json')
140
+ expect(response.data).toBe('not-json')
141
+ })
129
142
  })
package/mcp/client.ts CHANGED
@@ -151,11 +151,19 @@ const decodeFileContent = (content: string, encoding: string): string => {
151
151
  }
152
152
 
153
153
  const extractGitFileContent = (data: unknown): ExtractedGitFileContent | null => {
154
- if (!isRecord(data) || !isRecord(data.body)) {
154
+ const unwrapped = (() => {
155
+ if (!isRecord(data)) return data
156
+ if (typeof data.ok === 'boolean' && 'result' in data) {
157
+ return (data as any).result
158
+ }
159
+ return data
160
+ })()
161
+
162
+ if (!isRecord(unwrapped) || !isRecord(unwrapped.body)) {
155
163
  return null
156
164
  }
157
165
 
158
- const body = data.body
166
+ const body = unwrapped.body
159
167
  if (typeof body.content !== 'string') {
160
168
  return null
161
169
  }
@@ -366,7 +374,7 @@ export class ExampleMcpClient {
366
374
  return {
367
375
  isError: Boolean(response.isError),
368
376
  text: parsed.text,
369
- data: parsed.parsed,
377
+ data: parsed.parsed ?? parsed.text,
370
378
  }
371
379
  }
372
380
 
@@ -547,7 +555,7 @@ export class ExampleMcpClient {
547
555
  methodArgs: unknown[] = [],
548
556
  methodOptions: Record<string, unknown> = {},
549
557
  ): Promise<ExampleMcpCallResponse> {
550
- const args = { args: methodArgs.map((value) => String(value)), options: methodOptions }
558
+ const args = { args: methodArgs, options: methodOptions }
551
559
  return this.call(path, args)
552
560
  }
553
561
  }