@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,373 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
4
+ import { z, type ZodType } from 'zod'
5
+ import { getToolRegistry } from './tool-registry'
6
+ import { executeTool } from './tool-executor'
7
+ import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
8
+ import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'
9
+ import { jsonSchemaToZod } from './schema-utils'
10
+ import type { McpToolContext } from './types'
11
+ import type { SearchService } from '@open-mercato/search/service'
12
+
13
+ const DEFAULT_PORT = 3001
14
+
15
+ const log = (message: string, ...args: unknown[]) => {
16
+ console.error(`[MCP Dev] ${message}`, ...args)
17
+ }
18
+
19
+ async function getApiKeyFromMcpJson(): Promise<string | undefined> {
20
+ const { readFile } = await import('node:fs/promises')
21
+ const { resolve } = await import('node:path')
22
+
23
+ try {
24
+ const mcpJsonPath = resolve(process.cwd(), '.mcp.json')
25
+ const content = await readFile(mcpJsonPath, 'utf-8')
26
+ const config = JSON.parse(content)
27
+ const serverConfig = config?.mcpServers?.['open-mercato']
28
+
29
+ // Check env.OPEN_MERCATO_API_KEY first, then headers.x-api-key (HTTP style)
30
+ return serverConfig?.env?.OPEN_MERCATO_API_KEY ?? serverConfig?.headers?.['x-api-key']
31
+ } catch {
32
+ return undefined
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Maximum request body size (1MB).
38
+ */
39
+ const MAX_BODY_SIZE = 1 * 1024 * 1024
40
+
41
+ /**
42
+ * Parse JSON body from request with size limit.
43
+ */
44
+ async function parseJsonBody(req: IncomingMessage): Promise<unknown> {
45
+ return new Promise((resolve, reject) => {
46
+ const chunks: Buffer[] = []
47
+ let totalSize = 0
48
+
49
+ req.on('data', (chunk: Buffer) => {
50
+ totalSize += chunk.length
51
+ if (totalSize > MAX_BODY_SIZE) {
52
+ req.destroy()
53
+ reject(new Error('Request payload too large'))
54
+ return
55
+ }
56
+ chunks.push(chunk)
57
+ })
58
+ req.on('end', () => {
59
+ try {
60
+ const body = Buffer.concat(chunks).toString('utf-8')
61
+ resolve(body ? JSON.parse(body) : undefined)
62
+ } catch (error) {
63
+ reject(error)
64
+ }
65
+ })
66
+ req.on('error', reject)
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Create MCP server with tools pre-authenticated for dev use.
72
+ * No session tokens required - uses API key authentication directly.
73
+ */
74
+ function createDevMcpServer(
75
+ toolContext: McpToolContext,
76
+ authFeatures: string[],
77
+ isSuperAdmin: boolean,
78
+ debug: boolean
79
+ ): McpServer {
80
+ const server = new McpServer(
81
+ { name: 'open-mercato-mcp-dev', version: '0.1.0' },
82
+ { capabilities: { tools: {} } }
83
+ )
84
+
85
+ const registry = getToolRegistry()
86
+ const tools = Array.from(registry.getTools().values())
87
+
88
+ // Filter tools based on API key permissions
89
+ const accessibleTools = tools.filter((tool) =>
90
+ hasRequiredFeatures(tool.requiredFeatures, authFeatures, isSuperAdmin)
91
+ )
92
+
93
+ if (debug) {
94
+ log(`Registering ${accessibleTools.length}/${tools.length} tools (filtered by API key permissions)`)
95
+ }
96
+
97
+ for (const tool of accessibleTools) {
98
+ if (debug) {
99
+ log(`Registering tool: ${tool.name}`)
100
+ }
101
+
102
+ // Convert Zod schema to safe schema without Date types
103
+ let safeSchema: ZodType | undefined
104
+ if (tool.inputSchema) {
105
+ try {
106
+ const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>
107
+ const converted = jsonSchemaToZod(jsonSchema)
108
+ safeSchema = (converted as z.ZodObject<any>).passthrough()
109
+ } catch (error) {
110
+ if (debug) {
111
+ log(`Skipping tool ${tool.name} - schema conversion failed:`, error instanceof Error ? error.message : error)
112
+ }
113
+ continue
114
+ }
115
+ } else {
116
+ safeSchema = z.object({}).passthrough()
117
+ }
118
+
119
+ try {
120
+ server.registerTool(
121
+ tool.name,
122
+ {
123
+ description: tool.description,
124
+ inputSchema: safeSchema,
125
+ },
126
+ async (args: unknown) => {
127
+ const toolArgs = (args ?? {}) as Record<string, unknown>
128
+
129
+ if (debug) {
130
+ log(`Calling tool: ${tool.name}`, JSON.stringify(toolArgs))
131
+ }
132
+
133
+ const result = await executeTool(tool.name, toolArgs, toolContext)
134
+
135
+ if (!result.success) {
136
+ log(`Tool error: ${result.error}`)
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text' as const,
141
+ text: JSON.stringify({ error: result.error, code: result.errorCode }),
142
+ },
143
+ ],
144
+ isError: true,
145
+ }
146
+ }
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text' as const,
152
+ text: JSON.stringify(result.result, null, 2),
153
+ },
154
+ ],
155
+ }
156
+ }
157
+ )
158
+ } catch (error) {
159
+ if (debug) {
160
+ log(`Skipping tool ${tool.name} - registration failed:`, error instanceof Error ? error.message : error)
161
+ }
162
+ continue
163
+ }
164
+ }
165
+
166
+ return server
167
+ }
168
+
169
+ /**
170
+ * Development MCP server for Claude Code integration.
171
+ *
172
+ * This server uses HTTP transport and authenticates via the
173
+ * OPEN_MERCATO_API_KEY environment variable, .mcp.json file,
174
+ * or x-api-key header.
175
+ *
176
+ * Usage:
177
+ * OPEN_MERCATO_API_KEY=omk_xxx yarn mcp:dev
178
+ *
179
+ * Or configure in .mcp.json for Claude Code with HTTP transport.
180
+ */
181
+ export async function runMcpDevServer(): Promise<void> {
182
+ const envApiKey = process.env.OPEN_MERCATO_API_KEY || (await getApiKeyFromMcpJson())
183
+ const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT
184
+ const debug = process.env.MCP_DEBUG === 'true'
185
+
186
+ if (!envApiKey) {
187
+ log('Error: OPEN_MERCATO_API_KEY environment variable is required')
188
+ log('')
189
+ log('To get an API key:')
190
+ log(' 1. Log into Open Mercato as an admin')
191
+ log(' 2. Go to Settings > API Keys')
192
+ log(' 3. Create a new key with the required permissions')
193
+ log('')
194
+ log('Then either:')
195
+ log(' - Set environment variable: export OPEN_MERCATO_API_KEY=omk_xxx...')
196
+ log(' - Or configure in .mcp.json with headers.x-api-key')
197
+ process.exit(1)
198
+ }
199
+
200
+ log('Starting development MCP HTTP server...')
201
+
202
+ // Create DI container
203
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
204
+ const container = await createRequestContainer()
205
+
206
+ // Authenticate the API key upfront
207
+ log('Authenticating API key...')
208
+ const authResult = await authenticateMcpRequest(envApiKey, container)
209
+
210
+ if (!authResult.success) {
211
+ log(`Authentication failed: ${authResult.error}`)
212
+ process.exit(1)
213
+ }
214
+
215
+ log(`Authenticated as: ${authResult.keyName}`)
216
+ log(`Tenant: ${authResult.tenantId ?? '(global)'}`)
217
+ log(`Organization: ${authResult.organizationId ?? '(none)'}`)
218
+ log(`Super admin: ${authResult.isSuperAdmin}`)
219
+ log(`Features: ${authResult.features.length > 0 ? authResult.features.join(', ') : '(none)'}`)
220
+
221
+ // Load tools
222
+ log('Loading tools...')
223
+ await loadAllModuleTools()
224
+
225
+ // Index tools for search (if search service available)
226
+ try {
227
+ const searchService = container.resolve('searchService') as SearchService
228
+ await indexToolsForSearch(searchService)
229
+
230
+ const { indexApiEndpoints } = await import('./api-endpoint-index')
231
+ const endpointCount = await indexApiEndpoints(searchService)
232
+ if (endpointCount > 0) {
233
+ log(`Indexed ${endpointCount} API endpoints for discovery`)
234
+ }
235
+ } catch {
236
+ log('Search indexing skipped (search service not available)')
237
+ }
238
+
239
+ // Create tool context from auth result
240
+ const toolContext: McpToolContext = {
241
+ tenantId: authResult.tenantId,
242
+ organizationId: authResult.organizationId,
243
+ userId: authResult.userId,
244
+ container,
245
+ userFeatures: authResult.features,
246
+ isSuperAdmin: authResult.isSuperAdmin,
247
+ apiKeySecret: envApiKey,
248
+ }
249
+
250
+ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
251
+ const url = new URL(req.url || '/', `http://localhost:${port}`)
252
+
253
+ // Health check endpoint
254
+ if (url.pathname === '/health') {
255
+ res.writeHead(200, { 'Content-Type': 'application/json' })
256
+ res.end(JSON.stringify({
257
+ status: 'ok',
258
+ mode: 'development',
259
+ tools: getToolRegistry().listToolNames().length,
260
+ tenant: authResult.tenantId,
261
+ timestamp: new Date().toISOString(),
262
+ }))
263
+ return
264
+ }
265
+
266
+ if (url.pathname !== '/mcp') {
267
+ res.writeHead(404, { 'Content-Type': 'application/json' })
268
+ res.end(JSON.stringify({ error: 'Not found' }))
269
+ return
270
+ }
271
+
272
+ // Extract and validate API key from header
273
+ const headers: Record<string, string | undefined> = {}
274
+ for (const [key, value] of Object.entries(req.headers)) {
275
+ headers[key] = Array.isArray(value) ? value[0] : value
276
+ }
277
+
278
+ const providedApiKey = extractApiKeyFromHeaders(headers)
279
+ if (!providedApiKey) {
280
+ res.writeHead(401, { 'Content-Type': 'application/json' })
281
+ res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))
282
+ return
283
+ }
284
+
285
+ // Validate against the configured API key
286
+ if (providedApiKey !== envApiKey) {
287
+ res.writeHead(401, { 'Content-Type': 'application/json' })
288
+ res.end(JSON.stringify({ error: 'Invalid API key' }))
289
+ return
290
+ }
291
+
292
+ if (debug) {
293
+ log(`Authenticated request (${req.method})`)
294
+ }
295
+
296
+ try {
297
+ // Create stateless transport
298
+ const transport = new StreamableHTTPServerTransport({
299
+ sessionIdGenerator: undefined,
300
+ enableJsonResponse: req.method === 'POST',
301
+ })
302
+
303
+ // Create server with pre-authenticated context (no session tokens needed)
304
+ const mcpServer = createDevMcpServer(toolContext, authResult.features, authResult.isSuperAdmin, debug)
305
+
306
+ // Connect server to transport
307
+ await mcpServer.connect(transport)
308
+
309
+ // Handle the request
310
+ if (req.method === 'POST') {
311
+ const body = await parseJsonBody(req)
312
+ await transport.handleRequest(req, res, body)
313
+ } else {
314
+ await transport.handleRequest(req, res)
315
+ }
316
+
317
+ // Cleanup after response finishes
318
+ res.on('finish', () => {
319
+ transport.close()
320
+ mcpServer.close()
321
+ if (debug) {
322
+ log(`Request completed, cleaned up`)
323
+ }
324
+ })
325
+ } catch (error) {
326
+ log('Error handling request:', error)
327
+ if (!res.headersSent) {
328
+ if (error instanceof Error && error.message === 'Request payload too large') {
329
+ res.writeHead(413, { 'Content-Type': 'application/json' })
330
+ res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))
331
+ return
332
+ }
333
+
334
+ res.writeHead(500, { 'Content-Type': 'application/json' })
335
+ res.end(
336
+ JSON.stringify({
337
+ jsonrpc: '2.0',
338
+ error: {
339
+ code: -32603,
340
+ message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
341
+ },
342
+ id: null,
343
+ })
344
+ )
345
+ }
346
+ }
347
+ })
348
+
349
+ const toolCount = getToolRegistry().listToolNames().length
350
+
351
+ log(`Tools registered: ${toolCount}`)
352
+ log(`Endpoint: http://localhost:${port}/mcp`)
353
+ log(`Health: http://localhost:${port}/health`)
354
+ log(`Mode: Development (API key auth, no session tokens)`)
355
+
356
+ return new Promise<void>((resolve) => {
357
+ httpServer.listen(port, () => {
358
+ log(`Server listening on port ${port}`)
359
+ log('Ready for Claude Code connections')
360
+ })
361
+
362
+ const shutdown = async () => {
363
+ log('Shutting down...')
364
+ httpServer.close(() => {
365
+ log('Server closed')
366
+ resolve()
367
+ })
368
+ }
369
+
370
+ process.on('SIGINT', shutdown)
371
+ process.on('SIGTERM', shutdown)
372
+ })
373
+ }
@@ -0,0 +1,287 @@
1
+ import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
2
+
3
+ // Types
4
+
5
+ /**
6
+ * MCP server connection type.
7
+ */
8
+ export type McpServerType = 'http' | 'stdio'
9
+
10
+ /**
11
+ * Configuration for an external MCP server.
12
+ */
13
+ export interface McpServerConfig {
14
+ /** Unique identifier */
15
+ id: string
16
+ /** User-defined name */
17
+ name: string
18
+ /** Connection type */
19
+ type: McpServerType
20
+ /** Server URL (for HTTP type) */
21
+ url?: string
22
+ /** Command to run (for stdio type) */
23
+ command?: string
24
+ /** Command arguments (for stdio type) */
25
+ args?: string[]
26
+ /** API key for authentication (stored as reference, not the actual secret) */
27
+ apiKeyId?: string
28
+ /** Whether the server is enabled */
29
+ enabled: boolean
30
+ /** When the config was created */
31
+ createdAt: string
32
+ /** When the config was last updated */
33
+ updatedAt: string
34
+ }
35
+
36
+ /**
37
+ * Input for creating a new MCP server config.
38
+ */
39
+ export type McpServerConfigInput = Omit<McpServerConfig, 'id' | 'createdAt' | 'updatedAt'>
40
+
41
+ /**
42
+ * Input for updating an MCP server config.
43
+ */
44
+ export type McpServerConfigUpdate = Partial<Omit<McpServerConfig, 'id' | 'createdAt' | 'updatedAt'>>
45
+
46
+ // Constants
47
+
48
+ export const MCP_SERVERS_CONFIG_KEY = 'mcp_servers'
49
+
50
+ // Resolver type
51
+
52
+ type Resolver = {
53
+ resolve: <T = unknown>(name: string) => T
54
+ }
55
+
56
+ // Config functions
57
+
58
+ /**
59
+ * Get all MCP server configurations.
60
+ */
61
+ export async function getMcpServerConfigs(
62
+ resolver: Resolver
63
+ ): Promise<McpServerConfig[]> {
64
+ let service: ModuleConfigService
65
+ try {
66
+ service = resolver.resolve<ModuleConfigService>('moduleConfigService')
67
+ } catch {
68
+ return []
69
+ }
70
+
71
+ try {
72
+ const value = await service.getValue<McpServerConfig[]>(
73
+ 'ai_assistant',
74
+ MCP_SERVERS_CONFIG_KEY,
75
+ { defaultValue: [] }
76
+ )
77
+ return value ?? []
78
+ } catch {
79
+ return []
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get a single MCP server configuration by ID.
85
+ */
86
+ export async function getMcpServerConfig(
87
+ resolver: Resolver,
88
+ serverId: string
89
+ ): Promise<McpServerConfig | null> {
90
+ const configs = await getMcpServerConfigs(resolver)
91
+ return configs.find((c) => c.id === serverId) ?? null
92
+ }
93
+
94
+ /**
95
+ * Get only enabled MCP server configurations.
96
+ */
97
+ export async function getEnabledMcpServerConfigs(
98
+ resolver: Resolver
99
+ ): Promise<McpServerConfig[]> {
100
+ const configs = await getMcpServerConfigs(resolver)
101
+ return configs.filter((c) => c.enabled)
102
+ }
103
+
104
+ /**
105
+ * Save an MCP server configuration (create or update).
106
+ */
107
+ export async function saveMcpServerConfig(
108
+ resolver: Resolver,
109
+ config: McpServerConfigInput & { id?: string }
110
+ ): Promise<McpServerConfig> {
111
+ let service: ModuleConfigService
112
+ try {
113
+ service = resolver.resolve<ModuleConfigService>('moduleConfigService')
114
+ } catch {
115
+ throw new Error('Configuration service unavailable')
116
+ }
117
+
118
+ const configs = await getMcpServerConfigs(resolver)
119
+ const now = new Date().toISOString()
120
+
121
+ let updatedConfigs: McpServerConfig[]
122
+ let savedConfig: McpServerConfig
123
+
124
+ if (config.id) {
125
+ // Update existing
126
+ const existingIndex = configs.findIndex((c) => c.id === config.id)
127
+ if (existingIndex === -1) {
128
+ throw new Error(`MCP server config not found: ${config.id}`)
129
+ }
130
+
131
+ savedConfig = {
132
+ ...configs[existingIndex],
133
+ ...config,
134
+ id: config.id,
135
+ updatedAt: now,
136
+ }
137
+
138
+ updatedConfigs = [
139
+ ...configs.slice(0, existingIndex),
140
+ savedConfig,
141
+ ...configs.slice(existingIndex + 1),
142
+ ]
143
+ } else {
144
+ // Create new
145
+ savedConfig = {
146
+ ...config,
147
+ id: generateId(),
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ }
151
+
152
+ updatedConfigs = [...configs, savedConfig]
153
+ }
154
+
155
+ await service.setValue('ai_assistant', MCP_SERVERS_CONFIG_KEY, updatedConfigs)
156
+ return savedConfig
157
+ }
158
+
159
+ /**
160
+ * Update an existing MCP server configuration.
161
+ */
162
+ export async function updateMcpServerConfig(
163
+ resolver: Resolver,
164
+ serverId: string,
165
+ updates: McpServerConfigUpdate
166
+ ): Promise<McpServerConfig> {
167
+ let service: ModuleConfigService
168
+ try {
169
+ service = resolver.resolve<ModuleConfigService>('moduleConfigService')
170
+ } catch {
171
+ throw new Error('Configuration service unavailable')
172
+ }
173
+
174
+ const configs = await getMcpServerConfigs(resolver)
175
+ const existingIndex = configs.findIndex((c) => c.id === serverId)
176
+
177
+ if (existingIndex === -1) {
178
+ throw new Error(`MCP server config not found: ${serverId}`)
179
+ }
180
+
181
+ const updatedConfig: McpServerConfig = {
182
+ ...configs[existingIndex],
183
+ ...updates,
184
+ id: serverId, // Ensure ID is preserved
185
+ updatedAt: new Date().toISOString(),
186
+ }
187
+
188
+ const updatedConfigs = [
189
+ ...configs.slice(0, existingIndex),
190
+ updatedConfig,
191
+ ...configs.slice(existingIndex + 1),
192
+ ]
193
+
194
+ await service.setValue('ai_assistant', MCP_SERVERS_CONFIG_KEY, updatedConfigs)
195
+ return updatedConfig
196
+ }
197
+
198
+ /**
199
+ * Delete an MCP server configuration.
200
+ */
201
+ export async function deleteMcpServerConfig(
202
+ resolver: Resolver,
203
+ serverId: string
204
+ ): Promise<boolean> {
205
+ let service: ModuleConfigService
206
+ try {
207
+ service = resolver.resolve<ModuleConfigService>('moduleConfigService')
208
+ } catch {
209
+ throw new Error('Configuration service unavailable')
210
+ }
211
+
212
+ const configs = await getMcpServerConfigs(resolver)
213
+ const existingIndex = configs.findIndex((c) => c.id === serverId)
214
+
215
+ if (existingIndex === -1) {
216
+ return false
217
+ }
218
+
219
+ const updatedConfigs = [
220
+ ...configs.slice(0, existingIndex),
221
+ ...configs.slice(existingIndex + 1),
222
+ ]
223
+
224
+ await service.setValue('ai_assistant', MCP_SERVERS_CONFIG_KEY, updatedConfigs)
225
+ return true
226
+ }
227
+
228
+ /**
229
+ * Toggle the enabled state of an MCP server configuration.
230
+ */
231
+ export async function toggleMcpServerEnabled(
232
+ resolver: Resolver,
233
+ serverId: string
234
+ ): Promise<McpServerConfig> {
235
+ const config = await getMcpServerConfig(resolver, serverId)
236
+ if (!config) {
237
+ throw new Error(`MCP server config not found: ${serverId}`)
238
+ }
239
+
240
+ return updateMcpServerConfig(resolver, serverId, {
241
+ enabled: !config.enabled,
242
+ })
243
+ }
244
+
245
+ // Helpers
246
+
247
+ /**
248
+ * Generate a unique ID for a new MCP server config.
249
+ */
250
+ function generateId(): string {
251
+ return `mcp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`
252
+ }
253
+
254
+ /**
255
+ * Validate an MCP server configuration.
256
+ */
257
+ export function validateMcpServerConfig(
258
+ config: McpServerConfigInput
259
+ ): { valid: boolean; error?: string } {
260
+ if (!config.name?.trim()) {
261
+ return { valid: false, error: 'Name is required' }
262
+ }
263
+
264
+ if (!['http', 'stdio'].includes(config.type)) {
265
+ return { valid: false, error: 'Type must be "http" or "stdio"' }
266
+ }
267
+
268
+ if (config.type === 'http') {
269
+ if (!config.url?.trim()) {
270
+ return { valid: false, error: 'URL is required for HTTP servers' }
271
+ }
272
+
273
+ try {
274
+ new URL(config.url)
275
+ } catch {
276
+ return { valid: false, error: 'Invalid URL format' }
277
+ }
278
+ }
279
+
280
+ if (config.type === 'stdio') {
281
+ if (!config.command?.trim()) {
282
+ return { valid: false, error: 'Command is required for stdio servers' }
283
+ }
284
+ }
285
+
286
+ return { valid: true }
287
+ }