UNPKG

mcp-s-oauth

Version:

Express middleware library for MCP (Model Context Protocol) OAuth authentication

647 lines (514 loc) 24 kB
# MCP S OAuth Universal OAuth middleware library for MCP (Model Context Protocol) servers with support for any OAuth provider. ## Overview This library provides OAuth authentication for MCP servers using a flexible connector pattern. It handles the complete OAuth flow for any OAuth provider and provides an authenticated `/mcp` endpoint where you create your own MCP server with tools. Built using the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#streamable-http). ## Features - 🔐 **Universal OAuth Support** - Works with 20+ OAuth providers including GitHub, Google, Slack, and more - 🔌 **Connector Pattern** - Pre-built connectors for popular services + easily add support for new OAuth providers - 🚀 **Express Middleware** - Simple integration with `app.use()` - 🛠️ **Bring Your Own MCP Server** - Create MCP server in your handler function - 📊 **Session Management** - Automatic session handling via MCP SDK - 🌐 **Authenticated Context** - Access OAuth tokens in your tools - 🎯 **Production Ready** - Built with official MCP SDK components - 🔑 **Minimal Scopes** - Uses minimal OAuth scopes for enhanced security ## Installation ```bash npm install ``` ## Quick Start ### Basic Usage ```typescript import express from "express" import { Server } from "@modelcontextprotocol/sdk/server/index.js" import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" import { McpOAuth } from "mcp-s-oauth" import type { McpOAuthConfig } from "mcp-s-oauth" const app = express() // Choose your OAuth provider connector import { githubConnector } from "mcp-s-oauth" // or import { googleConnector } from "mcp-s-oauth" // or create your own custom connector // Configure OAuth with any provider const config: McpOAuthConfig = { baseUrl: "http://localhost:3000", clientId: "your-oauth-client-id", clientSecret: "your-oauth-client-secret", connector: githubConnector // or any other connector } // Create your MCP handler - this is where YOU create the MCP server const mcpHandler = async (req: express.Request, res: express.Response, { authInfo }) => { // Access the OAuth token from any provider const oauthToken = authInfo.token // Create transport and your MCP server const transport = new StreamableHTTPServerTransport(/* options */) const mcpServer = new Server({ name: "my-server", version: "1.0.0" }) // Register your tools (customize based on your OAuth provider) mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "get_profile", description: "Get authenticated user profile", inputSchema: { type: "object" } }] })) mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "get_profile") { // Call your OAuth provider's API using the token // Example for GitHub: https://api.github.com/user // Example for Google: https://www.googleapis.com/oauth2/v2/userinfo const apiUrl = "https://api.github.com/user" // adjust for your provider const response = await fetch(apiUrl, { headers: { "Authorization": `Bearer ${authInfo.token}` } }) const userData = await response.json() return { content: [{ type: "text", text: `Profile: ${JSON.stringify(userData, null, 2)}` }] } } }) // Handle the MCP request await mcpServer.connect(transport) await transport.handleRequest(req, res, req.body) } // Use the library const mcpOAuth = McpOAuth(config, mcpHandler) app.use("/", mcpOAuth.router) app.listen(3000) ``` ## OAuth Provider Setup ### GitHub OAuth App Setup 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 2. Create a new OAuth App: - **Authorization callback URL**: `${baseUrl}/oauth/callback` (e.g., `http://localhost:3000/oauth/callback`) 3. Use the Client ID and Secret in your config ### Google OAuth App Setup 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select existing 3. Enable the "Google+ API" or "People API" 4. Create OAuth 2.0 credentials: - **Authorized redirect URIs**: `${baseUrl}/oauth/callback` (e.g., `http://localhost:3000/oauth/callback`) 5. Use the Client ID and Secret in your config ### Custom OAuth Provider Setup For any OAuth 2.0 provider, you'll need: 1. Client ID and Client Secret from your provider 2. Authorization callback URL set to: `${baseUrl}/oauth/callback` ## Available Connectors This library includes pre-built connectors for popular OAuth providers with minimal scopes for enhanced security: | Service | Connector | |---------|-----------| | <img src="https://www.google.com/s2/favicons?sz=16&domain=slack.com" width="16" height="16" style="vertical-align: middle;" alt="Slack logo"> Slack | `slackConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=google.com" width="16" height="16" style="vertical-align: middle;" alt="Gmail logo"> Gmail | `gmailConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=calendar.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Calendar logo"> Google Calendar | `googleCalendarConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=atlassian.com" width="16" height="16" style="vertical-align: middle;" alt="Jira logo"> Jira | `jiraConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=trello.com" width="16" height="16" style="vertical-align: middle;" alt="Trello logo"> Trello | `trelloConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=asana.com" width="16" height="16" style="vertical-align: middle;" alt="Asana logo"> Asana | `asanaConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=notion.so" width="16" height="16" style="vertical-align: middle;" alt="Notion logo"> Notion | `notionConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=monday.com" width="16" height="16" style="vertical-align: middle;" alt="Monday.com logo"> Monday | `mondayConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=github.com" width="16" height="16" style="vertical-align: middle;" alt="GitHub logo"> GitHub | `githubConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=gitlab.com" width="16" height="16" style="vertical-align: middle;" alt="GitLab logo"> GitLab | `gitlabConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=workspace.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Workspace logo"> Google Workspace | `googleWorkspaceConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=drive.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Drive logo"> Google Drive | `googleDriveConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=sheets.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Sheets logo"> Google Sheets | `googleSheetsConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=forms.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Forms logo"> Google Forms | `googleFormsConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=slides.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Slides logo"> Google Slides | `googleSlidesConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=salesforce.com" width="16" height="16" style="vertical-align: middle;" alt="Salesforce logo"> Salesforce | `salesforceConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=figma.com" width="16" height="16" style="vertical-align: middle;" alt="Figma logo"> Figma | `figmaConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=zeplin.io" width="16" height="16" style="vertical-align: middle;" alt="Zeplin logo"> Zeplin | `zeplinConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=amplitude.com" width="16" height="16" style="vertical-align: middle;" alt="Amplitude logo"> Amplitude | `amplitudeConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=analytics.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Analytics logo"> Google Analytics | `googleAnalyticsConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=maps.google.com" width="16" height="16" style="vertical-align: middle;" alt="Google Maps logo"> Google Maps | `googleMapsConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=discord.com" width="16" height="16" style="vertical-align: middle;" alt="Discord logo"> Discord | `discordConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=spotify.com" width="16" height="16" style="vertical-align: middle;" alt="Spotify logo"> Spotify | `spotifyConnector` | | <img src="https://www.google.com/s2/favicons?sz=16&domain=twitter.com" width="16" height="16" style="vertical-align: middle;" alt="Twitter logo"> Twitter | `twitterConnector` | ## Example Usage with GitHub ```typescript import { githubConnector } from "mcp-s-oauth" const config: McpOAuthConfig = { baseUrl: "http://localhost:3000", clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, connector: githubConnector, } ``` ## Custom Connector For any OAuth provider not listed above: ```typescript import type { Connector } from "mcp-s-oauth" const myCustomConnector: Connector = { authUrl: "https://your-provider.com/oauth/authorize", tokenUrl: "https://your-provider.com/oauth/token", scopes: ["read", "write"], codeExchangeConfig: { isForm: true, modelCredentialsMapping: `{ "access_token": access_token, "expires_at": $fromMillis($millis() + expires_in * 1000), "refresh_token": refresh_token, "scope": scope, "token_type": token_type }`, }, authInitUrlParams: { prompt: "consent", }, } ``` ## Creating Custom Connectors The `Connector` interface allows you to integrate any OAuth 2.0 provider. Here's how to create your own: ### Connector Interface ```typescript export interface Connector { authUrl?: string // OAuth authorization endpoint tokenUrl?: string // OAuth token exchange endpoint refreshTokenUrl?: string // Token refresh endpoint (defaults to tokenUrl) scopes?: string[] | readonly string[] // OAuth scopes to request codeExchangeConfig?: { modelCredentialsMapping?: JsonataString<OAuthCredentials> | ((config: any) => OAuthCredentials) isForm?: boolean // Use form encoding vs JSON for token exchange authorizationMapping?: JsonataString<string> | ((config: any) => string) } authInitUrlParams?: Record<string, string> // Additional OAuth params } ``` ### Step-by-Step Guide 1. **Find OAuth Documentation** for your provider (authorization URL, token URL, scopes) 2. **Create Connector File**: ```typescript // src/connectors/my-provider.ts import type { Connector } from "../types/connector.types.js" export const myProviderConnector: Connector = { authUrl: "https://api.myprovider.com/oauth/authorize", tokenUrl: "https://api.myprovider.com/oauth/token", scopes: ["read", "write"], codeExchangeConfig: { isForm: true, // Most providers use form encoding modelCredentialsMapping: `{ "access_token": access_token, "expires_at": $fromMillis($millis() + expires_in * 1000), "refresh_token": refresh_token, "scope": scope, "token_type": token_type }`, }, } ``` 3. **Handle Special Cases**: ```typescript // Provider requires special auth parameters export const specialProviderConnector: Connector = { authUrl: "https://special.com/oauth/authorize", tokenUrl: "https://special.com/oauth/token", scopes: ["user:read"], authInitUrlParams: { access_type: "offline", prompt: "consent", response_mode: "query", }, codeExchangeConfig: { isForm: false, // This provider uses JSON }, } ``` 4. **Use Function for Complex Mapping**: ```typescript export const complexProviderConnector: Connector = { authUrl: "https://complex.com/oauth/authorize", tokenUrl: "https://complex.com/oauth/token", scopes: ["api"], codeExchangeConfig: { isForm: true, // Use function for complex response mapping modelCredentialsMapping: (tokenResponse) => ({ access_token: tokenResponse.accessToken, // Different field name expires_at: new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString(), refresh_token: tokenResponse.refreshToken, refresh_token_expires_at: null, scope: tokenResponse.scope, token_type: "Bearer", }), }, } ``` ### Common OAuth Patterns | Provider | Auth URL | Token URL | Form Encoding | Special Notes | |----------|----------|-----------|---------------|---------------| | GitHub | `/login/oauth/authorize` | `/login/oauth/access_token` | JSON | Simple flow | | Google | `/o/oauth2/v2/auth` | `/oauth2/token` | Form | Use `access_type: offline` | | Discord | `/api/oauth2/authorize` | `/api/oauth2/token` | Form | Standard OAuth | | Twitter | `/i/oauth2/authorize` | `/2/oauth2/token` | Form | Requires PKCE | | LinkedIn | `/authorization` | `/accessToken` | Form | Different field names | ### Testing Your Connector 1. **Create test config**: ```typescript const config: McpOAuthConfig = { baseUrl: "http://localhost:3000", clientId: process.env.MY_PROVIDER_CLIENT_ID!, clientSecret: process.env.MY_PROVIDER_CLIENT_SECRET!, connector: myProviderConnector, } ``` 2. **Test OAuth flow**: - Visit `/auth/authorize` - Complete OAuth on provider - Check console logs for token exchange - Test API calls with the token 3. **Common Issues**: - **Form vs JSON**: Check provider docs for token endpoint format - **Field Names**: Response might use different field names - **Scopes**: Ensure scopes are valid for your provider - **Callback URL**: Must match OAuth app configuration ## Example Tools Implementation The tools you create depend on your OAuth provider and their APIs. The library provides the OAuth token - you implement the tools. Here's a complete GitHub example: ### GitHub Tools Example ```typescript // In your mcpHandler function mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "github_me", description: "Get authenticated user's GitHub profile", inputSchema: { type: "object", properties: {} } }, { name: "github_repos", description: "List user's repositories", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["all", "owner", "member"] } } } }, { name: "github_issues", description: "List user's issues", inputSchema: { type: "object", properties: { state: { type: "string", enum: ["open", "closed", "all"] } } } } ] })) mcpServer.setRequestHandler(CallToolRequestSchema, async (request, { authInfo }) => { if (request.params.name === "github_me") { const response = await fetch("https://api.github.com/user", { headers: { "Authorization": `Bearer ${authInfo.token}` } }) return { content: [{ type: "text", text: await response.text() }] } } if (request.params.name === "github_repos") { const { type = "all" } = request.params.arguments || {} const response = await fetch(`https://api.github.com/user/repos?type=${type}`, { headers: { "Authorization": `Bearer ${authInfo.token}` } }) return { content: [{ type: "text", text: await response.text() }] } } if (request.params.name === "github_issues") { const { state = "open" } = request.params.arguments || {} const response = await fetch(`https://api.github.com/issues?state=${state}`, { headers: { "Authorization": `Bearer ${authInfo.token}` } }) return { content: [{ type: "text", text: await response.text() }] } } }) ``` ### Other Provider Examples For other OAuth providers, follow the same pattern using their respective APIs: - **Slack**: Use `https://slack.com/api/` endpoints - **Google Services**: Use Google API endpoints (Gmail, Drive, Sheets, etc.) - **Notion**: Use `https://api.notion.com/v1/` endpoints - **Jira**: Use Atlassian REST API endpoints - **And more...** - Each connector works with the provider's standard OAuth API ## MCP Client Usage ### Using with MCP Clients Your OAuth-authenticated MCP server implements the [MCP (Model Context Protocol)](https://modelcontextprotocol.io) and can be used with any MCP-compatible client: ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" const client = new Client({ name: "oauth-mcp-client", version: "1.0.0" }) const transport = new StreamableHTTPClientTransport( new URL("http://localhost:3000/mcp") ) await client.connect(transport) // List available tools (depends on your implementation) const tools = await client.listTools() console.log("Available tools:", tools) // Call any tool you've implemented const result = await client.callTool({ name: "get_profile", // or any tool name you've implemented arguments: {} }) console.log("Result:", result) ``` ### Authentication Flow 1. **Start OAuth Flow**: Make a request to `/auth/authorize` to initiate OAuth with your provider 2. **User Authorization**: User is redirected to OAuth provider to authorize your application 3. **Token Exchange**: Provider redirects back with an authorization code 4. **MCP Access**: Use the obtained token to make authenticated MCP requests to `/mcp` ## API Reference ### Server Information - **GET /** - Server information, available tools, and authentication endpoints - **GET /health** - Health check endpoint ### Authentication Endpoints - **POST /auth/authorize** - Start OAuth flow with configured provider - **POST /auth/token** - Exchange authorization code for access token - **GET /oauth/callback** - OAuth callback handler (used by the provider) ### MCP Protocol - **ALL /mcp** - Main MCP endpoint supporting all MCP protocol methods - Requires Bearer token authentication - Supports session management with `mcp-session-id` header - Handles tools/list and tools/call requests - Returns tools you've implemented in your mcpHandler ## Testing the Server Test the MCP server using the official SDK client: ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" // Connect to the server const client = new Client({ name: "test-client", version: "1.0.0" }) const transport = new StreamableHTTPClientTransport( new URL("http://localhost:3000/mcp"), { requestInit: { headers: { 'Authorization': 'Bearer your-oauth-token-here' } } } ) await client.connect(transport) // Test listing tools const tools = await client.listTools() console.log("Available tools:", tools.tools) // Test calling any tool you've implemented const result = await client.callTool({ name: "get_profile", // or any tool name you've implemented arguments: {} }) console.log("Result:", result.content) ``` ## Development ### Project Structure ```plaintext src/ ├── index.ts # Library exports ├── server.ts # MCP server implementation ├── lib.ts # Core utilities ├── connectors/ # OAuth provider connectors ├── services/ # Database & auth services └── types/ # TypeScript types ``` ### Building and Running ```bash # Install dependencies npm install # Development mode with auto-reload npm run dev # Build for production npm run build # Run production build npm start # Run tests npm test # Lint code npm run lint # Format code npm run format ``` ### Adding New Tools To add new MCP tools to your server: 1. **Update the tools list** in `ListToolsRequestSchema` handler 2. **Add tool implementation** in `CallToolRequestSchema` handler 3. **Use the OAuth token** to call your provider's APIs Example (provider-agnostic): ```typescript // In ListToolsRequestSchema handler { name: "get_user_data", description: "Get user data from OAuth provider", inputSchema: { type: "object", properties: { fields: { type: "array", items: { type: "string" }, description: "Fields to retrieve" } } } } // In CallToolRequestSchema handler if (name === "get_user_data") { const { fields = [] } = request.params.arguments || {} // Use the OAuth token to call your provider's API const apiUrl = "https://api.your-provider.com/user" // adjust for your provider const response = await fetch(apiUrl, { headers: { "Authorization": `Bearer ${authInfo.token}`, "Accept": "application/json" } }) const userData = await response.json() return { content: [{ type: "text", text: JSON.stringify(userData, null, 2) }] } } ``` ## Security Considerations 1. **OAuth Secrets**: Never commit OAuth client secrets to version control 2. **HTTPS**: Use HTTPS in production for secure OAuth flows 3. **Environment Variables**: Store all sensitive configuration in environment variables 4. **Token Storage**: Tokens are managed by the MCP SDK OAuth provider 5. **OAuth App Configuration**: Configure your OAuth app with correct callback URLs 6. **Scope Limitation**: Request only the OAuth scopes your application needs 7. **Token Validation**: The library handles token validation automatically ## Troubleshooting ### Common Issues 1. **"Missing required environment variables"** - Ensure your OAuth provider's `CLIENT_ID` and `CLIENT_SECRET` are set - Check environment variable names match your configuration 2. **OAuth callback errors** - Verify your `BASE_URL` matches your OAuth app configuration - Check that Authorization callback URL is set to `${baseUrl}/oauth/callback` - Ensure your OAuth provider app is configured correctly 3. **Token exchange errors** - Check `isForm` setting in your connector - most providers use form encoding - Verify `tokenUrl` is correct for your provider - Check if provider requires special headers or parameters 4. **API rate limits** - Authenticated requests usually have higher rate limits - Consider implementing caching for frequently accessed data - Check your provider's rate limiting documentation 5. **MCP client connection issues** - Ensure you're using the correct MCP endpoint URL: `/mcp` - Verify Bearer token authentication is properly configured - Check that your tools are properly registered in the mcpHandler 6. **Connector issues** - Verify OAuth URLs are correct for your provider - Check scopes are valid for your provider - Test OAuth flow manually in browser first ### Debug Mode Enable detailed logging by setting environment variables: ```bash DEBUG=mcp:* NODE_ENV=development ``` ## Contributing Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository. ## License MIT License - see [LICENSE](LICENSE) file for details. ## Support - 📚 [Documentation](https://github.com/mcp-s-ai/mcp-s-oauth/wiki) - 🐛 [Issues](https://github.com/mcp-s-ai/mcp-s-oauth/issues) - 💬 [Discussions](https://github.com/mcp-s-ai/mcp-s-oauth/discussions) ## Related Projects - [Model Context Protocol](https://github.com/modelcontextprotocol) - [MCP Servers](https://github.com/modelcontextprotocol/servers) - [Express.js](https://expressjs.com)