Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 18, 2026

Concurrent tool calls with the same mcp-session-id hung indefinitely due to race conditions in the MCP SDK's StreamableHTTPServerTransport internal state maps.

Changes

  • Request queuing per session: Serialize concurrent requests on the same transport instance to prevent race conditions in _streamMapping, _requestToStreamMapping, and _requestResponseMap
  • Queue processor: Process requests sequentially while maintaining proper error propagation via Promise reject/resolve
  • Automatic cleanup: Remove empty queues and clear processing flags when transport closes

Implementation

Added queue infrastructure in startStreamableHTTPServer():

// Queue requests for existing sessions
if (sessionId && streamableTransports[sessionId]) {
  transport = streamableTransports[sessionId];
  await queueRequest(sessionId, req, res, req.body);
} else {
  // New sessions bypass queue
  transport = new StreamableHTTPServerTransport({...});
  await transport.handleRequest(req, res, req.body);
}

The queue processor ensures sequential execution:

const processRequestQueue = async (sessionId: string) => {
  if (sessionProcessingFlags[sessionId]) return;
  sessionProcessingFlags[sessionId] = true;
  
  while (sessionRequestQueues[sessionId]?.length > 0) {
    const queued = sessionRequestQueues[sessionId].shift()!;
    try {
      await executeTransportRequest(sessionId, queued.req, queued.res, queued.body);
      queued.resolve();
    } catch (error) {
      queued.reject(error);
    }
  }
  
  sessionProcessingFlags[sessionId] = false;
};

Test Coverage

Added test/concurrent-requests-test.ts covering:

  • Concurrent calls on same session (previously hung for 60s+)
  • Multiple concurrent calls (5 simultaneous requests)
  • Interleaved concurrent and sequential patterns
Original prompt

This section details on the original issue you should resolve

<issue_title>Concurrent tool calls on same session hang indefinitely (Dynamic GitLab API)</issue_title>
<issue_description>When using the Dynamic GitLab API, making concurrent tool calls with the same mcp-session-id causes the second (and subsequent) requests to hang indefinitely until timeout.

Expected Behavior

Both tool calls should complete successfully and return file contents.

Actual Behavior

First request completes successfully (returns file content)
Second request hangs indefinitely (no response)
After 60 seconds: McpError: MCP error -32001: Request timed out

script.js

import { MultiServerMCPClient } from '@langchain/mcp-adapters';

/**
 * 
 * Setup:
 * 1. Start MCP server
 * 2. Configure MCP_ENDPOINT below
 * 3. Run: node test.js
 * 
 * Expected: Both files fetched successfully
 * Actual: Second request hangs for 60s then times out
 */

const MCP_ENDPOINT = process.env.MCP_ENDPOINT || 'http://127.0.0.1:3002/mcp';
const PROJECT_ID = process.env.PROJECT_ID || 'xxx';

async function testConcurrentCalls() {
    console.log('🔧 Connecting to MCP server at:', MCP_ENDPOINT);

    const client = new MultiServerMCPClient({
        mcpServers: {
            gitlab: {
                transport: 'http',
                url: MCP_ENDPOINT,
                headers: {
                    Authorization: `Bearer xxx`,
                    'X-GitLab-API-URL': 'http://gitlab.xxx.com',
                },
            },
        },
        beforeToolCall: ({ serverName, name, args }) => {
            // Inject custom headers for this specific call
            return {
                args,
                headers: {
                    Authorization: `Bearer xxx`,
                    'X-GitLab-API-URL': 'http://gitlab.xxx.com',
                },
            }
        },
    });

    // Get tools
    const tools = await client.getTools();
    const getFileContentsTool = tools.find((t) => t.name === 'get_file_contents');

    if (!getFileContentsTool) {
        throw new Error('get_file_contents tool not found');
    }

    console.log('✅ Found get_file_contents tool');

    // Test 1: Sequential calls (should work)
    console.log('\n📝 Test 1: Sequential calls');
    console.time('Sequential');

    await getFileContentsTool.invoke({
        project_id: PROJECT_ID,
        file_path: 'package.json',
        ref: 'test',
    });
    console.log('  ✅ File 1 fetched');

    await getFileContentsTool.invoke({
        project_id: PROJECT_ID,
        file_path: 'README.md',
        ref: 'test',
    });
    console.log('  ✅ File 2 fetched');

    console.timeEnd('Sequential');

    // Test 2: Concurrent calls (BUG - second one will hang)
    console.log('\n⚠️  Test 2: Concurrent calls (THIS WILL HANG)');
    console.time('Concurrent');

    try {
        const [file3, file4] = await Promise.all([
            getFileContentsTool.invoke({
                project_id: PROJECT_ID,
                file_path: 'package.json',
                ref: 'test',
            }),
            getFileContentsTool.invoke({
                project_id: PROJECT_ID,
                file_path: 'README.md',
                ref: 'test',
            }),
        ]);

        console.log('  ✅ File 3 fetched');
        console.log('  ✅ File 4 fetched');
        console.timeEnd('Concurrent');
    } catch (error) {
        console.timeEnd('Concurrent');
        console.error('  ❌ Error:', error.message);
        throw error;
    }
}

testConcurrentCalls()
    .then(() => {
        console.log('\n✅ All tests passed!');
        process.exit(0);
    })
    .catch((error) => {
        console.error('\n❌ Test failed:', error.message);
        process.exit(1);
    });

script.js Output

🔧 Connecting to MCP server at: http://127.0.0.1:3002/mcp
✅ Client created
✅ Found get_file_contents tool

📝 Test 1: Sequential calls
  ✅ File 1 fetched
  ✅ File 2 fetched
Sequential: 354.626ms

⚠️  Test 2: Concurrent calls (THIS WILL HANG)
McpError: MCP error -32001: Request timed out
    at McpError.fromError (file:///home/binqi/github/dependencies-builder/node_modules/.pnpm/@modelcontextprotocol+sdk@1.25.1_@cfworker+json-schema@4.1.1_hono@4.10.6_zod@4.1.12/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js:2035:16)
    at Timeout.timeoutHandler (file:///home/binqi/github/dependencies-builder/node_modules/.pnpm/@modelcontextprotocol+sdk@1.25.1_@cfworker+json-schema@4.1.1_hono@4.10.6_zod@4.1.12/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js:692:58)
    at listOnTimeout (node:internal/timers:594:17)
    at process.processTimers (node:internal/timers:529:7) {
  code: -32001,
  data: { timeout: 60000 }
}
Concurrent: 1:00.039 (m:ss.mmm)
  ❌ Error: Error calling tool get_file_contents: McpError: MCP error -32001: Request timed out

❌ Test failed: Error calling tool get_file_contents: McpError: MCP error -32001: Request timed out

mcp output

INFO [06:35:3...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes zereight/gitlab-mcp#310

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI and others added 3 commits January 18, 2026 14:08
Co-authored-by: zereight <42544600+zereight@users.noreply.github.com>
Co-authored-by: zereight <42544600+zereight@users.noreply.github.com>
…dling

Co-authored-by: zereight <42544600+zereight@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix concurrent tool calls hanging issue with Dynamic GitLab API Fix concurrent requests on same session hanging indefinitely Jan 18, 2026
Copilot AI requested a review from zereight January 18, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants