feat(web): WebSocket channel with MCP tool proxy and structured annotations#13
Open
bobbymarko wants to merge 6 commits into
Open
feat(web): WebSocket channel with MCP tool proxy and structured annotations#13bobbymarko wants to merge 6 commits into
bobbymarko wants to merge 6 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ide context - Add invoke_tool WebSocket handler: proxies to any configured MCP server via stdio - Add mcp-client.ts: lightweight JSON-RPC 2.0 stdio client with pending-map concurrency - Add get_history handler: reads Claude Code session JSONL for chat history replay - Replace BRIEF_UPDATE with generic [[CHANNEL_EVENT:key:value]] extraction: strips markers from the visible stream, emits typed 'annotation' WS events - Move context injection out of the channel into the client (text field now carries full context) - Add web.senderName config field (defaults to 'Owner') - Add web.mcpServers config schema for declaring allowed MCP servers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All WS protocol fields use contextId (generic opaque thread ID) - history-reader strips context via [USER_INPUT] marker only — no app-specific prefix checks - history-reader strips [[CHANNEL_EVENT:...]] only — no legacy BRIEF_UPDATE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bobbymarko
marked this pull request as ready for review
May 28, 2026 18:56
…T annotations message-tool responseMode waits 3s for a tool-final reply that never arrives (Claude CLI just outputs text, no send_message tool), then returns early without calling renderResponseText on the completed snapshot. This left accumulatedText as the last streaming chunk value, which was unreliable for CHANNEL_EVENT extraction. capture-pane takes the paneManagedDelivery path which always calls renderResponseText with the final completed snapshot before returning, guaranteeing accumulatedText contains the full response including any [[CHANNEL_EVENT:brief:...]] markers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fences Claude Code renders markdown in the terminal, so tmux pane capture strips code fence language identifiers (e.g. \`\`\`a2ui:contact\`). Reading the final assistant message directly from the session JSONL (raw API response) ensures code fences reach the web client intact. Also add 'web' as a passthrough platform in renderPlatformInteraction so the web channel bypasses Slack/Telegram formatting entirely — the web client handles its own markdown rendering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
Author
|
@longbkit I realize this is a pretty obscure use case of wanting to interact with Clisbot via a web interface. But I have found it to be a powerful unlock for expanding beyond the boundaries of what messaging apps support. On top of this I built my own todo app with each todo item being able to spawn its own conversation. I also built support for A2UI so my responses can have interactive visual components instead of being text. But if this is too specific, maybe we could build a federated channel plugin framework so others can add their own channels without having to add them to the codebase? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this enables
Most agent interfaces are a single chat thread. The web channel opens up a richer pattern: browser UIs that wire multiple agent conversations to structured data, where each piece of data gets its own isolated AI session and the UI stays in control of state.
A concrete example of what you can build: a kanban-style task manager where each task card opens a dedicated agent conversation. The UI loads all tasks directly from a data source via
invoke_tool— no agent round-trip, just a fast MCP call over the persistent WebSocket. When the user taps a card and starts chatting, the agent gets the task context injected into the message. As the agent researches (finding a phone number, a price, a contact), it emits[[CHANNEL_EVENT:brief:...]]annotations that flow back to the card as a live summary — without cluttering the chat transcript. Each card's conversation is scoped bycontextId, so tasks never bleed into each other's history.The channel is intentionally minimal: it handles auth, session routing, streaming, and tool proxying. Everything else — what the UI looks like, what MCP servers are connected, what context gets injected into messages — is the client's concern.
Summary
Adds a new web channel — a WebSocket-based channel that connects any browser-side app to a clisbot agent. Includes a generic MCP tool proxy so the UI can call tools directly, and a structured annotation mechanism so agents can push typed events to the client without leaking them into visible chat text.
What's included
New channel (
src/channels/web/)?key=query param)message— routes text to the configured agent;contextId(optional) scopes to a per-context conversation threadget_history— replays the agent's Claude Code session JSONL back to the client so the chat UI can restore history on reconnectinvoke_tool— proxies anytools/callto a configured MCP server over stdio; the client can call databases, APIs, or anything MCP-accessible without going through the agentmcp-client.ts— lightweight JSON-RPC 2.0 stdio MCP clientinitializehandshake, maps pending requests by IDcontent[0].textenvelope so callers get plain JSON backhistory-reader.ts— reads Claude Code session JSONL files for history replay[USER_INPUT]marker)[[CHANNEL_EVENT:...]]annotations from historical messagesGeneric
[[CHANNEL_EVENT:key:value]]annotations[[CHANNEL_EVENT:key:value]]anywhere in its reply{ type: "annotation", contextId, key, value }WS eventsConfig additions
bots.web.mcpServers— map of server name →{ command, args, env }spawn configs; only declared servers are accessible viainvoke_toolbots.web.senderName— display name for the human sender (default"Owner")WebSocket protocol
Client → server
{ "type": "message", "text": "...", "contextId": "optional-thread-id" } { "type": "get_history", "contextId": "..." } { "type": "invoke_tool", "server": "my-tools", "tool": "search", "args": { "q": "..." } }Server → client
{ "type": "ready" } { "type": "chunk", "id": "wc1", "text": "partial agent response..." } { "type": "update", "id": "wc1", "text": "revised agent response..." } { "type": "done" } { "type": "tool_result", "server": "my-tools", "tool": "search", "result": {...} } { "type": "annotation", "contextId": "...", "key": "my-key", "value": "..." } { "type": "history", "contextId": "...", "messages": [{"role":"user","text":"..."},...] } { "type": "error", "message": "..." }Example config
🤖 Generated with Claude Code