@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.
- package/AGENTS.md +1090 -0
- package/README.md +607 -0
- package/build.mjs +92 -0
- package/dist/di.js +8 -0
- package/dist/di.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
- package/dist/frontend/components/CommandPalette/index.js +28 -0
- package/dist/frontend/components/CommandPalette/index.js.map +7 -0
- package/dist/frontend/constants.js +41 -0
- package/dist/frontend/constants.js.map +7 -0
- package/dist/frontend/hooks/index.js +13 -0
- package/dist/frontend/hooks/index.js.map +7 -0
- package/dist/frontend/hooks/useCommandPalette.js +1094 -0
- package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
- package/dist/frontend/hooks/useMcpTools.js +66 -0
- package/dist/frontend/hooks/useMcpTools.js.map +7 -0
- package/dist/frontend/hooks/usePageContext.js +48 -0
- package/dist/frontend/hooks/usePageContext.js.map +7 -0
- package/dist/frontend/hooks/useRecentActions.js +56 -0
- package/dist/frontend/hooks/useRecentActions.js.map +7 -0
- package/dist/frontend/hooks/useRecentTools.js +55 -0
- package/dist/frontend/hooks/useRecentTools.js.map +7 -0
- package/dist/frontend/index.js +35 -0
- package/dist/frontend/index.js.map +7 -0
- package/dist/frontend/types.js +1 -0
- package/dist/frontend/types.js.map +7 -0
- package/dist/frontend/utils/index.js +7 -0
- package/dist/frontend/utils/index.js.map +7 -0
- package/dist/frontend/utils/toolMatcher.js +95 -0
- package/dist/frontend/utils/toolMatcher.js.map +7 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/ai_assistant/acl.js +14 -0
- package/dist/modules/ai_assistant/acl.js.map +7 -0
- package/dist/modules/ai_assistant/api/chat/route.js +152 -0
- package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/health/route.js +27 -0
- package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/route/route.js +123 -0
- package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +60 -0
- package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/route.js +48 -0
- package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +192 -0
- package/dist/modules/ai_assistant/cli.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +11 -0
- package/dist/modules/ai_assistant/di.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/index.js +13 -0
- package/dist/modules/ai_assistant/index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/auth.js +87 -0
- package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
- package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
- package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
- package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/http-server.js +367 -0
- package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
- package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
- package/dist/modules/ai_assistant/lib/types.js +1 -0
- package/dist/modules/ai_assistant/lib/types.js.map +7 -0
- package/package.json +108 -0
- package/src/di.ts +11 -0
- package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
- package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
- package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
- package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
- package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
- package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
- package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
- package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
- package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
- package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
- package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
- package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
- package/src/frontend/components/CommandPalette/index.ts +14 -0
- package/src/frontend/constants.ts +35 -0
- package/src/frontend/hooks/index.ts +5 -0
- package/src/frontend/hooks/useCommandPalette.ts +1389 -0
- package/src/frontend/hooks/useMcpTools.ts +73 -0
- package/src/frontend/hooks/usePageContext.ts +61 -0
- package/src/frontend/hooks/useRecentActions.ts +64 -0
- package/src/frontend/hooks/useRecentTools.ts +69 -0
- package/src/frontend/index.ts +39 -0
- package/src/frontend/types.ts +260 -0
- package/src/frontend/utils/index.ts +1 -0
- package/src/frontend/utils/toolMatcher.ts +127 -0
- package/src/index.ts +92 -0
- package/src/modules/ai_assistant/acl.ts +10 -0
- package/src/modules/ai_assistant/api/chat/route.ts +213 -0
- package/src/modules/ai_assistant/api/health/route.ts +30 -0
- package/src/modules/ai_assistant/api/route/route.ts +149 -0
- package/src/modules/ai_assistant/api/settings/route.ts +73 -0
- package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
- package/src/modules/ai_assistant/api/tools/route.ts +57 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +233 -0
- package/src/modules/ai_assistant/di.ts +9 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
- package/src/modules/ai_assistant/index.ts +11 -0
- package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
- package/src/modules/ai_assistant/lib/auth.ts +185 -0
- package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
- package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
- package/src/modules/ai_assistant/lib/http-server.ts +498 -0
- package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
- package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
- package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
- package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
- package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
- package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
- package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
- package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
- package/src/modules/ai_assistant/lib/types.ts +147 -0
- package/test-schema.ts +37 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|