Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/SECRETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ This document lists all secrets required to deploy MCPs via GitHub Actions.
- `pages_read_engagement` - Read associated pages
- `business_management` - Access business accounts

### MCP: `github` (Cloudflare Workers — `deploy-github.yml`)
Unlike the other MCPs, github deploys directly via `wrangler deploy` in
its own workflow. The GitHub Action only needs Cloudflare credentials:

- **`CLOUDFLARE_API_TOKEN`**: Workers deploy token (create at
https://dash.cloudflare.com/profile/api-tokens with "Edit Cloudflare
Workers" template)
- **`CLOUDFLARE_ACCOUNT_ID`**: your Cloudflare account id

Application secrets are stored directly on the worker via
`wrangler secret put` — one-time setup, not passed through Actions.
Bulk upload via `wrangler secret bulk .secrets.json` (gitignored):

```
cd github
bunx wrangler secret bulk .secrets.json
```

Required keys in `.secrets.json`: `GITHUB_APP_ID`, `GITHUB_PRIVATE_KEY`,
`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_WEBHOOK_SECRET`.

Trigger state persists in the `INSTALLATIONS` Workers KV namespace
(`triggers:*` prefix), so no Mesh/Studio credentials are needed.

Obtain the GitHub values at https://github.com/settings/apps → your app.

## How to Add Secrets on GitHub

1. Go to your repository on GitHub
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/deploy-github.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Deploy GitHub MCP

on:
workflow_dispatch:
push:
branches:
- main
paths:
- "github/**"
- ".github/workflows/deploy-github.yml"

jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: github

steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-github-${{ hashFiles('github/bun.lock', 'github/package.json') }}
restore-keys: ${{ runner.os }}-bun-github-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Type check
run: bun run check
continue-on-error: true

- name: Deploy to Cloudflare Workers
run: bunx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
23 changes: 2 additions & 21 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions deploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@
"shared/**"
]
},
"github": {
"site": "github-mcp",
"entrypoint": "./dist/server/main.js",
"platformName": "kubernetes-bun",
"watch": [
"github/**",
"shared/**"
]
},
"openrouter": {
"site": "openrouter",
"entrypoint": "./dist/server/main.js",
Expand Down
2 changes: 2 additions & 0 deletions github/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ dist
.env.*
!.env.example
!server/.env.example
.secrets.json
.wrangler/

2 changes: 1 addition & 1 deletion github/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"friendlyName": "GitHub",
"connection": {
"type": "HTTP",
"url": "https://sites-github-mcp.decocache.com/mcp"
"url": "https://github-mcp.decocms.com/mcp"
},
"description": "OAuth proxy for the official GitHub MCP Server — authenticates via GitHub App OAuth and exposes 30+ tools (repos, issues, PRs, code search, and more)",
"icon": "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png",
Expand Down
15 changes: 6 additions & 9 deletions github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
"private": true,
"type": "module",
"scripts": {
"dev": "bun run --env-file=server/.env --hot server/main.ts",
"configure": "deco configure",
"gen": "deco gen --output=shared/deco.gen.ts",
"dev": "bunx wrangler dev",
"check": "tsc --noEmit",
"build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js",
"build": "bun run build:server",
"publish": "cat app.json | deco registry publish -w /shared/deco -y",
"dev:link": "deco link -p 3004 -- PORT=3004 bun run dev"
"build": "bunx wrangler deploy --dry-run --outdir=dist",
"deploy": "bunx wrangler deploy"
},
"dependencies": {
"@decocms/bindings": "^1.4.0",
Expand All @@ -21,10 +17,11 @@
"zod": "^4.0.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"@decocms/mcps-shared": "1.0.0",
"@types/node": "^22.0.0",
"deco-cli": "^0.29.0",
"typescript": "^5.7.2"
"typescript": "^5.7.2",
"wrangler": "^4.28.0"
},
"engines": {
"node": ">=22.0.0"
Expand Down
23 changes: 11 additions & 12 deletions github/server/lib/github-app-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
*
* Generates a JWT from GITHUB_APP_ID + GITHUB_PRIVATE_KEY,
* then exchanges it for an installation access token.
* Used at startup to discover upstream MCP tools.
*
* Env vars are read lazily (per call) so this works on Cloudflare Workers
* where process.env isn't populated at module-init time.
*/

import crypto from "node:crypto";

const GITHUB_APP_ID = process.env.GITHUB_APP_ID || "";

function normalizePrivateKey(rawKey: string): string {
let key = rawKey.trim();

Expand Down Expand Up @@ -82,10 +82,6 @@ function normalizePrivateKey(rawKey: string): string {
return key;
}

const GITHUB_PRIVATE_KEY = normalizePrivateKey(
process.env.GITHUB_PRIVATE_KEY || "",
);

function base64url(data: Buffer | string): string {
const buf = typeof data === "string" ? Buffer.from(data) : data;
return buf.toString("base64url");
Expand All @@ -96,7 +92,10 @@ function base64url(data: Buffer | string): string {
* Valid for 10 minutes (GitHub's maximum).
*/
function createAppJWT(): string {
if (!GITHUB_APP_ID || !GITHUB_PRIVATE_KEY) {
const appId = process.env.GITHUB_APP_ID || "";
const privateKey = normalizePrivateKey(process.env.GITHUB_PRIVATE_KEY || "");

if (!appId || !privateKey) {
throw new Error(
"GitHub App credentials not configured. " +
"Set GITHUB_APP_ID and GITHUB_PRIVATE_KEY environment variables.",
Expand All @@ -109,7 +108,7 @@ function createAppJWT(): string {
JSON.stringify({
iat: now - 60, // 60s clock skew allowance
exp: now + 600, // 10 minutes
iss: GITHUB_APP_ID,
iss: appId,
}),
);

Expand All @@ -118,16 +117,16 @@ function createAppJWT(): string {

try {
const signingKey = crypto.createPrivateKey({
key: GITHUB_PRIVATE_KEY,
key: privateKey,
format: "pem",
});
signature = crypto
.createSign("RSA-SHA256")
.update(signingInput)
.sign(signingKey, "base64url");
} catch (error) {
const hasPemHeader = GITHUB_PRIVATE_KEY.includes("-----BEGIN");
const keyLen = GITHUB_PRIVATE_KEY.length;
const hasPemHeader = privateKey.includes("-----BEGIN");
const keyLen = privateKey.length;
throw new Error(
`Invalid GITHUB_PRIVATE_KEY (length=${keyLen}, hasPemHeader=${hasPemHeader}). ` +
"Expected a GitHub App PEM private key, " +
Expand Down
Loading
Loading