diff --git a/.env 2.example b/.env 2.example new file mode 100644 index 0000000..64f6497 --- /dev/null +++ b/.env 2.example @@ -0,0 +1,3 @@ +# GitHub Personal Access Token with appropriate scopes for your use case +# Create one at: https://github.com/settings/tokens +GITHUB_TOKEN=your_github_token_here \ No newline at end of file diff --git a/.gitignore 2 b/.gitignore 2 new file mode 100644 index 0000000..32a4157 --- /dev/null +++ b/.gitignore 2 @@ -0,0 +1,175 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.local +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/CITATION 2.md b/CITATION 2.md new file mode 100644 index 0000000..0789699 --- /dev/null +++ b/CITATION 2.md @@ -0,0 +1,25 @@ +# Citation Information + +If you use this software as part of research that results in a publication, please cite it as follows: + +## BibTeX Format +```bibtex +@software{cody_github_graphql_mcp_server_2025, + author = {Cody, Quentin}, + title = {GitHub GraphQL MCP Server: A Model Context Protocol Server for GitHub GraphQL API}, + year = {2025}, + url = {https://github.com/QuentinCody/github-graphql-mcp-server}, + note = {Version 1.0} +} +``` + +## APA Format +``` +Cody, Q. (2025). GitHub GraphQL MCP Server: A Model Context Protocol Server for GitHub GraphQL API. GitHub. https://github.com/QuentinCody/github-graphql-mcp-server +``` + +## Additional Information + +This software is licensed under the MIT License with an Academic Citation Requirement. See the LICENSE.md file for details. + +For questions regarding citation, please contact QuentinCody@gmail.com. \ No newline at end of file diff --git a/LICENSE 2.md b/LICENSE 2.md new file mode 100644 index 0000000..6c0c023 --- /dev/null +++ b/LICENSE 2.md @@ -0,0 +1,27 @@ +# MIT License with Academic Citation Requirement + +Copyright (c) 2025 Quentin Cody + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +2. Academic Citation Requirement: Any academic or scientific publication, + presentation, or report that uses the Software or results derived from its use + must include an appropriate citation to the Software and its author as specified + in the accompanying CITATION.md file. This condition does not apply to work that + is not intended for academic or scientific publication. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README 2.md b/README 2.md new file mode 100644 index 0000000..b0eb49b --- /dev/null +++ b/README 2.md @@ -0,0 +1,188 @@ +# GitHub GraphQL MCP Server +[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/QuentinCody/github-graphql-mcp-server)](https://archestra.ai/mcp-catalog/quentincody__github-graphql-mcp-server) + +A Model Context Protocol (MCP) server that provides access to GitHub's GraphQL API. This server exposes a single tool that allows executing arbitrary GraphQL queries and mutations against GitHub's API. + +## Features + +- Execute any GraphQL query against GitHub's API +- Comprehensive error handling and reporting +- Detailed documentation with example queries +- Support for variables in GraphQL operations + +## Prerequisites + +- Python 3.10 or higher +- A GitHub Personal Access Token (PAT) + +## Installation + +1. Clone this repository +2. Set up a virtual environment (recommended): + ```bash + # On macOS/Linux + python3 -m venv .venv + source .venv/bin/activate + + # On Windows + python -m venv .venv + .venv\Scripts\activate + ``` +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +## Usage + +### Running the Server + +```bash +# If using a virtual environment, make sure it's activated +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Run the server with your GitHub token +GITHUB_TOKEN=your_github_token_here python github_graphql_mcp_server.py +``` + +### Configuring with Claude for Desktop + +Add the following to your Claude Desktop configuration file: + +```json +{ + "github-graphql": { + "command": "/absolute/path/to/your/.venv/bin/python", + "args": [ + "/absolute/path/to/github_graphql_mcp_server.py" + ], + "options": { + "cwd": "/absolute/path/to/repository" + }, + "env": { + "GITHUB_TOKEN": "your_github_token_here" + } + } +} +``` + +Replace `/absolute/path/to/` with the actual path to your server file and add your GitHub token. + +### Example Queries + +#### Get Repository Information + +```graphql +query GetRepo($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + name + description + stargazerCount + url + createdAt + owner { + login + avatarUrl + } + } +} +``` + +Variables: +```json +{ + "owner": "octocat", + "name": "Hello-World" +} +``` + +#### Search Repositories + +```graphql +query SearchRepos($query: String!, $first: Int!) { + search(query: $query, type: REPOSITORY, first: $first) { + repositoryCount + edges { + node { + ... on Repository { + name + owner { login } + description + stargazerCount + url + } + } + } + } +} +``` + +Variables: +```json +{ + "query": "language:python stars:1000", + "first": 5 +} +``` + +#### Get User Information + +```graphql +query GetUserInfo($login: String!) { + user(login: $login) { + name + login + bio + avatarUrl + followers { + totalCount + } + repositories(first: 5, orderBy: {field: STARGAZERS, direction: DESC}) { + nodes { + name + description + stargazerCount + } + } + } +} +``` + +Variables: +```json +{ + "login": "octocat" +} +``` + +## GitHub API Rate Limits + +Be aware of GitHub's API rate limits: +- Authenticated requests: 5,000 requests per hour +- Unauthenticated requests: 60 requests per hour + +## Troubleshooting + +If you encounter issues: + +1. Check your GitHub token has the correct permissions +2. Verify your virtual environment is properly set up and activated +3. Ensure your token is correctly set in the environment variables +4. If using Claude Desktop, ensure the path to Python is correct (use absolute path to the virtual environment Python) +5. Look at the server logs for error messages +6. Ensure your GraphQL query is valid for GitHub's schema +7. Restart Claude for Desktop after making config changes + +### Common Errors + +**`spawn python ENOENT`** +- This error means the Python executable wasn't found +- Solution: Use the full path to your Python executable in the virtual environment (e.g., `/path/to/your/.venv/bin/python`) + +**`ModuleNotFoundError: No module named 'httpx'` (or other packages)** +- The Python environment doesn't have the required dependencies installed +- Solution: Make sure you've activated the virtual environment and run `pip install -r requirements.txt` + +**`Error: GitHub token not found in environment variables`** +- The server couldn't find your GitHub token +- Solution: Make sure you've set the GITHUB_TOKEN environment variable \ No newline at end of file diff --git a/README.md b/README.md index 5006b39..139034b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![QuentinCody/github-graphql-mcp-server context](https://badge.forgithub.com/QuentinCody/github-graphql-mcp-server?accept=text%2Fhtml&maxTokens=50000&ext=py)](https://uithub.com/QuentinCody/github-graphql-mcp-server?accept=text%2Fhtml&maxTokens=50000&ext=py) +[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/QuentinCody/github-graphql-mcp-server)](https://archestra.ai/mcp-catalog/quentincody__github-graphql-mcp-server) + A Model Context Protocol (MCP) server that provides access to GitHub's GraphQL API. This server exposes a single tool that allows executing arbitrary GraphQL queries and mutations against GitHub's API. ## Features diff --git a/config_template 2.json b/config_template 2.json new file mode 100644 index 0000000..6d177fb --- /dev/null +++ b/config_template 2.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "github-graphql": { + "command": "/absolute/path/to/your/.venv/bin/python", + "args": [ + "/absolute/path/to/github_graphql_mcp_server.py" + ], + "options": { + "cwd": "/absolute/path/to/repository", + "env_file": ".env.local" + } + } + } +} \ No newline at end of file diff --git a/github_graphql_mcp_server 2.py b/github_graphql_mcp_server 2.py new file mode 100644 index 0000000..22b57a9 --- /dev/null +++ b/github_graphql_mcp_server 2.py @@ -0,0 +1,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) \ No newline at end of file diff --git a/requirements 2.txt b/requirements 2.txt new file mode 100644 index 0000000..8992d81 --- /dev/null +++ b/requirements 2.txt @@ -0,0 +1,3 @@ +httpx>=0.25.0 +mcp>=1.2.0 +python-dotenv>=1.0.0 \ No newline at end of file