Skip to main content
Glama
ElOleksii

github-data-mcp

by ElOleksii

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

get_recent_activity

"what did I work on today?"

since (optional ISO 8601, default 24h ago)

list_open_prs

"what's waiting on me?"

none

get_repo_summary

"how is owner/name doing?"

repo (required, "owner/name")


Quick start

npm install
cp .env.example .env    # then fill in GITHUB_TOKEN and MCP_AUTH_TOKEN
npm run smoke

npm 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:00Z

To run the server for real:

npm run dev      # tsx watch, http://127.0.0.1:3000/mcp

A 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 for get_recent_activity

    • Metadata — mandatory, auto-selected

    • Issues — open issue count for get_repo_summary

    • Pull requestslist_open_prs

    • Commit statuses — CI state on PRs

Two things that silently return less data rather than erroring, so check them if a result looks empty:

  1. 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_activity entirely.

  2. 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

GITHUB_TOKEN

yes

Fine-grained PAT, read-only

MCP_AUTH_TOKEN

yes

Master secret: accepted as a bearer, used as the OAuth sign-in credential, and derives the OAuth signing key

PUBLIC_URL

recommended

Public origin, e.g. https://foo.vercel.app. OAuth metadata is built from it

ALLOWED_ORIGINS

no

Comma-separated browser origins allowed to call the endpoint

INCLUDE_ORG_REPOS

no

Include org-owned repos in get_recent_activity (default false)

INCLUDE_FORKS

no

Include forks in get_recent_activity (default false)

PORT

no

Local server port (default 3000, ignored on Vercel)

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 Origin header → allowed. This is the normal path for Claude, curl, and the smoke test.

  • Origin present → it must appear in ALLOWED_ORIGINS, otherwise 403. With ALLOWED_ORIGINS unset, every request carrying an Origin is 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}}

code

Meaning

bad_argument

Malformed since or repo — message says what was expected

config_missing

A required env var is unset; the message names it

github_auth

HTTP 401 from GitHub — names GITHUB_TOKEN as the thing to fix

github_forbidden

Valid token, missing permission or org approval

github_rate_limit

Includes reset_at and retry_after_seconds

github_not_found

Repo missing, renamed, or not visible to the token

github_error / network_error / internal_error

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 --prod

Then 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/mcp

401 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.ts is load-bearing, in both directions. The reserved entrypoint names (server, app, index, main, at the root or in src/) are matched by filename, and the matched file must call listen() 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 with FUNCTION_INVOCATION_FAILED. This is why the MCP server factory is named src/mcp-server.ts and not src/server.ts.

    • No file with a reserved name at all, and the build fails outright with "No entrypoint found". So src/server.ts must 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.json was deliberate. In captured-server mode the routing is ours, so rewrites are unnecessary; a functions block pointing at an api/ 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 McpServer and transport with sessionIdGenerator: 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/mcp

Claude 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

GET /.well-known/oauth-protected-resource

RFC 9728 — names the authorization server

GET /.well-known/oauth-authorization-server

RFC 8414 — endpoints and capabilities

POST /oauth/register

RFC 7591 dynamic client registration

GET /oauth/authorize

Sign-in and consent page

POST /oauth/authorize

Verifies the secret, issues an authorization code

POST /oauth/token

authorization_code and refresh_token grants

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 S256 is accepted — no plain, 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:

  1. Replace the sign-in step with real authentication. POST /oauth/authorize currently 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.

  2. Put the user in the token. AccessClaims in src/oauth/tokens.ts carries only audience and scope; it would gain a subject. Everything downstream reads the user from there.

  3. Resolve a GitHub token per request instead of per process. Today src/http.ts reads one GITHUB_TOKEN out of Config and hands it to buildServer. It would map the token's subject to that user's stored GitHub credentials — the SDK surfaces this as AuthInfo on the tool callback's extra argument — and pass a per-request context down to the tools, whose signatures already take a config object rather than reading globals.

  4. 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.

  5. Fix the viewer cache — this one is a real bug waiting to happen. getViewer in src/github.ts memoises 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.

  6. 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 API

The 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.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • F
    license
    Not graded
    quality
    D
    maintenance
    A 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
  • F
    license
    Not graded
    quality
    D
    maintenance
    A 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.
  • F
    license
    Not graded
    quality
    C
    maintenance
    A read-only MCP server that exposes GitHub user profiles, repository info, and search via tools for AI assistants like Claude.
  • A
    license
    A
    quality
    C
    maintenance
    An MCP server that gives Claude live access to your GitHub workspace — PR reviews, issue triaging, repo search, and weekly digest reports through natural language.
    7
    MIT

View all related MCP servers

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…

View all MCP Connectors

Latest Blog Posts

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