- 
                Notifications
    You must be signed in to change notification settings 
- Fork 0
Feat/enhanced mcp verification #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Extract MCP_TOOL_TOKEN_MULTIPLIER constant to tokens.ts for better maintainability - Add detailed documentation explaining token multiplier methodology - Create createMockClient helper in tests to reduce duplication - Replace string literal 'stdio' with MCPServerType.STDIO enum for type safety - Add concurrency limiting (5 concurrent) for resource/prompt fetches to prevent server overload - Add p-limit dependency for concurrency control These changes improve code quality, reduce magic numbers, and prevent overwhelming slow MCP servers with parallel requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add maxConcurrentFetches option to MCPVerificationOptions to allow users to configure the concurrency limit when fetching resources and prompts from MCP servers.
Changes:
- Add maxConcurrentFetches parameter to MCPVerificationOptions (default: 5)
- Update verifyServer JSDoc with example usage
- Apply configurable limit to both resource and prompt fetching
- Add 3 tests to verify default and custom concurrency limits
This allows external applications to tune concurrency based on their server characteristics - use higher limits for fast servers or lower limits for slow/rate-limited servers.
Example usage:
```typescript
const result = await verifier.verifyServer(server, {
  maxConcurrentFetches: 10  // Use 10 concurrent fetches for fast servers
});
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
    Update library API README with concise documentation for the new maxConcurrentFetches option: - Added to options parameter list - Added example showing fast vs slow server tuning - Updated type definitions - Added to performance considerations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
| WalkthroughThis pull request introduces concurrency control for MCP client resource and prompt fetching operations by adding a new p-limit dependency, defining a MAX_CONCURRENT_FETCHES constant, and extending MCPVerificationOptions with an optional maxConcurrentFetches parameter to limit parallel fetch operations. Changes
 Sequence DiagramsequenceDiagram
    participant Client as MCP Client
    participant Limiter as p-limit<br/>(concurrency)
    participant Tasks as Resource/Prompt<br/>Fetch Tasks
    
    Client->>Client: verifyServer({maxConcurrentFetches: N})
    Client->>Limiter: Create limit(N)
    
    loop For each resource/prompt
        Client->>Limiter: limit(() => fetchTask())
        par Parallel (bounded)
            Limiter->>Tasks: Execute up to N tasks
            Tasks-->>Limiter: Complete
        end
    end
    
    Limiter-->>Client: All fetches completed
    Client->>Client: Return verification result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes The changes involve focused concurrency control logic modifications in mcpClient.ts, new constant definitions, type extensions, and test additions. While the implementation is relatively straightforward, reviewers should verify the correctness of p-limit integration, default concurrency values, and abort/cleanup handling in async operations across resource and prompt fetching. Possibly related PRs
 Poem
 Pre-merge checks and finishing touches❌ Failed checks (1 warning)
 ✅ Passed checks (2 passed)
 ✨ Finishing touches
 🧪 Generate unit tests (beta)
 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (2)
package.json (1)
55-56: Engines mismatch: p-limit@7 requires Node 20+, but engines allow Node 18This will break users on Node 18 at install/runtime. Choose one:
Option A — raise Node baseline:
"engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" },Option B — keep Node 18 support: downgrade p-limit to v6.x (no code changes needed here):
- "p-limit": "^7.1.1", + "p-limit": "^6.2.0",Also applies to: 72-73
src/core/mcpClient.ts (1)
329-356: Prevent unhandled rejection when timeout wins the raceIf timeoutPromise rejects first, connectAndVerify may later reject with no awaiting consumer, causing an unhandled rejection. Attach a catch to connectPromise before racing.
- // Connect to the server with timeout - const connectPromise = this.connectAndVerify(client, transport, server, abortController.signal, options); - - const capabilities = await Promise.race([connectPromise, timeoutPromise]); + // Connect to the server with timeout + const connectPromise = this.connectAndVerify( + client, + transport, + server, + abortController.signal, + options + ); + // Swallow rejection if timeout wins the race to avoid unhandled rejection + connectPromise.catch(() => {}); + const capabilities = await Promise.race([connectPromise, timeoutPromise]);
🧹 Nitpick comments (3)
src/lib/verifier/README.md (1)
268-285: Document Node.js 20+ requirement and align default timeoutSince concurrency uses p-limit v7, call out “Requires Node.js ≥20” in this section. Also, earlier the constructor docs state a 10s default but code uses 30s (DEFAULT_CONNECTION_TIMEOUT_MS = 30000). Please sync the README.
src/types/index.ts (1)
222-223: Constrain and validate maxConcurrentFetchesType allows any number. At runtime, ensure it’s a positive safe integer with a sane upper bound (e.g., 1–50). See mcpClient comment for a concrete guard.
src/core/mcpClient.ts (1)
696-703: Avoid double-closing the client (cleanup already handled by caller)client.close() is called here and again in verifyServer’s finally. While errors are ignored, the double close is redundant. Prefer centralizing cleanup in verifyServer.
- } finally { - // Clean up the connection - try { - await client.close(); - } catch (error) { - // Ignore cleanup errors - } - } + } finally { + // Cleanup is handled by the caller (verifyServer.finally) + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
- package-lock.jsonis excluded by- !**/package-lock.json
📒 Files selected for processing (8)
- package.json(1 hunks)
- src/constants/index.ts(1 hunks)
- src/constants/mcp.ts(1 hunks)
- src/constants/tokens.ts(1 hunks)
- src/core/mcpClient.ts(9 hunks)
- src/lib/verifier/README.md(4 hunks)
- src/types/index.ts(1 hunks)
- tests/core/mcpClient.test.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Write clean, maintainable code following TypeScript best practices
Use TypeScript for type safety when applicable
Follow consistent naming conventions
Add comments for complex logic
Maintain consistency with existing code style
Add appropriate error handling
Never expose sensitive information in code or logs
Follow security best practices for TypeScript
Validate all inputs
Use environment variables for configuration
Document all public APIs, functions, and classes with clear descriptions
Files:
- src/constants/mcp.ts
- src/constants/tokens.ts
- tests/core/mcpClient.test.ts
- src/core/mcpClient.ts
- src/constants/index.ts
- src/types/index.ts
package.json
📄 CodeRabbit inference engine (CLAUDE.md)
Use npm for dependency management
Files:
- package.json
**/*.{test,spec}.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{test,spec}.ts: Write unit tests for all business logic using Jest
Test edge cases and error scenarios
Update tests when modifying functionality
Files:
- tests/core/mcpClient.test.ts
🧬 Code graph analysis (3)
src/constants/mcp.ts (1)
src/constants/index.ts (1)
MAX_CONCURRENT_FETCHES(1-1)
tests/core/mcpClient.test.ts (2)
src/types/index.ts (1)
MCPServerConfig(80-88)src/lib/types/index.ts (1)
MCPServerType(31-31)
src/core/mcpClient.ts (3)
src/constants/tokens.ts (1)
MCP_TOOL_TOKEN_MULTIPLIER(20-20)src/constants/mcp.ts (1)
MAX_CONCURRENT_FETCHES(11-11)src/lib/types/index.ts (1)
MCPServerType(31-31)
🔇 Additional comments (7)
src/constants/mcp.ts (1)
10-12: LGTM: default concurrency cap addedSensible default and clear doc string. No issues.
src/lib/verifier/README.md (1)
75-76: Option field docs look goodThe new option and tuning guidance are clear and match the implementation/default (5).
Also applies to: 475-476
src/constants/tokens.ts (1)
6-20: LGTM: multiplier constant and rationaleClear documentation and scoped export. Matches usage in token counting.
src/constants/index.ts (1)
1-2: Re-exports updated correctlySurface now includes MAX_CONCURRENT_FETCHES and MCP_TOOL_TOKEN_MULTIPLIER. Good.
src/core/mcpClient.ts (1)
818-835: Good: sanitize connection details in error outputAvoids leaking credentials and query params; STDIO command logging is helpful for debugging.
tests/core/mcpClient.test.ts (2)
25-49: LGTM! Excellent refactor to reduce test duplication.The
MockClienttype andcreateMockClienthelper function provide a clean, reusable pattern for test setup. The default implementations are sensible, and the override mechanism using the spread operator allows for flexible customization.
480-690: LGTM! Clean migration to the new mock helper.The tests now consistently use
createMockClient, which improves readability and maintainability by centralizing mock setup logic.
| // Fetch resources with concurrency limiting to avoid overwhelming servers | ||
| const concurrencyLimit = options?.maxConcurrentFetches || MAX_CONCURRENT_FETCHES; | ||
| const limit = pLimit(concurrencyLimit); | ||
| const resourcePromises = resourcesResponse.resources.map((resource) => limit(async () => { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate maxConcurrentFetches to avoid p-limit throwing or runaway parallelism
Current logic treats 0 as default (ok) but allows negatives/NaN/huge values. Guard to 1–50 safe integer.
-        const concurrencyLimit = options?.maxConcurrentFetches || MAX_CONCURRENT_FETCHES;
-        const limit = pLimit(concurrencyLimit);
+        const raw = options?.maxConcurrentFetches;
+        const concurrencyLimit =
+          Number.isSafeInteger(raw) && (raw as number) > 0
+            ? Math.min(raw as number, 50)
+            : MAX_CONCURRENT_FETCHES;
+        const limit = pLimit(concurrencyLimit);Apply the same guard in the prompts section.
-        const concurrencyLimit = options?.maxConcurrentFetches || MAX_CONCURRENT_FETCHES;
-        const limit = pLimit(concurrencyLimit);
+        const raw = options?.maxConcurrentFetches;
+        const concurrencyLimit =
+          Number.isSafeInteger(raw) && (raw as number) > 0
+            ? Math.min(raw as number, 50)
+            : MAX_CONCURRENT_FETCHES;
+        const limit = pLimit(concurrencyLimit);Also applies to: 630-633
🤖 Prompt for AI Agents
In src/core/mcpClient.ts around lines 573-576 (and also apply to 630-633), the
code reads options?.maxConcurrentFetches directly which can be negative, NaN,
zero or unbounded; p-limit can throw or you can end up with runaway concurrency.
Validate options.maxConcurrentFetches as a safe integer clamped to the range
1–50: if it's not a finite safe integer in [1,50] fall back to
MAX_CONCURRENT_FETCHES; then use that validated value when creating pLimit.
Apply the same validation logic in the prompts section (lines ~630-633) so both
usages share the same guarded, clamped concurrency value.
| it('should use default MAX_CONCURRENT_FETCHES when option not specified', async () => { | ||
| const mockClient = createMockClient({ | ||
| listResources: vi.fn().mockResolvedValue({ | ||
| resources: [ | ||
| { uri: 'file:///test1.txt' }, | ||
| { uri: 'file:///test2.txt' }, | ||
| { uri: 'file:///test3.txt' } | ||
| ] | ||
| }) | ||
| }); | ||
|  | ||
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | ||
| vi.mocked(Client).mockImplementation(() => mockClient as any); | ||
|  | ||
| const result = await verifier.verifyServer(testServer, { timeout: 5000 }); | ||
|  | ||
| expect(result.status).toBe('success'); | ||
| // Verify resources were fetched (concurrency limit was applied) | ||
| expect(result.capabilities?.resources).toHaveLength(3); | ||
| }); | ||
|  | ||
| it('should use custom maxConcurrentFetches when provided', async () => { | ||
| const mockClient = createMockClient({ | ||
| listResources: vi.fn().mockResolvedValue({ | ||
| resources: Array.from({ length: 20 }, (_, i) => ({ uri: `file:///test${i}.txt` })) | ||
| }) | ||
| }); | ||
|  | ||
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | ||
| vi.mocked(Client).mockImplementation(() => mockClient as any); | ||
|  | ||
| const result = await verifier.verifyServer(testServer, { | ||
| timeout: 5000, | ||
| maxConcurrentFetches: 2 // Very low limit | ||
| }); | ||
|  | ||
| expect(result.status).toBe('success'); | ||
| // Verify all resources were fetched despite low concurrency | ||
| expect(result.capabilities?.resources).toHaveLength(20); | ||
| }); | ||
|  | ||
| it('should apply maxConcurrentFetches to prompt fetching as well', async () => { | ||
| const mockClient = createMockClient({ | ||
| listPrompts: vi.fn().mockResolvedValue({ | ||
| prompts: Array.from({ length: 15 }, (_, i) => ({ | ||
| name: `prompt-${i}`, | ||
| description: `Test prompt ${i}` | ||
| })) | ||
| }) | ||
| }); | ||
|  | ||
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | ||
| vi.mocked(Client).mockImplementation(() => mockClient as any); | ||
|  | ||
| const result = await verifier.verifyServer(testServer, { | ||
| timeout: 5000, | ||
| maxConcurrentFetches: 3 | ||
| }); | ||
|  | ||
| expect(result.status).toBe('success'); | ||
| // Verify all prompts were fetched | ||
| expect(result.capabilities?.prompts).toHaveLength(15); | ||
| }); | ||
| }); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Concurrency tests don't trigger the code paths they claim to test.
These tests have fundamental flaws:
- 
Test 1 (lines 1273-1292): Doesn't set includeResourceContents: true, soreadResourceis never called. The test only verifieslistResourcesresults, which aren't subject to the concurrency limiting.
- 
Test 2 (lines 1294-1312): Same issue - without includeResourceContents: true, no resource content fetching occurs, so concurrency limiting isn't exercised.
- 
Test 3 (lines 1314-1336): Doesn't set includePromptDetails: true, sogetPromptis never called. Only verifieslistPromptsresults.
Additionally, even if the correct options were set, these tests only verify that all items are eventually fetched—they don't actually verify that the concurrency limit is respected. The tests would pass even if the concurrency limiting code was completely removed.
To fix these tests:
Apply this diff to properly trigger concurrency-limited operations:
     it('should use default MAX_CONCURRENT_FETCHES when option not specified', async () => {
       const mockClient = createMockClient({
         listResources: vi.fn().mockResolvedValue({
           resources: [
             { uri: 'file:///test1.txt' },
             { uri: 'file:///test2.txt' },
             { uri: 'file:///test3.txt' }
           ]
-        })
+        }),
+        readResource: vi.fn().mockResolvedValue({
+          contents: [{ text: 'test content' }]
+        })
       });
       const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
       vi.mocked(Client).mockImplementation(() => mockClient as any);
-      const result = await verifier.verifyServer(testServer, { timeout: 5000 });
+      const result = await verifier.verifyServer(testServer, {
+        timeout: 5000,
+        includeResourceContents: true
+      });
       expect(result.status).toBe('success');
-      // Verify resources were fetched (concurrency limit was applied)
       expect(result.capabilities?.resources).toHaveLength(3);
+      expect(mockClient.readResource).toHaveBeenCalledTimes(3);
     });
     it('should use custom maxConcurrentFetches when provided', async () => {
       const mockClient = createMockClient({
         listResources: vi.fn().mockResolvedValue({
           resources: Array.from({ length: 20 }, (_, i) => ({ uri: `file:///test${i}.txt` }))
-        })
+        }),
+        readResource: vi.fn().mockResolvedValue({
+          contents: [{ text: 'test content' }]
+        })
       });
       const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
       vi.mocked(Client).mockImplementation(() => mockClient as any);
       const result = await verifier.verifyServer(testServer, {
         timeout: 5000,
-        maxConcurrentFetches: 2  // Very low limit
+        maxConcurrentFetches: 2,  // Very low limit
+        includeResourceContents: true
       });
       expect(result.status).toBe('success');
-      // Verify all resources were fetched despite low concurrency
       expect(result.capabilities?.resources).toHaveLength(20);
+      expect(mockClient.readResource).toHaveBeenCalledTimes(20);
     });
     it('should apply maxConcurrentFetches to prompt fetching as well', async () => {
       const mockClient = createMockClient({
         listPrompts: vi.fn().mockResolvedValue({
           prompts: Array.from({ length: 15 }, (_, i) => ({
             name: `prompt-${i}`,
             description: `Test prompt ${i}`
           }))
+        }),
+        getPrompt: vi.fn().mockResolvedValue({
+          messages: [{ role: 'user', content: 'test prompt' }]
         })
       });
       const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
       vi.mocked(Client).mockImplementation(() => mockClient as any);
       const result = await verifier.verifyServer(testServer, {
         timeout: 5000,
-        maxConcurrentFetches: 3
+        maxConcurrentFetches: 3,
+        includePromptDetails: true
       });
       expect(result.status).toBe('success');
-      // Verify all prompts were fetched
       expect(result.capabilities?.prompts).toHaveLength(15);
+      expect(mockClient.getPrompt).toHaveBeenCalledTimes(15);
     });Note: These fixes ensure the concurrency code paths are executed. However, to truly verify concurrency limiting behavior (that at most N operations run simultaneously), you would need additional instrumentation, such as tracking in-flight operation counts or adding artificial delays to verify batching behavior.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should use default MAX_CONCURRENT_FETCHES when option not specified', async () => { | |
| const mockClient = createMockClient({ | |
| listResources: vi.fn().mockResolvedValue({ | |
| resources: [ | |
| { uri: 'file:///test1.txt' }, | |
| { uri: 'file:///test2.txt' }, | |
| { uri: 'file:///test3.txt' } | |
| ] | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { timeout: 5000 }); | |
| expect(result.status).toBe('success'); | |
| // Verify resources were fetched (concurrency limit was applied) | |
| expect(result.capabilities?.resources).toHaveLength(3); | |
| }); | |
| it('should use custom maxConcurrentFetches when provided', async () => { | |
| const mockClient = createMockClient({ | |
| listResources: vi.fn().mockResolvedValue({ | |
| resources: Array.from({ length: 20 }, (_, i) => ({ uri: `file:///test${i}.txt` })) | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { | |
| timeout: 5000, | |
| maxConcurrentFetches: 2 // Very low limit | |
| }); | |
| expect(result.status).toBe('success'); | |
| // Verify all resources were fetched despite low concurrency | |
| expect(result.capabilities?.resources).toHaveLength(20); | |
| }); | |
| it('should apply maxConcurrentFetches to prompt fetching as well', async () => { | |
| const mockClient = createMockClient({ | |
| listPrompts: vi.fn().mockResolvedValue({ | |
| prompts: Array.from({ length: 15 }, (_, i) => ({ | |
| name: `prompt-${i}`, | |
| description: `Test prompt ${i}` | |
| })) | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { | |
| timeout: 5000, | |
| maxConcurrentFetches: 3 | |
| }); | |
| expect(result.status).toBe('success'); | |
| // Verify all prompts were fetched | |
| expect(result.capabilities?.prompts).toHaveLength(15); | |
| }); | |
| }); | |
| it('should use default MAX_CONCURRENT_FETCHES when option not specified', async () => { | |
| const mockClient = createMockClient({ | |
| listResources: vi.fn().mockResolvedValue({ | |
| resources: [ | |
| { uri: 'file:///test1.txt' }, | |
| { uri: 'file:///test2.txt' }, | |
| { uri: 'file:///test3.txt' } | |
| ] | |
| }), | |
| readResource: vi.fn().mockResolvedValue({ | |
| contents: [{ text: 'test content' }] | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { | |
| timeout: 5000, | |
| includeResourceContents: true | |
| }); | |
| expect(result.status).toBe('success'); | |
| expect(result.capabilities?.resources).toHaveLength(3); | |
| expect(mockClient.readResource).toHaveBeenCalledTimes(3); | |
| }); | |
| it('should use custom maxConcurrentFetches when provided', async () => { | |
| const mockClient = createMockClient({ | |
| listResources: vi.fn().mockResolvedValue({ | |
| resources: Array.from({ length: 20 }, (_, i) => ({ uri: `file:///test${i}.txt` })) | |
| }), | |
| readResource: vi.fn().mockResolvedValue({ | |
| contents: [{ text: 'test content' }] | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { | |
| timeout: 5000, | |
| maxConcurrentFetches: 2, | |
| includeResourceContents: true | |
| }); | |
| expect(result.status).toBe('success'); | |
| expect(result.capabilities?.resources).toHaveLength(20); | |
| expect(mockClient.readResource).toHaveBeenCalledTimes(20); | |
| }); | |
| it('should apply maxConcurrentFetches to prompt fetching as well', async () => { | |
| const mockClient = createMockClient({ | |
| listPrompts: vi.fn().mockResolvedValue({ | |
| prompts: Array.from({ length: 15 }, (_, i) => ({ | |
| name: `prompt-${i}`, | |
| description: `Test prompt ${i}` | |
| })) | |
| }), | |
| getPrompt: vi.fn().mockResolvedValue({ | |
| messages: [{ role: 'user', content: 'test prompt' }] | |
| }) | |
| }); | |
| const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); | |
| vi.mocked(Client).mockImplementation(() => mockClient as any); | |
| const result = await verifier.verifyServer(testServer, { | |
| timeout: 5000, | |
| maxConcurrentFetches: 3, | |
| includePromptDetails: true | |
| }); | |
| expect(result.status).toBe('success'); | |
| expect(result.capabilities?.prompts).toHaveLength(15); | |
| expect(mockClient.getPrompt).toHaveBeenCalledTimes(15); | |
| }); | |
| }); | 
Summary by CodeRabbit
New Features
Chores