foxxe-mcp 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxxe_mcp-0.3.0/.gitignore +7 -0
- foxxe_mcp-0.3.0/CHANGELOG.md +106 -0
- foxxe_mcp-0.3.0/LICENSE +21 -0
- foxxe_mcp-0.3.0/PKG-INFO +137 -0
- foxxe_mcp-0.3.0/README.md +116 -0
- foxxe_mcp-0.3.0/pyproject.toml +58 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/__init__.py +86 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/_sdk.py +184 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/auth.py +110 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/errors.py +76 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/health.py +71 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/herd.py +156 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/logging.py +84 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/secrets.py +121 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/serve.py +243 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/telemetry.py +79 -0
- foxxe_mcp-0.3.0/src/foxxe_mcp/version.py +16 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Semver, honestly (PRD §7): transport or auth behaviour changes are **major**,
|
|
4
|
+
new helpers **minor**, SDK patch bumps that change no foxxe-mcp behaviour
|
|
5
|
+
**patch**. Every release records the exact SDK bound it pins.
|
|
6
|
+
|
|
7
|
+
## v0.3.0 — 2026-09-04
|
|
8
|
+
|
|
9
|
+
**First PyPI release.** `pip install foxxe-mcp==0.3.0`.
|
|
10
|
+
|
|
11
|
+
**SDK bound** — unchanged: `mcp>=1.29.1,<2`, `fastmcp>=2.14,<3` (extra).
|
|
12
|
+
|
|
13
|
+
**Changed**
|
|
14
|
+
|
|
15
|
+
- Distribution moves from a git dependency pinned by tag to PyPI, amending
|
|
16
|
+
PRD §7. Found by the first real deploy: the repo is private, so a Docker
|
|
17
|
+
build cannot clone it (`git clone ... exit code: 128`) — and no fleet
|
|
18
|
+
Dockerfile has GitHub credentials. The alternatives were threading a PAT
|
|
19
|
+
through eleven Dockerfiles or making the repo public, which would publish
|
|
20
|
+
`fleet.toml` and the internal audit with it. PyPI keeps the repo private and
|
|
21
|
+
publishes only the package. NG3 rules out a *private* index; this is not one.
|
|
22
|
+
- The sdist is an explicit allowlist (`src/`, README, CHANGELOG, LICENSE,
|
|
23
|
+
pyproject). `fleet.toml`, `docs/` and `scripts/` carry internal topology and
|
|
24
|
+
are deliberately not published.
|
|
25
|
+
|
|
26
|
+
## v0.2.1 — 2026-09-04
|
|
27
|
+
|
|
28
|
+
**SDK bound** — unchanged.
|
|
29
|
+
|
|
30
|
+
**Fixed**
|
|
31
|
+
|
|
32
|
+
- `setup_logging()` now takes sole ownership of the root handlers. The SDK
|
|
33
|
+
calls its own `configure_logging()` from the server constructor, so a plain
|
|
34
|
+
handler is already installed by the time `serve()` runs; adding a second one
|
|
35
|
+
made ainm emit every line twice — once bare, once JSON — doubling log volume
|
|
36
|
+
and leaving the JSON stream unparseable. Caught by booting the converted
|
|
37
|
+
pilot, not by the unit tests.
|
|
38
|
+
|
|
39
|
+
## v0.2.0 — 2026-09-04
|
|
40
|
+
|
|
41
|
+
Found by the ainm pilot, which is what a pilot is for.
|
|
42
|
+
|
|
43
|
+
**SDK bound** — unchanged: `mcp>=1.29.1,<2`, `fastmcp>=2.14,<3` (extra).
|
|
44
|
+
|
|
45
|
+
**Added**
|
|
46
|
+
|
|
47
|
+
- `serve(mcp, app=...)` runs an app already built by `build_app()` instead of
|
|
48
|
+
building a second one. ainm's tests reach for a module-level `server.app`, so
|
|
49
|
+
the object has to exist at import time *and* be the one `__main__` runs;
|
|
50
|
+
without this, `python server.py` left an inert second session manager behind.
|
|
51
|
+
Passing `app=` together with `build_app` keyword arguments is a `TypeError`
|
|
52
|
+
rather than a silent ignore.
|
|
53
|
+
|
|
54
|
+
## v0.1.0 — 2026-09-03
|
|
55
|
+
|
|
56
|
+
First release. Extracted from the ainm bootstrap, which had the cleanest
|
|
57
|
+
hand-rolled `server.py` in the fleet (PRD §8.1).
|
|
58
|
+
|
|
59
|
+
**SDK bound**
|
|
60
|
+
|
|
61
|
+
| name | bound | resolved in CI |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `mcp` | `>=1.29.1,<2` | 1.29.1 |
|
|
64
|
+
| `fastmcp` (extra) | `>=2.14,<3` | not installed by default |
|
|
65
|
+
|
|
66
|
+
PRD OQ1 answered: **hold at `<2` for the first release.** The fleet is entirely
|
|
67
|
+
pre-2.0, and converting the plumbing and the tool-definition API surface in one
|
|
68
|
+
step would double the blast radius of the ainm pilot. The `MCPServer` *name* is
|
|
69
|
+
exported now, so the v1.0 pin flip is a change to this library and to no server.
|
|
70
|
+
Verified against `mcp==2.1.1` by `scripts/verify_sdk2.py`.
|
|
71
|
+
|
|
72
|
+
The floor is 1.29.1 — the newest 1.x — deliberately above every floor in the
|
|
73
|
+
audit (`1.6.0`, `1.25`, `1.26.0`, `1.28.1`). Four floors across six apps was the
|
|
74
|
+
drift; one floor ends it.
|
|
75
|
+
|
|
76
|
+
**Added**
|
|
77
|
+
|
|
78
|
+
- `MCPServer` / `FastMCP` / `Context` / `Image` — unwrapped SDK re-exports under
|
|
79
|
+
a name stable across the 1.x→2.x rename (`_sdk`).
|
|
80
|
+
- `serve()` and `build_app()` — transport selection, Fly bind conventions,
|
|
81
|
+
allowed-host handling, lifespan wrapping, graceful shutdown.
|
|
82
|
+
- `/health` (dependency-free) and `/version` → `{server, foxxe_mcp, sdk}`.
|
|
83
|
+
- `check_sdk()` — boot-time enforcement of the bound on **both** names, which is
|
|
84
|
+
the only place the transitive `fastmcp` → `mcp>=2` case (PRD finding B) can be
|
|
85
|
+
caught.
|
|
86
|
+
- `BearerAuthMiddleware` — pure-ASGI, probes exempt.
|
|
87
|
+
- `ToolError`, `error_envelope()`, `error_json()`, `failing_to_json()`.
|
|
88
|
+
- `setup_logging()` — JSON lines, level and format from env.
|
|
89
|
+
- `taisce_fetch()` — service-token key reveal, never logs the value.
|
|
90
|
+
- `sentinel_push()` — opt-in, fire-and-forget, cannot raise into the caller.
|
|
91
|
+
- `foxxe-herd` — fleet drift report and herd redeploy (PRD §8.5, §11).
|
|
92
|
+
|
|
93
|
+
**Open questions answered**
|
|
94
|
+
|
|
95
|
+
- **OQ1** — hold at `<2`; export the 2.x name. See above.
|
|
96
|
+
- **OQ2** — bearer only. The SDK already implements the OAuth resource-server
|
|
97
|
+
model and three servers pass `auth_server_provider=` / `auth=AuthSettings(...)`
|
|
98
|
+
to the constructor; `serve()` carries those through untouched. Reimplementing
|
|
99
|
+
OAuth here would be NG2 in a different costume and would leave two
|
|
100
|
+
implementations to reconcile instead of one.
|
|
101
|
+
- **OQ3** — `sentinel_push()` is **opt-in**, off unless `SENTINEL_URL` is set. A
|
|
102
|
+
shared library that starts emitting to a central collector the moment it is
|
|
103
|
+
installed is a surprise, and a fleet migration is the worst moment for one.
|
|
104
|
+
|
|
105
|
+
Still open: **OQ4** (`git_create_repo` in git-mcp) and **OQ5** (mnemos' duplicate
|
|
106
|
+
root `requirements.txt` declaration) are fleet questions, not library ones.
|
foxxe_mcp-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Todd McCaffrey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
foxxe_mcp-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: foxxe-mcp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Shared MCP plumbing for the FoxxeLabs server fleet: one SDK pin, one bootstrap, thin re-export.
|
|
5
|
+
Project-URL: Homepage, https://github.com/todd427/foxxe-mcp
|
|
6
|
+
Author: Todd McCaffrey
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: mcp<2,>=1.29.1
|
|
12
|
+
Requires-Dist: pydantic>=2.0
|
|
13
|
+
Requires-Dist: starlette>=0.36.0
|
|
14
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
18
|
+
Provides-Extra: fastmcp
|
|
19
|
+
Requires-Dist: fastmcp<3,>=2.14; extra == 'fastmcp'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# foxxe-mcp
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
See [docs/PRD-foxxe-mcp.md](docs/PRD-foxxe-mcp.md) for why. See [docs/migration.md](docs/migration.md) for how to convert a server.
|
|
27
|
+
|
|
28
|
+
**Status:** v0.1.0 — library built, fleet unmigrated.
|
|
29
|
+
|
|
30
|
+
## The shape
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from foxxe_mcp import MCPServer, serve
|
|
34
|
+
|
|
35
|
+
mcp = MCPServer("tomhas")
|
|
36
|
+
|
|
37
|
+
@mcp.tool()
|
|
38
|
+
def status() -> str:
|
|
39
|
+
"""Report the current run state."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
serve(mcp)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`MCPServer` is the SDK's own class, re-exported unwrapped. The tool decorator is
|
|
47
|
+
the SDK's, reached through the instance — foxxe-mcp never wraps the registration
|
|
48
|
+
API (PRD NG2), so every SDK feature is available the day it ships.
|
|
49
|
+
|
|
50
|
+
`serve()` owns what eleven servers currently hand-roll: transport, bind address,
|
|
51
|
+
allowed host, `/health`, `/version`, logging, graceful shutdown.
|
|
52
|
+
|
|
53
|
+
## Why the name is `MCPServer` on an SDK that calls it `FastMCP`
|
|
54
|
+
|
|
55
|
+
v0.1 pins `mcp>=1.29.1,<2`, where the class is `FastMCP`. The export is named
|
|
56
|
+
`MCPServer` anyway — that is the mcp 2.x name, and exporting it now means the
|
|
57
|
+
eventual pin flip changes this library and no server code at all.
|
|
58
|
+
|
|
59
|
+
That is a claim, so it is tested: `scripts/verify_sdk2.py` runs the same library
|
|
60
|
+
against `mcp==2.1.1` and checks that `MCPServer` resolves to the renamed class,
|
|
61
|
+
that a tool still registers, and that `/health`, `/version` and `/mcp` all still
|
|
62
|
+
answer. Both majors, one codebase.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ python scripts/verify_sdk2.py
|
|
66
|
+
foxxe-mcp 0.1.0 against mcp 2.1.1
|
|
67
|
+
PASS SDK major detected as 2 — mcp.server.mcpserver
|
|
68
|
+
PASS MCPServer resolves to the renamed class
|
|
69
|
+
...
|
|
70
|
+
All checks passed — the v1.0 pin flip is a one-line change.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## The bound is enforced twice
|
|
74
|
+
|
|
75
|
+
Once at install time, in `pyproject.toml`, and again at boot, in `check_sdk()`.
|
|
76
|
+
The second one is not redundant. PRD finding B: rialu declares no `mcp` at all,
|
|
77
|
+
only an unbounded `fastmcp`, and `fastmcp` 3.x pulls `mcp>=2` transitively — a
|
|
78
|
+
declaration that binds only at install time cannot catch what a resolver did to
|
|
79
|
+
a *transitive* dependency. `serve()` calls `check_sdk()` before the first tool
|
|
80
|
+
call, so a wrong resolution fails at boot, next to its cause.
|
|
81
|
+
|
|
82
|
+
`mcp` out of range is fatal. `fastmcp` out of range warns, and is fatal under
|
|
83
|
+
`FOXXE_MCP_STRICT_SDK=1`.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
foxxe-mcp==0.3.0
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Add the `fastmcp` extra only if the server actually uses the third-party
|
|
92
|
+
`fastmcp` package: `foxxe-mcp[fastmcp]==0.3.0`. Never declare `mcp` or
|
|
93
|
+
`fastmcp` directly in a server again — that is the whole point.
|
|
94
|
+
|
|
95
|
+
Published to PyPI rather than installed as a git dependency, which is a
|
|
96
|
+
deliberate amendment to PRD §7. The git-by-tag plan died on contact with the
|
|
97
|
+
first real deploy: this repo is private, so `pip install git+https://...`
|
|
98
|
+
inside a Docker build has no credentials and fails with exit 128. The
|
|
99
|
+
alternatives were a build-time PAT threaded through eleven Dockerfiles, or
|
|
100
|
+
making the repo public — which would publish `fleet.toml` and the internal
|
|
101
|
+
audit along with it. PyPI keeps the repo private, publishes only the package,
|
|
102
|
+
and makes every Dockerfile a plain `pip install` again. NG3 rules out a
|
|
103
|
+
*private* index; this is the public one.
|
|
104
|
+
|
|
105
|
+
## Fleet operations
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
foxxe-herd status # poll every server's /version — who is on what
|
|
109
|
+
foxxe-herd redeploy # dry run
|
|
110
|
+
foxxe-herd redeploy --yes # fly deploy every migrated server
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`fleet.toml` maps server name to Fly app name (verified against `flyer
|
|
114
|
+
apps_list`, not guessed — the `-foxxelabs` suffix is inconsistent) and records
|
|
115
|
+
which servers are migrated and which are still unbounded.
|
|
116
|
+
|
|
117
|
+
## Environment
|
|
118
|
+
|
|
119
|
+
| Variable | Default | Meaning |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `PORT` | `8080` | Port to bind |
|
|
122
|
+
| `BIND_HOST` | `0.0.0.0` | Bind address (Fly requires all interfaces) |
|
|
123
|
+
| `ALLOWED_HOST` | `$FLY_APP_NAME.fly.dev` | Public hostname for DNS-rebinding protection — **not** the bind address |
|
|
124
|
+
| `MCP_TRANSPORT` | `http` | `http` or `stdio` |
|
|
125
|
+
| `LOG_LEVEL` | `INFO` | Root log level |
|
|
126
|
+
| `LOG_FORMAT` | `json` | `text` for human-readable |
|
|
127
|
+
| `MCP_BEARER_TOKENS` | unset | Comma-separated; presence enables bearer auth |
|
|
128
|
+
| `FOXXE_MCP_STRICT_SDK` | unset | `1` makes an out-of-range `fastmcp` fatal |
|
|
129
|
+
| `TAISCE_SERVICE_TOKEN` | unset | Service token for `taisce_fetch()` |
|
|
130
|
+
| `SENTINEL_URL` | unset | Presence enables `sentinel_push()` |
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
136
|
+
.venv/bin/python -m pytest
|
|
137
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# foxxe-mcp
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
See [docs/PRD-foxxe-mcp.md](docs/PRD-foxxe-mcp.md) for why. See [docs/migration.md](docs/migration.md) for how to convert a server.
|
|
6
|
+
|
|
7
|
+
**Status:** v0.1.0 — library built, fleet unmigrated.
|
|
8
|
+
|
|
9
|
+
## The shape
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from foxxe_mcp import MCPServer, serve
|
|
13
|
+
|
|
14
|
+
mcp = MCPServer("tomhas")
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
def status() -> str:
|
|
18
|
+
"""Report the current run state."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
serve(mcp)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`MCPServer` is the SDK's own class, re-exported unwrapped. The tool decorator is
|
|
26
|
+
the SDK's, reached through the instance — foxxe-mcp never wraps the registration
|
|
27
|
+
API (PRD NG2), so every SDK feature is available the day it ships.
|
|
28
|
+
|
|
29
|
+
`serve()` owns what eleven servers currently hand-roll: transport, bind address,
|
|
30
|
+
allowed host, `/health`, `/version`, logging, graceful shutdown.
|
|
31
|
+
|
|
32
|
+
## Why the name is `MCPServer` on an SDK that calls it `FastMCP`
|
|
33
|
+
|
|
34
|
+
v0.1 pins `mcp>=1.29.1,<2`, where the class is `FastMCP`. The export is named
|
|
35
|
+
`MCPServer` anyway — that is the mcp 2.x name, and exporting it now means the
|
|
36
|
+
eventual pin flip changes this library and no server code at all.
|
|
37
|
+
|
|
38
|
+
That is a claim, so it is tested: `scripts/verify_sdk2.py` runs the same library
|
|
39
|
+
against `mcp==2.1.1` and checks that `MCPServer` resolves to the renamed class,
|
|
40
|
+
that a tool still registers, and that `/health`, `/version` and `/mcp` all still
|
|
41
|
+
answer. Both majors, one codebase.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
$ python scripts/verify_sdk2.py
|
|
45
|
+
foxxe-mcp 0.1.0 against mcp 2.1.1
|
|
46
|
+
PASS SDK major detected as 2 — mcp.server.mcpserver
|
|
47
|
+
PASS MCPServer resolves to the renamed class
|
|
48
|
+
...
|
|
49
|
+
All checks passed — the v1.0 pin flip is a one-line change.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## The bound is enforced twice
|
|
53
|
+
|
|
54
|
+
Once at install time, in `pyproject.toml`, and again at boot, in `check_sdk()`.
|
|
55
|
+
The second one is not redundant. PRD finding B: rialu declares no `mcp` at all,
|
|
56
|
+
only an unbounded `fastmcp`, and `fastmcp` 3.x pulls `mcp>=2` transitively — a
|
|
57
|
+
declaration that binds only at install time cannot catch what a resolver did to
|
|
58
|
+
a *transitive* dependency. `serve()` calls `check_sdk()` before the first tool
|
|
59
|
+
call, so a wrong resolution fails at boot, next to its cause.
|
|
60
|
+
|
|
61
|
+
`mcp` out of range is fatal. `fastmcp` out of range warns, and is fatal under
|
|
62
|
+
`FOXXE_MCP_STRICT_SDK=1`.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
foxxe-mcp==0.3.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Add the `fastmcp` extra only if the server actually uses the third-party
|
|
71
|
+
`fastmcp` package: `foxxe-mcp[fastmcp]==0.3.0`. Never declare `mcp` or
|
|
72
|
+
`fastmcp` directly in a server again — that is the whole point.
|
|
73
|
+
|
|
74
|
+
Published to PyPI rather than installed as a git dependency, which is a
|
|
75
|
+
deliberate amendment to PRD §7. The git-by-tag plan died on contact with the
|
|
76
|
+
first real deploy: this repo is private, so `pip install git+https://...`
|
|
77
|
+
inside a Docker build has no credentials and fails with exit 128. The
|
|
78
|
+
alternatives were a build-time PAT threaded through eleven Dockerfiles, or
|
|
79
|
+
making the repo public — which would publish `fleet.toml` and the internal
|
|
80
|
+
audit along with it. PyPI keeps the repo private, publishes only the package,
|
|
81
|
+
and makes every Dockerfile a plain `pip install` again. NG3 rules out a
|
|
82
|
+
*private* index; this is the public one.
|
|
83
|
+
|
|
84
|
+
## Fleet operations
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
foxxe-herd status # poll every server's /version — who is on what
|
|
88
|
+
foxxe-herd redeploy # dry run
|
|
89
|
+
foxxe-herd redeploy --yes # fly deploy every migrated server
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`fleet.toml` maps server name to Fly app name (verified against `flyer
|
|
93
|
+
apps_list`, not guessed — the `-foxxelabs` suffix is inconsistent) and records
|
|
94
|
+
which servers are migrated and which are still unbounded.
|
|
95
|
+
|
|
96
|
+
## Environment
|
|
97
|
+
|
|
98
|
+
| Variable | Default | Meaning |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `PORT` | `8080` | Port to bind |
|
|
101
|
+
| `BIND_HOST` | `0.0.0.0` | Bind address (Fly requires all interfaces) |
|
|
102
|
+
| `ALLOWED_HOST` | `$FLY_APP_NAME.fly.dev` | Public hostname for DNS-rebinding protection — **not** the bind address |
|
|
103
|
+
| `MCP_TRANSPORT` | `http` | `http` or `stdio` |
|
|
104
|
+
| `LOG_LEVEL` | `INFO` | Root log level |
|
|
105
|
+
| `LOG_FORMAT` | `json` | `text` for human-readable |
|
|
106
|
+
| `MCP_BEARER_TOKENS` | unset | Comma-separated; presence enables bearer auth |
|
|
107
|
+
| `FOXXE_MCP_STRICT_SDK` | unset | `1` makes an out-of-range `fastmcp` fatal |
|
|
108
|
+
| `TAISCE_SERVICE_TOKEN` | unset | Service token for `taisce_fetch()` |
|
|
109
|
+
| `SENTINEL_URL` | unset | Presence enables `sentinel_push()` |
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
115
|
+
.venv/bin/python -m pytest
|
|
116
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "foxxe-mcp"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Shared MCP plumbing for the FoxxeLabs server fleet: one SDK pin, one bootstrap, thin re-export."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Todd McCaffrey" }]
|
|
13
|
+
|
|
14
|
+
# ── The fleet's single SDK pin (PRD G1) ──────────────────────────────────────
|
|
15
|
+
# This is the ONLY place in the fleet that may depend on `mcp` or `fastmcp`.
|
|
16
|
+
# v0.1 holds at the 1.x API surface (PRD OQ1): the fleet is entirely pre-2.0,
|
|
17
|
+
# and converting plumbing and the tool-definition API in one step would double
|
|
18
|
+
# the blast radius of the ainm pilot. `foxxe_mcp.MCPServer` already exports the
|
|
19
|
+
# 2.x *name*, so the v1.0 pin flip is a lib bump, not a fleet-wide edit.
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=1.29.1,<2",
|
|
22
|
+
"starlette>=0.36.0",
|
|
23
|
+
"uvicorn>=0.27.0",
|
|
24
|
+
"httpx>=0.27.0",
|
|
25
|
+
"pydantic>=2.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
# PRD finding B: bounding one name is insufficient — rialu declares no `mcp` at
|
|
30
|
+
# all, only unbounded `fastmcp`, and fastmcp 3.x pulls mcp>=2 transitively.
|
|
31
|
+
# A server that needs the third-party fastmcp takes it from HERE, bounded, and
|
|
32
|
+
# `foxxe_mcp.check_sdk()` enforces the bound at runtime whether or not the
|
|
33
|
+
# resolver honoured it.
|
|
34
|
+
fastmcp = ["fastmcp>=2.14,<3"]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-asyncio>=0.23",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/todd427/foxxe-mcp"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
foxxe-herd = "foxxe_mcp.herd:main"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/foxxe_mcp"]
|
|
48
|
+
|
|
49
|
+
# The repo is private and the package is public, so the sdist is an explicit
|
|
50
|
+
# allowlist rather than "everything not gitignored". fleet.toml, docs/ and
|
|
51
|
+
# scripts/ carry internal topology — Fly app names, which servers are broken,
|
|
52
|
+
# the audit — and must not be published.
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = ["src/foxxe_mcp", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""foxxe-mcp — shared MCP plumbing for the FoxxeLabs fleet.
|
|
2
|
+
|
|
3
|
+
A choke point, not a framework (PRD §5). A server reads as if it were written
|
|
4
|
+
directly against the SDK, with two differences: the import comes from
|
|
5
|
+
``foxxe_mcp``, and ``serve()`` replaces the hand-rolled transport wiring::
|
|
6
|
+
|
|
7
|
+
from foxxe_mcp import MCPServer, serve
|
|
8
|
+
|
|
9
|
+
mcp = MCPServer("tomhas")
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
def status() -> str:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
serve(mcp)
|
|
17
|
+
|
|
18
|
+
``MCPServer`` is the SDK's own class, re-exported unwrapped. The tool
|
|
19
|
+
decorator is the SDK's, reached through the instance — foxxe-mcp never wraps
|
|
20
|
+
the registration API (NG2), so every SDK feature is available the day it ships,
|
|
21
|
+
with no wrapper lag.
|
|
22
|
+
|
|
23
|
+
The name ``MCPServer`` is the mcp 2.x name, exported regardless of which SDK
|
|
24
|
+
major is installed. On the current 1.x pin it is ``FastMCP`` underneath. That
|
|
25
|
+
is the whole trick: when the fleet moves to 2.x, the pin changes here and
|
|
26
|
+
server code does not change at all.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from ._sdk import (
|
|
32
|
+
FASTMCP_VERSION,
|
|
33
|
+
SDK_MAJOR,
|
|
34
|
+
SDK_MODULE,
|
|
35
|
+
SDK_VERSION,
|
|
36
|
+
Context,
|
|
37
|
+
FastMCP,
|
|
38
|
+
Image,
|
|
39
|
+
MCPServer,
|
|
40
|
+
SdkReport,
|
|
41
|
+
check_sdk,
|
|
42
|
+
)
|
|
43
|
+
from .auth import BearerAuthMiddleware, tokens_from_env
|
|
44
|
+
from .errors import ToolError, error_envelope, error_json, failing_to_json
|
|
45
|
+
from .health import health_route, version_payload, version_route
|
|
46
|
+
from .logging import setup_logging
|
|
47
|
+
from .secrets import TaisceError, taisce_fetch
|
|
48
|
+
from .serve import build_app, resolve_allowed_host, resolve_bind, serve
|
|
49
|
+
from .telemetry import sentinel_enabled, sentinel_push
|
|
50
|
+
from .version import __version__
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
# ── SDK re-exports (unwrapped) ──────────────────────────────────────────
|
|
54
|
+
"MCPServer",
|
|
55
|
+
"FastMCP",
|
|
56
|
+
"Context",
|
|
57
|
+
"Image",
|
|
58
|
+
# ── Bootstrap (PRD G2) ──────────────────────────────────────────────────
|
|
59
|
+
"serve",
|
|
60
|
+
"build_app",
|
|
61
|
+
"resolve_bind",
|
|
62
|
+
"resolve_allowed_host",
|
|
63
|
+
# ── Shared plumbing (PRD G3) ────────────────────────────────────────────
|
|
64
|
+
"BearerAuthMiddleware",
|
|
65
|
+
"tokens_from_env",
|
|
66
|
+
"ToolError",
|
|
67
|
+
"error_envelope",
|
|
68
|
+
"error_json",
|
|
69
|
+
"failing_to_json",
|
|
70
|
+
"setup_logging",
|
|
71
|
+
"taisce_fetch",
|
|
72
|
+
"TaisceError",
|
|
73
|
+
"sentinel_push",
|
|
74
|
+
"sentinel_enabled",
|
|
75
|
+
"health_route",
|
|
76
|
+
"version_route",
|
|
77
|
+
"version_payload",
|
|
78
|
+
# ── Drift visibility (PRD G4) ───────────────────────────────────────────
|
|
79
|
+
"__version__",
|
|
80
|
+
"SDK_VERSION",
|
|
81
|
+
"SDK_MAJOR",
|
|
82
|
+
"SDK_MODULE",
|
|
83
|
+
"FASTMCP_VERSION",
|
|
84
|
+
"SdkReport",
|
|
85
|
+
"check_sdk",
|
|
86
|
+
]
|