Skip to content

MCP Integration

This guide explains how to integrate the Nevermined Payments Python SDK with MCP (Model Context Protocol) servers.

Overview

MCP (Model Context Protocol) enables AI applications to interact with external tools, resources, and prompts. The Nevermined SDK provides built-in MCP integration to:

  • Protect tools, resources, and prompts with paywalls
  • Handle OAuth 2.1 authentication
  • Manage credit consumption per operation

MCP Integration API

Access the MCP integration through payments.mcp:

from payments_py import Payments, PaymentOptions

payments = Payments.get_instance(
    PaymentOptions(nvm_api_key="nvm:your-key", environment="sandbox")
)

# MCP integration is available as:
mcp = payments.mcp

The simplified API handles server setup automatically:

Register a Tool

async def hello_handler(args, context=None):
    """Handle the hello tool request."""
    name = args.get("name", "World")
    return {
        "content": [{"type": "text", "text": f"Hello, {name}!"}]
    }

# Register the tool
payments.mcp.register_tool(
    name="hello_world",
    config={
        "description": "Says hello to someone",
        "inputSchema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name to greet"}
            }
        }
    },
    handler=hello_handler,
    options={"credits": 1}  # Cost: 1 credit per call
)

Register a Resource

async def config_handler(uri, variables, context=None):
    """Handle the configuration resource request."""
    return {
        "contents": [{
            "uri": str(uri),
            "mimeType": "application/json",
            "text": '{"version": "1.0.0", "feature_flags": {"beta": true}}'
        }]
    }

payments.mcp.register_resource(
    uri="data://config",
    config={
        "name": "Configuration",
        "description": "Application configuration",
        "mimeType": "application/json"
    },
    handler=config_handler,
    options={"credits": 2}  # Cost: 2 credits per access
)

Register a Prompt

async def greeting_handler(args, context=None):
    """Handle the greeting prompt request."""
    style = args.get("style", "formal")
    return {
        "messages": [{
            "role": "user",
            "content": {
                "type": "text",
                "text": f"Please greet me in a {style} way."
            }
        }]
    }

payments.mcp.register_prompt(
    name="greeting",
    config={
        "name": "Greeting",
        "description": "Generates a greeting"
    },
    handler=greeting_handler,
    options={"credits": 1}
)

Start the Server

import asyncio

async def main():
    # Register handlers first
    payments.mcp.register_tool("hello", {...}, hello_handler)

    # Start the MCP server
    result = await payments.mcp.start({
        "port": 5001,
        "agentId": "your-agent-id",
        "serverName": "my-mcp-server",
        "version": "1.0.0",
        "description": "My MCP server with Nevermined payments"
    })

    print(f"Server running at: {result['info']['baseUrl']}")
    print(f"Tools: {result['info']['tools']}")

    # Server runs until stopped
    # To stop: await payments.mcp.stop()

asyncio.run(main())

Advanced API

For more control, use the advanced API:

Configure and Protect Handlers

# Configure shared options
payments.mcp.configure({
    "agentId": "your-agent-id",
    "serverName": "my-mcp-server"
})

# Wrap a handler with paywall
async def my_handler(args):
    return {"result": "processed"}

protected_handler = payments.mcp.with_paywall(
    handler=my_handler,
    options={
        "kind": "tool",
        "name": "my_tool",
        "credits": 1
    }
)

Attach to Existing Server

from mcp.server import MCPServer

# Create your own MCP server
server = MCPServer()

# Attach payments integration
registrar = payments.mcp.attach(server)

# Register protected handlers
registrar.register_tool(
    name="hello",
    config={"description": "Hello tool"},
    handler=hello_handler,
    options={"credits": 1}
)

registrar.register_resource(
    name="config",
    template="data://{path}",
    config={"name": "Config"},
    handler=config_handler,
    options={"credits": 2}
)

Complete Example

import asyncio
from payments_py import Payments, PaymentOptions

# Initialize payments
payments = Payments.get_instance(
    PaymentOptions(nvm_api_key="nvm:your-key", environment="sandbox")
)

# Define handlers
async def analyze_code(args, context=None):
    """Analyze code for issues."""
    code = args.get("code", "")
    language = args.get("language", "python")

    # Your analysis logic here
    issues = analyze(code, language)

    return {
        "content": [{
            "type": "text",
            "text": f"Found {len(issues)} issues in {language} code."
        }]
    }

async def get_docs(uri, variables, context=None):
    """Return documentation."""
    topic = variables.get("topic", "general")

    return {
        "contents": [{
            "uri": str(uri),
            "mimeType": "text/markdown",
            "text": f"# Documentation for {topic}\n\nContent here..."
        }]
    }

async def code_review_prompt(args, context=None):
    """Generate code review prompt."""
    return {
        "messages": [{
            "role": "user",
            "content": {
                "type": "text",
                "text": "Please review the following code for best practices..."
            }
        }]
    }

# Register handlers
payments.mcp.register_tool(
    "analyze_code",
    {
        "description": "Analyzes code for potential issues",
        "inputSchema": {
            "type": "object",
            "properties": {
                "code": {"type": "string"},
                "language": {"type": "string", "default": "python"}
            },
            "required": ["code"]
        }
    },
    analyze_code,
    {"credits": 5}  # 5 credits per analysis
)

payments.mcp.register_resource(
    "docs://{topic}",
    {
        "name": "Documentation",
        "description": "Technical documentation",
        "mimeType": "text/markdown"
    },
    get_docs,
    {"credits": 1}
)

payments.mcp.register_prompt(
    "code_review",
    {
        "name": "Code Review",
        "description": "Generates a code review prompt"
    },
    code_review_prompt,
    {"credits": 2}
)

# Start server
async def main():
    result = await payments.mcp.start({
        "port": 5001,
        "agentId": "agent-123",
        "serverName": "code-assistant-mcp",
        "version": "1.0.0"
    })

    print(f"MCP Server running at {result['info']['baseUrl']}")
    print(f"Tools: {result['info']['tools']}")
    print(f"Resources: {result['info']['resources']}")
    print(f"Prompts: {result['info']['prompts']}")

    # Keep running
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        await payments.mcp.stop()

asyncio.run(main())

Server Configuration

Option Type Required Description
port int Yes Server port
agentId str Yes Nevermined agent DID
serverName str Yes Human-readable name
baseUrl str No Base URL (default: localhost)
version str No Server version
description str No Server description

Handler Options

Option Type Description
credits int or callable Credits to consume per call
planId str Optional override for the plan ID (otherwise inferred from token)
maxAmount int Max credits to verify during authentication (default: 1)
onRedeemError str "ignore" (default) or "propagate" to raise 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",
        "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"
    }
}
Field Type Description
success bool Whether credit redemption succeeded
txHash str or None Blockchain transaction hash (only on success)
creditsRedeemed str Number of credits burned ("0" on failure)
planId str Plan used for the operation
subscriberAddress str Subscriber's wallet address
errorReason str Error message (only on failure)

Endpoints

The MCP server exposes:

  • /.well-known/oauth-authorization-server - OAuth 2.1 discovery
  • /.well-known/oauth-protected-resource - Resource metadata
  • /register - Client registration
  • /mcp - MCP protocol endpoint (POST/GET/DELETE)
  • /health - Health check

Next Steps