Skip to content

Commit df2c91a

Browse files
committed
Add support for remote-oauth-support
1 parent 2ca2de7 commit df2c91a

File tree

10 files changed

+411
-11
lines changed

10 files changed

+411
-11
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Simple MCP Server with GitHub OAuth Authentication
2+
3+
This is a simple example of an MCP server with GitHub OAuth authentication. It demonstrates the essential components needed for OAuth integration with just a single tool.
4+
5+
This is just an example of a server that uses auth, an official GitHub mcp server is [here](https://github.com/github/github-mcp-server)
6+
7+
## Overview
8+
9+
This simple demo to show to set up a server with:
10+
- GitHub OAuth2 authorization flow
11+
- Single tool: `get_user_profile` to retrieve GitHub user information
12+
13+
14+
## Prerequisites
15+
16+
1. Create a GitHub OAuth App:
17+
- Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App
18+
- Application name: Any name (e.g., "Simple MCP Auth Demo")
19+
- Homepage URL: `http://localhost:8000`
20+
- Authorization callback URL: `http://localhost:8000/github/callback`
21+
- Click "Register application"
22+
- Note down your Client ID and Client Secret
23+
24+
## Required Environment Variables
25+
26+
You MUST set these environment variables before running the server:
27+
28+
```bash
29+
export MCP_GITHUB_GITHUB_CLIENT_ID="your_client_id_here"
30+
export MCP_GITHUB_GITHUB_CLIENT_SECRET="your_client_secret_here"
31+
```
32+
33+
The server will not start without these environment variables properly set.
34+
35+
36+
## Running the Server
37+
38+
```bash
39+
# Set environment variables first (see above)
40+
41+
# Run the server
42+
uv run mcp-simple-auth
43+
```
44+
45+
The server will start on `http://localhost:8000`.
46+
47+
### Transport Options
48+
49+
This server supports multiple transport protocols that can run on the same port:
50+
51+
#### SSE (Server-Sent Events) - Default
52+
```bash
53+
uv run mcp-simple-auth
54+
# or explicitly:
55+
uv run mcp-simple-auth --transport sse
56+
```
57+
58+
SSE transport provides endpoint:
59+
- `/sse`
60+
61+
#### Streamable HTTP
62+
```bash
63+
uv run mcp-simple-auth --transport streamable-http
64+
```
65+
66+
Streamable HTTP transport provides endpoint:
67+
- `/mcp`
68+
69+
70+
This ensures backward compatibility without needing multiple server instances. When using SSE transport (`--transport sse`), only the `/sse` endpoint is available.
71+
72+
## Available Tool
73+
74+
### get_user_profile
75+
76+
The only tool in this simple example. Returns the authenticated user's GitHub profile information.
77+
78+
**Required scope**: `user`
79+
80+
**Returns**: GitHub user profile data including username, email, bio, etc.
81+
82+
83+
## Troubleshooting
84+
85+
If the server fails to start, check:
86+
1. Environment variables `MCP_GITHUB_GITHUB_CLIENT_ID` and `MCP_GITHUB_GITHUB_CLIENT_SECRET` are set
87+
2. The GitHub OAuth app callback URL matches `http://localhost:8000/github/callback`
88+
3. No other service is using port 8000
89+
4. The transport specified is valid (`sse` or `streamable-http`)
90+
91+
You can use [Inspector](https://github.com/modelcontextprotocol/inspector) to test Auth
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Simple MCP server with GitHub OAuth authentication."""
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Main entry point for simple MCP server with GitHub OAuth authentication."""
2+
3+
import sys
4+
5+
from mcp_simple_remote_auth.server import main
6+
7+
sys.exit(main()) # type: ignore[call-arg]
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Simple MCP Server with GitHub OAuth Authentication."""
2+
3+
import logging
4+
import secrets
5+
import time
6+
from typing import Any, Literal
7+
8+
import click
9+
from pydantic import AnyHttpUrl
10+
from pydantic_settings import BaseSettings, SettingsConfigDict
11+
from starlette.exceptions import HTTPException
12+
from starlette.requests import Request
13+
from starlette.responses import JSONResponse, RedirectResponse, Response
14+
15+
from mcp.server.auth.middleware.auth_context import get_access_token
16+
from mcp.server.auth.provider import (
17+
AccessToken,
18+
AuthorizationCode,
19+
AuthorizationParams,
20+
OAuthAuthorizationServerProvider,
21+
RefreshToken,
22+
TokenValidator,
23+
construct_redirect_uri,
24+
)
25+
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
26+
from mcp.server.fastmcp.server import FastMCP
27+
from mcp.shared._httpx_utils import create_mcp_http_client
28+
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
29+
30+
logger = logging.getLogger(__name__)
31+
32+
33+
class ServerSettings(BaseSettings):
34+
"""Settings for the simple GitHub MCP server."""
35+
36+
model_config = SettingsConfigDict(env_prefix="MCP_GITHUB_")
37+
38+
# Server settings
39+
host: str = "localhost"
40+
port: int = 8000
41+
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8000")
42+
mcp_scope: str = "user"
43+
44+
def __init__(self, **data):
45+
"""Initialize settings with values from environment variables.
46+
47+
Note: github_client_id and github_client_secret are required but can be
48+
loaded automatically from environment variables (MCP_GITHUB_GITHUB_CLIENT_ID
49+
and MCP_GITHUB_GITHUB_CLIENT_SECRET) and don't need to be passed explicitly.
50+
"""
51+
super().__init__(**data)
52+
53+
54+
def create_simple_mcp_server(settings: ServerSettings) -> FastMCP:
55+
"""Create a simple FastMCP server with GitHub OAuth."""
56+
57+
auth_settings = AuthSettings(
58+
issuer_url=settings.server_url,
59+
client_registration_options=ClientRegistrationOptions(
60+
enabled=True,
61+
valid_scopes=[settings.mcp_scope],
62+
default_scopes=[settings.mcp_scope],
63+
),
64+
required_scopes=[settings.mcp_scope],
65+
)
66+
67+
app = FastMCP(
68+
name="Simple GitHub MCP Server",
69+
instructions="A simple MCP server with GitHub OAuth authentication",
70+
host=settings.host,
71+
port=settings.port,
72+
debug=True,
73+
auth=auth_settings,
74+
token_validator=TokenValidator(),
75+
protected_resource_metadata={"resource": "asdasd", "authorization_servers": ["https://auth.devramp.ai"], "scopes_supported": ["user"]}
76+
)
77+
78+
@app.tool()
79+
async def get_user_profile() -> dict[str, Any]:
80+
"""Get the authenticated user's GitHub profile information.
81+
82+
This is the only tool in our simple example. It requires the 'user' scope.
83+
"""
84+
return {"user": "asdasd"}
85+
86+
return app
87+
88+
89+
@click.command()
90+
@click.option("--port", default=8000, help="Port to listen on")
91+
@click.option("--host", default="localhost", help="Host to bind to")
92+
@click.option(
93+
"--transport",
94+
default="streamable-http",
95+
type=click.Choice(["sse", "streamable-http"]),
96+
help="Transport protocol to use ('sse' or 'streamable-http')",
97+
)
98+
def main(port: int, host: str, transport: Literal["sse", "streamable-http"]) -> int:
99+
"""Run the simple GitHub MCP server."""
100+
logging.basicConfig(level=logging.INFO)
101+
102+
try:
103+
# No hardcoded credentials - all from environment variables
104+
settings = ServerSettings(host=host, port=port)
105+
except ValueError as e:
106+
logger.error(
107+
"Failed to load settings. Make sure environment variables are set:"
108+
)
109+
logger.error(" MCP_GITHUB_GITHUB_CLIENT_ID=<your-client-id>")
110+
logger.error(" MCP_GITHUB_GITHUB_CLIENT_SECRET=<your-client-secret>")
111+
logger.error(f"Error: {e}")
112+
return 1
113+
114+
mcp_server = create_simple_mcp_server(settings)
115+
logger.info(f"Starting server with {transport} transport")
116+
mcp_server.run(transport=transport)
117+
return 0
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[project]
2+
name = "mcp-simple-remote-auth"
3+
version = "0.1.0"
4+
description = "A simple MCP server demonstrating OAuth authentication"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
authors = [{ name = "Anthropic, PBC." }]
8+
license = { text = "MIT" }
9+
dependencies = [
10+
"anyio>=4.5",
11+
"click>=8.1.0",
12+
"httpx>=0.27",
13+
"mcp",
14+
"pydantic>=2.0",
15+
"pydantic-settings>=2.5.2",
16+
"sse-starlette>=1.6.1",
17+
"uvicorn>=0.23.1; sys_platform != 'emscripten'",
18+
]
19+
20+
[project.scripts]
21+
mcp-simple-remote-auth = "mcp_simple_remote_auth.server:main"
22+
23+
[build-system]
24+
requires = ["hatchling"]
25+
build-backend = "hatchling.build"
26+
27+
[tool.hatch.build.targets.wheel]
28+
packages = ["mcp_simple_remote_auth"]
29+
30+
[tool.uv]
31+
dev-dependencies = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]

src/mcp/server/auth/middleware/bearer_auth.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from starlette.requests import HTTPConnection
1111
from starlette.types import Receive, Scope, Send
1212

13-
from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider
13+
from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, TokenValidator
1414

1515

1616
class AuthenticatedUser(SimpleUser):
@@ -21,6 +21,42 @@ def __init__(self, auth_info: AccessToken):
2121
self.access_token = auth_info
2222
self.scopes = auth_info.scopes
2323

24+
class JWTBearerTokenAuthBackend(AuthenticationBackend):
25+
"""
26+
Authentication backend that validates Bearer tokens.
27+
"""
28+
29+
def __init__(
30+
self,
31+
provider: TokenValidator[AccessToken],
32+
):
33+
self.provider = provider
34+
35+
async def authenticate(self, conn: HTTPConnection):
36+
auth_header = next(
37+
(
38+
conn.headers.get(key)
39+
for key in conn.headers
40+
if key.lower() == "authorization"
41+
),
42+
None,
43+
)
44+
if not auth_header or not auth_header.lower().startswith("bearer "):
45+
return None
46+
47+
token = auth_header[7:] # Remove "Bearer " prefix
48+
49+
# Validate the token with the provider
50+
auth_info = await self.provider.validate_token(token)
51+
52+
53+
if not auth_info:
54+
return None
55+
56+
if auth_info.expires_at and auth_info.expires_at < int(time.time()):
57+
return None
58+
59+
return AuthCredentials(auth_info.scopes), AuthenticatedUser(auth_info)
2460

2561
class BearerAuthBackend(AuthenticationBackend):
2662
"""

src/mcp/server/auth/provider.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Generic, Literal, Protocol, TypeVar
2+
from typing import Generic, Literal, Protocol, TypeVar, Any
33
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
44

55
from pydantic import AnyHttpUrl, BaseModel
@@ -10,6 +10,14 @@
1010
)
1111

1212

13+
# Define type variables
14+
AccessTokenT = TypeVar('AccessTokenT', bound='AccessToken')
15+
16+
class TokenValidator(Generic[AccessTokenT], BaseModel):
17+
async def validate_token(self, token: str) -> AccessTokenT | None:
18+
...
19+
20+
1321
class AuthorizationParams(BaseModel):
1422
state: str | None
1523
scopes: list[str] | None

0 commit comments

Comments
 (0)