> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leanmcp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding Auth and Payment

> Implementing authentication and payment in your MCP servers

## Core Philosophy

### When Auth Matters

Authentication in MCPs shines when you **already have**:

* An existing API with user accounts
* A database with access control
* Scopes and permissions defined
* A working SaaS with authenticated users

The goal: **expose the same access control to MCP users** that your existing app users have. Same scopes, same permissions, same data boundaries.

<Note>
  Don't create a separate auth system for MCPs. Use your **existing OAuth provider** — same client ID, same tenant, same everything.
</Note>

### The Architecture

```mermaid theme={null}
flowchart TD
    A[Your OAuth Provider<br/>Clerk / Cognito / Auth0] --> B[Web App Users]
    A --> C[MCP Users]
    B --> D[Same Database<br/>Same Permissions<br/>Same Scopes]
    C --> D
```

***

## Setting Up Authentication

Install `@leanmcp/auth`:

```bash theme={null}
npm install @leanmcp/auth @leanmcp/core
```

### Provider-Specific Dependencies

<Tabs>
  <Tab title="Clerk">
    ```bash theme={null}
    npm install axios jsonwebtoken jwk-to-pem
    ```
  </Tab>

  <Tab title="AWS Cognito">
    ```bash theme={null}
    npm install @aws-sdk/client-cognito-identity-provider axios jsonwebtoken jwk-to-pem
    ```
  </Tab>

  <Tab title="Auth0">
    ```bash theme={null}
    npm install axios jsonwebtoken jwk-to-pem
    ```
  </Tab>
</Tabs>

***

## Provider Setup

### Clerk (Recommended)

Clerk is the easiest option, especially if you plan to add payments later.

```typescript theme={null}
import { AuthProvider, Authenticated } from "@leanmcp/auth";

const authProvider = new AuthProvider('clerk', {
  frontendApi: process.env.CLERK_FRONTEND_API,  // e.g., 'xxx.clerk.accounts.dev'
  secretKey: process.env.CLERK_SECRET_KEY       // e.g., 'sk_test_xxx'
});

await authProvider.init();
```

**Environment variables:**

```bash theme={null}
CLERK_FRONTEND_API=your-app.clerk.accounts.dev
CLERK_SECRET_KEY=sk_test_...
```

### AWS Cognito

If you're using Amazon Amplify, use the **same client ID and user pool**:

```typescript theme={null}
const authProvider = new AuthProvider('cognito', {
  region: process.env.AWS_REGION,
  userPoolId: process.env.COGNITO_USER_POOL_ID,
  clientId: process.env.COGNITO_CLIENT_ID  // Same as your web app!
});

await authProvider.init();
```

### Auth0

```typescript theme={null}
const authProvider = new AuthProvider('auth0', {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  audience: process.env.AUTH0_AUDIENCE
});

await authProvider.init();
```

***

## Protecting Tools

### Method-Level Protection

```typescript theme={null}
import { Tool } from "@leanmcp/core";
import { Authenticated } from "@leanmcp/auth";

export class UserService {
  
  // Protected - requires authentication
  @Tool({ description: "Get user's private data" })
  @Authenticated(authProvider)
  async getPrivateData(input: { dataId: string }) {
    // authUser is automatically injected
    console.log('User ID:', authUser.sub);
    console.log('Email:', authUser.email);
    
    return await db.getData({
      userId: authUser.sub,
      dataId: input.dataId
    });
  }
  
  // Public - no authentication
  @Tool({ description: "Get public info" })
  async getPublicInfo() {
    return { status: "online" };
  }
}
```

### Class-Level Protection

Protect all methods in a service:

```typescript theme={null}
@Authenticated(authProvider)
export class SecureService {
  
  @Tool({ description: "Tool 1" })
  async tool1(input: { data: string }) {
    // authUser available
    return { userId: authUser.sub };
  }
  
  @Tool({ description: "Tool 2" })
  async tool2(input: { data: string }) {
    // authUser available here too
    return { email: authUser.email };
  }
}
```

***

## The authUser Object

When using `@Authenticated`, a global `authUser` variable is injected containing the decoded JWT:

<Tabs>
  <Tab title="Clerk">
    ```typescript theme={null}
    {
      sub: 'user_2abc123xyz',
      userId: 'user_2abc123xyz',
      email: 'user@example.com',
      firstName: 'John',
      lastName: 'Doe',
      imageUrl: 'https://img.clerk.com/...'
    }
    ```
  </Tab>

  <Tab title="AWS Cognito">
    ```typescript theme={null}
    {
      sub: 'user-uuid',
      email: 'user@example.com',
      email_verified: true,
      'cognito:username': 'username',
      'cognito:groups': ['admin', 'users']
    }
    ```
  </Tab>

  <Tab title="Auth0">
    ```typescript theme={null}
    {
      sub: 'auth0|507f1f77bcf86cd799439011',
      email: 'user@example.com',
      email_verified: true,
      name: 'John Doe'
    }
    ```
  </Tab>
</Tabs>

***

## Client-Side: Passing Tokens

Clients pass tokens via `_meta.authorization`:

```typescript theme={null}
await mcpClient.callTool({
  name: "getPrivateData",
  arguments: { dataId: "123" },
  _meta: {
    authorization: {
      type: "bearer",
      token: "eyJhbGciOiJIUzI1NiIs..."  // JWT from your auth provider
    }
  }
});
```

**Raw MCP request:**

```json theme={null}
{
  "method": "tools/call",
  "params": {
    "name": "getPrivateData",
    "arguments": { "dataId": "123" },
    "_meta": {
      "authorization": {
        "type": "bearer",
        "token": "your-jwt-token"
      }
    }
  }
}
```

***

## Adding Payments

### The Challenge

Previously, you'd pass Stripe session data to your frontend via API. With MCPs, you need to:

1. Create a payment session
2. Return the payment URL via MCP
3. Let the agent show it to the user

### Using Elicitation for Payments

Trigger payment flows with elicitation:

```typescript theme={null}
import { Tool } from "@leanmcp/core";
import { Elicitation } from "@leanmcp/elicitation";
import { Authenticated } from "@leanmcp/auth";
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export class PaymentService {
  
  @Tool({ description: "Upgrade to premium" })
  @Authenticated(authProvider)
  @Elicitation({
    title: "Upgrade to Premium",
    fields: [
      {
        name: "plan",
        type: "select",
        label: "Select Plan",
        options: [
          { value: "monthly", label: "Monthly - $9.99/mo" },
          { value: "yearly", label: "Yearly - $99/yr (save 17%)" }
        ],
        required: true
      },
      {
        name: "confirm",
        type: "boolean",
        label: "I agree to the terms of service",
        required: true
      }
    ]
  })
  async upgradeToPremium(input: { plan: string; confirm: boolean }) {
    if (!input.confirm) {
      return { error: "Please agree to terms" };
    }
    
    const price = input.plan === 'yearly' 
      ? process.env.STRIPE_YEARLY_PRICE_ID 
      : process.env.STRIPE_MONTHLY_PRICE_ID;
    
    // Create Stripe checkout session
    const session = await stripe.checkout.sessions.create({
      customer_email: authUser.email,
      line_items: [{ price, quantity: 1 }],
      mode: 'subscription',
      success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.APP_URL}/cancelled`,
      metadata: {
        userId: authUser.sub
      }
    });
    
    return {
      message: "Click the link below to complete payment",
      paymentUrl: session.url,
      sessionId: session.id
    };
  }
}
```

### Handling Webhooks

**Webhooks remain unchanged.** Your existing Stripe webhook handler works the same:

```typescript theme={null}
// Your existing webhook - no changes needed
app.post('/webhook/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET
  );
  
  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object;
      await upgradeUser(session.metadata.userId);
      break;
    // ... other events
  }
  
  res.json({ received: true });
});
```

### Checking Subscription Status

```typescript theme={null}
@Tool({ description: "Check subscription status" })
@Authenticated(authProvider)
async checkSubscription() {
  const user = await db.getUser(authUser.sub);
  
  return {
    plan: user.subscription?.plan || 'free',
    status: user.subscription?.status || 'none',
    expiresAt: user.subscription?.expiresAt,
    canUpgrade: !user.subscription || user.subscription.plan === 'free'
  };
}
```

***

## Complete Example

```typescript theme={null}
import { Service, Tool, MCPServer, createHTTPServer } from "@leanmcp/core";
import { AuthProvider, Authenticated } from "@leanmcp/auth";
import { Elicitation } from "@leanmcp/elicitation";
import Stripe from 'stripe';

// Initialize auth (use your existing provider!)
const authProvider = new AuthProvider('clerk', {
  frontendApi: process.env.CLERK_FRONTEND_API,
  secretKey: process.env.CLERK_SECRET_KEY
});
await authProvider.init();

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

@Service()
@Authenticated(authProvider)
export class PremiumService {
  
  @Tool({ description: "Get user profile and subscription" })
  async getProfile() {
    const user = await db.getUser(authUser.sub);
    return {
      email: authUser.email,
      name: `${authUser.firstName} ${authUser.lastName}`,
      plan: user.subscription?.plan || 'free'
    };
  }
  
  @Tool({ description: "Access premium feature" })
  async premiumFeature(input: { data: string }) {
    const user = await db.getUser(authUser.sub);
    
    if (user.subscription?.plan !== 'premium') {
      return {
        error: "Premium subscription required",
        upgradeAvailable: true
      };
    }
    
    // Premium feature logic
    return { result: "Premium data processed" };
  }
  
  @Tool({ description: "Upgrade to premium" })
  @Elicitation({
    title: "Upgrade to Premium",
    fields: [
      { name: "plan", type: "select", label: "Plan", 
        options: [
          { value: "monthly", label: "$9.99/month" },
          { value: "yearly", label: "$99/year" }
        ]
      }
    ]
  })
  async upgrade(input: { plan: string }) {
    const session = await stripe.checkout.sessions.create({
      customer_email: authUser.email,
      line_items: [{ 
        price: input.plan === 'yearly' 
          ? process.env.STRIPE_YEARLY_PRICE 
          : process.env.STRIPE_MONTHLY_PRICE,
        quantity: 1 
      }],
      mode: 'subscription',
      success_url: `${process.env.APP_URL}/success`,
      cancel_url: `${process.env.APP_URL}/cancel`,
      metadata: { userId: authUser.sub }
    });
    
    return {
      message: "Complete payment to upgrade",
      paymentUrl: session.url
    };
  }
}

// Start server
const serverFactory = () => {
  const server = new MCPServer({ name: "premium-service", version: "1.0.0" });
  server.registerService(new PremiumService());
  return server.getServer();
};

await createHTTPServer(serverFactory, { port: 3000 });
```

***

## Error Handling

```typescript theme={null}
import { AuthenticationError } from "@leanmcp/auth";

// Error codes
| Code | When | Action |
|------|------|--------|
| `MISSING_TOKEN` | No token in request | Prompt user to authenticate |
| `INVALID_TOKEN` | Token expired/invalid | Refresh token or re-authenticate |
| `VERIFICATION_FAILED` | Token verification error | Check provider configuration |
```

***

## Summary

| Aspect            | Recommendation                                  |
| ----------------- | ----------------------------------------------- |
| **Auth Provider** | Use your existing OAuth (Clerk, Cognito, Auth0) |
| **Client ID**     | Same as your web app                            |
| **Scopes**        | Same as your web app                            |
| **Payments**      | Use elicitation → Stripe checkout URL           |
| **Webhooks**      | No changes needed                               |
| **Token Passing** | `_meta.authorization.token`                     |

<CardGroup cols={2}>
  <Card title="Auth Examples" icon="lock" href="/examples/advanced">
    See working auth examples
  </Card>

  <Card title="Elicitation Guide" icon="message-question" href="/examples/advanced#elicitation">
    Learn about elicitation
  </Card>

  <Card title="OAuth Client" icon="user" href="/sdk/auth-oauth-client">
    Browser-based OAuth flows with PKCE
  </Card>

  <Card title="OAuth Server" icon="server" href="/sdk/auth-oauth-server">
    Build authorization servers with provider proxy
  </Card>
</CardGroup>
