## Summary
Adds in-process SDK MCP server support to the Python SDK, building on
the control protocol from #139.
**Note: Targets `dickson/control` branch (PR #139), not `main`.**
## Key Changes
- Added `@tool` decorator and `create_sdk_mcp_server()` API for defining
in-process MCP servers
- SDK MCP servers run directly in the Python process (no subprocess
overhead)
- Moved SDK MCP handling from Transport to Query class for proper
architectural layering
- Added `McpSdkServerConfig` type and integrated with control protocol
## Example
```python
from claude_code_sdk import tool, create_sdk_mcp_server
@tool("greet", "Greet a user", {"name": str})
async def greet_user(args):
return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
server = create_sdk_mcp_server(name="my-tools", tools=[greet_user])
options = ClaudeCodeOptions(mcp_servers={"tools": server})
```
## Testing
- Added integration tests in `test_sdk_mcp_integration.py`
- Added example calculator server in `examples/mcp_calculator.py`
---------
Co-authored-by: Dickson Tsai <dickson@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
This PR implements control protocol support in the Python SDK, aligning
it with the TypeScript implementation pattern. The refactor introduces a
Query + Transport separation to enable bidirectional communication
between the SDK and CLI.
## Motivation
The previous Python SDK implementation used a high-level abstraction in
the Transport ABC (`send_request`/`receive_messages`) that couldn't
handle bidirectional communication. This prevented support for:
- Control messages from CLI to SDK that need responses
- Hooks implementation
- Dynamic permission mode changes
- SDK MCP servers
## Changes
### Core Architecture Refactor
1. **New Query Class** (`src/claude_code_sdk/_internal/query.py`)
- Manages control protocol on top of Transport
- Handles control request/response routing
- Manages initialization handshake with timeout
- Supports hook callbacks and tool permission callbacks
- Implements message streaming
2. **Refactored Transport ABC**
(`src/claude_code_sdk/_internal/transport/__init__.py`)
- Changed from high-level (`send_request`/`receive_messages`) to
low-level (`write`/`read_messages`) interface
- Now handles raw I/O instead of protocol logic
- Aligns with TypeScript ProcessTransport pattern
3. **Updated SubprocessCLITransport**
(`src/claude_code_sdk/_internal/transport/subprocess_cli.py`)
- Simplified to focus on raw message streaming
- Removed protocol logic (moved to Query)
- Improved cleanup and error handling
4. **Enhanced ClaudeSDKClient** (`src/claude_code_sdk/client.py`)
- Now uses Query for control protocol
- Supports initialization messages
- Better error handling for control protocol failures
### Control Protocol Features
- **Initialization handshake**: SDK sends initialize request, CLI
responds with supported commands
- **Control message types**:
- `initialize`: Establish bidirectional connection
- `interrupt`: Cancel ongoing operations
- `set_permission_mode`: Change permission mode dynamically
- **Timeout handling**: 60-second timeout for initialization to handle
CLI versions without control support
### Examples
Updated `examples/streaming_mode.py` to demonstrate control protocol
initialization and error handling.
## Testing
- Tested with current CLI (no control protocol support yet) - gracefully
falls back
- Verified backward compatibility with existing `query()` function
- Tested initialization timeout handling
- Verified proper cleanup on errors
## Design Alignment
This implementation closely follows the TypeScript reference:
- `src/core/Query.ts` → `src/claude_code_sdk/_internal/query.py`
- `src/transport/ProcessTransport.ts` →
`src/claude_code_sdk/_internal/transport/subprocess_cli.py`
- `src/entrypoints/sdk.ts` → `src/claude_code_sdk/client.py`
## Next Steps
Once the CLI implements the control protocol handler, this will enable:
- Hooks support
- Dynamic permission mode changes
- SDK MCP servers
- Improved error recovery
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kashyap Murali <kashyap@anthropic.com>
The subprocess transport was ignoring user-specified cwd from
ClaudeCodeOptions, causing the working directory preference to be
overridden by os.environ. This fix ensures the CWD environment variable
is properly passed to child processes.
- Add PWD env var when self._cwd is set in SubprocessCLITransport
- Fixes issue where custom working directory was not respected
- Maintains backward compatibility
Tests: All existing tests pass
The PR should address the issue where user prompt maybe treated as a
option with leading `-`
Assuming `claude` does follow POSIX standard, the `--` option should
solve the issue.
fixes#129
This PR simplifies the example with interrupt.
There is a flag interrupt_sent that isn't required since the execution
loop will terminate after interrupt with the following message:
`{"content": [{"type": "text", "text": "[Request interrupted by
user]"}]}`
## Key changes
- Adds env field to `ClaudeCodeOptions`, allowing custom env vars to cli
- Updates tests and examples
## Motivation
Bringing Python SDK to feature parity with TS SDK, which supports custom
env vars
## Notes
- Environment variables are merged in order: system env → user env → SDK
required vars
- This implementation seems slightly more robust than the TypeScript
version, which can exclude OS envs vars if a user passes a minimal env
object
- Some linting changes seem to have been picked up
## Summary
This PR exposes the `Transport` interface in the public API, enabling
users to pass custom transport implementations to the `query()`
function. Previously, transport selection was internal and users had no
way to provide custom implementations.
**Primary Benefits:**
- 🔌 **Remote claude code** - Connect to Claude Code CLIs running
remotely
- The concrete use case here is to be able to implement a custom
transport that can communicate to claude code instances running in
remote sandboxes. Currently the the sdk only works with Claude Codes
running in a local subprocess limiting the scenarios in which this SDK
can be used.
## Changes
### Public API Changes
- **Exposed the previously internal `Transport` abstract base class** in
`claude_code_sdk.__init__.py`
- **Added `transport` parameter** to `query()` function signature
- **Updated docstring** with transport parameter documentation
### Internal Changes
- **Modified `InternalClient.process_query()`** to accept optional
transport parameter
- **Added transport selection logic** - use provided transport or
default to `SubprocessCLITransport`
- **Updated `__all__` exports** to include `Transport`
### Testing
- **Updated existing tests** to work with new transport parameter
- **Maintained backward compatibility** - all existing code continues to
work unchanged
## Testing
### Existing Tests
- ✅ All existing unit tests pass with new transport parameter
- ✅ Integration tests updated to mock new transport interface
- ✅ Subprocess buffering tests continue to work with exposed transport
### New Functionality Testing
- ✅ Verified custom transport can be passed to `query()`
- ✅ Confirmed default behavior unchanged when no transport provided
- ✅ Validated transport lifecycle ( connect → receive → disconnect)
- ✅ Tested transport interface compliance with abstract base class
## Example Usage
### Basic Custom Transport
```python
from claude_code_sdk import query, ClaudeCodeOptions, Transport
class MyCustomTransport(Transport):
# Implement abstract methods: connect, disconnect,
# send_request, receive_messages, is_connected
pass
transport = MyCustomTransport()
async for message in query(
prompt="Hello",
transport=transport
):
print(message)
```
## Related
- #85🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Signed-off-by: Rushil Patel <rpatel@codegen.com>
This PR updates the version to 0.0.20 after publishing to PyPI.
## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_code_sdk/__init__.py`
## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/0.0.20/
- Install with: `pip install claude-code-sdk==0.0.20`
🤖 Generated by GitHub Actions
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
Fixes an issue where `thinking` content blocks in Claude Code responses
were not being parsed, resulting in empty `AssistantMessage` content
arrays.
## Changes
- Added `ThinkingBlock` dataclass to handle thinking content with
`thinking` and `signature` fields
- Updated client parsing logic in `_internal/client.py` to recognize and
create `ThinkingBlock` instances
- Added comprehensive test coverage for thinking block functionality
## Before
```python
# Claude Code response with thinking block resulted in:
AssistantMessage(content=[]) # Empty content!
```
## After
```python
# Now correctly parses to:
AssistantMessage(content=[
ThinkingBlock(thinking="...", signature="...")
])
```
Fixes#27
---------
Co-authored-by: Dickson Tsai <dickson@anthropic.com>
`claude`'s `--mcp-config` takes either a JSON file or string, so support
this in `ClaudeCodeOptions`
```
--mcp-config <file or string> Load MCP servers from a JSON file or string
```
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Sam Fu <shunfu@users.noreply.github.com>
## Summary
- Adds `extra_args` field to `ClaudeCodeOptions` to support passing
arbitrary CLI flags
- Enables forward compatibility with future CLI flags without requiring
SDK updates
- Supports both valued flags (`--flag value`) and boolean flags
(`--flag`)
## Changes
- Add `extra_args: dict[str, str | None]` field to `ClaudeCodeOptions`
- Implement logic in `SubprocessCLITransport` to handle extra args:
- `None` values create boolean flags (e.g., `{"verbose": None}` →
`--verbose`)
- String values create flags with arguments (e.g., `{"output": "json"}`
→ `--output json`)
- Add comprehensive tests for the new functionality
## Test plan
- [x] Added unit tests for settings file path handling
- [x] Added unit tests for settings JSON object handling
- [x] Added unit tests for extra_args with both valued and boolean flags
- [x] All tests pass (`python -m pytest tests/`)
- [x] Type checking passes (`python -m mypy src/`)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Claude <noreply@anthropic.com>
the `mcp_tools` field in `ClaudeCodeOptions` seems to have been there
since the initial commit but I couldn't find any references in the repo,
so I believe it may be vestigial and unused
This PR updates the version to 0.0.19 after publishing to PyPI.
## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_code_sdk/__init__.py`
## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/0.0.19/
- Install with: `pip install claude-code-sdk==0.0.19`
🤖 Generated by GitHub Actions
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
Fixes a critical deadlock issue that occurs when MCP servers produce
verbose stderr output. The SDK would hang indefinitely when the stderr
pipe buffer filled up.
## The Problem
The deadlock occurred due to sequential reading of subprocess streams:
1. SDK reads stdout completely before reading stderr
2. When stderr pipe buffer fills (64KB on Linux, 16KB on macOS),
subprocess blocks on write
3. Subprocess can't continue to stdout, parent waits for stdout →
**DEADLOCK** 🔒
## The Solution
Redirect stderr to a temporary file instead of a pipe:
- **No pipe buffer** = no possibility of deadlock
- Temp file can grow as needed (no 64KB limit)
- Still capture stderr for error reporting (last 100 lines)
- Works consistently across all async backends
## Implementation Details
- `stderr=tempfile.NamedTemporaryFile()` instead of `stderr=PIPE`
- Use `deque(maxlen=100)` to keep only recent stderr lines in memory
- Temp file is automatically cleaned up on disconnect
- Add `[stderr truncated, showing last 100 lines]` message when buffer
is full
## Testing
- Verified no deadlock with 150+ lines of stderr output
- Confirmed stderr is still captured for error reporting
- All existing tests pass
- Works with asyncio, trio, and other anyio backends
## Impact
- Fixes consistent hangs in production with MCP servers
- No functional regression - stderr handling is preserved
- Simpler than concurrent reading alternatives
- More robust than pipe-based solutions
Fixes the issue reported in Slack where SDK would hang indefinitely when
receiving messages from MCP servers with verbose logging.
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
This PR updates the version to 0.0.18 after publishing to PyPI.
## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_code_sdk/__init__.py`
## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/0.0.18/
- Install with: `pip install claude-code-sdk==0.0.18`
🤖 Generated by GitHub Actions
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
- Add `settings` field to `ClaudeCodeOptions` to expose the `--settings`
CLI flag
- Allow SDK users to specify custom settings configuration path
## Changes
- Added `settings: str | None = None` field to `ClaudeCodeOptions`
dataclass
- Added CLI argument conversion logic in `SubprocessCLITransport` to
pass `--settings` flag to Claude Code CLI
## Test plan
- [x] All existing tests pass
- [x] Linting passes (`python -m ruff check`)
- [x] Type checking passes (`python -m mypy src/`)
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
This PR updates the version to 0.0.17 after publishing to PyPI.
## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_code_sdk/__init__.py`
## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/0.0.17/
- Install with: `pip install claude-code-sdk==0.0.17`
🤖 Generated by GitHub Actions
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
- Replace asyncio.create_task() with anyio task group for trio
compatibility
- Update client.py docstring example to use anyio.sleep
- Add trio example demonstrating multi-turn conversation
## Details
The SDK already uses anyio for most async operations, but one line was
using asyncio.create_task() which broke trio compatibility. This PR
fixes that by using anyio's task group API with proper lifecycle
management.
### Changes:
1. **subprocess_cli.py**: Replace asyncio.create_task() with anyio task
group, ensuring proper cleanup on disconnect
2. **client.py**: Update docstring example to use anyio.sleep instead of
asyncio.sleep
3. **streaming_mode_trio.py**: Add new example showing how to use the
SDK with trio
## Test plan
- [x] All existing tests pass
- [x] Manually tested with trio runtime (created test script that
successfully runs multi-turn conversation)
- [x] Linting and type checking pass
🤖 Generated with [Claude Code](https://claude.ai/code)
This PR updates the version to 0.0.16 after publishing to PyPI.
## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_code_sdk/__init__.py`
## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/0.0.16/
- Install with: `pip install claude-code-sdk==0.0.16`
🤖 Generated by GitHub Actions
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Adds commit signing
* Converts sed pattern matching to a script to ensure we don't update
version values unrelated to PyPI
* Remove the `Publish to Test PyPI first'` step since it no longer works
- Uses claude-code-base-action for automated issue triage
- Analyzes new issues and applies appropriate labels
- Uses GitHub MCP server for issue operations
- Requires ANTHROPIC_API_KEY secret to be configured
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>