Initial commit
This commit is contained in:
329
templates/mcp-oauth-proxy.ts
Normal file
329
templates/mcp-oauth-proxy.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* MCP Server with OAuth Proxy (GitHub Example)
|
||||
*
|
||||
* Uses Cloudflare's workers-oauth-provider to proxy OAuth to GitHub.
|
||||
* This pattern lets you integrate with GitHub, Google, Azure, etc.
|
||||
*
|
||||
* Perfect for: Authenticated API access, user-scoped tools
|
||||
*
|
||||
* Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth
|
||||
*
|
||||
* ⚠️ CRITICAL OAuth URL CONFIGURATION:
|
||||
*
|
||||
* ALL OAuth URLs must use the SAME domain and protocol!
|
||||
*
|
||||
* Client configuration after deployment:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "github-mcp": {
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse",
|
||||
* "auth": {
|
||||
* "type": "oauth",
|
||||
* "authorizationUrl": "https://YOUR-WORKER.workers.dev/authorize",
|
||||
* "tokenUrl": "https://YOUR-WORKER.workers.dev/token"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Common mistakes:
|
||||
* ❌ Mixed protocols: http://... and https://...
|
||||
* ❌ Missing /sse in main URL
|
||||
* ❌ Wrong domain after deployment (still using localhost)
|
||||
* ❌ Typos in endpoint paths (/authorize vs /auth)
|
||||
*
|
||||
* Post-deployment checklist:
|
||||
* 1. Deploy: npx wrangler deploy
|
||||
* 2. Note deployed URL
|
||||
* 3. Update ALL three URLs in client config
|
||||
* 4. Test OAuth flow in Claude Desktop
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider";
|
||||
import { z } from "zod";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
type Env = {
|
||||
OAUTH_KV: KVNamespace; // Required for OAuth token storage
|
||||
GITHUB_CLIENT_ID?: string; // Optional: pre-register client
|
||||
GITHUB_CLIENT_SECRET?: string; // Optional: pre-register client
|
||||
};
|
||||
|
||||
/**
|
||||
* Props passed to MCP server after OAuth authentication
|
||||
* Contains user info and access token
|
||||
*/
|
||||
type Props = {
|
||||
login: string; // GitHub username
|
||||
name: string; // User's display name
|
||||
email: string; // User's email
|
||||
accessToken: string; // GitHub API access token
|
||||
};
|
||||
|
||||
/**
|
||||
* Allowlist for sensitive operations (optional)
|
||||
* Replace with your GitHub usernames
|
||||
*/
|
||||
const ALLOWED_USERNAMES = new Set(["your-github-username"]);
|
||||
|
||||
/**
|
||||
* MyMCP extends McpAgent with OAuth-authenticated context
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env, Record<string, never>, Props> {
|
||||
server = new McpServer({
|
||||
name: "GitHub MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize tools with authenticated context
|
||||
* this.props contains user info and accessToken
|
||||
*/
|
||||
async init() {
|
||||
// Create Octokit client with user's access token
|
||||
const octokit = new Octokit({
|
||||
auth: this.props!.accessToken,
|
||||
});
|
||||
|
||||
// Tool: List user's repositories
|
||||
this.server.tool(
|
||||
"list_repos",
|
||||
"List GitHub repositories for the authenticated user",
|
||||
{
|
||||
visibility: z
|
||||
.enum(["all", "public", "private"])
|
||||
.default("all")
|
||||
.describe("Filter by repository visibility"),
|
||||
sort: z
|
||||
.enum(["created", "updated", "pushed", "full_name"])
|
||||
.default("updated")
|
||||
.describe("Sort order"),
|
||||
per_page: z.number().min(1).max(100).default(30).describe("Results per page"),
|
||||
},
|
||||
async ({ visibility, sort, per_page }) => {
|
||||
try {
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
visibility,
|
||||
sort,
|
||||
per_page,
|
||||
});
|
||||
|
||||
const repoList = repos
|
||||
.map(
|
||||
(repo) =>
|
||||
`- ${repo.full_name} (${repo.visibility}) - ${repo.description || "No description"}`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${repos.length} repositories:\n\n${repoList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching repositories: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Get repository details
|
||||
this.server.tool(
|
||||
"get_repo",
|
||||
"Get detailed information about a GitHub repository",
|
||||
{
|
||||
owner: z.string().describe("Repository owner (username or org)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
},
|
||||
async ({ owner, repo }) => {
|
||||
try {
|
||||
const { data: repository } = await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `# ${repository.full_name}
|
||||
|
||||
${repository.description || "No description"}
|
||||
|
||||
**Stars**: ${repository.stargazers_count}
|
||||
**Forks**: ${repository.forks_count}
|
||||
**Open Issues**: ${repository.open_issues_count}
|
||||
**Language**: ${repository.language || "Not specified"}
|
||||
**License**: ${repository.license?.name || "None"}
|
||||
**Homepage**: ${repository.homepage || "None"}
|
||||
**Created**: ${repository.created_at}
|
||||
**Updated**: ${repository.updated_at}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching repository: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Create GitHub issue (requires write permissions)
|
||||
this.server.tool(
|
||||
"create_issue",
|
||||
"Create a new issue in a GitHub repository",
|
||||
{
|
||||
owner: z.string().describe("Repository owner"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
title: z.string().describe("Issue title"),
|
||||
body: z.string().optional().describe("Issue description"),
|
||||
labels: z.array(z.string()).optional().describe("Issue labels"),
|
||||
},
|
||||
async ({ owner, repo, title, body, labels }) => {
|
||||
try {
|
||||
const { data: issue } = await octokit.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
labels,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Created issue #${issue.number}: ${issue.title}\n\nURL: ${issue.html_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating issue: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Optional: Allowlist-protected tool
|
||||
// Only registers if user is in ALLOWED_USERNAMES
|
||||
if (ALLOWED_USERNAMES.has(this.props!.login)) {
|
||||
this.server.tool(
|
||||
"delete_repo",
|
||||
"Delete a repository (DANGEROUS - restricted to allowlisted users)",
|
||||
{
|
||||
owner: z.string().describe("Repository owner"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
},
|
||||
async ({ owner, repo }) => {
|
||||
try {
|
||||
await octokit.rest.repos.delete({ owner, repo });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully deleted repository ${owner}/${repo}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deleting repository: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Provider configuration
|
||||
* Handles GitHub OAuth flow and token management
|
||||
*/
|
||||
export default new OAuthProvider({
|
||||
/**
|
||||
* OAuth endpoints
|
||||
*/
|
||||
authorizeEndpoint: "/authorize",
|
||||
tokenEndpoint: "/token",
|
||||
clientRegistrationEndpoint: "/register",
|
||||
|
||||
/**
|
||||
* GitHub OAuth handler
|
||||
* Automatically handles OAuth flow with GitHub
|
||||
*/
|
||||
defaultHandler: new GitHubHandler({
|
||||
// Optional: Pre-configure client credentials
|
||||
// If not set, uses Dynamic Client Registration
|
||||
clientId: (env: Env) => env.GITHUB_CLIENT_ID,
|
||||
clientSecret: (env: Env) => env.GITHUB_CLIENT_SECRET,
|
||||
|
||||
// Scopes: What permissions to request
|
||||
scopes: ["repo", "user:email", "read:org"],
|
||||
|
||||
// Context: Extract user info to pass to MCP server
|
||||
context: async (accessToken: string) => {
|
||||
const octokit = new Octokit({ auth: accessToken });
|
||||
const { data: user } = await octokit.rest.users.getAuthenticated();
|
||||
|
||||
return {
|
||||
login: user.login,
|
||||
name: user.name || user.login,
|
||||
email: user.email || `${user.login}@github.com`,
|
||||
accessToken,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* KV namespace for token storage
|
||||
* Must be bound in wrangler.jsonc
|
||||
*/
|
||||
kv: (env: Env) => env.OAUTH_KV,
|
||||
|
||||
/**
|
||||
* API handlers: MCP transport endpoints
|
||||
*/
|
||||
apiHandlers: {
|
||||
"/sse": MyMCP.serveSSE("/sse"),
|
||||
"/mcp": MyMCP.serve("/mcp"),
|
||||
},
|
||||
|
||||
/**
|
||||
* Security settings
|
||||
*/
|
||||
allowConsentScreen: true, // Show consent screen (required for production)
|
||||
allowDynamicClientRegistration: true, // Allow clients to register on-the-fly
|
||||
});
|
||||
Reference in New Issue
Block a user