"""MCP server exposing GitHub tooling for ChatGPT apps.""" from __future__ import annotations from typing import Any, Dict, List, Optional from mcp.server.fastmcp import FastMCP from github_client import GitHubAPIError, GitHubClient server = FastMCP("github-gpt-tools") _client = GitHubClient() HTML_MIME_TYPE = "text/html+skybridge" RESOURCE_URIS = { "repo_info": "ui://github/repo_info.html", "issues": "ui://github/issues.html", "pulls": "ui://github/pulls.html", "readme": "ui://github/readme.html", "contributors": "ui://github/contributors.html", "files": "ui://github/files.html", "search": "ui://github/search.html", } def _tool_meta(resource_uri: str) -> Dict[str, Any]: return { "openai/outputTemplate": resource_uri, "openai/resultCanProduceWidget": True, "openai/widgetAccessible": True, } @server.resource( RESOURCE_URIS["repo_info"], title="Repository Overview", description="Summarize repository metadata with links and stats.", mime_type=HTML_MIME_TYPE, ) def repo_info_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["issues"], title="Issue List", description="Show currently fetched GitHub issues.", mime_type=HTML_MIME_TYPE, ) def issues_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["pulls"], title="Pull Request List", description="Summaries for GitHub pull requests.", mime_type=HTML_MIME_TYPE, ) def pull_requests_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["readme"], title="README Viewer", description="Render README content as preformatted text.", mime_type=HTML_MIME_TYPE, ) def readme_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["contributors"], title="Contributor List", description="Display contributors and their commit counts.", mime_type=HTML_MIME_TYPE, ) def contributors_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["files"], title="Repository Files", description="Visualize the returned Git tree entries.", mime_type=HTML_MIME_TYPE, ) def files_resource() -> str: return """
""" @server.resource( RESOURCE_URIS["search"], title="Repository Search Results", description="Render search results with metadata.", mime_type=HTML_MIME_TYPE, ) def search_resource() -> str: return """
""" def _summarize_repo(data: Dict[str, Any]) -> Dict[str, Any]: return { "id": data.get("id"), "full_name": data.get("full_name"), "description": data.get("description"), "visibility": data.get("visibility"), "default_branch": data.get("default_branch"), "language": data.get("language"), "license": (data.get("license") or {}).get("spdx_id") if data.get("license") else None, "stargazers_count": data.get("stargazers_count"), "forks_count": data.get("forks_count"), "open_issues_count": data.get("open_issues_count"), "html_url": data.get("html_url"), "topics": data.get("topics", []), "updated_at": data.get("updated_at"), } def _summarize_issue(issue: Dict[str, Any]) -> Dict[str, Any]: return { "number": issue.get("number"), "title": issue.get("title"), "state": issue.get("state"), "html_url": issue.get("html_url"), "author": (issue.get("user") or {}).get("login"), "comments": issue.get("comments"), "labels": [label.get("name") for label in issue.get("labels", [])], "created_at": issue.get("created_at"), "updated_at": issue.get("updated_at"), "assignees": [assignee.get("login") for assignee in issue.get("assignees", [])], } def _summarize_pr(pr: Dict[str, Any]) -> Dict[str, Any]: return { "number": pr.get("number"), "title": pr.get("title"), "state": pr.get("state"), "draft": pr.get("draft"), "html_url": pr.get("html_url"), "author": (pr.get("user") or {}).get("login"), "created_at": pr.get("created_at"), "updated_at": pr.get("updated_at"), "merged_at": pr.get("merged_at"), "head": (pr.get("head") or {}).get("ref"), "base": (pr.get("base") or {}).get("ref"), } def _summarize_contributor(contributor: Dict[str, Any]) -> Dict[str, Any]: return { "login": contributor.get("login"), "contributions": contributor.get("contributions"), "html_url": contributor.get("html_url"), "type": contributor.get("type"), } def _handle_errors(coro): async def wrapper(*args, **kwargs): try: return await coro(*args, **kwargs) except GitHubAPIError as exc: return {"error": str(exc)} return wrapper @server.tool(meta=_tool_meta(RESOURCE_URIS["repo_info"])) async def get_repo_info(owner: str, repo: str) -> Dict[str, Any]: """Return metadata for a repository.""" data = await _client.get_repo_info(owner, repo) return _summarize_repo(data) @server.tool(meta=_tool_meta(RESOURCE_URIS["issues"])) async def list_issues( owner: str, repo: str, state: str = "open", labels: Optional[List[str]] = None, limit: int = 25, include_pull_requests: bool = False, ) -> List[Dict[str, Any]]: """List issues in a repository.""" issues = await _client.list_issues(owner, repo, state, labels, limit, include_pull_requests) return [_summarize_issue(issue) for issue in issues] @server.tool(meta=_tool_meta(RESOURCE_URIS["pulls"])) async def list_pull_requests(owner: str, repo: str, state: str = "open", limit: int = 25) -> List[Dict[str, Any]]: """List pull requests for a repository.""" pulls = await _client.list_pull_requests(owner, repo, state, limit) return [_summarize_pr(pr) for pr in pulls] @server.tool(meta=_tool_meta(RESOURCE_URIS["readme"])) async def get_readme(owner: str, repo: str, ref: Optional[str] = None) -> Dict[str, Any]: """Fetch the README contents for a repository.""" return await _client.get_readme(owner, repo, ref) @server.tool(meta=_tool_meta(RESOURCE_URIS["contributors"])) async def list_contributors(owner: str, repo: str, limit: int = 25, include_anonymous: bool = False) -> List[Dict[str, Any]]: """Return contributors for a repository.""" contributors = await _client.list_contributors(owner, repo, limit, include_anonymous) return [_summarize_contributor(contributor) for contributor in contributors] @server.tool(meta=_tool_meta(RESOURCE_URIS["files"])) async def list_files( owner: str, repo: str, ref: str = "main", directory: Optional[str] = None, limit: int = 200, ) -> List[Dict[str, Any]]: """List files (tree entries) for a branch or tag.""" entries = await _client.list_files(owner, repo, ref, directory, limit) base_url = f"https://github.com/{owner}/{repo}" enriched: List[Dict[str, Any]] = [] for entry in entries: path = entry.get("path") if not path: enriched.append(entry) continue entry_type = entry.get("type") html_url = f"{base_url}/tree/{ref}/{path}" if entry_type == "tree" else f"{base_url}/blob/{ref}/{path}" enriched.append({**entry, "html_url": html_url}) return enriched @server.tool(meta=_tool_meta(RESOURCE_URIS["search"])) async def search_repos(query: str, sort: Optional[str] = None, order: str = "desc", limit: int = 25) -> List[Dict[str, Any]]: """Search repositories using the GitHub search API.""" data = await _client.search_repositories(query, sort, order, limit) return [_summarize_repo(repo) for repo in data] if __name__ == "__main__": server.run(transport="streamable-http")