foxxe-mcp
Shared MCP plumbing for the FoxxeLabs server fleet. One SDK pin, one bootstrap, thin re-export of the SDK server class — so a spec revision is one commit here, not eleven latent crash-loops.
See docs/PRD-foxxe-mcp.md for why. See docs/migration.md for how to convert a server.
Status: v0.1.0 — library built, fleet unmigrated.
Two shapes
The fleet splits evenly, and the library supports both as first-class.
1. The MCP app is the app (ainm, git-mcp) — use serve() / build_app().
2. MCP is mounted inside a parent app (tomhas's FastAPI dashboard, mnemos's
Starlette API) — use mounted_app(), keep your own lifespan and /health:
from foxxe_mcp import MCPServer, mounted_app, version_payload
mcp = MCPServer("tomhas")
@asynccontextmanager
async def lifespan(app):
async with mcp.session_manager.run(): # same name on both SDK majors
yield
app = FastAPI(lifespan=lifespan)
app.get("/version")(lambda: version_payload("tomhas"))
app.mount("/", mounted_app(mcp, allowed_host=EXTERNAL_HOST)) # last: routes above win
mounted_app() carries the fix for Starlette stripping the leading / under
Mount("/") — plumbing mnemos and tomhas had each written by hand.
The shape
from foxxe_mcp import MCPServer, serve
mcp = MCPServer("tomhas")
@mcp.tool()
def status() -> str:
"""Report the current run state."""
...
if __name__ == "__main__":
serve(mcp)
MCPServer is the SDK's own class, re-exported unwrapped. The tool decorator is
the SDK's, reached through the instance — foxxe-mcp never wraps the registration
API (PRD NG2), so every SDK feature is available the day it ships.
serve() owns what eleven servers currently hand-roll: transport, bind address,
allowed host, /health, /version, logging, graceful shutdown.
Why the name is MCPServer on an SDK that calls it FastMCP
v0.1 pins mcp>=1.29.1,<2, where the class is FastMCP. The export is named
MCPServer anyway — that is the mcp 2.x name, and exporting it now means the
eventual pin flip changes this library and no server code at all.
That is a claim, so it is tested: scripts/verify_sdk2.py runs the same library
against mcp==2.1.1 and checks that MCPServer resolves to the renamed class,
that a tool still registers, and that /health, /version and /mcp all still
answer. Both majors, one codebase.
$ python scripts/verify_sdk2.py
foxxe-mcp 0.1.0 against mcp 2.1.1
PASS SDK major detected as 2 — mcp.server.mcpserver
PASS MCPServer resolves to the renamed class
...
All checks passed — the v1.0 pin flip is a one-line change.
The bound is enforced twice
Once at install time, in pyproject.toml, and again at boot, in check_sdk().
The second one is not redundant. PRD finding B: rialu declares no mcp at all,
only an unbounded fastmcp, and fastmcp 3.x pulls mcp>=2 transitively — a
declaration that binds only at install time cannot catch what a resolver did to
a transitive dependency. serve() calls check_sdk() before the first tool
call, so a wrong resolution fails at boot, next to its cause.
mcp out of range is fatal. fastmcp out of range warns, and is fatal under
FOXXE_MCP_STRICT_SDK=1.
Install
foxxe-mcp==0.5.0
Add the fastmcp extra only if the server actually uses the third-party
fastmcp package: foxxe-mcp[fastmcp]==0.5.0. Never declare mcp or
fastmcp directly in a server again — that is the whole point.
Published to PyPI rather than installed as a git dependency, which is a
deliberate amendment to PRD §7. The git-by-tag plan died on contact with the
first real deploy: this repo is private, so pip install git+https://...
inside a Docker build has no credentials and fails with exit 128. The
alternatives were a build-time PAT threaded through eleven Dockerfiles, or
making the repo public — which would publish fleet.toml and the internal
audit along with it. PyPI keeps the repo private, publishes only the package,
and makes every Dockerfile a plain pip install again. NG3 rules out a
private index; this is the public one.
Fleet operations
foxxe-herd status # poll every server's /version — who is on what
foxxe-herd redeploy # dry run
foxxe-herd redeploy --yes # fly deploy every migrated server
fleet.toml maps server name to Fly app name (verified against flyer apps_list, not guessed — the -foxxelabs suffix is inconsistent) and records
which servers are migrated and which are still unbounded.
Environment
| Variable | Default | Meaning |
|---|---|---|
PORT |
8080 |
Port to bind |
BIND_HOST |
0.0.0.0 |
Bind address (Fly requires all interfaces) |
ALLOWED_HOST |
$FLY_APP_NAME.fly.dev |
Public hostname for DNS-rebinding protection — not the bind address |
MCP_TRANSPORT |
http |
http or stdio |
LOG_LEVEL |
INFO |
Root log level |
LOG_FORMAT |
json |
text for human-readable |
MCP_BEARER_TOKENS |
unset | Comma-separated; presence enables bearer auth |
FOXXE_MCP_STRICT_SDK |
unset | 1 makes an out-of-range fastmcp fatal |
FOXXE_MCP_DNS_REBINDING |
unset | 1 re-enables Host/Origin checking (off by default — the edge pins the Host) |
TAISCE_SERVICE_TOKEN |
unset | Service token for taisce_fetch() |
SENTINEL_URL |
unset | Presence enables sentinel_push() |
Development
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file foxxe_mcp-0.5.2.tar.gz.
File metadata
- Download URL: foxxe_mcp-0.5.2.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c54c957fb36e131791f82ec84d003be7c9819f96c848af8125bffa4ff9e05e5a
|
|
| MD5 |
ea3f2cffdd4f8306a5872c329e680201
|
|
| BLAKE2b-256 |
bdbf6c4a0b5231509feccbaa0b49fd6b189fd83d753a9da4e004e562a3d92232
|
File details
Details for the file foxxe_mcp-0.5.2-py3-none-any.whl.
File metadata
- Download URL: foxxe_mcp-0.5.2-py3-none-any.whl
- Upload date:
- Size: 28.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57fee2e1c46b8dddf71013eab929651d9e6284994fd60fffd0b5621a9b5fc8ce
|
|
| MD5 |
b6cce77380353eca4bc9abba61ed7a6a
|
|
| BLAKE2b-256 |
e70ae1bf4b233e69b69b14b1bdddb306ed984772a63649b9490584aef01a0f08
|