Files
gh-jezweb-claude-skills-ski…/templates/mcp-oauth-proxy.ts
2025-11-30 08:24:23 +08:00

330 lines
9.1 KiB
TypeScript

/**
* 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
});