snow-flow 10.0.9 → 10.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.9",
3
+ "version": "10.0.11",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -604,8 +604,23 @@ function DialogAuthServiceNowOAuth() {
604
604
  <text fg={theme.primary} attributes={TextAttributes.BOLD}>
605
605
  Remote Environment Detected
606
606
  </text>
607
- <text fg={theme.text}>Open this URL in your browser to authenticate (copied to clipboard):</text>
607
+ <text fg={theme.text}>Open this URL in your browser to authenticate:</text>
608
608
  <text fg={theme.primary}>{headlessAuthUrl()}</text>
609
+ <box flexDirection="row" gap={2} paddingTop={1}>
610
+ <box
611
+ paddingLeft={2}
612
+ paddingRight={2}
613
+ backgroundColor={theme.primary}
614
+ onMouseUp={() => {
615
+ Clipboard.copy(headlessAuthUrl()).then(
616
+ () => toast.show({ variant: "success", message: "URL copied to clipboard!", duration: 3000 }),
617
+ () => toast.show({ variant: "error", message: "Failed to copy URL", duration: 3000 }),
618
+ )
619
+ }}
620
+ >
621
+ <text fg={theme.selectedListItemText} attributes={TextAttributes.BOLD}>[ Copy URL ]</text>
622
+ </box>
623
+ </box>
609
624
  <box paddingTop={1}>
610
625
  <text fg={theme.textMuted}>After clicking "Allow" in ServiceNow:</text>
611
626
  <text fg={theme.textMuted}> 1. Your browser will redirect to a localhost URL</text>
@@ -12,8 +12,11 @@ import {
12
12
  } from '@modelcontextprotocol/sdk/types.js';
13
13
  import { proxyToolCall, listEnterpriseTools } from './proxy.js';
14
14
  import { mcpDebug } from '../shared/mcp-debug.js';
15
+ import { fetchAndCacheTools, buildEnterpriseToolIndex, ToolSearch, getCurrentSessionId } from './tool-cache.js';
16
+ import { ENTERPRISE_META_TOOLS, executeToolSearch, executeToolExecute } from './meta-tools.js';
15
17
 
16
18
  const VERSION = process.env.SNOW_FLOW_VERSION || '8.30.31';
19
+ const LAZY_TOOLS_ENABLED = process.env.SNOW_ENTERPRISE_LAZY_TOOLS !== 'false';
17
20
 
18
21
  /**
19
22
  * Create MCP Server
@@ -33,6 +36,7 @@ const server = new Server(
33
36
  /**
34
37
  * Handle tools/list request
35
38
  * Returns list of available enterprise tools from license server
39
+ * In lazy mode: returns only meta-tools + session-enabled tools
36
40
  */
37
41
  server.setRequestHandler(ListToolsRequestSchema, async () => {
38
42
  mcpDebug('[Enterprise Proxy] Received tools/list request from MCP client');
@@ -40,8 +44,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
40
44
  try {
41
45
  const tools = await listEnterpriseTools();
42
46
 
43
- mcpDebug(`[Enterprise Proxy] Returning ${tools.length} tools to MCP client`);
47
+ mcpDebug(`[Enterprise Proxy] Fetched ${tools.length} tools from license server`);
48
+
49
+ if (LAZY_TOOLS_ENABLED) {
50
+ // Build search index from fetched tools
51
+ buildEnterpriseToolIndex(tools);
52
+
53
+ // Collect session-enabled tools to include alongside meta-tools
54
+ const sessionId = getCurrentSessionId();
55
+ const enabledTools: { name: string; description: string; inputSchema: any }[] = [];
56
+ if (sessionId) {
57
+ const enabledSet = await ToolSearch.getEnabledTools(sessionId);
58
+ for (const toolId of enabledSet) {
59
+ const found = tools.find((t) => t.name === toolId);
60
+ if (found) {
61
+ enabledTools.push({
62
+ name: found.name,
63
+ description: found.description || `Enterprise tool: ${found.name}`,
64
+ inputSchema: found.inputSchema,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ mcpDebug(`[Enterprise Proxy] Lazy mode: returning ${ENTERPRISE_META_TOOLS.length} meta-tools + ${enabledTools.length} enabled tools`);
71
+ return {
72
+ tools: [...ENTERPRISE_META_TOOLS, ...enabledTools],
73
+ };
74
+ }
44
75
 
76
+ mcpDebug(`[Enterprise Proxy] Returning ${tools.length} tools to MCP client`);
45
77
  return {
46
78
  tools: tools.map((tool) => ({
47
79
  name: tool.name,
@@ -50,13 +82,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
50
82
  })),
51
83
  };
52
84
  } catch (error) {
53
- // Return empty list on error (allows MCP server to start even if enterprise server is down)
54
- mcpDebug('[Enterprise Proxy] Failed to list tools - returning empty list', {
85
+ mcpDebug('[Enterprise Proxy] Failed to list tools', {
55
86
  error: error instanceof Error ? error.message : String(error)
56
87
  });
57
- mcpDebug(
58
- `[Enterprise Proxy] Failed to list tools: ${error instanceof Error ? error.message : String(error)}`
59
- );
88
+
89
+ if (LAZY_TOOLS_ENABLED) {
90
+ // In lazy mode, always return meta-tools so search remains available
91
+ mcpDebug('[Enterprise Proxy] Returning meta-tools only due to backend error');
92
+ return { tools: [...ENTERPRISE_META_TOOLS] };
93
+ }
94
+
95
+ mcpDebug('[Enterprise Proxy] Returning empty tools list due to backend error');
60
96
  return { tools: [] };
61
97
  }
62
98
  });
@@ -64,6 +100,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
64
100
  /**
65
101
  * Handle tools/call request
66
102
  * Proxies tool call to enterprise license server via HTTPS
103
+ * In lazy mode: routes meta-tool calls and enforces tool enabling
67
104
  */
68
105
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
69
106
  const { name, arguments: args } = request.params;
@@ -72,6 +109,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
109
  arguments: args
73
110
  });
74
111
 
112
+ // Route meta-tool calls in lazy mode
113
+ if (LAZY_TOOLS_ENABLED) {
114
+ if (name === 'enterprise_tool_search') {
115
+ return executeToolSearch((args || {}) as Record<string, unknown>);
116
+ }
117
+ if (name === 'enterprise_tool_execute') {
118
+ return executeToolExecute((args || {}) as Record<string, unknown>);
119
+ }
120
+
121
+ // For direct tool calls in lazy mode, check if tool is enabled
122
+ const sessionId = getCurrentSessionId();
123
+ if (sessionId) {
124
+ const canExecute = await ToolSearch.canExecuteTool(sessionId, name);
125
+ if (!canExecute) {
126
+ mcpDebug(`[Enterprise Proxy] Tool ${name} not enabled for session ${sessionId}`);
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text',
131
+ text: `Tool "${name}" is not enabled. Use enterprise_tool_search to find and enable it first.`,
132
+ },
133
+ ],
134
+ isError: true,
135
+ };
136
+ }
137
+ }
138
+ }
139
+
75
140
  try {
76
141
  const result = await proxyToolCall(name, args || {});
77
142
 
@@ -100,7 +165,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
100
165
  content: [
101
166
  {
102
167
  type: 'text',
103
- text: `❌ Enterprise tool error: ${errorMessage}`,
168
+ text: `Enterprise tool error: ${errorMessage}`,
104
169
  },
105
170
  ],
106
171
  isError: true,
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Enterprise Meta-Tools for Lazy Loading
3
+ *
4
+ * Two meta-tools that replace the full 76+ enterprise tool list:
5
+ * - enterprise_tool_search: Search and enable enterprise tools
6
+ * - enterprise_tool_execute: Execute an enabled enterprise tool
7
+ *
8
+ * Prefix `enterprise_` prevents collision with the unified server's
9
+ * `tool_search` / `tool_execute` (both MCP servers run simultaneously).
10
+ */
11
+
12
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js'
13
+ import { ToolSearch, getCurrentSessionId, fetchAndCacheTools, buildEnterpriseToolIndex, getCachedTool } from './tool-cache.js'
14
+ import { proxyToolCall } from './proxy.js'
15
+ import { mcpDebug } from '../shared/mcp-debug.js'
16
+
17
+ /**
18
+ * The two meta-tools exposed in ListTools when lazy loading is active
19
+ */
20
+ export const ENTERPRISE_META_TOOLS: Tool[] = [
21
+ {
22
+ name: 'enterprise_tool_search',
23
+ description:
24
+ 'Search through 76+ enterprise integration tools (Jira, Azure DevOps, Confluence, GitHub, GitLab, Process Mining). ' +
25
+ 'Returns matching tools with their full schemas. Found tools are automatically enabled for the session so you can call them directly. ' +
26
+ 'Always search before using an enterprise tool for the first time.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ query: {
31
+ type: 'string',
32
+ description:
33
+ 'Search query — use tool names (e.g. "jira_create_issue"), domains (e.g. "confluence"), ' +
34
+ 'or actions (e.g. "create sprint", "search issues", "get page").',
35
+ },
36
+ limit: {
37
+ type: 'number',
38
+ description: 'Maximum number of results to return (default: 10, max: 30).',
39
+ },
40
+ enable: {
41
+ type: 'boolean',
42
+ description: 'Whether to automatically enable found tools for direct calling (default: true).',
43
+ },
44
+ },
45
+ required: ['query'],
46
+ },
47
+ },
48
+ {
49
+ name: 'enterprise_tool_execute',
50
+ description:
51
+ 'Execute an enterprise tool by name. Use enterprise_tool_search first to discover available tools. ' +
52
+ 'After searching, tools are enabled and can also be called directly by name without this wrapper.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ tool: {
57
+ type: 'string',
58
+ description: 'The exact tool name to execute (e.g. "jira_get_issue", "confluence_search_pages").',
59
+ },
60
+ args: {
61
+ type: 'object',
62
+ description: 'Arguments to pass to the tool. See the tool schema from enterprise_tool_search results.',
63
+ additionalProperties: true,
64
+ },
65
+ },
66
+ required: ['tool'],
67
+ },
68
+ },
69
+ ]
70
+
71
+ /**
72
+ * Handle enterprise_tool_search calls
73
+ */
74
+ export async function executeToolSearch(args: Record<string, unknown>): Promise<{
75
+ content: { type: string; text: string }[]
76
+ isError?: boolean
77
+ }> {
78
+ const query = args.query as string
79
+ const limit = Math.min((args.limit as number) || 10, 30)
80
+ const enable = args.enable !== false // default true
81
+
82
+ if (!query) {
83
+ return {
84
+ content: [{ type: 'text', text: 'Error: "query" parameter is required.' }],
85
+ isError: true,
86
+ }
87
+ }
88
+
89
+ try {
90
+ // Ensure tools are cached and indexed
91
+ const tools = await fetchAndCacheTools()
92
+ buildEnterpriseToolIndex(tools)
93
+
94
+ // Search the index
95
+ const results = ToolSearch.search(query, limit)
96
+ mcpDebug(`[Enterprise MetaTools] Search "${query}" returned ${results.length} results`)
97
+
98
+ if (results.length === 0) {
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: `No enterprise tools found for "${query}". Try broader terms like "jira", "confluence", "azure", "sprint", "issue", "page".`,
104
+ },
105
+ ],
106
+ }
107
+ }
108
+
109
+ // Enable found tools for the session
110
+ const sessionId = getCurrentSessionId()
111
+ if (enable && sessionId) {
112
+ const toolIds = results.map((r) => r.id)
113
+ await ToolSearch.enableTools(sessionId, toolIds)
114
+ mcpDebug(`[Enterprise MetaTools] Enabled ${toolIds.length} tools for session ${sessionId}`)
115
+ }
116
+
117
+ // Build response with full tool schemas from cache
118
+ const formatted = results.map((entry) => {
119
+ const cached = getCachedTool(entry.id)
120
+ const schema = cached?.inputSchema
121
+ ? JSON.stringify(cached.inputSchema, null, 2)
122
+ : '(schema unavailable — tool will still work)'
123
+ return [
124
+ `### ${entry.id}`,
125
+ `**Category:** ${entry.category}`,
126
+ `**Description:** ${entry.description}`,
127
+ `**Input Schema:**`,
128
+ '```json',
129
+ schema,
130
+ '```',
131
+ ].join('\n')
132
+ })
133
+
134
+ const header = enable && sessionId
135
+ ? `Found ${results.length} enterprise tools (enabled for this session — you can now call them directly):\n`
136
+ : `Found ${results.length} enterprise tools:\n`
137
+
138
+ return {
139
+ content: [{ type: 'text', text: header + '\n' + formatted.join('\n\n') }],
140
+ }
141
+ } catch (error) {
142
+ const msg = error instanceof Error ? error.message : String(error)
143
+ mcpDebug(`[Enterprise MetaTools] Search failed: ${msg}`)
144
+ return {
145
+ content: [{ type: 'text', text: `Enterprise tool search failed: ${msg}` }],
146
+ isError: true,
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Handle enterprise_tool_execute calls
153
+ */
154
+ export async function executeToolExecute(args: Record<string, unknown>): Promise<{
155
+ content: { type: string; text: string }[]
156
+ isError?: boolean
157
+ }> {
158
+ const toolName = args.tool as string
159
+ const toolArgs = (args.args as Record<string, unknown>) || {}
160
+
161
+ if (!toolName) {
162
+ return {
163
+ content: [{ type: 'text', text: 'Error: "tool" parameter is required.' }],
164
+ isError: true,
165
+ }
166
+ }
167
+
168
+ // Check if tool is enabled for the session
169
+ const sessionId = getCurrentSessionId()
170
+ if (sessionId) {
171
+ const canExecute = await ToolSearch.canExecuteTool(sessionId, toolName)
172
+ if (!canExecute) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text',
177
+ text:
178
+ `Tool "${toolName}" is not enabled for this session. ` +
179
+ `Use enterprise_tool_search first to find and enable it.`,
180
+ },
181
+ ],
182
+ isError: true,
183
+ }
184
+ }
185
+ }
186
+
187
+ // Proxy the call to the license server
188
+ try {
189
+ mcpDebug(`[Enterprise MetaTools] Executing tool: ${toolName}`)
190
+ const startTime = Date.now()
191
+ const result = await proxyToolCall(toolName, toolArgs as Record<string, any>)
192
+ const duration = Date.now() - startTime
193
+ mcpDebug(`[Enterprise MetaTools] Tool ${toolName} completed in ${duration}ms`)
194
+
195
+ return {
196
+ content: [
197
+ {
198
+ type: 'text',
199
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
200
+ },
201
+ ],
202
+ }
203
+ } catch (error) {
204
+ const msg = error instanceof Error ? error.message : String(error)
205
+ mcpDebug(`[Enterprise MetaTools] Tool ${toolName} failed: ${msg}`)
206
+ return {
207
+ content: [{ type: 'text', text: `Enterprise tool error (${toolName}): ${msg}` }],
208
+ isError: true,
209
+ }
210
+ }
211
+ }
@@ -39,9 +39,12 @@ import path from 'path';
39
39
  import os from 'os';
40
40
  import { listEnterpriseTools, proxyToolCall } from './proxy.js';
41
41
  import { mcpDebug } from '../shared/mcp-debug.js';
42
+ import { fetchAndCacheTools, buildEnterpriseToolIndex, ToolSearch, getCurrentSessionId } from './tool-cache.js';
43
+ import { ENTERPRISE_META_TOOLS, executeToolSearch, executeToolExecute } from './meta-tools.js';
42
44
 
43
45
  // Configuration from environment variables
44
46
  const LICENSE_SERVER_URL = process.env.SNOW_ENTERPRISE_URL || 'https://enterprise.snow-flow.dev';
47
+ const LAZY_TOOLS_ENABLED = process.env.SNOW_ENTERPRISE_LAZY_TOOLS !== 'false';
45
48
 
46
49
  /**
47
50
  * Check if a valid token source exists
@@ -155,12 +158,41 @@ class EnterpriseProxyServer {
155
158
 
156
159
  mcpDebug(`[Proxy] ✓ ${this.availableTools.length} tools available`);
157
160
 
161
+ if (LAZY_TOOLS_ENABLED) {
162
+ // Build search index from fetched tools
163
+ buildEnterpriseToolIndex(tools);
164
+
165
+ // Collect session-enabled tools to include alongside meta-tools
166
+ const sessionId = getCurrentSessionId();
167
+ const enabledTools: Tool[] = [];
168
+ if (sessionId) {
169
+ const enabledSet = await ToolSearch.getEnabledTools(sessionId);
170
+ for (const toolId of enabledSet) {
171
+ const found = this.availableTools.find(t => t.name === toolId);
172
+ if (found) enabledTools.push(found);
173
+ }
174
+ }
175
+
176
+ mcpDebug(`[Proxy] Lazy mode: returning ${ENTERPRISE_META_TOOLS.length} meta-tools + ${enabledTools.length} enabled tools`);
177
+ return {
178
+ tools: [...ENTERPRISE_META_TOOLS, ...enabledTools]
179
+ };
180
+ }
181
+
158
182
  return {
159
183
  tools: this.availableTools
160
184
  };
161
185
  } catch (error: any) {
162
186
  mcpDebug('[Proxy] ✗ Failed to fetch tools:', error.message);
163
187
 
188
+ if (LAZY_TOOLS_ENABLED) {
189
+ // In lazy mode, always return meta-tools so search remains available
190
+ mcpDebug('[Proxy] Returning meta-tools only due to backend error');
191
+ return {
192
+ tools: [...ENTERPRISE_META_TOOLS]
193
+ };
194
+ }
195
+
164
196
  // Return empty tools list instead of crashing - allows graceful degradation
165
197
  mcpDebug('[Proxy] Returning empty tools list due to backend error');
166
198
  return {
@@ -174,6 +206,34 @@ class EnterpriseProxyServer {
174
206
  var toolName = request.params.name;
175
207
  var toolArgs = request.params.arguments || {};
176
208
 
209
+ // Route meta-tool calls in lazy mode
210
+ if (LAZY_TOOLS_ENABLED) {
211
+ if (toolName === 'enterprise_tool_search') {
212
+ return executeToolSearch(toolArgs as Record<string, unknown>);
213
+ }
214
+ if (toolName === 'enterprise_tool_execute') {
215
+ return executeToolExecute(toolArgs as Record<string, unknown>);
216
+ }
217
+
218
+ // For direct tool calls in lazy mode, check if tool is enabled
219
+ const sessionId = getCurrentSessionId();
220
+ if (sessionId) {
221
+ const canExecute = await ToolSearch.canExecuteTool(sessionId, toolName);
222
+ if (!canExecute) {
223
+ mcpDebug(`[Proxy] Tool ${toolName} not enabled for session ${sessionId}`);
224
+ return {
225
+ content: [
226
+ {
227
+ type: 'text',
228
+ text: `Tool "${toolName}" is not enabled. Use enterprise_tool_search to find and enable it first.`
229
+ }
230
+ ],
231
+ isError: true
232
+ };
233
+ }
234
+ }
235
+ }
236
+
177
237
  try {
178
238
  mcpDebug(`[Proxy] Executing tool: ${toolName}`);
179
239
 
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Enterprise Tool Cache + Search Index
3
+ *
4
+ * Builds a search index from remote enterprise tool definitions fetched via HTTPS.
5
+ * Reuses the shared ToolSearch module for search, session management, and tool enabling.
6
+ *
7
+ * Cache is in-memory with a configurable TTL (default 5 minutes) to avoid
8
+ * hitting the license server on every ListTools request.
9
+ */
10
+
11
+ import { ToolSearch, getCurrentSessionId } from '../servicenow-mcp-unified/shared/tool-search.js'
12
+ import type { ToolIndexEntry } from '../servicenow-mcp-unified/shared/tool-search.js'
13
+ import { listEnterpriseTools } from './proxy.js'
14
+ import type { EnterpriseTool } from './types.js'
15
+ import { mcpDebug } from '../shared/mcp-debug.js'
16
+
17
+ export { ToolSearch, getCurrentSessionId }
18
+ export type { ToolIndexEntry }
19
+
20
+ const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
21
+
22
+ let cachedTools: EnterpriseTool[] = []
23
+ let cacheTimestamp = 0
24
+ let indexBuilt = false
25
+
26
+ /**
27
+ * Infer domain/category from a tool name prefix
28
+ */
29
+ export function inferDomain(toolName: string): string {
30
+ if (toolName.startsWith('jira_')) return 'jira'
31
+ if (toolName.startsWith('azure_') || toolName.startsWith('azdo_')) return 'azure-devops'
32
+ if (toolName.startsWith('confluence_')) return 'confluence'
33
+ if (toolName.startsWith('github_') || toolName.startsWith('gh_')) return 'github'
34
+ if (toolName.startsWith('gitlab_') || toolName.startsWith('gl_')) return 'gitlab'
35
+ if (toolName.startsWith('process_mining_') || toolName.startsWith('pm_')) return 'process-mining'
36
+ return 'enterprise'
37
+ }
38
+
39
+ /**
40
+ * Extract search keywords from tool name and description
41
+ */
42
+ export function extractKeywords(name: string, description: string): string[] {
43
+ const words = new Set<string>()
44
+
45
+ // Split tool name on underscores
46
+ for (const part of name.split('_')) {
47
+ if (part.length > 2) words.add(part.toLowerCase())
48
+ }
49
+
50
+ // Extract meaningful words from description
51
+ const descWords = description.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/)
52
+ const stopWords = new Set([
53
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
54
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
55
+ 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
56
+ 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
57
+ 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
58
+ 'under', 'again', 'further', 'then', 'once', 'and', 'but', 'or',
59
+ 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
60
+ 'this', 'that', 'these', 'those', 'it', 'its',
61
+ ])
62
+ for (const w of descWords) {
63
+ if (w.length > 2 && !stopWords.has(w)) words.add(w)
64
+ }
65
+
66
+ return Array.from(words)
67
+ }
68
+
69
+ /**
70
+ * Fetch enterprise tools from license server with caching
71
+ * Returns cached tools if still fresh, otherwise fetches new ones
72
+ */
73
+ export async function fetchAndCacheTools(): Promise<EnterpriseTool[]> {
74
+ const now = Date.now()
75
+ if (cachedTools.length > 0 && now - cacheTimestamp < CACHE_TTL_MS) {
76
+ mcpDebug(`[Enterprise ToolCache] Using cached tools (${cachedTools.length} tools, age: ${Math.round((now - cacheTimestamp) / 1000)}s)`)
77
+ return cachedTools
78
+ }
79
+
80
+ mcpDebug('[Enterprise ToolCache] Fetching tools from license server...')
81
+ const tools = await listEnterpriseTools()
82
+ cachedTools = tools
83
+ cacheTimestamp = Date.now()
84
+ mcpDebug(`[Enterprise ToolCache] Cached ${tools.length} tools`)
85
+ return tools
86
+ }
87
+
88
+ /**
89
+ * Build the ToolSearch index from enterprise tools
90
+ * All enterprise tools are registered as deferred (lazy-loaded)
91
+ */
92
+ export function buildEnterpriseToolIndex(tools: EnterpriseTool[]): void {
93
+ if (indexBuilt && cachedTools.length === tools.length) return
94
+
95
+ const entries: ToolIndexEntry[] = tools.map((tool) => ({
96
+ id: tool.name,
97
+ description: tool.description || `Enterprise tool: ${tool.name}`,
98
+ category: inferDomain(tool.name),
99
+ keywords: extractKeywords(tool.name, tool.description || ''),
100
+ deferred: true,
101
+ }))
102
+
103
+ ToolSearch.clearIndex()
104
+ ToolSearch.registerTools(entries)
105
+ indexBuilt = true
106
+
107
+ const stats = ToolSearch.getStats()
108
+ mcpDebug(`[Enterprise ToolCache] Index built: ${stats.total} tools across ${Object.keys(stats.categories).length} categories`)
109
+ }
110
+
111
+ /**
112
+ * Get a single cached tool definition by name
113
+ */
114
+ export function getCachedTool(name: string): EnterpriseTool | undefined {
115
+ return cachedTools.find((t) => t.name === name)
116
+ }
117
+
118
+ /**
119
+ * Get all cached tool definitions
120
+ */
121
+ export function getCachedTools(): EnterpriseTool[] {
122
+ return cachedTools
123
+ }
124
+
125
+ /**
126
+ * Force refetch on next request
127
+ */
128
+ export function invalidateCache(): void {
129
+ cachedTools = []
130
+ cacheTimestamp = 0
131
+ indexBuilt = false
132
+ ToolSearch.clearIndex()
133
+ mcpDebug('[Enterprise ToolCache] Cache invalidated')
134
+ }
@@ -58,6 +58,11 @@ export interface EnterpriseToolListResponse {
58
58
  tools: EnterpriseTool[];
59
59
  }
60
60
 
61
+ export interface EnterpriseToolCacheConfig {
62
+ cacheTtlMs: number;
63
+ maxCacheSize: number;
64
+ }
65
+
61
66
  export interface LicenseValidationResponse {
62
67
  valid: boolean;
63
68
  error?: string;