└── 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) --------------------------------------------------------------------------------