feat: chat with local Claude Code via MCP endpoint#557
Draft
tkuhn wants to merge 5 commits into
Draft
Conversation
First vertical slice of docs/claude-code-chat.md: /claudechat page backed by a persistent headless Claude Code subprocess (stream-json over stdin/stdout, plan-login preserved by scrubbing API-key env vars), and a hand-rolled MCP endpoint at /mcp (JSON-RPC 2.0, startup-random bearer token) exposing five read-only tools: search_nanopubs, get_nanopub, list_templates, get_template, run_query. Off by default; enable with claudeChatEnabled / NANODASH_CLAUDE_CHAT_ENABLED=true on local instances. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assistant messages render as sanitized markdown (commonmark + Utils.sanitizeHtml). New MCP tools: prepare_publication returns a /publish path with param_ prefills (user still reviews, signs, and publishes), and open_page queues an in-app navigation that the chat panel's poll executes in the browser; the calling session is identified by an X-Nanodash-Chat-Session header in the subprocess's --mcp-config. The subprocess now runs from ~/.nanodash/claude-chat instead of inheriting the server's working directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the chat UI into ClaudeChatPanel and render it on every page from the NanodashPage base markup: a corner button when collapsed, a docked panel when expanded (state in NanodashSession). Since the conversation lives server-side, it stays visible across open_page navigations. The poll now only re-renders the chat box when messages or the busy state changed, and auto-scrolls to the bottom. /claudechat reuses the same panel as its full-page body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Curated nanopub background (resource background.md) passed to the Claude Code subprocess via --append-system-prompt, incl. pinned meta-templates for creating new templates, queries, and views - Each chat message carries the user's current in-app page as invisible context, so "this page"/"this nanopub" references resolve - get_template and prepare_publication resolve template URIs to their latest version (supersedes chain + space-governed resolution); new get_latest_version tool for arbitrary nanopub URIs - WebFetch added to the subprocess tool allowlist - Stop button while Claude is working, sending an interrupt over the stream-json control protocol; turn ends with an "(interrupted)" marker and the session stays usable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New mcpRemoteEnabled pref (NANODASH_MCP_REMOTE_ENABLED), independent of claudeChatEnabled: hosted instances can open /mcp to users' own AI agents (local Claude Code, Claude API mcp_servers, any bearer-token MCP client). - ApiTokenService: personal API tokens (ndmcp_ + 64 hex), shown once, stored SHA-256-hashed in ~/.nanopub/nanodash-api-tokens.yml; unit-tested. - McpServlet: dual bearer auth resolving user tokens to the owner's IRI (401 + WWW-Authenticate), protocol version negotiation, instructions field serving the domain background, real serverInfo version, 405 on GET/DELETE. - McpCaller record threads the caller through the tools; open_page routes remote callers to per-user navigation queues (RemoteAgentService) that logged-in pages poll only while the user's agent was recently active. - ApiTokenPanel on the own About tab: create/revoke tokens with one-time reveal and ready-to-paste client setup (Ajax-only: a full-page render can hit the auto-refresh redirect and lose the one-time token). - background.md split into client-neutral core (served as MCP instructions) and local-chat addendum. - prepare_publication accepts array param values to prefill the rows of repeatable template statements (param_x, param_x__1, ...), documented in background.md; fixes agents claiming repeated rows must be added by hand. - docs: new docs/remote-mcp.md; claude-code-chat.md updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
First slices of the design in docs/claude-code-chat.md (the "Tier 2" option of #434): a chat page backed by the user's local Claude Code installation, which can act on Nanodash through an MCP endpoint. Uses the user's existing Claude plan login — no API keys, no operator cost. Off by default; intended for locally running instances only.
What's included
ClaudeChatPanel, rendered from theNanodashPagebase markup: a corner 💬 button when collapsed, a docked panel when expanded (state inNanodashSession). The conversation lives server-side, so it stays open acrossopen_pagenavigations — Claude can show you a nanopub while the chat remains in the corner. Per-kind message styling (user / assistant / tool activity / error); assistant messages render as markdown (commonmark), sanitized with the existing OWASP policy. The 2s poll only re-renders the chat box when messages or the busy state changed./claudechatoffers the same panel as a full page.ClaudeChatService/ClaudeSession— one persistent headlessclaude --print --input-format stream-json --output-format stream-jsonsubprocess per Wicket session;ANTHROPIC_API_KEY/AUTH_TOKEN/BASE_URLscrubbed from the child env so usage stays on the plan login; 10-min idle reaping; working directory pinned to~/.nanodash/claude-chatso the CLI doesn't pick up the server directory's CLAUDE.md./mcpendpoint — hand-rolled MCP server (JSON-RPC 2.0 over POST, stateless streamable-HTTP JSON mode), guarded by a startup-random bearer token injected into the subprocess's--mcp-config. Hand-rolled rather than the official MCP Java SDK because the SDK's servlet transport targets Servlet 6.0 (dev jetty is 11 / Servlet 5.0).search_nanopubs,get_nanopub,get_latest_version,list_templates,get_template,run_query. Template URIs passed toget_template/prepare_publicationare resolved to the latest version server-side (supersedes chains + space-governed resolution), andget_latest_versioncovers arbitrary nanopub URIs. The subprocess runs with--allowedTools mcp__nanodash* WebFetchonly.prepare_publicationbuilds a/publish?template=...¶m_...path with prefilled placeholder values (nothing is signed or published server-side; the user reviews and publishes through the normal flow), andopen_pagequeues an in-app navigation that the chat panel executes in the user's browser; the calling session is identified via anX-Nanodash-Chat-Sessionheader, and paths are validated to be in-app only.chat/background.md, passed via--append-system-prompt) teaches Claude nanopub anatomy, version semantics, and pins the meta-templates to use when creating new templates, queries, or views; each chat message invisibly carries the in-app path the user is on, so “this page” / “this nanopub” references resolve without asking.interruptover the stream-json control protocol, the turn ends with an(interrupted)marker and the session stays usable.claudeChatEnabled/claudeChatBinary/claudeChatModelprefs withNANODASH_CLAUDE_CHAT_*env overrides.Remote MCP access (per-user tokens) — docs/remote-mcp.md
The
/mcpendpoint can now also serve hosted multi-user instances, letting users point their own agents at it (bring-your-own-agent instead of a server-side subprocess):mcpRemoteEnabledpref (NANODASH_MCP_REMOTE_ENABLED), independent ofclaudeChatEnabled— either feature works without the other.ApiTokenService): created/revoked in a new "🔑 Remote AI access" box on the user's own About tab;ndmcp_-prefixed secrets shown once, stored SHA-256-hashed in~/.nanopub/nanodash-api-tokens.yml; unit-tested. The one-time reveal includes a ready-to-pasteclaude mcp add --transport http ...command.McpServlet: the existing startup-random chat token, or a user token resolved to the owner's IRI (401s carryWWW-Authenticate). Plus protocol polish: version negotiation (2024-11-05 → 2025-06-18), the domain background served as MCPinstructionsto every client, realserverInfo.version, explicit 405 on GET/DELETE.open_pagefor remote agents: aMcpCallerrecord routes navigations either to the chat-session queue (unchanged) or to per-user queues (RemoteAgentService); pages of the logged-in user poll every 3s only while their agent was active in the last 30 minutes (render-time gate, self-stopping) — no background polling for anyone else.instructions) + local-chat addendum (--append-system-prompt).prepare_publicationaccepts JSON-array param values and emits the publish form's indexed convention (param_x,param_x__1, ...), so agents can fully prefill repeatable template statements (e.g. the statement rows of the template meta-template) instead of telling the user to add them by hand.OAuth 2.1 (what claude.ai custom connectors need) is documented as a follow-up in docs/remote-mcp.md.
Try it
then open http://localhost:37373/claudechat (needs a logged-in
claudeCLI on the PATH). Try: "I want to publish a nanopub saying X relates to Y. Find a suitable template, prefill it, and open the form for me."Verified
End-to-end on an isolated instance, twice: (1) chat message → Claude called
search_nanopubsover MCP (401 without the token) → answer rendered in the panel; (2) "pick a template, prepare a publication, open it" → Claude chainedlist_templates→prepare_publication→open_pageand the browser landed on a fully prefilled publish form, with Publish untouched. Later slices verified the same way: page-context question answered without tool calls, superseded-URI resolution throughget_latest_version/get_template, WebFetch on an arbitrary domain, and mid-turn interrupt with the session staying usable. The remote slice was verified the same way: token creation/one-time-reveal/revoke in the UI, the curl protocol matrix (versions, instructions, 401/405/202),open_pagesteering a logged-in tab within 3s through the user-keyed queue, a realclaude mcp add --transport httpclient resolving a supersedes chain, array params filling both statement rows of the template meta-template (screenshot-checked) with the local docked chat unaffected.Not yet (follow-ups per the design doc)
--resumerevival of idle-reaped sessions🤖 Generated with Claude Code