@foundation0/api 1.0.0

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/server.ts ADDED
@@ -0,0 +1,461 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
+ import * as agentsApi from '../agents.ts'
5
+ import * as projectsApi from '../projects.ts'
6
+
7
+ type ApiMethod = (...args: unknown[]) => unknown
8
+ type ToolInvocationPayload = {
9
+ args?: unknown[]
10
+ options?: Record<string, unknown>
11
+ [key: string]: unknown
12
+ }
13
+ type BatchToolCall = {
14
+ tool: string
15
+ args?: unknown[]
16
+ options?: Record<string, unknown>
17
+ [key: string]: unknown
18
+ }
19
+ type BatchToolCallPayload = {
20
+ calls: BatchToolCall[]
21
+ continueOnError: boolean
22
+ }
23
+ type BatchResult = {
24
+ index: number
25
+ tool: string
26
+ isError: boolean
27
+ data: unknown
28
+ }
29
+ type ToolDefinition = {
30
+ name: string
31
+ method: ApiMethod
32
+ path: string[]
33
+ }
34
+
35
+ type ToolNamespace = Record<string, unknown>
36
+
37
+ type ApiEndpoint = {
38
+ agents: ToolNamespace
39
+ projects: ToolNamespace
40
+ }
41
+
42
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
43
+ typeof value === 'object' && value !== null && !Array.isArray(value)
44
+
45
+ const normalizePayload = (payload: unknown): { args: string[]; options: Record<string, unknown> } => {
46
+ if (!isRecord(payload)) {
47
+ return {
48
+ args: [],
49
+ options: {},
50
+ }
51
+ }
52
+
53
+ const explicitArgs = Array.isArray(payload.args) ? payload.args : undefined
54
+ const explicitOptions = isRecord(payload.options) ? payload.options : undefined
55
+
56
+ const args = explicitArgs ? explicitArgs.map((entry) => String(entry)) : []
57
+ const options: Record<string, unknown> = explicitOptions ? { ...explicitOptions } : {}
58
+
59
+ for (const [key, value] of Object.entries(payload)) {
60
+ if (key === 'args' || key === 'options') {
61
+ continue
62
+ }
63
+
64
+ if (value !== undefined) {
65
+ options[key] = value
66
+ }
67
+ }
68
+
69
+ return {
70
+ args,
71
+ options,
72
+ }
73
+ }
74
+
75
+ const normalizeBatchToolCall = (
76
+ call: unknown,
77
+ index: number,
78
+ ): { tool: string; payload: ToolInvocationPayload } => {
79
+ if (!isRecord(call)) {
80
+ throw new Error(`Invalid batch call at index ${index}: expected object`)
81
+ }
82
+
83
+ const tool = typeof call.tool === 'string' ? call.tool.trim() : ''
84
+ if (!tool) {
85
+ throw new Error(`Invalid batch call at index ${index}: missing "tool"`)
86
+ }
87
+
88
+ const args = Array.isArray(call.args) ? call.args : []
89
+ const { options, ...extras } = call
90
+ const normalized: ToolInvocationPayload = {
91
+ args,
92
+ options: isRecord(options) ? options : {},
93
+ }
94
+
95
+ for (const [key, value] of Object.entries(extras)) {
96
+ if (value !== undefined) {
97
+ normalized.options[key] = value
98
+ }
99
+ }
100
+
101
+ return {
102
+ tool,
103
+ payload: normalized,
104
+ }
105
+ }
106
+
107
+ const normalizeBatchPayload = (payload: unknown): BatchToolCallPayload => {
108
+ if (!isRecord(payload)) {
109
+ throw new Error('Batch tool call requires an object payload')
110
+ }
111
+
112
+ if (!Array.isArray(payload.calls)) {
113
+ throw new Error('Batch tool call requires a "calls" array')
114
+ }
115
+
116
+ const calls = payload.calls.map((call, index) => normalizeBatchToolCall(call, index))
117
+
118
+ return {
119
+ calls: calls.map(({ tool, payload }) => ({
120
+ tool,
121
+ ...payload,
122
+ })),
123
+ continueOnError: Boolean(payload.continueOnError),
124
+ }
125
+ }
126
+
127
+ const collectTools = (api: ToolNamespace, namespace: string[], path: string[] = []): ToolDefinition[] => {
128
+ const tools: ToolDefinition[] = []
129
+ const currentPath = [...path, ...namespace]
130
+
131
+ for (const [segment, value] of Object.entries(api)) {
132
+ if (typeof value !== 'function') {
133
+ continue
134
+ }
135
+
136
+ tools.push({
137
+ name: [...currentPath, segment].join('.'),
138
+ method: value as ApiMethod,
139
+ path: [...currentPath, segment],
140
+ })
141
+ }
142
+
143
+ return tools
144
+ }
145
+
146
+ const buildToolName = (tool: ToolDefinition, prefix?: string): string =>
147
+ prefix ? `${prefix}.${tool.name}` : tool.name
148
+
149
+ const buildToolList = (tools: ToolDefinition[], batchToolName: string, prefix?: string) => {
150
+ const toolEntries = tools.map((tool) => ({
151
+ name: buildToolName(tool, prefix),
152
+ description: `Call API method ${tool.path.join('.')}`,
153
+ inputSchema: {
154
+ type: 'object',
155
+ additionalProperties: true,
156
+ properties: {
157
+ args: {
158
+ type: 'array',
159
+ items: { type: 'string' },
160
+ description: 'Positional arguments',
161
+ },
162
+ options: {
163
+ type: 'object',
164
+ additionalProperties: true,
165
+ description: 'Named options',
166
+ },
167
+ },
168
+ },
169
+ }))
170
+
171
+ const batchTool = {
172
+ name: batchToolName,
173
+ description:
174
+ 'Preferred mode for client calls: execute multiple MCP tool calls in one request with parallel execution.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ additionalProperties: true,
178
+ properties: {
179
+ calls: {
180
+ type: 'array',
181
+ minItems: 1,
182
+ items: {
183
+ type: 'object',
184
+ additionalProperties: true,
185
+ properties: {
186
+ tool: {
187
+ type: 'string',
188
+ description: 'Full MCP tool name to execute',
189
+ },
190
+ args: {
191
+ type: 'array',
192
+ items: { type: 'string' },
193
+ description: 'Positional args for the tool',
194
+ },
195
+ options: {
196
+ type: 'object',
197
+ additionalProperties: true,
198
+ description: 'Tool invocation options',
199
+ },
200
+ },
201
+ required: ['tool'],
202
+ },
203
+ description: 'List of tool calls to execute',
204
+ },
205
+ continueOnError: {
206
+ type: 'boolean',
207
+ description: 'Whether to continue when a call in the batch fails',
208
+ default: false,
209
+ },
210
+ },
211
+ required: ['calls'],
212
+ },
213
+ }
214
+
215
+ return [...toolEntries, batchTool]
216
+ }
217
+
218
+ type ToolInvoker = (args: string[], options: Record<string, unknown>) => unknown[]
219
+
220
+ const buildOptionsOnly = (args: string[], options: Record<string, unknown>): unknown[] => {
221
+ const invocationArgs: unknown[] = [...args]
222
+ if (Object.keys(options).length > 0) {
223
+ invocationArgs.push(options)
224
+ }
225
+ return invocationArgs
226
+ }
227
+
228
+ const buildOptionsThenProcessRoot = (args: string[], options: Record<string, unknown>): unknown[] => {
229
+ const invocationArgs: unknown[] = [...args]
230
+ const remaining = { ...options }
231
+ const processRoot = remaining.processRoot
232
+ if (typeof processRoot === 'string') {
233
+ delete remaining.processRoot
234
+ }
235
+
236
+ if (Object.keys(remaining).length > 0) {
237
+ invocationArgs.push(remaining)
238
+ }
239
+ if (typeof processRoot === 'string') {
240
+ invocationArgs.push(processRoot)
241
+ }
242
+
243
+ return invocationArgs
244
+ }
245
+
246
+ const buildProcessRootThenOptions = (args: string[], options: Record<string, unknown>): unknown[] => {
247
+ const invocationArgs: unknown[] = [...args]
248
+ const remaining = { ...options }
249
+ const processRoot = remaining.processRoot
250
+ if (typeof processRoot === 'string') {
251
+ delete remaining.processRoot
252
+ }
253
+
254
+ if (typeof processRoot === 'string') {
255
+ invocationArgs.push(processRoot)
256
+ }
257
+ if (Object.keys(remaining).length > 0) {
258
+ invocationArgs.push(remaining)
259
+ }
260
+
261
+ return invocationArgs
262
+ }
263
+
264
+ const buildProcessRootOnly = (args: string[], options: Record<string, unknown>): unknown[] => {
265
+ const invocationArgs: unknown[] = [...args]
266
+ const processRoot = options.processRoot
267
+ if (typeof processRoot === 'string') {
268
+ invocationArgs.push(processRoot)
269
+ }
270
+ return invocationArgs
271
+ }
272
+
273
+ const toolInvocationPlans: Record<string, ToolInvoker> = {
274
+ 'agents.setActive': buildProcessRootThenOptions,
275
+ 'agents.resolveAgentsRootFrom': buildProcessRootOnly,
276
+ 'projects.setActive': buildProcessRootThenOptions,
277
+ 'projects.generateSpec': buildOptionsThenProcessRoot,
278
+ 'projects.syncTasks': buildOptionsThenProcessRoot,
279
+ 'projects.clearIssues': buildOptionsThenProcessRoot,
280
+ 'projects.fetchGitTasks': buildOptionsThenProcessRoot,
281
+ 'projects.readGitTask': buildOptionsThenProcessRoot,
282
+ 'projects.writeGitTask': buildOptionsThenProcessRoot,
283
+ 'agents.resolveTargetFile': buildOptionsOnly,
284
+ 'projects.resolveProjectTargetFile': buildOptionsOnly,
285
+ 'agents.loadAgent': buildProcessRootOnly,
286
+ 'agents.main': buildProcessRootOnly,
287
+ 'agents.resolveAgentsRoot': buildProcessRootOnly,
288
+ 'agents.resolveAgentsRootFrom': buildProcessRootOnly,
289
+ 'agents.listAgents': buildProcessRootOnly,
290
+ 'projects.resolveProjectRoot': buildProcessRootOnly,
291
+ 'projects.listProjects': buildProcessRootOnly,
292
+ 'projects.listProjectDocs': buildProcessRootOnly,
293
+ 'projects.readProjectDoc': buildProcessRootOnly,
294
+ 'projects.main': buildProcessRootOnly,
295
+ }
296
+
297
+ const invokeTool = async (tool: ToolDefinition, payload: unknown): Promise<unknown> => {
298
+ const { args, options } = normalizePayload(payload)
299
+ const invoke = toolInvocationPlans[tool.name] ?? ((rawArgs, rawOptions) => {
300
+ const invocationArgs = [...rawArgs]
301
+ if (Object.keys(rawOptions).length > 0) {
302
+ invocationArgs.push(rawOptions)
303
+ }
304
+ return invocationArgs
305
+ })
306
+ const invocationArgs = invoke(args, options)
307
+
308
+ return Promise.resolve(tool.method(...invocationArgs))
309
+ }
310
+
311
+ export interface F0McpServerOptions {
312
+ serverName?: string
313
+ serverVersion?: string
314
+ toolsPrefix?: string
315
+ }
316
+
317
+ export type F0McpServerInstance = {
318
+ api: ApiEndpoint
319
+ tools: ToolDefinition[]
320
+ server: Server
321
+ run: () => Promise<Server>
322
+ }
323
+
324
+ export const createF0McpServer = (options: F0McpServerOptions = {}): F0McpServerInstance => {
325
+ const api: ApiEndpoint = {
326
+ agents: agentsApi,
327
+ projects: projectsApi,
328
+ }
329
+
330
+ const tools = [
331
+ ...collectTools(api.agents, ['agents']),
332
+ ...collectTools(api.projects, ['projects']),
333
+ ]
334
+ const prefix = options.toolsPrefix
335
+ const batchToolName = prefix ? `${prefix}.batch` : 'batch'
336
+
337
+ const server = new Server(
338
+ {
339
+ name: options.serverName ?? 'f0-api',
340
+ version: options.serverVersion ?? '1.0.0',
341
+ },
342
+ {
343
+ capabilities: {
344
+ tools: {},
345
+ },
346
+ },
347
+ )
348
+
349
+ const toolByName = new Map<string, ToolDefinition>(tools.map((tool) => [buildToolName(tool, prefix), tool]))
350
+
351
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
352
+ tools: buildToolList(tools, batchToolName, prefix),
353
+ }))
354
+
355
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
356
+ const requestedName = request.params.name
357
+
358
+ if (requestedName === batchToolName) {
359
+ try {
360
+ const { calls, continueOnError } = normalizeBatchPayload(request.params.arguments)
361
+ const executions = calls.map(({ tool, args = [], options = {} }, index) => ({ tool, args, options, index }))
362
+ const results = await Promise.all(
363
+ executions.map(async ({ tool, args, options, index }) => {
364
+ const toolDefinition = toolByName.get(tool)
365
+ if (!toolDefinition) {
366
+ return {
367
+ index,
368
+ tool,
369
+ isError: true,
370
+ data: `Unknown tool: ${tool}`,
371
+ } as BatchResult
372
+ }
373
+
374
+ try {
375
+ const data = await invokeTool(toolDefinition, { args, options })
376
+ return {
377
+ index,
378
+ tool,
379
+ isError: false,
380
+ data,
381
+ } as BatchResult
382
+ } catch (error) {
383
+ if (continueOnError) {
384
+ return {
385
+ index,
386
+ tool,
387
+ isError: true,
388
+ data: error instanceof Error ? error.message : String(error),
389
+ } as BatchResult
390
+ }
391
+ throw error
392
+ }
393
+ }),
394
+ )
395
+
396
+ return {
397
+ isError: results.some((result) => result.isError),
398
+ content: [
399
+ {
400
+ type: 'text',
401
+ text: JSON.stringify(results, null, 2),
402
+ },
403
+ ],
404
+ }
405
+ } catch (error) {
406
+ return {
407
+ isError: true,
408
+ content: [
409
+ {
410
+ type: 'text',
411
+ text: error instanceof Error ? error.message : String(error),
412
+ },
413
+ ],
414
+ }
415
+ }
416
+ }
417
+
418
+ const tool = toolByName.get(requestedName)
419
+
420
+ if (!tool) {
421
+ throw new Error(`Unknown tool: ${requestedName}`)
422
+ }
423
+
424
+ try {
425
+ const data = await invokeTool(tool, request.params.arguments)
426
+ return {
427
+ content: [
428
+ {
429
+ type: 'text',
430
+ text: JSON.stringify(data, null, 2),
431
+ },
432
+ ],
433
+ }
434
+ } catch (error) {
435
+ return {
436
+ isError: true,
437
+ content: [
438
+ {
439
+ type: 'text',
440
+ text: error instanceof Error ? error.message : String(error),
441
+ },
442
+ ],
443
+ }
444
+ }
445
+ })
446
+
447
+ const run = async (): Promise<Server> => {
448
+ await server.connect(new StdioServerTransport())
449
+ return server
450
+ }
451
+
452
+ return { api, tools, server, run }
453
+ }
454
+
455
+ export const runF0McpServer = async (options: F0McpServerOptions = {}): Promise<Server> => {
456
+ const instance = createF0McpServer(options)
457
+ return instance.run()
458
+ }
459
+
460
+ export const normalizeToolCallNameForServer = (prefix: string | undefined, toolName: string): string =>
461
+ prefix ? toolName.replace(new RegExp(`^${prefix}\\.`), '') : toolName
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@foundation0/api",
3
+ "version": "1.0.0",
4
+ "description": "Foundation 0 API",
5
+ "type": "module",
6
+ "bin": {
7
+ "f0-mcp": "./mcp/cli.mjs",
8
+ "f0-mcp-server": "./mcp/cli.mjs"
9
+ },
10
+ "private": false,
11
+ "main": "agents.ts",
12
+ "keywords": [
13
+ "foundation0",
14
+ "f0",
15
+ "api"
16
+ ],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "files": [
20
+ "agents.ts",
21
+ "git.ts",
22
+ "dist/git.js",
23
+ "taskgraph-parser.ts",
24
+ "projects.ts",
25
+ "mcp"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "scripts": {
32
+ "build:git": "bun build ../git/packages/git/src/index.ts --target bun --format esm --minify --outfile ./dist/git.js",
33
+ "mcp": "bun run mcp/cli.ts",
34
+ "test": "echo \"Error: no test specified\" && exit 1",
35
+ "deploy": "pnpm prepublishOnly && pnpm publish --access public",
36
+ "version:patch": "pnpm version patch",
37
+ "version:minor": "pnpm version minor",
38
+ "version:major": "pnpm version major"
39
+ }
40
+ }