Skip to main content

Table of Contents

Installation

Install the required packages:
npm install @solvapay/server @modelcontextprotocol/sdk
# or
pnpm add @solvapay/server @modelcontextprotocol/sdk
# or
yarn add @solvapay/server @modelcontextprotocol/sdk

Basic Setup

1. Initialize SolvaPay

Create a SolvaPay instance in your MCP server:
import { createSolvaPay } from '@solvapay/server'
import { Server } from '@modelcontextprotocol/sdk/server/index'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'

// Initialize SolvaPay
const solvaPay = createSolvaPay({
  apiKey: process.env.SOLVAPAY_SECRET_KEY,
})

// Create payable handler for your product
const payable = solvaPay.payable({
  product: 'prd_YOUR_PRODUCT_ID',
  plan: 'pln_YOUR_PLAN_ID', // Optional: can be set per tool
})

2. Create MCP Server

Set up your MCP server:
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types'

// Create MCP server
const server = new Server(
  {
    name: 'solvapay-protected-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  },
)

3. Define Tools

Define your MCP tools:
const tools: Tool[] = [
  {
    name: 'create_task',
    description: 'Create a new task (requires purchase)',
    inputSchema: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          description: 'Title of the task',
        },
        auth: {
          type: 'object',
          description: 'Authentication information',
          properties: {
            customer_ref: { type: 'string' },
          },
          required: ['customer_ref'],
        },
      },
      required: ['title', 'auth'],
    },
  },
]

Protecting MCP Tools

Basic Tool Protection

Wrap your tool handlers with payable.mcp():
// Your business logic function
async function createTask(args: { title: string; auth: { customer_ref: string } }) {
  const { title } = args

  // Your business logic here
  const task = {
    id: Date.now().toString(),
    title,
    createdAt: new Date().toISOString(),
  }

  return {
    success: true,
    task,
  }
}

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'create_task': {
      // Protect the tool with paywall
      const handler = payable.mcp(createTask)
      return await handler(args)
    }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

Multiple Tools with Same Plan

If all tools use the same plan, create one payable handler:
const payable = solvaPay.payable({
  product: 'prd_myapi',
  plan: 'pln_premium',
})

server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'create_task': {
      const handler = payable.mcp(createTask)
      return await handler(args)
    }

    case 'get_task': {
      const handler = payable.mcp(getTask)
      return await handler(args)
    }

    case 'list_tasks': {
      const handler = payable.mcp(listTasks)
      return await handler(args)
    }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

Different Plans per Tool

Create multiple payable handlers for different plans:
const freeTier = solvaPay.payable({
  product: 'prd_myapi',
  plan: 'pln_free',
})

const premiumTier = solvaPay.payable({
  product: 'prd_myapi',
  plan: 'pln_premium',
})

server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'list_tasks': {
      // Free tier tool
      const handler = freeTier.mcp(listTasks)
      return await handler(args)
    }

    case 'create_task': {
      // Premium tier tool
      const handler = premiumTier.mcp(createTask)
      return await handler(args)
    }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

Authentication

Customer Reference in Tool Arguments

The MCP adapter expects customer reference in the auth object:
const tools: Tool[] = [
  {
    name: 'create_task',
    description: 'Create a new task',
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string' },
        auth: {
          type: 'object',
          properties: {
            customer_ref: { type: 'string' },
          },
          required: ['customer_ref'],
        },
      },
      required: ['title', 'auth'],
    },
  },
]

Extract Customer Reference

The MCP adapter automatically extracts customer_ref from args.auth.customer_ref:
async function createTask(args: { title: string; auth: { customer_ref: string } }) {
  // customer_ref is automatically extracted by the adapter
  // Your business logic here
  return { success: true, task: {} }
}

const handler = payable.mcp(createTask)

Custom Customer Reference Extraction

Override customer reference extraction:
const handler = payable.mcp(createTask, {
  getCustomerRef: (args: any) => {
    // Custom logic to extract customer reference
    return args.auth?.customer_ref || args.userId || null
  },
})

OAuth Bearer Token Helper

For MCP servers that authenticate with bearer tokens, you can use the server SDK helper utilities:
import {
  getCustomerRefFromBearerAuthHeader,
  McpBearerAuthError,
} from '@solvapay/server'

const handler = payable.mcp(createTask, {
  getCustomerRef: args => {
    const header = args._authHeader as string | undefined
    try {
      return getCustomerRefFromBearerAuthHeader(header)
    } catch (error) {
      if (error instanceof McpBearerAuthError) {
        throw new Error('Unauthorized')
      }
      throw error
    }
  },
})
These helpers only decode claims (customerRef, customer_ref, sub) and do not verify signatures. Use them after token validation (for example with /v1/customer/auth/userinfo). Fail closed on auth failures. Do not substitute fallback identities such as anonymous.

Non-hosted OAuth Bridge Pattern

If your MCP server runs on localhost/custom domain (not SolvaPay hosted MCP proxy):
  1. Serve local discovery endpoints:
    • /.well-known/oauth-protected-resource
    • /.well-known/oauth-authorization-server
  2. Point discovery metadata to SolvaPay OAuth endpoints (/v1/customer/auth/*)
  3. Ensure registration_endpoint includes mcp_server_id
  4. Protect /mcp with bearer auth and return RFC9728 WWW-Authenticate challenge when missing
Reference implementation: examples/mcp-oauth-bridge.

Error Handling

Basic Error Handling

The MCP adapter automatically handles PaywallError and converts it to MCP error format:
server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  try {
    switch (name) {
      case 'create_task': {
        const handler = payable.mcp(createTask)
        return await handler(args)
      }

      default:
        throw new Error(`Unknown tool: ${name}`)
    }
  } catch (error) {
    // PaywallError is automatically handled by the adapter
    // Other errors are re-thrown
    throw error
  }
})

Custom Error Handling

Handle errors manually for custom error responses:
import { PaywallError } from '@solvapay/server'

server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  try {
    switch (name) {
      case 'create_task': {
        const handler = payable.mcp(createTask)
        return await handler(args)
      }

      default:
        throw new Error(`Unknown tool: ${name}`)
    }
  } catch (error) {
    if (error instanceof PaywallError) {
      // Custom paywall error response
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              error: 'Payment required',
              message: error.message,
              checkoutUrl: error.structuredContent.checkoutUrl,
              product: error.structuredContent.product,
            }),
          },
        ],
        isError: true,
      }
    }

    // Handle other errors
    throw error
  }
})

Complete Example

Here’s a complete MCP server with SolvaPay integration:
import 'dotenv/config'
import { Server } from '@modelcontextprotocol/sdk/server/index'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types'
import { createSolvaPay, PaywallError } from '@solvapay/server'

// Initialize SolvaPay
const solvaPay = createSolvaPay({
  apiKey: process.env.SOLVAPAY_SECRET_KEY,
})

// Create payable handler
const payable = solvaPay.payable({
  product: 'prd_myapi',
  plan: 'pln_premium',
})

// Define tools
const tools: Tool[] = [
  {
    name: 'create_task',
    description: 'Create a new task (requires purchase)',
    inputSchema: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          description: 'Title of the task',
        },
        description: {
          type: 'string',
          description: 'Optional description of the task',
        },
        auth: {
          type: 'object',
          description: 'Authentication information',
          properties: {
            customer_ref: { type: 'string' },
          },
          required: ['customer_ref'],
        },
      },
      required: ['title', 'auth'],
    },
  },
  {
    name: 'get_task',
    description: 'Get a task by ID (requires purchase)',
    inputSchema: {
      type: 'object',
      properties: {
        id: {
          type: 'string',
          description: 'ID of the task to retrieve',
        },
        auth: {
          type: 'object',
          description: 'Authentication information',
          properties: {
            customer_ref: { type: 'string' },
          },
          required: ['customer_ref'],
        },
      },
      required: ['id', 'auth'],
    },
  },
  {
    name: 'list_tasks',
    description: 'List all tasks (requires purchase)',
    inputSchema: {
      type: 'object',
      properties: {
        limit: {
          type: 'number',
          description: 'Maximum number of tasks to return (default: 10)',
        },
        offset: {
          type: 'number',
          description: 'Number of tasks to skip (default: 0)',
        },
        auth: {
          type: 'object',
          description: 'Authentication information',
          properties: {
            customer_ref: { type: 'string' },
          },
          required: ['customer_ref'],
        },
      },
    },
  },
]

// Business logic functions
async function createTask(args: {
  title: string
  description?: string
  auth: { customer_ref: string }
}) {
  const { title, description } = args

  const task = {
    id: Date.now().toString(),
    title,
    description,
    createdAt: new Date().toISOString(),
  }

  return {
    success: true,
    message: 'Task created successfully',
    task,
  }
}

async function getTask(args: { id: string; auth: { customer_ref: string } }) {
  const { id } = args

  // Simulate fetching from database
  const task = {
    id,
    title: 'Sample Task',
    description: 'Task description',
    createdAt: new Date().toISOString(),
  }

  return {
    success: true,
    task,
  }
}

async function listTasks(args: {
  limit?: number
  offset?: number
  auth: { customer_ref: string }
}) {
  const { limit = 10, offset = 0 } = args

  // Simulate fetching from database
  const tasks = Array.from({ length: limit }, (_, i) => ({
    id: (offset + i + 1).toString(),
    title: `Task ${offset + i + 1}`,
    description: `Description for task ${offset + i + 1}`,
    createdAt: new Date().toISOString(),
  }))

  return {
    success: true,
    tasks,
    total: tasks.length,
    limit,
    offset,
  }
}

// Create MCP server
const server = new Server(
  {
    name: 'solvapay-protected-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  },
)

// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools }
})

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async request => {
  const { name, arguments: args } = request.params

  try {
    switch (name) {
      case 'create_task': {
        const handler = payable.mcp(createTask)
        const result = await handler(args)
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(result),
            },
          ],
        }
      }

      case 'get_task': {
        const handler = payable.mcp(getTask)
        const result = await handler(args)
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(result),
            },
          ],
        }
      }

      case 'list_tasks': {
        const handler = payable.mcp(listTasks)
        const result = await handler(args)
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(result),
            },
          ],
        }
      }

      default:
        throw new Error(`Unknown tool: ${name}`)
    }
  } catch (error) {
    if (error instanceof PaywallError) {
      // Return paywall error in MCP format
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              error: 'Payment required',
              message: error.message,
              checkoutUrl: error.structuredContent.checkoutUrl,
              product: error.structuredContent.product,
            }),
          },
        ],
        isError: true,
      }
    }

    // Re-throw other errors
    throw error
  }
})

// Start the server
async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)

  console.error('SolvaPay Protected MCP Server started')
  console.error('Available tools: create_task, get_task, list_tasks')
  console.error('Paywall protection enabled')
}

main().catch(error => {
  console.error('Failed to start MCP server:', error)
  process.exit(1)
})

Testing the Example

  1. Save the code to src/index.ts
  2. Set environment variable: SOLVAPAY_SECRET_KEY=sk_...
  3. Run the server: node dist/index.js
The server will listen on stdio and respond to MCP protocol messages.

Tool Response Format

The MCP adapter automatically formats responses. Your business logic should return:
  • Object: Automatically converted to MCP response format
  • Error: Thrown as exception (PaywallError handled automatically)
async function createTask(args: any) {
  // Return object - automatically formatted
  return { success: true, task: {} }

  // Or throw error - automatically handled
  if (!args.title) {
    throw new Error('Title is required')
  }
}

Best Practices

  1. Customer Reference: Always require customer_ref in the auth object for tool arguments.
  2. Error Handling: Handle PaywallError appropriately to provide clear error messages to MCP clients.
  3. Tool Documentation: Provide clear descriptions in tool schemas so users understand what each tool does.
  4. Type Safety: Use TypeScript for better type safety and developer experience.
  5. Environment Variables: Store API keys in environment variables, not in code.
  6. Tool Naming: Use clear, descriptive names for your tools.

Next Steps