Repository navigation
feat(bin): add a local stdio MCP server for Claude Desktop and Claude Code - #6232
Open
maruthiprithivi wants to merge 10 commits into
Open
maruthiprithivi wants to merge 10 commits into
maruthiprithivi wants to merge 10 commits into
Conversation
|
maruthiprithivi
force-pushed
the
fm/firstmate-mcp-upstream
branch
from
October 1, 2026 02:40
7639e1f to
bf6e8cf
Compare
added 10 commits
October 1, 2026 14:07
… Code bin/fm-mcp.py serves eight tools over stdio JSON-RPC. It wraps fm-inbox.sh, fm-tasks-axi.sh, fm-crew-state.sh, state/home-summary.json, and data/<id>/report.md. Its only write is an idempotent inbox note; it never spawns, steers, merges, tears down, or edits backlog or state. Tradeoff: the server uses only the Python standard library instead of the official mcp SDK. The repo's portable CI has python3 but no uv or mcp package, so an SDK-based test would always skip there, and the template would pick up its first Python dependency and lockfile. The cost is that protocol negotiation (2024-11-05 through 2025-11-25) is maintained here rather than inherited from the SDK. tests/fm-mcp.test.sh drives every tool over the real protocol against a temporary home, and docs/mcp.md owns client setup.
… tests/fm-mcp.test.sh and docs/mcp.md. `bash tests/fm-mcp.test.sh` passes (8 ok, 0 failures), ruff and py_compile are clean, and shellcheck reports only the existing SC1091 info about the sourced lib.sh. Nothing is committed yet. - **ci-1 (a note must be created only once per retry):** `request_id` is now a required argument and the server no longer generates one, so the `uuid` import is gone. The tool description tells the client to pick a new id for each note and reuse the same id when retrying. Tests now check that both `message` and `request_id` are required and that a note without a `request_id` is refused. I removed the old auto-id case. - **ci-2 (home files must not be read through a symlink or from outside FM_HOME):** the check sits in `home_file`, which both the crew report and the home summary go through. It refuses the read if any path component is a symlink or the resolved path is outside FM_HOME. This matches the existing `! -L` check on reports in fm-inbox.sh's sibling script, fm-inactive-reconcile.sh. Tests cover a symlinked `report.md` and a symlinked `data/<id>` directory, and confirm the outside file's contents are never returned. - **ci-3 (a poll with no cursor should only fetch the newest page):** `fm-inbox.sh receipts` has no option for "newest page", so I bounded it in the server and left fm-inbox.sh unchanged. The server reads the reply counter (`state/inbox/.replies/.seq`) and passes `--after` set to the counter minus 20, so the script sends back only the newest 20 replies. Two limits: - The server now reads one of fm-inbox.sh's internal files. - fm-inbox.sh itself still reads every note to build its answer; only the amount it sends back is bounded. - A note that got two replies leaves a gap in the counter, so the page can hold fewer than 20. There is a `ponytail:` comment on this, and docs/mcp.md says so. The `after` cursor works as before, and the hint for older replies names only `note_id`. The existing more-than-20-replies test still passes. - **ci-4 (read tools must not write anything in the home):** the test now hashes every path and file byte under the temporary home before and after all the read and error calls, and requires the two hashes to match. This replaces the wake-queue line count. I also updated the fm-mcp.py header and docs/mcp.md, and dropped a doc clause about "when the client supplies its own request_id", which no longer applies now that it always does. I confirmed the updated tests fail against the previous fm-mcp.py; the script stops at the first failure, which was the `request_id`-required check
The note marker becomes "[via firstmate MCP from <name> <version>]", taken from the client's initialize clientInfo and flattened to one capped line, with "unknown client" when absent. Claude Desktop (claude-ai) and Claude Code are distinguishable this way; Desktop's chat, Cowork, and Code tabs share one clientInfo. firstmate_note_replies now states that each reply names the note id it answers, so a session passing its own note ids sees only its own answers. Tests cover the client marker, a client name that tries to inject note lines, and the reply note id.
…mate_status A session lock whose recorded owner process is gone, such as a resumed primary that has not re-taken it, made fm-inbox.sh ready report can_receive false, and MCP clients told the user firstmate was not running. firstmate_status now adds a line saying liveness cannot be proven right now while notes still queue and wake firstmate, and the tool description and docs/mcp.md say the same. A test drives a lock owned by an exited process.
maruthiprithivi
force-pushed
the
fm/firstmate-mcp-upstream
branch
from
October 1, 2026 06:15
cf9b69d to
2806283
Compare
Author
|
@kunchenguid Speaking as Maruthi’s firstmate: could you confirm the next review/CI step for this contribution? The reported Greptile check passes, the original feedback has author responses, and the later cross-version replay concern was explicitly withdrawn. I currently see only the Greptile check, so I am not treating that as full CI clearance. Please let us know whether workflow approval or another author action is needed before maintainer review. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
bin/fm-mcp.py, a local stdio MCP server that uses only the Python standard library. It has one write tool,firstmate_send_note. That tool queues an inbox note throughfm-inbox.sh noteand wakes firstmate. Each note starts with a[via firstmate MCP from <client> <version>]provenance line, and the caller must supply arequest_id. The server stores that id prefixed with a hash of the client name, so a retry with the same id makes only one note and two clients reusing an id still get separate notes.reply_cursorfor newer ones), status and readiness, the home summary, the backlog list and show, a crew's state, and a crew's report. Home files are never read through a symlink or from outsideFM_HOME. When firstmate's session lock is stale,firstmate_statussays liveness cannot be proven but notes are still queued.docs/mcp.md, which covers the authority boundary and setup for Claude Desktop and Claude Code. Adds links fromREADME.md,docs/scripts.md, and the documentation audiences index. Addstests/fm-mcp.test.sh, which drives every tool over stdio against a temporary home and checks that the read tools leave the home byte-identical.🤖 Generated with Claude Code
Risk Assessment
✅ Low: The fix round only reworded the stale-lock text, using the exact wording the user decided on, in all three places (the STALE_LOCK constant, the firstmate_status description, docs/mcp.md), and updated the test assertion to match; no logic changed, and no text anywhere still claims the wake reaches firstmate.
Testing
The existing end-to-end MCP test passed. I then built a throwaway firstmate home with fm-lab-home.sh and drove the real server over stdio as both Claude Desktop (claude-ai) and Claude Code. Status gives the decided stale-lock wording only when the lock is stale. Notes are kept separate per client and a retry replays the original note. Replies round-trip with the caller's own unprefixed id. Everything passed and the lab was removed. The live Claude Desktop GUI was not driven because it is a native app outside this gate.
Evidence: firstmate_status with stale, held, and free locks
Source: firstmate_status with stale, held, and free locks
Evidence: Same request_id from Desktop and Code gives two notes; a retry replays
Source: Same request_id from Desktop and Code gives two notes; a retry replays
Evidence: Reply round-trip with an unprefixed request_id per client
Source: Reply round-trip with an unprefixed request_id per client
Evidence: firstmate_status tool description
Source: firstmate_status tool description
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/fm-mcp.py:163- The stale-lock line says something nobody has checked: "notes still queue and wake firstmate". In fm-session-lock-lib.sh, "stale" means only that the recorded pid is gone (bin/fm-session-lock-lib.sh:330). The most common way to get there is a firstmate session that crashed or exited without releasing its lock, not just a resumed session that has not re-taken it. When the lock is not held, fm-inbox.sh ready never works out who consumes wakes: resolved_model stays empty, so wake_consumer is "unknown" (bin/fm-inbox.sh:891-904). It also deliberately reports can_receive:false for stale:* (bin/fm-inbox.sh:929). Concrete case: the firstmate session dies, so state/.lock holds a dead pid. Claude Desktop calls firstmate_status and sees can_receive:false and then the server's own line saying a wake will reach firstmate. It tells the captain firstmate will pick the note up, but nothing drains the wake queue until someone restarts firstmate. No error is shown. The note really is saved and the wake really is appended, but delivery is not proven. The wording overrides fm-inbox.sh's own judgment without new evidence, and the same claim appears in the tool description (bin/fm-mcp.py:254-255) and docs/mcp.md:15. Smallest honest fix: keep "liveness cannot be proven" and replace the second clause with something firstmate can stand behind, such as "notes are still saved and queued; firstmate picks them up the next time its session runs". Apply the same wording at all three places. This is ask-user because it changes text the captain sees and that the captain asked for in this fix.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-mcp.test.sh(10 ok, including 'status: a stale lock reads as unproven liveness')bin/fm-lab-home.sh create $LAB, then the realbin/fm-mcp.pydriven over stdio JSON-RPC by a Python client that sends clientInfo claude-ai or claude-codefirstmate_status with state/.lock naming a dead pid (stale), a live pid (held), and no lock (free)firstmate_send_note: claude-ai and claude-code both send request_id note-1, then claude-ai retries the same request; checked the .note files on disk for markers and prefixed request_idsbin/fm-inbox.sh replyagainst each note, then firstmate_note_replies from each client on its own note and on the other client's notetools/list: checked the firstmate_status description wordingTore the lab down withrm -rf $LAB; worktree is clean✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.