Skip to content

feat(freshclaude): freshclaude and kilroy panes get the Freshell MCP tools, following the Agent MCP setting - #873

Merged
danshapiro merged 21 commits into
mainfrom
feat/freshclaude-freshell-mcp
Oct 23, 2026
Merged

danshapiro merged 21 commits into
mainfrom
feat/freshclaude-freshell-mcp

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

What users see

Freshclaude and Kilroy panes now get the Freshell MCP tools, the same tools a Claude terminal pane already has. The agent in the pane can list tabs, split its own pane, send keys and so on. Calls that name no target act on the agent's own pane, not on the pane the user happens to be looking at.

The tools follow Settings → Advanced → "Agent MCP server". The setting is read each time an agent starts, so changing it applies to the next pane you open or resume, with no server restart. Terminal panes do not follow the switch yet (#446, out of scope here).

What changed

  1. One launch builder carries the MCP recipe and the pane. ClaudeLaunchSpec (claude_launch.rs) gained two optional fields: freshellMcp (the command and arguments of the Freshell MCP server) and freshellContext (the tab and pane the session runs for). Every way an agent starts goes through it: create, attach-resume, handoff resume, undo/redo and fork. An undo replacement or a fork child keeps its parent's pane.
  2. The setting is read live. main.rs installs a provider that reads settings.codingCli.mcpServer at each sidecar start (new module crates/freshell-server/src/claude_freshell_mcp.rs). When the setting is on but the server command cannot be found, the pane starts without the tools and a warning is logged.
  3. The token never goes on a command line. The sidecar always gets FRESHELL=1, FRESHELL_URL and FRESHELL_TOKEN in its environment, built from the server's own port and token. The MCP recipe holds only a command and arguments, never an env key, so the token never reaches the CLI's argv. Pane ids inherited from elsewhere are removed; only the pane's own ids are set.
  4. Hosted and ordinary recipes never mix. The new sidecar module mcp-servers.mjs (it replaces hosted-mcp.mjs) gives a hosted session the session host's fixed recipe and an ordinary session the recipe Rust resolved. A session never gets both. Hosted Kilroy now gets the hosted recipe too; before this, the sidecar refused it.
  5. Create and attach name their pane. freshAgent.create and freshAgent.attach carry tabId/paneId (protocol and client).
  6. A handoff resumes for the pane that adopts the session. Switching a Claude terminal pane to Freshclaude/Kilroy, or stop-and-reopen, passes the pane's ids through to the resumed CLI. Without this, an untargeted MCP call from such a pane would have acted on the user's active pane.
  7. Logs. freshagent.claude.mcp_injection (info) is logged at each launch, and freshagent.claude.mcp_unavailable (warn) when the command cannot be resolved. In the sidecar, mcp.freshell_recipe_rejected (warn) is logged when a recipe arrives malformed and is left out.
  8. Test tools. The CLI simulator now starts stdio MCP servers the way the pinned CLI does (conformance scenario mcp). It hands them only the SDK's environment, and a program's text step can repeat the last tool result. The Rust claude rig gained shared helpers for reading query() options, finishing turns on disk, and killing a sidecar.
  9. README describes the tools and the switch.

Tests (all on the final commit)

  • Vitest: pnpm run test:vitest run test/unit/claude-sidecar/ test/unit/shared/ws-protocol.fresh-agent-pane-context.test.ts test/unit/client/components/fresh-agent/FreshAgentView.test.tsx test/integration/providers/claude-cli-sim.test.ts test/integration/providers/claude-freshell-mcp.test.ts test/integration/providers/claude-cli-contract.test.ts test/unit/mcp/freshell-tool.test.ts --config config/vitest/vitest.config.ts: 26 files, 931 passed. This includes the pinned-CLI contract test, which reads /proc/<pid>/cmdline to confirm the token is not in the CLI's argv.
  • Rust, every claude integration file in freshell-ws (15 files): 100 passed. This includes freshagent_claude_mcp_launch (8: create for both flavours, the setting changing between launches, the hosted lane, attach-resume, handoff resume for freshclaude and for kilroy, undo, fork). Each launch-test binary also ran 4 times in parallel: all green.
  • cargo test -p freshell-freshagent --lib claude: 466 passed. --lib handoff: 103 passed. cargo test -p freshell-server --bin freshell-server claude_freshell_mcp: 2 passed.
  • Cloud e2e (GCLOUD_ROBOT_REQUIRE=1 pnpm run test:e2e <specs> --project=chromium): 33 passed, 0 flaky, 0 failed, on the final commit ab3cd3ba4.
    • The new spec freshclaude-freshell-mcp-rust.spec.ts covers three cases. First, for freshclaude and for kilroy, the agent lists tabs through the real MCP server, and its own tab is in the list. Second, while the user is on another tab, an untargeted split-pane splits the agent's own pane and leaves the active tab alone. Third, with the setting turned off, the same call answers "No such tool available".
    • The four other specs that drive the simulator were rerun, because every freshclaude session in them now starts a real MCP child: fresh-agent-error-turn-attention, fresh-agent-rollback-rust, freshclaude-live-transcript-rust and freshclaude-fork-rust.
  • cargo fmt --check, clippy on the touched crates, and pnpm run typecheck are clean.

Each fix was written test-first, and each test was seen failing before its fix. For the e2e default-target check, I also made the MCP tool ignore the pane context on purpose and confirmed the spec fails ("the agent's own tab gains the new pane": expected 2, received 1).

Follow-ups

  1. Terminal panes do not follow the "Agent MCP server" switch yet (Agent MCP server toggle is saved but not honored by agent launches #446).
  2. The browser sends its layout to the server after a debounce that restarts on every layout change. During the first second or so after the page loads, the agent API may therefore not list the browser's tabs yet. The e2e now waits for that sync. It only shows up when an agent calls the API within a second of page load.
  3. test/unit/port/ws-contract-freeze.test.ts has 2 failures on main as well: the frozen server-message schema is missing modelEffortLevels. It is not touched here.
  4. Three freshell-platform provider_environment tests count a node_modules directory found above the test's temp directory. A stray /tmp/node_modules/ws on the dev host made them fail in the pre-push gate. Not from this branch: the push ran with TMPDIR set to a clean directory, and they pass there. The tests should not depend on what sits above their temp directory.
  5. managed_provider_sdk_tests.rs (feature managed-provider-qualification, needs an owned sandbox) was updated to import mcp-servers.mjs but was not run.

🤖 Generated with Claude Code

danshapiro and others added 21 commits October 23, 2026 08:42
…d agents

Both build_terminal_base_env copies (freshell-ws terminal.rs and
freshell-freshagent terminal_tabs.rs) now delegate to
freshell_platform::freshell_context_env, which also serves the
freshclaude/kilroy sidecar next. Pinned by the existing base_env tests
and the terminal_tabs env tests; the js_number_string test moves with
the function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he fixed MCP recipe

The session host already selects the hosted Freshell MCP for kilroy lanes
(kilroy consumes the Claude launch recipes), but the sidecar offered the
fixed recipe and the managed exec check to hosted claude only, so a hosted
kilroy pane had no Freshell MCP at all and spawned its CLI unchecked.
Both rules now apply to every hosted Claude runtime (api-hosted-ops plan
§2.4 open decision).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the pane context

mcp-servers.mjs (absorbing hosted-mcp.mjs) picks the session's one
Freshell MCP recipe: the hosted fixed command when a hosted lane selected
it, otherwise the command the server resolved (create.freshellMcp); a
hosted sidecar never gets the ordinary one, and no recipe ever carries an
env key (the SDK puts the MCP config on the CLI's argv). The create's
freshellContext ({tabId, paneId}) is merged into the SDK env, replacing
any inherited pane-scoped names. The simulator ledger now records the
Freshell context values (and a digest of the token) so tests can see what
the CLI environment carried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FreshAgentCreate gains an optional paneId (tabId already existed) and
FreshAgentAttach optional tabId/paneId, in the zod schema, the frozen
contract and the Rust structs. Absent fields stay absent on re-serialize
and older frames still parse. The pane context is what the agent's
Freshell MCP will act for (G19). Existing struct literals get the new
fields as None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dpoint and its pane

A claude lane can now be wired with the server's endpoint and a Freshell
MCP recipe provider (set-once, like the identity sink). Every sidecar's
environment carries FRESHELL=1, FRESHELL_URL and FRESHELL_TOKEN from that
endpoint and none of another pane's ids; the provider is asked at each
launch in boot_sidecar, the one spawn path, so the recipe reaches the
create, attach-resume, handoff resume, undo/redo respawn and fork alike,
and a setting change applies to the next launch. A hosted lane never gets
the ordinary recipe. ClaudeLaunchSpec carries freshellMcp and
freshellContext; the pane comes from the create's or attach's tabId and
paneId, and an undo replacement or fork child inherits its session's.
Each launch logs freshagent.claude.mcp_injection, and an unresolvable
command logs freshagent.claude.mcp_unavailable (warn) and starts the pane
without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive setting

main.rs hands the claude lane this server's own endpoint (its port and
auth token) and a recipe provider that reads settings.codingCli.mcpServer
from the live settings tree at every sidecar launch and resolves the same
Freshell MCP command the terminal lanes use, rendered for this host's CLI.
ServerSettings::agent_mcp_server_enabled is the one gate the terminal
lanes will read next (#446). A PATCH of the setting turns the recipe off
and back on with no restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
freshAgent.create now carries the pane's paneId beside its tabId, and
freshAgent.attach carries tabId and paneId, so a freshclaude/kilroy
sidecar launched or resumed for the pane runs its Freshell MCP for that
pane (G19). The two tests that pin an attach frame exactly now expect the
pane fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…act on the pinned CLI

The real sidecar over the pinned SDK 0.3.237 / CLI 2.1.237 and a local
provider, launched the way the Rust server launches a freshclaude pane:
the CLI's argv carries the MCP config but never the token, the stdio MCP
child receives FRESHELL, FRESHELL_URL, the token and the pane context
(and no terminal id), the server connects (its tool is listed) and a
model call of its tool returns what it saw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulator now starts each stdio MCP server in options.mcpServers with
the SDK env (not the simulator's own environment), reports it connected
or failed, lists its tools in init once a turn has waited for the servers
to settle, calls a listed tool over tools/call (heartbeats while it runs),
and answers a call of an MCP tool no server lists with the CLI's
"No such tool available" error before the message's trailing stream.

A new conformance scenario (mcp, marker MCP:call=<tool>) and the G19 fact
test pin this on the pinned CLI 2.1.237: the server gets the CLI's
environment, its tool is listed and called, and an unknown MCP tool is an
error result. The full contract and simulator files stay green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…result

A text emission with echoToolResult repeats what the turn's last tool call
returned (after an optional text prefix), so a scripted model can show
what an MCP tool answered. Pinned by a simulator test that calls a probe
stdio MCP server through a program's tool step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings toggle removes it

Through the real Rust server, the production sidecar and the CLI
simulator: the agent's list-tabs call through the Freshell MCP server the
server resolved lists the open tabs (its pane context and recipe recorded
without the token), for freshclaude and kilroy; with Settings > Advanced >
Agent MCP server turned off, a new pane's agent has no Freshell MCP and the
same call is "No such tool available".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the owner record

Under parallel load a fork could read the transcript before the CLI
flushed the turn ("nothing to fork yet"), and the owner record can trail
the first result. The tests now wait for both, the way the fork suite
does; 48 parallel runs stay green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for the tool result

The CLI flushes the tool_result line about 100 ms after the turn's frames,
so reading the transcript right after the result could miss it under load
(1 of 16 parallel runs). 18 parallel runs now pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g the sidecar

No sidecar process waits on the settings read and command resolution.
Pinned by the MCP launch and launch parity integration tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e browser tab id

The agent API's tab list holds the tabs the server knows (here the tab the
test created through REST); the browser's boot tab id is not part of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pts the session

Switching a terminal Claude pane to freshclaude/kilroy, or stop-and-reopen,
posts the handoff with the pane's tabId/paneId. The claude lane dropped them,
so the resumed CLI and its Freshell MCP had no FRESHELL_TAB_ID/PANE_ID and an
untargeted MCP call fell back to the user's active pane. The handoff now
threads the ids into the resume's launch spec, for both flavors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ropped silently

The server logs each launch's recipe as resolved before the sidecar checks
it, so a recipe the sidecar could not use (a non-string argument, an empty
command) left the pane without the Freshell MCP and no log said so. The
sidecar now logs a warn record, mcp.freshell_recipe_rejected, with the
session id, and still starts the session without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idecar-kill helpers

The MCP launch test and the G15 launch-parity test each carried copies of
query_options, wait_for_query, kill_the_sidecar and converse. They now live
on ClaudeRig. The shared converse waits for each turn to reach the
transcript before the next, so the parity test's resumes and undo no longer
race the CLI's transcript flush either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plits its own pane

The own-tab check was dropped after a cloud run where the server's tab list
lacked the browser's tab. The cause is timing, not the MCP wiring: the
browser mirrors its layout to the server after a debounce that every layout
change restarts, and the simulated turn ran within that window, so the
server knew only the tab the test created through REST. The spec now waits
until the server knows the browser's tabs, then checks that the agent's
list-tabs reply includes its own tab.

It also covers the default target: while the model thinks, the user
switches to another tab, and the agent's untargeted split-pane must split
its own pane in its own tab and leave the active tab alone. Ignoring the
pane context in the MCP tool's default target makes this fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danshapiro
danshapiro merged commit 913904a into main Oct 23, 2026
7 checks passed
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