A step-by-step guide to deploying a remote MCP server that gives Claude (or any MCP client) read/write access to a personal file vault backed by Cloudflare R2, with GitHub OAuth authentication.
By the end of this guide, you'll have a working MCP server that Claude.ai can connect to natively — no browser extensions, no local servers, no mcp-remote proxy needed.
A Cloudflare Worker that exposes 9 MCP tools for file management:
- Read/write/delete text files (markdown, JSON, YAML, code)
- List, search, browse the file tree
- Upload/download any file type via presigned URLs
- Health check to verify the server is operational
The server authenticates users via GitHub OAuth and restricts access to a single authorized GitHub account (yours).
| Requirement | Why |
|---|---|
| Node.js 18+ | To run wrangler and build the project |
| A Cloudflare account | Free tier is sufficient for Workers, KV, and R2 |
| A GitHub account | Used for OAuth authentication |
| wrangler CLI | npm install -g wrangler (Cloudflare's CLI) |
You should also be comfortable with the command line and have a basic understanding of what API keys and environment variables are.
MCP Client (Claude.ai, MCP Inspector)
|
| HTTPS (Streamable HTTP transport)
v
┌──────────────────────────────────────────────┐
│ OAuthProvider wrapper │
│ - Intercepts /authorize, /token, /register │
│ - Validates OAuth tokens on /mcp │
├──────────────────────────────────────────────┤
│ /mcp → MCP Server │
│ - 9 file management tools │
│ - Reads/writes to R2 via internal binding │
├──────────────────────────────────────────────┤
│ /authorize, /callback → GitHub OAuth │
│ - Redirects to GitHub for login │
│ - Verifies user identity (ID + username) │
│ - Calls completeAuthorization() on success │
├──────────────────────────────────────────────┤
│ KV Namespace (OAUTH_KV) │
│ - Stores OAuth tokens and sessions │
├──────────────────────────────────────────────┤
│ R2 Bucket (VAULT) │
│ - Stores all vault files │
└──────────────────────────────────────────────┘
The key insight: @cloudflare/workers-oauth-provider wraps the entire Worker. It intercepts OAuth requests, manages tokens, and only passes authenticated requests to the MCP server. You don't handle token generation yourself — the library does it.
git clone https://github.com/your-username/second-brain-vault.git
cd second-brain-vault
npm installnpx wrangler loginThis opens a browser window. Authorize wrangler to access your Cloudflare account.
You need two resources: a KV namespace for OAuth session storage and an R2 bucket for file storage.
npx wrangler kv namespace create "OAUTH_KV"This returns something like:
✨ Successfully created KV namespace "OAUTH_KV" with id "abc123def456..."
Copy the ID and paste it into wrangler.jsonc:
npx wrangler r2 bucket create mcp-vaultThe bucket name mcp-vault matches what's already in wrangler.jsonc. If you choose a different name, update the bucket_name field in the config.
The get_upload_url and get_download_url tools use presigned URLs, which require R2 API credentials (separate from your Cloudflare API token).
- Go to Cloudflare Dashboard > R2 > Manage R2 API Tokens
- Click Create API Token
- Give it Object Read & Write permission on the
mcp-vaultbucket - Copy the Access Key ID and Secret Access Key — you'll need them in Step 5
You need a GitHub OAuth App to handle authentication. You'll create two: one for local development and one for production.
- Go to GitHub Developer Settings > OAuth Apps
- Click New OAuth App
- Fill in:
- Application name:
MCP Vault (local) - Homepage URL:
http://localhost:8787 - Authorization callback URL:
http://localhost:8787/callback
- Application name:
- Click Register application
- Copy the Client ID
- Click Generate a new client secret and copy it immediately
You need your numeric GitHub user ID for the access control. Run:
curl -s https://api.github.com/users/YOUR_GITHUB_USERNAME | grep '"id"'This returns something like "id": 12345678. Note this number.
Create a .dev.vars file at the project root (this is wrangler's format for local secrets):
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
COOKIE_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32
R2_ACCESS_KEY_ID=your-r2-api-key-id
R2_SECRET_ACCESS_KEY=your-r2-api-secret
CF_ACCOUNT_ID=your-cloudflare-account-id
ALLOWED_GITHUB_ID=your-github-user-id
ALLOWED_GITHUB_LOGIN=your-github-usernameTo generate the cookie encryption key:
openssl rand -hex 32Your Cloudflare Account ID is visible on the Cloudflare dashboard main page, in the right sidebar.
npm run devThe server starts on http://localhost:8787. Visit it in your browser — you should see the MCP Vault home page.
npx @modelcontextprotocol/inspector@latest- Open http://localhost:5173 in your browser
- Enter
http://localhost:8787/mcpas the server URL - In OAuth Settings, enable Quick OAuth Flow
- Click Connect — you'll be redirected to GitHub for authorization
- After authorizing, click List Tools
You should see all 9 tools. Try calling alive to confirm everything works.
Go back to GitHub Developer Settings and create a second OAuth App:
- Application name:
MCP Vault (prod) - Homepage URL:
https://your-worker-name.your-subdomain.workers.dev - Authorization callback URL:
https://your-worker-name.your-subdomain.workers.dev/callback
Replace the URL with your actual Worker URL. The Worker name comes from the "name" field in wrangler.jsonc (second-brain-vault by default). Your subdomain is your Cloudflare Workers subdomain.
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
npx wrangler secret put CF_ACCOUNT_ID
npx wrangler secret put ALLOWED_GITHUB_ID
npx wrangler secret put ALLOWED_GITHUB_LOGINWrangler prompts you interactively for each value. Use the production GitHub OAuth App credentials (not the local ones).
npm run deployYour MCP server is now live at https://your-worker-name.your-subdomain.workers.dev/mcp.
- Go to Settings > Integrations in Claude.ai
- Click Add custom integration
- Enter your Worker URL:
https://your-worker-name.your-subdomain.workers.dev/mcp - Claude will redirect you to GitHub for authentication
- Once authorized, the vault tools appear in your conversations
Add to your Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"vault": {
"command": "npx",
"args": [
"mcp-remote",
"https://your-worker-name.your-subdomain.workers.dev/mcp"
]
}
}
}Restart Claude Desktop. On first use, a GitHub authorization window opens.
| Test | Command / Action | Expected Result |
|---|---|---|
| Server responds | Call alive tool |
Success message |
| Write a file | Call write_file with key: "test/hello.md" |
Write confirmation |
| Read it back | Call read_file with key: "test/hello.md" |
File content returned |
| List files | Call list_files |
Shows test/hello.md |
| Browse tree | Call tree |
Shows directory structure |
| Search | Call search_files with query: "hello" |
Finds the test file |
| Delete | Call delete_file with key: "test/hello.md", confirm: true |
Deletion confirmed |
If you want to sync vault files to a local folder (so you can edit them in your text editor and have changes reflected in the vault), you can set up rclone.
# macOS
brew install rclone
# Linux
curl https://rclone.org/install.sh | sudo bashrclone configCreate a new remote with these settings:
- Name:
r2-vault - Type:
s3 - Provider:
Cloudflare - Access Key ID: your R2 API key ID
- Secret Access Key: your R2 API secret
- Endpoint:
https://<YOUR_ACCOUNT_ID>.r2.cloudflarestorage.com
rclone bisync ~/documents/vault r2-vault:mcp-vault --create-empty-src-dirs --resyncThe --resync flag is required on the first run to establish the baseline.
crontab -eAdd:
*/5 * * * * /usr/local/bin/rclone bisync ~/documents/vault r2-vault:mcp-vault --create-empty-src-dirs --exclude "*.log" --exclude ".DS_Store" 2>&1 >> ~/documents/.vault-sync.log
This syncs every 5 minutes. The log file is stored outside the sync folder to avoid syncing it back to R2.
This section explains what happens under the hood when Claude connects to your MCP server. You don't need to understand this to use the server, but it helps if you want to customize it or troubleshoot issues.
Claude.ai's MCP connector expects the server to implement OAuth 2.1. When Claude connects to a custom MCP server, it performs OAuth discovery: it hits /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server, and /register. If these endpoints don't exist, Claude gives up and never sends an actual MCP request.
This means you can't just expose a simple HTTP endpoint — you need a full OAuth server.
The @cloudflare/workers-oauth-provider library handles all of this. It's a wrapper around your Worker that:
- Responds to OAuth discovery requests
- Handles dynamic client registration (
/register) - Manages the token lifecycle (
/token) - Validates tokens on every request to
/mcp - Injects
env.OAUTH_PROVIDERinto your handlers
Claude.ai Worker GitHub
| | |
|-- GET /.well-known/* ----->| |
|<-- OAuth metadata --------| |
| | |
|-- POST /register -------->| |
|<-- client_id, secret -----| |
| | |
|-- GET /authorize -------->| |
| (OAuthProvider parses) |-- redirect to GitHub ------->|
| | |
| |<-- redirect with code -------|
| | |
| |-- POST github.com/access_token
| |<-- GitHub access token ------|
| | |
| |-- GET api.github.com/user ---|
| |<-- user info (id, login) ----|
| | |
| | Check: id == ALLOWED_GITHUB_ID
| | login == ALLOWED_GITHUB_LOGIN
| | |
| | completeAuthorization() |
|<-- redirect with code ----| |
| | |
|-- POST /token ----------->| |
|<-- MCP access token ------| |
| | |
|-- POST /mcp (with token)->| |
|<-- tool results -----------| |
The most common mistake when building this is trying to generate OAuth codes and tokens yourself. Don't. The OAuthProvider wrapper handles all of that internally. Your handler's only job is:
/authorize: Parse the OAuth request withenv.OAUTH_PROVIDER.parseAuthRequest(), store it in KV, and redirect to GitHub./callback: Exchange GitHub's code for a token, verify the user, then callenv.OAUTH_PROVIDER.completeAuthorization(). That function generates the MCP authorization code and returns a redirect URL.
That's it. You never touch token generation, code exchange, or any other OAuth plumbing.
| Problem | Cause | Fix |
|---|---|---|
| Claude says "Failed to connect" | OAuth callback URL mismatch | Check the callback URL in your GitHub OAuth App matches your Worker URL exactly |
InvalidGrantError: Invalid authorization code format |
You're generating codes manually | Use env.OAUTH_PROVIDER.completeAuthorization() instead |
| 403 "Access denied" after GitHub login | Your GitHub ID or username doesn't match | Verify ALLOWED_GITHUB_ID and ALLOWED_GITHUB_LOGIN secrets |
alive works but read_file returns errors |
R2 bucket not created or binding missing | Check wrangler.jsonc has the R2 binding and the bucket exists |
| Presigned URLs fail with checksum errors | AWS SDK auto-checksum issue | Make sure requestChecksumCalculation: "WHEN_REQUIRED" is set in the S3 client config |
| Local dev works but production doesn't | Using local OAuth App credentials in production | Create a separate GitHub OAuth App with the production callback URL |
To cleanly remove everything:
| Resource | How to Remove |
|---|---|
| Worker | npx wrangler delete from the project folder |
| KV Namespace | Cloudflare Dashboard > Workers & Pages > KV > Delete |
| R2 Bucket | Cloudflare Dashboard > R2 > mcp-vault > Delete |
| R2 API Token | Cloudflare Dashboard > R2 > Manage R2 API Tokens > Revoke |
| GitHub OAuth App (local) | GitHub > Settings > Developer settings > OAuth Apps > Delete |
| GitHub OAuth App (prod) | GitHub > Settings > Developer settings > OAuth Apps > Delete |
| rclone config | rm -rf ~/.config/rclone/ |
| Cron job | crontab -e then delete the rclone line |
- ARCHITECTURE.md — Detailed architecture and data flow
- GUIDE-MCP-WORKER-OAUTH.md — OAuth implementation reference and common pitfalls
- Cloudflare Workers docs
- MCP specification
- @cloudflare/workers-oauth-provider