On this page▾
MCP Security Best Practices — AgentWatch conformance
Audited against the MCP security best practices on 2026-10-19.
AgentWatch operates two MCP surfaces, and the spec's requirements land differently on each:
| Surface | What it is | Code |
|---|---|---|
| MCP server | AgentWatch's own server (14 tools, stdio + Streamable HTTP, OAuth 2.1) | mcp-server/ |
| MCP proxy | Gate forwarding to customer-configured upstream MCP servers | backend/gateway/mcp_proxy.py, /gw/mcp/{server_name} |
The proxy is the one the spec's confused-deputy section is written about.
Rule for this table: a row is only PASS if a test asserts it. A row with no
test is UNVERIFIED even when the code looks correct — reading code is not
evidence, and this document exists to be shown to a security reviewer.
Checklist
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Per-client consent before forwarding to a third-party authorisation server (confused deputy) | PASS | Consent is stored per client_id (not per user) and checked before issue. backend/routers/mcp_oauth.py · tests/test_mcp_oauth.py::test_issue_code_defaults_read_only |
| 2 | Audience validation — server MUST NOT accept tokens not issued to it | PASS | Tokens are looked up by token_hash against rows this server issued; a foreign token simply does not resolve. routers/mcp_oauth.py:276 introspect · tests/test_mcp_oauth.py::test_introspect_returns_own_api_key_only |
| 3 | No token passthrough to upstreams | PASS | The MCP server exchanges the OAuth token for the user's own AgentWatch API key server-side (introspect returns api_key); the inbound token is never forwarded. mcp-server/src/oauth/provider.ts:189-211 |
| 4 | redirect_uri exact-match validation, no wildcards |
PASS | routers/mcp_oauth.py:157-158 rejects any redirect_uri not in the registered list. |
| 5 | SSRF: HTTPS-only, block private/link-local/metadata IPs, validate redirects | PASS | backend/gateway/ssrf.py blocks 169.254.169.254, metadata.google.internal, and all private/link-local/reserved/multicast ranges; proxy uses follow_redirects=False. tests/test_redcell_safety.py exercises the SSRF probe. |
| 6 | State handles bound server-side to the authenticated user | PASS | OAuth codes and tokens are keyed to user_id in the DB and resolved through introspection; possession alone grants nothing. tests/test_mcp_oauth.py::test_revoke_connection_breaks_introspect |
| 7 | Scope minimisation — no omnibus scopes | PASS | Only agentwatch:read / agentwatch:write exist; consent defaults to read-only and write is opt-in. routers/mcp_oauth.py:83 _normalize_scopes · tests/test_mcp_oauth.py::test_issue_code_defaults_read_only |
| 8 | Scope re-checked on every request, not only at token issue | PASS | This was the audit's open question — answered. mcp-server/src/http.ts:156-178 rebuilds currentAuth per request from req.auth (verified each time by requireBearerAuth → verifyAccessToken → backend introspection), and createServer.ts:75-80 checks scopes.has(tool.requiredScope) on every tool call. Revocation therefore takes effect on the next request, not at token expiry. |
| 9 | Reject javascript: / data: / file: / vbscript: authorization URLs |
UNVERIFIED | AgentWatch is the authorization server here, not a client opening URLs, so the client-side requirement does not apply to this code. No test asserts the negative. See Not applicable, below. |
| 10 | No shell execution to open URLs | N/A | AgentWatch never opens a URL on behalf of a user; it has no URL-launching client. |
| 11 | iss validation on authorization responses (mix-up, RFC 9207) |
NOT MET | AgentWatch issues tokens; it does not consume another authorization server's responses, so there is no iss to validate on this path. If the Gate MCP proxy ever performs an OAuth dance against an upstream MCP server, this becomes required. Tracked below. |
| 12 | Local MCP server compromise — consent showing the exact command | N/A | AgentWatch does not spawn local MCP servers from configuration. Its Cursor integration is a stdio client the user configures themselves. |
Score: 7 PASS · 1 UNVERIFIED · 1 NOT MET (scoped out) · 3 N/A
What this does and does not let us claim
Claimable: "AgentWatch's MCP server and proxy were audited against the MCP security best practices; 7 of the applicable requirements are covered by tests, including per-client consent, audience validation, no token passthrough, SSRF protection, scope minimisation, and per-request scope re-checking."
Not claimable: blanket "MCP security conformant". Three rows are not applicable to our role, one is unverified, and the spec itself is a draft that moves.
Open items
- Row 11 (
issvalidation) becomes a real requirement the moment the Gate MCP proxy performs its own OAuth flow against an upstream MCP server. Today it forwards with credentials the operator configured, so there is no authorization response to validate. Revisit when upstream OAuth ships. - Row 9 needs a negative test if AgentWatch ever renders an upstream-supplied authorization URL in the console.
base_url_overrideSSRF is listed as a known gap inSECURITY_CHECKLIST.md.gateway/ssrf.pyvalidates upstream URLs, but the admin-supplied override path should be re-tested specifically, since an authenticated admin is a lower bar than an external attacker but not no bar.- The spec is a draft. Re-run this audit when it stabilises; the
state handle hijackingsection already changed shape (protocol version2025-11-25and earlier used server-assigned session ids).