-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
165 lines (132 loc) · 5.24 KB
/
main.py
File metadata and controls
165 lines (132 loc) · 5.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""GitHub API Integration with Keycard Delegated Access.
This example demonstrates how to use the @grant decorator to request
token exchange for accessing external APIs (GitHub) on behalf of
authenticated users.
Key concepts demonstrated:
- AuthProvider setup with ClientSecret credentials
- @grant decorator for requesting token exchange
- AccessContext for accessing exchanged tokens
- Comprehensive error handling patterns
"""
import os
import httpx
from fastmcp import Context, FastMCP
from keycardai.mcp.integrations.fastmcp import AccessContext, AuthProvider, ClientSecret
# Configure Keycard authentication with client credentials for delegated access
# Set KEYCARD_ZONE_ID (or KEYCARD_ZONE_URL) and client credentials from console.keycard.ai
auth_provider = AuthProvider(
mcp_server_name="GitHub API Server",
mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000/"),
# ClientSecret enables token exchange for delegated access
application_credential=ClientSecret(
(
os.getenv("KEYCARD_CLIENT_ID"),
os.getenv("KEYCARD_CLIENT_SECRET"),
)
),
)
# Get the RemoteAuthProvider for FastMCP
auth = auth_provider.get_remote_auth_provider()
# Create authenticated FastMCP server
mcp = FastMCP("GitHub API Server", auth=auth)
@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def get_github_user(ctx: Context) -> dict:
"""Get the authenticated GitHub user's profile.
Demonstrates:
- Basic @grant decorator usage
- Error checking with has_errors()
- Token access via AccessContext
Args:
ctx: FastMCP context with Keycard authentication state
Returns:
User profile data or error details
"""
# Get access context from FastMCP context namespace
access_context: AccessContext = await ctx.get_state("keycardai")
# Check for any errors (global or resource-specific)
if access_context.has_errors():
errors = access_context.get_errors()
return {"error": "Token exchange failed", "details": errors}
# Get the exchanged token for GitHub API
token = access_context.access("https://api.github.com").access_token
# Call GitHub API with delegated token
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
},
)
if response.status_code != 200:
return {
"error": f"GitHub API error: {response.status_code}",
"details": response.text,
}
user_data = response.json()
return {
"login": user_data.get("login"),
"name": user_data.get("name"),
"email": user_data.get("email"),
"public_repos": user_data.get("public_repos"),
"followers": user_data.get("followers"),
}
@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def list_github_repos(ctx: Context, per_page: int = 5) -> dict:
"""List the authenticated user's GitHub repositories.
Demonstrates:
- Resource-specific error checking with has_resource_error()
- Getting resource-specific errors with get_resource_errors()
- Parameterized API calls
Args:
ctx: FastMCP context with Keycard authentication state
per_page: Number of repositories to return (default: 5)
Returns:
List of repositories or error details
"""
access_context: AccessContext = await ctx.get_state("keycardai")
# Check for resource-specific error (alternative to has_errors())
if access_context.has_resource_error("https://api.github.com"):
resource_errors = access_context.get_resource_errors("https://api.github.com")
return {
"message": "Token exchange failed for GitHub API",
"details": resource_errors,
}
# Check for global errors (e.g., no auth token available)
if access_context.has_error():
return {"error": "Global token error", "details": access_context.get_error()}
token = access_context.access("https://api.github.com").access_token
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.github.com/user/repos",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
},
params={"per_page": per_page, "sort": "updated"},
)
if response.status_code != 200:
return {
"error": f"GitHub API error: {response.status_code}",
"details": response.text,
}
repos = response.json()
return {
"count": len(repos),
"repositories": [
{
"name": repo.get("name"),
"full_name": repo.get("full_name"),
"private": repo.get("private"),
"html_url": repo.get("html_url"),
}
for repo in repos
],
}
def main():
"""Entry point for the MCP server."""
mcp.run(transport="streamable-http")
if __name__ == "__main__":
main()