Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion cookbook/anthropic_agent_sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,24 @@ litellm --config config.yaml

### 3. Run the chat

**Basic Agent (no MCP):**

```bash
python main.py
```

**Agent with MCP (DeepWiki2 for research):**

```bash
python agent_with_mcp.py
```

If MCP connection fails, you can disable it:

```bash
USE_MCP=false python agent_with_mcp.py
```

That's it! You can now chat with the agent in your terminal.

### Chat Commands
Expand All @@ -45,11 +59,19 @@ Set these environment variables if needed:
```bash
export LITELLM_PROXY_URL="http://localhost:4000"
export LITELLM_API_KEY="sk-1234"
export LITELLM_MODEL="claude-sonnet-4-20250514"
export LITELLM_MODEL="bedrock-claude-sonnet-4.5"
```

Or just use the defaults - it'll connect to `http://localhost:4000` by default.

## Files

- `main.py` - Basic interactive agent without MCP
- `agent_with_mcp.py` - Agent with MCP server integration (DeepWiki2)
- `common.py` - Shared utilities and functions
- `config.example.yaml` - Example LiteLLM configuration
- `requirements.txt` - Python dependencies

## Example Config File

If you want to use multiple models, create a `config.yaml` (see `config.example.yaml`):
Expand Down Expand Up @@ -110,6 +132,11 @@ Note: Don't add `/anthropic` to the base URL - LiteLLM handles the routing autom
- Check the model name matches what's in your LiteLLM config
- Run `litellm --model your-model` to test it works

**Agent with MCP stuck or failing?**
- The MCP server might not be available at `http://localhost:4000/mcp/deepwiki2`
- Try disabling MCP: `USE_MCP=false python agent_with_mcp.py`
- Or use the basic agent: `python main.py`

## Learn More

- [LiteLLM Docs](https://docs.litellm.ai/)
Expand Down
140 changes: 140 additions & 0 deletions cookbook/anthropic_agent_sdk/agent_with_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
Interactive Claude Agent SDK CLI with MCP Support

This example demonstrates an interactive CLI chat with the Anthropic Agent SDK using LiteLLM as a proxy,
with MCP (Model Context Protocol) server integration for enhanced capabilities.
"""

import asyncio
import os
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from common import (
Config,
fetch_available_models,
setup_litellm_env,
print_header,
handle_model_list,
handle_model_switch,
stream_response,
)


async def interactive_chat_with_mcp():
"""
Interactive CLI chat with the agent and MCP server
"""
config = Config()

# Configure Anthropic SDK to point to LiteLLM gateway
litellm_base_url = setup_litellm_env(config)

# Fetch available models from proxy
available_models = await fetch_available_models(litellm_base_url, config.LITELLM_API_KEY)

current_model = config.LITELLM_MODEL

# MCP server configuration
mcp_server_url = f"{litellm_base_url}/mcp/deepwiki2"
use_mcp = os.getenv("USE_MCP", "true").lower() == "true"

if not use_mcp:
print("⚠️ MCP disabled via USE_MCP=false")

print_header(litellm_base_url, current_model, has_mcp=use_mcp)

while True:
# Configure agent options
if use_mcp:
try:
# Try with MCP server (HTTP transport)
# Using McpHttpServerConfig format from Agent SDK
options = ClaudeAgentOptions(
system_prompt="You are a helpful AI assistant with access to DeepWiki for research. Be concise, accurate, and friendly.",
model=current_model,
max_turns=50,
mcp_servers={
"deepwiki2": {
"type": "http",
"url": mcp_server_url,
"headers": {
"Authorization": f"Bearer {config.LITELLM_API_KEY}"
}
}
},
)
except Exception as e:
print(f"⚠️ Warning: Could not configure MCP server: {e}")
print("Continuing without MCP...\n")
use_mcp = False
options = ClaudeAgentOptions(
system_prompt="You are a helpful AI assistant. Be concise, accurate, and friendly.",
model=current_model,
max_turns=50,
)
else:
# Without MCP
options = ClaudeAgentOptions(
system_prompt="You are a helpful AI assistant. Be concise, accurate, and friendly.",
model=current_model,
max_turns=50,
)

# Create agent client
try:
async with ClaudeSDKClient(options=options) as client:
conversation_active = True

while conversation_active:
# Get user input
try:
user_input = input("\nπŸ‘€ You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n\nπŸ‘‹ Goodbye!")
return

# Handle commands
if user_input.lower() in ['quit', 'exit']:
print("\nπŸ‘‹ Goodbye!")
return

if user_input.lower() == 'clear':
print("\nπŸ”„ Starting new conversation...\n")
conversation_active = False
continue

if user_input.lower() == 'models':
handle_model_list(available_models, current_model)
continue

if user_input.lower() == 'model':
new_model, should_restart = handle_model_switch(available_models, current_model)
if should_restart:
current_model = new_model
conversation_active = False
continue

if not user_input:
continue

# Stream response from agent
await stream_response(client, user_input)

except Exception as e:
print(f"\n❌ Error creating agent client: {e}")
print("This might be an MCP configuration issue. Try running without MCP:")
print(" USE_MCP=false python agent_with_mcp.py")
print("\nOr use the basic agent:")
print(" python main.py")
return


def main():
"""Run interactive chat with MCP"""
try:
asyncio.run(interactive_chat_with_mcp())
except KeyboardInterrupt:
print("\n\nπŸ‘‹ Goodbye!")


if __name__ == "__main__":
main()
160 changes: 160 additions & 0 deletions cookbook/anthropic_agent_sdk/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""
Common utilities for Claude Agent SDK examples
"""

import os
import httpx


class Config:
"""Configuration for LiteLLM Gateway connection"""

# LiteLLM proxy URL (default to local instance)
LITELLM_PROXY_URL = os.getenv("LITELLM_PROXY_URL", "http://localhost:4000")

# LiteLLM API key (master key or virtual key)
LITELLM_API_KEY = os.getenv("LITELLM_API_KEY", "sk-1234")

# Model name as configured in LiteLLM (e.g., "bedrock-claude-sonnet-4", "gpt-4", etc.)
LITELLM_MODEL = os.getenv("LITELLM_MODEL", "bedrock-claude-sonnet-4.5")


async def fetch_available_models(base_url: str, api_key: str) -> list[str]:
"""
Fetch available models from LiteLLM proxy /models endpoint
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/models",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10.0
)
response.raise_for_status()
data = response.json()
return [model["id"] for model in data.get("data", [])]
except Exception as e:
print(f"⚠️ Warning: Could not fetch models from proxy: {e}")
print("Using default model list...")
# Fallback to default models
return [
"bedrock-claude-sonnet-3.5",
"bedrock-claude-sonnet-4",
"bedrock-claude-sonnet-4.5",
"bedrock-claude-opus-4.5",
"bedrock-nova-premier",
]


def setup_litellm_env(config: Config):
"""
Configure environment variables to point Agent SDK to LiteLLM
"""
litellm_base_url = config.LITELLM_PROXY_URL.rstrip('/')
os.environ["ANTHROPIC_BASE_URL"] = litellm_base_url
os.environ["ANTHROPIC_API_KEY"] = config.LITELLM_API_KEY
return litellm_base_url


def print_header(base_url: str, current_model: str, has_mcp: bool = False):
"""
Print the chat header
"""
mcp_indicator = " + MCP" if has_mcp else ""
print("=" * 70)
print(f"πŸ€– Claude Agent SDK with LiteLLM Gateway{mcp_indicator} - Interactive Chat")
print("=" * 70)
print(f"πŸš€ Connected to: {base_url}")
print(f"πŸ“¦ Current model: {current_model}")
if has_mcp:
print("πŸ”Œ MCP: deepwiki2 enabled")
print("\nType your messages below. Commands:")
print(" - 'quit' or 'exit' to end the conversation")
print(" - 'clear' to start a new conversation")
print(" - 'model' to switch models")
print(" - 'models' to list available models")
print("=" * 70)
print()


def handle_model_list(available_models: list[str], current_model: str):
"""
Display available models
"""
print("\nπŸ“‹ Available models:")
for i, model in enumerate(available_models, 1):
marker = "βœ“" if model == current_model else " "
print(f" {marker} {i}. {model}")


def handle_model_switch(available_models: list[str], current_model: str) -> tuple[str, bool]:
"""
Handle model switching

Returns:
tuple: (new_model, should_restart_conversation)
"""
print("\nπŸ“‹ Select a model:")
for i, model in enumerate(available_models, 1):
marker = "βœ“" if model == current_model else " "
print(f" {marker} {i}. {model}")

try:
choice = input("\nEnter number (or press Enter to cancel): ").strip()
if choice:
idx = int(choice) - 1
if 0 <= idx < len(available_models):
new_model = available_models[idx]
print(f"\nβœ… Switched to: {new_model}")
print("πŸ”„ Starting new conversation with new model...\n")
return new_model, True
else:
print("❌ Invalid choice")
except (ValueError, IndexError):
print("❌ Invalid input")

return current_model, False


async def stream_response(client, user_input: str):
"""
Stream response from the agent
"""
print("\nπŸ€– Assistant: ", end='', flush=True)

try:
await client.query(user_input)

# Show loading indicator
print("⏳ thinking...", end='', flush=True)

# Stream the response
first_chunk = True
async for msg in client.receive_response():
# Clear loading indicator on first message
if first_chunk:
print("\rπŸ€– Assistant: ", end='', flush=True)
first_chunk = False

# Handle different message types
if hasattr(msg, 'type'):
if msg.type == 'content_block_delta':
# Streaming text delta
if hasattr(msg, 'delta') and hasattr(msg.delta, 'text'):
print(msg.delta.text, end='', flush=True)
elif msg.type == 'content_block_start':
# Start of content block
if hasattr(msg, 'content_block') and hasattr(msg.content_block, 'text'):
print(msg.content_block.text, end='', flush=True)

# Fallback to original content handling
if hasattr(msg, 'content'):
for content_block in msg.content:
if hasattr(content_block, 'text'):
print(content_block.text, end='', flush=True)

print() # New line after response

except Exception as e:
print(f"\r\n❌ Error: {e}")
print("Please check your LiteLLM gateway is running and configured correctly.")
Loading
Loading