Files
ottyl/tickets/109-mcp-not-tied-to-a-single-session-at-startup.md
T

73 lines
3.6 KiB
Markdown

# 109 — Stop binding `ottyl mcp` to one session for its whole lifetime
## Summary
`ottyl mcp --session <name>` resolves the session name to a port
once, at process startup, and then serves stdio JSON-RPC for the
life of the process against that fixed URL. In practice this means
every ottyl session an agent wants to talk to needs its own
registered MCP server entry (e.g. `ottyl-hygienic-gyrfalcon`), and
switching which running `ottyl` instance a client is looking at
requires killing that entry and re-registering a new one — which,
for MCP clients that only read server config at startup, means the
whole host process needs restarting before the new tools are even
callable. This got hit directly: an agent needed to switch from an
old dead session (`enormous-warthog`) to a new one
(`hygienic-gyrfalcon`) mid-conversation and had no way to do it
without editing MCP config and waiting for a restart.
## Where
- `src/cli/mod.rs:43-49` — `Command::Mcp { session: Option<String>,
port: Option<u16> }`, the CLI surface.
- `src/cli/mod.rs:171-195` (`run_mcp`) — resolves `session` to a port
via `sessions::resolve_session` exactly once, builds one
`OttylMcp::new(base_url)`, then blocks forever on
`service.waiting()`. There is no re-resolution after startup.
- `src/api/mcp_server.rs:254-282` — `OttylMcp` stores `base_url:
String` as a plain field set once in `new()`; every `#[tool]`
method builds request URLs via `self.url(path)`
(`mcp_server.rs:282-284`), so the fixed base URL is threaded
through every single tool call.
- `src/cli/sessions.rs` — `SessionInfo { name, port, pid }` and
`resolve_session`/`list_sessions`, the registry this would need to
re-query if the bound session can change after startup.
## Direction
Two ways to remove the "pick a session forever at startup" behavior;
pick one during implementation rather than building both:
1. **Request/response CLI, no long-running process.** Add a CLI
subcommand (e.g. `ottyl call <session> <tool> <json-args>`) that
resolves the session, makes the one HTTP call, prints the result,
and exits — no `rmcp`/stdio JSON-RPC server at all. Simplest fix
for the "MCP config never changes, session does" mismatch, but
loses whatever the MCP client gives you over a bare CLI call
(tool discovery/schema, a persistent connection) — check whether
that matters to how this is actually invoked before going this
route.
2. **Long-running MCP server, session switchable at runtime.** Keep
`ottyl mcp` as a stdio server, but stop fixing `base_url` at
construction: either (a) accept a `session` (or `port`) argument
on every tool call, resolved against `sessions::resolve_session`
per-call instead of once in `run_mcp`, with the constructor's
`--session`/`--port` becoming just a default; or (b) add an
explicit `switch_session` tool that mutates the stored target
(`base_url` would need to move from a plain field to something
mutable, e.g. `Mutex<String>` or resolved fresh from the registry
on every call from a stored session name instead of a
pre-resolved URL).
## Scope
- Land one of the two directions above for `ottyl mcp` /
`src/api/mcp_server.rs`; out of scope to build both.
- If going with direction 2, update `SKILL.md`'s "Connect an MCP
client" section, since it currently documents the
resolve-once-at-startup model.
- Out of scope: changing anything about the underlying `/api/...`
HTTP routes themselves, or the session registry format in
`src/cli/sessions.rs` — this is purely about how the MCP-facing
process picks and holds (or doesn't hold) its target session.