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 +1 -1
- package/src/cli/cmd/tui/component/dialog-auth.tsx +16 -1
- package/src/servicenow/enterprise-proxy/index.ts +72 -7
- package/src/servicenow/enterprise-proxy/meta-tools.ts +211 -0
- package/src/servicenow/enterprise-proxy/server.ts +60 -0
- package/src/servicenow/enterprise-proxy/tool-cache.ts +134 -0
- package/src/servicenow/enterprise-proxy/types.ts +5 -0
package/package.json
CHANGED
|
@@ -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
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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:
|
|
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;
|