github-data-mcp
Provides tools to retrieve your recent GitHub activity, list open pull requests, and get repository summaries from GitHub.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@github-data-mcpwhat did I work on today?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
github-data-mcp
A single-user remote MCP server that exposes your own GitHub activity to Claude as a custom connector. Three read-only tools, Streamable HTTP on one endpoint, deployed as one Vercel function.
POST https://<your-project>.vercel.app/mcp
Authorization: Bearer <access token, or MCP_AUTH_TOKEN directly>Authentication is OAuth 2.1 for clients that speak it (the claude.ai web
connector discovers it automatically), with the static MCP_AUTH_TOKEN still
accepted directly for Claude Code, curl and the smoke test.
Tool | Answers | Arguments |
| "what did I work on today?" |
|
| "what's waiting on me?" | none |
| "how is owner/name doing?" |
|
Quick start
npm install
cp .env.example .env # then fill in GITHUB_TOKEN and MCP_AUTH_TOKEN
npm run smokenpm run smoke boots the server in-process on a random port, drives it through
a real MCP client over Streamable HTTP, and calls all three tools against
the live GitHub API. It also checks the guards (missing bearer → 401, wrong
bearer → 401, unexpected Origin → 403) and that error payloads never contain
your token. Run this before wiring anything into Claude.
npm run smoke -- --repo owner/name --since 2026-08-01T00:00:00ZTo run the server for real:
npm run dev # tsx watch, http://127.0.0.1:3000/mcpA hand-rolled request (note that the MCP spec requires both Accept types, even though this server only ever answers with JSON):
curl -sS http://127.0.0.1:3000/mcp -H "Authorization: Bearer $MCP_AUTH_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Related MCP server: GitHub MCP Server
The GitHub token
Create a fine-grained PAT at https://github.com/settings/personal-access-tokens:
Resource owner: your own account
Repository access: all repositories (or just the ones you care about)
Repository permissions, all read-only:
Contents— commit history forget_recent_activityMetadata— mandatory, auto-selectedIssues— open issue count forget_repo_summaryPull requests—list_open_prsCommit statuses— CI state on PRs
Two things that silently return less data rather than erroring, so check them if a result looks empty:
Private contributions. Turn on Settings → Public profile → Include private contributions on my profile. Without it the contributions API omits private-repo commits from
get_recent_activityentirely.Organisation approval. A fine-grained PAT only reaches an org's repos if that org has approved fine-grained tokens and you selected it as the resource owner. Unapproved org repos come back empty, not 403.
Configuration
Variable | Required | Purpose |
| yes | Fine-grained PAT, read-only |
| yes | Master secret: accepted as a bearer, used as the OAuth sign-in credential, and derives the OAuth signing key |
| recommended | Public origin, e.g. |
| no | Comma-separated browser origins allowed to call the endpoint |
| no | Include org-owned repos in |
| no | Include forks in |
| no | Local server port (default |
Generate the bearer with real entropy — it is the only thing between the open internet and your GitHub data:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"Origin handling (DNS-rebinding guard)
A browser always attaches an Origin header; a server-to-server MCP client
normally does not. So the rule is:
No
Originheader → allowed. This is the normal path for Claude, curl, and the smoke test.Originpresent → it must appear inALLOWED_ORIGINS, otherwise403. WithALLOWED_ORIGINSunset, every request carrying anOriginis rejected.
That is what stops a page on evil.com from driving this endpoint through your
browser while your session cookies ride along. Set ALLOWED_ORIGINS only if a
browser app of yours genuinely needs to call the server. * is accepted as a
wildcard and disables the guard — don't.
Scope of the data
Two deliberate limits, worth knowing before you trust an empty result:
get_recent_activity sees default branches only. It issues one GraphQL
request: contributionsCollection names the repos you committed to in the
window, and the same query walks each repo's defaultBranchRef history filtered
to your author id. Getting per-commit detail off every branch would mean
fanning out per ref — several requests instead of one — so commits sitting on an
unmerged feature branch will not appear. The tool description tells the model
this explicitly, so it reports "nothing landed on a default branch" rather than
"you did no work". (One extra request happens on a cold process: the viewer's
node id is fetched once and cached, because history(author:) needs it.)
Repo ownership filters apply to get_recent_activity only. By default it
covers repos you own, private included, excluding forks and org-owned repos —
flip INCLUDE_ORG_REPOS / INCLUDE_FORKS to widen it. list_open_prs is
deliberately not filtered this way: review requests are by definition mostly
on repos you don't own, so filtering them out would defeat the tool.
Output and error shape
Every tool returns compact JSON in a single text block, with null/empty fields
omitted rather than serialised as null:
{"since":"2026-08-10T13:00:00.000Z","until":"2026-08-11T13:00:00.000Z","commit_count":2,"repo_count":1,
"commits":[{"repo":"me/thing","branch":"main","message":"Fix the parser","at":"2026-08-11T09:12:44Z","additions":31,"deletions":4,"sha":"a1b2c3d","private":true}]}get_recent_activity is capped at 50 commits, newest first. When more exist
the response carries "truncated": true plus a truncated_note saying how many
were available, so the model can say so instead of presenting a partial list as
complete.
Nothing throws past the tool boundary. Failures come back as isError: true
with one consistent shape:
{"error":{"code":"github_rate_limit","message":"GitHub API rate limit exhausted. Retry after the reset time; no data was returned.","reset_at":"2026-08-11T14:03:11.000Z","retry_after_seconds":1847,"limit":5000}}
| Meaning |
| Malformed |
| A required env var is unset; the message names it |
| HTTP 401 from GitHub — names |
| Valid token, missing permission or org approval |
| Includes |
| Repo missing, renamed, or not visible to the token |
| Everything else |
Token values are never echoed. Messages name the environment variable, and a
final redaction pass scrubs both configured secrets and anything matching
GitHub's token formats (ghp_…, github_pat_…) before a message leaves the
process.
Deploying to Vercel
Deployment uses Vercel's captured Node.js server mode, so there is no
vercel.json and no api/ directory to keep in sync. Vercel's Node runtime
looks for a reserved entrypoint name — src/server.ts is one of them — captures
the HTTP server that file starts, and routes every request to it. Our own router
then serves POST /mcp, so the public endpoint is
https://<project>.vercel.app/mcp.
The upshot is that production and npm run dev run the same code path: both
start src/server.ts, and the smoke test drives the same router on an ephemeral
port.
npm i -g vercel
vercel link
vercel env add GITHUB_TOKEN production
vercel env add MCP_AUTH_TOKEN production
vercel deploy --prodThen confirm the deployed endpoint is reachable and rejecting anonymous callers:
curl -sS -o /dev/null -w "%{http_code}\n" -X POST https://<project>.vercel.app/mcp401 is the correct answer. Anything else — especially 200 with HTML — means
you are not hitting the function.
Four Vercel-specific things:
Deployment Protection. If Vercel Authentication is enabled on the project, every request gets bounced to an SSO login page and Claude will see HTML instead of JSON-RPC. Turn it off for production (Project → Settings → Deployment Protection), or the connector cannot reach you. This is the single most common reason a deployed MCP server "works locally and not in Claude".
Use the production URL. Preview deployments get a fresh URL per commit; only the production domain is stable enough to register as a connector.
src/server.tsis load-bearing, in both directions. The reserved entrypoint names (server,app,index,main, at the root or insrc/) are matched by filename, and the matched file must calllisten()during module startup. Two failure modes follow, and this project hit both:A file with a reserved name that does not call
listen()still gets captured, and every request — including paths that should 404 — dies withFUNCTION_INVOCATION_FAILED. This is why the MCP server factory is namedsrc/mcp-server.tsand notsrc/server.ts.No file with a reserved name at all, and the build fails outright with "No entrypoint found". So
src/server.tsmust exist and must listen.
If you rename or move
src/server.ts, the deployment breaks one way or the other. It is a five-line file for exactly this reason.Deleting
vercel.jsonwas deliberate. In captured-server mode the routing is ours, so rewrites are unnecessary; afunctionsblock pointing at anapi/path that no longer exists would fail the build. Timeouts and memory come from project settings instead.Stateless by design. Each request builds its own
McpServerand transport withsessionIdGenerator: undefined, so no state is assumed to survive between invocations — which is exactly right for serverless, where it wouldn't.
Connecting it to Claude
claude.ai web connector — OAuth
Paste the plain endpoint URL, with no token in it:
https://<project>.vercel.app/mcpClaude discovers the OAuth flow on its own: it gets a 401 carrying a
resource_metadata pointer, reads the metadata documents, registers itself, and
opens a sign-in page. Paste MCP_AUTH_TOKEN into that page, click Authorize,
and the connector is live. You do this once — afterwards Claude holds a
short-lived access token and refreshes it silently.
Leave the OAuth Client ID field empty; the server supports dynamic registration.
Claude Code / Claude Desktop — header
claude mcp add --transport http github-data https://<project>.vercel.app/mcp --header "Authorization: Bearer <MCP_AUTH_TOKEN>"The static token is still accepted directly, which keeps curl debugging and the smoke test simple. Claude Code can also do the OAuth flow if you prefer; the header just skips it.
How the OAuth works
The server is both the resource server and its own authorization server. The
flow is standard OAuth 2.1 — discovery, dynamic client registration,
authorization code with PKCE, refresh tokens — with exactly one shortcut for the
single-user case: the sign-in step asks for MCP_AUTH_TOKEN instead of looking
up a user. There is one principal, so holding the deployment's secret is
being that principal. No user table, no password database, no third-party
identity provider.
Endpoint | Purpose |
| RFC 9728 — names the authorization server |
| RFC 8414 — endpoints and capabilities |
| RFC 7591 dynamic client registration |
| Sign-in and consent page |
| Verifies the secret, issues an authorization code |
|
|
Stateless by construction
Serverless functions have nowhere to keep an auth-code table, so nothing is
stored. Authorization codes, access tokens, refresh tokens and registered
client IDs are each an HMAC-signed blob carrying their own contents
(src/oauth/tokens.ts). Any instance can verify anything it issued because they
share a key. The key is derived from MCP_AUTH_TOKEN, which gives rotation for
free: change that variable and every previously issued token stops verifying.
Lifetimes: authorization codes 2 minutes, access tokens 1 hour, refresh tokens 30 days.
What is actually enforced
PKCE is mandatory and only
S256is accepted — noplain, no omission.Redirect URIs match exactly against the ones registered for that client. No prefix or wildcard matching, which is where open redirects come from.
Audience binding. Access tokens are bound to this server's own resource URL and validated on every MCP request, so a token minted elsewhere cannot be replayed here. A client that asks for a foreign audience still gets a token bound to ours — this server never mints credentials for somewhere else.
Codes are single-purpose. The token kind is inside the signed payload, so an authorization code cannot be presented as an access token.
Registration rejects non-HTTPS redirect URIs except on loopback.
The smoke test exercises all of this end to end: registration, the sign-in page,
a wrong secret (401, no code issued), a good secret, a wrong code_verifier
(400), the real exchange, calling /mcp with the resulting token, the refresh
grant, and a tampered token (401).
What this does not solve
It is still one shared secret at the root of everything. Anyone holding
MCP_AUTH_TOKEN can complete the sign-in page and mint themselves a token, and
the GitHub PAT behind it is unchanged. What OAuth bought is narrower than it
looks, and worth being precise about: the long-lived secret is typed into a form
once instead of being stored by every client, what clients hold afterwards
expires in an hour, tokens are audience-bound, and revocation is a single env var
change. The secret no longer travels in a URL, which is the specific exposure
this replaced.
Exactly what would change for multi-user
The protocol layer is now done — discovery, registration, PKCE, audience binding and refresh all work for any number of clients. What remains is identity and per-user data:
Replace the sign-in step with real authentication.
POST /oauth/authorizecurrently compares one secret. It would redirect to an identity provider — GitHub OAuth being the obvious one — and carry the resulting user id into the authorization code's claims.Put the user in the token.
AccessClaimsinsrc/oauth/tokens.tscarries only audience and scope; it would gain a subject. Everything downstream reads the user from there.Resolve a GitHub token per request instead of per process. Today
src/http.tsreads oneGITHUB_TOKENout ofConfigand hands it tobuildServer. It would map the token's subject to that user's stored GitHub credentials — the SDK surfaces this asAuthInfoon the tool callback'sextraargument — and pass a per-request context down to the tools, whose signatures already take a config object rather than reading globals.Store and refresh per-user GitHub tokens encrypted at rest (Vercel KV, Postgres, whatever). This is the part that turns a stateless function into a service with a database, and it also forces real token revocation: today revoking means rotating the signing secret, which logs everyone out at once.
Fix the viewer cache — this one is a real bug waiting to happen.
getViewerinsrc/github.tsmemoises the authenticated user's node id and login in a module-level variable. For one user that is a free request saved; for many it is a cross-user data leak the moment two people share a warm Lambda. It must become keyed by user, or be dropped.Per-user rate limiting and quotas. GitHub's 5000 points/hour is currently yours alone. Shared, you need per-user accounting and backpressure.
Items 1–2 are protocol work, 3–5 are this codebase, and 6 is operations. The
Origin guard and the error shapes need no changes.
Layout
src/server.ts Entrypoint: starts the HTTP server (Vercel captures this)
src/node-server.ts node:http routing — /mcp, /oauth/*, metadata, /healthz
src/oauth/routes.ts Authorization server: discovery, register, authorize, token
src/oauth/tokens.ts HMAC-signed codes/tokens/client IDs, PKCE verification
src/http.ts Origin guard, bearer check, stateless transport wiring
src/mcp-server.ts McpServer + the three tool registrations
src/github.ts GraphQL client, HTTP/GraphQL error mapping, viewer cache
src/tools/ One file per tool: query, shaping, and its description
src/config.ts Env loading (names may appear in errors, values never)
src/errors.ts ToolError, structured payloads, redaction
scripts/smoke.ts End-to-end verification against the real APIThe tool descriptions live at the bottom of each src/tools/*.ts file. They
are the part a model actually reads, so they carry the argument examples, the
"call this when…" triggers, the "don't call this for…" boundaries, and the
caveats needed to interpret an empty result correctly. Edit them with more care
than the code.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server that connects Claude AI directly to the GitHub API, enabling natural language queries for live repository data, issues, PRs, and contributions.1
- FlicenseNot gradedqualityDmaintenanceA working MCP server that connects to the real GitHub API, enabling users to manage repositories, issues, pull requests, and more through natural language in Claude Desktop.
- FlicenseNot gradedqualityCmaintenanceA read-only MCP server that exposes GitHub user profiles, repository info, and search via tools for AI assistants like Claude.
- AlicenseAqualityCmaintenanceAn MCP server that gives Claude live access to your GitHub workspace — PR reviews, issue triaging, repo search, and weekly digest reports through natural language.7MIT
Related MCP Connectors
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
An MCP server that gives your AI access to the source code and docs of all public github repos
A MCP server built for developers enabling Git based project management with project and personal…
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ElOleksii/github-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server