Repository navigation
Serve MCP revision 2026-07-28: fastmcp 4, tests, conformance, startup fixes - #1
Merged
Merged
Conversation
Adds the first test suite of the repository. It covers what a fresh clone needs before docker compose up can serve a request: HIVEMIND_ prefixed configuration, Alembic migrations applying on an empty pgvector database, /health, and the MCP endpoint answering tools/list at exactly /mcp. Model singletons (PII, embeddings, injection) are replaced by doubles in conftest.py so the suite never downloads a model. Migration tests need a PostgreSQL reachable through HIVEMIND_TEST_ADMIN_URL and skip otherwise. Against the previous commit, 10 of these tests fail and 3 error out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Measured on 2026-09-05 from a fresh clone: ten corrections separated the repository from a 200 on /health, six of them code that had never run. - docker-compose.yml passed DATABASE_URL and REDIS_URL without the HIVEMIND_ prefix that config.py requires; the container connected to itself. Both variables are now prefixed. - alembic/env.py imported register_vector, which pgvector.sqlalchemy does not export, so no migration had ever applied. Migration 001 re-emitted CREATE TYPE through create_table, and 004 passed create_type to sa.Enum, which ignores it. Both apply on an empty database now. - casbin_rule was created by no migration and the enforcer reads it at startup: migration 007 creates it. rbac.py no longer strips +asyncpg from the URL handed to an async adapter. - The MCP endpoint answered on /mcp/mcp while /mcp returned 307 then 404. main.py now exposes create_app(), mounts the FastMCP app at the root after the other routes, and serves /mcp exactly. The server card advertises the transport path and the bearer authentication the tools actually check, instead of X-API-Key. - The Dockerfile runs docker/entrypoint.sh, which applies alembic upgrade head before uvicorn. Health checks use the interpreter: the slim image has no curl. The spaCy model presidio loads is pinned in a PEP 735 dependency group so the runtime image never downloads it. - uv.lock is tracked, .env.example exists at the root, .env and deep_research/ are ignored, the distribution name is hivemind-mcp. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Runs of @modelcontextprotocol/conformance 0.2.0-alpha.11 against the server at 844e5eb, for --requirements 2025-11-25 (8 of 30 scored scenarios pass) and 2026-07-28 (5 of 36 pass, the server rejects the protocol version). Zero wire schema violation, zero server-side 500. dns-rebinding-protection fails on both and is the only defect unrelated to the revision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Thirty-five tests in seven files, one per tool, plus a CI workflow. Every tool is called through POST /mcp with tools/call and a bearer token, against a PostgreSQL database migrated to head and created once per session, so that a change in how FastMCP surfaces the Authorization header cannot pass unseen. Each file covers the nominal path and at least three refusals: missing token, token of another organisation, unknown handle. Refusals are asserted at the JSON-RPC level, isError true, which is what an MCP client reads. Against the previous commit, 26 of these tests fail: refusals were returned as CallToolResult objects that FastMCP serialises as ordinary values, so the outer isError read false, and manage_roles awaited a synchronous pycasbin method. The workflow runs the suite on ubuntu with a pgvector service. It does not run make check-sdk-drift: regenerating the Python SDK with the pinned generator already rewrites 24 files on master. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Tool refusals were built as mcp.types.CallToolResult(isError=True) and returned. FastMCP does not recognise that type as a tool result: it serialised the object as an ordinary value, the JSON-RPC result carried isError false, and the real flag sat inside structuredContent. A client reading the top level believed every refused call had succeeded. The seven tools now raise fastmcp.exceptions.ToolError with the same messages, which FastMCP renders as isError true with the text in content. The REST routes that reuse the search internals catch ToolError instead of type-checking the return value. Return annotations and docstrings follow. rbac.enforce awaited casbin.AsyncEnforcer.enforce, which is synchronous and returns a bool; manage_roles had therefore never completed a call. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Six tests: server/discover without handshake or session, ttlMs and cacheScope on tools/list, Mcp-Method header validation, the legacy era still served on the same URL, a foreign Host header refused, and a tool call with a bearer token on the modern era. Against fastmcp 2.14.5 three of them fail: the server rejects the protocol version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
The README, the Dockerfile label and the npm manifest all declared MIT, and no LICENSE file existed. SITREP.md was an internal planning document. The npx/ launcher told users to run a package name owned by someone else and sent the wrong authentication header. glama.json served a directory listing that is out of scope. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
fastmcp 2.14.5 (mcp 1.26.0) becomes fastmcp 4.0.3 on the mcp 2.1.1 SDK, with
fastapi >= 0.133.0 and pydantic >= 2.12. The server now answers
server/discover without a handshake, emits ttlMs and cacheScope on
tools/list, validates the Mcp-Method and Mcp-Name headers, sets no session
id on the 2026-07-28 era, and still serves the handshake revisions on the
same URL. None of that is application code: the six tests written before
the upgrade pass on the framework alone.
Two things the upgrade guides do not mention had to change:
- get_http_headers() drops the Authorization header by default in 4.x. The
eight call sites pass include={"authorization"}; without it every tool
refused every caller, silently. The tool tests that go through the HTTP
transport with a bearer token are what caught it.
- The DNS rebinding guard is off by default and, in auto mode, infers
nothing when uvicorn listens on 0.0.0.0 as it does in Docker. The app
passes host_origin_protection="auto" with an explicit allowed_hosts list,
read from HIVEMIND_ALLOWED_HOSTS (default localhost,127.0.0.1). A foreign
Host header now gets 421; the conformance baseline showed 200.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
431 dashes in 61 files, replaced by a comma, or a colon after a short title. Comments, docstrings and log messages only; no identifier or behaviour changes, the suite is unchanged at 58 passed. README.md, dashboard/ and sdks/ are left for the publication pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Same tool, same commands as the baseline. On 2026-07-28, 13 of 37 scored scenarios pass (5 before), 111 checks pass against 56 (55 against 102 before). Eleven scenarios move from failure to success, none regress. dns-rebinding-protection, the one defect unrelated to the revision, passes on both requirement sets. Two expected-failures files justify every remaining failure line by line: reference tools with imposed names, or capabilities this server does not expose. With them the tool exits 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
server/discover returned version 4.0.3, the fastmcp release. The FastMCP instance, the FastAPI app and the server card now all read hivemind.__version__. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
What it does, what it does not do, a quick start measured on a fresh clone, the seven tools with their refusals, the conformance numbers before and after the upgrade, how to run the tests, the configuration, the layout of the repository including the parts nothing depends on, and an end of life note that names the revision served, the pinned framework and what breaks next. Gone: the demo image that did not exist, the npx one-liner that installed a third party's package, the hosted-instance placeholders, the directory submission checklist, the six-tool table that omitted manage_roles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
…2026-07-28 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
The commons refuses the same text twice for one organisation, so the contribution carries a run stamp. A refused call now exits non-zero, so the script stops instead of failing later on an empty handle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Two stages. Without a model: the pattern recognizers alone flag Anthropic, OpenAI, GitHub, AWS and DSN secrets and flag nothing in five technical sentences. With GLiNER (marker "models", run locally with pytest -m models): strip() returns four technical sentences word for word, never doubles a placeholder, removes an email, a key, a name and an address, is stable over ten calls, and preserves a fenced code block whatever the placeholder UUID. Before the fix, 20 of the 28 model-backed tests fail: asyncpg, SQLAlchemy, pool_size and PostgreSQL come back as [[API_KEY]] or [REDACTED], and a code block is lost when its placeholder token gets classified. One known limit is kept as an expected failure: Prometheus and Grafana are still redacted by the spaCy recognizer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
The demo stored "With asyncpg behind SQLAlchemy 2, a pool_size above the PostgreSQL max_connections" as "With [[API_KEY]] behind [[API_KEY]], a [REDACTED] above the [REDACTED] max_connections". No pattern matched those words. analyze() was called without an entity list, so Presidio passed every registered entity, GLiNERRecognizer turned the unknown name API_KEY into a zero-shot label, and package names matched it between 0.30 and 0.51. The list came from a set, so the label order, and the output, changed per process; the content hash is computed after redaction, so duplicates slipped between workers. - GLiNER threshold 0.60: measured technical false positives sit between 0.30 and 0.51, real secrets, names and addresses between 0.79 and 0.93. - A literal entity list is passed to both analyze() calls: fixed order, no leaked label. - The second pass drops findings inside its own placeholders, which is what produced [[API_KEY]]. - The verbatim check replaces a value only where the analyzer flagged it, instead of a global str.replace that turned /etc/nginx/nginx.conf into /etc/[REDACTED]/[REDACTED][REDACTED]. - Analyzer findings that overlap a __CODE_BLOCK_<hex>__ or __INLINE_<hex>__ token are dropped: about 3 percent of placeholder draws were classified as PERSON or MEDICAL_LICENSE and the block was silently lost. - Two patterns for sk-ant-api and sk-proj keys, previously left to the zero-shot judgement alone. scripts/demo-transcript.txt is the demo replayed on a fresh clone after the fix: bob reads alice's sentence word for word. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
…and the skill file Nothing in the repository depended on them and nothing tested them. The Next.js dashboard built and linted clean on 2026-09-06 but had no lock file and needed a next upgrade for three advisories; the two SDKs were generated in February and the pinned generator already rewrites most of their files; the FalkorDB driver was imported nowhere and its three settings pointed at a service no compose file started; the skill file listed categories the enum does not have. The Makefile only regenerated the SDKs. All of it stays in the history. The README says what is not planned (an A2A facade) and what is known and not corrected (two product names still redacted), and states the issue policy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
…and build files Findings of a line-by-line review against the repository: French paragraph in the English README; a test count that was wrong (69 in the CI run, 29 more behind the models marker); a pytest command that would have downloaded GLiNER; build and startup durations with no source in the repository; a Docker image that is never published; BM25 named where PostgreSQL full text is used; two tool tables missing refusals the code makes; a seven-day answer promise on an unmaintained repository; "dated" read as "outdated". The conformance note gave 36 scored, 31 failed and 1 skipped for the 2026-07-28 baseline where its own log says 37, 32 and 0, and referred to session notes and server logs a reader cannot see. The changelog gets a 0.1.0 header, the demo entries and the serverInfo fix. Health checks allow four minutes before the first verdict, matching the first-run model download. scripts/demo.tape produced a GIF that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
…de the repository The test files were written in French while the application is in English; names, docstrings, comments and assertion messages are now English. Server error strings under test are unchanged. Fake secrets used by the PII tests are assembled at run time so that no line of the repository carries a complete key pattern. Comments across hivemind/ no longer cite research notes, plans or pitfall numbers a reader cannot open; the explanation each one carried stays. The pytest marker description and the asyncpg comment in pyproject.toml are English and self-contained. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6
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 this branch does
Brings the repository from its February state to a server that starts on a fresh clone with one command, serves MCP revision
2026-07-28(no sessions) and the handshake revisions on the same URL, and carries a test suite and a conformance record.HIVEMIND_prefix, an import that does not exist inalembic/env.py, two migrations failing on an empty database,casbin_rulecreated by no migration, the async driver stripped from an async adapter). Migrations run fromdocker/entrypoint.shbefore uvicorn.modelsmarker for the PII pipeline. Written before each change; the commit messages record how many failed.get_http_headers()dropsAuthorizationby default (eight call sites ask for it), and the DNS rebinding guard is off by default (armed throughHIVEMIND_ALLOWED_HOSTS).conformance/. On2026-07-28, 5 of 37 scored scenarios before, 13 after, zero regression, every remaining failure justified inexpected-failures-2026-07-28.yaml; the tool exits 0 with it.asyncpgbecame[[API_KEY]]).How it was checked
uv run pytest -q -m "not models": 69 passed (CI on every commit of the branch).uv run pytest -q -m models: 28 passed, 1 expected failure documented in the README.cp .env.example .env && docker compose up -d --build,./scripts/demo.shtwice: exit 0, transcript inscripts/demo-transcript.txt.Merged by the repository owner without an external reviewer; this is a single-maintainer demonstration repository.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KVgN94SLLoPZUt1cfucDZ6