mcp-gtw 0.0.1__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.
Files changed (63) hide show
  1. mcp_gtw-0.0.1/.claude/settings.json +28 -0
  2. mcp_gtw-0.0.1/.dockerignore +16 -0
  3. mcp_gtw-0.0.1/.editorconfig +40 -0
  4. mcp_gtw-0.0.1/.env.example +36 -0
  5. mcp_gtw-0.0.1/.github/FUNDING.yml +2 -0
  6. mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  7. mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/other.md +10 -0
  9. mcp_gtw-0.0.1/.github/RELEASE-TEMPLATE.md +4 -0
  10. mcp_gtw-0.0.1/.github/workflows/ci.yml +26 -0
  11. mcp_gtw-0.0.1/.github/workflows/release.yml +34 -0
  12. mcp_gtw-0.0.1/.gitignore +57 -0
  13. mcp_gtw-0.0.1/.python-version +1 -0
  14. mcp_gtw-0.0.1/CLAUDE.md +149 -0
  15. mcp_gtw-0.0.1/Dockerfile +16 -0
  16. mcp_gtw-0.0.1/LICENSE.md +21 -0
  17. mcp_gtw-0.0.1/Makefile +33 -0
  18. mcp_gtw-0.0.1/PKG-INFO +139 -0
  19. mcp_gtw-0.0.1/README.md +116 -0
  20. mcp_gtw-0.0.1/docker-compose.yml +27 -0
  21. mcp_gtw-0.0.1/docs/admin.md +60 -0
  22. mcp_gtw-0.0.1/docs/architecture.md +107 -0
  23. mcp_gtw-0.0.1/docs/configuration.md +69 -0
  24. mcp_gtw-0.0.1/docs/deployment.md +115 -0
  25. mcp_gtw-0.0.1/docs/gateway-library.md +164 -0
  26. mcp_gtw-0.0.1/docs/mcp-clients.md +169 -0
  27. mcp_gtw-0.0.1/docs/provider-protocol.md +225 -0
  28. mcp_gtw-0.0.1/docs/provider-sdk.md +228 -0
  29. mcp_gtw-0.0.1/docs/quickstart.md +66 -0
  30. mcp_gtw-0.0.1/docs/security.md +146 -0
  31. mcp_gtw-0.0.1/docs/testing.md +52 -0
  32. mcp_gtw-0.0.1/extras/images/banner.png +0 -0
  33. mcp_gtw-0.0.1/extras/images/logo-icon.png +0 -0
  34. mcp_gtw-0.0.1/extras/images/logo-icon.svg +20 -0
  35. mcp_gtw-0.0.1/extras/images/logo.png +0 -0
  36. mcp_gtw-0.0.1/nginx.conf +96 -0
  37. mcp_gtw-0.0.1/pyproject.toml +63 -0
  38. mcp_gtw-0.0.1/src/mcp_gtw/__init__.py +0 -0
  39. mcp_gtw-0.0.1/src/mcp_gtw/channel.py +752 -0
  40. mcp_gtw-0.0.1/src/mcp_gtw/config.py +65 -0
  41. mcp_gtw-0.0.1/src/mcp_gtw/errors.py +26 -0
  42. mcp_gtw-0.0.1/src/mcp_gtw/gateway.py +414 -0
  43. mcp_gtw-0.0.1/src/mcp_gtw/helpers/__init__.py +0 -0
  44. mcp_gtw-0.0.1/src/mcp_gtw/helpers/security.py +29 -0
  45. mcp_gtw-0.0.1/src/mcp_gtw/listeners.py +23 -0
  46. mcp_gtw-0.0.1/src/mcp_gtw/main.py +31 -0
  47. mcp_gtw-0.0.1/src/mcp_gtw/protocol.py +117 -0
  48. mcp_gtw-0.0.1/src/mcp_gtw/py.typed +0 -0
  49. mcp_gtw-0.0.1/src/mcp_gtw/registry.py +185 -0
  50. mcp_gtw-0.0.1/src/mcp_gtw/web/admin.html +142 -0
  51. mcp_gtw-0.0.1/src/mcp_gtw/web/index.html +26 -0
  52. mcp_gtw-0.0.1/src/mcp_gtw/web/logo.svg +20 -0
  53. mcp_gtw-0.0.1/tests/conftest.py +36 -0
  54. mcp_gtw-0.0.1/tests/gateway/test_channel.py +1141 -0
  55. mcp_gtw-0.0.1/tests/gateway/test_config.py +37 -0
  56. mcp_gtw-0.0.1/tests/gateway/test_gateway.py +539 -0
  57. mcp_gtw-0.0.1/tests/gateway/test_listeners.py +20 -0
  58. mcp_gtw-0.0.1/tests/gateway/test_protocol.py +48 -0
  59. mcp_gtw-0.0.1/tests/gateway/test_registry.py +238 -0
  60. mcp_gtw-0.0.1/tests/gateway/test_security.py +33 -0
  61. mcp_gtw-0.0.1/tests/support.py +26 -0
  62. mcp_gtw-0.0.1/tests/test_entrypoints.py +34 -0
  63. mcp_gtw-0.0.1/uv.lock +1167 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(~/Developer/workspaces/**)",
5
+ "Write(~/Developer/workspaces/**)",
6
+ "Edit(~/Developer/workspaces/**)",
7
+ "MultiEdit(~/Developer/workspaces/**)",
8
+ "Glob(~/Developer/workspaces/**)",
9
+ "Grep(~/Developer/workspaces/**)",
10
+ "LS(~/Developer/workspaces/**)",
11
+
12
+ "Bash(*)",
13
+
14
+ "WebFetch(*)",
15
+ "WebSearch",
16
+
17
+ "mcp__my-gateway"
18
+ ],
19
+ "deny": [
20
+ "Read(~/Developer/workspaces/**/.env)",
21
+ "Read(~/Developer/workspaces/**/secrets/**)",
22
+ "Read(~/Developer/workspaces/**/*.pem)",
23
+ "Read(~/Developer/workspaces/**/*.key)",
24
+ "Bash(rm -rf *)",
25
+ "Bash(sudo rm *)"
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ .env
2
+ .env.*
3
+ .venv/
4
+ __pycache__/
5
+ *.pyc
6
+ .git/
7
+ .github/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ coverage.xml
15
+ .coverage
16
+ .DS_Store
@@ -0,0 +1,40 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ charset = utf-8
6
+ insert_final_newline = true
7
+ indent_style = space
8
+ indent_size = 4
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.html]
12
+ indent_size = 4
13
+
14
+ [*.json]
15
+ indent_size = 2
16
+ insert_final_newline = false
17
+
18
+ [{*[Mm]akefile*,*.mak,*.mk,depend}]
19
+ indent_style = tab
20
+
21
+ [*.{bat, cmd, cmd.*}]
22
+ end_of_line = crlf
23
+
24
+ [*.{yml, yaml}]
25
+ indent_size = 2
26
+
27
+ [*.gemspec]
28
+ indent_size = 2
29
+
30
+ [*.rb,Fastfile,Gemfile,Brewfile,Podfile]
31
+ indent_size = 2
32
+
33
+ [*.{kt,kts}]
34
+ ktlint_standard_argument-list-wrapping = disabled
35
+ ktlint_standard_trailing-comma-on-call-site = disabled
36
+ ktlint_standard_trailing-comma-on-declaration-site = disabled
37
+
38
+ [*.dart]
39
+ indent_size = 2
40
+ indent_brace_style = K&R
@@ -0,0 +1,36 @@
1
+ # Copy to .env and override any GATEWAY_* setting. See docs/configuration.md.
2
+ # List values are comma separated.
3
+
4
+ # GATEWAY_APP_NAME=MCP Gateway
5
+ # GATEWAY_APP_VERSION is derived from the installed package version.
6
+
7
+ # Bind address for the bundled runner (python -m mcp_gateway.main).
8
+ # GATEWAY_HOST=127.0.0.1
9
+ # GATEWAY_PORT=8000
10
+
11
+ # Origins allowed to open the private provider WebSocket.
12
+ GATEWAY_ALLOWED_PROVIDER_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
13
+ # Origins allowed on /mcp. Empty accepts native clients that send no Origin.
14
+ GATEWAY_ALLOWED_MCP_ORIGINS=
15
+ # Origins allowed by CORS on the HTTP endpoints.
16
+ GATEWAY_CORS_ALLOW_ORIGINS=*
17
+
18
+ # GATEWAY_TOOL_CALL_TIMEOUT_SECONDS=60
19
+ # GATEWAY_MAXIMUM_TOOLS=128
20
+ # GATEWAY_MAXIMUM_TOOL_DEFINITION_BYTES=65536
21
+ # GATEWAY_MAXIMUM_WEBSOCKET_MESSAGE_BYTES=524288
22
+ # GATEWAY_MAXIMUM_JSON_DEPTH=100
23
+ # GATEWAY_MAXIMUM_PENDING_CALLS_PER_CHANNEL=64
24
+ # GATEWAY_MAXIMUM_MCP_SESSIONS_PER_CHANNEL=16
25
+ # GATEWAY_MAXIMUM_CHANNELS=10000
26
+ # Cap simultaneous connections at the server. Empty means no app-level cap.
27
+ # GATEWAY_MAXIMUM_CONCURRENT_CONNECTIONS=
28
+ # GATEWAY_OFFLINE_TTL_SECONDS=300
29
+ # GATEWAY_REAPER_INTERVAL_SECONDS=30
30
+
31
+ # GATEWAY_MCP_JSON_RESPONSE=false
32
+ # GATEWAY_MCP_STATELESS=false
33
+ # GATEWAY_MCP_SESSION_IDLE_TIMEOUT_SECONDS=900
34
+
35
+ # GATEWAY_ADMIN_ENABLED=false
36
+ # GATEWAY_ADMIN_KEY=
@@ -0,0 +1,2 @@
1
+ github: paulocoutinhox
2
+ ko_fi: paulocoutinho
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+
16
+ 1. Go to '...'
17
+ 2. Execute on terminal '....'
18
+ 3. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **System (please complete the following information):**
27
+ - OS: [e.g. Ubuntu]
28
+ - Python [e.g. 3.10]
29
+
30
+ **Additional context**
31
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: Other type
3
+ about: Other issue type not related to bug or feature.
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+
@@ -0,0 +1,4 @@
1
+ # Announcements
2
+ * First announcement
3
+
4
+ # Changes
@@ -0,0 +1,26 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ python:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.12", "3.13", "3.14"]
15
+ env:
16
+ UV_PYTHON: ${{ matrix.python-version }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Install uv
20
+ run: pip install uv
21
+ - name: Create the environment
22
+ run: make install
23
+ - name: Lint
24
+ run: make lint
25
+ - name: Coverage gate
26
+ run: make coverage
@@ -0,0 +1,34 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - name: Install uv
17
+ run: pip install uv
18
+ - name: Verify the tag matches the package version
19
+ run: |
20
+ VERSION=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
21
+ if [ "${GITHUB_REF_NAME#v}" != "$VERSION" ]; then
22
+ echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${VERSION}"
23
+ exit 1
24
+ fi
25
+ - name: Install
26
+ run: make install
27
+ - name: Lint
28
+ run: make lint
29
+ - name: Coverage gate
30
+ run: make coverage
31
+ - name: Build
32
+ run: uv build
33
+ - name: Publish to PyPI
34
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,57 @@
1
+ # os
2
+ Thumbs.db
3
+ .DS_Store
4
+
5
+ # ide
6
+ .idea
7
+ .vscode
8
+ .iml
9
+
10
+ # auto generated
11
+ **/*.pyc
12
+ *.autosave
13
+ *.user
14
+ *.user.*
15
+ .vs
16
+ __pycache__
17
+ Brewfile.lock.json
18
+ .metals
19
+ .ruff_cache
20
+
21
+ # node
22
+ node_modules
23
+ package-lock.json
24
+
25
+ # cmake
26
+ CMakeUserPresets.json
27
+ CMakeSettings.json
28
+ CMakeLists.txt.user
29
+
30
+ # java/kotlin
31
+ .gradle
32
+
33
+ # cxx
34
+ .cxx
35
+
36
+ # flutter
37
+ *.reflectable.dart
38
+
39
+ # postgres
40
+ postgres-data
41
+
42
+ # python
43
+ *.egg-info
44
+ .pytest_cache
45
+ poetry.lock
46
+ .venv
47
+ /htmlcov
48
+
49
+ # app
50
+ *.db
51
+ .coverage
52
+ coverage.xml
53
+ .fastapicloud
54
+ data/
55
+
56
+ # local secrets (keep .env.example tracked)
57
+ .env
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,149 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for working in this repository.
4
+
5
+ ## How to use this file
6
+
7
+ **CLAUDE.md is a map, not a copy.** Each subject gets a one-line essence here and a pointer to the
8
+ `docs/*.md` that owns the full detail. Never duplicate doc content into this file — when the code
9
+ changes, update the doc and keep the pointer accurate. The docs are the source of truth for
10
+ behaviour. This file is the source of truth for the conventions and repo mechanics that have no doc
11
+ (module layout, code style, commands, gotchas). If a subject has a doc, read the doc.
12
+
13
+ ## What this is
14
+
15
+ `mcp-gtw` is a generic, installable [Model Context Protocol](https://modelcontextprotocol.io)
16
+ gateway. It publishes a real MCP endpoint over Streamable HTTP whose capabilities are **registered
17
+ and executed by connected providers** (a browser app or any WebSocket client) over a private
18
+ WebSocket. The gateway never knows them in advance — a provider connects, publishes its own MCP
19
+ tools, resources, resource templates and prompts, and runs their handlers; it can also serve
20
+ completion, logging, progress, and reverse calls (sampling, elicitation). The gateway only registers,
21
+ publishes and routes.
22
+
23
+ It is a **library**: you build a real app by subclassing `Gateway`, never by editing this package.
24
+ This repo is published to PyPI as `mcp-gtw` (the import package stays `mcp_gtw`).
25
+
26
+ ## Subjects (essence + where the detail lives)
27
+
28
+ - **How it works / architecture** — public transport `MCP Client ⇄ Streamable HTTP ⇄ /mcp`. Private
29
+ transport `Gateway ⇄ WebSocket ⇄ provider (/provider)`. A **channel** is one session (one provider,
30
+ its registries, its pending calls) holding two tokens: `mcp_token` (client) and `provider_token`
31
+ (provider). One MCP `Server` + one `StreamableHTTPSessionManager` serve every channel, resolved per
32
+ request by Bearer token on the ASGI scope, with per-channel input validation. Full flows and
33
+ diagrams: [docs/architecture.md](docs/architecture.md).
34
+ - **Provider protocol** — a JSON-RPC-shaped relay of MCP: gateway→provider `request`/`cancel`/
35
+ `response`, provider→gateway `register`/`result`/`call`/`notify`, plus `hello.ack`/`ack`/`ping`/
36
+ `pong`/`protocol.error`, version `mcp-gtw-provider/1`. See
37
+ [docs/provider-protocol.md](docs/provider-protocol.md).
38
+ - **Provider SDK** — the JavaScript side is the [`mcp-gtw-provider`](https://github.com/mcp-gtw/mcp-gtw-provider)
39
+ npm package (`registerTool`/`registerResource`/`registerResourceTemplate`/`registerPrompt`,
40
+ `onComplete`/`onSubscribe`, `notifyResourceUpdated`, `log`, `requestSampling`/`requestElicit`).
41
+ Usage guide in [docs/provider-sdk.md](docs/provider-sdk.md).
42
+ - **Extending** — subclass `Gateway`, override the hooks, swap `channel_class`/`registry_class`/
43
+ `settings_class`. Every override point: [docs/gateway-library.md](docs/gateway-library.md).
44
+ - **Configuration** — all `GATEWAY_*` env vars (or `.env`) map to `GatewaySettings`. Lists are comma
45
+ separated (`NoDecode`, not JSON). Numeric limits/timeouts are validated positive and `port` is
46
+ bounded, so a bad value fails at startup. Table: [docs/configuration.md](docs/configuration.md).
47
+ - **Runtime, limits & performance** — everything runs on one event loop and is fully async, with no
48
+ blocking IO on request/websocket paths. Tool validators are compiled once at registration, not per
49
+ call. A channel holds at most one live provider socket (a new connection atomically replaces the
50
+ old); channels are capped by `maximum_channels` (checked before allocation); every externally-fed
51
+ collection (pending calls, remembered sessions, tools) is bounded. The bundled runner
52
+ (`python -m mcp_gtw.main`) sets the transport frame limit (`ws_max_size`) to
53
+ `maximum_websocket_message_bytes` and applies `maximum_concurrent_connections` — so run it that way
54
+ in production, not bare `uvicorn`. Details: [docs/security.md](docs/security.md).
55
+ - **Admin dashboard** — off by default. `GATEWAY_ADMIN_ENABLED=true` registers `/admin` and
56
+ `/admin/stats`, gated by `GATEWAY_ADMIN_KEY` (required — enabling admin without it raises
57
+ `GatewayConfigurationError` at construction). Details: [docs/admin.md](docs/admin.md).
58
+ - **Security** — trust boundaries, the two tokens, origin checks, WebSocket robustness, resource
59
+ limits: [docs/security.md](docs/security.md).
60
+ - **Quickstart / MCP clients / testing / deployment** — [docs/quickstart.md](docs/quickstart.md),
61
+ [docs/mcp-clients.md](docs/mcp-clients.md), [docs/testing.md](docs/testing.md),
62
+ [docs/deployment.md](docs/deployment.md).
63
+
64
+ ## Key modules (`src/mcp_gtw/`)
65
+
66
+ - `gateway.py` — the `Gateway` class: FastAPI app factory, CORS, routes (`/mcp`, `/provider`,
67
+ `/health`, `/`, `/logo.svg`, optional `/admin`), the `/mcp` ASGI wrapper, the `/provider` websocket
68
+ pump, the admin dashboard, and the lifespan (session manager + reaper + `serve`).
69
+ - `channel.py` — `Channel`: attach/detach a provider, compile/replace the registries (tools,
70
+ resources, resource templates, prompts), relay a request to the provider (correlate a `Future` over
71
+ the WebSocket with timeout), run reverse `call`s against the client, fan out notifications
72
+ (progress, logging, resource-updated), validate output, notify MCP sessions.
73
+ - `registry.py` — `ChannelRegistry`: create/remove/resolve channels, expiry, `admin_channels`.
74
+ - `protocol.py` — private message builders and `decode_message` (depth-bounded JSON parse).
75
+ - `config.py` — `GatewaySettings` (pydantic-settings, env prefix `GATEWAY_`).
76
+ - `helpers/security.py` — token generation, bearer extraction, origin check, constant-time compare.
77
+ - `listeners.py` — `GatewayListener` hook interface that `Gateway` implements.
78
+
79
+ ## Module organization
80
+
81
+ - One public class per module, plus its small private support types (`channel.py` keeps
82
+ `CompiledTool`, `PendingRequest`, `JsonWebSocket`).
83
+ - `errors.py` is the exceptions module (the whole hierarchy lives there).
84
+ - Pure utility functions live under `helpers/`.
85
+ - Every `__init__.py` is empty, so import from the concrete submodule.
86
+
87
+ ## Conventions
88
+
89
+ - Managed with `uv`. Ruff with `line-length = 100`, formatter is the source of truth.
90
+ - **100% branch coverage is a hard gate** (`fail_under = 100`). Every change keeps it at 100%.
91
+ - `__init__.py` files are **empty** — import from submodules (`from mcp_gtw.gateway import Gateway`).
92
+ - Code and comments are in **English**. Comments are **rare** — only for genuinely non-obvious intent.
93
+ No narrating comments, no artificial section separators, no semicolons splitting sentences.
94
+ - **Separate blocks with a blank line.** A compound block (`if`/`for`/`while`/`try`/`with`/`def`/
95
+ `class`) gets a blank line between it and the adjacent statement, so each block reads as its own
96
+ unit. Never stack blocks directly on top of each other.
97
+ - **No legacy, no back-compat, no fallbacks.** Build the final version and refactor freely. Do not add
98
+ checks that only exist because something used to be different.
99
+ - Prefer single-line signatures and calls where they fit the line length.
100
+
101
+ ## Supported Python
102
+
103
+ - `requires-python = ">=3.12"`. Supported versions are **3.12, 3.13, 3.14**.
104
+ - `.python-version` pins **3.12** for local `uv` and the Docker image (`python:3.12-slim`).
105
+ - CI (`.github/workflows/ci.yml`) runs the full `make install` + `make lint` + `make coverage` matrix
106
+ across all three versions; `UV_PYTHON` selects the interpreter per matrix leg.
107
+
108
+ ## Commands
109
+
110
+ ```bash
111
+ make install # uv sync --extra dev
112
+ make lint # ruff check + ruff format --check
113
+ make format # apply formatting and safe fixes
114
+ make test # pytest
115
+ make coverage # pytest behind the 100% gate
116
+ make version v=X.Y.Z # rewrite the pyproject.toml version (validates semver)
117
+ make build # uv build (wheel + sdist)
118
+ make run # serve the bare gateway on 127.0.0.1:8000
119
+ ```
120
+
121
+ ## Versioning and releasing
122
+
123
+ `pyproject.toml` is the single source of the version: `GatewaySettings.app_version` reads it back at
124
+ runtime through `importlib.metadata.version("mcp-gtw")`, so never hardcode a version elsewhere.
125
+ Bump with `make version v=X.Y.Z` (semver `X.Y.Z`, validated), then push a matching `v<version>` tag.
126
+ `.github/workflows/release.yml` verifies the tag equals the `pyproject.toml` version, runs lint + the
127
+ coverage gate, builds, and `uv publish --trusted-publishing always`.
128
+
129
+ Publishing uses PyPI **Trusted Publishing** (OIDC) — no `PYPI_API_TOKEN` secret. The workflow grants
130
+ `id-token: write`. PyPI supports a **pending publisher**, so configure it before the first release
131
+ (pypi.org → Account → Publishing → Add a pending publisher): PyPI project `mcp-gtw`, owner
132
+ `mcp-gtw`, repository `mcp-gtw`, workflow `release.yml`. Pushing the first `v<version>` tag then
133
+ creates the project token-lessly.
134
+
135
+ ## Documentation policy
136
+
137
+ Docs must stay consistent with the code. When you change a setting, default, endpoint, module path,
138
+ or behaviour, update the affected `docs/*.md`, `README.md`, and the pointer in this file in the same
139
+ change. Treat a doc that describes something the code no longer does as a bug.
140
+
141
+ ## Gotchas
142
+
143
+ - `.env` and `.env.example` are under a hard permission deny rule here (Read, Edit, Bash, and Write
144
+ all fail). The file is effectively immutable in this environment — make `config.py` and
145
+ `docs/configuration.md` the source of truth and flag any drift to the user.
146
+ - The home page reads `web/index.html` at import and `str.format`s `{name}`/`{initial}` — keep that
147
+ file free of other `{`/`}`. `web/admin.html` is served raw (its JS braces are fine).
148
+ - Package data (`web/*.html`, `web/logo.svg`, `py.typed`) must ship in the wheel — hatchling includes
149
+ everything under the package.
@@ -0,0 +1,16 @@
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ GATEWAY_HOST=0.0.0.0
6
+
7
+ WORKDIR /srv
8
+
9
+ COPY pyproject.toml README.md ./
10
+ COPY src ./src
11
+
12
+ RUN pip install --no-cache-dir .
13
+
14
+ EXPOSE 8000
15
+
16
+ CMD ["python", "-m", "mcp_gtw.main"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paulo Coutinho
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.
mcp_gtw-0.0.1/Makefile ADDED
@@ -0,0 +1,33 @@
1
+ .DEFAULT_GOAL := help
2
+ .PHONY: help install lint format test coverage version build run
3
+
4
+ help: ## Show this help
5
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "} {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'
6
+
7
+ install: ## Install the environment with dev dependencies
8
+ uv sync --extra dev
9
+
10
+ lint: ## Check linting and formatting
11
+ uv run ruff check .
12
+ uv run ruff format --check .
13
+
14
+ format: ## Apply formatting and safe lint fixes
15
+ uv run ruff format .
16
+ uv run ruff check --fix .
17
+
18
+ test: ## Run the test suite
19
+ uv run pytest -q
20
+
21
+ coverage: ## Run the test suite with the coverage gate
22
+ uv run pytest -q --cov --cov-report=term-missing --cov-report=xml
23
+
24
+ version: ## Set the release version, e.g. make version v=1.2.3
25
+ @echo "$(v)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || { echo "usage: make version v=X.Y.Z"; exit 1; }
26
+ @sed -i.bak -E 's/^version = ".*"/version = "$(v)"/' pyproject.toml && rm -f pyproject.toml.bak
27
+ @echo "version set to $(v)"
28
+
29
+ build: ## Build the wheel and sdist for PyPI
30
+ uv build
31
+
32
+ run: ## Serve the bare generic gateway on 127.0.0.1:8000
33
+ uv run python -m mcp_gtw.main