DEV Community

Cover image for Implementing OAuth for MCP servers: a developer's guide
Saif
Saif

Posted on

Implementing OAuth for MCP servers: a developer's guide

Imagine you've built an AI-driven sales analytics tool used by enterprise SaaS businesses. It aggregates sales data, integrates with CRMs, and helps companies forecast revenue accurately.

In today’s AI-centric ecosystem, you would need to have the capability to use conversational prompts to surface the right insights. How do you securely handle authentication and manage API requests at scale without exposing sensitive customer data?

MCP servers act as a secure layer that enables your AI application to interact safely and efficiently with external systems. With an MCP server in place, your sales analytics tool can securely handle auth, manage access to sensitive data, and control interactions between your app and client systems, all without exposing direct access credentials or data endpoints.

As your MCP server moves from prototype to production, OAuth 2.1 authentication becomes critical. Here’s a step-by-step guide on securely implementing OAuth 2.1, with code examples.

OAuth implementation overview

Implementing OAuth for MCP involves four core steps:

  1. Register your MCP server and define OAuth scopes
  2. Expose OAuth protected resource metadata
  3. Validate JWT tokens
  4. Implement scope-based authorization

Let's dive right in.

Step 1: Register your MCP server

First, register your MCP server in your Scalekit dashboard:

  • Server name: e.g., "Sales Analytics API"
  • Resource identifier: Unique URL (e.g., https://api.sales-analytics.com)
  • Dynamic client registration: Enabled for automatic onboarding
  • Token lifetime: Access tokens (5 min–1 hr), refresh tokens (up to 24 hrs)

Define your OAuth scopes

Scopes should clearly represent permissions. Examples:

  • mcp:tools:sales-data:read - Read sales data
  • mcp:tools:crm:write - Update CRM records
  • mcp:resources:customer-insights:read - Access customer insights

Step 2: OAuth protected resource metadata

Your MCP server exposes metadata for clients to auto-discover OAuth endpoints:

app.get('/.well-known/oauth-protected-resource', (req, res) => {
  res.json({
    resource: 'https://api.sales-analytics.com',
    authorization_servers: ['https://your-org.scalekit.com'],
    bearer_methods_supported: ['header'],
    resource_documentation: 'https://api.sales-analytics.com/docs',
    scopes_supported: [
      'mcp:tools:sales-data',
      'mcp:tools:crm:read',
      'mcp:tools:crm:write',
      'mcp:tools:notifications:send',
      'mcp:resources:*'
    ]
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Validate JWT tokens

Each request to your MCP endpoints must validate a JWT token:

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://your-org.scalekit.com/.well-known/jwks')
);

const validateToken = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Bearer token required' });
  }

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://your-org.scalekit.com',
      audience: 'https://api.sales-analytics.com'
    });

    req.auth = {
      userId: payload.sub,
      scopes: payload.scope?.split(' ') || [],
      clientId: payload.client_id,
      expiresAt: payload.exp
    };

    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

app.use('/mcp', validateToken);
Enter fullscreen mode Exit fullscreen mode

Step 4: Scope-based authorization

Scopes provide granular permissions:

const requireScope = (requiredScope) => (req, res, next) => {
  const userScopes = req.auth.scopes;
  const hasScope = userScopes.some(scope =>
    scope === requiredScope || scope.endsWith(':*') && requiredScope.startsWith(scope.slice(0, -1))
  );

  if (!hasScope) {
    return res.status(403).json({
      error: 'insufficient_scope',
      required_scope: requiredScope
    });
  }

  next();
};

app.post('/mcp/tools/sales-data',
  requireScope('mcp:tools:sales-data:read'),
  handleSalesDataRequest
);
Enter fullscreen mode Exit fullscreen mode

Testing your implementation

Thoroughly test OAuth integration:

const testMCPAuth = async () => {
  const tokenResponse = await fetch('https://your-org.scalekit.com/oauth2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: 'test-client-id',
      client_secret: 'test-client-secret',
      scope: 'mcp:tools:sales-data'
    })
  });

  const { access_token } = await tokenResponse.json();

  const response = await fetch('https://api.sales-analytics.com/mcp/tools/sales-data', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${access_token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      method: 'sales/get_forecast',
      params: { region: 'North America' }
    })
  });

  console.log(await response.json());
};
Enter fullscreen mode Exit fullscreen mode

For further details, explore our comprehensive guide: Implement OAuth authentication for MCP servers using Scalekit.

Top comments (0)