@open-mercato/ai-assistant 0.4.2-canary-c02407ff85

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.
Files changed (193) hide show
  1. package/AGENTS.md +1090 -0
  2. package/README.md +607 -0
  3. package/build.mjs +92 -0
  4. package/dist/di.js +8 -0
  5. package/dist/di.js.map +7 -0
  6. package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
  7. package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
  8. package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
  9. package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
  10. package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
  11. package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
  12. package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
  13. package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
  14. package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
  15. package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
  16. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
  17. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
  18. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
  19. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
  20. package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
  21. package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
  22. package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
  23. package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
  24. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
  25. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
  26. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
  27. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
  28. package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
  29. package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
  30. package/dist/frontend/components/CommandPalette/index.js +28 -0
  31. package/dist/frontend/components/CommandPalette/index.js.map +7 -0
  32. package/dist/frontend/constants.js +41 -0
  33. package/dist/frontend/constants.js.map +7 -0
  34. package/dist/frontend/hooks/index.js +13 -0
  35. package/dist/frontend/hooks/index.js.map +7 -0
  36. package/dist/frontend/hooks/useCommandPalette.js +1094 -0
  37. package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
  38. package/dist/frontend/hooks/useMcpTools.js +66 -0
  39. package/dist/frontend/hooks/useMcpTools.js.map +7 -0
  40. package/dist/frontend/hooks/usePageContext.js +48 -0
  41. package/dist/frontend/hooks/usePageContext.js.map +7 -0
  42. package/dist/frontend/hooks/useRecentActions.js +56 -0
  43. package/dist/frontend/hooks/useRecentActions.js.map +7 -0
  44. package/dist/frontend/hooks/useRecentTools.js +55 -0
  45. package/dist/frontend/hooks/useRecentTools.js.map +7 -0
  46. package/dist/frontend/index.js +35 -0
  47. package/dist/frontend/index.js.map +7 -0
  48. package/dist/frontend/types.js +1 -0
  49. package/dist/frontend/types.js.map +7 -0
  50. package/dist/frontend/utils/index.js +7 -0
  51. package/dist/frontend/utils/index.js.map +7 -0
  52. package/dist/frontend/utils/toolMatcher.js +95 -0
  53. package/dist/frontend/utils/toolMatcher.js.map +7 -0
  54. package/dist/index.js +57 -0
  55. package/dist/index.js.map +7 -0
  56. package/dist/modules/ai_assistant/acl.js +14 -0
  57. package/dist/modules/ai_assistant/acl.js.map +7 -0
  58. package/dist/modules/ai_assistant/api/chat/route.js +152 -0
  59. package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
  60. package/dist/modules/ai_assistant/api/health/route.js +27 -0
  61. package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
  62. package/dist/modules/ai_assistant/api/route/route.js +123 -0
  63. package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
  64. package/dist/modules/ai_assistant/api/settings/route.js +60 -0
  65. package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
  66. package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
  67. package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
  68. package/dist/modules/ai_assistant/api/tools/route.js +48 -0
  69. package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
  70. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
  71. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
  72. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
  73. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
  74. package/dist/modules/ai_assistant/cli.js +192 -0
  75. package/dist/modules/ai_assistant/cli.js.map +7 -0
  76. package/dist/modules/ai_assistant/di.js +11 -0
  77. package/dist/modules/ai_assistant/di.js.map +7 -0
  78. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
  79. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
  80. package/dist/modules/ai_assistant/index.js +13 -0
  81. package/dist/modules/ai_assistant/index.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
  83. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
  85. package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
  87. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
  89. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/auth.js +87 -0
  91. package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
  93. package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
  95. package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/http-server.js +367 -0
  97. package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
  99. package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
  101. package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
  103. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
  105. package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
  107. package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
  109. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
  111. package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
  113. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
  115. package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
  117. package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
  119. package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
  121. package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
  123. package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
  125. package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/types.js +1 -0
  127. package/dist/modules/ai_assistant/lib/types.js.map +7 -0
  128. package/package.json +108 -0
  129. package/src/di.ts +11 -0
  130. package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
  131. package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
  132. package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
  133. package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
  134. package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
  135. package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
  136. package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
  137. package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
  138. package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
  139. package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
  140. package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
  141. package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
  142. package/src/frontend/components/CommandPalette/index.ts +14 -0
  143. package/src/frontend/constants.ts +35 -0
  144. package/src/frontend/hooks/index.ts +5 -0
  145. package/src/frontend/hooks/useCommandPalette.ts +1389 -0
  146. package/src/frontend/hooks/useMcpTools.ts +73 -0
  147. package/src/frontend/hooks/usePageContext.ts +61 -0
  148. package/src/frontend/hooks/useRecentActions.ts +64 -0
  149. package/src/frontend/hooks/useRecentTools.ts +69 -0
  150. package/src/frontend/index.ts +39 -0
  151. package/src/frontend/types.ts +260 -0
  152. package/src/frontend/utils/index.ts +1 -0
  153. package/src/frontend/utils/toolMatcher.ts +127 -0
  154. package/src/index.ts +92 -0
  155. package/src/modules/ai_assistant/acl.ts +10 -0
  156. package/src/modules/ai_assistant/api/chat/route.ts +213 -0
  157. package/src/modules/ai_assistant/api/health/route.ts +30 -0
  158. package/src/modules/ai_assistant/api/route/route.ts +149 -0
  159. package/src/modules/ai_assistant/api/settings/route.ts +73 -0
  160. package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
  161. package/src/modules/ai_assistant/api/tools/route.ts +57 -0
  162. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
  163. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
  164. package/src/modules/ai_assistant/cli.ts +233 -0
  165. package/src/modules/ai_assistant/di.ts +9 -0
  166. package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
  167. package/src/modules/ai_assistant/index.ts +11 -0
  168. package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
  169. package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
  170. package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
  171. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
  172. package/src/modules/ai_assistant/lib/auth.ts +185 -0
  173. package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
  174. package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
  175. package/src/modules/ai_assistant/lib/http-server.ts +498 -0
  176. package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
  177. package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
  178. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
  179. package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
  180. package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
  181. package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
  182. package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
  183. package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
  184. package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
  185. package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
  186. package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
  187. package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
  188. package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
  189. package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
  190. package/src/modules/ai_assistant/lib/types.ts +147 -0
  191. package/test-schema.ts +37 -0
  192. package/tsconfig.json +10 -0
  193. package/watch.mjs +6 -0
@@ -0,0 +1,214 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import {
4
+ ListToolsRequestSchema,
5
+ CallToolRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js'
7
+ import { zodToJsonSchema } from 'zod-to-json-schema'
8
+ import { getToolRegistry } from './tool-registry'
9
+ import { executeTool } from './tool-executor'
10
+ import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
11
+ import { authenticateMcpRequest, hasRequiredFeatures } from './auth'
12
+ import type { McpServerOptions, McpToolContext } from './types'
13
+ import type { SearchService } from '@open-mercato/search/service'
14
+
15
+ /**
16
+ * Create and configure an MCP server instance.
17
+ */
18
+ export async function createMcpServer(options: McpServerOptions): Promise<Server> {
19
+ const { config, container, context, apiKeySecret } = options
20
+
21
+ let tenantId: string | null = null
22
+ let organizationId: string | null = null
23
+ let userId: string | null = null
24
+ let userFeatures: string[] = []
25
+ let isSuperAdmin = false
26
+
27
+ // API key authentication takes precedence
28
+ if (apiKeySecret) {
29
+ const authResult = await authenticateMcpRequest(apiKeySecret, container)
30
+ if (!authResult.success) {
31
+ throw new Error(`API key authentication failed: ${authResult.error}`)
32
+ }
33
+ tenantId = authResult.tenantId
34
+ organizationId = authResult.organizationId
35
+ userId = authResult.userId
36
+ userFeatures = authResult.features
37
+ isSuperAdmin = authResult.isSuperAdmin
38
+ console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`)
39
+ } else if (context) {
40
+ // Manual context provided
41
+ tenantId = context.tenantId
42
+ organizationId = context.organizationId
43
+ userId = context.userId
44
+
45
+ if (userId) {
46
+ try {
47
+ const rbacService = container.resolve('rbacService') as {
48
+ loadAcl: (
49
+ userId: string,
50
+ scope: { tenantId: string | null; organizationId: string | null }
51
+ ) => Promise<{
52
+ isSuperAdmin: boolean
53
+ features: string[]
54
+ }>
55
+ }
56
+ const acl = await rbacService.loadAcl(userId, {
57
+ tenantId,
58
+ organizationId,
59
+ })
60
+ userFeatures = acl.features
61
+ isSuperAdmin = acl.isSuperAdmin
62
+ } catch (error) {
63
+ console.error('[MCP Server] Failed to load user ACL:', error)
64
+ }
65
+ } else {
66
+ // No user specified - grant superadmin access for development/testing
67
+ isSuperAdmin = true
68
+ console.error('[MCP Server] No user specified, running with superadmin access')
69
+ }
70
+ } else {
71
+ // No context and no API key - superadmin for dev/testing
72
+ isSuperAdmin = true
73
+ console.error('[MCP Server] No auth context, running with superadmin access')
74
+ }
75
+
76
+ const toolContext: McpToolContext = {
77
+ tenantId,
78
+ organizationId,
79
+ userId,
80
+ container,
81
+ userFeatures,
82
+ isSuperAdmin,
83
+ apiKeySecret,
84
+ }
85
+
86
+ const server = new Server(
87
+ { name: config.name, version: config.version },
88
+ { capabilities: { tools: {} } }
89
+ )
90
+
91
+ // List tools handler
92
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
93
+ const registry = getToolRegistry()
94
+ const tools = Array.from(registry.getTools().values())
95
+
96
+ // Filter tools based on user permissions
97
+ const accessibleTools = tools.filter((tool) =>
98
+ hasRequiredFeatures(tool.requiredFeatures, userFeatures, isSuperAdmin)
99
+ )
100
+
101
+ if (config.debug) {
102
+ console.error(
103
+ `[MCP Server] Listing ${accessibleTools.length}/${tools.length} tools (filtered by ACL)`
104
+ )
105
+ }
106
+
107
+ return {
108
+ tools: accessibleTools.map((tool) => ({
109
+ name: tool.name,
110
+ description: tool.description,
111
+ // Cast to any for Zod v4 compatibility with zod-to-json-schema
112
+ inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,
113
+ })),
114
+ }
115
+ })
116
+
117
+ // Call tool handler
118
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
119
+ const { name, arguments: args } = request.params
120
+
121
+ if (config.debug) {
122
+ console.error(`[MCP Server] Calling tool: ${name}`, JSON.stringify(args))
123
+ }
124
+
125
+ const result = await executeTool(name, args ?? {}, toolContext)
126
+
127
+ if (!result.success) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: JSON.stringify({ error: result.error, code: result.errorCode }),
133
+ },
134
+ ],
135
+ isError: true,
136
+ }
137
+ }
138
+
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: JSON.stringify(result.result, null, 2),
144
+ },
145
+ ],
146
+ }
147
+ })
148
+
149
+ return server
150
+ }
151
+
152
+ /**
153
+ * Run MCP server with stdio transport.
154
+ * This keeps the process running until terminated.
155
+ *
156
+ * Supports two authentication modes:
157
+ * 1. API key: Provide `apiKeySecret` option
158
+ * 2. Manual context: Provide `context` with tenant/org/user
159
+ */
160
+ export async function runMcpServer(options: McpServerOptions): Promise<void> {
161
+ // Load tools from all modules before starting
162
+ await loadAllModuleTools()
163
+
164
+ // Index tools and API endpoints for hybrid search discovery (if search service available)
165
+ try {
166
+ const searchService = options.container.resolve('searchService') as SearchService
167
+
168
+ // Index MCP tools
169
+ await indexToolsForSearch(searchService)
170
+
171
+ // Index API endpoints for api_discover
172
+ const { indexApiEndpoints } = await import('./api-endpoint-index')
173
+ const endpointCount = await indexApiEndpoints(searchService)
174
+ if (endpointCount > 0) {
175
+ console.error(`[MCP Server] Indexed ${endpointCount} API endpoints for hybrid search`)
176
+ }
177
+ } catch (error) {
178
+ // Search service might not be configured - discovery will use fallback
179
+ console.error('[MCP Server] Search indexing skipped (search service not available)')
180
+ }
181
+
182
+ const server = await createMcpServer(options)
183
+ const transport = new StdioServerTransport()
184
+
185
+ const toolCount = getToolRegistry().listToolNames().length
186
+
187
+ console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`)
188
+
189
+ if (options.apiKeySecret) {
190
+ console.error(`[MCP Server] Authentication: API key`)
191
+ } else if (options.context) {
192
+ console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? '(none)'}`)
193
+ console.error(`[MCP Server] Organization: ${options.context.organizationId ?? '(none)'}`)
194
+ console.error(`[MCP Server] User: ${options.context.userId ?? '(superadmin)'}`)
195
+ } else {
196
+ console.error(`[MCP Server] Authentication: none (superadmin mode)`)
197
+ }
198
+
199
+ console.error(`[MCP Server] Tools registered: ${toolCount}`)
200
+
201
+ await server.connect(transport)
202
+
203
+ console.error('[MCP Server] Connected and ready for requests')
204
+
205
+ // Handle shutdown gracefully
206
+ const shutdown = async () => {
207
+ console.error('[MCP Server] Shutting down...')
208
+ await server.close()
209
+ process.exit(0)
210
+ }
211
+
212
+ process.on('SIGINT', shutdown)
213
+ process.on('SIGTERM', shutdown)
214
+ }
@@ -0,0 +1,76 @@
1
+ import { dynamicTool, type Tool } from 'ai'
2
+ import type { InProcessMcpClient, ToolInfoWithSchema } from './in-process-client'
3
+ import { toSafeZodSchema } from './schema-utils'
4
+
5
+ /**
6
+ * Convert MCP tools to Vercel AI SDK format.
7
+ *
8
+ * This adapter takes tools from an MCP client and converts them
9
+ * to the format expected by the AI SDK's streamText function.
10
+ *
11
+ * Uses dynamicTool for dynamic schema support, which allows
12
+ * tools with runtime-determined schemas (from MCP servers).
13
+ *
14
+ * @param mcpClient - MCP client to execute tools
15
+ * @param mcpTools - List of tools with Zod schemas
16
+ * @returns Record of AI SDK tools
17
+ */
18
+ export function convertMcpToolsToAiSdk(
19
+ mcpClient: InProcessMcpClient,
20
+ mcpTools: ToolInfoWithSchema[]
21
+ ): Record<string, Tool<unknown, unknown>> {
22
+ const aiTools: Record<string, Tool<unknown, unknown>> = {}
23
+
24
+ for (const mcpTool of mcpTools) {
25
+ try {
26
+ // Convert schema using Zod4's toJSONSchema with unrepresentable: 'any'
27
+ // This handles Date types by converting them to 'any' in JSON Schema,
28
+ // then we convert back to a clean Zod schema
29
+ const safeSchema = toSafeZodSchema(mcpTool.inputSchema)
30
+
31
+ aiTools[mcpTool.name] = dynamicTool({
32
+ description: mcpTool.description,
33
+ inputSchema: safeSchema,
34
+ execute: async (args: unknown) => {
35
+ const result = await mcpClient.callTool(mcpTool.name, args)
36
+
37
+ if (!result.success) {
38
+ throw new Error(result.error || 'Tool execution failed')
39
+ }
40
+
41
+ // Return the result in a format suitable for LLM consumption
42
+ return formatToolResult(result.result)
43
+ },
44
+ })
45
+ } catch (error) {
46
+ console.error(`[MCP Adapter] Error converting tool "${mcpTool.name}":`, error)
47
+ }
48
+ }
49
+
50
+ return aiTools
51
+ }
52
+
53
+ /**
54
+ * Format tool result for LLM consumption.
55
+ * Converts various result types to a string representation.
56
+ */
57
+ function formatToolResult(result: unknown): string {
58
+ if (result === null || result === undefined) {
59
+ return 'No result returned'
60
+ }
61
+
62
+ if (typeof result === 'string') {
63
+ return result
64
+ }
65
+
66
+ if (typeof result === 'number' || typeof result === 'boolean') {
67
+ return String(result)
68
+ }
69
+
70
+ // For objects and arrays, return JSON representation
71
+ try {
72
+ return JSON.stringify(result, null, 2)
73
+ } catch {
74
+ return String(result)
75
+ }
76
+ }
@@ -0,0 +1,426 @@
1
+ /**
2
+ * OpenCode Agent Client
3
+ *
4
+ * Client for communicating with OpenCode server running in headless mode.
5
+ * OpenCode is used as an AI agent that can execute MCP tools.
6
+ */
7
+
8
+ export type OpenCodeClientConfig = {
9
+ baseUrl: string
10
+ password?: string
11
+ }
12
+
13
+ export type OpenCodeSession = {
14
+ id: string
15
+ slug: string
16
+ version: string
17
+ projectID: string
18
+ directory: string
19
+ title: string
20
+ time: {
21
+ created: number
22
+ updated: number
23
+ }
24
+ }
25
+
26
+ export type OpenCodeMessagePart = {
27
+ type: 'text'
28
+ text: string
29
+ }
30
+
31
+ export type OpenCodeMessageInfo = {
32
+ id: string
33
+ sessionID: string
34
+ role: 'user' | 'assistant'
35
+ time: {
36
+ created: number
37
+ completed?: number
38
+ }
39
+ modelID?: string
40
+ providerID?: string
41
+ tokens?: {
42
+ input: number
43
+ output: number
44
+ }
45
+ error?: {
46
+ name: string
47
+ data: Record<string, unknown>
48
+ }
49
+ }
50
+
51
+ export type OpenCodeMessage = {
52
+ info: OpenCodeMessageInfo
53
+ parts: Array<{
54
+ id: string
55
+ type: string
56
+ text?: string
57
+ [key: string]: unknown
58
+ }>
59
+ }
60
+
61
+ export type OpenCodeHealth = {
62
+ healthy: boolean
63
+ version: string
64
+ }
65
+
66
+ export type OpenCodeMcpStatus = Record<
67
+ string,
68
+ {
69
+ status: 'connected' | 'failed' | 'connecting'
70
+ error?: string
71
+ }
72
+ >
73
+
74
+ export type OpenCodeQuestionOption = {
75
+ label: string
76
+ description: string
77
+ }
78
+
79
+ export type OpenCodeQuestion = {
80
+ id: string
81
+ sessionID: string
82
+ questions: Array<{
83
+ question: string
84
+ header: string
85
+ options: OpenCodeQuestionOption[]
86
+ }>
87
+ tool: {
88
+ messageID: string
89
+ callID: string
90
+ }
91
+ }
92
+
93
+ /**
94
+ * SSE Event from OpenCode event stream.
95
+ */
96
+ export type OpenCodeSSEEvent = {
97
+ type: string
98
+ properties: Record<string, unknown>
99
+ }
100
+
101
+ /**
102
+ * Callback for SSE events.
103
+ */
104
+ export type OpenCodeSSECallback = (event: OpenCodeSSEEvent) => void
105
+
106
+ /**
107
+ * Client for OpenCode server API.
108
+ */
109
+ export class OpenCodeClient {
110
+ private baseUrl: string
111
+ private headers: Record<string, string>
112
+
113
+ constructor(config: OpenCodeClientConfig) {
114
+ this.baseUrl = config.baseUrl.replace(/\/$/, '')
115
+ this.headers = {
116
+ 'Content-Type': 'application/json',
117
+ }
118
+
119
+ if (config.password) {
120
+ const credentials = Buffer.from(`opencode:${config.password}`).toString('base64')
121
+ this.headers['Authorization'] = `Basic ${credentials}`
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Subscribe to SSE event stream for real-time updates.
127
+ * Returns an abort function to stop the stream.
128
+ */
129
+ subscribeToEvents(
130
+ onEvent: OpenCodeSSECallback,
131
+ onError?: (error: Error) => void
132
+ ): () => void {
133
+ const controller = new AbortController()
134
+
135
+ const connect = async () => {
136
+ try {
137
+ const res = await fetch(`${this.baseUrl}/event`, {
138
+ headers: {
139
+ ...this.headers,
140
+ Accept: 'text/event-stream',
141
+ },
142
+ signal: controller.signal,
143
+ })
144
+
145
+ if (!res.ok || !res.body) {
146
+ throw new Error(`SSE connection failed: ${res.status}`)
147
+ }
148
+
149
+ const reader = res.body.getReader()
150
+ const decoder = new TextDecoder()
151
+ let buffer = ''
152
+
153
+ while (true) {
154
+ const { done, value } = await reader.read()
155
+ if (done) break
156
+
157
+ buffer += decoder.decode(value, { stream: true })
158
+
159
+ // Process complete SSE messages
160
+ const lines = buffer.split('\n')
161
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
162
+
163
+ for (const line of lines) {
164
+ if (line.startsWith('data: ')) {
165
+ try {
166
+ const data = JSON.parse(line.slice(6))
167
+ onEvent(data)
168
+ } catch {
169
+ // Ignore parse errors
170
+ }
171
+ }
172
+ }
173
+ }
174
+ } catch (error) {
175
+ if ((error as Error).name !== 'AbortError') {
176
+ onError?.(error as Error)
177
+ }
178
+ }
179
+ }
180
+
181
+ connect()
182
+
183
+ return () => controller.abort()
184
+ }
185
+
186
+ /**
187
+ * Check OpenCode server health.
188
+ */
189
+ async health(): Promise<OpenCodeHealth> {
190
+ const res = await fetch(`${this.baseUrl}/global/health`, {
191
+ headers: this.headers,
192
+ })
193
+
194
+ if (!res.ok) {
195
+ throw new Error(`Health check failed: ${res.status}`)
196
+ }
197
+
198
+ return res.json()
199
+ }
200
+
201
+ /**
202
+ * Get MCP server connection status.
203
+ */
204
+ async mcpStatus(): Promise<OpenCodeMcpStatus> {
205
+ const res = await fetch(`${this.baseUrl}/mcp`, {
206
+ headers: this.headers,
207
+ })
208
+
209
+ if (!res.ok) {
210
+ throw new Error(`MCP status check failed: ${res.status}`)
211
+ }
212
+
213
+ return res.json()
214
+ }
215
+
216
+ /**
217
+ * Create a new conversation session.
218
+ */
219
+ async createSession(): Promise<OpenCodeSession> {
220
+ const res = await fetch(`${this.baseUrl}/session`, {
221
+ method: 'POST',
222
+ headers: this.headers,
223
+ body: JSON.stringify({}),
224
+ })
225
+
226
+ if (!res.ok) {
227
+ const error = await res.text()
228
+ throw new Error(`Failed to create session: ${error}`)
229
+ }
230
+
231
+ return res.json()
232
+ }
233
+
234
+ /**
235
+ * Get an existing session by ID.
236
+ */
237
+ async getSession(sessionId: string): Promise<OpenCodeSession> {
238
+ const res = await fetch(`${this.baseUrl}/session/${sessionId}`, {
239
+ headers: this.headers,
240
+ })
241
+
242
+ if (!res.ok) {
243
+ throw new Error(`Failed to get session: ${res.status}`)
244
+ }
245
+
246
+ return res.json()
247
+ }
248
+
249
+ /**
250
+ * Send a message to a session and wait for response.
251
+ */
252
+ async sendMessage(
253
+ sessionId: string,
254
+ message: string,
255
+ options?: {
256
+ model?: { providerID: string; modelID: string }
257
+ }
258
+ ): Promise<OpenCodeMessage> {
259
+ const body: Record<string, unknown> = {
260
+ parts: [{ type: 'text', text: message }],
261
+ }
262
+
263
+ if (options?.model) {
264
+ body.model = options.model
265
+ }
266
+
267
+ const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`, {
268
+ method: 'POST',
269
+ headers: this.headers,
270
+ body: JSON.stringify(body),
271
+ })
272
+
273
+ if (!res.ok) {
274
+ const error = await res.text()
275
+ throw new Error(`Failed to send message: ${error}`)
276
+ }
277
+
278
+ return res.json()
279
+ }
280
+
281
+ /**
282
+ * Set authentication credentials for a provider.
283
+ */
284
+ async setAuth(providerId: string, apiKey: string): Promise<void> {
285
+ const res = await fetch(`${this.baseUrl}/auth/${providerId}`, {
286
+ method: 'PUT',
287
+ headers: this.headers,
288
+ body: JSON.stringify({ type: 'api', key: apiKey }),
289
+ })
290
+
291
+ if (!res.ok) {
292
+ throw new Error(`Failed to set auth: ${res.status}`)
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Get current configuration.
298
+ */
299
+ async getConfig(): Promise<Record<string, unknown>> {
300
+ const res = await fetch(`${this.baseUrl}/config`, {
301
+ headers: this.headers,
302
+ })
303
+
304
+ if (!res.ok) {
305
+ throw new Error(`Failed to get config: ${res.status}`)
306
+ }
307
+
308
+ return res.json()
309
+ }
310
+
311
+ /**
312
+ * Get pending questions that need user response.
313
+ */
314
+ async getPendingQuestions(): Promise<OpenCodeQuestion[]> {
315
+ const res = await fetch(`${this.baseUrl}/question`, {
316
+ headers: this.headers,
317
+ })
318
+
319
+ if (!res.ok) {
320
+ throw new Error(`Failed to get questions: ${res.status}`)
321
+ }
322
+
323
+ return res.json()
324
+ }
325
+
326
+ /**
327
+ * Answer a pending question.
328
+ * OpenCode expects: POST /question/{requestID}/reply with { answers: [["label"]] }
329
+ * Each answer is an array of selected option labels (for multi-select support).
330
+ */
331
+ async answerQuestion(questionId: string, answerIndex: number): Promise<void> {
332
+ // First get the question to find the selected option label
333
+ const questions = await this.getPendingQuestions()
334
+ const question = questions.find((q) => q.id === questionId)
335
+
336
+ if (!question) {
337
+ throw new Error(`Question ${questionId} not found`)
338
+ }
339
+
340
+ // Build answers array - each question's answer is an array of selected labels
341
+ const answers: string[][] = []
342
+ for (const q of question.questions) {
343
+ const selectedOption = q.options[answerIndex]
344
+ if (selectedOption) {
345
+ // Each answer is an array of selected labels (supports multi-select)
346
+ answers.push([selectedOption.label])
347
+ }
348
+ }
349
+
350
+ const body = { answers }
351
+
352
+ console.log('[OpenCode Client] Answering question', questionId, 'with body:', JSON.stringify(body))
353
+
354
+ const res = await fetch(`${this.baseUrl}/question/${questionId}/reply`, {
355
+ method: 'POST',
356
+ headers: this.headers,
357
+ body: JSON.stringify(body),
358
+ })
359
+
360
+ const responseText = await res.text()
361
+ console.log('[OpenCode Client] Answer response:', res.status, responseText.substring(0, 200))
362
+
363
+ if (!res.ok) {
364
+ throw new Error(`Failed to answer question: ${res.status} - ${responseText}`)
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Reject a pending question.
370
+ */
371
+ async rejectQuestion(questionId: string): Promise<void> {
372
+ console.log('[OpenCode Client] Rejecting question', questionId)
373
+
374
+ const res = await fetch(`${this.baseUrl}/question/${questionId}/reject`, {
375
+ method: 'POST',
376
+ headers: this.headers,
377
+ })
378
+
379
+ if (!res.ok) {
380
+ const responseText = await res.text()
381
+ throw new Error(`Failed to reject question: ${res.status} - ${responseText}`)
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get session status (idle, busy, waiting for question).
387
+ * Falls back to inferring status from pending questions if endpoint doesn't exist.
388
+ */
389
+ async getSessionStatus(sessionId: string): Promise<{ status: string; questionId?: string }> {
390
+ try {
391
+ const res = await fetch(`${this.baseUrl}/session/${sessionId}/status`, {
392
+ headers: this.headers,
393
+ })
394
+
395
+ if (res.ok) {
396
+ const contentType = res.headers.get('content-type')
397
+ if (contentType && contentType.includes('application/json')) {
398
+ return res.json()
399
+ }
400
+ }
401
+ } catch {
402
+ // Endpoint doesn't exist or network error - fall through to inference
403
+ }
404
+
405
+ // Fall back to inferring status from pending questions
406
+ // Note: We can't tell if OpenCode is busy without the status endpoint
407
+ // Return 'unknown' to let SSE events determine actual state
408
+ const questions = await this.getPendingQuestions()
409
+ const sessionQuestion = questions.find((q) => q.sessionID === sessionId)
410
+ if (sessionQuestion) {
411
+ return { status: 'waiting', questionId: sessionQuestion.id }
412
+ }
413
+ // Don't assume idle - we can't know without SSE events
414
+ return { status: 'unknown' }
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Create an OpenCode client with default configuration from environment.
420
+ */
421
+ export function createOpenCodeClient(config?: Partial<OpenCodeClientConfig>): OpenCodeClient {
422
+ return new OpenCodeClient({
423
+ baseUrl: config?.baseUrl ?? process.env.OPENCODE_URL ?? 'http://localhost:4096',
424
+ password: config?.password ?? process.env.OPENCODE_PASSWORD,
425
+ })
426
+ }