@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,130 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import type { McpClientInterface } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client connection mode.
|
|
6
|
+
*/
|
|
7
|
+
export type ClientMode = 'in-process' | 'stdio' | 'http'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for creating an MCP client.
|
|
11
|
+
*/
|
|
12
|
+
export type CreateClientOptions = {
|
|
13
|
+
/** Connection mode */
|
|
14
|
+
mode: ClientMode
|
|
15
|
+
/** API key secret for authentication */
|
|
16
|
+
apiKeySecret: string
|
|
17
|
+
/** DI container (required for in-process mode) */
|
|
18
|
+
container?: AwilixContainer
|
|
19
|
+
/** HTTP server URL (required for http mode) */
|
|
20
|
+
httpUrl?: string
|
|
21
|
+
/** Custom command for stdio mode (default: 'yarn') */
|
|
22
|
+
stdioCommand?: string
|
|
23
|
+
/** Custom args for stdio mode (default: mercato mcp:serve) */
|
|
24
|
+
stdioArgs?: string[]
|
|
25
|
+
/** Working directory for stdio mode */
|
|
26
|
+
cwd?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create an MCP client with the specified connection mode.
|
|
31
|
+
*
|
|
32
|
+
* All modes authenticate via API key, ensuring consistent ACL enforcement.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // In-process mode (fastest, same process)
|
|
37
|
+
* const client = await createMcpClient({
|
|
38
|
+
* mode: 'in-process',
|
|
39
|
+
* apiKeySecret: 'omk_xxx.yyy',
|
|
40
|
+
* container: diContainer,
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Stdio mode (subprocess)
|
|
44
|
+
* const client = await createMcpClient({
|
|
45
|
+
* mode: 'stdio',
|
|
46
|
+
* apiKeySecret: 'omk_xxx.yyy',
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* // HTTP mode (network)
|
|
50
|
+
* const client = await createMcpClient({
|
|
51
|
+
* mode: 'http',
|
|
52
|
+
* apiKeySecret: 'omk_xxx.yyy',
|
|
53
|
+
* httpUrl: 'http://localhost:3001/mcp',
|
|
54
|
+
* })
|
|
55
|
+
*
|
|
56
|
+
* // Use client (same interface for all modes)
|
|
57
|
+
* const tools = await client.listTools()
|
|
58
|
+
* const result = await client.callTool('search.query', { query: 'test' })
|
|
59
|
+
* await client.close()
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export async function createMcpClient(options: CreateClientOptions): Promise<McpClientInterface> {
|
|
63
|
+
const { mode, apiKeySecret } = options
|
|
64
|
+
|
|
65
|
+
if (!apiKeySecret) {
|
|
66
|
+
throw new Error('API key secret is required')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (mode) {
|
|
70
|
+
case 'in-process': {
|
|
71
|
+
if (!options.container) {
|
|
72
|
+
throw new Error('DI container is required for in-process mode')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { InProcessMcpClient } = await import('./in-process-client')
|
|
76
|
+
return InProcessMcpClient.create({
|
|
77
|
+
apiKeySecret,
|
|
78
|
+
container: options.container,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'stdio': {
|
|
83
|
+
const { McpClient } = await import('./mcp-client')
|
|
84
|
+
|
|
85
|
+
const stdioOptions: any = {
|
|
86
|
+
transport: 'stdio' as const,
|
|
87
|
+
apiKeySecret,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.stdioCommand) {
|
|
91
|
+
stdioOptions.command = options.stdioCommand
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.stdioArgs) {
|
|
95
|
+
stdioOptions.args = options.stdioArgs
|
|
96
|
+
} else {
|
|
97
|
+
// Default args include the API key
|
|
98
|
+
stdioOptions.args = [
|
|
99
|
+
'mercato',
|
|
100
|
+
'ai_assistant',
|
|
101
|
+
'mcp:serve',
|
|
102
|
+
'--api-key',
|
|
103
|
+
apiKeySecret,
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.cwd) {
|
|
108
|
+
stdioOptions.cwd = options.cwd
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return McpClient.connect(stdioOptions)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'http': {
|
|
115
|
+
if (!options.httpUrl) {
|
|
116
|
+
throw new Error('HTTP URL is required for http mode')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { McpClient } = await import('./mcp-client')
|
|
120
|
+
return McpClient.connect({
|
|
121
|
+
transport: 'http',
|
|
122
|
+
apiKeySecret,
|
|
123
|
+
url: options.httpUrl,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unknown client mode: ${mode}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
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 type { AwilixContainer } from 'awilix'
|
|
5
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
+
import { z, type ZodType } from 'zod'
|
|
7
|
+
import { getToolRegistry } from './tool-registry'
|
|
8
|
+
import { executeTool } from './tool-executor'
|
|
9
|
+
import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
|
|
10
|
+
import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'
|
|
11
|
+
import { jsonSchemaToZod, toSafeZodSchema } from './schema-utils'
|
|
12
|
+
import type { McpServerConfig, McpToolContext } from './types'
|
|
13
|
+
import type { SearchService } from '@open-mercato/search/service'
|
|
14
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
15
|
+
import { findApiKeyBySessionToken } from '@open-mercato/core/modules/api_keys/services/apiKeyService'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for the HTTP MCP server.
|
|
19
|
+
*/
|
|
20
|
+
export type McpHttpServerOptions = {
|
|
21
|
+
config: McpServerConfig
|
|
22
|
+
container: AwilixContainer
|
|
23
|
+
port: number
|
|
24
|
+
/** Static API key for server-level authentication (from env MCP_SERVER_API_KEY) */
|
|
25
|
+
serverApiKey?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve user context from session token.
|
|
30
|
+
* Returns null if session token is invalid or expired.
|
|
31
|
+
*/
|
|
32
|
+
async function resolveSessionContext(
|
|
33
|
+
sessionToken: string,
|
|
34
|
+
baseContext: McpToolContext,
|
|
35
|
+
debug?: boolean
|
|
36
|
+
): Promise<McpToolContext | null> {
|
|
37
|
+
try {
|
|
38
|
+
const em = baseContext.container.resolve<EntityManager>('em')
|
|
39
|
+
const rbacService = baseContext.container.resolve<RbacService>('rbacService')
|
|
40
|
+
|
|
41
|
+
// Look up ephemeral key by session token
|
|
42
|
+
const sessionKey = await findApiKeyBySessionToken(em, sessionToken)
|
|
43
|
+
if (!sessionKey) {
|
|
44
|
+
if (debug) {
|
|
45
|
+
console.error(`[MCP HTTP] Session token not found or expired: ${sessionToken}`)
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load ACL for the session user
|
|
51
|
+
const userId = sessionKey.sessionUserId || sessionKey.createdBy
|
|
52
|
+
if (!userId) {
|
|
53
|
+
if (debug) {
|
|
54
|
+
console.error(`[MCP HTTP] Session key has no associated user`)
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const acl = await rbacService.loadAcl(`api_key:${sessionKey.id}`, {
|
|
60
|
+
tenantId: sessionKey.tenantId ?? null,
|
|
61
|
+
organizationId: sessionKey.organizationId ?? null,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (debug) {
|
|
65
|
+
console.error(`[MCP HTTP] Session context resolved for user ${userId}:`, {
|
|
66
|
+
tenantId: sessionKey.tenantId,
|
|
67
|
+
organizationId: sessionKey.organizationId,
|
|
68
|
+
features: acl.features.length,
|
|
69
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
tenantId: sessionKey.tenantId ?? null,
|
|
75
|
+
organizationId: sessionKey.organizationId ?? null,
|
|
76
|
+
userId,
|
|
77
|
+
container: baseContext.container,
|
|
78
|
+
userFeatures: acl.features,
|
|
79
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
80
|
+
apiKeySecret: baseContext.apiKeySecret,
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (debug) {
|
|
84
|
+
console.error(`[MCP HTTP] Error resolving session context:`, error)
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a stateless MCP server instance for a single request.
|
|
92
|
+
* Tools are registered without pre-filtering - permission checks happen at execution time
|
|
93
|
+
* based on the session token provided in each tool call.
|
|
94
|
+
*/
|
|
95
|
+
function createMcpServerForRequest(
|
|
96
|
+
config: McpServerConfig,
|
|
97
|
+
toolContext: McpToolContext
|
|
98
|
+
): McpServer {
|
|
99
|
+
const server = new McpServer(
|
|
100
|
+
{ name: config.name, version: config.version },
|
|
101
|
+
{ capabilities: { tools: {} } }
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const registry = getToolRegistry()
|
|
105
|
+
const tools = Array.from(registry.getTools().values())
|
|
106
|
+
|
|
107
|
+
if (config.debug) {
|
|
108
|
+
console.error(`[MCP HTTP] Registering ${tools.length} tools (ACL checked per-call via session token)`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Register ALL tools - permission checks happen at execution time via session token
|
|
112
|
+
for (const tool of tools) {
|
|
113
|
+
if (config.debug) {
|
|
114
|
+
console.error(`[MCP HTTP] Registering tool: ${tool.name}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Convert Zod schema to a "safe" schema without Date types
|
|
118
|
+
// This uses JSON Schema round-trip to avoid issues with MCP SDK's internal conversion
|
|
119
|
+
// Also inject _sessionToken as an optional parameter so the AI knows to pass it
|
|
120
|
+
let safeSchema: ZodType | undefined
|
|
121
|
+
if (tool.inputSchema) {
|
|
122
|
+
try {
|
|
123
|
+
// Convert to JSON Schema first
|
|
124
|
+
const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>
|
|
125
|
+
|
|
126
|
+
// Inject _sessionToken into the JSON schema properties
|
|
127
|
+
const properties = (jsonSchema.properties ?? {}) as Record<string, unknown>
|
|
128
|
+
properties._sessionToken = {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Session authorization token (REQUIRED for all tool calls)',
|
|
131
|
+
}
|
|
132
|
+
jsonSchema.properties = properties
|
|
133
|
+
|
|
134
|
+
// Convert back to Zod with passthrough to allow extra properties
|
|
135
|
+
const converted = jsonSchemaToZod(jsonSchema)
|
|
136
|
+
// Use type assertion since we know it's an object schema (we added properties above)
|
|
137
|
+
safeSchema = (converted as z.ZodObject<any>).passthrough()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (config.debug) {
|
|
140
|
+
console.error(
|
|
141
|
+
`[MCP HTTP] Skipping tool ${tool.name} - schema conversion failed:`,
|
|
142
|
+
error instanceof Error ? error.message : error
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// If no schema, create one with just _sessionToken
|
|
149
|
+
safeSchema = z.object({
|
|
150
|
+
_sessionToken: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Session authorization token (REQUIRED for all tool calls)'),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Wrap in try/catch to handle any remaining edge cases
|
|
158
|
+
try {
|
|
159
|
+
server.registerTool(
|
|
160
|
+
tool.name,
|
|
161
|
+
{
|
|
162
|
+
description: tool.description,
|
|
163
|
+
inputSchema: safeSchema,
|
|
164
|
+
},
|
|
165
|
+
async (args: unknown) => {
|
|
166
|
+
const toolArgs = (args ?? {}) as Record<string, unknown>
|
|
167
|
+
|
|
168
|
+
// Extract session token from args
|
|
169
|
+
const sessionToken = toolArgs._sessionToken as string | undefined
|
|
170
|
+
delete toolArgs._sessionToken // Remove before passing to tool handler
|
|
171
|
+
|
|
172
|
+
if (config.debug) {
|
|
173
|
+
console.error(`[MCP HTTP] Calling tool: ${tool.name}`, {
|
|
174
|
+
hasSessionToken: !!sessionToken,
|
|
175
|
+
args: JSON.stringify(toolArgs),
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Resolve user context from session token
|
|
180
|
+
let effectiveContext = toolContext
|
|
181
|
+
if (sessionToken) {
|
|
182
|
+
const sessionContext = await resolveSessionContext(sessionToken, toolContext, config.debug)
|
|
183
|
+
if (sessionContext) {
|
|
184
|
+
effectiveContext = sessionContext
|
|
185
|
+
} else {
|
|
186
|
+
// Session token expired - return user-friendly error for AI to relay
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: 'text' as const,
|
|
191
|
+
text: JSON.stringify({
|
|
192
|
+
error: 'Your chat session has expired. Please close and reopen the chat window to continue.',
|
|
193
|
+
code: 'SESSION_EXPIRED',
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
isError: true,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// No session token provided - reject if base context has no permissions
|
|
202
|
+
if (!effectiveContext.userId && effectiveContext.userFeatures.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'text' as const,
|
|
207
|
+
text: JSON.stringify({
|
|
208
|
+
error: 'Session token required (_sessionToken parameter)',
|
|
209
|
+
code: 'UNAUTHORIZED',
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
isError: true,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if user has required permissions for this tool
|
|
219
|
+
if (tool.requiredFeatures?.length) {
|
|
220
|
+
const hasAccess = hasRequiredFeatures(
|
|
221
|
+
tool.requiredFeatures,
|
|
222
|
+
effectiveContext.userFeatures,
|
|
223
|
+
effectiveContext.isSuperAdmin
|
|
224
|
+
)
|
|
225
|
+
if (!hasAccess) {
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: 'text' as const,
|
|
230
|
+
text: JSON.stringify({
|
|
231
|
+
error: `Insufficient permissions for tool "${tool.name}". Required: ${tool.requiredFeatures.join(', ')}`,
|
|
232
|
+
code: 'UNAUTHORIZED',
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
isError: true,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = await executeTool(tool.name, toolArgs, effectiveContext)
|
|
242
|
+
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text' as const,
|
|
248
|
+
text: JSON.stringify({ error: result.error, code: result.errorCode }),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
isError: true,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: 'text' as const,
|
|
259
|
+
text: JSON.stringify(result.result, null, 2),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
} catch (error) {
|
|
266
|
+
// Skip tools with schemas that can't be registered
|
|
267
|
+
if (config.debug) {
|
|
268
|
+
console.error(
|
|
269
|
+
`[MCP HTTP] Skipping tool ${tool.name} - registration failed:`,
|
|
270
|
+
error instanceof Error ? error.message : error
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return server
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Maximum request body size (1MB).
|
|
282
|
+
* Prevents memory exhaustion from oversized payloads.
|
|
283
|
+
*/
|
|
284
|
+
const MAX_BODY_SIZE = 1 * 1024 * 1024
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse JSON body from request with size limit.
|
|
288
|
+
*/
|
|
289
|
+
async function parseJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
const chunks: Buffer[] = []
|
|
292
|
+
let totalSize = 0
|
|
293
|
+
|
|
294
|
+
req.on('data', (chunk: Buffer) => {
|
|
295
|
+
totalSize += chunk.length
|
|
296
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
297
|
+
req.destroy()
|
|
298
|
+
reject(new Error('Request payload too large'))
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
chunks.push(chunk)
|
|
302
|
+
})
|
|
303
|
+
req.on('end', () => {
|
|
304
|
+
try {
|
|
305
|
+
const body = Buffer.concat(chunks).toString('utf-8')
|
|
306
|
+
resolve(body ? JSON.parse(body) : undefined)
|
|
307
|
+
} catch (error) {
|
|
308
|
+
reject(error)
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
req.on('error', reject)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Run MCP server with HTTP transport (stateless mode).
|
|
317
|
+
*
|
|
318
|
+
* Each request creates a new MCP server instance and transport.
|
|
319
|
+
* The server authenticates requests using API keys from the x-api-key header.
|
|
320
|
+
*/
|
|
321
|
+
export async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {
|
|
322
|
+
const { config, container, port } = options
|
|
323
|
+
|
|
324
|
+
await loadAllModuleTools()
|
|
325
|
+
|
|
326
|
+
// Index tools and API endpoints for hybrid search discovery (if search service available)
|
|
327
|
+
try {
|
|
328
|
+
const searchService = container.resolve('searchService') as SearchService
|
|
329
|
+
|
|
330
|
+
// Index MCP tools
|
|
331
|
+
await indexToolsForSearch(searchService)
|
|
332
|
+
|
|
333
|
+
// Index API endpoints for api_discover
|
|
334
|
+
const { indexApiEndpoints } = await import('./api-endpoint-index')
|
|
335
|
+
const endpointCount = await indexApiEndpoints(searchService)
|
|
336
|
+
if (endpointCount > 0) {
|
|
337
|
+
console.error(`[MCP HTTP] Indexed ${endpointCount} API endpoints for hybrid search`)
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
// Search service might not be configured - discovery will use fallback
|
|
341
|
+
console.error('[MCP HTTP] Search indexing skipped (search service not available):', error)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
345
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`)
|
|
346
|
+
|
|
347
|
+
// Health check endpoint
|
|
348
|
+
if (url.pathname === '/health') {
|
|
349
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
350
|
+
res.end(JSON.stringify({
|
|
351
|
+
status: 'ok',
|
|
352
|
+
tools: getToolRegistry().listToolNames().length,
|
|
353
|
+
timestamp: new Date().toISOString(),
|
|
354
|
+
}))
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (url.pathname !== '/mcp') {
|
|
359
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
360
|
+
res.end(JSON.stringify({ error: 'Not found' }))
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Extract headers
|
|
365
|
+
const headers: Record<string, string | undefined> = {}
|
|
366
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
367
|
+
headers[key] = Array.isArray(value) ? value[0] : value
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Server-level authentication with static API key
|
|
371
|
+
const providedApiKey = extractApiKeyFromHeaders(headers)
|
|
372
|
+
if (!providedApiKey) {
|
|
373
|
+
res.writeHead(401, { 'Content-Type': 'application/json' })
|
|
374
|
+
res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check against static server API key (from env MCP_SERVER_API_KEY)
|
|
379
|
+
const serverApiKey = options.serverApiKey || process.env.MCP_SERVER_API_KEY
|
|
380
|
+
if (!serverApiKey) {
|
|
381
|
+
console.error('[MCP HTTP] Warning: MCP_SERVER_API_KEY not configured, rejecting all requests')
|
|
382
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
383
|
+
res.end(JSON.stringify({ error: 'MCP server not properly configured' }))
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (providedApiKey !== serverApiKey) {
|
|
388
|
+
res.writeHead(401, { 'Content-Type': 'application/json' })
|
|
389
|
+
res.end(JSON.stringify({ error: 'Invalid API key' }))
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (config.debug) {
|
|
394
|
+
console.error(`[MCP HTTP] Server-level auth passed (${req.method})`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Create base tool context (will be overridden by session token per-tool)
|
|
398
|
+
// Start with minimal permissions - session tokens provide user-level auth
|
|
399
|
+
const toolContext: McpToolContext = {
|
|
400
|
+
tenantId: null,
|
|
401
|
+
organizationId: null,
|
|
402
|
+
userId: null,
|
|
403
|
+
container,
|
|
404
|
+
userFeatures: [],
|
|
405
|
+
isSuperAdmin: false,
|
|
406
|
+
apiKeySecret: providedApiKey,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
// Create stateless transport (no session ID generator = stateless)
|
|
411
|
+
const transport = new StreamableHTTPServerTransport({
|
|
412
|
+
sessionIdGenerator: undefined,
|
|
413
|
+
enableJsonResponse: req.method === 'POST',
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// Create new server for this request
|
|
417
|
+
const mcpServer = createMcpServerForRequest(config, toolContext)
|
|
418
|
+
|
|
419
|
+
if (config.debug) {
|
|
420
|
+
// Check registered tools on the server
|
|
421
|
+
const registeredTools = (mcpServer as any)._registeredTools || {}
|
|
422
|
+
console.error(`[MCP HTTP] Registered tools in McpServer:`, Object.keys(registeredTools))
|
|
423
|
+
console.error(`[MCP HTTP] Tool handlers initialized:`, (mcpServer as any)._toolHandlersInitialized)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Connect server to transport
|
|
427
|
+
await mcpServer.connect(transport)
|
|
428
|
+
|
|
429
|
+
// Handle the request
|
|
430
|
+
if (req.method === 'POST') {
|
|
431
|
+
const body = await parseJsonBody(req)
|
|
432
|
+
await transport.handleRequest(req, res, body)
|
|
433
|
+
} else {
|
|
434
|
+
await transport.handleRequest(req, res)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Cleanup after response finishes
|
|
438
|
+
res.on('finish', () => {
|
|
439
|
+
transport.close()
|
|
440
|
+
mcpServer.close()
|
|
441
|
+
if (config.debug) {
|
|
442
|
+
console.error(`[MCP HTTP] Request completed, cleaned up`)
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error('[MCP HTTP] Error handling request:', error)
|
|
447
|
+
if (!res.headersSent) {
|
|
448
|
+
// Handle payload too large error
|
|
449
|
+
if (error instanceof Error && error.message === 'Request payload too large') {
|
|
450
|
+
res.writeHead(413, { 'Content-Type': 'application/json' })
|
|
451
|
+
res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
456
|
+
res.end(
|
|
457
|
+
JSON.stringify({
|
|
458
|
+
jsonrpc: '2.0',
|
|
459
|
+
error: {
|
|
460
|
+
code: -32603,
|
|
461
|
+
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
462
|
+
},
|
|
463
|
+
id: null,
|
|
464
|
+
})
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const toolCount = getToolRegistry().listToolNames().length
|
|
471
|
+
const serverKeyConfigured = !!(options.serverApiKey || process.env.MCP_SERVER_API_KEY)
|
|
472
|
+
|
|
473
|
+
console.error(`[MCP HTTP] Starting ${config.name} v${config.version}`)
|
|
474
|
+
console.error(`[MCP HTTP] Endpoint: http://localhost:${port}/mcp`)
|
|
475
|
+
console.error(`[MCP HTTP] Health: http://localhost:${port}/health`)
|
|
476
|
+
console.error(`[MCP HTTP] Tools registered: ${toolCount}`)
|
|
477
|
+
console.error(`[MCP HTTP] Mode: Stateless (new server per request)`)
|
|
478
|
+
console.error(`[MCP HTTP] Server Auth: ${serverKeyConfigured ? 'MCP_SERVER_API_KEY configured' : 'WARNING: MCP_SERVER_API_KEY not set!'}`)
|
|
479
|
+
console.error(`[MCP HTTP] User Auth: Session token in _sessionToken parameter`)
|
|
480
|
+
|
|
481
|
+
// Return a Promise that keeps the process alive until shutdown
|
|
482
|
+
return new Promise<void>((resolve) => {
|
|
483
|
+
httpServer.listen(port, () => {
|
|
484
|
+
console.error(`[MCP HTTP] Server listening on port ${port}`)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const shutdown = async () => {
|
|
488
|
+
console.error('[MCP HTTP] Shutting down...')
|
|
489
|
+
httpServer.close(() => {
|
|
490
|
+
console.error('[MCP HTTP] Server closed')
|
|
491
|
+
resolve()
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
process.on('SIGINT', shutdown)
|
|
496
|
+
process.on('SIGTERM', shutdown)
|
|
497
|
+
})
|
|
498
|
+
}
|