3. Jeden sdílený MCP server na repozitář, peery a klienti, kteří jej spouštějí sami
Rozšiřuje ADR 1 a ADR 2.
Kontext
ADR 1 učinilo z majordomus mcp proces na klienta: jen stdio, žádný port, mrtvý s rourou. ADR 2 přidalo serve, druhý příkaz se stejným registrem na loopback socketu, včetně Swagger UI. Operátor pak požádal o čtyři věci v tomto pořadí: Swagger UI má startovat s majordomus mcp ve výchozím stavu a jeho URL má být v logu; MCP klient (Claude Code, Codex, Gemini CLI, ...) otevřený v repozitáři má server sestavit, spustit a použít bez ručního kroku; v repozitáři má běžet právě jeden server a nechat různé klienty koordinovat hned po instalaci; a justfile má směrovat to, co člověk spouští, na Rust executable všude, kde to zvládne.
Dva nebo tři klienti MCP otevření v jednom checkoutu jsou nyní běžný případ, ne výjimka. Každý spouští vlastní proces stdio, takže „jeden server“ nemůže znamenat „jeden proces na klienta“: znamená, že první proces obsluhuje a ostatní se k němu připojí. Seznam „Intentionally Absent“ repozitáře odmítá démony, servery, fronty a registry pracovníků; níže uvedené rozhodnutí na něj musí odpovědět řádek po řádku, jako ADR 1.
Rozhodnutí
- Jeden sdílený server na repozitář, vlastněný tím, kdo ho spustil. První
majordomus mcp(neboserve) v repozitáři naváže loopback HTTP projekci (Swagger UI na/docs,/openapi.json, každá trasa schopnosti a MCP přes HTTP na/mcp) vedle své vlastní stdio relace a zaloguje URL. Skončí, když zmizí jeho vlastní klient a odejde poslední připojený peer. Neexistuje proces bez klienta. - Lease, ne démon. Server se nachází přes jediný soubor,
.ai/local/state/mcp/server.json, v polovině lokální pro checkout, kterou vrstva už vyhrazuje pro provozní stav: vytvoří ho atomicky proces, který vyhraje, nese URL, jakmile je navázáno, a odstraní se, když server skončí. Lease, jehož server pro tento kořen neodpovídá, je zastaralý a převezme se. Nic jiného se nikam nezapisuje;--inspecta--standalonenezapisují vůbec nic. - Každý další
majordomus mcpje most. Nenese žádný index ani registr: přeposílá rámce svého klienta na/mcpserveru a posílá ping, aby jeho relace zůstala zachována. Když jeho server zanikne, zvolí znovu a buď převezme roli (a přeneseinitializesvého klienta, takže si klient ničeho nevšimne), nebo se připojí k tomu, kdo ji převzal. - MCP přes HTTP je požadavková polovina transportu Streamable HTTP, na
/mcp, s hlavičkouMcp-Session-Id,DELETEpro ukončení relace a klidovým vypršením. Žádný proud iniciovaný serverem: tento server neposílá žádná oznámení, takže by proud nenesl nic. Klient, který transport mluví sám, je peer jako kterýkoli most. - Peeři jsou schopnosti registru.
peers.listapeers.announcejsou typované deskriptory vbuiltin.rss explicitními expozicemi (nástrojemajordomus_peersamajordomus_announce,GET /api/v1/peers,POST /api/v1/peers/announce), promítnuté jako každá jiná schopnost; nic v kódu MCP ani HTTP o peerech neví. Peer je relace pojmenovaná tím, co klient řekl vinitialize; ohlášení je jeden řádek záměru a cest, kterých se peer chystá dotknout, informativní, v paměti serveru, zmizí s procesem. Instrukceinitializejmenují URL serveru, vlastní id peera volajícího a každého dalšího peera, takže se klient o ostatních dozví před svým prvním voláním nástroje. - Třetí druh schopnosti,
command. Dva druhy v ADR 2 byly query (provedena, jen pro čtení) a resource (čtení).peers.announceje provedena a mění paměť tohoto procesu, takže je tocommand: vázaná naPOSTa klientům MCP oznamovaná jako ne jen pro čtení, obojí odvozeno z druhu kontrolami registru, ne deklarováno na projekci. Žádný druh nezapisuje do repozitáře. - Klienti ho spouštějí.
bin/majordomus-mcpsestaví spustitelný soubor, když chybí nebo je starší než jeho zdroje (nikdy když to řekneMAJORDOMUS_BINneboMAJORDOMUS_NO_BUILD), nastaví adresář share aexecmajordomus mcp, bez zápisu na stdout. Kořen nese.mcp.json(Claude Code),.gemini/settings.json(Gemini CLI) a.codex/config.toml(Codex), každý jmenující tento launcher; který klient otevře první, stane se serverem, a ostatní se připojí. justfilev kořeni směruje na Rust executable každý recept, který umí obsloužit (mcp,serve,inspect,capabilities,describe,validate,generate,generate-check, měření výkonu, pokrytí) a recepty životního cyklu nechává shell nástroji. Je tenký z podstaty: každý recept jmenuje skript nebo příkaz, který chování vlastní.
Vůči seznamu „Intentionally Absent“, bod po bodu
| seznam odmítá | toto rozhodnutí |
|---|---|
| démon, server, monitor na pozadí | sdílený server je potomek klienta, který se zdržuje, jen dokud je připojen jiný klient, a končí s posledním; nic ho nespouští kromě klienta, nic ho nedrží naživu kromě klientů a --standalone ho odstraní úplně |
| databáze, fronta, vektorové úložiště | jeden JSON soubor leasu v lokální polovině, při ukončení odstraněný; nástěnka peerů je paměť jednoho procesu |
| registr nebo katalog pojmenovaných pracovníků | nástěnka uvádí živé relace pod jménem, které klient sám poslal, nic podle něj nesměruje, nepřiděluje žádnou roli, úroveň ani personu, nic neukládá a nelze ji upravit; je to stejný druh faktu jako zpráva o překryvu: kdo je tu právě teď |
| nález bez příkazu reprodukce | zastaralý lease, obsazený port, převzetí a nezdařené převzetí se každé loguje s URL nebo důvodem; GET /api/v1/peers a majordomus_peers reprodukují nástěnku |
| vlastní hlášení, kterému se věří bez nezávislé kontroly | tests/mcp_shared.rs spouští procesy a mluví s nimi přes skutečné roury a sockety, zabije server a sleduje, jak most přebírá; test/cases/90_mcp_shared_server.sh to dělá přes launcher v repozitáři, který init zapsal |
| číslo napsané tam, kde by ho mohl spočítat příkaz | každý počet (peerů, relací, rout) se počítá; měření výkonu nic nehlásí ani netvrdí |
Invarianty
- Za každý kořen repozitáře odpovídá nejvýše jeden server; druhý proces ho najde přes lease nebo převezme zastaralý, nikdy se nenaváže vedle něj.
- Stdio relace klienta přežije smrt svého serveru, když vrstvu může obsloužit jakýkoli proces; když žádný nemůže, klient dostane chybu JSON-RPC, která jmenuje důvod, nikdy ticho.
- Stdio relace vlastníka neplatí za stranu HTTP nic: workeři jsou samostatná vlákna nad neměnným registrem;
benches/shared.rsměří totéž volání s navázaným a nečinným serverem proti žádnému serveru. - Peery jsou projekce registru jako všechno ostatní; druh rozhoduje o metodě HTTP a anotacích MCP.
- Žádný režim nezapisuje nic pod sledovaný strom repozitáře; lease žije tam, kam kontrakt vrstvy už dává provozní stav.
Dodatek, 2026-09-05: nic zanechaného nezamyká klienta venku
Adversariální sondy první implementace našly dva způsoby, jak klient nedostane žádný server: lease soubor, který nebyl dokumentem lease, způsobil, že každé majordomus mcp skončilo, a adresář lease, který odmítal zápis, totéž. Byly přidány tři invarianty; jsou to doktrína project.shared-server-resilience, prokázaná tests/mcp_shared.rs a test/cases/90_mcp_shared_server.sh a uplatňovaná jako mcp-lease-resilience:
- Ať lease soubor po pádu, zabití nebo zkopírovaném checkoutu obsahuje cokoli, další proces ho převezme a pojmenuje, co našel; volba je časově omezená.
- Proces, který nemůže sdílet (lease nelze zapsat ani nahradit, server nelze spustit), obslouží samotného svého klienta, jako to dělá
--standalone, a řekne proč; klienta odmítnou jen vlastní chyby vrstvy. SIGTERM,SIGINTaSIGHUPodstraní lease dříve, než proces na signál zemře;kill -9nechá zastaralý lease pro další proces, což je invariant 6.
Zamítnuté alternativy
- Démon nebo systémová služba. Seznam to odmítá a je to zbytečné: server, který končí s posledním klientem, odpovídá na operátorovo „vždy právě jeden“.
- Jeden server na klienta na odlišných portech. Každý klient by měl vlastní Swagger UI a žádný by neviděl ostatní; „jeden server“ byl požadavek.
- Unix socket nebo soubor pod
/tmpjako rendezvous. Vrstva už má pro přesně tuto třídu stavu lokální polovinu checkoutu, po worktree, a lease si člověk přečte pomocícat. - Booleovské
read_onlyna deskriptoru místo druhu: kontroly tvaru registru, metoda HTTP a anotace by se na něj musely dotazovat každá zvlášť; druh nese sémantiku jednou. - Ponechat vlastní index mostu, aby se
--stricta--discoveryuplatňovaly na klienta: most by pak startoval stejně pomalu jako server pro nic; volby serveru se uplatní na relaci, log to říká a peer, který nemůže obsluhovat vrstvu s vlastními volbami, to řekne, když je požádán o převzetí. - SDK pro MCP nebo crate HTTP klienta pro most: jeden loopback požadavek na zprávu je třicet řádků nad
TcpStreama server vždy odpovídáContent-Length. - Nechat shellový nástroj dispečovat
mcp,serve,capabilitiesageneratev této změně: namasterje dispečovaný příkaz veřejný příkaz s položkou v registru, fixturou a pokrytím (share/commands.yaml); to je změna plochy shellového nástroje a přistane jako vlastní krok poté, co se tato větev rebasuje na ni. Do té doby jsou vstupními bodybin/majordomus-mcpajustfile.
Důsledky
Soubor lease, jeho schéma, koncový bod /mcp, hlavička relace, schopnosti peerů a druh command se stávají povrchy kompatibility. Dlouho žijící sdílený server si ponechává index, který sestavil při startu; klient, který se připojí později, vidí vrstvu, jaká byla tehdy, což je znovuobjevení založené na restartu, které ADR 2 už učinilo kontraktem, a server skončí, když odejde poslední klient. Dokumenty, které říkaly „jeden proces na klienta, žádný port“, nyní říkají „jeden server na repozitář, vlastněný svými klienty“ a docs/MCP.md nese životní cyklus.
Strategie testování
Nejprve black-box: tests/mcp_shared.rs spouští binárku jako vlastníka, most a přímého HTTP klienta, přes skutečné roury a skutečný socket, a pokrývá lease, záložní port, odkládání serve, --standalone, převzetí po zabití, opětovné připojení, když lease získal jiný proces, a odmítnutí, když převzímající nemůže vrstvu servírovat; tests/shared_units.rs pokrývá nástěnku, lease, relace koncového bodu a expiraci, klienta mostu a smyčku stdio v procesu; benches/shared.rs měří cesty, o jejichž optimalizaci provozovatel požádal; test/cases/90_mcp_shared_server.sh spouští launcher a dva klienty v repozitáři, který napsal init shellového nástroje, a kontroluje konfigurace klientů. Kontroly ADR 2 jsou beze změny.