Skip to content

feat: chat with local Claude Code via MCP endpoint#557

Draft
tkuhn wants to merge 5 commits into
masterfrom
feat/claude-code-chat
Draft

feat: chat with local Claude Code via MCP endpoint#557
tkuhn wants to merge 5 commits into
masterfrom
feat/claude-code-chat

Conversation

@tkuhn

@tkuhn tkuhn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

  • Docked chat panel on every pageClaudeChatPanel, rendered from the NanodashPage base markup: a corner 💬 button when collapsed, a docked panel when expanded (state in NanodashSession). The conversation lives server-side, so it stays open across open_page navigations — 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. /claudechat offers the same panel as a full page.
  • ClaudeChatService / ClaudeSession — one persistent headless claude --print --input-format stream-json --output-format stream-json subprocess per Wicket session; ANTHROPIC_API_KEY/AUTH_TOKEN/BASE_URL scrubbed from the child env so usage stays on the plan login; 10-min idle reaping; working directory pinned to ~/.nanodash/claude-chat so the CLI doesn't pick up the server directory's CLAUDE.md.
  • /mcp endpoint — 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).
  • Read-only toolssearch_nanopubs, get_nanopub, get_latest_version, list_templates, get_template, run_query. Template URIs passed to get_template/prepare_publication are resolved to the latest version server-side (supersedes chains + space-governed resolution), and get_latest_version covers arbitrary nanopub URIs. The subprocess runs with --allowedTools mcp__nanodash* WebFetch only.
  • Action toolsprepare_publication builds a /publish?template=...&param_... path with prefilled placeholder values (nothing is signed or published server-side; the user reviews and publishes through the normal flow), and open_page queues an in-app navigation that the chat panel executes in the user's browser; the calling session is identified via an X-Nanodash-Chat-Session header, and paths are validated to be in-app only.
  • Domain background + page context — a curated background prompt (classpath resource 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.
  • Stop button — visible while Claude is working; sends an interrupt over the stream-json control protocol, the turn ends with an (interrupted) marker and the session stays usable.
  • ConfigclaudeChatEnabled / claudeChatBinary / claudeChatModel prefs with NANODASH_CLAUDE_CHAT_* env overrides.

Remote MCP access (per-user tokens) — docs/remote-mcp.md

The /mcp endpoint 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):

  • mcpRemoteEnabled pref (NANODASH_MCP_REMOTE_ENABLED), independent of claudeChatEnabled — either feature works without the other.
  • Personal API tokens (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-paste claude mcp add --transport http ... command.
  • Dual auth in McpServlet: the existing startup-random chat token, or a user token resolved to the owner's IRI (401s carry WWW-Authenticate). Plus protocol polish: version negotiation (2024-11-05 → 2025-06-18), the domain background served as MCP instructions to every client, real serverInfo.version, explicit 405 on GET/DELETE.
  • open_page for remote agents: a McpCaller record 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.
  • background.md split into a client-neutral core (served as instructions) + local-chat addendum (--append-system-prompt).
  • Repeatable-row prefill: prepare_publication accepts 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

NANODASH_CLAUDE_CHAT_ENABLED=true ./mvnw jetty:run

then open http://localhost:37373/claudechat (needs a logged-in claude CLI 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_nanopubs over MCP (401 without the token) → answer rendered in the panel; (2) "pick a template, prepare a publication, open it" → Claude chained list_templatesprepare_publicationopen_page and 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 through get_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_page steering a logged-in tab within 3s through the user-keyed queue, a real claude mcp add --transport http client 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)

  • WebSocket/SSE streaming instead of polling
  • --resume revival of idle-reaped sessions

🤖 Generated with Claude Code

tkuhn and others added 5 commits July 14, 2026 16:12
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant