[BUG] Claude Code SDK: SDK MCP server faills to connect due to closed stream

Resolved 💬 26 comments Opened Aug 28, 2025 by codylund Closed Oct 8, 2025
💡 Likely answer: A maintainer (dltn, contributor) responded on this thread — see the highlighted reply below.

Environment

  • Platform (select one):
  • [ ] Anthropic API
  • [ ] AWS Bedrock
  • [ ] Google Vertex AI
  • [x] Other: Claude Code SDK
  • Claude CLI version: 1.0.96
  • Operating System: macOS 15.1
  • Terminal: Terminal App

Bug Description

The new SDK MCP server type does not work with basic string prompts.
Running with DEBUG=true, I see the following error:

[ERROR] MCP server "test" Failed to connect SDK MCP server: Error: Stream closed

Steps to Reproduce

Below is a basic sample:

import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-code";
import { z } from "zod";

const mcp = createSdkMcpServer({
  name: "test",
  tools: [
    tool(
      "add",
      "Add two numbers",
      {
        a: z.number(),
        b: z.number(),
      },
      async ({ a, b }) => {
        return {
          content: [
            {
              type: "text",
              text: `${a + b}`,
            },
          ],
        };
      }
    ),
  ],
});

for await (const message of query({
  //prompt: generateMessages(),
  prompt: "Use the tool to add 1 and 2 and return the result.",
  options: {
    mcpServers: {
      test: mcp,
    },
    allowedTools: ["mcp__test__add"],
  },
})) {
  console.log(JSON.stringify(message));
}

Expected Behavior

Prompt should run with successful connection and access to the SDK MCP server.

Actual Behavior

Claude CLI process exits with an error.

View original on GitHub ↗

26 Comments

selenehyun · 10 months ago

I encountered the same issue today and was able to work around it with a minimal adjustment:

  1. Refactored into an MCP Server using @modelcontextprotocol/sdk (this was straightforward since the code format is nearly identical).
  2. Updated the mcpServers definition as shown below.

Before:

for await (const message of query({
    prompt: ...,
    options: {
        allowedTools: [
            ...
            'mcp__test',
        ],
        mcpServers: {
            'test': testMcpServer,
        },
    },
})) {

After:

for await (const message of query({
    prompt: ...,
    options: {
        allowedTools: [
            ...
            'mcp__test',
        ],
        mcpServers: {
            'test': {
                type: 'stdio',
                command: 'npx',
                args: ['-y', 'tsx', 'src/mcps/test.ts'],
            },
        },
    },
})) {

With this approach, I was able to get things running in under five minutes with only minimal changes.
Depending on your setup, you may need to use a built JS artifact instead of tsx, but for my use case this was sufficient.

codylund · 10 months ago

@selenehyun I'm glad to hear this works for you, but this does not run the local MCP server in the same process. That's my main motivation for using the SDK type.

codylund · 10 months ago

This still reproduces with v1.0.96...

bolahanna44 · 10 months ago

same issue for me

ShuangLiu1992 · 10 months ago

Broken with v1.0.98 as well

xpluscal · 10 months ago

same issue

tom-n-terra · 10 months ago

same issue , even without the allowedTools prop

tariqkb · 10 months ago

Trying the example here https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-typescript#creating-custom-tools but it doesn't seem to work either:

[ERROR] MCP server "my-custom-tools" Failed to connect SDK MCP server: Error: Stream closed
codylund · 10 months ago

FWIW, I have the SDK MCP server type working in a single Bun-based project using v1.0.98, but I can't get it working in any other project. I have no idea why it works yet...

ShuangLiu1992 · 10 months ago

@codylund by any other project, do you mean projects using node or deno?

codylund · 10 months ago

@ShuangLiu1992 Just any other Bun-based projects in general. I don't know what's special about the project where the SDK MCP server is functional. It's an overall basic set up and is very similar to the example I added in my original post. It makes me suspect some kind of obscure race condition in the Claude Code SDK.

adamk-au · 10 months ago

It seems to still be an issue in v1.0.105

kmaillet · 10 months ago

Same issue here

will-dayai · 10 months ago

Running into this bug as well!

tariqkb · 10 months ago

Found a workaround

import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-code"
import { z } from "zod/v3"

const options = {
  name: "my-custom-tools",
  version: "1.0.0",
  tools: [
    tool(
      "calculate_compound_interest",
      "Calculate compound interest for an investment",
      {
        principal: z.number().describe("Initial investment amount"),
        rate: z.number().describe("Annual interest rate (as decimal, e.g., 0.05 for 5%)"),
        time: z.number().describe("Investment period in years"),
        n: z.number().default(12).describe("Compounding frequency per year"),
      },
      async args => {
        console.log("calculate_compound_interest was called")
        
        const amount = args.principal * (1 + args.rate / args.n) ** (args.n * args.time)
        const interest = amount - args.principal

        return {
          content: [
            {
              type: "text",
              text: `Final amount: $${amount.toFixed(2)}\nInterest earned: $${interest.toFixed(2)}`,
            },
          ],
        }
      },
    ),
  ],
}

const customServer = createSdkMcpServer(options)

function userMessage(content) {
  return {
    type: "user",
    message: { role: "user", content: content },
  }
}

async function* prompt() {
  yield userMessage("Calculate compound interest for $10,000 at 5% for 10 years using the calculate_compound_interest tool")
  await new Promise(res => setTimeout(res, 10000))
}

for await (const message of query({
  prompt: prompt(),
  options: {
    permissionMode: "bypassPermissions",
    mcpServers: {
      "my-custom-tools": customServer,
    },
    maxTurns: 3,
  },
})) {
  if (message.type === "result") {
    console.log(message.result)
  }
}

The key is keeping the input stream artificially open via the await new Promise(res => setTimeout(res, 10000)) sleep. It isn't ideal, but there is likely a better workaround that closes the input stream once claude-code returns the result.

arthurgousset · 10 months ago

Looks like this was fixed in the Python Claude Code SDK: https://github.com/anthropics/claude-code-sdk-python/pull/157

xujialiang · 10 months ago

same issue

stevenzg · 10 months ago

same issue

xujialiang · 10 months ago
I encountered the same issue today and was able to work around it with a minimal adjustment: 1. Refactored into an MCP Server using @modelcontextprotocol/sdk (this was straightforward since the code format is nearly identical). 2. Updated the mcpServers definition as shown below. Before: for await (const message of query({ prompt: ..., options: { allowedTools: [ ... 'mcp__test', ], mcpServers: { 'test': testMcpServer, }, }, })) { After: for await (const message of query({ prompt: ..., options: { allowedTools: [ ... 'mcp__test', ], mcpServers: { 'test': { type: 'stdio', command: 'npx', args: ['-y', 'tsx', 'src/mcps/test.ts'], }, }, }, })) { With this approach, I was able to get things running in under five minutes with only minimal changes. Depending on your setup, you may need to use a built JS artifact instead of tsx, but for my use case this was sufficient.

work for me

arthurgousset · 10 months ago
await new Promise(res => setTimeout(res, 10000))

Works for me as a workaround. Thanks for sharing @tariqkb ❤️

Gerauddasp · 10 months ago

Here is the same workaround but not waiting for the full timeout simply by checking if "results" are in messages. Also no need to have bypassPermissions and you can allow and disallow tools. Thanks @claude for the help.

describe('SDK MCP Tools - TypeScript Translation', () => {
  let executions: string[];

  beforeEach(() => {
    executions = [];
  });

  it('should execute SDK MCP tools when allowed', { timeout: 35000 }, async () => {
    // Create echo tool that tracks executions
    const echoTool = tool(
      'echo',
      'Echo back the input text',
      {
        text: z.string().describe('Text to echo')
      },
      async (args) => {
        executions.push('echo');
        return {
          content: [{ type: 'text', text: `Echo: ${args.text}` }]
        };
      }
    );

    // Create test server with echo tool
    const server = createSdkMcpServer({
      name: 'test',
      version: '1.0.0',
      tools: [echoTool]
    });

    // Shared messages array that both generator and consumer can access
    const messages: any[] = [];

    // Create async generator that monitors for result message
    async function* promptGenerator() {
      yield {
        type: 'user' as const,
        message: { 
          role: 'user' as const, 
          content: 'Call the mcp__test__echo tool with text "Hello World"'
        }
      } as SDKUserMessage;
      
      // Wait for result message
      const maxChecks = 300; // Max 30 seconds (300 * 100ms)
      for (let i = 0; i < maxChecks; i++) {
        await new Promise(resolve => setTimeout(resolve, 100));
        
        // Check if we received a result message
        if (messages.some(m => m.type === 'result')) {
          break;
        }
      }
    }

    // Consume messages and populate the shared array
    for await (const message of query({
      prompt: promptGenerator(),
      options: {
        mcpServers: { test: server },
        allowedTools: ['mcp__test__echo'],
        maxTurns: 3
      }
    })) {
      messages.push(message);
    }

    // Verify the tool function was actually executed
    expect(executions).toContain('echo');
    expect(executions.length).toBe(1);
  });
}
dltn contributor · 10 months ago

Hi folks! Fix is ready and should be out soon in ~v1.0.121. You'll no longer need the timeout, but you will still need to provide a AsyncIterable<SDKUserMessage> streaming prompt (not a string prompt)

tom-n-terra · 9 months ago

Hi @dltn i am using the 1.0128 version and the createSdkMcpServer creates the mcp server but in the init message it is always on status "failed". i am using an AsyncIterable and claude code is running but can't locate the custom tool, used exactly the same example in the docs.
any thoughts?

const customServer = createSdkMcpServer({
      name: 'weather-tools',
      tools: [
        tool(
          'get_weather',
          'Get current weather for a location',
          {
            location: z.string().describe('City name or coordinates'),
            units: z
              .enum(['celsius', 'fahrenheit'])
              .default('celsius')
              .describe('Temperature units'),
          },
          async (args) => {
            // Call weather API
            const response = await fetch(
              `https://api.weather.com/v1/current?q=${args.location}&units=${args.units}`,
            );
            const data = await response.json();

            return {
              content: [
                {
                  type: 'text',
                  text: `Temperature: ${data.temp}°\nConditions: ${data.conditions}\nHumidity: ${data.humidity}%`,
                },
              ],
            };
          },
        ),
      ],
    });

    async function* generateMessages() {
      yield {
        type: 'user' as const,
        message: {
          role: 'user' as const,
          content: 'what is the weather in Tokyo ? use the weather tool',
        },
      } as SDKUserMessage;
    }
    for await (const message of query({
      prompt: generateMessages(),
      options: {
        mcpServers: {
          test: customServer,
        },
        allowedTools: ['mcp__weather-tools__get_weather'],
      },
    })) {
      console.log(JSON.stringify(message, null, 2));
    }

and in the init message i am always seeing this:

"mcp_servers": [..., { "name" : "test" , "status": "failed" }]

which dosn't let's claude code use the whether tool

dltn contributor · 9 months ago

@tom-n-terra What does your npm list claude-code say? With @anthropic-ai/claude-cli@2.0.3, your test file works for me:

  "mcp_servers": [
    {
      "name": "test",
      "status": "connected"
    }
dltn contributor · 9 months ago

Going to close this. Please file another issue if you experience this again!

github-actions[bot] · 9 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.