Skip to content

MCP over HTTP picks up source changes without a restart - #337

Merged
skavhaug merged 5 commits into
masterfrom
worktree-worktree-mcp-reload-sources
Oct 22, 2026
Merged

skavhaug merged 5 commits into
masterfrom
worktree-worktree-mcp-reload-sources

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 90: rasputin mcp --http recomputes its read roots (the folders a call may read besides the caller's own) when the sources file changes, checked at each call's start, so a new or removed source applies from the next call with no restart and download links stay valid. A broken file keeps the last good roots and logs one warning; a running call keeps its roots; tokens and links are never reloaded. The catalogue itself was already fresh per call (each call is a child process). 49 counted lines; tests U1-U8, S1-S2; full suite 9,573 passed. Review: design APPROVED WITH WORDINGS on 721dfbb, code APPROVED WITH WORDINGS on 38940a4 (@Reviewer), applied in 831d126.

🤖 Generated with Claude Code

skavhaug and others added 5 commits October 22, 2026 10:03
…tart (@architect)

Ola, 2026-10-22: "yes, make the server reload sources".

Finding: every tool call already runs a fresh child that reads the
catalogue, so the sources tool and sources named by id follow the file
over both transports. What is stale is HTTP's read roots, computed once
in build_app: a probe showed a source added after start refused when
read by path, and a removed one still readable.

Design: SharedRoots in tin_engine.mcp.http compares the content of the
user's sources file at each call's start (17 us on Ola's 8.6 KB file);
on a change it re-reads the roots (3.1 ms), under a lock, off the event
loop. A refused file or an overlap with --out-root keeps the last good
roots and logs one warning; a running call keeps its scope; links and
tokens are never reloaded. scope_for becomes async. Short path, about 40
counted lines. Tests U1-U7 (unit, no server) and S1-S2 (over HTTP).

Applied with the design: README "Serving over HTTP" paragraph replacing
"Changing the sources takes a restart", increment 69 section 5.2, and
the ROADMAP row (designed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tester)

New tests/python/test_mcp_reload_sources.py, the design's section 7:
U1 added source, U2 removed source, U3 broken file keeps the last good
roots and warns once naming the file, then heals; U4 a source inside the
output root is refused with "overlap"; U5 unchanged or only touched file
reloads nothing; U6 ten concurrent arrivals reload once; U7 a missing file
leaves the cache plus the plugins' local sources (computed from
plugin_source_files(), so it holds once a source plugin is installed);
S1 over HTTP, an added path runs the child, a removed one is refused
before it; S2 a link made before a reload still downloads.

Added beyond the list: U8, a reload cancelled while it reads the
catalogue is retried by the next call (the stamp is stored after the
load). It holds the load by patching tin_engine.mcp.http._shared_roots.

Red at this commit: U1-U8 (no SharedRoots) and S1 (b.tif refused,
"outside your folder"). S2 green, as the design says.

Checked against the design's fix applied in a scratch copy (SharedRoots,
async scope_for, three awaits): all 10 green five times in a row; the
whole Python suite 9554 passed, 0 failed; storing the stamp before the
load turns U8 red.

mcp_http_driver.py: rasputin_server's docstring now says RASPUTIN_DATA
holds for the server's life, since the app copies the environment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…(@developer)

SharedRoots (tin_engine.mcp.http) holds the read roots every user shares.
Each call's scope_for awaits current(): under one asyncio lock it reads the
sources-file candidates' bytes in a worker thread and compares them with the
stored stamp; on a change it reloads through the module-level _shared_roots
plus the --out-root overlap check, in a worker thread. A refused file or an
overlap logs one warning and keeps the last good roots; that stamp is stored
so the same content is judged once. The stamp of a good file is stored only
after the load, so a cancelled reload is retried (planted the early store:
the cancelled-reload test fails). build_app copies os.environ once. scope_for
becomes async in build_server; stdio's default is an async def.
HTTP_INSTRUCTIONS: a change to the sources applies from the next call.

Counted lines (tools/count_loc.py 94a157b): 49 net (http.py 47, server.py 2),
against the design's estimate of about 40, worst case 55; __all__ and the
sources import are split over lines by ruff format (14 of the 49 lines).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@skavhaug
skavhaug enabled auto-merge October 22, 2026 10:38
@skavhaug
skavhaug added this pull request to the merge queue Oct 22, 2026
Merged via the queue into master with commit ffbae90 Oct 22, 2026
15 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