-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathgithub_graphql_mcp_server.py
More file actions
302 lines (270 loc) · 9.48 KB
/
github_graphql_mcp_server.py
File metadata and controls
302 lines (270 loc) · 9.48 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import os
import sys
import httpx
import json
import logging
from typing import Any, Dict, Optional
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
stream=sys.stderr)
# Helper function to print to stderr
# def log(message):
# print(message, file=sys.stderr)
# GitHub Configuration - get directly from environment variables
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
# Simplify error handling and do more logging
if not GITHUB_TOKEN:
logging.error("GitHub token not found in environment variables")
logging.warning(f"Available environment variables: {list(os.environ.keys())}")
else:
logging.info(f"Successfully loaded GitHub token starting with: {GITHUB_TOKEN[:4]}")
# GitHub GraphQL API Endpoint
GITHUB_GRAPHQL_API_URL = "https://api.github.com/graphql"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("github-graphql", version="0.1.0")
logging.info("GitHub GraphQL MCP Server initialized.")
async def make_github_request(query: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Makes an authenticated GraphQL request to the GitHub API.
Handles authentication and error checking.
"""
if not GITHUB_TOKEN:
logging.error("GitHub API token is missing. Cannot make request.")
return {"errors": [{"message": "Server missing GitHub API token."}]}
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
"User-Agent": "MCPGitHubServer/0.1.0"
}
payload = {"query": query}
if variables:
payload["variables"] = variables
async with httpx.AsyncClient() as client:
try:
logging.debug(f"Sending request to GitHub: {query[:100]}...")
response = await client.post(
GITHUB_GRAPHQL_API_URL,
headers=headers,
json=payload,
timeout=30.0
)
# Log Rate Limit Info
rate_limit = response.headers.get('X-RateLimit-Limit')
rate_remaining = response.headers.get('X-RateLimit-Remaining')
rate_reset = response.headers.get('X-RateLimit-Reset')
if rate_limit is not None and rate_remaining is not None:
logging.info(f"GitHub Rate Limit: {rate_remaining}/{rate_limit} remaining. Resets at timestamp {rate_reset}.")
if int(rate_remaining) < 50:
logging.warning(f"GitHub Rate Limit low: {rate_remaining} remaining.")
response.raise_for_status()
logging.debug(f"GitHub response status: {response.status_code}")
result = response.json()
# Check for GraphQL errors within the response body
if "errors" in result:
logging.warning(f"GraphQL Errors: {result['errors']}")
return result
except httpx.RequestError as e:
logging.error(f"HTTP Request Error: {e}", exc_info=True)
return {"errors": [{"message": f"HTTP Request Error connecting to GitHub: {e}"}]}
except httpx.HTTPStatusError as e:
logging.error(f"HTTP Status Error: {e.response.status_code} - Response: {e.response.text[:500]}", exc_info=True)
error_detail = f"HTTP Status Error: {e.response.status_code}"
try:
# Try to parse GitHub's error response if JSON
err_resp = e.response.json()
if "errors" in err_resp:
error_detail += f" - {err_resp['errors'][0]['message']}"
elif "message" in err_resp:
error_detail += f" - {err_resp['message']}"
else:
pass
except json.JSONDecodeError:
pass
return {"errors": [{"message": error_detail}]}
except Exception as e:
logging.error(f"Generic Error during GitHub request: {e}", exc_info=True)
return {"errors": [{"message": f"An unexpected error occurred: {e}"}]}
@mcp.tool()
async def github_execute_graphql(query: str, variables: Dict[str, Any] = None) -> str:
"""
Executes an arbitrary GraphQL query or mutation against the GitHub API.
This powerful tool provides unlimited flexibility for any GitHub GraphQL operation
by directly passing queries with full control over selection sets and variables.
## GraphQL Introspection
You can discover the GitHub API schema using GraphQL introspection queries such as:
```graphql
# Get all available query types
query IntrospectionQuery {
__schema {
queryType { name }
types {
name
kind
description
fields {
name
description
args {
name
description
type { name kind }
}
type { name kind }
}
}
}
}
# Get details for a specific type
query TypeQuery {
__type(name: "Repository") {
name
description
fields {
name
description
type { name kind ofType { name kind } }
}
}
}
```
## Common Operation Patterns
### Fetching a repository
```graphql
query GetRepository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
description
url
stargazerCount
forkCount
issues(first: 10, states: OPEN) {
nodes {
title
url
createdAt
}
}
}
}
```
Variables: `{"owner": "octocat", "name": "Hello-World"}`
### Fetching user information
```graphql
query GetUser($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
repositories(first: 10, orderBy: {field: STARGAZERS, direction: DESC}) {
nodes {
name
description
stargazerCount
}
}
}
}
```
Variables: `{"login": "octocat"}`
### Creating an issue
```graphql
mutation CreateIssue($repositoryId: ID!, $title: String!, $body: String) {
createIssue(input: {
repositoryId: $repositoryId,
title: $title,
body: $body
}) {
issue {
id
url
number
}
}
}
```
### Searching repositories
```graphql
query SearchRepositories($query: String!, $first: Int!) {
search(query: $query, type: REPOSITORY, first: $first) {
repositoryCount
edges {
node {
... on Repository {
name
owner {
login
}
description
url
stargazerCount
}
}
}
}
}
```
Variables: `{"query": "language:javascript stars:>1000", "first": 10}`
## Pagination
For paginated results, use the `after` parameter with the `endCursor` from previous queries:
```graphql
query GetNextPage($login: String!, $after: String) {
user(login: $login) {
repositories(first: 10, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
name
}
}
}
}
```
## Error Handling Tips
- Check for the "errors" array in the response
- Common error reasons:
- Invalid GraphQL syntax: verify query structure
- Unknown fields: check field names through introspection
- Missing required fields: ensure all required fields are in queries
- Permission issues: verify API token has appropriate permissions
- Rate limits: GitHub has API rate limits which may be exceeded
## Variables Usage
Variables should be provided as a Python dictionary where:
- Keys match the variable names defined in the query/mutation
- Values follow the appropriate data types expected by GitHub
- Nested objects must be structured according to GraphQL input types
Args:
query: The complete GraphQL query or mutation to execute.
variables: Optional dictionary of variables for the query. Should match
the parameter names defined in the query with appropriate types.
Returns:
JSON string containing the complete response from GitHub, including data and errors if any.
"""
if not query:
logging.warning("Received empty query for github_execute_graphql.")
return json.dumps({"errors": [{"message": "Query cannot be empty."}]})
logging.info(f"Executing github_execute_graphql with query starting: {query[:50]}...")
# Make the API call
result = await make_github_request(query, variables)
# Return the raw result as JSON
return json.dumps(result)
if __name__ == "__main__":
logging.info("Attempting to run GitHub GraphQL MCP server via stdio...")
# Basic check before running
if not GITHUB_TOKEN:
logging.critical("FATAL: Cannot start server, GitHub token missing.")
sys.exit(1)
else:
logging.info(f"Configured for GitHub GraphQL API with token: {GITHUB_TOKEN[:4]}...")
try:
mcp.run(transport='stdio')
logging.info("Server stopped.")
except Exception as e:
logging.exception("Error running server")
sys.exit(1)