@probelabs/probe 0.6.0-rc207 → 0.6.0-rc208
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/bin/binaries/probe-v0.6.0-rc208-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc208-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc208-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc208-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc208-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/bashPermissions.js +88 -7
- package/build/agent/index.js +334 -16
- package/build/agent/mcp/client.js +234 -4
- package/build/agent/mcp/config.js +87 -0
- package/build/agent/mcp/xmlBridge.js +15 -5
- package/build/agent/simpleTelemetry.js +26 -0
- package/build/tools/bash.js +5 -3
- package/cjs/agent/ProbeAgent.cjs +312 -16
- package/cjs/agent/simpleTelemetry.cjs +22 -0
- package/cjs/index.cjs +334 -16
- package/package.json +1 -1
- package/src/agent/bashPermissions.js +88 -7
- package/src/agent/mcp/client.js +234 -4
- package/src/agent/mcp/config.js +87 -0
- package/src/agent/mcp/xmlBridge.js +15 -5
- package/src/agent/simpleTelemetry.js +26 -0
- package/src/tools/bash.js +5 -3
- package/bin/binaries/probe-v0.6.0-rc207-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -85,9 +85,11 @@ export class BashPermissionChecker {
|
|
|
85
85
|
* @param {boolean} [config.disableDefaultAllow] - Disable default allow list
|
|
86
86
|
* @param {boolean} [config.disableDefaultDeny] - Disable default deny list
|
|
87
87
|
* @param {boolean} [config.debug] - Enable debug logging
|
|
88
|
+
* @param {Object} [config.tracer] - Optional tracer for telemetry
|
|
88
89
|
*/
|
|
89
90
|
constructor(config = {}) {
|
|
90
91
|
this.debug = config.debug || false;
|
|
92
|
+
this.tracer = config.tracer || null;
|
|
91
93
|
|
|
92
94
|
// Build allow patterns
|
|
93
95
|
this.allowPatterns = [];
|
|
@@ -122,6 +124,27 @@ export class BashPermissionChecker {
|
|
|
122
124
|
if (this.debug) {
|
|
123
125
|
console.log(`[BashPermissions] Total patterns - Allow: ${this.allowPatterns.length}, Deny: ${this.denyPatterns.length}`);
|
|
124
126
|
}
|
|
127
|
+
|
|
128
|
+
// Record initialization event
|
|
129
|
+
this.recordBashEvent('permissions.initialized', {
|
|
130
|
+
allowPatternCount: this.allowPatterns.length,
|
|
131
|
+
denyPatternCount: this.denyPatterns.length,
|
|
132
|
+
hasCustomAllowPatterns: !!(config.allow && config.allow.length > 0),
|
|
133
|
+
hasCustomDenyPatterns: !!(config.deny && config.deny.length > 0),
|
|
134
|
+
disableDefaultAllow: !!config.disableDefaultAllow,
|
|
135
|
+
disableDefaultDeny: !!config.disableDefaultDeny
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Record a bash telemetry event if tracer is available
|
|
141
|
+
* @param {string} eventType - Event type (e.g., 'permission.checked', 'permission.denied')
|
|
142
|
+
* @param {Object} data - Event data
|
|
143
|
+
*/
|
|
144
|
+
recordBashEvent(eventType, data = {}) {
|
|
145
|
+
if (this.tracer && typeof this.tracer.recordBashEvent === 'function') {
|
|
146
|
+
this.tracer.recordBashEvent(eventType, data);
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
/**
|
|
@@ -131,11 +154,17 @@ export class BashPermissionChecker {
|
|
|
131
154
|
*/
|
|
132
155
|
check(command) {
|
|
133
156
|
if (!command || typeof command !== 'string') {
|
|
134
|
-
|
|
157
|
+
const result = {
|
|
135
158
|
allowed: false,
|
|
136
159
|
reason: 'Invalid or empty command',
|
|
137
160
|
command: command
|
|
138
161
|
};
|
|
162
|
+
this.recordBashEvent('permission.denied', {
|
|
163
|
+
command: String(command),
|
|
164
|
+
reason: result.reason,
|
|
165
|
+
isComplex: false
|
|
166
|
+
});
|
|
167
|
+
return result;
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
// Check if this is a complex command
|
|
@@ -150,19 +179,32 @@ export class BashPermissionChecker {
|
|
|
150
179
|
const parsed = parseCommand(command);
|
|
151
180
|
|
|
152
181
|
if (parsed.error) {
|
|
153
|
-
|
|
182
|
+
const result = {
|
|
154
183
|
allowed: false,
|
|
155
184
|
reason: parsed.error,
|
|
156
185
|
command: command
|
|
157
186
|
};
|
|
187
|
+
this.recordBashEvent('permission.denied', {
|
|
188
|
+
command,
|
|
189
|
+
reason: result.reason,
|
|
190
|
+
isComplex: false,
|
|
191
|
+
parseError: true
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
if (!parsed.command) {
|
|
161
|
-
|
|
197
|
+
const result = {
|
|
162
198
|
allowed: false,
|
|
163
199
|
reason: 'No valid command found',
|
|
164
200
|
command: command
|
|
165
201
|
};
|
|
202
|
+
this.recordBashEvent('permission.denied', {
|
|
203
|
+
command,
|
|
204
|
+
reason: result.reason,
|
|
205
|
+
isComplex: false
|
|
206
|
+
});
|
|
207
|
+
return result;
|
|
166
208
|
}
|
|
167
209
|
|
|
168
210
|
if (this.debug) {
|
|
@@ -173,24 +215,39 @@ export class BashPermissionChecker {
|
|
|
173
215
|
// Check deny patterns first (deny takes precedence)
|
|
174
216
|
if (matchesAnyPattern(parsed, this.denyPatterns)) {
|
|
175
217
|
const matchedPatterns = this.denyPatterns.filter(pattern => matchesPattern(parsed, pattern));
|
|
176
|
-
|
|
218
|
+
const result = {
|
|
177
219
|
allowed: false,
|
|
178
220
|
reason: `Command matches deny pattern: ${matchedPatterns[0]}`,
|
|
179
221
|
command: command,
|
|
180
222
|
parsed: parsed,
|
|
181
223
|
matchedPatterns: matchedPatterns
|
|
182
224
|
};
|
|
225
|
+
this.recordBashEvent('permission.denied', {
|
|
226
|
+
command,
|
|
227
|
+
parsedCommand: parsed.command,
|
|
228
|
+
reason: 'matches_deny_pattern',
|
|
229
|
+
matchedPattern: matchedPatterns[0],
|
|
230
|
+
isComplex: false
|
|
231
|
+
});
|
|
232
|
+
return result;
|
|
183
233
|
}
|
|
184
234
|
|
|
185
235
|
// Check allow patterns
|
|
186
236
|
if (this.allowPatterns.length > 0) {
|
|
187
237
|
if (!matchesAnyPattern(parsed, this.allowPatterns)) {
|
|
188
|
-
|
|
238
|
+
const result = {
|
|
189
239
|
allowed: false,
|
|
190
240
|
reason: 'Command not in allow list',
|
|
191
241
|
command: command,
|
|
192
242
|
parsed: parsed
|
|
193
243
|
};
|
|
244
|
+
this.recordBashEvent('permission.denied', {
|
|
245
|
+
command,
|
|
246
|
+
parsedCommand: parsed.command,
|
|
247
|
+
reason: 'not_in_allow_list',
|
|
248
|
+
isComplex: false
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
194
251
|
}
|
|
195
252
|
}
|
|
196
253
|
|
|
@@ -206,6 +263,12 @@ export class BashPermissionChecker {
|
|
|
206
263
|
console.log(`[BashPermissions] ALLOWED - command passed all checks`);
|
|
207
264
|
}
|
|
208
265
|
|
|
266
|
+
this.recordBashEvent('permission.allowed', {
|
|
267
|
+
command,
|
|
268
|
+
parsedCommand: parsed.command,
|
|
269
|
+
isComplex: false
|
|
270
|
+
});
|
|
271
|
+
|
|
209
272
|
return result;
|
|
210
273
|
}
|
|
211
274
|
|
|
@@ -235,13 +298,20 @@ export class BashPermissionChecker {
|
|
|
235
298
|
if (this.debug) {
|
|
236
299
|
console.log(`[BashPermissions] DENIED - matches complex deny pattern: ${pattern}`);
|
|
237
300
|
}
|
|
238
|
-
|
|
301
|
+
const result = {
|
|
239
302
|
allowed: false,
|
|
240
303
|
reason: `Command matches deny pattern: ${pattern}`,
|
|
241
304
|
command: command,
|
|
242
305
|
isComplex: true,
|
|
243
306
|
matchedPatterns: [pattern]
|
|
244
307
|
};
|
|
308
|
+
this.recordBashEvent('permission.denied', {
|
|
309
|
+
command,
|
|
310
|
+
reason: 'matches_deny_pattern',
|
|
311
|
+
matchedPattern: pattern,
|
|
312
|
+
isComplex: true
|
|
313
|
+
});
|
|
314
|
+
return result;
|
|
245
315
|
}
|
|
246
316
|
}
|
|
247
317
|
|
|
@@ -251,12 +321,18 @@ export class BashPermissionChecker {
|
|
|
251
321
|
if (this.debug) {
|
|
252
322
|
console.log(`[BashPermissions] ALLOWED - matches complex allow pattern: ${pattern}`);
|
|
253
323
|
}
|
|
254
|
-
|
|
324
|
+
const result = {
|
|
255
325
|
allowed: true,
|
|
256
326
|
command: command,
|
|
257
327
|
isComplex: true,
|
|
258
328
|
matchedPattern: pattern
|
|
259
329
|
};
|
|
330
|
+
this.recordBashEvent('permission.allowed', {
|
|
331
|
+
command,
|
|
332
|
+
matchedPattern: pattern,
|
|
333
|
+
isComplex: true
|
|
334
|
+
});
|
|
335
|
+
return result;
|
|
260
336
|
}
|
|
261
337
|
}
|
|
262
338
|
|
|
@@ -264,6 +340,11 @@ export class BashPermissionChecker {
|
|
|
264
340
|
if (this.debug) {
|
|
265
341
|
console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
|
|
266
342
|
}
|
|
343
|
+
this.recordBashEvent('permission.denied', {
|
|
344
|
+
command,
|
|
345
|
+
reason: 'no_matching_complex_pattern',
|
|
346
|
+
isComplex: true
|
|
347
|
+
});
|
|
267
348
|
return {
|
|
268
349
|
allowed: false,
|
|
269
350
|
reason: 'Complex shell commands require explicit allow patterns (e.g., "cd * && git *")',
|
package/src/agent/mcp/client.js
CHANGED
|
@@ -9,6 +9,47 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
|
9
9
|
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
|
10
10
|
import { loadMCPConfiguration, parseEnabledServers, DEFAULT_TIMEOUT } from './config.js';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Check if a method is allowed based on server's method filter configuration
|
|
14
|
+
* Supports wildcard patterns (e.g., "*_read", "search_*", "prefix_*_suffix")
|
|
15
|
+
* @param {string} methodName - The method name to check
|
|
16
|
+
* @param {string[]|null} allowedMethods - Array of allowed method patterns (null = all allowed)
|
|
17
|
+
* @param {string[]|null} blockedMethods - Array of blocked method patterns (null = none blocked)
|
|
18
|
+
* @returns {boolean} Whether the method is allowed
|
|
19
|
+
*/
|
|
20
|
+
export function isMethodAllowed(methodName, allowedMethods, blockedMethods) {
|
|
21
|
+
/**
|
|
22
|
+
* Check if a method name matches a pattern
|
|
23
|
+
* Supports * wildcard which matches any characters
|
|
24
|
+
* @param {string} name - Method name to check
|
|
25
|
+
* @param {string} pattern - Pattern to match against (may contain *)
|
|
26
|
+
* @returns {boolean} Whether the name matches the pattern
|
|
27
|
+
*/
|
|
28
|
+
const matchesPattern = (name, pattern) => {
|
|
29
|
+
if (!pattern.includes('*')) {
|
|
30
|
+
return name === pattern;
|
|
31
|
+
}
|
|
32
|
+
// Convert pattern to regex: escape special chars, replace * with .*
|
|
33
|
+
const regexPattern = pattern
|
|
34
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
35
|
+
.replace(/\*/g, '.*'); // Replace * with .*
|
|
36
|
+
return new RegExp(`^${regexPattern}$`).test(name);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// If allowedMethods is specified (whitelist mode), only those methods are allowed
|
|
40
|
+
if (allowedMethods && allowedMethods.length > 0) {
|
|
41
|
+
return allowedMethods.some(pattern => matchesPattern(methodName, pattern));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If blockedMethods is specified (blacklist mode), all methods except those are allowed
|
|
45
|
+
if (blockedMethods && blockedMethods.length > 0) {
|
|
46
|
+
return !blockedMethods.some(pattern => matchesPattern(methodName, pattern));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// No filter specified - all methods are allowed
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
12
53
|
/**
|
|
13
54
|
* Create transport based on configuration
|
|
14
55
|
* @param {Object} serverConfig - Server configuration
|
|
@@ -117,6 +158,18 @@ export class MCPClientManager {
|
|
|
117
158
|
this.tools = new Map();
|
|
118
159
|
this.debug = options.debug || process.env.DEBUG_MCP === '1';
|
|
119
160
|
this.config = null;
|
|
161
|
+
this.tracer = options.tracer || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Record an MCP telemetry event if tracer is available
|
|
166
|
+
* @param {string} eventType - Event type (e.g., 'server.connect', 'tool.discovered')
|
|
167
|
+
* @param {Object} data - Event data
|
|
168
|
+
*/
|
|
169
|
+
recordMcpEvent(eventType, data = {}) {
|
|
170
|
+
if (this.tracer && typeof this.tracer.recordMcpEvent === 'function') {
|
|
171
|
+
this.tracer.recordMcpEvent(eventType, data);
|
|
172
|
+
}
|
|
120
173
|
}
|
|
121
174
|
|
|
122
175
|
/**
|
|
@@ -128,12 +181,24 @@ export class MCPClientManager {
|
|
|
128
181
|
this.config = config || loadMCPConfiguration();
|
|
129
182
|
const servers = parseEnabledServers(this.config);
|
|
130
183
|
|
|
184
|
+
// Record initialization start
|
|
185
|
+
this.recordMcpEvent('initialization.started', {
|
|
186
|
+
serverCount: servers.length,
|
|
187
|
+
serverNames: servers.map(s => s.name)
|
|
188
|
+
});
|
|
189
|
+
|
|
131
190
|
// Always log the number of servers found
|
|
132
191
|
console.error(`[MCP INFO] Found ${servers.length} enabled MCP server${servers.length !== 1 ? 's' : ''}`);
|
|
133
192
|
|
|
134
193
|
if (servers.length === 0) {
|
|
135
194
|
console.error('[MCP INFO] No MCP servers configured or enabled');
|
|
136
195
|
console.error('[MCP INFO] 0 MCP tools available');
|
|
196
|
+
this.recordMcpEvent('initialization.completed', {
|
|
197
|
+
connected: 0,
|
|
198
|
+
total: 0,
|
|
199
|
+
toolCount: 0,
|
|
200
|
+
tools: []
|
|
201
|
+
});
|
|
137
202
|
return {
|
|
138
203
|
connected: 0,
|
|
139
204
|
total: 0,
|
|
@@ -178,10 +243,19 @@ export class MCPClientManager {
|
|
|
178
243
|
});
|
|
179
244
|
}
|
|
180
245
|
|
|
246
|
+
// Record initialization completion
|
|
247
|
+
const toolNames = Array.from(this.tools.keys());
|
|
248
|
+
this.recordMcpEvent('initialization.completed', {
|
|
249
|
+
connected: connectedCount,
|
|
250
|
+
total: servers.length,
|
|
251
|
+
toolCount: this.tools.size,
|
|
252
|
+
tools: toolNames
|
|
253
|
+
});
|
|
254
|
+
|
|
181
255
|
return {
|
|
182
256
|
connected: connectedCount,
|
|
183
257
|
total: servers.length,
|
|
184
|
-
tools:
|
|
258
|
+
tools: toolNames
|
|
185
259
|
};
|
|
186
260
|
}
|
|
187
261
|
|
|
@@ -192,6 +266,14 @@ export class MCPClientManager {
|
|
|
192
266
|
async connectToServer(serverConfig) {
|
|
193
267
|
const { name } = serverConfig;
|
|
194
268
|
|
|
269
|
+
// Record connection attempt
|
|
270
|
+
this.recordMcpEvent('server.connecting', {
|
|
271
|
+
serverName: name,
|
|
272
|
+
transport: serverConfig.transport,
|
|
273
|
+
hasAllowedMethods: !!(serverConfig.allowedMethods && serverConfig.allowedMethods.length > 0),
|
|
274
|
+
hasBlockedMethods: !!(serverConfig.blockedMethods && serverConfig.blockedMethods.length > 0)
|
|
275
|
+
});
|
|
276
|
+
|
|
195
277
|
try {
|
|
196
278
|
if (this.debug) {
|
|
197
279
|
console.error(`[MCP DEBUG] Connecting to ${name} via ${serverConfig.transport}...`);
|
|
@@ -223,10 +305,34 @@ export class MCPClientManager {
|
|
|
223
305
|
|
|
224
306
|
// Fetch and register tools
|
|
225
307
|
const toolsResponse = await client.listTools();
|
|
226
|
-
const
|
|
308
|
+
const totalToolCount = toolsResponse?.tools?.length || 0;
|
|
309
|
+
let registeredCount = 0;
|
|
310
|
+
let filteredCount = 0;
|
|
311
|
+
const registeredTools = [];
|
|
312
|
+
const filteredTools = [];
|
|
227
313
|
|
|
228
314
|
if (toolsResponse && toolsResponse.tools) {
|
|
315
|
+
const { allowedMethods, blockedMethods } = serverConfig;
|
|
316
|
+
const allToolNames = toolsResponse.tools.map(t => t.name);
|
|
317
|
+
|
|
318
|
+
// Record tools discovered from server
|
|
319
|
+
this.recordMcpEvent('tools.discovered', {
|
|
320
|
+
serverName: name,
|
|
321
|
+
toolCount: totalToolCount,
|
|
322
|
+
tools: allToolNames
|
|
323
|
+
});
|
|
324
|
+
|
|
229
325
|
for (const tool of toolsResponse.tools) {
|
|
326
|
+
// Apply method filtering based on server config
|
|
327
|
+
if (!isMethodAllowed(tool.name, allowedMethods, blockedMethods)) {
|
|
328
|
+
filteredCount++;
|
|
329
|
+
filteredTools.push(tool.name);
|
|
330
|
+
if (this.debug) {
|
|
331
|
+
console.error(`[MCP DEBUG] Filtered out tool: ${tool.name} (not allowed by method filter)`);
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
230
336
|
// Add server prefix to avoid conflicts
|
|
231
337
|
const qualifiedName = `${name}_${tool.name}`;
|
|
232
338
|
this.tools.set(qualifiedName, {
|
|
@@ -234,14 +340,68 @@ export class MCPClientManager {
|
|
|
234
340
|
serverName: name,
|
|
235
341
|
originalName: tool.name
|
|
236
342
|
});
|
|
343
|
+
registeredCount++;
|
|
344
|
+
registeredTools.push(qualifiedName);
|
|
237
345
|
|
|
238
346
|
if (this.debug) {
|
|
239
347
|
console.error(`[MCP DEBUG] Registered tool: ${qualifiedName}`);
|
|
240
348
|
}
|
|
241
349
|
}
|
|
350
|
+
|
|
351
|
+
// Record method filtering results if any filtering was applied
|
|
352
|
+
if (filteredCount > 0) {
|
|
353
|
+
this.recordMcpEvent('tools.filtered', {
|
|
354
|
+
serverName: name,
|
|
355
|
+
filteredCount,
|
|
356
|
+
filteredTools,
|
|
357
|
+
allowedMethods: allowedMethods || [],
|
|
358
|
+
blockedMethods: blockedMethods || []
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check for unmatched patterns in allowedMethods and warn users
|
|
363
|
+
if (allowedMethods && allowedMethods.length > 0) {
|
|
364
|
+
const unmatchedPatterns = allowedMethods.filter(pattern => {
|
|
365
|
+
// Check if this pattern matches at least one tool
|
|
366
|
+
return !allToolNames.some(toolName => isMethodAllowed(toolName, [pattern], null));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (unmatchedPatterns.length > 0) {
|
|
370
|
+
console.error(`[MCP WARN] Server '${name}': The following allowedMethods patterns did not match any tools: ${unmatchedPatterns.join(', ')}`);
|
|
371
|
+
console.error(`[MCP WARN] Available methods from '${name}': ${allToolNames.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check for unmatched patterns in blockedMethods and warn users
|
|
376
|
+
if (blockedMethods && blockedMethods.length > 0) {
|
|
377
|
+
const unmatchedPatterns = blockedMethods.filter(pattern => {
|
|
378
|
+
// Check if this pattern matches at least one tool
|
|
379
|
+
return !allToolNames.some(toolName => !isMethodAllowed(toolName, null, [pattern]));
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (unmatchedPatterns.length > 0) {
|
|
383
|
+
console.error(`[MCP WARN] Server '${name}': The following blockedMethods patterns did not match any tools: ${unmatchedPatterns.join(', ')}`);
|
|
384
|
+
console.error(`[MCP WARN] Available methods from '${name}': ${allToolNames.join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Log connection result with filtering info
|
|
390
|
+
if (filteredCount > 0) {
|
|
391
|
+
console.error(`[MCP INFO] Connected to ${name}: ${registeredCount} tool${registeredCount !== 1 ? 's' : ''} loaded (${filteredCount} filtered out)`);
|
|
392
|
+
} else {
|
|
393
|
+
console.error(`[MCP INFO] Connected to ${name}: ${registeredCount} tool${registeredCount !== 1 ? 's' : ''} loaded`);
|
|
242
394
|
}
|
|
243
395
|
|
|
244
|
-
|
|
396
|
+
// Record successful connection
|
|
397
|
+
this.recordMcpEvent('server.connected', {
|
|
398
|
+
serverName: name,
|
|
399
|
+
transport: serverConfig.transport,
|
|
400
|
+
totalToolCount,
|
|
401
|
+
registeredCount,
|
|
402
|
+
filteredCount,
|
|
403
|
+
registeredTools
|
|
404
|
+
});
|
|
245
405
|
|
|
246
406
|
return true;
|
|
247
407
|
} catch (error) {
|
|
@@ -249,6 +409,14 @@ export class MCPClientManager {
|
|
|
249
409
|
if (this.debug) {
|
|
250
410
|
console.error(`[MCP DEBUG] Full error details:`, error);
|
|
251
411
|
}
|
|
412
|
+
|
|
413
|
+
// Record connection failure
|
|
414
|
+
this.recordMcpEvent('server.connection_failed', {
|
|
415
|
+
serverName: name,
|
|
416
|
+
transport: serverConfig.transport,
|
|
417
|
+
error: error.message
|
|
418
|
+
});
|
|
419
|
+
|
|
252
420
|
return false;
|
|
253
421
|
}
|
|
254
422
|
}
|
|
@@ -261,14 +429,32 @@ export class MCPClientManager {
|
|
|
261
429
|
async callTool(toolName, args) {
|
|
262
430
|
const tool = this.tools.get(toolName);
|
|
263
431
|
if (!tool) {
|
|
432
|
+
this.recordMcpEvent('tool.call_failed', {
|
|
433
|
+
toolName,
|
|
434
|
+
error: 'Unknown tool'
|
|
435
|
+
});
|
|
264
436
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
265
437
|
}
|
|
266
438
|
|
|
267
439
|
const clientInfo = this.clients.get(tool.serverName);
|
|
268
440
|
if (!clientInfo) {
|
|
441
|
+
this.recordMcpEvent('tool.call_failed', {
|
|
442
|
+
toolName,
|
|
443
|
+
serverName: tool.serverName,
|
|
444
|
+
error: 'Server not connected'
|
|
445
|
+
});
|
|
269
446
|
throw new Error(`Server ${tool.serverName} not connected`);
|
|
270
447
|
}
|
|
271
448
|
|
|
449
|
+
const startTime = Date.now();
|
|
450
|
+
|
|
451
|
+
// Record tool call start
|
|
452
|
+
this.recordMcpEvent('tool.call_started', {
|
|
453
|
+
toolName,
|
|
454
|
+
serverName: tool.serverName,
|
|
455
|
+
originalToolName: tool.originalName
|
|
456
|
+
});
|
|
457
|
+
|
|
272
458
|
try {
|
|
273
459
|
if (this.debug) {
|
|
274
460
|
console.error(`[MCP DEBUG] Calling ${toolName} with args:`, JSON.stringify(args, null, 2));
|
|
@@ -296,16 +482,39 @@ export class MCPClientManager {
|
|
|
296
482
|
timeoutPromise
|
|
297
483
|
]);
|
|
298
484
|
|
|
485
|
+
const durationMs = Date.now() - startTime;
|
|
486
|
+
|
|
299
487
|
if (this.debug) {
|
|
300
488
|
console.error(`[MCP DEBUG] Tool ${toolName} executed successfully`);
|
|
301
489
|
}
|
|
302
490
|
|
|
491
|
+
// Record successful tool call
|
|
492
|
+
this.recordMcpEvent('tool.call_completed', {
|
|
493
|
+
toolName,
|
|
494
|
+
serverName: tool.serverName,
|
|
495
|
+
originalToolName: tool.originalName,
|
|
496
|
+
durationMs
|
|
497
|
+
});
|
|
498
|
+
|
|
303
499
|
return result;
|
|
304
500
|
} catch (error) {
|
|
501
|
+
const durationMs = Date.now() - startTime;
|
|
502
|
+
|
|
305
503
|
console.error(`[MCP ERROR] Error calling tool ${toolName}:`, error.message);
|
|
306
504
|
if (this.debug) {
|
|
307
505
|
console.error(`[MCP DEBUG] Full error details:`, error);
|
|
308
506
|
}
|
|
507
|
+
|
|
508
|
+
// Record failed tool call
|
|
509
|
+
this.recordMcpEvent('tool.call_failed', {
|
|
510
|
+
toolName,
|
|
511
|
+
serverName: tool.serverName,
|
|
512
|
+
originalToolName: tool.originalName,
|
|
513
|
+
error: error.message,
|
|
514
|
+
durationMs,
|
|
515
|
+
isTimeout: error.message.includes('timeout')
|
|
516
|
+
});
|
|
517
|
+
|
|
309
518
|
throw error;
|
|
310
519
|
}
|
|
311
520
|
}
|
|
@@ -357,6 +566,7 @@ export class MCPClientManager {
|
|
|
357
566
|
*/
|
|
358
567
|
async disconnect() {
|
|
359
568
|
const disconnectPromises = [];
|
|
569
|
+
const serverNames = Array.from(this.clients.keys());
|
|
360
570
|
|
|
361
571
|
if (this.clients.size === 0) {
|
|
362
572
|
if (this.debug) {
|
|
@@ -365,6 +575,12 @@ export class MCPClientManager {
|
|
|
365
575
|
return;
|
|
366
576
|
}
|
|
367
577
|
|
|
578
|
+
// Record disconnection start
|
|
579
|
+
this.recordMcpEvent('disconnection.started', {
|
|
580
|
+
serverCount: this.clients.size,
|
|
581
|
+
serverNames
|
|
582
|
+
});
|
|
583
|
+
|
|
368
584
|
if (this.debug) {
|
|
369
585
|
console.error(`[MCP DEBUG] Disconnecting from ${this.clients.size} MCP server${this.clients.size !== 1 ? 's' : ''}...`);
|
|
370
586
|
}
|
|
@@ -376,9 +592,16 @@ export class MCPClientManager {
|
|
|
376
592
|
if (this.debug) {
|
|
377
593
|
console.error(`[MCP DEBUG] Disconnected from ${name}`);
|
|
378
594
|
}
|
|
595
|
+
this.recordMcpEvent('server.disconnected', {
|
|
596
|
+
serverName: name
|
|
597
|
+
});
|
|
379
598
|
})
|
|
380
599
|
.catch(error => {
|
|
381
600
|
console.error(`[MCP ERROR] Error disconnecting from ${name}:`, error.message);
|
|
601
|
+
this.recordMcpEvent('server.disconnect_failed', {
|
|
602
|
+
serverName: name,
|
|
603
|
+
error: error.message
|
|
604
|
+
});
|
|
382
605
|
})
|
|
383
606
|
);
|
|
384
607
|
}
|
|
@@ -387,6 +610,12 @@ export class MCPClientManager {
|
|
|
387
610
|
this.clients.clear();
|
|
388
611
|
this.tools.clear();
|
|
389
612
|
|
|
613
|
+
// Record disconnection completion
|
|
614
|
+
this.recordMcpEvent('disconnection.completed', {
|
|
615
|
+
serverCount: serverNames.length,
|
|
616
|
+
serverNames
|
|
617
|
+
});
|
|
618
|
+
|
|
390
619
|
if (this.debug) {
|
|
391
620
|
console.error('[MCP DEBUG] All MCP connections closed');
|
|
392
621
|
}
|
|
@@ -405,5 +634,6 @@ export async function createMCPManager(options = {}) {
|
|
|
405
634
|
export default {
|
|
406
635
|
MCPClientManager,
|
|
407
636
|
createMCPManager,
|
|
408
|
-
createTransport
|
|
637
|
+
createTransport,
|
|
638
|
+
isMethodAllowed
|
|
409
639
|
};
|
package/src/agent/mcp/config.js
CHANGED
|
@@ -29,6 +29,61 @@ export function validateTimeout(value) {
|
|
|
29
29
|
return Math.min(num, MAX_TIMEOUT); // Cap at max timeout
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Validate and normalize method filter configuration
|
|
34
|
+
* @param {Object} serverConfig - Server configuration
|
|
35
|
+
* @param {string} serverName - Server name for logging
|
|
36
|
+
* @returns {Object} Object with allowedMethods and blockedMethods (null if not configured)
|
|
37
|
+
*/
|
|
38
|
+
export function validateMethodFilter(serverConfig, serverName = 'unknown') {
|
|
39
|
+
const result = { allowedMethods: null, blockedMethods: null };
|
|
40
|
+
const debug = process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1';
|
|
41
|
+
|
|
42
|
+
// Check if both are specified - allowedMethods takes precedence
|
|
43
|
+
if (serverConfig.allowedMethods && serverConfig.blockedMethods) {
|
|
44
|
+
console.error(`[MCP WARN] Server '${serverName}' has both allowedMethods and blockedMethods - using allowedMethods only`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Process allowedMethods
|
|
48
|
+
if (serverConfig.allowedMethods) {
|
|
49
|
+
if (!Array.isArray(serverConfig.allowedMethods)) {
|
|
50
|
+
console.error(`[MCP WARN] Server '${serverName}' allowedMethods must be an array, ignoring`);
|
|
51
|
+
} else {
|
|
52
|
+
const validMethods = serverConfig.allowedMethods.filter(m => typeof m === 'string' && m.length > 0);
|
|
53
|
+
if (validMethods.length !== serverConfig.allowedMethods.length) {
|
|
54
|
+
console.error(`[MCP WARN] Server '${serverName}' allowedMethods contains non-string values, skipping those`);
|
|
55
|
+
}
|
|
56
|
+
if (validMethods.length > 0) {
|
|
57
|
+
result.allowedMethods = validMethods;
|
|
58
|
+
if (debug) {
|
|
59
|
+
console.error(`[MCP DEBUG] Server '${serverName}' allowedMethods: ${validMethods.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result; // If allowedMethods is specified (even if invalid), don't process blockedMethods
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Process blockedMethods (only if allowedMethods not specified)
|
|
67
|
+
if (serverConfig.blockedMethods) {
|
|
68
|
+
if (!Array.isArray(serverConfig.blockedMethods)) {
|
|
69
|
+
console.error(`[MCP WARN] Server '${serverName}' blockedMethods must be an array, ignoring`);
|
|
70
|
+
} else {
|
|
71
|
+
const validMethods = serverConfig.blockedMethods.filter(m => typeof m === 'string' && m.length > 0);
|
|
72
|
+
if (validMethods.length !== serverConfig.blockedMethods.length) {
|
|
73
|
+
console.error(`[MCP WARN] Server '${serverName}' blockedMethods contains non-string values, skipping those`);
|
|
74
|
+
}
|
|
75
|
+
if (validMethods.length > 0) {
|
|
76
|
+
result.blockedMethods = validMethods;
|
|
77
|
+
if (debug) {
|
|
78
|
+
console.error(`[MCP DEBUG] Server '${serverName}' blockedMethods: ${validMethods.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
/**
|
|
33
88
|
* Default MCP configuration structure
|
|
34
89
|
*/
|
|
@@ -187,6 +242,16 @@ function mergeWithEnvironment(config) {
|
|
|
187
242
|
console.error(`[MCP WARN] Invalid timeout value for ${normalizedName}: ${value}`);
|
|
188
243
|
}
|
|
189
244
|
break;
|
|
245
|
+
case 'ALLOWLIST':
|
|
246
|
+
// Comma-separated list of allowed method names (supports wildcards)
|
|
247
|
+
// e.g., MCP_SERVERS_MYSERVER_ALLOWLIST=method1,method2,prefix_*
|
|
248
|
+
config.mcpServers[normalizedName].allowedMethods = value.split(',').map(m => m.trim()).filter(Boolean);
|
|
249
|
+
break;
|
|
250
|
+
case 'BLOCKLIST':
|
|
251
|
+
// Comma-separated list of blocked method names (supports wildcards)
|
|
252
|
+
// e.g., MCP_SERVERS_MYSERVER_BLOCKLIST=dangerous_*,risky_method
|
|
253
|
+
config.mcpServers[normalizedName].blockedMethods = value.split(',').map(m => m.trim()).filter(Boolean);
|
|
254
|
+
break;
|
|
190
255
|
}
|
|
191
256
|
}
|
|
192
257
|
}
|
|
@@ -256,6 +321,11 @@ export function parseEnabledServers(config) {
|
|
|
256
321
|
server.timeout = validatedTimeout;
|
|
257
322
|
}
|
|
258
323
|
|
|
324
|
+
// Validate and normalize method filter configuration
|
|
325
|
+
const methodFilter = validateMethodFilter(serverConfig, name);
|
|
326
|
+
server.allowedMethods = methodFilter.allowedMethods;
|
|
327
|
+
server.blockedMethods = methodFilter.blockedMethods;
|
|
328
|
+
|
|
259
329
|
servers.push(server);
|
|
260
330
|
}
|
|
261
331
|
|
|
@@ -321,6 +391,22 @@ export function createSampleConfig() {
|
|
|
321
391
|
enabled: false,
|
|
322
392
|
timeout: 120000,
|
|
323
393
|
description: 'Example server with custom 2-minute timeout (overrides global setting)'
|
|
394
|
+
},
|
|
395
|
+
'filtered-server-example': {
|
|
396
|
+
command: 'npx',
|
|
397
|
+
args: ['-y', '@example/mcp-server'],
|
|
398
|
+
transport: 'stdio',
|
|
399
|
+
enabled: false,
|
|
400
|
+
allowedMethods: ['safe_read', 'safe_query'],
|
|
401
|
+
description: 'Example server with method allowlist - only safe_read and safe_query are available'
|
|
402
|
+
},
|
|
403
|
+
'blocklist-server-example': {
|
|
404
|
+
command: 'npx',
|
|
405
|
+
args: ['-y', '@example/mcp-server'],
|
|
406
|
+
transport: 'stdio',
|
|
407
|
+
enabled: false,
|
|
408
|
+
blockedMethods: ['dangerous_delete', 'dangerous_*'],
|
|
409
|
+
description: 'Example server with method blocklist - all methods except dangerous ones (supports wildcards)'
|
|
324
410
|
}
|
|
325
411
|
},
|
|
326
412
|
// Global settings (apply to all servers unless overridden per-server)
|
|
@@ -356,6 +442,7 @@ export default {
|
|
|
356
442
|
createSampleConfig,
|
|
357
443
|
saveConfig,
|
|
358
444
|
validateTimeout,
|
|
445
|
+
validateMethodFilter,
|
|
359
446
|
DEFAULT_TIMEOUT,
|
|
360
447
|
MAX_TIMEOUT
|
|
361
448
|
};
|