Skip to content

Commit c1d0acc

Browse files
authored
Merge pull request #15 from sacha-development-stuff/codex/fix-merge-conflicts-and-maintain-fork-features
Resolve merge conflicts, integrate client-credential features
2 parents 9e06753 + b935a6f commit c1d0acc

File tree

3 files changed

+236
-1150
lines changed

3 files changed

+236
-1150
lines changed

examples/servers/simple-auth/mcp_simple_auth/server.py

Lines changed: 13 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -51,248 +51,20 @@ def __init__(self, **data):
5151
super().__init__(**data)
5252

5353

54-
# <<<<<<< main
55-
class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
56-
"""Simple GitHub OAuth provider with essential functionality."""
57-
58-
def __init__(self, settings: ServerSettings):
59-
self.settings = settings
60-
self.clients: dict[str, OAuthClientInformationFull] = {}
61-
self.auth_codes: dict[str, AuthorizationCode] = {}
62-
self.tokens: dict[str, AccessToken] = {}
63-
self.state_mapping: dict[str, dict[str, str]] = {}
64-
# Store GitHub tokens with MCP tokens using the format:
65-
# {"mcp_token": "github_token"}
66-
self.token_mapping: dict[str, str] = {}
67-
68-
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
69-
"""Get OAuth client information."""
70-
return self.clients.get(client_id)
71-
72-
async def register_client(self, client_info: OAuthClientInformationFull):
73-
"""Register a new OAuth client."""
74-
self.clients[client_info.client_id] = client_info
75-
76-
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
77-
"""Generate an authorization URL for GitHub OAuth flow."""
78-
state = params.state or secrets.token_hex(16)
79-
80-
# Store the state mapping
81-
self.state_mapping[state] = {
82-
"redirect_uri": str(params.redirect_uri),
83-
"code_challenge": params.code_challenge,
84-
"redirect_uri_provided_explicitly": str(params.redirect_uri_provided_explicitly),
85-
"client_id": client.client_id,
86-
}
87-
88-
# Build GitHub authorization URL
89-
auth_url = (
90-
f"{self.settings.github_auth_url}"
91-
f"?client_id={self.settings.github_client_id}"
92-
f"&redirect_uri={self.settings.github_callback_path}"
93-
f"&scope={self.settings.github_scope}"
94-
f"&state={state}"
95-
)
96-
97-
return auth_url
98-
99-
async def handle_github_callback(self, code: str, state: str) -> str:
100-
"""Handle GitHub OAuth callback."""
101-
state_data = self.state_mapping.get(state)
102-
if not state_data:
103-
raise HTTPException(400, "Invalid state parameter")
104-
105-
redirect_uri = state_data["redirect_uri"]
106-
code_challenge = state_data["code_challenge"]
107-
redirect_uri_provided_explicitly = state_data["redirect_uri_provided_explicitly"] == "True"
108-
client_id = state_data["client_id"]
109-
110-
# Exchange code for token with GitHub
111-
async with create_mcp_http_client() as client:
112-
response = await client.post(
113-
self.settings.github_token_url,
114-
data={
115-
"client_id": self.settings.github_client_id,
116-
"client_secret": self.settings.github_client_secret,
117-
"code": code,
118-
"redirect_uri": self.settings.github_callback_path,
119-
},
120-
headers={"Accept": "application/json"},
121-
)
122-
123-
if response.status_code != 200:
124-
raise HTTPException(400, "Failed to exchange code for token")
125-
126-
data = response.json()
127-
128-
if "error" in data:
129-
raise HTTPException(400, data.get("error_description", data["error"]))
130-
131-
github_token = data["access_token"]
132-
133-
# Create MCP authorization code
134-
new_code = f"mcp_{secrets.token_hex(16)}"
135-
auth_code = AuthorizationCode(
136-
code=new_code,
137-
client_id=client_id,
138-
redirect_uri=AnyHttpUrl(redirect_uri),
139-
redirect_uri_provided_explicitly=redirect_uri_provided_explicitly,
140-
expires_at=time.time() + 300,
141-
scopes=[self.settings.mcp_scope],
142-
code_challenge=code_challenge,
143-
)
144-
self.auth_codes[new_code] = auth_code
145-
146-
# Store GitHub token - we'll map the MCP token to this later
147-
self.tokens[github_token] = AccessToken(
148-
token=github_token,
149-
client_id=client_id,
150-
scopes=[self.settings.github_scope],
151-
expires_at=None,
152-
)
153-
154-
del self.state_mapping[state]
155-
return construct_redirect_uri(redirect_uri, code=new_code, state=state)
156-
157-
async def load_authorization_code(
158-
self, client: OAuthClientInformationFull, authorization_code: str
159-
) -> AuthorizationCode | None:
160-
"""Load an authorization code."""
161-
return self.auth_codes.get(authorization_code)
162-
163-
async def exchange_authorization_code(
164-
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
165-
) -> OAuthToken:
166-
"""Exchange authorization code for tokens."""
167-
if authorization_code.code not in self.auth_codes:
168-
raise ValueError("Invalid authorization code")
169-
170-
# Generate MCP access token
171-
mcp_token = f"mcp_{secrets.token_hex(32)}"
172-
173-
# Store MCP token
174-
self.tokens[mcp_token] = AccessToken(
175-
token=mcp_token,
176-
client_id=client.client_id,
177-
scopes=authorization_code.scopes,
178-
expires_at=int(time.time()) + 3600,
179-
)
180-
181-
# Find GitHub token for this client
182-
github_token = next(
183-
(
184-
token
185-
for token, data in self.tokens.items()
186-
# see https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
187-
# which you get depends on your GH app setup.
188-
if (token.startswith("ghu_") or token.startswith("gho_")) and data.client_id == client.client_id
189-
),
190-
None,
191-
)
192-
193-
# Store mapping between MCP token and GitHub token
194-
if github_token:
195-
self.token_mapping[mcp_token] = github_token
196-
197-
del self.auth_codes[authorization_code.code]
198-
199-
return OAuthToken(
200-
access_token=mcp_token,
201-
token_type="Bearer",
202-
expires_in=3600,
203-
scope=" ".join(authorization_code.scopes),
204-
)
205-
206-
async def load_access_token(self, token: str) -> AccessToken | None:
207-
"""Load and validate an access token."""
208-
access_token = self.tokens.get(token)
209-
if not access_token:
210-
return None
211-
212-
# Check if expired
213-
if access_token.expires_at and access_token.expires_at < time.time():
214-
del self.tokens[token]
215-
return None
216-
217-
return access_token
218-
219-
async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
220-
"""Load a refresh token - not supported."""
221-
return None
222-
223-
async def exchange_refresh_token(
224-
self,
225-
client: OAuthClientInformationFull,
226-
refresh_token: RefreshToken,
227-
scopes: list[str],
228-
) -> OAuthToken:
229-
"""Exchange refresh token"""
230-
raise NotImplementedError("Not supported")
231-
232-
async def exchange_token(
233-
self,
234-
client: OAuthClientInformationFull,
235-
subject_token: str,
236-
subject_token_type: str,
237-
actor_token: str | None,
238-
actor_token_type: str | None,
239-
scope: list[str] | None,
240-
audience: str | None,
241-
resource: str | None,
242-
) -> OAuthToken:
243-
"""Exchange an external token for an MCP access token."""
244-
raise NotImplementedError("Token exchange is not supported")
245-
246-
async def exchange_client_credentials(self, client: OAuthClientInformationFull, scopes: list[str]) -> OAuthToken:
247-
"""Exchange client credentials for an access token."""
248-
token = f"mcp_{secrets.token_hex(32)}"
249-
self.tokens[token] = AccessToken(
250-
token=token,
251-
client_id=client.client_id,
252-
scopes=scopes,
253-
expires_at=int(time.time()) + 3600,
254-
)
255-
return OAuthToken(
256-
access_token=token,
257-
token_type="Bearer",
258-
expires_in=3600,
259-
scope=" ".join(scopes),
260-
)
261-
262-
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
263-
"""Revoke a token."""
264-
if token in self.tokens:
265-
del self.tokens[token]
266-
267-
268-
def create_simple_mcp_server(settings: ServerSettings) -> FastMCP:
269-
"""Create a simple FastMCP server with GitHub OAuth."""
270-
oauth_provider = SimpleGitHubOAuthProvider(settings)
54+
def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
55+
"""
56+
Create MCP Resource Server with token introspection.
27157
272-
auth_settings = AuthSettings(
273-
issuer_url=settings.server_url,
274-
client_registration_options=ClientRegistrationOptions(
275-
enabled=True,
276-
valid_scopes=[settings.mcp_scope],
277-
default_scopes=[settings.mcp_scope],
278-
),
279-
required_scopes=[settings.mcp_scope],
280-
# =======
281-
# def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
282-
# """
283-
# Create MCP Resource Server with token introspection.
284-
285-
# This server:
286-
# 1. Provides protected resource metadata (RFC 9728)
287-
# 2. Validates tokens via Authorization Server introspection
288-
# 3. Serves MCP tools and resources
289-
# """
290-
# # Create token verifier for introspection with RFC 8707 resource validation
291-
# token_verifier = IntrospectionTokenVerifier(
292-
# introspection_endpoint=settings.auth_server_introspection_endpoint,
293-
# server_url=str(settings.server_url),
294-
# validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set
295-
# >>>>>>> main
58+
This server:
59+
1. Provides protected resource metadata (RFC 9728)
60+
2. Validates tokens via Authorization Server introspection
61+
3. Serves MCP tools and resources
62+
"""
63+
# Create token verifier for introspection with RFC 8707 resource validation
64+
token_verifier = IntrospectionTokenVerifier(
65+
introspection_endpoint=settings.auth_server_introspection_endpoint,
66+
server_url=str(settings.server_url),
67+
validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set
29668
)
29769

29870
# Create FastMCP server as a Resource Server

0 commit comments

Comments
 (0)