A WebSocket server that wraps the Claude Agent SDK, allowing real-time bidirectional communication with Claude through WebSockets. Deploy it as an E2B sandbox and connect via the TypeScript client library.
Typical Workflow:
- Build Your E2B Image - Deploy the server as an E2B sandbox template using
bun run build:e2b - Use the Client Library - Install
@dzhng/claude-agentin your project and connect to your E2B sandbox - Modify the Server (Optional) - If you need custom behavior, edit the server code in
packages/server/ - Test Locally - Use
bun run start:serverandbun run test:localto test your changes before rebuilding
Create a .env file in the root directory:
cp .env.example .envAdd your API keys:
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
E2B_API_KEY=e2b_your-api-key-hereInstall dependencies:
bun installBuild and deploy the server as an E2B template:
bun run build:e2bThis creates a sandbox template named claude-agent-server on E2B. The build process:
- Creates a sandbox based on Bun 1.3
- Installs git and clones this repository
- Installs dependencies
- Configures the server to start automatically on port 3000
The build may take a few minutes. Once complete, your template is ready to use.
Install the client library in your project:
npm install @dzhng/claude-agent
# or
bun add @dzhng/claude-agentConnect to your E2B sandbox:
import { ClaudeAgentClient } from '@dzhng/claude-agent'
const client = new ClaudeAgentClient({
e2bApiKey: process.env.E2B_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
template: 'claude-agent-server', // Your E2B template name
debug: true,
})
// Start the client (creates E2B sandbox and connects)
await client.start()
// Listen for messages from Claude
client.onMessage(message => {
if (message.type === 'sdk_message') {
console.log('Claude:', message.data)
}
})
// Send a message to Claude
client.send({
type: 'user_message',
data: {
type: 'user',
session_id: 'my-session',
message: {
role: 'user',
content: 'Hello, Claude!',
},
},
})
// Clean up when done
await client.stop()That's it! The client library handles:
- Creating and managing E2B sandboxes
- WebSocket connection
- Message serialization
- Cleanup and resource management
If you want to customize the server behavior:
The server code is in packages/server/:
index.ts- Main server and WebSocket handlingmessage-handler.ts- Message processing logicconst.ts- Configuration constants
Start the server locally:
bun run start:serverIn another terminal, run the test client against localhost:
bun run test:localThis runs packages/client/example-client.ts connected to localhost:3000 instead of E2B.
Once you're satisfied with your changes, rebuild the E2B template:
bun run build:e2bYour updated server will be deployed to E2B with the same template name.
Builds and deploys the server as an E2B template. This is the main way to deploy your server to the cloud.
Runs the example client (packages/client/example-client.ts) connected to an E2B sandbox. Requires both E2B_API_KEY and ANTHROPIC_API_KEY in your .env file.
Starts the server locally on http://localhost:3000. Use this for local development and testing.
Runs the example client connected to localhost:3000. Use this to test your local server changes before rebuilding the E2B image.
npm install @dzhng/claude-agent
# or
bun add @dzhng/claude-agentinterface ClientOptions {
// Required (unless using environment variables)
anthropicApiKey?: string
// E2B Configuration (optional if using connectionUrl)
e2bApiKey?: string
template?: string // E2B template name, defaults to 'claude-agent-server'
timeoutMs?: number // Sandbox timeout, defaults to 5 minutes
// Connection Configuration
connectionUrl?: string // Use this to connect to local/custom server instead of E2B
// Other Options
debug?: boolean // Enable debug logging
// Query Configuration (passed to server)
agents?: Record<string, AgentDefinition>
allowedTools?: string[]
systemPrompt?:
| string
| { type: 'preset'; preset: 'claude_code'; append?: string }
model?: string
}async start()- Initialize the client and connect to the serversend(message: WSInputMessage)- Send a message to the agentonMessage(handler: (message: WSOutputMessage) => void)- Register a message handler (returns unsubscribe function)async stop()- Disconnect and clean up resources
import { ClaudeAgentClient } from '@dzhng/claude-agent'
const client = new ClaudeAgentClient({
e2bApiKey: process.env.E2B_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
template: 'claude-agent-server',
debug: true,
})
await client.start()
client.onMessage(message => {
if (message.type === 'sdk_message') {
console.log('Claude:', message.data)
}
})
client.send({
type: 'user_message',
data: {
type: 'user',
session_id: 'session-1',
message: { role: 'user', content: 'Hello' },
},
})
await client.stop()const client = new ClaudeAgentClient({
connectionUrl: 'http://localhost:3000',
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
})
await client.start()For more details, see packages/client/README.md.
Note: If you're using the @dzhng/claude-agent library, you don't need to interact with these endpoints directly. The client handles configuration and WebSocket connections for you. This section is for advanced users who want to connect to the server directly or build their own client.
The server runs on http://localhost:3000 (or your E2B sandbox URL) with:
- Config endpoint:
http://localhost:3000/config - WebSocket endpoint:
ws://localhost:3000/ws
Set the configuration for the Claude Agent SDK query:
type QueryConfig = {
agents?: Record<string, AgentDefinition>
allowedTools?: string[]
systemPrompt?:
| string
| {
type: 'preset'
preset: 'claude_code'
append?: string
}
model?: string
anthropicApiKey?: string
}Example:
curl -X POST http://localhost:3000/config \
-H "Content-Type: application/json" \
-d '{
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"],
"anthropicApiKey": "sk-ant-...",
"model": "claude-3-5-sonnet-20241022",
"agents": {
"myAgent": {
"name": "My Custom Agent",
"description": "A custom agent"
}
}
}'Response:
{
"success": true,
"config": {
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"],
"agents": { ... }
}
}Get the current configuration:
curl http://localhost:3000/configResponse:
{
"config": {
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"]
}
}Connect to the WebSocket endpoint:
const ws = new WebSocket('ws://localhost:3000/ws')Note: The server only accepts one active connection at a time. If another client is already connected, new connection attempts will be rejected with an error message.
Sending Messages (Client → Server)
type WSInputMessage =
| {
type: 'user_message'
data: SDKUserMessage
}
| {
type: 'interrupt'
}User Message:
Send a wrapped SDKUserMessage:
{
"type": "user_message",
"data": {
"type": "user",
"session_id": "your-session-id",
"parent_tool_use_id": null,
"message": {
"role": "user",
"content": "Hello, Claude!"
}
}
}Structure:
type: Must be"user_message"data: AnSDKUserMessageobject containing:type: Must be"user"session_id: Your session identifier (string)message: An object withroleandcontentrole: Must be"user"content: The message content (string)
parent_tool_use_id: Optional, for tool use responsesuuid: Optional, message UUID (auto-generated if not provided)
Interrupt Message:
Send an interrupt to stop the current agent operation:
{
"type": "interrupt"
}Receiving Messages (Server → Client)
type WSOutputMessage =
| { type: 'connected' }
| { type: 'sdk_message'; data: unknown }
| { type: 'error'; error: string }Connection confirmation:
{
"type": "connected"
}SDK messages (responses from Claude):
{
"type": "sdk_message",
"data": {
"type": "assistant",
"session_id": "...",
"message": {
/* Claude's response */
}
}
}Error messages:
{
"type": "error",
"error": "Error description"
}The server is a simple 1-to-1 relay between a single WebSocket client and the Claude Agent SDK:
- Configuration (optional): Client can POST to
/configto set agents, allowedTools, and systemPrompt - Client Connects: A WebSocket connection is established (only one allowed at a time)
- Client Sends Message: Client sends a user message (or interrupt)
- Message Queuing: Server adds messages to the queue and processes them with the SDK
- SDK Processing: The SDK query stream processes messages using the configured options
- Response Relay: SDK responses are immediately sent back to the connected WebSocket client
- Cleanup: When the client disconnects, the server is ready to accept a new connection
Key Design Principles:
- Pre-connection configuration: Configure query options via
/configendpoint before connecting - Lazy initialization: Query stream only starts when first WebSocket connection is made
- Single connection only: Server rejects additional connection attempts while one is active
- Simple relay: Server relays messages between WebSocket and SDK without session management
- Message queue: Incoming messages are queued and processed by the SDK stream
- Interrupt support: Clients can send interrupt messages to stop ongoing operations
- Direct routing: All SDK responses go to the single active WebSocket connection
The codebase follows a monorepo structure:
claude-agent-server/
├── packages/
│ ├── server/ # Main server implementation
│ │ ├── index.ts
│ │ ├── message-handler.ts
│ │ └── ...
│ ├── client/ # Client library and examples
│ │ ├── src/
│ │ └── example-client.ts
│ └── e2b-build/ # E2B build scripts
│ └── build.prod.ts
├── package.json # Root package.json (workspaces)
└── README.md
Open http://localhost:3000/ in your browser to access the built-in test client. You can:
- Send messages to Claude
- See real-time responses
- View the full JSON structure of SDK messages
Run the example client script:
bun run test:clientThis will connect to the server (or E2B sandbox), send a few test messages, and display the responses.
This section provides additional details about E2B deployment. For the basic setup, see the Quick Start section.
By default, bun run build:e2b creates a template named claude-agent-server. To use a different name, you can modify packages/e2b-build/build.prod.ts or specify it when using the client:
import { ClaudeAgentClient } from '@dzhng/claude-agent'
const client = new ClaudeAgentClient({
template: 'my-custom-template', // Use your custom template name
e2bApiKey: process.env.E2B_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
})
await client.start()When you use the client library with E2B:
- Sandbox Creation: A fresh sandbox is created from your built template (
claude-agent-serverby default) - Automatic Startup: The server starts automatically in the sandbox on port 3000 (configured via
setStartCmdinbuild.prod.ts) - Secure Endpoints: E2B provides HTTPS and WSS endpoints for your sandbox
- Isolation: Each sandbox runs in complete isolation with its own filesystem and resources
- Automatic Cleanup: Sandboxes are terminated when the client disconnects
To test with E2B, simply run:
bun run test:clientThis runs packages/client/example-client.ts which creates an E2B sandbox, connects to it, runs test commands, and cleans up.
The template is defined in packages/e2b-build/build.prod.ts:
const template = Template()
.fromBunImage('1.3') // Use Bun 1.3 base image
.runCmd('sudo apt install -y git') // Install git
.gitClone('https://github.com/...', ...) // Clone repository
.setWorkdir('/home/user/app') // Set working directory
.runCmd('bun install') // Install dependencies
.setStartCmd('bun packages/server/index.ts', waitForPort(3000)) // Start serverYou can customize this template to:
- Install additional system packages
- Pre-configure environment variables
- Change resource limits (CPU, memory)
- Modify the startup command
Local Development (localhost):
- Faster iteration
- Direct access to local filesystem
- No sandbox overhead
- Good for development and testing
E2B Deployment:
- Isolated execution environment
- Secure cloud sandboxes
- Scalable infrastructure
- Production-ready
- No local setup required
The server uses port 3000 by default. You can modify this in packages/server/index.ts:
const server = Bun.serve<SessionData>({
port: 3000, // Change this
// ...
})Environment variables are loaded from the root .env file. See Quick Start for setup instructions.
API Key Priority:
- If you set
anthropicApiKeyvia the Configuration API (/configendpoint), it will override theANTHROPIC_API_KEYenvironment variable. - When using the client library, you can pass
anthropicApiKeyin the constructor options.
MIT
