mcp-s-oauth
Version:
Express middleware library for MCP (Model Context Protocol) OAuth authentication
647 lines (514 loc) • 24 kB
Markdown
# 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)