index.ts•12.3 kB
#!/usr/bin/env node
/**
* X402 MCP Template
*
* Generic MCP server template for consuming X402-protected APIs with gasless micropayments.
* Supports both demo mode (no private key) and payment mode (with wallet).
*
* Features:
* - Automatic X402 payment handling via x402-axios
* - Demo mode with sample data when no private key provided
* - Service discovery integration
* - Error handling and payment flow management
* - Claude Desktop compatible
*
* Setup:
* 1. Copy .env.example to .env
* 2. Configure RESOURCE_SERVER_URL (your X402 API endpoint)
* 3. Optional: Add PRIVATE_KEY for payment mode
* 4. Run: npm run build
* 5. Test: npm run inspector
*/
import { config } from "dotenv";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import { privateKeyToAccount } from "viem/accounts";
import { withPaymentInterceptor } from "x402-axios";
config();
// =============================================================================
// CONFIGURATION
// =============================================================================
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const baseURL = process.env.RESOURCE_SERVER_URL || "https://your-x402-api.example.com";
const network = process.env.NETWORK || "base-sepolia";
// =============================================================================
// PAYMENT CLIENT INITIALIZATION
// =============================================================================
let client: any;
let paymentEnabled = false;
if (privateKey && !privateKey.includes("<") && privateKey.startsWith("0x") && privateKey.length === 66) {
try {
const account = privateKeyToAccount(privateKey);
client = withPaymentInterceptor(axios.create({ baseURL }), account);
paymentEnabled = true;
console.error("✅ X402 Payment client initialized");
console.error(` Wallet: ${account.address}`);
console.error(` Network: ${network}`);
console.error(` API: ${baseURL}`);
} catch (error) {
console.error("❌ Failed to initialize payment client:", error);
console.error(" Falling back to demo mode");
client = axios.create({ baseURL });
}
} else {
client = axios.create({ baseURL });
console.error("⚠️ Running in DEMO MODE - no private key provided");
console.error(" Tools will return sample data with setup instructions");
console.error(` API: ${baseURL}`);
}
// =============================================================================
// MCP SERVER SETUP
// =============================================================================
const server = new Server(
{
name: "x402-mcp-template",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// =============================================================================
// TOOL DEFINITIONS
// =============================================================================
/**
* TODO: Customize these tool definitions for your specific API
*
* Each tool should:
* 1. Define clear name and description
* 2. Specify inputSchema matching your API requirements
* 3. Make payment-enabled API calls using the client
* 4. Handle errors gracefully
* 5. Provide demo mode fallback
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "example_api_call",
description: "Example tool for making X402-protected API calls. Replace with your actual API endpoints.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Example query parameter - customize for your API",
},
limit: {
type: "number",
description: "Optional: Maximum number of results to return",
minimum: 1,
maximum: 100,
default: 10,
},
},
required: ["query"],
},
},
{
name: "service_info",
description: "Get information about the X402 API service including available endpoints, pricing, and payment requirements",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "health_check",
description: "Check if the X402 API service is available and responding",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
// =============================================================================
// TOOL HANDLERS
// =============================================================================
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "example_api_call": {
const { query, limit = 10 } = args as { query: string; limit?: number };
if (!query?.trim()) {
throw new McpError(ErrorCode.InvalidParams, "Query parameter is required");
}
// Demo mode - return sample data with setup instructions
if (!paymentEnabled) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
demo_mode: true,
message: "X402 payment client not configured - returning sample data",
sample_data: {
query: query,
limit: limit,
results: [
{
id: "sample-1",
name: "Sample Result 1",
description: "This is sample data returned in demo mode",
},
{
id: "sample-2",
name: "Sample Result 2",
description: "Configure PRIVATE_KEY in .env to enable real payments",
},
],
},
setup_instructions: {
step_1: "Get testnet USDC from https://faucet.circle.com/",
step_2: "Add your private key to .env file: PRIVATE_KEY=0x...",
step_3: "Set RESOURCE_SERVER_URL to your X402 API endpoint",
step_4: "Rebuild and restart: npm run build",
step_5: "Try the tool again - payments will be automatic!",
},
payment_info: {
protocol: "X402 v1.0",
network: network,
cost: "Typically $0.001 - $0.01 per request",
gasless: true,
note: "You only pay for the API call, no gas fees!",
},
},
null,
2
),
},
],
};
}
// Payment mode - make real API call
// TODO: Customize endpoint path and request structure for your API
const endpointPath = "/api/your-endpoint"; // ← Replace with your actual endpoint
try {
const response = await client.post(endpointPath, {
query: query.trim(),
limit: limit,
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
payment_processed: true,
data: response.data,
metadata: {
endpoint: endpointPath,
payment_protocol: "X402 v1.0",
network: network,
},
},
null,
2
),
},
],
};
} catch (error: any) {
// Handle X402 payment errors
if (error.response?.status === 402) {
throw new McpError(
ErrorCode.InternalError,
`Payment Required: ${error.response.data?.error || "Insufficient USDC balance or payment failed"}`
);
}
throw error;
}
}
case "service_info": {
// Fetch service discovery metadata from /.well-known/x402
try {
const response = await axios.get(`${baseURL}/.well-known/x402`);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
service_discovery: true,
...response.data,
connection_info: {
base_url: baseURL,
payment_enabled: paymentEnabled,
network: network,
},
},
null,
2
),
},
],
};
} catch (error: any) {
// If service discovery not available, return basic info
return {
content: [
{
type: "text",
text: JSON.stringify(
{
service_discovery: false,
message: "Service discovery endpoint not available",
connection_info: {
base_url: baseURL,
payment_enabled: paymentEnabled,
network: network,
},
note: "API may not support X402 protocol or /.well-known/x402 endpoint",
},
null,
2
),
},
],
};
}
}
case "health_check": {
try {
// Try health endpoint
const response = await axios.get(`${baseURL}/health`, { timeout: 5000 });
return {
content: [
{
type: "text",
text: JSON.stringify(
{
status: "healthy",
api_url: baseURL,
payment_enabled: paymentEnabled,
network: network,
response: response.data,
},
null,
2
),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
status: "error",
api_url: baseURL,
payment_enabled: paymentEnabled,
network: network,
error: error.message,
note: "API may be down or health endpoint not available",
},
null,
2
),
},
],
};
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error: any) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error.message}`
);
}
});
// =============================================================================
// SERVER STARTUP
// =============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("🚀 X402 MCP Template server running");
console.error(" Status: Ready for connections");
console.error(" Payment Mode:", paymentEnabled ? "ENABLED ✅" : "DEMO MODE ⚠️");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});