@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.
@@ -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
- return {
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
- return {
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
- return {
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
- return {
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
- return {
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
- return {
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
- return {
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 *")',
@@ -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: Array.from(this.tools.keys())
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 toolCount = toolsResponse?.tools?.length || 0;
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
- console.error(`[MCP INFO] Connected to ${name}: ${toolCount} tool${toolCount !== 1 ? 's' : ''} loaded`);
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
  };
@@ -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
  };