Skip to content

Serve MCP revision 2026-07-28: fastmcp 4, tests, conformance, startup fixes - #1

Merged
AmirK-S merged 25 commits into
masterfrom
chore/etape-1-demarrage
Sep 6, 2026
Merged

AmirK-S merged 25 commits into
masterfrom
chore/etape-1-demarrage

Conversation

@AmirK-S

@AmirK-S AmirK-S commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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.

  • Startup: ten corrections, six of them code that had never run (compose variables without the HIVEMIND_ prefix, an import that does not exist in alembic/env.py, two migrations failing on an empty database, casbin_rule created by no migration, the async driver stripped from an async adapter). Migrations run from docker/entrypoint.sh before uvicorn.
  • Tests: 69 in CI (startup contract, one file per MCP tool through the HTTP transport with a bearer token, the 2026-07-28 protocol contract), 29 more behind the models marker for the PII pipeline. Written before each change; the commit messages record how many failed.
  • Upgrade fastmcp 2.14.5 to 4.0.3 on the mcp 2.1.1 SDK. Two things the guides do not say: get_http_headers() drops Authorization by default (eight call sites ask for it), and the DNS rebinding guard is off by default (armed through HIVEMIND_ALLOWED_HOSTS).
  • Conformance: official suite before and after, conformance/. On 2026-07-28, 5 of 37 scored scenarios before, 13 after, zero regression, every remaining failure justified in expected-failures-2026-07-28.yaml; the tool exits 0 with it.
  • Two defects found by the tests and the demo, fixed: tool refusals were not MCP errors at the JSON-RPC level; the PII pipeline redacted library names (asyncpg became [[API_KEY]]).
  • Removed: dashboard, generated SDKs, FalkorDB driver, skill file, npx launcher, internal planning files. README rewritten, LICENSE, CHANGELOG 0.1.0.

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.
  • Fresh clone, cp .env.example .env && docker compose up -d --build, ./scripts/demo.sh twice: exit 0, transcript in scripts/demo-transcript.txt.
  • Line-by-line review of every public text against the repository before this PR.

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

AmirK-S and others added 25 commits September 5, 2026 20:17
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
…2026-07-28

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
…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
@AmirK-S
AmirK-S merged commit 504cc4a into master Sep 6, 2026
2 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