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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/AGENTS.md +1090 -0
  2. package/README.md +607 -0
  3. package/build.mjs +92 -0
  4. package/dist/di.js +8 -0
  5. package/dist/di.js.map +7 -0
  6. package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
  7. package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
  8. package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
  9. package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
  10. package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
  11. package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
  12. package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
  13. package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
  14. package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
  15. package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
  16. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
  17. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
  18. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
  19. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
  20. package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
  21. package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
  22. package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
  23. package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
  24. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
  25. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
  26. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
  27. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
  28. package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
  29. package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
  30. package/dist/frontend/components/CommandPalette/index.js +28 -0
  31. package/dist/frontend/components/CommandPalette/index.js.map +7 -0
  32. package/dist/frontend/constants.js +41 -0
  33. package/dist/frontend/constants.js.map +7 -0
  34. package/dist/frontend/hooks/index.js +13 -0
  35. package/dist/frontend/hooks/index.js.map +7 -0
  36. package/dist/frontend/hooks/useCommandPalette.js +1094 -0
  37. package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
  38. package/dist/frontend/hooks/useMcpTools.js +66 -0
  39. package/dist/frontend/hooks/useMcpTools.js.map +7 -0
  40. package/dist/frontend/hooks/usePageContext.js +48 -0
  41. package/dist/frontend/hooks/usePageContext.js.map +7 -0
  42. package/dist/frontend/hooks/useRecentActions.js +56 -0
  43. package/dist/frontend/hooks/useRecentActions.js.map +7 -0
  44. package/dist/frontend/hooks/useRecentTools.js +55 -0
  45. package/dist/frontend/hooks/useRecentTools.js.map +7 -0
  46. package/dist/frontend/index.js +35 -0
  47. package/dist/frontend/index.js.map +7 -0
  48. package/dist/frontend/types.js +1 -0
  49. package/dist/frontend/types.js.map +7 -0
  50. package/dist/frontend/utils/index.js +7 -0
  51. package/dist/frontend/utils/index.js.map +7 -0
  52. package/dist/frontend/utils/toolMatcher.js +95 -0
  53. package/dist/frontend/utils/toolMatcher.js.map +7 -0
  54. package/dist/index.js +57 -0
  55. package/dist/index.js.map +7 -0
  56. package/dist/modules/ai_assistant/acl.js +14 -0
  57. package/dist/modules/ai_assistant/acl.js.map +7 -0
  58. package/dist/modules/ai_assistant/api/chat/route.js +152 -0
  59. package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
  60. package/dist/modules/ai_assistant/api/health/route.js +27 -0
  61. package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
  62. package/dist/modules/ai_assistant/api/route/route.js +123 -0
  63. package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
  64. package/dist/modules/ai_assistant/api/settings/route.js +60 -0
  65. package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
  66. package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
  67. package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
  68. package/dist/modules/ai_assistant/api/tools/route.js +48 -0
  69. package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
  70. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
  71. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
  72. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
  73. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
  74. package/dist/modules/ai_assistant/cli.js +192 -0
  75. package/dist/modules/ai_assistant/cli.js.map +7 -0
  76. package/dist/modules/ai_assistant/di.js +11 -0
  77. package/dist/modules/ai_assistant/di.js.map +7 -0
  78. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
  79. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
  80. package/dist/modules/ai_assistant/index.js +13 -0
  81. package/dist/modules/ai_assistant/index.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
  83. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
  85. package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
  87. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
  89. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/auth.js +87 -0
  91. package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
  93. package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
  95. package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/http-server.js +367 -0
  97. package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
  99. package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
  101. package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
  103. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
  105. package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
  107. package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
  109. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
  111. package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
  113. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
  115. package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
  117. package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
  119. package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
  121. package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
  123. package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
  125. package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/types.js +1 -0
  127. package/dist/modules/ai_assistant/lib/types.js.map +7 -0
  128. package/package.json +108 -0
  129. package/src/di.ts +11 -0
  130. package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
  131. package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
  132. package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
  133. package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
  134. package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
  135. package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
  136. package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
  137. package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
  138. package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
  139. package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
  140. package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
  141. package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
  142. package/src/frontend/components/CommandPalette/index.ts +14 -0
  143. package/src/frontend/constants.ts +35 -0
  144. package/src/frontend/hooks/index.ts +5 -0
  145. package/src/frontend/hooks/useCommandPalette.ts +1389 -0
  146. package/src/frontend/hooks/useMcpTools.ts +73 -0
  147. package/src/frontend/hooks/usePageContext.ts +61 -0
  148. package/src/frontend/hooks/useRecentActions.ts +64 -0
  149. package/src/frontend/hooks/useRecentTools.ts +69 -0
  150. package/src/frontend/index.ts +39 -0
  151. package/src/frontend/types.ts +260 -0
  152. package/src/frontend/utils/index.ts +1 -0
  153. package/src/frontend/utils/toolMatcher.ts +127 -0
  154. package/src/index.ts +92 -0
  155. package/src/modules/ai_assistant/acl.ts +10 -0
  156. package/src/modules/ai_assistant/api/chat/route.ts +213 -0
  157. package/src/modules/ai_assistant/api/health/route.ts +30 -0
  158. package/src/modules/ai_assistant/api/route/route.ts +149 -0
  159. package/src/modules/ai_assistant/api/settings/route.ts +73 -0
  160. package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
  161. package/src/modules/ai_assistant/api/tools/route.ts +57 -0
  162. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
  163. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
  164. package/src/modules/ai_assistant/cli.ts +233 -0
  165. package/src/modules/ai_assistant/di.ts +9 -0
  166. package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
  167. package/src/modules/ai_assistant/index.ts +11 -0
  168. package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
  169. package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
  170. package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
  171. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
  172. package/src/modules/ai_assistant/lib/auth.ts +185 -0
  173. package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
  174. package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
  175. package/src/modules/ai_assistant/lib/http-server.ts +498 -0
  176. package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
  177. package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
  178. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
  179. package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
  180. package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
  181. package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
  182. package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
  183. package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
  184. package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
  185. package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
  186. package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
  187. package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
  188. package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
  189. package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
  190. package/src/modules/ai_assistant/lib/types.ts +147 -0
  191. package/test-schema.ts +37 -0
  192. package/tsconfig.json +10 -0
  193. package/watch.mjs +6 -0
@@ -0,0 +1,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
+ }