@shaykec/agent-web
v0.3.5
Published
Generic framework for exposing Claude Code to web apps — server middleware, React hooks, and embeddable components
Downloads
848
Maintainers
Readme
@shaykec/agent-web
A framework for adding Claude Code capabilities to any application — web, desktop, or server. Provides a Node.js server that wraps the Claude Agent SDK, a layered configuration model, and multiple client integration options.
Web App — Multi-Session Chat

macOS App — Native SwiftUI with Embedded Server

Install
npm install @shaykec/agent-webPeer dependencies (install as needed):
npm install @anthropic-ai/claude-agent-sdk # Required for server
npm install react react-dom # Required for React hooks/componentsHow It Works
flowchart LR
App["Your App"]
Server["agent-web Server"]
SDK["Claude Agent SDK"]
CLI["Claude Code CLI"]
App <-->|"WS / SSE / REST"| Server
Server <-->|"sessions + config"| SDK
SDK <-->|"subprocess"| CLIYour application talks to the agent-web server over standard protocols. The server manages sessions, resolves configuration, and streams Claude responses back. Claude Code runs locally via the SDK — no API key required if you have a Claude subscription.
When to Use What
| I want to… | Use | Example |
|---|---|---|
| Drop a chat widget into my React app | Embeddable Component (<ClaudeChat />) | Product support bot, internal tool |
| Build a custom chat UI with full control | React Hooks (useChat, useSessions) | IDE-like experience, branded AI assistant |
| Add Claude to a non-React app (Vue, Svelte, vanilla) | Vanilla JS Client (ClaudeClient) | Any framework, or no framework |
| Mount Claude as a route on my Express/Hono server | Server Middleware (agent.middleware()) | Multi-service backend, API gateway |
| Run multiple concurrent conversations with config control | Multi-Session | Agent dashboard, team collaboration |
| Build a native desktop app with Claude | WebSocket Protocol | macOS (SwiftUI), Electron, Tauri |
Integration Architectures
1. Embeddable Component
Best for: quickly adding a chat widget to any React app.
flowchart LR
subgraph Browser
RC["<ClaudeChat />"]
end
subgraph Node
AS["agent-web Server<br/>(standalone)"]
end
RC <-->|WS + REST| AS
AS <--> SDK["Agent SDK"]import { ClaudeChat } from '@shaykec/agent-web/components';
function App() {
return <ClaudeChat url="http://localhost:3456" theme="dark" />;
}One line to render. The component handles connection, session management, streaming, tool-use display, and reconnection. Pass config props to control model, tools, and system prompt.
Every section of ClaudeChat is customizable via render props:
<ClaudeChat
url="http://localhost:3456"
theme="dark"
themeTokens={{ primary: '#58a6ff', background: '#0d1117' }}
labels={{ title: 'MyApp', placeholder: 'Ask anything...', send: 'Go' }}
renderWelcome={({ send }) => <MyWelcome onQuickStart={send} />}
renderMessage={(msg, theme) => <MyMessage msg={msg} />}
renderInput={(props) => <MyInput {...props} />}
handshake={{ clientType: 'myapp', capabilities: ['chat'] }}
onRawMessage={(msg) => {
if (msg.type.startsWith('custom:')) handleCustomMessage(msg);
}}
/>2. React Hooks (Custom UI)
Best for: full control over the UI while letting the framework handle protocol and state.
flowchart LR
subgraph Browser
P["ClaudeProvider"]
H["useChat / useSessions"]
UI["Your Components"]
P --> H --> UI
end
subgraph Node
AS["agent-web Server"]
end
Browser <-->|WS + REST| AS
AS <--> SDK["Agent SDK"]import { ClaudeProvider, useChat, useSessions } from '@shaykec/agent-web/react';
function App() {
return (
<ClaudeProvider
url="http://localhost:3456"
config={{ model: 'claude-sonnet-4-6' }}
onMessage={(msg) => {
// Route non-chat messages (visual commands, notifications, etc.)
if (msg.type.startsWith('canvas:')) handleVisual(msg);
}}
handshake={{ clientType: 'myapp', capabilities: ['dashboard'] }}
>
<MyChatUI />
</ClaudeProvider>
);
}
function MyChatUI() {
const { messages, send, stop, isStreaming, resolvedConfig } = useChat();
const { sessions, create, resume } = useSessions();
// Render however you want
}Or use the composable building blocks for even more control:
import { useChat, useSessions } from '@shaykec/agent-web/react';
import { ChatMessages, ChatInput, SessionPicker } from '@shaykec/agent-web/components';
function MyChatUI() {
const chat = useChat();
const { sessions, resume } = useSessions();
return (
<div className="my-layout">
<SessionPicker sessions={sessions} onResume={resume} activeSessionId={chat.sessionId} />
<ChatMessages
messages={chat.messages}
renderWelcome={() => <MyWelcomeScreen />}
renderMessage={(msg) => <MyCustomBubble msg={msg} />}
/>
<ChatInput onSend={chat.send} onStop={chat.stop} isStreaming={chat.isStreaming} />
</div>
);
}3. Vanilla JS Client
Best for: non-React frameworks (Vue, Svelte, Angular) or plain HTML pages.
flowchart LR
subgraph Browser
VC["ClaudeClient<br/>(vanilla JS)"]
end
subgraph Node
AS["agent-web Server"]
end
VC <-->|WS + REST| AS
AS <--> SDK["Agent SDK"]import { ClaudeClient } from '@shaykec/agent-web/client';
const client = new ClaudeClient('http://localhost:3456');
await client.connect();
const { sessionId } = await client.createSession();
client.onMessage((msg) => console.log(msg));
await client.send(sessionId, 'What files are here?');4. Server Middleware
Best for: mounting Claude alongside existing routes on your HTTP server.
flowchart LR
subgraph Your Server
EX["Express / Hono / etc."]
MW["agent.middleware()"]
EX --> MW
end
subgraph External
SDK["Agent SDK"]
end
Client <-->|"HTTP + WS"| EX
MW <--> SDKimport express from 'express';
import { createAgentServer } from '@shaykec/agent-web/server';
const app = express();
const agent = createAgentServer({ config: { model: 'claude-sonnet-4-6' } });
app.use('/claude', agent.middleware());
agent.attachWebSocket(server, '/claude/ws');5. Multi-Session + Config Control
Best for: dashboards, admin tools, or apps that need multiple concurrent conversations with per-session configuration.
flowchart TB
subgraph Browser
T1["Session A<br/>model: haiku"]
T2["Session B<br/>model: sonnet"]
T3["Session C<br/>model: opus"]
end
subgraph Node
CR["ConfigResolver<br/>(merge + constrain)"]
SM["SessionManager<br/>(Map of sessions)"]
TR["Transport<br/>(broadcast)"]
end
T1 & T2 & T3 <-->|WS| TR
TR --> SM
SM --> CR
SM <--> SDK["Agent SDK"]Each session can request its own model, tools, and system prompt. The server merges requests with defaults and enforces constraints (e.g., maxModel, disallowedTools). Messages are tagged with sessionId so clients route them correctly.
6. Native Desktop App (macOS / Electron / Tauri)
Best for: native apps with an embedded server — users just launch the app.
flowchart LR
subgraph Desktop["macOS App"]
SW["SwiftUI Views"]
SP["ServerProcess<br/>(spawns node)"]
WC["WebSocket Client"]
SW --> WC
SW --> SP
SP -->|"child process"| AS
end
subgraph AS["agent-web Server"]
SM["SessionManager"]
end
WC <-->|"WS protocol"| AS
AS <--> SDK["Agent SDK"]The macOS demo app embeds the agent-web server as a child process. Users just launch the app — ServerProcess finds node, starts server.js, health-checks, and auto-connects via WebSocket. Server logs are visible in-app. Any language/framework that speaks WebSocket can integrate — the protocol is simple JSON envelopes.
Server Setup
import { createAgentServer } from '@shaykec/agent-web/server';
const agent = createAgentServer({
config: { ... }, // Default session config (see below)
constraints: { ... }, // Hard limits clients cannot exceed
hooks: { ... }, // Server-side event hooks
});
agent.listen(3456);REST Endpoints
| Endpoint | Method | Description |
|---|---|---|
| /chat/start | POST | Create a new session ({ config }) |
| /chat/message | POST | Send a message ({ sessionId, text }) |
| /chat/stop | POST | Stop generation ({ sessionId }) |
| /chat/resume | POST | Resume session ({ sessionId, config }) |
| /chat/sessions | GET | List available sessions |
| /chat/config | GET | Get server config (sanitized) |
| /health | GET | Health check |
| /ws | WS | WebSocket endpoint |
| /sse | GET | SSE stream |
Configuration Reference
Config flows through a merge pipeline with clear precedence:
flowchart TD
CP["Component Props"] --> REQ["Client Request<br/>(POST /chat/start)"]
PD["Provider Config"] --> REQ
REQ --> MR["Server Merge"]
SD["Server Defaults"] --> MR
SC["Server Constraints"] --> MR
MR --> RC["Resolved Config"]
MR --> WN["Warnings<br/>(if clamped)"]
RC --> SO["SDK Options"]| Field | Server | Client | Merge Rule |
|---|---|---|---|
| model | cap via maxModel | request | clamped to ceiling |
| tools | superset | narrow | intersection (client can only reduce) |
| disallowedTools | block | block | union (always honored) |
| systemPrompt | base | append | concatenated |
| maxTurns | cap | request | min() |
| agents | define | select subset | server validates |
| plugins, cwd, permissionMode, mcpServers | set | — | server-only (never sent to client) |
Models
Three model tiers are supported, ordered cheapest to most capable:
createAgentServer({
config: {
model: 'claude-sonnet-4-6', // Default model for all sessions
},
constraints: {
maxModel: 'claude-sonnet-4-6', // Cap: clients can't request opus
// OR use an explicit allowlist:
allowedModels: ['claude-haiku-3-5', 'claude-sonnet-4-6'],
},
});| Model | Tier | Best For |
|---|---|---|
| claude-haiku-3-5 | Fast | Quick tasks, low cost |
| claude-sonnet-4-6 | Balanced | General use (default) |
| claude-opus-4-6 | Capable | Complex reasoning |
Clients request a model via POST /chat/start:
{ "config": { "model": "claude-opus-4-6" } }If the requested model exceeds maxModel, the server clamps it down and returns a warning.
Tools & Permissions
The server defines the superset of tools. Clients can narrow but never expand.
createAgentServer({
config: {
tools: ['Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep'],
disallowedTools: ['Write'], // Block Write for all sessions
permissionMode: 'bypassPermissions', // or 'default' for interactive approval
},
constraints: {
disallowedTools: ['Bash(rm -rf *)'], // Hard block at constraint level
},
});| permissionMode | Behavior |
|---|---|
| 'bypassPermissions' | All tool calls auto-approved (default, best for server use) |
| 'default' | Requires interactive approval (typically for local dev) |
Client narrowing example — request only read tools:
{ "config": { "tools": ["Read", "Glob", "Grep"] } }System Prompts
Server and client system prompts are concatenated (server first):
createAgentServer({
config: {
systemPrompt: 'You are a code review assistant. Focus on security issues.',
},
});Client appends context:
{ "config": { "systemPrompt": "The user is working on a React project." } }Resolved prompt: "You are a code review assistant. Focus on security issues.\n\nThe user is working on a React project."
Plugins
Plugins extend Claude Code with custom skills. Server-only — clients cannot inject plugins.
createAgentServer({
config: {
plugins: [
{ type: 'local', path: './my-plugin' }, // Local plugin directory
{ type: 'local', path: '/absolute/path/plugin' }, // Absolute path
],
},
});A plugin directory should contain a SKILL.md file with the skill definition. See the Claude Code plugin docs for the full format.
MCP Servers
Connect Claude Code to external tool servers via the Model Context Protocol. Server-only — clients cannot add MCP servers.
createAgentServer({
config: {
mcpServers: {
'my-database': {
command: 'npx',
args: ['-y', '@my-org/db-mcp-server'],
env: { DATABASE_URL: 'postgres://...' },
},
'file-search': {
command: 'node',
args: ['./mcp-servers/search.js'],
},
'remote-api': {
url: 'https://mcp.example.com/sse', // SSE-based remote MCP
},
},
},
});Each MCP server definition follows the Claude Code format:
| Field | Type | Description |
|---|---|---|
| command | string | CLI binary to spawn |
| args | string[] | Arguments for the command |
| env | object | Environment variables |
| url | string | URL for remote (SSE) MCP servers |
Agents (Subagents)
Define specialized subagents the server exposes. Clients can select from the set but cannot define new ones.
createAgentServer({
config: {
agents: {
reviewer: {
description: 'Reviews code for bugs and security issues',
prompt: 'You are a senior code reviewer...',
tools: ['Read', 'Grep', 'Glob'],
},
writer: {
description: 'Writes new code and tests',
prompt: 'You are a senior engineer...',
tools: ['Read', 'Write', 'Edit', 'Bash(*)'],
},
},
},
});Client selects agents by name:
{ "config": { "agents": ["reviewer"] } }Working Directory & Settings
createAgentServer({
config: {
cwd: '/path/to/project', // Working directory for Claude Code
settingSources: ['user', 'project'], // Where to read .claude settings from
maxTurns: 50, // Max agentic turns per message
includeToolResults: true, // Stream tool output to clients
},
constraints: {
maxTurns: 100, // Hard cap even if client requests more
},
});Hooks (Server Events)
Monitor and react to session lifecycle events:
createAgentServer({
hooks: {
onSessionStart: ({ sessionId, config }) => {
console.log(`Session ${sessionId} started with model ${config.model}`);
},
onSessionEnd: ({ sessionId }) => {
console.log(`Session ${sessionId} ended`);
},
onMessage: (envelope) => {
// Every protocol message (stream, assistant, tool-use, etc.)
},
onToolUse: (payload, sessionId) => {
console.log(`Tool: ${payload.toolName} in session ${sessionId}`);
// payload: { toolName, toolId, input }
},
onError: (err) => {
console.error('Agent error:', err.message);
},
onClientConnect: ({ clientId, transport }) => {
console.log(`Client ${clientId} connected via ${transport}`);
},
onClientDisconnect: ({ clientId }) => {
console.log(`Client ${clientId} disconnected`);
},
},
});Full Configuration Example
A production server with all options:
const agent = createAgentServer({
config: {
model: 'claude-sonnet-4-6',
tools: ['Read', 'Write', 'Edit', 'Bash(*)', 'Glob', 'Grep'],
disallowedTools: ['Bash(rm -rf *)'],
permissionMode: 'bypassPermissions',
systemPrompt: 'You are a helpful coding assistant for an e-commerce platform.',
cwd: '/app/workspace',
maxTurns: 50,
includeToolResults: true,
settingSources: ['user', 'project'],
plugins: [
{ type: 'local', path: './plugins/db-helper' },
],
mcpServers: {
postgres: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-postgres'],
env: { DATABASE_URL: process.env.DATABASE_URL },
},
},
agents: {
reviewer: {
description: 'Code reviewer',
prompt: 'Review code for bugs, security, and performance.',
tools: ['Read', 'Grep'],
},
},
},
constraints: {
maxModel: 'claude-sonnet-4-6',
disallowedTools: [],
maxTurns: 100,
},
hooks: {
onSessionStart: ({ sessionId, config }) => {
console.log(`[${new Date().toISOString()}] Session: ${sessionId} (${config.model})`);
},
onToolUse: (payload, sessionId) => {
console.log(`[tool] ${payload.toolName} in ${sessionId}`);
},
onError: (err) => {
console.error('[error]', err.message);
},
},
});
agent.listen(3456);Protocol
All messages use a standard JSON envelope:
{
"v": 1,
"type": "chat:stream",
"payload": { "delta": "..." },
"source": "server",
"timestamp": 1710000000,
"sessionId": "uuid"
}Message Types
| Category | Types | Direction |
|---|---|---|
| Chat | stream, assistant, tool-use, tool-result, status, error, user | Server → Client |
| Chat | send, stop | Client → Server |
| Session | created, resumed, list, closed | Server → Client |
| Config | request, resolved | Bidirectional |
| System | connect, disconnect, heartbeat | Bidirectional |
Transport
- WebSocket (primary): bidirectional, real-time streaming. Client sends
sys:connecthandshake, server acknowledges withclientId. - SSE (fallback): server-push only. Client sends messages via REST POST. Auto-fallback if WS fails.
- REST: stateless endpoints for session management and message sending. Always available.
Modules
| Import | Contents |
|---|---|
| @shaykec/agent-web/server | createAgentServer(), SessionManager, ConfigResolver, Transport |
| @shaykec/agent-web/react | ClaudeProvider, useChat, useSessions, useConnection |
| @shaykec/agent-web/components | Drop-in: ClaudeChat, ClaudeMessage, ClaudeToolUse. Composable: ChatMessages, ChatInput, ChatStatus, SessionPicker. Theme: THEME_PRESETS |
| @shaykec/agent-web/client | ClaudeClient (vanilla JS, framework-agnostic) |
| @shaykec/agent-web/protocol | Message types, createEnvelope, parseEnvelope, config schema |
Examples
Seven reference implementations — each serves as documentation, test target, and starting point:
| Example | Port | Use Case | Run |
|---|---|---|---|
| minimal-chat | 3456 | Quickstart — simplest possible demo | node examples/minimal-chat/server.js |
| react-embeddable | 4010 | Drop-in <ClaudeChat /> widget | node examples/react-embeddable/server.js |
| react-custom-hooks | 4011 | Custom UI with hooks | node examples/react-custom-hooks/server.js |
| vanilla-js | 4012 | Framework-agnostic client | node examples/vanilla-js/server.js |
| express-middleware | 4014 | Express app.use() integration | node examples/express-middleware/server.js |
| multi-session | 4015 | Config negotiation + concurrent sessions | node examples/multi-session/server.js |
| macos-app | 4020 | Native macOS app (embedded server) | cd examples/macos-app/AgentChat && bash build-app.sh && open .build/AgentChat.app |
Quickstart
npm install @shaykec/agent-web @anthropic-ai/claude-agent-sdkCreate server.js:
import { createAgentServer } from '@shaykec/agent-web/server';
const agent = createAgentServer({
config: {
model: 'claude-sonnet-4-6',
permissionMode: 'bypassPermissions',
systemPrompt: 'You are a helpful coding assistant.',
},
});
agent.listen(3456);
console.log('Server running at http://localhost:3456');node server.jsOr clone the repo for full examples:
git clone https://github.com/shayke-cohen/local-agent-web.git
cd local-agent-web && npm install
node examples/minimal-chat/server.js
# Open http://localhost:3456Testing
340+ total tests across Node.js, Swift, and Argus YAML:
| Tier | Command | Tests | What It Tests |
|---|---|---|---|
| Unit | npm run test:unit | ~220 | Protocol, server logic, config (MCPs, plugins, agents, permissions), client hooks, composable components, vanilla client |
| Integration | npm run test:integration | ~50 | Real HTTP server, WebSocket handshake, config negotiation, macOS server |
| E2E (SDK) | npm run test:e2e | 5 | Real Claude Agent SDK via local CLI (no API key needed) |
| E2E (Web) | npm run test:e2e | 7 | Web app HTML, health, config endpoint, sessions, WebSocket handshake |
| E2E (macOS) | npm run test:e2e:macos | 4 | Build macOS app, server health, session creation |
| Argus (Web UI) | tests/argus/web-quickstart.yaml | 4 | Browser: Connected status, Send button, input field |
| Argus (Web API) | tests/argus/web-api.yaml | 17 | REST: health, config, sessions, validation, 404 |
| Argus (macOS UI) | tests/argus/macos-app.yaml | 4 | Native: title, connected status, empty state |
| Argus (macOS API) | tests/argus/macos-api.yaml | 8 | Embedded server: health, sessions |
| Swift | swift test (in examples/macos-app/AgentChat) | 49 | Models, settings, view model, server process |
npm test # All Node.js tests (295)
npm run test:e2e # Real Claude Code CLI + web app
npm run test:coverage # Coverage report
# Swift tests
cd examples/macos-app/AgentChat && swift test # 49 tests
# Argus regression tests (requires Argus MCP + running apps)
# Web: node examples/minimal-chat/server.js, then:
argus test tests/argus/web-quickstart.yaml
argus test tests/argus/web-api.yaml
# macOS: swift run in examples/macos-app/AgentChat, then:
argus test tests/argus/macos-app.yaml
argus test tests/argus/macos-api.yamlRequirements
- Node.js >= 18
@anthropic-ai/claude-agent-sdk(peer dependency, for server)- React >= 18 (peer dependency, for hooks/components — optional)
- Claude Code CLI (for E2E tests — optional)
Architecture
See docs/architecture.md for detailed system diagrams, data flows, session lifecycle, and security model.
AI Agent Skill
An integration skill is available for AI coding agents (Cursor, Claude Code) to help add @shaykec/agent-web to new projects:
.cursor/skills/integrate-agent-web/SKILL.mdThe skill walks through all 7 integration paths step-by-step — from embeddable components to native desktop apps — with complete code examples, config deep dives, and a verification checklist. When using Cursor or Claude Code in a project that depends on @shaykec/agent-web, the agent can read this skill to produce correct integration code.
