mcp-audprobe 1.0.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.
- mcp_audprobe-1.0.0/LICENSE +21 -0
- mcp_audprobe-1.0.0/PKG-INFO +268 -0
- mcp_audprobe-1.0.0/README.md +222 -0
- mcp_audprobe-1.0.0/pyproject.toml +71 -0
- mcp_audprobe-1.0.0/setup.cfg +4 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/__init__.py +3 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/__main__.py +3 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/authserver.py +47 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/checks.py +133 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/cli.py +101 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/probe.py +128 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/report.py +79 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/safety.py +36 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe/tokens.py +100 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/PKG-INFO +268 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/SOURCES.txt +23 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/dependency_links.txt +1 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/entry_points.txt +2 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/requires.txt +16 -0
- mcp_audprobe-1.0.0/src/mcp_audprobe.egg-info/top_level.txt +1 -0
- mcp_audprobe-1.0.0/tests/test_checks.py +66 -0
- mcp_audprobe-1.0.0/tests/test_e2e.py +165 -0
- mcp_audprobe-1.0.0/tests/test_probe.py +117 -0
- mcp_audprobe-1.0.0/tests/test_safety.py +29 -0
- mcp_audprobe-1.0.0/tests/test_tokens.py +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mustafa ÖZTÜRK
|
|
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.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-audprobe
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Actively test whether an MCP server you own validates bearer-token audience (aud), issuer (iss) and expiry (exp).
|
|
5
|
+
Author: Mustafa ÖZTÜRK
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bymustfa/mcp-audprobe
|
|
8
|
+
Project-URL: Repository, https://github.com/bymustfa/mcp-audprobe
|
|
9
|
+
Project-URL: Issues, https://github.com/bymustfa/mcp-audprobe/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/bymustfa/mcp-audprobe/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/bymustfa/mcp-audprobe#readme
|
|
12
|
+
Project-URL: Security Policy, https://github.com/bymustfa/mcp-audprobe/security/policy
|
|
13
|
+
Keywords: mcp,model-context-protocol,oauth,jwt,audience,aud,security,security-testing,bearer-token,authorization,github-action,ci
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Security
|
|
26
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
27
|
+
Classifier: Topic :: Software Development :: Testing
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: httpx>=0.27
|
|
32
|
+
Requires-Dist: PyJWT[crypto]>=2.8
|
|
33
|
+
Requires-Dist: cryptography>=42
|
|
34
|
+
Requires-Dist: starlette>=0.37
|
|
35
|
+
Requires-Dist: uvicorn>=0.30
|
|
36
|
+
Requires-Dist: rich>=13
|
|
37
|
+
Provides-Extra: demo
|
|
38
|
+
Requires-Dist: mcp<2,>=1.30.0; extra == "demo"
|
|
39
|
+
Requires-Dist: pillow>=10; extra == "demo"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
42
|
+
Requires-Dist: mcp<2,>=1.30.0; extra == "test"
|
|
43
|
+
Requires-Dist: pillow>=10; extra == "test"
|
|
44
|
+
Requires-Dist: ruff>=0.5; extra == "test"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
[**English**](https://github.com/bymustfa/mcp-audprobe/blob/main/README.md) | [Türkçe](https://github.com/bymustfa/mcp-audprobe/blob/main/README.tr.md)
|
|
48
|
+
|
|
49
|
+
[](https://github.com/bymustfa/mcp-audprobe/actions/workflows/ci.yml) [](https://pypi.org/project/mcp-audprobe/) [](https://github.com/bymustfa/mcp-audprobe/actions/workflows/codeql.yml) [](https://scorecard.dev/viewer/?uri=github.com/bymustfa/mcp-audprobe) [](https://github.com/bymustfa/mcp-audprobe/blob/main/LICENSE)
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
# mcp-audprobe
|
|
54
|
+
|
|
55
|
+
Actively test whether an **MCP server you own** really validates the bearer token's audience (`aud`),
|
|
56
|
+
issuer (`iss`) and expiry (`exp`). It mints test tokens with a local fake authorization server key,
|
|
57
|
+
sends them to your server, and reports **FAIL** if a token that must be rejected is accepted.
|
|
58
|
+
|
|
59
|
+
> ## ⚠️ Only test servers you own or are authorized to test
|
|
60
|
+
> `mcp-audprobe` sends authenticated requests with deliberately invalid tokens. Use it **only** against
|
|
61
|
+
> servers you operate or have explicit written permission to test. By default the CLI refuses any target
|
|
62
|
+
> that is not on loopback (`localhost`, `127.0.0.0/8`, `::1`); `--allow-remote` exists solely for your own
|
|
63
|
+
> staging/test deployments. Never point it at someone else's server. The demo and test-suite only talk to
|
|
64
|
+
> servers they start on `127.0.0.1`.
|
|
65
|
+
|
|
66
|
+
## The problem
|
|
67
|
+
|
|
68
|
+
A resource server must accept only tokens issued *for it*. Two recent advisories show servers that checked
|
|
69
|
+
signature, issuer and expiry but not the audience, so a valid token for a *different* service of the same
|
|
70
|
+
authorization server was accepted:
|
|
71
|
+
|
|
72
|
+
- MCP Python SDK: [GHSA-w4fh-qvv9-3v23](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-w4fh-qvv9-3v23)
|
|
73
|
+
(CVE-2026-116684, medium). Affected `mcp` 1.10.0–1.29.1 and 2.0.0–2.1.1; fixed in 1.30.0 / 2.2.0.
|
|
74
|
+
**Upgrading alone changes nothing**: the patched versions add `AuthSettings(validate_token_resource=...)`,
|
|
75
|
+
which is off unless set. To enable the check you must (1) set `validate_token_resource=True` with
|
|
76
|
+
`resource_server_url` = the audience your authorization server puts in tokens for your server, **and**
|
|
77
|
+
(2) return the token's audience from `TokenVerifier.verify_token()` as `AccessToken.resource`. With only
|
|
78
|
+
the first change every request gets 401. (Confirmed against the advisory text and `mcp` 1.30.0 source:
|
|
79
|
+
`mcp/server/auth/settings.py`, `mcp/server/auth/middleware/bearer_auth.py`.)
|
|
80
|
+
- AWS API MCP Server: [GHSA-4prq-p2x5-w9fh](https://github.com/awslabs/mcp/security/advisories/GHSA-4prq-p2x5-w9fh)
|
|
81
|
+
(CVE-2026-115413, high). `awslabs.aws-api-mcp-server` >=1.3.0, <1.6.0 (streamable-http + OAuth) validated
|
|
82
|
+
signature, issuer and expiry but not the audience/client; fixed in 1.6.0, which requires operators to set an
|
|
83
|
+
expected audience or client ID.
|
|
84
|
+
|
|
85
|
+
Context: [thread](https://x.com/karlmehta/status/2115834904739033160). A config setting that is "off unless
|
|
86
|
+
you set it" is easy to miss, hence an *active* test that you can run in CI.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
Once the first release is on PyPI (see [Releases](https://github.com/bymustfa/mcp-audprobe/releases)):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pipx install mcp-audprobe # recommended: isolated CLI (or: uvx mcp-audprobe --help)
|
|
94
|
+
pip install mcp-audprobe # into the current environment (Python >= 3.10)
|
|
95
|
+
mcp-audprobe --version
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
From a clone (development):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e . # CLI only (Python >= 3.10)
|
|
102
|
+
pip install -e ".[demo]" # + MCP Python SDK (>=1.30.0,<2) and Pillow, for the demo servers / PNG
|
|
103
|
+
pip install -e ".[test]" # + pytest, ruff
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
1. `mcp-audprobe auth-server` runs a **fake authorization server** on loopback: RS256 JWKS at
|
|
109
|
+
`/.well-known/jwks.json` plus OIDC/OAuth metadata. The RSA key is generated locally into `--key-file`.
|
|
110
|
+
2. Your server under test is configured to trust that issuer/JWKS (in CI/dev only!).
|
|
111
|
+
3. `mcp-audprobe probe` signs tokens with the same key file and sends each one as
|
|
112
|
+
`Authorization: Bearer …` on an MCP JSON-RPC `initialize` request (`POST`, `Accept: application/json, text/event-stream`).
|
|
113
|
+
|
|
114
|
+
| # | Check id | Token | Must be |
|
|
115
|
+
|---|----------|-------|---------|
|
|
116
|
+
| 0 | `CONTROL` | valid: correct iss, aud, signature, exp | **accepted**, else the run is `INCONCLUSIVE` (attack checks are `SKIPPED`) |
|
|
117
|
+
| 1 | `AUD-OTHER` | valid signature/iss/exp, `aud` = another service | rejected |
|
|
118
|
+
| 2 | `AUD-MISSING` | valid signature/iss/exp, **no** `aud` claim | rejected |
|
|
119
|
+
| 3 | `ISS-WRONG` | signed with the trusted key, `iss` ≠ trusted issuer (isolates the `iss` check) | rejected |
|
|
120
|
+
| 4 | `EXP-EXPIRED` | valid otherwise, `exp` one hour in the past | rejected |
|
|
121
|
+
|
|
122
|
+
**Definition of "accepted"**: an HTTP **2xx** response to the authenticated request. **Rejected** = **401 or 403**.
|
|
123
|
+
Anything else (other 4xx/5xx, redirects, network error/timeout) is "unexpected": the control then makes the run
|
|
124
|
+
`INCONCLUSIVE`, and for an attack token the check is `ERROR`. Verdicts: accepted → `FAIL`, rejected → `PASS`.
|
|
125
|
+
|
|
126
|
+
Overall result / exit code: `PASS` (0), `FAIL` (1, any attack token accepted), `INCONCLUSIVE` (2, control not
|
|
127
|
+
accepted or an `ERROR`), usage/safety error (3).
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 1. fake authorization server (creates key.pem if missing; keep it out of git)
|
|
133
|
+
mcp-audprobe auth-server --port 9000 --issuer http://127.0.0.1:9000 --key-file key.pem &
|
|
134
|
+
|
|
135
|
+
# 2. start your MCP server so it trusts issuer http://127.0.0.1:9000 and JWKS
|
|
136
|
+
# http://127.0.0.1:9000/.well-known/jwks.json (fetched lazily; see demo/demo_server.py)
|
|
137
|
+
|
|
138
|
+
# 3. probe
|
|
139
|
+
mcp-audprobe probe http://127.0.0.1:8001/mcp \
|
|
140
|
+
--audience http://127.0.0.1:8001/mcp \
|
|
141
|
+
--issuer http://127.0.0.1:9000 --key-file key.pem \
|
|
142
|
+
--auth-server http://127.0.0.1:9000 # optional: verify the AS serves our key id first
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Options: `--json` (JSON on stdout instead of the table), `--json-out FILE` (table *and* JSON file),
|
|
146
|
+
`--other-audience URL`, `--timeout S`, `--no-color`, `--allow-remote`. `python -m mcp_audprobe` also works.
|
|
147
|
+
|
|
148
|
+
## CI / GitHub Action
|
|
149
|
+
|
|
150
|
+
`action.yml` (composite) installs the tool, starts the fake auth server in the background, probes, writes
|
|
151
|
+
`audprobe-report.json`, stops the auth server, and fails the job on FAIL/INCONCLUSIVE. Your server must be
|
|
152
|
+
started in an earlier step on loopback, trusting `http://127.0.0.1:9000` (default `auth-port`).
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
- name: Start my MCP server (127.0.0.1:8001, trusts the fake AS at http://127.0.0.1:9000)
|
|
156
|
+
run: ./start-server.sh
|
|
157
|
+
- uses: bymustfa/mcp-audprobe@v1
|
|
158
|
+
with:
|
|
159
|
+
url: http://127.0.0.1:8001/mcp
|
|
160
|
+
audience: http://127.0.0.1:8001/mcp
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> `@v1` is a floating major tag that exists **after the first release** (until then use `@main` or a commit SHA).
|
|
164
|
+
> For supply-chain safety you can instead pin a full commit SHA: `uses: bymustfa/mcp-audprobe@<sha> # v1.0.0`.
|
|
165
|
+
|
|
166
|
+
Full example: [`examples/audprobe-workflow.yml`](examples/audprobe-workflow.yml).
|
|
167
|
+
|
|
168
|
+
| Input | Default | Meaning |
|
|
169
|
+
|-------|---------|---------|
|
|
170
|
+
| `url` (required) | | MCP endpoint of **your** server, e.g. `http://127.0.0.1:8001/mcp` |
|
|
171
|
+
| `audience` (required) | | audience/resource the server must accept (usually its own URL) |
|
|
172
|
+
| `auth-port` | `9000` | port for the fake authorization server |
|
|
173
|
+
| `issuer` | `http://127.0.0.1:<auth-port>` | issuer your server trusts |
|
|
174
|
+
| `other-audience` | `https://other-service.example/mcp` | audience for the wrong-audience token |
|
|
175
|
+
| `json-output` | `audprobe-report.json` | path for the JSON report |
|
|
176
|
+
| `allow-remote` | `false` | allow a non-loopback target (own staging only) |
|
|
177
|
+
| `python-version` | `3.12` | Python used to run the tool |
|
|
178
|
+
|
|
179
|
+
Outputs: `result` (`PASS`/`FAIL`/`INCONCLUSIVE`) and `report` (path of the JSON report). Check
|
|
180
|
+
[`action.yml`](https://github.com/bymustfa/mcp-audprobe/blob/main/action.yml) for the authoritative list.
|
|
181
|
+
|
|
182
|
+
## JSON schema (`schema_version: 1`)
|
|
183
|
+
|
|
184
|
+
```jsonc
|
|
185
|
+
{
|
|
186
|
+
"schema_version": 1, "tool": "mcp-audprobe", "tool_version": "0.1.0",
|
|
187
|
+
"target": "http://127.0.0.1:18001/mcp", "expected_audience": "…", "other_audience": "…", "issuer": "…",
|
|
188
|
+
"request": "POST initialize (JSON-RPC) with Authorization: Bearer <token>",
|
|
189
|
+
"control": { "id": "CONTROL", "name": "…", "description": "…",
|
|
190
|
+
"token_claims": {"iss": "…", "aud": "…", "exp": 0}, // aud absent when not sent
|
|
191
|
+
"aud_present": true, "http_status": 200 /* null on network error */,
|
|
192
|
+
"outcome": "accepted" /* accepted|rejected|unexpected */, "error": null, "accepted": true },
|
|
193
|
+
"checks": [ { "id": "AUD-OTHER" /* AUD-OTHER|AUD-MISSING|ISS-WRONG|EXP-EXPIRED */, "name": "…", "description": "…",
|
|
194
|
+
"token_claims": {…}, "aud_present": true, "http_status": 200,
|
|
195
|
+
"outcome": "accepted" /* null when SKIPPED */,
|
|
196
|
+
"verdict": "FAIL" /* PASS|FAIL|ERROR|SKIPPED */, "error": null } ],
|
|
197
|
+
"summary": { "passed": 2, "failed": 2, "errors": 0, "skipped": 0 },
|
|
198
|
+
"result": "FAIL", // PASS | FAIL | INCONCLUSIVE
|
|
199
|
+
"exit_code": 1
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Raw tokens are never written to the report. Field names and order are stable within a `schema_version`.
|
|
204
|
+
|
|
205
|
+
## Demo
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install -e ".[demo]" # or: make install
|
|
209
|
+
./demo/run_demo.sh # or: make demo
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Starts the fake auth server and two real MCP Python SDK servers (`demo/demo_server.py`) on 127.0.0.1,
|
|
213
|
+
probes both, saves everything to `demo/output/` (`*.txt`, `*.json`, `demo.png`), then stops all processes.
|
|
214
|
+
|
|
215
|
+
- **vulnerable**: verifies signature, `iss`, `exp`, not `aud`; `validate_token_resource=False`; `verify_token()`
|
|
216
|
+
does not set `AccessToken.resource`.
|
|
217
|
+
- **hardened**: same verifier, but `verify_token()` returns the token's `aud` as `AccessToken.resource` and
|
|
218
|
+
`AuthSettings(validate_token_resource=True, resource_server_url=<its own URL>)`.
|
|
219
|
+
|
|
220
|
+

|
|
221
|
+
|
|
222
|
+
Expected: vulnerable = FAIL (`AUD-OTHER`, `AUD-MISSING` accepted), hardened = PASS. The vulnerable server still
|
|
223
|
+
rejects wrong-`iss` and expired tokens: exactly the AWS advisory's description (signature, issuer and expiry
|
|
224
|
+
checked, audience not). Tests: `pytest` (unit tests for minting and verdict logic, mocked-transport probe
|
|
225
|
+
tests, and an end-to-end test against both demo servers).
|
|
226
|
+
|
|
227
|
+
## Limitations
|
|
228
|
+
|
|
229
|
+
- Black-box, HTTP-level only: it observes status codes, not why a server rejected a token. A 401 for an unrelated
|
|
230
|
+
reason (e.g. server cannot fetch JWKS) looks like a pass for attack checks; the `CONTROL` token guards against
|
|
231
|
+
the "rejects everything" case.
|
|
232
|
+
- Your server must be configured to trust the fake issuer/JWKS. Run it against a dev/CI configuration of the
|
|
233
|
+
*same code*, not production with real trust anchors; a production config will reject the control token
|
|
234
|
+
(→ `INCONCLUSIVE`).
|
|
235
|
+
- Tests the audience as an exact string/URL you provide. Servers that accept several audiences or list-valued
|
|
236
|
+
`aud`, scope/client-ID based checks (e.g. Cognito `client_id`), opaque (non-JWT) tokens, DPoP and
|
|
237
|
+
mTLS-bound tokens are not covered.
|
|
238
|
+
- The wrong-`iss` token uses the trusted key, so it tests the `iss` comparison, not key/issuer binding.
|
|
239
|
+
- Uses `initialize` only. Servers that enforce auth later (per tool call) or need sessions may need more.
|
|
240
|
+
- Clock skew leeway on the server is not probed (expired token is 1 h old).
|
|
241
|
+
- Demo servers are for demonstration, not production templates. Developed against `mcp` 1.30.0; the demo servers use the 1.x `FastMCP` API (`mcp>=1.30.0,<2`; `mcp` 2.x renamed it and is not supported by the demo yet).
|
|
242
|
+
|
|
243
|
+
## Comparison with authgent
|
|
244
|
+
|
|
245
|
+
[authgent](https://github.com/authgent/authgent) (Apache-2.0) was read on 2026-11-01 (README,
|
|
246
|
+
`docs/methodology.md`, `server/authgent_server/scanner.py`, read-only). Its `authgent-server lint <mcp-url>`
|
|
247
|
+
is an MCP-OAuth **conformance scanner**: it inspects discovery metadata (RFC 9728 protected resource metadata,
|
|
248
|
+
RFC 8414 AS metadata), advertised capabilities (PKCE methods, RFC 8707 resource indicators, DCR, `iss`
|
|
249
|
+
parameter, …), plus a few probes of the authorization server (PKCE `plain` at `/authorize`, duplicate DCR
|
|
250
|
+
registrations) and a heuristic `GET /`. Its `MCP-AUD-001` only checks that the AS **advertises**
|
|
251
|
+
`resource_indicators_supported`.
|
|
252
|
+
|
|
253
|
+
**Based on that review, authgent does not appear to submit forged or mis-audienced bearer tokens to the MCP
|
|
254
|
+
resource server, so it does not perform this active `aud`/`iss`/`exp` acceptance test.** It can tell you the AS
|
|
255
|
+
*supports* resource indicators; it cannot tell you whether your server *rejects* a token minted for another
|
|
256
|
+
audience, which is the failure in the two advisories above. The tools are complementary: authgent audits
|
|
257
|
+
metadata/spec conformance of a (possibly remote) server and also ships an AS and SDKs; mcp-audprobe is a narrow
|
|
258
|
+
black-box test of token validation on servers you run locally with a throwaway fake issuer. (I did not audit
|
|
259
|
+
authgent's own SDK middleware, nor verify claims beyond the files named above; its behaviour may change.)
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
See [CONTRIBUTING.md](https://github.com/bymustfa/mcp-audprobe/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/bymustfa/mcp-audprobe/blob/main/CODE_OF_CONDUCT.md) and the
|
|
264
|
+
[Security Policy](https://github.com/bymustfa/mcp-audprobe/blob/main/SECURITY.md). Türkçe: [README.tr.md](https://github.com/bymustfa/mcp-audprobe/blob/main/README.tr.md).
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT, see [LICENSE](https://github.com/bymustfa/mcp-audprobe/blob/main/LICENSE).
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
[**English**](https://github.com/bymustfa/mcp-audprobe/blob/main/README.md) | [Türkçe](https://github.com/bymustfa/mcp-audprobe/blob/main/README.tr.md)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bymustfa/mcp-audprobe/actions/workflows/ci.yml) [](https://pypi.org/project/mcp-audprobe/) [](https://github.com/bymustfa/mcp-audprobe/actions/workflows/codeql.yml) [](https://scorecard.dev/viewer/?uri=github.com/bymustfa/mcp-audprobe) [](https://github.com/bymustfa/mcp-audprobe/blob/main/LICENSE)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
# mcp-audprobe
|
|
8
|
+
|
|
9
|
+
Actively test whether an **MCP server you own** really validates the bearer token's audience (`aud`),
|
|
10
|
+
issuer (`iss`) and expiry (`exp`). It mints test tokens with a local fake authorization server key,
|
|
11
|
+
sends them to your server, and reports **FAIL** if a token that must be rejected is accepted.
|
|
12
|
+
|
|
13
|
+
> ## ⚠️ Only test servers you own or are authorized to test
|
|
14
|
+
> `mcp-audprobe` sends authenticated requests with deliberately invalid tokens. Use it **only** against
|
|
15
|
+
> servers you operate or have explicit written permission to test. By default the CLI refuses any target
|
|
16
|
+
> that is not on loopback (`localhost`, `127.0.0.0/8`, `::1`); `--allow-remote` exists solely for your own
|
|
17
|
+
> staging/test deployments. Never point it at someone else's server. The demo and test-suite only talk to
|
|
18
|
+
> servers they start on `127.0.0.1`.
|
|
19
|
+
|
|
20
|
+
## The problem
|
|
21
|
+
|
|
22
|
+
A resource server must accept only tokens issued *for it*. Two recent advisories show servers that checked
|
|
23
|
+
signature, issuer and expiry but not the audience, so a valid token for a *different* service of the same
|
|
24
|
+
authorization server was accepted:
|
|
25
|
+
|
|
26
|
+
- MCP Python SDK: [GHSA-w4fh-qvv9-3v23](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-w4fh-qvv9-3v23)
|
|
27
|
+
(CVE-2026-116684, medium). Affected `mcp` 1.10.0–1.29.1 and 2.0.0–2.1.1; fixed in 1.30.0 / 2.2.0.
|
|
28
|
+
**Upgrading alone changes nothing**: the patched versions add `AuthSettings(validate_token_resource=...)`,
|
|
29
|
+
which is off unless set. To enable the check you must (1) set `validate_token_resource=True` with
|
|
30
|
+
`resource_server_url` = the audience your authorization server puts in tokens for your server, **and**
|
|
31
|
+
(2) return the token's audience from `TokenVerifier.verify_token()` as `AccessToken.resource`. With only
|
|
32
|
+
the first change every request gets 401. (Confirmed against the advisory text and `mcp` 1.30.0 source:
|
|
33
|
+
`mcp/server/auth/settings.py`, `mcp/server/auth/middleware/bearer_auth.py`.)
|
|
34
|
+
- AWS API MCP Server: [GHSA-4prq-p2x5-w9fh](https://github.com/awslabs/mcp/security/advisories/GHSA-4prq-p2x5-w9fh)
|
|
35
|
+
(CVE-2026-115413, high). `awslabs.aws-api-mcp-server` >=1.3.0, <1.6.0 (streamable-http + OAuth) validated
|
|
36
|
+
signature, issuer and expiry but not the audience/client; fixed in 1.6.0, which requires operators to set an
|
|
37
|
+
expected audience or client ID.
|
|
38
|
+
|
|
39
|
+
Context: [thread](https://x.com/karlmehta/status/2115834904739033160). A config setting that is "off unless
|
|
40
|
+
you set it" is easy to miss, hence an *active* test that you can run in CI.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
Once the first release is on PyPI (see [Releases](https://github.com/bymustfa/mcp-audprobe/releases)):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install mcp-audprobe # recommended: isolated CLI (or: uvx mcp-audprobe --help)
|
|
48
|
+
pip install mcp-audprobe # into the current environment (Python >= 3.10)
|
|
49
|
+
mcp-audprobe --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
From a clone (development):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e . # CLI only (Python >= 3.10)
|
|
56
|
+
pip install -e ".[demo]" # + MCP Python SDK (>=1.30.0,<2) and Pillow, for the demo servers / PNG
|
|
57
|
+
pip install -e ".[test]" # + pytest, ruff
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
1. `mcp-audprobe auth-server` runs a **fake authorization server** on loopback: RS256 JWKS at
|
|
63
|
+
`/.well-known/jwks.json` plus OIDC/OAuth metadata. The RSA key is generated locally into `--key-file`.
|
|
64
|
+
2. Your server under test is configured to trust that issuer/JWKS (in CI/dev only!).
|
|
65
|
+
3. `mcp-audprobe probe` signs tokens with the same key file and sends each one as
|
|
66
|
+
`Authorization: Bearer …` on an MCP JSON-RPC `initialize` request (`POST`, `Accept: application/json, text/event-stream`).
|
|
67
|
+
|
|
68
|
+
| # | Check id | Token | Must be |
|
|
69
|
+
|---|----------|-------|---------|
|
|
70
|
+
| 0 | `CONTROL` | valid: correct iss, aud, signature, exp | **accepted**, else the run is `INCONCLUSIVE` (attack checks are `SKIPPED`) |
|
|
71
|
+
| 1 | `AUD-OTHER` | valid signature/iss/exp, `aud` = another service | rejected |
|
|
72
|
+
| 2 | `AUD-MISSING` | valid signature/iss/exp, **no** `aud` claim | rejected |
|
|
73
|
+
| 3 | `ISS-WRONG` | signed with the trusted key, `iss` ≠ trusted issuer (isolates the `iss` check) | rejected |
|
|
74
|
+
| 4 | `EXP-EXPIRED` | valid otherwise, `exp` one hour in the past | rejected |
|
|
75
|
+
|
|
76
|
+
**Definition of "accepted"**: an HTTP **2xx** response to the authenticated request. **Rejected** = **401 or 403**.
|
|
77
|
+
Anything else (other 4xx/5xx, redirects, network error/timeout) is "unexpected": the control then makes the run
|
|
78
|
+
`INCONCLUSIVE`, and for an attack token the check is `ERROR`. Verdicts: accepted → `FAIL`, rejected → `PASS`.
|
|
79
|
+
|
|
80
|
+
Overall result / exit code: `PASS` (0), `FAIL` (1, any attack token accepted), `INCONCLUSIVE` (2, control not
|
|
81
|
+
accepted or an `ERROR`), usage/safety error (3).
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 1. fake authorization server (creates key.pem if missing; keep it out of git)
|
|
87
|
+
mcp-audprobe auth-server --port 9000 --issuer http://127.0.0.1:9000 --key-file key.pem &
|
|
88
|
+
|
|
89
|
+
# 2. start your MCP server so it trusts issuer http://127.0.0.1:9000 and JWKS
|
|
90
|
+
# http://127.0.0.1:9000/.well-known/jwks.json (fetched lazily; see demo/demo_server.py)
|
|
91
|
+
|
|
92
|
+
# 3. probe
|
|
93
|
+
mcp-audprobe probe http://127.0.0.1:8001/mcp \
|
|
94
|
+
--audience http://127.0.0.1:8001/mcp \
|
|
95
|
+
--issuer http://127.0.0.1:9000 --key-file key.pem \
|
|
96
|
+
--auth-server http://127.0.0.1:9000 # optional: verify the AS serves our key id first
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Options: `--json` (JSON on stdout instead of the table), `--json-out FILE` (table *and* JSON file),
|
|
100
|
+
`--other-audience URL`, `--timeout S`, `--no-color`, `--allow-remote`. `python -m mcp_audprobe` also works.
|
|
101
|
+
|
|
102
|
+
## CI / GitHub Action
|
|
103
|
+
|
|
104
|
+
`action.yml` (composite) installs the tool, starts the fake auth server in the background, probes, writes
|
|
105
|
+
`audprobe-report.json`, stops the auth server, and fails the job on FAIL/INCONCLUSIVE. Your server must be
|
|
106
|
+
started in an earlier step on loopback, trusting `http://127.0.0.1:9000` (default `auth-port`).
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
- name: Start my MCP server (127.0.0.1:8001, trusts the fake AS at http://127.0.0.1:9000)
|
|
110
|
+
run: ./start-server.sh
|
|
111
|
+
- uses: bymustfa/mcp-audprobe@v1
|
|
112
|
+
with:
|
|
113
|
+
url: http://127.0.0.1:8001/mcp
|
|
114
|
+
audience: http://127.0.0.1:8001/mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> `@v1` is a floating major tag that exists **after the first release** (until then use `@main` or a commit SHA).
|
|
118
|
+
> For supply-chain safety you can instead pin a full commit SHA: `uses: bymustfa/mcp-audprobe@<sha> # v1.0.0`.
|
|
119
|
+
|
|
120
|
+
Full example: [`examples/audprobe-workflow.yml`](examples/audprobe-workflow.yml).
|
|
121
|
+
|
|
122
|
+
| Input | Default | Meaning |
|
|
123
|
+
|-------|---------|---------|
|
|
124
|
+
| `url` (required) | | MCP endpoint of **your** server, e.g. `http://127.0.0.1:8001/mcp` |
|
|
125
|
+
| `audience` (required) | | audience/resource the server must accept (usually its own URL) |
|
|
126
|
+
| `auth-port` | `9000` | port for the fake authorization server |
|
|
127
|
+
| `issuer` | `http://127.0.0.1:<auth-port>` | issuer your server trusts |
|
|
128
|
+
| `other-audience` | `https://other-service.example/mcp` | audience for the wrong-audience token |
|
|
129
|
+
| `json-output` | `audprobe-report.json` | path for the JSON report |
|
|
130
|
+
| `allow-remote` | `false` | allow a non-loopback target (own staging only) |
|
|
131
|
+
| `python-version` | `3.12` | Python used to run the tool |
|
|
132
|
+
|
|
133
|
+
Outputs: `result` (`PASS`/`FAIL`/`INCONCLUSIVE`) and `report` (path of the JSON report). Check
|
|
134
|
+
[`action.yml`](https://github.com/bymustfa/mcp-audprobe/blob/main/action.yml) for the authoritative list.
|
|
135
|
+
|
|
136
|
+
## JSON schema (`schema_version: 1`)
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
{
|
|
140
|
+
"schema_version": 1, "tool": "mcp-audprobe", "tool_version": "0.1.0",
|
|
141
|
+
"target": "http://127.0.0.1:18001/mcp", "expected_audience": "…", "other_audience": "…", "issuer": "…",
|
|
142
|
+
"request": "POST initialize (JSON-RPC) with Authorization: Bearer <token>",
|
|
143
|
+
"control": { "id": "CONTROL", "name": "…", "description": "…",
|
|
144
|
+
"token_claims": {"iss": "…", "aud": "…", "exp": 0}, // aud absent when not sent
|
|
145
|
+
"aud_present": true, "http_status": 200 /* null on network error */,
|
|
146
|
+
"outcome": "accepted" /* accepted|rejected|unexpected */, "error": null, "accepted": true },
|
|
147
|
+
"checks": [ { "id": "AUD-OTHER" /* AUD-OTHER|AUD-MISSING|ISS-WRONG|EXP-EXPIRED */, "name": "…", "description": "…",
|
|
148
|
+
"token_claims": {…}, "aud_present": true, "http_status": 200,
|
|
149
|
+
"outcome": "accepted" /* null when SKIPPED */,
|
|
150
|
+
"verdict": "FAIL" /* PASS|FAIL|ERROR|SKIPPED */, "error": null } ],
|
|
151
|
+
"summary": { "passed": 2, "failed": 2, "errors": 0, "skipped": 0 },
|
|
152
|
+
"result": "FAIL", // PASS | FAIL | INCONCLUSIVE
|
|
153
|
+
"exit_code": 1
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Raw tokens are never written to the report. Field names and order are stable within a `schema_version`.
|
|
158
|
+
|
|
159
|
+
## Demo
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install -e ".[demo]" # or: make install
|
|
163
|
+
./demo/run_demo.sh # or: make demo
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Starts the fake auth server and two real MCP Python SDK servers (`demo/demo_server.py`) on 127.0.0.1,
|
|
167
|
+
probes both, saves everything to `demo/output/` (`*.txt`, `*.json`, `demo.png`), then stops all processes.
|
|
168
|
+
|
|
169
|
+
- **vulnerable**: verifies signature, `iss`, `exp`, not `aud`; `validate_token_resource=False`; `verify_token()`
|
|
170
|
+
does not set `AccessToken.resource`.
|
|
171
|
+
- **hardened**: same verifier, but `verify_token()` returns the token's `aud` as `AccessToken.resource` and
|
|
172
|
+
`AuthSettings(validate_token_resource=True, resource_server_url=<its own URL>)`.
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
Expected: vulnerable = FAIL (`AUD-OTHER`, `AUD-MISSING` accepted), hardened = PASS. The vulnerable server still
|
|
177
|
+
rejects wrong-`iss` and expired tokens: exactly the AWS advisory's description (signature, issuer and expiry
|
|
178
|
+
checked, audience not). Tests: `pytest` (unit tests for minting and verdict logic, mocked-transport probe
|
|
179
|
+
tests, and an end-to-end test against both demo servers).
|
|
180
|
+
|
|
181
|
+
## Limitations
|
|
182
|
+
|
|
183
|
+
- Black-box, HTTP-level only: it observes status codes, not why a server rejected a token. A 401 for an unrelated
|
|
184
|
+
reason (e.g. server cannot fetch JWKS) looks like a pass for attack checks; the `CONTROL` token guards against
|
|
185
|
+
the "rejects everything" case.
|
|
186
|
+
- Your server must be configured to trust the fake issuer/JWKS. Run it against a dev/CI configuration of the
|
|
187
|
+
*same code*, not production with real trust anchors; a production config will reject the control token
|
|
188
|
+
(→ `INCONCLUSIVE`).
|
|
189
|
+
- Tests the audience as an exact string/URL you provide. Servers that accept several audiences or list-valued
|
|
190
|
+
`aud`, scope/client-ID based checks (e.g. Cognito `client_id`), opaque (non-JWT) tokens, DPoP and
|
|
191
|
+
mTLS-bound tokens are not covered.
|
|
192
|
+
- The wrong-`iss` token uses the trusted key, so it tests the `iss` comparison, not key/issuer binding.
|
|
193
|
+
- Uses `initialize` only. Servers that enforce auth later (per tool call) or need sessions may need more.
|
|
194
|
+
- Clock skew leeway on the server is not probed (expired token is 1 h old).
|
|
195
|
+
- Demo servers are for demonstration, not production templates. Developed against `mcp` 1.30.0; the demo servers use the 1.x `FastMCP` API (`mcp>=1.30.0,<2`; `mcp` 2.x renamed it and is not supported by the demo yet).
|
|
196
|
+
|
|
197
|
+
## Comparison with authgent
|
|
198
|
+
|
|
199
|
+
[authgent](https://github.com/authgent/authgent) (Apache-2.0) was read on 2026-11-01 (README,
|
|
200
|
+
`docs/methodology.md`, `server/authgent_server/scanner.py`, read-only). Its `authgent-server lint <mcp-url>`
|
|
201
|
+
is an MCP-OAuth **conformance scanner**: it inspects discovery metadata (RFC 9728 protected resource metadata,
|
|
202
|
+
RFC 8414 AS metadata), advertised capabilities (PKCE methods, RFC 8707 resource indicators, DCR, `iss`
|
|
203
|
+
parameter, …), plus a few probes of the authorization server (PKCE `plain` at `/authorize`, duplicate DCR
|
|
204
|
+
registrations) and a heuristic `GET /`. Its `MCP-AUD-001` only checks that the AS **advertises**
|
|
205
|
+
`resource_indicators_supported`.
|
|
206
|
+
|
|
207
|
+
**Based on that review, authgent does not appear to submit forged or mis-audienced bearer tokens to the MCP
|
|
208
|
+
resource server, so it does not perform this active `aud`/`iss`/`exp` acceptance test.** It can tell you the AS
|
|
209
|
+
*supports* resource indicators; it cannot tell you whether your server *rejects* a token minted for another
|
|
210
|
+
audience, which is the failure in the two advisories above. The tools are complementary: authgent audits
|
|
211
|
+
metadata/spec conformance of a (possibly remote) server and also ships an AS and SDKs; mcp-audprobe is a narrow
|
|
212
|
+
black-box test of token validation on servers you run locally with a throwaway fake issuer. (I did not audit
|
|
213
|
+
authgent's own SDK middleware, nor verify claims beyond the files named above; its behaviour may change.)
|
|
214
|
+
|
|
215
|
+
## Contributing
|
|
216
|
+
|
|
217
|
+
See [CONTRIBUTING.md](https://github.com/bymustfa/mcp-audprobe/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/bymustfa/mcp-audprobe/blob/main/CODE_OF_CONDUCT.md) and the
|
|
218
|
+
[Security Policy](https://github.com/bymustfa/mcp-audprobe/blob/main/SECURITY.md). Türkçe: [README.tr.md](https://github.com/bymustfa/mcp-audprobe/blob/main/README.tr.md).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT, see [LICENSE](https://github.com/bymustfa/mcp-audprobe/blob/main/LICENSE).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-audprobe"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Actively test whether an MCP server you own validates bearer-token audience (aud), issuer (iss) and expiry (exp)."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Mustafa ÖZTÜRK" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp", "model-context-protocol", "oauth", "jwt", "audience", "aud", "security",
|
|
16
|
+
"security-testing", "bearer-token", "authorization", "github-action", "ci",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Security",
|
|
31
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
32
|
+
"Topic :: Software Development :: Testing",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"httpx>=0.27",
|
|
36
|
+
"PyJWT[crypto]>=2.8",
|
|
37
|
+
"cryptography>=42",
|
|
38
|
+
"starlette>=0.37",
|
|
39
|
+
"uvicorn>=0.30",
|
|
40
|
+
"rich>=13",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/bymustfa/mcp-audprobe"
|
|
45
|
+
Repository = "https://github.com/bymustfa/mcp-audprobe"
|
|
46
|
+
Issues = "https://github.com/bymustfa/mcp-audprobe/issues"
|
|
47
|
+
Changelog = "https://github.com/bymustfa/mcp-audprobe/blob/main/CHANGELOG.md"
|
|
48
|
+
Documentation = "https://github.com/bymustfa/mcp-audprobe#readme"
|
|
49
|
+
"Security Policy" = "https://github.com/bymustfa/mcp-audprobe/security/policy"
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
# The demo servers use the real MCP Python SDK. >=1.30.0 is the first 1.x release with validate_token_resource.
|
|
53
|
+
demo = ["mcp>=1.30.0,<2", "pillow>=10"]
|
|
54
|
+
test = ["pytest>=8", "mcp>=1.30.0,<2", "pillow>=10", "ruff>=0.5"]
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
mcp-audprobe = "mcp_audprobe.cli:main"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 120
|
|
67
|
+
target-version = "py310"
|
|
68
|
+
src = ["src", "tests", "demo"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = ["E", "F", "W", "I", "B", "UP"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Minimal fake authorization server: JWKS + OAuth/OIDC discovery metadata.
|
|
2
|
+
|
|
3
|
+
It issues nothing over HTTP. Tokens are minted locally by the probe with the same key file; the
|
|
4
|
+
MCP server under test only needs to trust this server's JWKS. Demo/testing use only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from starlette.applications import Starlette
|
|
10
|
+
from starlette.requests import Request
|
|
11
|
+
from starlette.responses import JSONResponse
|
|
12
|
+
from starlette.routing import Route
|
|
13
|
+
|
|
14
|
+
from .tokens import KeyMaterial
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_app(km: KeyMaterial, issuer: str) -> Starlette:
|
|
18
|
+
issuer = issuer.rstrip("/")
|
|
19
|
+
jwks_uri = f"{issuer}/.well-known/jwks.json"
|
|
20
|
+
|
|
21
|
+
async def jwks(_: Request) -> JSONResponse:
|
|
22
|
+
return JSONResponse(km.jwks())
|
|
23
|
+
|
|
24
|
+
async def metadata(_: Request) -> JSONResponse:
|
|
25
|
+
return JSONResponse(
|
|
26
|
+
{
|
|
27
|
+
"issuer": issuer,
|
|
28
|
+
"jwks_uri": jwks_uri,
|
|
29
|
+
"id_token_signing_alg_values_supported": ["RS256"],
|
|
30
|
+
"response_types_supported": ["code"],
|
|
31
|
+
"subject_types_supported": ["public"],
|
|
32
|
+
"token_endpoint_auth_methods_supported": ["none"],
|
|
33
|
+
"note": "mcp-audprobe fake authorization server (testing only)",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
async def health(_: Request) -> JSONResponse:
|
|
38
|
+
return JSONResponse({"status": "ok", "kid": km.kid})
|
|
39
|
+
|
|
40
|
+
return Starlette(
|
|
41
|
+
routes=[
|
|
42
|
+
Route("/.well-known/jwks.json", jwks),
|
|
43
|
+
Route("/.well-known/openid-configuration", metadata),
|
|
44
|
+
Route("/.well-known/oauth-authorization-server", metadata),
|
|
45
|
+
Route("/health", health),
|
|
46
|
+
]
|
|
47
|
+
)
|