Skip to content

Commit a965b79

Browse files
saqadriroman-van-der-krogt
authored andcommitted
OAuth updates for mcp-agent (lastmile-ai#547)
Allow OAuth for MCP servers used by agents --------- Co-authored-by: Roman van der Krogt <[email protected]>
1 parent 37adffd commit a965b79

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+5592
-37
lines changed

auth_spec.png

406 KB
Loading

docs/configuration.mdx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,54 @@ mcp-agent uses two configuration files:
123123
</Tab>
124124
</Tabs>
125125

126+
## OAuth Configuration
127+
128+
MCP Agent exposes two complementary OAuth configuration blocks:
129+
130+
- `authorization` describes how the MCP Agent server validates inbound bearer tokens and publishes protected resource metadata.
131+
- `oauth` configures delegated authorization when the agent connects to downstream MCP servers.
132+
133+
```yaml
134+
authorization:
135+
enabled: true
136+
issuer_url: https://auth.example.com
137+
resource_server_url: https://agent.example.com/mcp
138+
required_scopes: ["mcp.read", "mcp.write"]
139+
introspection_endpoint: https://auth.example.com/oauth/introspect
140+
introspection_client_id: ${INTROSPECTION_CLIENT_ID}
141+
introspection_client_secret: ${INTROSPECTION_CLIENT_SECRET}
142+
143+
oauth:
144+
callback_base_url: https://agent.example.com
145+
flow_timeout_seconds: 180
146+
token_store:
147+
backend: memory # set to "redis" for multi-instance deployments
148+
refresh_leeway_seconds: 90
149+
redis_url: redis://localhost:6379
150+
redis_prefix: mcp_agent:oauth_tokens
151+
152+
mcp:
153+
servers:
154+
github:
155+
transport: streamable_http
156+
url: https://github.mcp.example.com/mcp
157+
auth:
158+
oauth:
159+
enabled: true
160+
scopes: ["repo", "user:email"]
161+
client_id: ${GITHUB_MCP_CLIENT_ID}
162+
client_secret: ${GITHUB_MCP_CLIENT_SECRET}
163+
include_resource_parameter: false # disable RFC 8707 resource param for providers like GitHub
164+
redirect_uri_options:
165+
- https://agent.example.com/internal/oauth/callback
166+
```
167+
168+
- When `authorization.enabled` is true the MCP server advertises `/.well-known/oauth-protected-resource` and enforces bearer tokens using the provided introspection or JWKS configuration.
169+
- `oauth` enables delegated authorization flows; the default in-memory token store is ideal for local development while Redis is recommended for production clusters.
170+
- To use Redis for token storage, configure `token_store.backend: redis` and supply `redis_url` (see optional dependency `mcp-agent[redis]`).
171+
- Downstream servers opt into OAuth via `mcp.servers.<name>.auth.oauth`. Supplying a `client_id`/`client_secret` allows immediate usage; support for dynamic client registration is planned as a follow-up.
172+
- Some providers (including GitHub) reject the RFC 8707 `resource` parameter. Set `include_resource_parameter: false` in the client settings for those services.
173+
126174
## Configuration Reference
127175

128176
### Execution Engine

docs/oauth_support_design.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# MCP Agent OAuth Support
2+
3+
## Goals
4+
- Protect MCP Agent Cloud servers using OAuth 2.1 so MCP clients obtain tokens via standard flows.
5+
- Enable MCP Agent runtimes to authenticate to downstream MCP servers that require OAuth access tokens.
6+
- Provide pluggable token storage for both local development (in-memory) and multi-instance deployments (Redis planned).
7+
- Maintain compatibility with MCP Authorization spec (RFC 8414, RFC 9728, OAuth 2.1 + PKCE, Resource Indicators) and the proposed delegated authorization SEP.
8+
9+
## Architecture Overview
10+
11+
### Components
12+
1. **Auth Server Integration** – Configure the FastMCP instance with `AuthSettings` and a custom `TokenVerifier` that calls MCP Agent Cloud auth services.
13+
2. **Protected Resource Metadata** – Serve `/.well-known/oauth-protected-resource` using FastMCP hooks so clients can discover the auth server.
14+
3. **Access Token Validation** – Enforce bearer tokens on every inbound MCP request via `RequireAuthMiddleware`, populating the request context with the authenticated user.
15+
4. **OAuth Token Service** – New `mcp_agent.oauth` package with:
16+
- `TokenStore`/`TokenRecord` abstractions
17+
- `InMemoryTokenStore` and Redis-backed implementation (optional for multi-instance)
18+
- `TokenManager` orchestration (acquire, refresh, revoke)
19+
- `OAuthHttpxAuth` for attaching tokens to downstream HTTP transports
20+
- `AuthorizationFlowCoordinator` that interacts with the user via MCP `auth/request`.
21+
When no upstream client session is available, a client-only loopback flow starts a
22+
temporary local callback listener on 127.0.0.1 using a configurable fixed port list
23+
(default: 33418, 33419, 33420), opens the browser, and completes the PKCE code flow.
24+
5. **Delegated Authorization UI Flow** – Extend the gateway/session relay so servers can send `auth/request` messages to MCP clients, capturing authorization codes via either:
25+
- Client-returned callback URL (preferred, works with SEP-capable clients)
26+
- MCP Agent hosted callback endpoint (`/internal/oauth/callback/{flow_id}`) as a fallback / native-app style loopback.
27+
6. **Configuration Surface** – Extend `Settings` and per-server `MCPServerAuthSettings` to describe OAuth behaviour (scopes, preferred auth server, redirect URIs, etc.) and global token-store configuration.
28+
29+
### Key Data Flow
30+
1. **Inbound Requests**
31+
- Client presents bearer token ⇒ `BearerAuthBackend` + `MCPAgentTokenVerifier` introspect token.
32+
- Verified token populates context with `OAuthUserIdentity` (provider + subject + email).
33+
- Context is propagated into workflows/sessions so downstream OAuth flows know the acting user.
34+
35+
2. **Outbound HTTP (downstream MCP server)**
36+
- `ServerRegistry` detects `auth.oauth` configuration.
37+
- Wraps HTTP transport with `OAuthHttpxAuth` which requests an access token from `TokenManager`.
38+
- `TokenManager` checks store; if missing/expired ⇒ `AuthorizationFlowCoordinator` performs RFC 9728 discovery, PKCE, delegated browser flow through MCP client, exchanges code for tokens, caches result.
39+
- Requests automatically retry after token refresh when a response returns 401/invalid token.
40+
41+
3. **Token Storage**
42+
- Tokens stored per `(user_identity, resource, authorization_server)` tuple with metadata (scopes, expiry, refresh token, provider claims).
43+
- Store implements optimistic locking to avoid concurrent refresh storms.
44+
- Pluggable backend (`InMemoryTokenStore` initial, Redis follow-up).
45+
46+
## Module Plan
47+
48+
```
49+
src/mcp_agent/oauth/
50+
__init__.py
51+
identity.py # OAuthUserIdentity, helpers to extract from auth context
52+
records.py # TokenRecord dataclass/pydantic model
53+
store/base.py # TokenStore protocol
54+
store/in_memory.py # Default store
55+
manager.py # TokenManager (get/refresh/invalidate)
56+
flow.py # AuthorizationFlowCoordinator
57+
http/auth.py # OAuthHttpxAuth (httpx.Auth implementation)
58+
metadata.py # RFC 8414 + RFC 9728 discovery helpers
59+
pkce.py # PKCE + state utilities
60+
errors.py # Custom exception hierarchy
61+
```
62+
63+
Integration touchpoints:
64+
- `mcp_agent/config.py` – add OAuth settings models.
65+
- `mcp_agent/core/context.py` – add `token_manager`, `token_store`, `oauth_config` fields.
66+
- `mcp_agent/app.py` – initialize token store/manager based on settings.
67+
- `mcp_agent/server/app_server.py` – configure FastMCP auth settings, register callback route, surface user identity, extend relay to handle `auth/request`.
68+
- `mcp_agent/mcp/mcp_server_registry.py` & `mcp_agent/mcp/mcp_connection_manager.py` – wire `OAuthHttpxAuth` into HTTP transports and expose helper for manual token teardown.
69+
- `mcp_agent/mcp/client_proxy.py` – add proxy helpers for `auth/request`.
70+
- `SessionProxy` – add direct request helper for `auth/request` and ensure Temporal flow support.
71+
- `examples/mcp_agent_server/*` – demonstrate configuration changes.
72+
- Tests – new suite exercising token store, metadata discovery, flow orchestration (with mocked HTTP + client responses).
73+
74+
## OAuth Flow Details
75+
1. **Discovery**
76+
- If downstream server responds 401 with `WWW-Authenticate`, parse for `resource_metadata` ⇒ GET metadata ⇒ determine auth server URL(s).
77+
- Fetch authorization server metadata (RFC 8414).
78+
- Perform optional dynamic client registration when configured and supported.
79+
80+
2. **Authorization Request**
81+
- Generate PKCE challenge/verifier, secure `state`, choose `redirect_uri`.
82+
- Build authorization URL including `resource` parameter (RFC 8707) + requested scopes.
83+
- Invoke `auth/request` via SessionProxy → MCP client opens browser.
84+
85+
3. **Callback Handling**
86+
- Preferred: MCP client returns callback URL payload via request result.
87+
- Fallback: Authorization server redirects to `/internal/oauth/callback/{flow_id}`.
88+
- Coordinator validates `state`, extracts `code` (and errors).
89+
90+
4. **Token Exchange / Storage**
91+
- POST token endpoint with code + PKCE verifier + resource.
92+
- Store access token, refresh token, expiry, scope, provider metadata.
93+
- Associate tokens with user identity for reuse.
94+
95+
5. **Refresh / Revocation**
96+
- Manager refreshes when expiry within configurable grace window.
97+
- Invalidate token on refresh failure or when server responses indicate revocation.
98+
- Provide method to revoke tokens via authorization server when supported.
99+
100+
## Open Questions / Follow-ups
101+
- Additional operational hardening (token rotation policies, rate limits).
102+
- How LastMile auth server exposes token introspection + JWKS; need concrete endpoint specs to finalize `MCPAgentTokenVerifier`.
103+
- MCP client adoption of `auth/request` SEP – need capability detection; until widely supported we rely on hosted callback fallback & manual instructions.
104+
- Access control DSL (include/exclude by email/domain) – to be evaluated once token identity payload finalized.
105+
106+
## Testing Strategy
107+
- Unit tests for token store concurrency + expiry handling.
108+
- Metadata discovery + PKCE generation (pure python tests).
109+
- Integration-style test for delegated flow using mocked HTTP server + fake MCP client (ensures `auth/request` plumbing works end-to-end).
110+
- Tests around server 401 enforcement + WWW-Authenticate header.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# OAuth Basic MCP Agent example (client-only loopback)
2+
3+
This example mirrors `mcp_basic_agent` but adds GitHub MCP with OAuth using the client-only loopback flow.
4+
5+
## Setup
6+
7+
1. Register a GitHub OAuth App and add redirect URIs (at least one of):
8+
9+
- `http://127.0.0.1:33418/callback`
10+
- `http://127.0.0.1:33419/callback`
11+
- `http://localhost:33418/callback`
12+
13+
2. Copy the secrets template and fill in your API keys / OAuth client (or export the env vars manually):
14+
15+
```bash
16+
cp mcp_agent.secrets.yaml.example mcp_agent.secrets.yaml
17+
```
18+
19+
3. Configuration is loaded from `mcp_agent.config.yaml` and secrets from
20+
`mcp_agent.secrets.yaml`. Populate the secrets file (or export the matching
21+
environment variables) with your GitHub OAuth credentials before running.
22+
23+
4. (Optional) To persist tokens across runs, start Redis and set `OAUTH_REDIS_URL`:
24+
25+
```bash
26+
docker run --rm -p 6379:6379 redis:7-alpine
27+
export OAUTH_REDIS_URL="redis://127.0.0.1:6379"
28+
```
29+
30+
5. Install deps and run:
31+
32+
```bash
33+
uv pip install -r requirements.txt
34+
# If you populated the secrets file you can skip these exports.
35+
export GITHUB_CLIENT_ID=...
36+
export GITHUB_CLIENT_SECRET=...
37+
uv run main.py
38+
```
39+
40+
On first run, a browser window opens to authorize GitHub; subsequent runs reuse the cached token.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import asyncio
2+
import inspect
3+
import os
4+
import time
5+
6+
from mcp_agent.app import MCPApp
7+
from mcp_agent.config import get_settings, OAuthTokenStoreSettings, OAuthSettings
8+
from mcp_agent.agents.agent import Agent
9+
from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
10+
from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
11+
from mcp_agent.tracing.token_counter import TokenSummary
12+
13+
14+
def _load_settings():
15+
signature = inspect.signature(get_settings)
16+
if "set_global" in signature.parameters:
17+
return get_settings(set_global=False)
18+
return get_settings()
19+
20+
21+
settings = _load_settings()
22+
23+
redis_url = os.environ.get("OAUTH_REDIS_URL")
24+
if redis_url:
25+
settings.oauth = settings.oauth or OAuthSettings()
26+
settings.oauth.token_store = OAuthTokenStoreSettings(
27+
backend="redis",
28+
redis_url=redis_url,
29+
)
30+
elif not getattr(settings.oauth, "token_store", None):
31+
settings.oauth = settings.oauth or OAuthSettings()
32+
settings.oauth.token_store = OAuthTokenStoreSettings()
33+
34+
github_settings = (
35+
settings.mcp.servers.get("github")
36+
if settings.mcp and settings.mcp.servers
37+
else None
38+
)
39+
github_oauth = (
40+
github_settings.auth.oauth
41+
if github_settings and github_settings.auth and github_settings.auth.oauth
42+
else None
43+
)
44+
45+
if not github_oauth or not github_oauth.client_id or not github_oauth.client_secret:
46+
raise SystemExit(
47+
"GitHub OAuth client_id/client_secret must be provided via mcp_agent.config.yaml or mcp_agent.secrets.yaml."
48+
)
49+
50+
app = MCPApp(
51+
name="oauth_basic_agent", settings=settings, session_id="oauth-basic-agent"
52+
)
53+
54+
55+
@app.tool()
56+
async def example_usage() -> str:
57+
async with app.run() as agent_app:
58+
logger = agent_app.logger
59+
context = agent_app.context
60+
result = ""
61+
62+
logger.info("Current config:", data=context.config.model_dump())
63+
64+
context.config.mcp.servers["filesystem"].args.extend([os.getcwd()])
65+
66+
finder_agent = Agent(
67+
name="finder",
68+
instruction="""You are an agent with access to the filesystem,
69+
as well as the ability to fetch URLs and GitHub MCP. Your job is to
70+
identify the closest match to a user's request, make the appropriate tool
71+
calls, and return useful results.""",
72+
server_names=["fetch", "filesystem", "github"],
73+
)
74+
75+
async with finder_agent:
76+
logger.info("finder: Connected to server, calling list_tools...")
77+
tools_list = await finder_agent.list_tools()
78+
logger.info("Tools available:", data=tools_list.model_dump())
79+
80+
llm = await finder_agent.attach_llm(OpenAIAugmentedLLM)
81+
82+
# GitHub MCP server use
83+
github_repos = await llm.generate_str(
84+
message="Use the GitHub MCP server to find the top 3 public repositories for the GitHub organization lastmile-ai and list their names.",
85+
)
86+
logger.info(
87+
f"Top 3 public repositories for the GitHub organization lastmile-ai: {github_repos}"
88+
)
89+
90+
result += f"\n\nTop 3 public repositories for the GitHub organization lastmile-ai: {github_repos}"
91+
92+
# Filesystem MCP server use
93+
config_contents = await llm.generate_str(
94+
message="Print the contents of mcp_agent.config.yaml verbatim",
95+
)
96+
logger.info(f"mcp_agent.config.yaml contents: {config_contents}")
97+
result += f"\n\nContents of mcp_agent.config.yaml: {config_contents}"
98+
99+
# Switch to Anthropic LLM
100+
llm = await finder_agent.attach_llm(AnthropicAugmentedLLM)
101+
102+
# fetch MCP server use
103+
mcp_introduction = await llm.generate_str(
104+
message="Print the first 2 paragraphs of https://modelcontextprotocol.io/introduction",
105+
)
106+
logger.info(
107+
f"First 2 paragraphs of Model Context Protocol docs: {mcp_introduction}"
108+
)
109+
result += f"\n\nFirst 2 paragraphs of Model Context Protocol docs: {mcp_introduction}"
110+
111+
await display_token_summary(agent_app)
112+
return result
113+
114+
115+
async def display_token_summary(app_ctx: MCPApp, agent: Agent | None = None):
116+
summary: TokenSummary = await app_ctx.get_token_summary()
117+
118+
print("\n" + "=" * 50)
119+
print("TOKEN USAGE SUMMARY")
120+
print("=" * 50)
121+
122+
print("\nTotal Usage:")
123+
print(f" Total tokens: {summary.usage.total_tokens:,}")
124+
print(f" Input tokens: {summary.usage.input_tokens:,}")
125+
print(f" Output tokens: {summary.usage.output_tokens:,}")
126+
print(f" Total cost: ${summary.cost:.4f}")
127+
128+
if summary.model_usage:
129+
print("\nBreakdown by Model:")
130+
for model_key, data in summary.model_usage.items():
131+
print(f"\n {model_key}:")
132+
print(
133+
f" Tokens: {data.usage.total_tokens:,} (input: {data.usage.input_tokens:,}, output: {data.usage.output_tokens:,})"
134+
)
135+
print(f" Cost: ${data.cost:.4f}")
136+
137+
print("\n" + "=" * 50)
138+
139+
140+
if __name__ == "__main__":
141+
start = time.time()
142+
asyncio.run(example_usage())
143+
end = time.time()
144+
print(f"Total run time: {end - start:.2f}s")
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
$schema: ../../../schema/mcp-agent.config.schema.json
2+
3+
execution_engine: asyncio
4+
logger:
5+
transports: [console, file]
6+
level: info
7+
path_settings:
8+
path_pattern: "logs/mcp-agent-{unique_id}.jsonl"
9+
unique_id: "timestamp"
10+
11+
oauth:
12+
loopback_ports: [33418, 33419, 33420]
13+
14+
mcp:
15+
servers:
16+
fetch:
17+
command: "uvx"
18+
args: ["mcp-server-fetch"]
19+
filesystem:
20+
command: "npx"
21+
args: ["-y", "@modelcontextprotocol/server-filesystem"]
22+
github:
23+
transport: streamable_http
24+
url: "https://api.githubcopilot.com/mcp/"
25+
auth:
26+
oauth:
27+
enabled: true
28+
scopes: ["read:org", "public_repo", "user:email"]
29+
authorization_server: "https://github.com/login/oauth"
30+
use_internal_callback: false
31+
include_resource_parameter: false
32+
33+
openai:
34+
default_model: "gpt-4o-mini"
35+
anthropic:
36+
default_model: claude-sonnet-4-20250514

0 commit comments

Comments
 (0)