feat: Add in-process SDK MCP server support (#142)

## 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>
This commit is contained in:
kashyap murali 2025-09-03 08:29:32 -07:00 committed by GitHub
parent 22fa9f473e
commit 9ef57859af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 879 additions and 18 deletions

181
examples/mcp_calculator.py Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Example: Calculator MCP Server.
This example demonstrates how to create an in-process MCP server with
calculator tools using the Claude Code Python SDK.
Unlike external MCP servers that require separate processes, this server
runs directly within your Python application, providing better performance
and simpler deployment.
"""
import asyncio
from typing import Any
from claude_code_sdk import (
ClaudeCodeOptions,
create_sdk_mcp_server,
query,
tool,
)
# Define calculator tools using the @tool decorator
@tool("add", "Add two numbers", {"a": float, "b": float})
async def add_numbers(args: dict[str, Any]) -> dict[str, Any]:
"""Add two numbers together."""
result = args["a"] + args["b"]
return {
"content": [
{
"type": "text",
"text": f"{args['a']} + {args['b']} = {result}"
}
]
}
@tool("subtract", "Subtract one number from another", {"a": float, "b": float})
async def subtract_numbers(args: dict[str, Any]) -> dict[str, Any]:
"""Subtract b from a."""
result = args["a"] - args["b"]
return {
"content": [
{
"type": "text",
"text": f"{args['a']} - {args['b']} = {result}"
}
]
}
@tool("multiply", "Multiply two numbers", {"a": float, "b": float})
async def multiply_numbers(args: dict[str, Any]) -> dict[str, Any]:
"""Multiply two numbers."""
result = args["a"] * args["b"]
return {
"content": [
{
"type": "text",
"text": f"{args['a']} × {args['b']} = {result}"
}
]
}
@tool("divide", "Divide one number by another", {"a": float, "b": float})
async def divide_numbers(args: dict[str, Any]) -> dict[str, Any]:
"""Divide a by b."""
if args["b"] == 0:
return {
"content": [
{
"type": "text",
"text": "Error: Division by zero is not allowed"
}
],
"is_error": True
}
result = args["a"] / args["b"]
return {
"content": [
{
"type": "text",
"text": f"{args['a']} ÷ {args['b']} = {result}"
}
]
}
@tool("sqrt", "Calculate square root", {"n": float})
async def square_root(args: dict[str, Any]) -> dict[str, Any]:
"""Calculate the square root of a number."""
n = args["n"]
if n < 0:
return {
"content": [
{
"type": "text",
"text": f"Error: Cannot calculate square root of negative number {n}"
}
],
"is_error": True
}
import math
result = math.sqrt(n)
return {
"content": [
{
"type": "text",
"text": f"{n} = {result}"
}
]
}
@tool("power", "Raise a number to a power", {"base": float, "exponent": float})
async def power(args: dict[str, Any]) -> dict[str, Any]:
"""Raise base to the exponent power."""
result = args["base"] ** args["exponent"]
return {
"content": [
{
"type": "text",
"text": f"{args['base']}^{args['exponent']} = {result}"
}
]
}
async def main():
"""Run example calculations using the SDK MCP server."""
# Create the calculator server with all tools
calculator = create_sdk_mcp_server(
name="calculator",
version="2.0.0",
tools=[
add_numbers,
subtract_numbers,
multiply_numbers,
divide_numbers,
square_root,
power
]
)
# Configure Claude to use the calculator server
options = ClaudeCodeOptions(
mcp_servers={"calc": calculator},
# Allow Claude to use calculator tools without permission prompts
permission_mode="bypassPermissions"
)
# Example prompts to demonstrate calculator usage
prompts = [
"Calculate 15 + 27",
"What is 100 divided by 7?",
"Calculate the square root of 144",
"What is 2 raised to the power of 8?",
"Calculate (12 + 8) * 3 - 10" # Complex calculation
]
for prompt in prompts:
print(f"\n{'='*50}")
print(f"Prompt: {prompt}")
print(f"{'='*50}")
async for message in query(prompt=prompt, options=options):
# Print the message content
if hasattr(message, 'content'):
for content_block in message.content:
if hasattr(content_block, 'text'):
print(f"Claude: {content_block.text}")
elif hasattr(content_block, 'name'):
print(f"Using tool: {content_block.name}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -14,7 +14,7 @@ bash commands, edit files, search the web, fetch web content) to accomplish.
# BASIC STREAMING
# ============================================================================
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock, ResultMessage
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
async with ClaudeSDKClient() as client:
print("User: What is 2+2?")
@ -32,7 +32,8 @@ async with ClaudeSDKClient() as client:
# ============================================================================
import asyncio
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client:
async def send_and_receive(prompt):
@ -53,7 +54,7 @@ async with ClaudeSDKClient() as client:
# PERSISTENT CLIENT FOR MULTIPLE QUESTIONS
# ============================================================================
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
# Create client
client = ClaudeSDKClient()
@ -88,8 +89,7 @@ await client.disconnect()
# IMPORTANT: Interrupts require active message consumption. You must be
# consuming messages from the client for the interrupt to be processed.
import asyncio
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock, ResultMessage
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client:
print("\n--- Sending initial message ---\n")
@ -141,7 +141,7 @@ async with ClaudeSDKClient() as client:
# ERROR HANDLING PATTERN
# ============================================================================
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
try:
async with ClaudeSDKClient() as client:
@ -168,7 +168,8 @@ except Exception as e:
# SENDING ASYNC ITERABLE OF MESSAGES
# ============================================================================
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async def message_generator():
"""Generate multiple messages as an async iterable."""
@ -209,7 +210,7 @@ async with ClaudeSDKClient() as client:
# COLLECTING ALL MESSAGES INTO A LIST
# ============================================================================
from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock, ResultMessage
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client:
print("User: What are the primary colors?")