@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,142 @@
|
|
|
1
|
+
import { z, type ZodType } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache for converted safe schemas to avoid repeated conversions per request.
|
|
5
|
+
*/
|
|
6
|
+
const safeSchemaCache = new WeakMap<ZodType, ZodType>()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert a JSON Schema to a simple Zod schema.
|
|
10
|
+
* This creates a schema that can be converted back to JSON Schema without errors.
|
|
11
|
+
*
|
|
12
|
+
* Supports:
|
|
13
|
+
* - Basic types: string, number, integer, boolean, null
|
|
14
|
+
* - Arrays with item types
|
|
15
|
+
* - Objects with properties and required fields
|
|
16
|
+
* - Records/dictionaries via additionalProperties
|
|
17
|
+
* - Union types via anyOf/oneOf
|
|
18
|
+
* - Enum values
|
|
19
|
+
*/
|
|
20
|
+
export function jsonSchemaToZod(jsonSchema: Record<string, unknown>): ZodType {
|
|
21
|
+
const type = jsonSchema.type as string | undefined
|
|
22
|
+
|
|
23
|
+
if (type === 'string') {
|
|
24
|
+
return z.string()
|
|
25
|
+
}
|
|
26
|
+
if (type === 'number' || type === 'integer') {
|
|
27
|
+
return z.number()
|
|
28
|
+
}
|
|
29
|
+
if (type === 'boolean') {
|
|
30
|
+
return z.boolean()
|
|
31
|
+
}
|
|
32
|
+
if (type === 'null') {
|
|
33
|
+
return z.null()
|
|
34
|
+
}
|
|
35
|
+
if (type === 'array') {
|
|
36
|
+
const items = jsonSchema.items as Record<string, unknown> | undefined
|
|
37
|
+
if (items) {
|
|
38
|
+
return z.array(jsonSchemaToZod(items))
|
|
39
|
+
}
|
|
40
|
+
return z.array(z.unknown())
|
|
41
|
+
}
|
|
42
|
+
if (type === 'object') {
|
|
43
|
+
const properties = jsonSchema.properties as Record<string, Record<string, unknown>> | undefined
|
|
44
|
+
const required = (jsonSchema.required as string[]) || []
|
|
45
|
+
const additionalProperties = jsonSchema.additionalProperties
|
|
46
|
+
|
|
47
|
+
// Handle z.record() - objects with additionalProperties but no fixed properties
|
|
48
|
+
if (additionalProperties && (!properties || Object.keys(properties).length === 0)) {
|
|
49
|
+
// This is a record/dictionary type - allow any properties
|
|
50
|
+
if (typeof additionalProperties === 'object') {
|
|
51
|
+
return z.record(z.string(), jsonSchemaToZod(additionalProperties as Record<string, unknown>))
|
|
52
|
+
}
|
|
53
|
+
// additionalProperties: true means any value
|
|
54
|
+
return z.record(z.string(), z.unknown())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (properties) {
|
|
58
|
+
const shape: Record<string, ZodType> = {}
|
|
59
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
60
|
+
let fieldSchema = jsonSchemaToZod(propSchema)
|
|
61
|
+
// Make field optional if not in required array
|
|
62
|
+
if (!required.includes(key)) {
|
|
63
|
+
fieldSchema = fieldSchema.optional()
|
|
64
|
+
}
|
|
65
|
+
shape[key] = fieldSchema
|
|
66
|
+
}
|
|
67
|
+
// If additionalProperties is allowed, use passthrough
|
|
68
|
+
if (additionalProperties) {
|
|
69
|
+
return z.object(shape).passthrough()
|
|
70
|
+
}
|
|
71
|
+
return z.object(shape)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Empty object with additionalProperties - treat as record
|
|
75
|
+
if (additionalProperties) {
|
|
76
|
+
return z.record(z.string(), z.unknown())
|
|
77
|
+
}
|
|
78
|
+
return z.object({})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle union types (anyOf, oneOf)
|
|
82
|
+
const anyOf = jsonSchema.anyOf as Record<string, unknown>[] | undefined
|
|
83
|
+
const oneOf = jsonSchema.oneOf as Record<string, unknown>[] | undefined
|
|
84
|
+
const unionTypes = anyOf || oneOf
|
|
85
|
+
if (unionTypes && unionTypes.length >= 2) {
|
|
86
|
+
const schemas = unionTypes.map(s => jsonSchemaToZod(s))
|
|
87
|
+
return z.union(schemas as [ZodType, ZodType, ...ZodType[]])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle nullable via anyOf with null
|
|
91
|
+
if (anyOf && anyOf.length === 2) {
|
|
92
|
+
const types = anyOf.map((s) => s.type)
|
|
93
|
+
if (types.includes('null')) {
|
|
94
|
+
const nonNullSchema = anyOf.find((s) => s.type !== 'null')
|
|
95
|
+
if (nonNullSchema) {
|
|
96
|
+
return jsonSchemaToZod(nonNullSchema).nullable()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle enum
|
|
102
|
+
const enumValues = jsonSchema.enum as string[] | undefined
|
|
103
|
+
if (enumValues && enumValues.length > 0) {
|
|
104
|
+
return z.enum(enumValues as [string, ...string[]])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fallback for empty schemas (like Date converted with unrepresentable: 'any')
|
|
108
|
+
return z.unknown()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert a Zod schema to a safe Zod schema that has no Date types.
|
|
113
|
+
* Uses JSON Schema as an intermediate format to handle all Zod v4 internal complexities.
|
|
114
|
+
* Results are cached to avoid repeated conversions.
|
|
115
|
+
*
|
|
116
|
+
* @param schema - The original Zod schema
|
|
117
|
+
* @returns A safe Zod schema without Date types
|
|
118
|
+
*/
|
|
119
|
+
export function toSafeZodSchema(schema: ZodType): ZodType {
|
|
120
|
+
// Check cache first
|
|
121
|
+
const cached = safeSchemaCache.get(schema)
|
|
122
|
+
if (cached) {
|
|
123
|
+
return cached
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Use Zod 4's toJSONSchema with unrepresentable: 'any' to handle Date types
|
|
128
|
+
const jsonSchema = z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>
|
|
129
|
+
|
|
130
|
+
// Convert back to a simple Zod schema without Date types
|
|
131
|
+
const safeSchema = jsonSchemaToZod(jsonSchema)
|
|
132
|
+
|
|
133
|
+
// Cache the result
|
|
134
|
+
safeSchemaCache.set(schema, safeSchema)
|
|
135
|
+
|
|
136
|
+
return safeSchema
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[Schema Utils] Error converting schema:', error)
|
|
139
|
+
// Fallback to the original schema if conversion fails
|
|
140
|
+
return schema
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { McpToolContext, ToolExecutionResult } from './types'
|
|
2
|
+
import { getToolRegistry } from './tool-registry'
|
|
3
|
+
import { hasRequiredFeatures } from './auth'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute a tool with full context and ACL checks.
|
|
7
|
+
*/
|
|
8
|
+
export async function executeTool(
|
|
9
|
+
toolName: string,
|
|
10
|
+
input: unknown,
|
|
11
|
+
context: McpToolContext
|
|
12
|
+
): Promise<ToolExecutionResult> {
|
|
13
|
+
const registry = getToolRegistry()
|
|
14
|
+
const tool = registry.getTool(toolName)
|
|
15
|
+
|
|
16
|
+
if (!tool) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
error: `Tool "${toolName}" not found`,
|
|
20
|
+
errorCode: 'NOT_FOUND',
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ACL check
|
|
25
|
+
if (tool.requiredFeatures?.length) {
|
|
26
|
+
const hasAccess = hasRequiredFeatures(
|
|
27
|
+
tool.requiredFeatures,
|
|
28
|
+
context.userFeatures,
|
|
29
|
+
context.isSuperAdmin
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (!hasAccess) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: `Insufficient permissions for tool "${toolName}". Required: ${tool.requiredFeatures.join(', ')}`,
|
|
36
|
+
errorCode: 'UNAUTHORIZED',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Input validation
|
|
42
|
+
const parseResult = tool.inputSchema.safeParse(input)
|
|
43
|
+
if (!parseResult.success) {
|
|
44
|
+
// Use any cast for Zod v4 compatibility
|
|
45
|
+
const issues = (parseResult.error as any).issues ?? []
|
|
46
|
+
const errorMessages = issues
|
|
47
|
+
.map((issue: { path: PropertyKey[]; message: string }) =>
|
|
48
|
+
`${issue.path.join('.')}: ${issue.message}`
|
|
49
|
+
)
|
|
50
|
+
.join('; ')
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: `Invalid input: ${errorMessages || 'Validation failed'}`,
|
|
54
|
+
errorCode: 'VALIDATION_ERROR',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Execute tool
|
|
59
|
+
try {
|
|
60
|
+
const result = await tool.handler(parseResult.data, context)
|
|
61
|
+
return { success: true, result }
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
64
|
+
console.error(`[MCP Tool] Error executing "${toolName}":`, error)
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: message,
|
|
68
|
+
errorCode: 'EXECUTION_ERROR',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SearchEntityConfig,
|
|
3
|
+
SearchResultPresenter,
|
|
4
|
+
IndexableRecord,
|
|
5
|
+
} from '@open-mercato/search/types'
|
|
6
|
+
import type { McpToolDefinition } from './types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Entity ID for MCP tools in the search index.
|
|
10
|
+
* Following the module:entity naming convention.
|
|
11
|
+
*/
|
|
12
|
+
export const TOOL_ENTITY_ID = 'ai_assistant:mcp_tool' as const
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tenant ID for global tools.
|
|
16
|
+
* Tools are not tenant-scoped, so we use a special "system" UUID.
|
|
17
|
+
* This is the nil UUID (all zeros) reserved for system-wide resources.
|
|
18
|
+
*/
|
|
19
|
+
export const GLOBAL_TENANT_ID = '00000000-0000-0000-0000-000000000000'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Essential tools that should always be available regardless of search results.
|
|
23
|
+
* These provide fundamental functionality for the AI assistant.
|
|
24
|
+
*/
|
|
25
|
+
export const ESSENTIAL_TOOLS = [
|
|
26
|
+
'context_whoami', // Auth context awareness
|
|
27
|
+
'search_query', // Universal search
|
|
28
|
+
'search_schema', // Entity discovery
|
|
29
|
+
'search_status', // Check integrations
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default configuration for tool search.
|
|
34
|
+
*/
|
|
35
|
+
export const TOOL_SEARCH_CONFIG = {
|
|
36
|
+
/** Maximum tools to return from search */
|
|
37
|
+
defaultLimit: 12,
|
|
38
|
+
/** Minimum relevance score (0-1) */
|
|
39
|
+
minScore: 0.2,
|
|
40
|
+
/** Strategies to use (in priority order) */
|
|
41
|
+
strategies: ['fulltext', 'vector', 'tokens'] as const,
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Search entity configuration for MCP tools.
|
|
46
|
+
* This configures how tools are indexed and searched.
|
|
47
|
+
*/
|
|
48
|
+
export const toolSearchEntityConfig: SearchEntityConfig = {
|
|
49
|
+
entityId: TOOL_ENTITY_ID,
|
|
50
|
+
enabled: true,
|
|
51
|
+
priority: 100, // High priority for tool results
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build searchable content from a tool definition.
|
|
55
|
+
*/
|
|
56
|
+
buildSource: (ctx) => {
|
|
57
|
+
const tool = ctx.record as unknown as McpToolDefinition
|
|
58
|
+
const name = tool.name || ''
|
|
59
|
+
const description = tool.description || ''
|
|
60
|
+
const moduleId = (ctx.record as Record<string, unknown>).moduleId as string | undefined
|
|
61
|
+
|
|
62
|
+
// Normalize name: replace underscores/dots with spaces for better search
|
|
63
|
+
const normalizedName = name.replace(/[_.-]/g, ' ')
|
|
64
|
+
|
|
65
|
+
// Build text content for embedding and fulltext search
|
|
66
|
+
const textContent = [
|
|
67
|
+
normalizedName,
|
|
68
|
+
description,
|
|
69
|
+
moduleId ? `module ${moduleId}` : '',
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' | ')
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
text: textContent,
|
|
76
|
+
fields: {
|
|
77
|
+
name: normalizedName,
|
|
78
|
+
originalName: name,
|
|
79
|
+
description,
|
|
80
|
+
moduleId: moduleId ?? null,
|
|
81
|
+
requiredFeatures: tool.requiredFeatures ?? [],
|
|
82
|
+
},
|
|
83
|
+
presenter: {
|
|
84
|
+
title: name,
|
|
85
|
+
subtitle: description.slice(0, 100),
|
|
86
|
+
icon: 'tool',
|
|
87
|
+
},
|
|
88
|
+
checksumSource: { name, description, moduleId },
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Format result for display in search UI.
|
|
94
|
+
*/
|
|
95
|
+
formatResult: (ctx) => {
|
|
96
|
+
const tool = ctx.record as unknown as McpToolDefinition
|
|
97
|
+
return {
|
|
98
|
+
title: tool.name || 'Unknown Tool',
|
|
99
|
+
subtitle: (tool.description || '').slice(0, 100),
|
|
100
|
+
icon: 'tool',
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Field policy for search strategies.
|
|
106
|
+
*/
|
|
107
|
+
fieldPolicy: {
|
|
108
|
+
searchable: ['name', 'description', 'moduleId'],
|
|
109
|
+
hashOnly: [],
|
|
110
|
+
excluded: ['requiredFeatures', 'inputSchema', 'handler'],
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert an MCP tool definition to an indexable record for search.
|
|
116
|
+
*
|
|
117
|
+
* @param tool - The tool definition to index
|
|
118
|
+
* @param moduleId - The module that registered this tool
|
|
119
|
+
* @returns IndexableRecord ready for search indexing
|
|
120
|
+
*/
|
|
121
|
+
export function toolToIndexableRecord(
|
|
122
|
+
tool: McpToolDefinition,
|
|
123
|
+
moduleId?: string
|
|
124
|
+
): IndexableRecord {
|
|
125
|
+
const normalizedName = tool.name.replace(/[_.-]/g, ' ')
|
|
126
|
+
const description = tool.description || ''
|
|
127
|
+
|
|
128
|
+
// Build text for vector embedding
|
|
129
|
+
const embeddingText = `${normalizedName} | ${description}`
|
|
130
|
+
|
|
131
|
+
const presenter: SearchResultPresenter = {
|
|
132
|
+
title: tool.name,
|
|
133
|
+
subtitle: description.slice(0, 100),
|
|
134
|
+
icon: 'tool',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
entityId: TOOL_ENTITY_ID,
|
|
139
|
+
recordId: tool.name,
|
|
140
|
+
tenantId: GLOBAL_TENANT_ID,
|
|
141
|
+
organizationId: null,
|
|
142
|
+
fields: {
|
|
143
|
+
name: normalizedName,
|
|
144
|
+
originalName: tool.name,
|
|
145
|
+
description,
|
|
146
|
+
moduleId: moduleId ?? null,
|
|
147
|
+
requiredFeatures: tool.requiredFeatures ?? [],
|
|
148
|
+
},
|
|
149
|
+
presenter,
|
|
150
|
+
text: embeddingText,
|
|
151
|
+
checksumSource: {
|
|
152
|
+
name: tool.name,
|
|
153
|
+
description,
|
|
154
|
+
moduleId,
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compute a simple checksum for tool definitions.
|
|
161
|
+
* Used to detect changes and avoid unnecessary re-indexing.
|
|
162
|
+
*/
|
|
163
|
+
export function computeToolsChecksum(
|
|
164
|
+
tools: Array<{ name: string; description: string }>
|
|
165
|
+
): string {
|
|
166
|
+
const content = tools
|
|
167
|
+
.map((t) => `${t.name}:${t.description}`)
|
|
168
|
+
.sort()
|
|
169
|
+
.join('|')
|
|
170
|
+
|
|
171
|
+
// Simple hash using string code points
|
|
172
|
+
let hash = 0
|
|
173
|
+
for (let i = 0; i < content.length; i++) {
|
|
174
|
+
const char = content.charCodeAt(i)
|
|
175
|
+
hash = ((hash << 5) - hash + char) | 0
|
|
176
|
+
}
|
|
177
|
+
return hash.toString(16)
|
|
178
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { SearchService } from '@open-mercato/search/service'
|
|
3
|
+
import { registerMcpTool, getToolRegistry } from './tool-registry'
|
|
4
|
+
import type { McpToolDefinition, McpToolContext } from './types'
|
|
5
|
+
import { ToolSearchService } from './tool-search'
|
|
6
|
+
import { loadApiDiscoveryTools } from './api-discovery-tools'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Module tool definition as exported from ai-tools.ts files.
|
|
10
|
+
*/
|
|
11
|
+
type ModuleAiTool = {
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
inputSchema: any
|
|
15
|
+
requiredFeatures?: string[]
|
|
16
|
+
handler: (input: any, ctx: any) => Promise<unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Built-in context.whoami tool that returns the current authentication context.
|
|
21
|
+
* This is useful for AI to understand its current tenant/org scope.
|
|
22
|
+
*/
|
|
23
|
+
const contextWhoamiTool: McpToolDefinition = {
|
|
24
|
+
name: 'context_whoami',
|
|
25
|
+
description:
|
|
26
|
+
'Get the current authentication context including tenant ID, organization ID, user ID, and available features. Use this to understand your current scope before performing operations.',
|
|
27
|
+
inputSchema: z.object({}),
|
|
28
|
+
requiredFeatures: [], // No specific feature required - available to all authenticated users
|
|
29
|
+
handler: async (_input: unknown, ctx: McpToolContext) => {
|
|
30
|
+
return {
|
|
31
|
+
tenantId: ctx.tenantId,
|
|
32
|
+
organizationId: ctx.organizationId,
|
|
33
|
+
userId: ctx.userId,
|
|
34
|
+
isSuperAdmin: ctx.isSuperAdmin,
|
|
35
|
+
features: ctx.userFeatures,
|
|
36
|
+
featureCount: ctx.userFeatures.length,
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load and register AI tools from a module's ai-tools.ts export.
|
|
43
|
+
*
|
|
44
|
+
* @param moduleId - The module identifier (e.g., 'search', 'customers')
|
|
45
|
+
* @param tools - Array of tool definitions from the module
|
|
46
|
+
*/
|
|
47
|
+
export function loadModuleTools(moduleId: string, tools: ModuleAiTool[]): void {
|
|
48
|
+
for (const tool of tools) {
|
|
49
|
+
registerMcpTool(
|
|
50
|
+
{
|
|
51
|
+
name: tool.name,
|
|
52
|
+
description: tool.description,
|
|
53
|
+
inputSchema: tool.inputSchema,
|
|
54
|
+
requiredFeatures: tool.requiredFeatures,
|
|
55
|
+
handler: tool.handler,
|
|
56
|
+
} as McpToolDefinition,
|
|
57
|
+
{ moduleId }
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dynamically load tools from known module paths.
|
|
64
|
+
* This is called during MCP server startup.
|
|
65
|
+
*/
|
|
66
|
+
export async function loadAllModuleTools(): Promise<void> {
|
|
67
|
+
// 1. Register built-in tools
|
|
68
|
+
registerMcpTool(contextWhoamiTool, { moduleId: 'context' })
|
|
69
|
+
console.error('[MCP Tools] Registered built-in context_whoami tool')
|
|
70
|
+
|
|
71
|
+
// 2. Load manual ai-tools.ts files from modules
|
|
72
|
+
const moduleToolPaths = [
|
|
73
|
+
{ moduleId: 'search', importPath: '@open-mercato/search/modules/search/ai-tools' },
|
|
74
|
+
// Add more modules here as they define ai-tools.ts
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
for (const { moduleId, importPath } of moduleToolPaths) {
|
|
78
|
+
try {
|
|
79
|
+
const module = await import(importPath)
|
|
80
|
+
const tools = module.aiTools ?? module.default ?? []
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
83
|
+
loadModuleTools(moduleId, tools)
|
|
84
|
+
console.error(`[MCP Tools] Loaded ${tools.length} tools from ${moduleId}`)
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Module might not have ai-tools.ts or import failed
|
|
88
|
+
// This is not an error - modules can optionally provide tools
|
|
89
|
+
console.error(`[MCP Tools] Could not load tools from ${moduleId}:`, error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Load API discovery tools (api_discover, api_execute, api_schema)
|
|
94
|
+
try {
|
|
95
|
+
const apiToolCount = await loadApiDiscoveryTools()
|
|
96
|
+
console.error(`[MCP Tools] Loaded ${apiToolCount} API discovery tools`)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[MCP Tools] Could not load API discovery tools:', error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Index all registered tools for hybrid search discovery.
|
|
104
|
+
* This should be called after loadAllModuleTools() when the search service is available.
|
|
105
|
+
*
|
|
106
|
+
* @param searchService - The search service from DI container
|
|
107
|
+
* @param force - Force re-indexing even if checksums match
|
|
108
|
+
* @returns Indexing result with statistics
|
|
109
|
+
*/
|
|
110
|
+
export async function indexToolsForSearch(
|
|
111
|
+
searchService: SearchService,
|
|
112
|
+
force = false
|
|
113
|
+
): Promise<{
|
|
114
|
+
indexed: number
|
|
115
|
+
skipped: number
|
|
116
|
+
strategies: string[]
|
|
117
|
+
checksum: string
|
|
118
|
+
}> {
|
|
119
|
+
const registry = getToolRegistry()
|
|
120
|
+
const toolSearchService = new ToolSearchService(searchService, registry)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await toolSearchService.indexTools(force)
|
|
124
|
+
|
|
125
|
+
console.error(`[MCP Tools] Indexed ${result.indexed} tools for search`)
|
|
126
|
+
console.error(`[MCP Tools] Search strategies available: ${result.strategies.join(', ')}`)
|
|
127
|
+
|
|
128
|
+
if (result.skipped > 0) {
|
|
129
|
+
console.error(`[MCP Tools] Skipped ${result.skipped} tools (unchanged)`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[MCP Tools] Failed to index tools for search:', error)
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a ToolSearchService instance for tool discovery.
|
|
141
|
+
* Use this to get a configured service for discovering relevant tools.
|
|
142
|
+
*
|
|
143
|
+
* @param searchService - The search service from DI container
|
|
144
|
+
* @returns Configured ToolSearchService
|
|
145
|
+
*/
|
|
146
|
+
export function createToolSearchService(searchService: SearchService): ToolSearchService {
|
|
147
|
+
const registry = getToolRegistry()
|
|
148
|
+
return new ToolSearchService(searchService, registry)
|
|
149
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { McpToolDefinition, McpToolRegistry, ToolRegistrationOptions } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global tool registry singleton.
|
|
5
|
+
* Modules call registerMcpTool() to add their tools.
|
|
6
|
+
*/
|
|
7
|
+
class ToolRegistryImpl implements McpToolRegistry {
|
|
8
|
+
private tools = new Map<string, McpToolDefinition>()
|
|
9
|
+
private moduleMap = new Map<string, string[]>()
|
|
10
|
+
|
|
11
|
+
registerTool<TInput, TOutput>(
|
|
12
|
+
tool: McpToolDefinition<TInput, TOutput>,
|
|
13
|
+
options?: ToolRegistrationOptions
|
|
14
|
+
): void {
|
|
15
|
+
if (!tool?.name) {
|
|
16
|
+
throw new Error('MCP tool must define a name')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (this.tools.has(tool.name)) {
|
|
20
|
+
console.warn(`[McpToolRegistry] Tool "${tool.name}" already registered, overwriting`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.tools.set(tool.name, tool as McpToolDefinition)
|
|
24
|
+
|
|
25
|
+
if (options?.moduleId) {
|
|
26
|
+
const existing = this.moduleMap.get(options.moduleId) ?? []
|
|
27
|
+
if (!existing.includes(tool.name)) {
|
|
28
|
+
existing.push(tool.name)
|
|
29
|
+
}
|
|
30
|
+
this.moduleMap.set(options.moduleId, existing)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getTools(): Map<string, McpToolDefinition> {
|
|
35
|
+
return new Map(this.tools)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getTool(name: string): McpToolDefinition | undefined {
|
|
39
|
+
return this.tools.get(name)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
listToolNames(): string[] {
|
|
43
|
+
return Array.from(this.tools.keys())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
listToolsByModule(moduleId: string): string[] {
|
|
47
|
+
return this.moduleMap.get(moduleId) ?? []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
unregisterTool(name: string): void {
|
|
51
|
+
this.tools.delete(name)
|
|
52
|
+
for (const [moduleId, tools] of this.moduleMap.entries()) {
|
|
53
|
+
const index = tools.indexOf(name)
|
|
54
|
+
if (index !== -1) {
|
|
55
|
+
tools.splice(index, 1)
|
|
56
|
+
this.moduleMap.set(moduleId, tools)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear(): void {
|
|
62
|
+
this.tools.clear()
|
|
63
|
+
this.moduleMap.clear()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const toolRegistry = new ToolRegistryImpl()
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register an MCP tool from any module.
|
|
71
|
+
*
|
|
72
|
+
* Note: Tool names must match the pattern ^[a-zA-Z0-9_-]{1,128}$
|
|
73
|
+
* (no dots allowed - use underscores instead).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* import { registerMcpTool } from '@open-mercato/ai-assistant/tools'
|
|
78
|
+
* import { z } from 'zod'
|
|
79
|
+
*
|
|
80
|
+
* registerMcpTool({
|
|
81
|
+
* name: 'customers_search',
|
|
82
|
+
* description: 'Search for customers by name or email',
|
|
83
|
+
* inputSchema: z.object({
|
|
84
|
+
* query: z.string(),
|
|
85
|
+
* limit: z.number().optional().default(10),
|
|
86
|
+
* }),
|
|
87
|
+
* requiredFeatures: ['customers.people.view'],
|
|
88
|
+
* handler: async (input, ctx) => {
|
|
89
|
+
* const queryEngine = ctx.container.resolve('queryEngine')
|
|
90
|
+
* // ... implementation
|
|
91
|
+
* }
|
|
92
|
+
* }, { moduleId: 'customers' })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function registerMcpTool<TInput, TOutput>(
|
|
96
|
+
tool: McpToolDefinition<TInput, TOutput>,
|
|
97
|
+
options?: ToolRegistrationOptions
|
|
98
|
+
): void {
|
|
99
|
+
toolRegistry.registerTool(tool, options)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the global tool registry instance.
|
|
104
|
+
*/
|
|
105
|
+
export function getToolRegistry(): McpToolRegistry {
|
|
106
|
+
return toolRegistry
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Unregister an MCP tool by name.
|
|
111
|
+
*/
|
|
112
|
+
export function unregisterMcpTool(name: string): void {
|
|
113
|
+
toolRegistry.unregisterTool(name)
|
|
114
|
+
}
|