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.
- mcp_gtw-0.0.1/.claude/settings.json +28 -0
- mcp_gtw-0.0.1/.dockerignore +16 -0
- mcp_gtw-0.0.1/.editorconfig +40 -0
- mcp_gtw-0.0.1/.env.example +36 -0
- mcp_gtw-0.0.1/.github/FUNDING.yml +2 -0
- mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- mcp_gtw-0.0.1/.github/ISSUE_TEMPLATE/other.md +10 -0
- mcp_gtw-0.0.1/.github/RELEASE-TEMPLATE.md +4 -0
- mcp_gtw-0.0.1/.github/workflows/ci.yml +26 -0
- mcp_gtw-0.0.1/.github/workflows/release.yml +34 -0
- mcp_gtw-0.0.1/.gitignore +57 -0
- mcp_gtw-0.0.1/.python-version +1 -0
- mcp_gtw-0.0.1/CLAUDE.md +149 -0
- mcp_gtw-0.0.1/Dockerfile +16 -0
- mcp_gtw-0.0.1/LICENSE.md +21 -0
- mcp_gtw-0.0.1/Makefile +33 -0
- mcp_gtw-0.0.1/PKG-INFO +139 -0
- mcp_gtw-0.0.1/README.md +116 -0
- mcp_gtw-0.0.1/docker-compose.yml +27 -0
- mcp_gtw-0.0.1/docs/admin.md +60 -0
- mcp_gtw-0.0.1/docs/architecture.md +107 -0
- mcp_gtw-0.0.1/docs/configuration.md +69 -0
- mcp_gtw-0.0.1/docs/deployment.md +115 -0
- mcp_gtw-0.0.1/docs/gateway-library.md +164 -0
- mcp_gtw-0.0.1/docs/mcp-clients.md +169 -0
- mcp_gtw-0.0.1/docs/provider-protocol.md +225 -0
- mcp_gtw-0.0.1/docs/provider-sdk.md +228 -0
- mcp_gtw-0.0.1/docs/quickstart.md +66 -0
- mcp_gtw-0.0.1/docs/security.md +146 -0
- mcp_gtw-0.0.1/docs/testing.md +52 -0
- mcp_gtw-0.0.1/extras/images/banner.png +0 -0
- mcp_gtw-0.0.1/extras/images/logo-icon.png +0 -0
- mcp_gtw-0.0.1/extras/images/logo-icon.svg +20 -0
- mcp_gtw-0.0.1/extras/images/logo.png +0 -0
- mcp_gtw-0.0.1/nginx.conf +96 -0
- mcp_gtw-0.0.1/pyproject.toml +63 -0
- mcp_gtw-0.0.1/src/mcp_gtw/__init__.py +0 -0
- mcp_gtw-0.0.1/src/mcp_gtw/channel.py +752 -0
- mcp_gtw-0.0.1/src/mcp_gtw/config.py +65 -0
- mcp_gtw-0.0.1/src/mcp_gtw/errors.py +26 -0
- mcp_gtw-0.0.1/src/mcp_gtw/gateway.py +414 -0
- mcp_gtw-0.0.1/src/mcp_gtw/helpers/__init__.py +0 -0
- mcp_gtw-0.0.1/src/mcp_gtw/helpers/security.py +29 -0
- mcp_gtw-0.0.1/src/mcp_gtw/listeners.py +23 -0
- mcp_gtw-0.0.1/src/mcp_gtw/main.py +31 -0
- mcp_gtw-0.0.1/src/mcp_gtw/protocol.py +117 -0
- mcp_gtw-0.0.1/src/mcp_gtw/py.typed +0 -0
- mcp_gtw-0.0.1/src/mcp_gtw/registry.py +185 -0
- mcp_gtw-0.0.1/src/mcp_gtw/web/admin.html +142 -0
- mcp_gtw-0.0.1/src/mcp_gtw/web/index.html +26 -0
- mcp_gtw-0.0.1/src/mcp_gtw/web/logo.svg +20 -0
- mcp_gtw-0.0.1/tests/conftest.py +36 -0
- mcp_gtw-0.0.1/tests/gateway/test_channel.py +1141 -0
- mcp_gtw-0.0.1/tests/gateway/test_config.py +37 -0
- mcp_gtw-0.0.1/tests/gateway/test_gateway.py +539 -0
- mcp_gtw-0.0.1/tests/gateway/test_listeners.py +20 -0
- mcp_gtw-0.0.1/tests/gateway/test_protocol.py +48 -0
- mcp_gtw-0.0.1/tests/gateway/test_registry.py +238 -0
- mcp_gtw-0.0.1/tests/gateway/test_security.py +33 -0
- mcp_gtw-0.0.1/tests/support.py +26 -0
- mcp_gtw-0.0.1/tests/test_entrypoints.py +34 -0
- 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,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,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,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
|
mcp_gtw-0.0.1/.gitignore
ADDED
|
@@ -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
|
mcp_gtw-0.0.1/CLAUDE.md
ADDED
|
@@ -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.
|
mcp_gtw-0.0.1/Dockerfile
ADDED
|
@@ -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"]
|
mcp_gtw-0.0.1/LICENSE.md
ADDED
|
@@ -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
|