@foundation0/api 1.1.1 → 1.1.2

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.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
  }
@@ -1,4 +1,7 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
+ import fs from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
2
5
  import { createExampleMcpServer } from './server'
3
6
 
4
7
  describe('createExampleMcpServer endpoint whitelist', () => {
@@ -115,3 +118,133 @@ describe('createExampleMcpServer endpoint whitelist', () => {
115
118
  expect(adminNames).toContain('projects.clearIssues')
116
119
  })
117
120
  })
121
+
122
+ describe('createExampleMcpServer request handling', () => {
123
+ const getToolHandler = (instance: ReturnType<typeof createExampleMcpServer>) => {
124
+ const handler = (instance.server as any)._requestHandlers?.get('tools/call')
125
+ if (!handler) {
126
+ throw new Error('tools/call handler not registered')
127
+ }
128
+ return handler as (request: any, extra: any) => Promise<any>
129
+ }
130
+
131
+ it('passes structured args without stringifying', async () => {
132
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'f0-mcp-server-test-'))
133
+ try {
134
+ await fs.writeFile(path.join(tempDir, 'boot.v0.0.1.md'), '# boot\n', 'utf8')
135
+ const instance = createExampleMcpServer()
136
+ const handler = getToolHandler(instance)
137
+
138
+ const result = await handler({
139
+ method: 'tools/call',
140
+ params: {
141
+ name: 'agents.resolveTargetFile',
142
+ arguments: {
143
+ args: [
144
+ tempDir,
145
+ { parts: [], base: 'boot', version: 'v0.0.1', ext: 'md' },
146
+ ],
147
+ },
148
+ },
149
+ }, {})
150
+
151
+ const payload = JSON.parse(result.content?.[0]?.text ?? '{}')
152
+ expect(payload.ok).toBe(true)
153
+ expect(payload.result).toBe('boot.v0.0.1.md')
154
+ } finally {
155
+ await fs.rm(tempDir, { recursive: true, force: true })
156
+ }
157
+ })
158
+
159
+ it('parses continueOnError from string "false" (fails fast)', async () => {
160
+ const instance = createExampleMcpServer()
161
+ const handler = getToolHandler(instance)
162
+
163
+ const result = await handler({
164
+ method: 'tools/call',
165
+ params: {
166
+ name: 'batch',
167
+ arguments: {
168
+ calls: [
169
+ { tool: 'projects.usage' },
170
+ { tool: 'unknown.tool' },
171
+ ],
172
+ continueOnError: 'false',
173
+ },
174
+ },
175
+ }, {})
176
+
177
+ expect(result.isError).toBe(true)
178
+ const payload = JSON.parse(result.content?.[0]?.text ?? '{}')
179
+ expect(payload.ok).toBe(false)
180
+ expect(payload.error?.message).toContain('Unknown tool')
181
+ })
182
+
183
+ it('parses continueOnError from string "true" (returns per-call errors)', async () => {
184
+ const instance = createExampleMcpServer()
185
+ const handler = getToolHandler(instance)
186
+
187
+ const result = await handler({
188
+ method: 'tools/call',
189
+ params: {
190
+ name: 'batch',
191
+ arguments: {
192
+ calls: [
193
+ { tool: 'projects.usage' },
194
+ { tool: 'unknown.tool' },
195
+ ],
196
+ continueOnError: 'true',
197
+ maxConcurrency: 2,
198
+ },
199
+ },
200
+ }, {})
201
+
202
+ expect(result.isError).toBe(true)
203
+ const payload = JSON.parse(result.content?.[0]?.text ?? '{}')
204
+ expect(payload.ok).toBe(false)
205
+ expect(payload.error?.message).toContain('batch calls failed')
206
+ expect(Array.isArray(payload.error?.details?.results)).toBe(true)
207
+ expect(payload.error.details.results[1].isError).toBe(true)
208
+ })
209
+
210
+ it('accepts unprefixed tool name when toolsPrefix is set', async () => {
211
+ const instance = createExampleMcpServer({ toolsPrefix: 'api' })
212
+ const handler = getToolHandler(instance)
213
+
214
+ const result = await handler({
215
+ method: 'tools/call',
216
+ params: {
217
+ name: 'projects.usage',
218
+ arguments: {},
219
+ },
220
+ }, {})
221
+
222
+ expect(result.isError).toBe(false)
223
+ const payload = JSON.parse(result.content?.[0]?.text ?? '{}')
224
+ expect(payload.ok).toBe(true)
225
+ expect(typeof payload.result).toBe('string')
226
+ expect(payload.result).toContain('Usage')
227
+ })
228
+
229
+ it('exposes mcp.describeTool for tool discovery', async () => {
230
+ const instance = createExampleMcpServer({ toolsPrefix: 'api' })
231
+ const handler = getToolHandler(instance)
232
+
233
+ const result = await handler({
234
+ method: 'tools/call',
235
+ params: {
236
+ name: 'mcp.describeTool',
237
+ arguments: {
238
+ args: ['projects.usage'],
239
+ },
240
+ },
241
+ }, {})
242
+
243
+ expect(result.isError).toBe(false)
244
+ const payload = JSON.parse(result.content?.[0]?.text ?? '{}')
245
+ expect(payload.ok).toBe(true)
246
+ expect(payload.result.name).toBe('api.projects.usage')
247
+ expect(payload.result.unprefixedName).toBe('projects.usage')
248
+ expect(payload.result.access).toBe('read')
249
+ })
250
+ })