The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
└── github_graphql_mcp_server.py


/github_graphql_mcp_server.py:
--------------------------------------------------------------------------------
  1 | import os
  2 | import sys
  3 | import httpx
  4 | import json
  5 | import logging
  6 | from typing import Any, Dict, Optional
  7 | from dotenv import load_dotenv
  8 | 
  9 | # Load environment variables from .env file
 10 | load_dotenv()
 11 | 
 12 | # Configure logging
 13 | logging.basicConfig(level=logging.INFO,
 14 |                     format='%(asctime)s - %(levelname)s - %(message)s',
 15 |                     stream=sys.stderr)
 16 | 
 17 | # Helper function to print to stderr
 18 | # def log(message):
 19 | #    print(message, file=sys.stderr)
 20 | 
 21 | # GitHub Configuration - get directly from environment variables
 22 | GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
 23 | 
 24 | # Simplify error handling and do more logging
 25 | if not GITHUB_TOKEN:
 26 |     logging.error("GitHub token not found in environment variables")
 27 |     logging.warning(f"Available environment variables: {list(os.environ.keys())}")
 28 | else:
 29 |     logging.info(f"Successfully loaded GitHub token starting with: {GITHUB_TOKEN[:4]}")
 30 | 
 31 | # GitHub GraphQL API Endpoint
 32 | GITHUB_GRAPHQL_API_URL = "https://api.github.com/graphql"
 33 | 
 34 | from mcp.server.fastmcp import FastMCP
 35 | mcp = FastMCP("github-graphql", version="0.1.0")
 36 | logging.info("GitHub GraphQL MCP Server initialized.")
 37 | 
 38 | async def make_github_request(query: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
 39 |     """
 40 |     Makes an authenticated GraphQL request to the GitHub API.
 41 |     Handles authentication and error checking.
 42 |     """
 43 |     if not GITHUB_TOKEN:
 44 |         logging.error("GitHub API token is missing. Cannot make request.")
 45 |         return {"errors": [{"message": "Server missing GitHub API token."}]}
 46 | 
 47 |     headers = {
 48 |         "Authorization": f"Bearer {GITHUB_TOKEN}",
 49 |         "Content-Type": "application/json",
 50 |         "User-Agent": "MCPGitHubServer/0.1.0"
 51 |     }
 52 | 
 53 |     payload = {"query": query}
 54 |     if variables:
 55 |         payload["variables"] = variables
 56 | 
 57 |     async with httpx.AsyncClient() as client:
 58 |         try:
 59 |             logging.debug(f"Sending request to GitHub: {query[:100]}...")
 60 |             response = await client.post(
 61 |                 GITHUB_GRAPHQL_API_URL,
 62 |                 headers=headers,
 63 |                 json=payload,
 64 |                 timeout=30.0
 65 |             )
 66 | 
 67 |             # Log Rate Limit Info
 68 |             rate_limit = response.headers.get('X-RateLimit-Limit')
 69 |             rate_remaining = response.headers.get('X-RateLimit-Remaining')
 70 |             rate_reset = response.headers.get('X-RateLimit-Reset')
 71 |             if rate_limit is not None and rate_remaining is not None:
 72 |                  logging.info(f"GitHub Rate Limit: {rate_remaining}/{rate_limit} remaining. Resets at timestamp {rate_reset}.")
 73 |                  if int(rate_remaining) < 50:
 74 |                      logging.warning(f"GitHub Rate Limit low: {rate_remaining} remaining.")
 75 | 
 76 |             response.raise_for_status()
 77 |             logging.debug(f"GitHub response status: {response.status_code}")
 78 |             result = response.json()
 79 |             # Check for GraphQL errors within the response body
 80 |             if "errors" in result:
 81 |                 logging.warning(f"GraphQL Errors: {result['errors']}")
 82 |             return result
 83 |         except httpx.RequestError as e:
 84 |             logging.error(f"HTTP Request Error: {e}", exc_info=True)
 85 |             return {"errors": [{"message": f"HTTP Request Error connecting to GitHub: {e}"}]}
 86 |         except httpx.HTTPStatusError as e:
 87 |             logging.error(f"HTTP Status Error: {e.response.status_code} - Response: {e.response.text[:500]}", exc_info=True)
 88 |             error_detail = f"HTTP Status Error: {e.response.status_code}"
 89 |             try:
 90 |                 # Try to parse GitHub's error response if JSON
 91 |                 err_resp = e.response.json()
 92 |                 if "errors" in err_resp:
 93 |                     error_detail += f" - {err_resp['errors'][0]['message']}"
 94 |                 elif "message" in err_resp:
 95 |                     error_detail += f" - {err_resp['message']}"
 96 |                 else:
 97 |                     pass
 98 |             except json.JSONDecodeError:
 99 |                  pass
100 | 
101 |             return {"errors": [{"message": error_detail}]}
102 |         except Exception as e:
103 |             logging.error(f"Generic Error during GitHub request: {e}", exc_info=True)
104 |             return {"errors": [{"message": f"An unexpected error occurred: {e}"}]}
105 | 
106 | @mcp.tool()
107 | async def github_execute_graphql(query: str, variables: Dict[str, Any] = None) -> str:
108 |     """
109 |     Executes an arbitrary GraphQL query or mutation against the GitHub API.
110 |     This powerful tool provides unlimited flexibility for any GitHub GraphQL operation
111 |     by directly passing queries with full control over selection sets and variables.
112 |     
113 |     ## GraphQL Introspection
114 |     You can discover the GitHub API schema using GraphQL introspection queries such as:
115 |     
116 |     ```graphql
117 |     # Get all available query types
118 |     query IntrospectionQuery {
119 |       __schema {
120 |         queryType { name }
121 |         types {
122 |           name
123 |           kind
124 |           description
125 |           fields {
126 |             name
127 |             description
128 |             args {
129 |               name
130 |               description
131 |               type { name kind }
132 |             }
133 |             type { name kind }
134 |           }
135 |         }
136 |       }
137 |     }
138 |     
139 |     # Get details for a specific type
140 |     query TypeQuery {
141 |       __type(name: "Repository") {
142 |         name
143 |         description
144 |         fields {
145 |           name
146 |           description
147 |           type { name kind ofType { name kind } }
148 |         }
149 |       }
150 |     }
151 |     ```
152 |     
153 |     ## Common Operation Patterns
154 |     
155 |     ### Fetching a repository
156 |     ```graphql
157 |     query GetRepository($owner: String!, $name: String!) {
158 |       repository(owner: $owner, name: $name) {
159 |         name
160 |         description
161 |         url
162 |         stargazerCount
163 |         forkCount
164 |         issues(first: 10, states: OPEN) {
165 |           nodes {
166 |             title
167 |             url
168 |             createdAt
169 |           }
170 |         }
171 |       }
172 |     }
173 |     ```
174 |     Variables: `{"owner": "octocat", "name": "Hello-World"}`
175 |     
176 |     ### Fetching user information
177 |     ```graphql
178 |     query GetUser($login: String!) {
179 |       user(login: $login) {
180 |         name
181 |         bio
182 |         avatarUrl
183 |         url
184 |         repositories(first: 10, orderBy: {field: STARGAZERS, direction: DESC}) {
185 |           nodes {
186 |             name
187 |             description
188 |             stargazerCount
189 |           }
190 |         }
191 |       }
192 |     }
193 |     ```
194 |     Variables: `{"login": "octocat"}`
195 |     
196 |     ### Creating an issue
197 |     ```graphql
198 |     mutation CreateIssue($repositoryId: ID!, $title: String!, $body: String) {
199 |       createIssue(input: {
200 |         repositoryId: $repositoryId,
201 |         title: $title,
202 |         body: $body
203 |       }) {
204 |         issue {
205 |           id
206 |           url
207 |           number
208 |         }
209 |       }
210 |     }
211 |     ```
212 |     
213 |     ### Searching repositories
214 |     ```graphql
215 |     query SearchRepositories($query: String!, $first: Int!) {
216 |       search(query: $query, type: REPOSITORY, first: $first) {
217 |         repositoryCount
218 |         edges {
219 |           node {
220 |             ... on Repository {
221 |               name
222 |               owner {
223 |                 login
224 |               }
225 |               description
226 |               url
227 |               stargazerCount
228 |             }
229 |           }
230 |         }
231 |       }
232 |     }
233 |     ```
234 |     Variables: `{"query": "language:javascript stars:>1000", "first": 10}`
235 |     
236 |     ## Pagination
237 |     For paginated results, use the `after` parameter with the `endCursor` from previous queries:
238 |     ```graphql
239 |     query GetNextPage($login: String!, $after: String) {
240 |       user(login: $login) {
241 |         repositories(first: 10, after: $after) {
242 |           pageInfo {
243 |             hasNextPage
244 |             endCursor
245 |           }
246 |           nodes {
247 |             name
248 |           }
249 |         }
250 |       }
251 |     }
252 |     ```
253 |     
254 |     ## Error Handling Tips
255 |     - Check for the "errors" array in the response
256 |     - Common error reasons:
257 |       - Invalid GraphQL syntax: verify query structure
258 |       - Unknown fields: check field names through introspection
259 |       - Missing required fields: ensure all required fields are in queries
260 |       - Permission issues: verify API token has appropriate permissions
261 |       - Rate limits: GitHub has API rate limits which may be exceeded
262 |     
263 |     ## Variables Usage
264 |     Variables should be provided as a Python dictionary where:
265 |     - Keys match the variable names defined in the query/mutation
266 |     - Values follow the appropriate data types expected by GitHub
267 |     - Nested objects must be structured according to GraphQL input types
268 |     
269 |     Args:
270 |         query: The complete GraphQL query or mutation to execute.
271 |         variables: Optional dictionary of variables for the query. Should match
272 |                   the parameter names defined in the query with appropriate types.
273 | 
274 |     Returns:
275 |         JSON string containing the complete response from GitHub, including data and errors if any.
276 |     """
277 |     if not query:
278 |         logging.warning("Received empty query for github_execute_graphql.")
279 |         return json.dumps({"errors": [{"message": "Query cannot be empty."}]})
280 | 
281 |     logging.info(f"Executing github_execute_graphql with query starting: {query[:50]}...")
282 | 
283 |     # Make the API call
284 |     result = await make_github_request(query, variables)
285 | 
286 |     # Return the raw result as JSON
287 |     return json.dumps(result)
288 | 
289 | if __name__ == "__main__":
290 |     logging.info("Attempting to run GitHub GraphQL MCP server via stdio...")
291 |     # Basic check before running
292 |     if not GITHUB_TOKEN:
293 |         logging.critical("FATAL: Cannot start server, GitHub token missing.")
294 |         sys.exit(1)
295 |     else:
296 |         logging.info(f"Configured for GitHub GraphQL API with token: {GITHUB_TOKEN[:4]}...")
297 |         try:
298 |             mcp.run(transport='stdio')
299 |             logging.info("Server stopped.")
300 |         except Exception as e:
301 |             logging.exception("Error running server")
302 |             sys.exit(1)


--------------------------------------------------------------------------------