Skip to main content

MCP Integration

The Model Context Protocol (MCP) integration provides a complete payment-protected MCP server with OAuth 2.1 support and automatic credit management. This is the simplest way to run a payment-protected AI agent.

Overview of MCP Integration

The MCP integration automatically handles:
  • OAuth 2.1 Discovery: Auto-configured .well-known endpoints
  • Paywall Protection: Automatic token verification and credit burning
  • Streaming Support: SSE (Server-Sent Events) for streaming responses
  • Credit Management: Fixed or dynamic credit costs per tool/resource/prompt
  • Client Registration: Dynamic client registration for OAuth flows

OAuth 2.1 Discovery

When you start an MCP server, the library automatically exposes OAuth 2.1 discovery endpoints required by the X402 spec:
EndpointDescription
/.well-known/oauth-protected-resourceProtected resource metadata
/.well-known/oauth-authorization-serverAuthorization server metadata
/.well-known/openid-configurationOpenID Connect discovery
POST /registerDynamic client registration
These endpoints are generated automatically—no manual configuration required.

Configure MCP

Initialize the MCP integration with your agent details:
import { Payments, EnvironmentName } from '@nevermined-io/payments'

const payments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

// Configure MCP integration
payments.mcp.configure({
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'my-mcp-server',
})

Register Tools with Credits

Register MCP tools and specify their credit cost:
import { z } from 'zod'

// Register a tool with fixed credit cost
payments.mcp.registerTool(
  'get_weather',                    // Tool name
  {
    title: 'Get Weather',
    description: 'Get current weather for a city',
    inputSchema: z.object({
      city: z.string().describe('City name'),
    }),
  },
  async (args) => {                 // Handler function
    // Your tool logic here
    const weather = await getWeatherData(args.city)

    return {
      content: [{ type: 'text', text: `Weather in ${args.city}: ${weather}` }],
    }
  },
  { credits: 1n }                   // Fixed: 1 credit per call
)

Dynamic Credit Calculation

For variable costs based on input/output:
payments.mcp.registerTool(
  'analyze_text',
  {
    title: 'Analyze Text',
    description: 'Perform text analysis',
    inputSchema: z.object({
      text: z.string(),
    }),
  },
  async (args) => {
    const analysis = await analyzeText(args.text)
    return {
      content: [{ type: 'text', text: JSON.stringify(analysis) }],
    }
  },
  {
    // Dynamic credits based on text length
    credits: (args, result) => {
      const textLength = args.text.length
      return BigInt(Math.ceil(textLength / 1000))  // 1 credit per KB
    }
  }
)

Register Resources

Register MCP resources with payment protection:
payments.mcp.registerResource(
  'weather-data',                   // Resource name
  'weather://current',              // Resource URI
  {
    title: 'Current Weather Data',
    description: 'Real-time weather information',
  },
  async (uri, vars) => {            // Handler function
    const data = await fetchWeatherData()

    return {
      contents: [{
        uri: 'weather://current',
        mimeType: 'application/json',
        text: JSON.stringify(data),
      }],
    }
  },
  { credits: 2n }                   // 2 credits per access
)

Register Prompts

Register MCP prompts with credit costs:
payments.mcp.registerPrompt(
  'weather-query',                  // Prompt name
  {
    title: 'Weather Query Template',
    description: 'Generate weather queries',
  },
  async (args) => {                 // Handler function
    return {
      messages: [{
        role: 'user',
        content: {
          type: 'text',
          text: `What is the weather in ${args.city}?`,
        },
      }],
    }
  },
  { credits: 1n }                   // 1 credit per prompt
)

Start Managed Server

Start a complete MCP server with all endpoints configured:
import { Payments, EnvironmentName } from '@nevermined-io/payments'

const payments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

// Configure
payments.mcp.configure({
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'weather-server',
})

// Register tools
payments.mcp.registerTool(
  'get_weather',
  {
    title: 'Get Weather',
    description: 'Get current weather',
    inputSchema: z.object({
      city: z.string(),
    }),
  },
  async (args) => ({
    content: [{ type: 'text', text: `Weather in ${args.city}: Sunny` }],
  }),
  { credits: 1n }
)

// Start server
const { info, stop } = await payments.mcp.start({
  port: 5001,
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'weather-server',
  version: '1.0.0',
})

console.log(`MCP Server running at: ${info.baseUrl}`)
console.log(`MCP endpoint: ${info.baseUrl}/mcp`)

// Graceful shutdown
process.on('SIGINT', async () => {
  await stop()
  console.log('Server stopped')
  process.exit(0)
})

MCP Logical URIs

When registering your agent at nevermined.app, use MCP logical URIs instead of HTTP URLs:

URI Format

mcp://<serverName>/<type>/<name>

Examples

TypeURIDescription
Toolmcp://weather-server/tools/get_weatherSingle tool
Tool Wildcardmcp://weather-server/tools/*All tools
Resourcemcp://weather-server/resources/weather://currentSingle resource
Resource Wildcardmcp://weather-server/resources/*All resources
Promptmcp://weather-server/prompts/weather-querySingle prompt

Wildcard Registration

Use wildcards to register all tools/resources/prompts at once:
mcp://weather-server/tools/*
mcp://weather-server/resources/*
mcp://weather-server/prompts/*

Auto-Configured Endpoints

The start() method automatically creates these endpoints:
POST /mcp              - MCP JSON-RPC endpoint
GET /mcp               - SSE streaming endpoint
DELETE /mcp            - Session cleanup
GET /                  - Server info
GET /health            - Health check
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-authorization-server
GET /.well-known/openid-configuration
POST /register         - Dynamic client registration

Complete Example: Weather Agent

import { Payments, EnvironmentName } from '@nevermined-io/payments'
import { z } from 'zod'

const payments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: 'sandbox' as EnvironmentName,
})

// Configure MCP
payments.mcp.configure({
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'weather-agent',
})

// Register multiple tools
payments.mcp.registerTool(
  'get_current_weather',
  {
    title: 'Get Current Weather',
    description: 'Get real-time weather for a location',
    inputSchema: z.object({
      city: z.string(),
      country: z.string().optional(),
    }),
  },
  async (args) => {
    const weather = await fetchWeather(args.city, args.country)
    return {
      content: [{ type: 'text', text: JSON.stringify(weather) }],
    }
  },
  { credits: 1n }
)

payments.mcp.registerTool(
  'get_forecast',
  {
    title: 'Get Weather Forecast',
    description: 'Get 7-day weather forecast',
    inputSchema: z.object({
      city: z.string(),
      days: z.number().min(1).max(7).default(3),
    }),
  },
  async (args) => {
    const forecast = await fetchForecast(args.city, args.days)
    return {
      content: [{ type: 'text', text: JSON.stringify(forecast) }],
    }
  },
  { credits: 2n }  // Forecasts cost more
)

// Register resource
payments.mcp.registerResource(
  'weather-alerts',
  'weather://alerts',
  {
    title: 'Weather Alerts',
    description: 'Active weather alerts',
  },
  async (uri) => {
    const alerts = await fetchAlerts()
    return {
      contents: [{
        uri: 'weather://alerts',
        mimeType: 'application/json',
        text: JSON.stringify(alerts),
      }],
    }
  },
  { credits: 1n }
)

// Start server
const { info, stop } = await payments.mcp.start({
  port: 5001,
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'weather-agent',
  version: '1.0.0',
})

console.log(`Weather MCP Agent running at ${info.baseUrl}`)
console.log(`Register in Nevermined App with:`)
console.log(`  - mcp://weather-agent/tools/*`)
console.log(`  - mcp://weather-agent/resources/*`)

// Graceful shutdown
process.on('SIGINT', async () => {
  await stop()
  process.exit(0)
})

// Mock functions
async function fetchWeather(city: string, country?: string) {
  return { city, temp: 72, condition: 'sunny' }
}

async function fetchForecast(city: string, days: number) {
  return { city, days, forecast: [] }
}

async function fetchAlerts() {
  return { alerts: [] }
}

Handler Options

OptionTypeDescription
creditsbigint or functionCredits to consume per call
planIdstringOptional override for the plan ID (otherwise inferred from token)
maxAmountbigintMax credits to verify during authentication (default: 1n)
onRedeemErrorstring'ignore' (default) or 'propagate' to throw on redemption failure

Response Metadata (_meta)

After each paywall-protected call, the SDK injects a _meta field into the response following the MCP specification. This field is always present regardless of whether credit redemption succeeded or failed:
// Successful redemption
{
  content: [{ type: 'text', text: 'result' }],
  _meta: {
    success: true,
    txHash: '0xabc...',
    creditsRedeemed: '5',
    remainingBalance: '95',
    planId: 'plan-123',
    subscriberAddress: '0x123...',
  }
}

// Failed redemption
{
  content: [{ type: 'text', text: 'result' }],
  _meta: {
    success: false,
    creditsRedeemed: '0',
    planId: 'plan-123',
    subscriberAddress: '0x123...',
    errorReason: 'Insufficient credits',
  }
}
FieldTypeDescription
successbooleanWhether credit redemption succeeded
txHashstringBlockchain transaction hash (only on success)
creditsRedeemedstringNumber of credits burned ('0' on failure)
remainingBalancestringCredits remaining after redemption
planIdstringPlan used for the operation
subscriberAddressstringSubscriber’s wallet address
errorReasonstringError message (only on failure)

Error Handling

The paywall automatically returns 402 errors for invalid/missing tokens:
// Automatic 402 handling - no code needed
payments.mcp.registerTool(
  'premium_tool',
  toolConfig,
  handler,
  {
    credits: 5n,
    onRedeemError: 'propagate',  // Propagate credit errors (default: 'ignore')
  }
)

Advanced: Custom Express App

For existing Express applications, use createRouter:
import express from 'express'
import { Payments } from '@nevermined-io/payments'

const app = express()
const payments = Payments.getInstance({...})

payments.mcp.configure({
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'my-server',
})

// Register tools...

// Create MCP router
const mcpRouter = payments.mcp.createRouter({
  agentId: process.env.NVM_AGENT_ID!,
  serverName: 'my-server',
})

// Mount router
app.use('/mcp', mcpRouter)

// Your other routes
app.get('/custom', (req, res) => {
  res.json({ status: 'ok' })
})

app.listen(5001)

Best Practices

  1. Descriptive Names: Use clear tool/resource/prompt names
  2. Schema Validation: Always define proper input schemas with Zod
  3. Credit Pricing: Set appropriate credits based on resource cost
  4. Error Messages: Provide clear error messages in responses
  5. Graceful Shutdown: Always implement SIGINT handlers
  6. Wildcard URIs: Use wildcards for simpler agent registration
  7. Version Control: Include version in server configuration

Source References:
  • RUN.md (MCP Server section, lines 16-86)
  • src/mcp/index.ts (MCP integration API)
  • tests/integration/mcp-integration.test.ts (integration examples)