snow-flow 2.9.0 → 2.9.5
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/check-mcp-resources.sh +85 -0
- package/dist/config/snow-flow-config.js +1 -1
- package/dist/mcp/mcp-on-demand-proxy.d.ts +7 -0
- package/dist/mcp/mcp-on-demand-proxy.js +187 -0
- package/dist/mcp/servicenow-deployment-mcp.js +132 -55
- package/dist/mcp/servicenow-machine-learning-mcp.js +310 -60
- package/dist/mcp/servicenow-operations-mcp.d.ts +71 -1
- package/dist/mcp/servicenow-operations-mcp.js +10 -6
- package/dist/test-smart-limits.d.ts +13 -0
- package/dist/test-smart-limits.js +100 -0
- package/dist/utils/deployment-auth-fix.d.ts +43 -0
- package/dist/utils/deployment-auth-fix.js +272 -0
- package/dist/utils/mcp-on-demand-manager.d.ts +69 -0
- package/dist/utils/mcp-on-demand-manager.js +309 -0
- package/dist/utils/mcp-process-manager.d.ts +58 -0
- package/dist/utils/mcp-process-manager.js +220 -0
- package/dist/utils/mcp-server-manager.js +50 -9
- package/dist/utils/mcp-singleton-lock.d.ts +4 -0
- package/dist/utils/mcp-singleton-lock.js +35 -8
- package/dist/utils/ml-data-fetcher.d.ts +66 -0
- package/dist/utils/ml-data-fetcher.js +288 -0
- package/dist/utils/servicenow-client.js +43 -17
- package/dist/utils/timeout-manager.d.ts +62 -0
- package/dist/utils/timeout-manager.js +352 -0
- package/package.json +1 -1
- package/test-ml-improvements.sh +76 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Authentication Fix
|
|
3
|
+
* Ensures OAuth tokens are properly refreshed and validated before deployment operations
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthValidationResult {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
hasWriteScope: boolean;
|
|
8
|
+
tokenAge?: number;
|
|
9
|
+
expiresIn?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
recommendations?: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare class DeploymentAuthManager {
|
|
14
|
+
private oauth;
|
|
15
|
+
private lastTokenRefresh;
|
|
16
|
+
constructor();
|
|
17
|
+
/**
|
|
18
|
+
* Ensure we have valid tokens for deployment operations
|
|
19
|
+
* This is MORE strict than regular authentication
|
|
20
|
+
*/
|
|
21
|
+
ensureDeploymentAuth(): Promise<AuthValidationResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Validate token by making a simple API call
|
|
24
|
+
*/
|
|
25
|
+
private validateTokenWithAPI;
|
|
26
|
+
/**
|
|
27
|
+
* Check if token has write permissions by attempting to read widget table
|
|
28
|
+
*/
|
|
29
|
+
private checkWritePermissions;
|
|
30
|
+
/**
|
|
31
|
+
* Force a fresh token for deployment operations
|
|
32
|
+
*/
|
|
33
|
+
forceTokenRefresh(): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
accessToken?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Get fresh access token for deployment
|
|
40
|
+
*/
|
|
41
|
+
getDeploymentToken(): Promise<string | null>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=deployment-auth-fix.d.ts.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Deployment Authentication Fix
|
|
4
|
+
* Ensures OAuth tokens are properly refreshed and validated before deployment operations
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DeploymentAuthManager = void 0;
|
|
8
|
+
const snow_oauth_js_1 = require("./snow-oauth.js");
|
|
9
|
+
const unified_auth_store_js_1 = require("./unified-auth-store.js");
|
|
10
|
+
const logger_js_1 = require("./logger.js");
|
|
11
|
+
const logger = new logger_js_1.Logger('DeploymentAuthFix');
|
|
12
|
+
class DeploymentAuthManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.lastTokenRefresh = 0;
|
|
15
|
+
this.oauth = new snow_oauth_js_1.ServiceNowOAuth();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ensure we have valid tokens for deployment operations
|
|
19
|
+
* This is MORE strict than regular authentication
|
|
20
|
+
*/
|
|
21
|
+
async ensureDeploymentAuth() {
|
|
22
|
+
try {
|
|
23
|
+
logger.info('🔐 Validating deployment authentication...');
|
|
24
|
+
// Step 1: Check if we have any auth at all
|
|
25
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
26
|
+
if (!isAuth) {
|
|
27
|
+
return {
|
|
28
|
+
isValid: false,
|
|
29
|
+
hasWriteScope: false,
|
|
30
|
+
error: 'Not authenticated',
|
|
31
|
+
recommendations: [
|
|
32
|
+
'Run: snow-flow auth login',
|
|
33
|
+
'Ensure OAuth app has write/admin scopes'
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Step 2: Get current tokens
|
|
38
|
+
let tokens = await this.oauth.loadTokens();
|
|
39
|
+
if (!tokens || !tokens.accessToken) {
|
|
40
|
+
// Try unified auth store as fallback
|
|
41
|
+
tokens = await unified_auth_store_js_1.unifiedAuthStore.getTokens();
|
|
42
|
+
if (!tokens || !tokens.accessToken) {
|
|
43
|
+
return {
|
|
44
|
+
isValid: false,
|
|
45
|
+
hasWriteScope: false,
|
|
46
|
+
error: 'No access token found',
|
|
47
|
+
recommendations: [
|
|
48
|
+
'Run: snow-flow auth login',
|
|
49
|
+
'Check .env configuration'
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Step 3: Check token age and expiry
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const tokenAge = tokens.issuedAt ? now - tokens.issuedAt : null;
|
|
57
|
+
const expiresIn = tokens.expiresAt ? tokens.expiresAt - now : null;
|
|
58
|
+
// If token expires in less than 5 minutes, refresh it
|
|
59
|
+
if (expiresIn && expiresIn < 300000) { // 5 minutes
|
|
60
|
+
logger.info('⚠️ Token expires soon, refreshing...');
|
|
61
|
+
try {
|
|
62
|
+
const refreshResult = await this.oauth.refreshAccessToken();
|
|
63
|
+
if (refreshResult.success && refreshResult.accessToken) {
|
|
64
|
+
logger.info('✅ Token refreshed successfully');
|
|
65
|
+
tokens = {
|
|
66
|
+
...tokens,
|
|
67
|
+
accessToken: refreshResult.accessToken,
|
|
68
|
+
expiresAt: refreshResult.expiresIn ? now + refreshResult.expiresIn * 1000 : undefined,
|
|
69
|
+
issuedAt: now
|
|
70
|
+
};
|
|
71
|
+
// Update unified auth store
|
|
72
|
+
await unified_auth_store_js_1.unifiedAuthStore.saveTokens(tokens);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger.warn('Failed to refresh token:', refreshResult.error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
logger.error('Token refresh error:', error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Step 4: Validate token by making a test API call
|
|
83
|
+
try {
|
|
84
|
+
const testResponse = await this.validateTokenWithAPI(tokens.accessToken);
|
|
85
|
+
if (!testResponse.success) {
|
|
86
|
+
// Token is invalid, try to refresh
|
|
87
|
+
logger.warn('Token validation failed, attempting refresh...');
|
|
88
|
+
const refreshResult = await this.oauth.refreshAccessToken();
|
|
89
|
+
if (refreshResult.success && refreshResult.accessToken) {
|
|
90
|
+
tokens.accessToken = refreshResult.accessToken;
|
|
91
|
+
await unified_auth_store_js_1.unifiedAuthStore.saveTokens(tokens);
|
|
92
|
+
// Validate again
|
|
93
|
+
const retryResponse = await this.validateTokenWithAPI(tokens.accessToken);
|
|
94
|
+
if (!retryResponse.success) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: false,
|
|
97
|
+
hasWriteScope: false,
|
|
98
|
+
error: 'Token validation failed after refresh',
|
|
99
|
+
recommendations: [
|
|
100
|
+
'Run: snow-flow auth login',
|
|
101
|
+
'Check ServiceNow OAuth app configuration',
|
|
102
|
+
'Verify API access is enabled for your user'
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return {
|
|
109
|
+
isValid: false,
|
|
110
|
+
hasWriteScope: false,
|
|
111
|
+
error: 'Failed to refresh invalid token',
|
|
112
|
+
recommendations: [
|
|
113
|
+
'Run: snow-flow auth login',
|
|
114
|
+
'Your session may have expired'
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.error('Token validation error:', error);
|
|
122
|
+
return {
|
|
123
|
+
isValid: false,
|
|
124
|
+
hasWriteScope: false,
|
|
125
|
+
error: `Token validation failed: ${error.message}`,
|
|
126
|
+
recommendations: [
|
|
127
|
+
'Check network connectivity',
|
|
128
|
+
'Verify ServiceNow instance is accessible',
|
|
129
|
+
'Run: snow-flow auth login'
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Step 5: Check for write permissions
|
|
134
|
+
const hasWriteScope = await this.checkWritePermissions(tokens.accessToken);
|
|
135
|
+
return {
|
|
136
|
+
isValid: true,
|
|
137
|
+
hasWriteScope,
|
|
138
|
+
tokenAge: tokenAge ? Math.round(tokenAge / 1000) : undefined,
|
|
139
|
+
expiresIn: expiresIn ? Math.round(expiresIn / 1000) : undefined,
|
|
140
|
+
recommendations: hasWriteScope ? [] : [
|
|
141
|
+
'OAuth token is valid but may lack write permissions',
|
|
142
|
+
'Check OAuth app scopes in ServiceNow',
|
|
143
|
+
'Ensure user has sp_admin or admin role'
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.error('Deployment auth validation error:', error);
|
|
149
|
+
return {
|
|
150
|
+
isValid: false,
|
|
151
|
+
hasWriteScope: false,
|
|
152
|
+
error: error.message,
|
|
153
|
+
recommendations: [
|
|
154
|
+
'Unexpected error during authentication',
|
|
155
|
+
'Run: snow-flow auth login',
|
|
156
|
+
'Check error logs for details'
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Validate token by making a simple API call
|
|
163
|
+
*/
|
|
164
|
+
async validateTokenWithAPI(accessToken) {
|
|
165
|
+
try {
|
|
166
|
+
const axios = require('axios');
|
|
167
|
+
const credentials = await this.oauth.loadCredentials();
|
|
168
|
+
if (!credentials?.instance) {
|
|
169
|
+
return { success: false, error: 'No instance configured' };
|
|
170
|
+
}
|
|
171
|
+
const response = await axios.get(`https://${credentials.instance}/api/now/table/sys_user?sysparm_limit=1`, {
|
|
172
|
+
headers: {
|
|
173
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
174
|
+
'Accept': 'application/json'
|
|
175
|
+
},
|
|
176
|
+
timeout: 10000
|
|
177
|
+
});
|
|
178
|
+
return { success: response.status === 200 };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.response?.status === 401) {
|
|
182
|
+
return { success: false, error: 'Token is invalid or expired' };
|
|
183
|
+
}
|
|
184
|
+
return { success: false, error: error.message };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if token has write permissions by attempting to read widget table
|
|
189
|
+
*/
|
|
190
|
+
async checkWritePermissions(accessToken) {
|
|
191
|
+
try {
|
|
192
|
+
const axios = require('axios');
|
|
193
|
+
const credentials = await this.oauth.loadCredentials();
|
|
194
|
+
if (!credentials?.instance) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Try to read from sp_widget table (requires portal access)
|
|
198
|
+
const response = await axios.get(`https://${credentials.instance}/api/now/table/sp_widget?sysparm_limit=1`, {
|
|
199
|
+
headers: {
|
|
200
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
201
|
+
'Accept': 'application/json'
|
|
202
|
+
},
|
|
203
|
+
timeout: 10000
|
|
204
|
+
});
|
|
205
|
+
// If we can read widgets, we likely have portal access
|
|
206
|
+
return response.status === 200;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// 403 means we don't have permission
|
|
210
|
+
if (error.response?.status === 403) {
|
|
211
|
+
logger.warn('No write permissions for Service Portal');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
// Other errors we'll assume no permission
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Force a fresh token for deployment operations
|
|
220
|
+
*/
|
|
221
|
+
async forceTokenRefresh() {
|
|
222
|
+
try {
|
|
223
|
+
logger.info('🔄 Forcing token refresh for deployment...');
|
|
224
|
+
const refreshResult = await this.oauth.refreshAccessToken();
|
|
225
|
+
if (refreshResult.success && refreshResult.accessToken) {
|
|
226
|
+
// Store in unified auth store
|
|
227
|
+
const tokens = await this.oauth.loadTokens();
|
|
228
|
+
if (tokens) {
|
|
229
|
+
tokens.accessToken = refreshResult.accessToken;
|
|
230
|
+
tokens.expiresAt = refreshResult.expiresIn ? Date.now() + refreshResult.expiresIn * 1000 : undefined;
|
|
231
|
+
tokens.issuedAt = Date.now();
|
|
232
|
+
await unified_auth_store_js_1.unifiedAuthStore.saveTokens(tokens);
|
|
233
|
+
}
|
|
234
|
+
logger.info('✅ Token refreshed successfully');
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
accessToken: refreshResult.accessToken
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: refreshResult.error || 'Failed to refresh token'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
logger.error('Force refresh error:', error);
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: error.message
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get fresh access token for deployment
|
|
255
|
+
*/
|
|
256
|
+
async getDeploymentToken() {
|
|
257
|
+
// First ensure we have valid auth
|
|
258
|
+
const authResult = await this.ensureDeploymentAuth();
|
|
259
|
+
if (!authResult.isValid) {
|
|
260
|
+
logger.error('Invalid authentication for deployment:', authResult.error);
|
|
261
|
+
throw new Error(authResult.error || 'Authentication failed');
|
|
262
|
+
}
|
|
263
|
+
if (!authResult.hasWriteScope) {
|
|
264
|
+
logger.warn('⚠️ Token may lack write permissions, deployment might fail');
|
|
265
|
+
}
|
|
266
|
+
// Get the token
|
|
267
|
+
const tokens = await this.oauth.loadTokens();
|
|
268
|
+
return tokens?.accessToken || null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
exports.DeploymentAuthManager = DeploymentAuthManager;
|
|
272
|
+
//# sourceMappingURL=deployment-auth-fix.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP On-Demand Manager
|
|
3
|
+
* Starts MCP servers only when needed and stops them after inactivity
|
|
4
|
+
*/
|
|
5
|
+
import { ChildProcess } from 'child_process';
|
|
6
|
+
export declare class MCPOnDemandManager {
|
|
7
|
+
private static instance;
|
|
8
|
+
private servers;
|
|
9
|
+
private inactivityTimeout;
|
|
10
|
+
private cleanupInterval?;
|
|
11
|
+
private constructor();
|
|
12
|
+
static getInstance(): MCPOnDemandManager;
|
|
13
|
+
/**
|
|
14
|
+
* Get or start an MCP server on demand
|
|
15
|
+
*/
|
|
16
|
+
getServer(serverName: string): Promise<ChildProcess>;
|
|
17
|
+
/**
|
|
18
|
+
* Start an MCP server
|
|
19
|
+
*/
|
|
20
|
+
private startServer;
|
|
21
|
+
/**
|
|
22
|
+
* Wait for a server to finish starting
|
|
23
|
+
*/
|
|
24
|
+
private waitForServer;
|
|
25
|
+
/**
|
|
26
|
+
* Stop a specific server
|
|
27
|
+
*/
|
|
28
|
+
stopServer(serverName: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Stop least recently used servers to free resources
|
|
31
|
+
*/
|
|
32
|
+
private stopLeastRecentlyUsed;
|
|
33
|
+
/**
|
|
34
|
+
* Stop all inactive servers
|
|
35
|
+
*/
|
|
36
|
+
private stopInactiveServers;
|
|
37
|
+
/**
|
|
38
|
+
* Start monitoring for inactive servers
|
|
39
|
+
*/
|
|
40
|
+
private startInactivityMonitor;
|
|
41
|
+
/**
|
|
42
|
+
* Stop the inactivity monitor
|
|
43
|
+
*/
|
|
44
|
+
stopInactivityMonitor(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Get the script path for a server
|
|
47
|
+
*/
|
|
48
|
+
private getScriptPath;
|
|
49
|
+
/**
|
|
50
|
+
* Get status of all servers
|
|
51
|
+
*/
|
|
52
|
+
getStatus(): {
|
|
53
|
+
total: number;
|
|
54
|
+
running: number;
|
|
55
|
+
stopped: number;
|
|
56
|
+
servers: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
status: string;
|
|
59
|
+
lastUsed: string;
|
|
60
|
+
useCount: number;
|
|
61
|
+
uptime?: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Stop all servers
|
|
66
|
+
*/
|
|
67
|
+
stopAll(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=mcp-on-demand-manager.d.ts.map
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP On-Demand Manager
|
|
4
|
+
* Starts MCP servers only when needed and stops them after inactivity
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MCPOnDemandManager = void 0;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const logger_js_1 = require("./logger.js");
|
|
13
|
+
const mcp_process_manager_js_1 = require("./mcp-process-manager.js");
|
|
14
|
+
const unified_auth_store_js_1 = require("./unified-auth-store.js");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const logger = new logger_js_1.Logger('MCPOnDemand');
|
|
18
|
+
class MCPOnDemandManager {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.servers = new Map();
|
|
21
|
+
this.inactivityTimeout = parseInt(process.env.SNOW_MCP_INACTIVITY_TIMEOUT || '300000'); // 5 minutes default
|
|
22
|
+
this.startInactivityMonitor();
|
|
23
|
+
}
|
|
24
|
+
static getInstance() {
|
|
25
|
+
if (!MCPOnDemandManager.instance) {
|
|
26
|
+
MCPOnDemandManager.instance = new MCPOnDemandManager();
|
|
27
|
+
}
|
|
28
|
+
return MCPOnDemandManager.instance;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get or start an MCP server on demand
|
|
32
|
+
*/
|
|
33
|
+
async getServer(serverName) {
|
|
34
|
+
let server = this.servers.get(serverName);
|
|
35
|
+
if (!server) {
|
|
36
|
+
server = {
|
|
37
|
+
name: serverName,
|
|
38
|
+
lastUsed: Date.now(),
|
|
39
|
+
useCount: 0,
|
|
40
|
+
status: 'stopped'
|
|
41
|
+
};
|
|
42
|
+
this.servers.set(serverName, server);
|
|
43
|
+
}
|
|
44
|
+
// Update last used time
|
|
45
|
+
server.lastUsed = Date.now();
|
|
46
|
+
server.useCount++;
|
|
47
|
+
// If server is running, return it
|
|
48
|
+
if (server.status === 'running' && server.process) {
|
|
49
|
+
logger.debug(`✅ Reusing existing ${serverName} (used ${server.useCount} times)`);
|
|
50
|
+
return server.process;
|
|
51
|
+
}
|
|
52
|
+
// If server is starting, wait for it
|
|
53
|
+
if (server.status === 'starting') {
|
|
54
|
+
logger.debug(`⏳ Waiting for ${serverName} to start...`);
|
|
55
|
+
return this.waitForServer(serverName);
|
|
56
|
+
}
|
|
57
|
+
// Start the server
|
|
58
|
+
return this.startServer(serverName);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Start an MCP server
|
|
62
|
+
*/
|
|
63
|
+
async startServer(serverName) {
|
|
64
|
+
const server = this.servers.get(serverName);
|
|
65
|
+
if (!server) {
|
|
66
|
+
throw new Error(`Server ${serverName} not found`);
|
|
67
|
+
}
|
|
68
|
+
// Check resource limits
|
|
69
|
+
const processManager = mcp_process_manager_js_1.MCPProcessManager.getInstance();
|
|
70
|
+
if (!processManager.canSpawnServer()) {
|
|
71
|
+
// Try to free up resources by stopping least recently used servers
|
|
72
|
+
await this.stopLeastRecentlyUsed();
|
|
73
|
+
if (!processManager.canSpawnServer()) {
|
|
74
|
+
throw new Error('Cannot start server: resource limits exceeded');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
server.status = 'starting';
|
|
78
|
+
logger.info(`🚀 Starting ${serverName} on demand...`);
|
|
79
|
+
try {
|
|
80
|
+
// Get the script path
|
|
81
|
+
const scriptPath = this.getScriptPath(serverName);
|
|
82
|
+
// Get auth tokens
|
|
83
|
+
await unified_auth_store_js_1.unifiedAuthStore.bridgeToMCP();
|
|
84
|
+
const tokens = await unified_auth_store_js_1.unifiedAuthStore.getTokens();
|
|
85
|
+
const authEnv = {};
|
|
86
|
+
if (tokens) {
|
|
87
|
+
authEnv.SNOW_OAUTH_TOKENS = JSON.stringify(tokens);
|
|
88
|
+
authEnv.SNOW_INSTANCE = tokens.instance;
|
|
89
|
+
authEnv.SNOW_CLIENT_ID = tokens.clientId;
|
|
90
|
+
authEnv.SNOW_CLIENT_SECRET = tokens.clientSecret;
|
|
91
|
+
if (tokens.accessToken) {
|
|
92
|
+
authEnv.SNOW_ACCESS_TOKEN = tokens.accessToken;
|
|
93
|
+
}
|
|
94
|
+
if (tokens.refreshToken) {
|
|
95
|
+
authEnv.SNOW_REFRESH_TOKEN = tokens.refreshToken;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Start the process
|
|
99
|
+
const childProcess = (0, child_process_1.spawn)('node', [scriptPath], {
|
|
100
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
...authEnv,
|
|
104
|
+
SNOW_MCP_ON_DEMAND: 'true' // Flag to indicate on-demand mode
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// Handle process events
|
|
108
|
+
childProcess.on('error', (error) => {
|
|
109
|
+
logger.error(`${serverName} error:`, error);
|
|
110
|
+
server.status = 'stopped';
|
|
111
|
+
});
|
|
112
|
+
childProcess.on('exit', (code) => {
|
|
113
|
+
logger.info(`${serverName} exited with code ${code}`);
|
|
114
|
+
server.status = 'stopped';
|
|
115
|
+
server.process = undefined;
|
|
116
|
+
});
|
|
117
|
+
// Log output for debugging
|
|
118
|
+
childProcess.stdout?.on('data', (data) => {
|
|
119
|
+
logger.debug(`${serverName} stdout:`, data.toString());
|
|
120
|
+
});
|
|
121
|
+
childProcess.stderr?.on('data', (data) => {
|
|
122
|
+
logger.debug(`${serverName} stderr:`, data.toString());
|
|
123
|
+
});
|
|
124
|
+
server.process = childProcess;
|
|
125
|
+
server.status = 'running';
|
|
126
|
+
server.startTime = Date.now();
|
|
127
|
+
logger.info(`✅ ${serverName} started (PID: ${childProcess.pid})`);
|
|
128
|
+
return childProcess;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
server.status = 'stopped';
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Wait for a server to finish starting
|
|
137
|
+
*/
|
|
138
|
+
async waitForServer(serverName, timeout = 30000) {
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
while (Date.now() - startTime < timeout) {
|
|
141
|
+
const server = this.servers.get(serverName);
|
|
142
|
+
if (server?.status === 'running' && server.process) {
|
|
143
|
+
return server.process;
|
|
144
|
+
}
|
|
145
|
+
if (server?.status === 'stopped') {
|
|
146
|
+
throw new Error(`Server ${serverName} failed to start`);
|
|
147
|
+
}
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Timeout waiting for ${serverName} to start`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Stop a specific server
|
|
154
|
+
*/
|
|
155
|
+
async stopServer(serverName) {
|
|
156
|
+
const server = this.servers.get(serverName);
|
|
157
|
+
if (!server || !server.process) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
server.status = 'stopping';
|
|
161
|
+
logger.info(`🛑 Stopping ${serverName} (was used ${server.useCount} times)`);
|
|
162
|
+
try {
|
|
163
|
+
server.process.kill('SIGTERM');
|
|
164
|
+
// Wait for graceful shutdown
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
const timeout = setTimeout(() => {
|
|
167
|
+
if (server.process) {
|
|
168
|
+
server.process.kill('SIGKILL');
|
|
169
|
+
}
|
|
170
|
+
resolve(undefined);
|
|
171
|
+
}, 5000);
|
|
172
|
+
server.process?.once('exit', () => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
resolve(undefined);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
logger.error(`Error stopping ${serverName}:`, error);
|
|
180
|
+
}
|
|
181
|
+
server.status = 'stopped';
|
|
182
|
+
server.process = undefined;
|
|
183
|
+
server.startTime = undefined;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Stop least recently used servers to free resources
|
|
187
|
+
*/
|
|
188
|
+
async stopLeastRecentlyUsed() {
|
|
189
|
+
const runningServers = Array.from(this.servers.values())
|
|
190
|
+
.filter(s => s.status === 'running')
|
|
191
|
+
.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
192
|
+
if (runningServers.length > 0) {
|
|
193
|
+
const oldest = runningServers[0];
|
|
194
|
+
logger.info(`📦 Stopping least recently used server: ${oldest.name}`);
|
|
195
|
+
await this.stopServer(oldest.name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Stop all inactive servers
|
|
200
|
+
*/
|
|
201
|
+
async stopInactiveServers() {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const promises = [];
|
|
204
|
+
for (const [name, server] of this.servers) {
|
|
205
|
+
if (server.status === 'running' &&
|
|
206
|
+
now - server.lastUsed > this.inactivityTimeout) {
|
|
207
|
+
const inactiveMinutes = Math.round((now - server.lastUsed) / 60000);
|
|
208
|
+
logger.info(`⏰ Stopping ${name} due to inactivity (${inactiveMinutes} minutes)`);
|
|
209
|
+
promises.push(this.stopServer(name));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await Promise.all(promises);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Start monitoring for inactive servers
|
|
216
|
+
*/
|
|
217
|
+
startInactivityMonitor() {
|
|
218
|
+
// Check every minute
|
|
219
|
+
this.cleanupInterval = setInterval(() => {
|
|
220
|
+
this.stopInactiveServers().catch(error => {
|
|
221
|
+
logger.error('Error during inactivity cleanup:', error);
|
|
222
|
+
});
|
|
223
|
+
}, 60000);
|
|
224
|
+
// Don't block process exit
|
|
225
|
+
this.cleanupInterval.unref();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Stop the inactivity monitor
|
|
229
|
+
*/
|
|
230
|
+
stopInactivityMonitor() {
|
|
231
|
+
if (this.cleanupInterval) {
|
|
232
|
+
clearInterval(this.cleanupInterval);
|
|
233
|
+
this.cleanupInterval = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get the script path for a server
|
|
238
|
+
*/
|
|
239
|
+
getScriptPath(serverName) {
|
|
240
|
+
// Map server names to script files
|
|
241
|
+
const serverMap = {
|
|
242
|
+
'servicenow-operations': 'servicenow-operations-mcp.js',
|
|
243
|
+
'servicenow-deployment': 'servicenow-deployment-mcp.js',
|
|
244
|
+
'servicenow-machine-learning': 'servicenow-machine-learning-mcp.js',
|
|
245
|
+
'servicenow-update-set': 'servicenow-update-set-mcp.js',
|
|
246
|
+
'servicenow-platform-development': 'servicenow-platform-development-mcp.js',
|
|
247
|
+
'servicenow-integration': 'servicenow-integration-mcp.js',
|
|
248
|
+
'servicenow-automation': 'servicenow-automation-mcp.js',
|
|
249
|
+
'servicenow-security-compliance': 'servicenow-security-compliance-mcp.js',
|
|
250
|
+
'servicenow-reporting-analytics': 'servicenow-reporting-analytics-mcp.js',
|
|
251
|
+
'servicenow-flow-composer': 'servicenow-flow-composer-mcp.js',
|
|
252
|
+
'servicenow-intelligent': 'servicenow-intelligent-mcp.js',
|
|
253
|
+
'servicenow-development-assistant': 'servicenow-development-assistant-mcp.js',
|
|
254
|
+
'servicenow-graph-memory': 'servicenow-graph-memory-mcp.js',
|
|
255
|
+
'snow-flow': 'snow-flow-mcp.js'
|
|
256
|
+
};
|
|
257
|
+
const scriptFile = serverMap[serverName];
|
|
258
|
+
if (!scriptFile) {
|
|
259
|
+
throw new Error(`Unknown server: ${serverName}`);
|
|
260
|
+
}
|
|
261
|
+
// Check different possible locations
|
|
262
|
+
const possiblePaths = [
|
|
263
|
+
path_1.default.join(__dirname, '..', 'mcp', scriptFile),
|
|
264
|
+
path_1.default.join(process.cwd(), 'dist', 'mcp', scriptFile),
|
|
265
|
+
path_1.default.join('/Users/nielsvanderwerf/.nvm/versions/node/v20.15.0/lib/node_modules/snow-flow/dist/mcp', scriptFile)
|
|
266
|
+
];
|
|
267
|
+
for (const scriptPath of possiblePaths) {
|
|
268
|
+
if (fs_1.default.existsSync(scriptPath)) {
|
|
269
|
+
return scriptPath;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Script not found for ${serverName}: ${scriptFile}`);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get status of all servers
|
|
276
|
+
*/
|
|
277
|
+
getStatus() {
|
|
278
|
+
const servers = Array.from(this.servers.values()).map(server => {
|
|
279
|
+
const lastUsedMinutes = Math.round((Date.now() - server.lastUsed) / 60000);
|
|
280
|
+
const uptime = server.startTime ?
|
|
281
|
+
Math.round((Date.now() - server.startTime) / 60000) + ' minutes' :
|
|
282
|
+
undefined;
|
|
283
|
+
return {
|
|
284
|
+
name: server.name,
|
|
285
|
+
status: server.status,
|
|
286
|
+
lastUsed: `${lastUsedMinutes} minutes ago`,
|
|
287
|
+
useCount: server.useCount,
|
|
288
|
+
uptime
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
total: servers.length,
|
|
293
|
+
running: servers.filter(s => s.status === 'running').length,
|
|
294
|
+
stopped: servers.filter(s => s.status === 'stopped').length,
|
|
295
|
+
servers
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Stop all servers
|
|
300
|
+
*/
|
|
301
|
+
async stopAll() {
|
|
302
|
+
logger.info('🛑 Stopping all MCP servers...');
|
|
303
|
+
const promises = Array.from(this.servers.keys()).map(name => this.stopServer(name));
|
|
304
|
+
await Promise.all(promises);
|
|
305
|
+
this.stopInactivityMonitor();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
exports.MCPOnDemandManager = MCPOnDemandManager;
|
|
309
|
+
//# sourceMappingURL=mcp-on-demand-manager.js.map
|