#!/usr/bin/env node
import express from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
McpError,
ListToolsRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { AgentService } from "./core/agent-service.js";
import { PaymentManager } from "./payments/payment-manager.js";
import {
createAgentTools,
validateToolArguments,
handleCreateAgent,
handleListAgents,
handleGetAgent,
handlePromptAgent
} from "./tools/agent-tools.js";
import { PlatformAPIClient } from "./platform/api-client.js";
import { handlePlatformError } from "./platform/error-handler.js";
import { atxpServer } from '@atxp/server';
console.log("π MoluAbi MCP Server starting...");
console.log("π SECURITY UPDATE: Now using API key authentication with platform integration");
// Initialize services
const paymentMode = (process.env.PAYMENT_MODE as "none" | "atxp" | "subscription") || "none";
const paymentManager = new PaymentManager(paymentMode);
const agentService = new AgentService();
const platformClient = new PlatformAPIClient(process.env.PLATFORM_API_URL || 'https://app.moluabi.com');
async function main() {
try {
// Initialize payment system
await paymentManager.initialize();
// Create HTTP server
const app = express();
const PORT = parseInt(process.env.PORT || '5000', 10);
// Configure ATXP server setup for payment context
const PAYMENT_DESTINATION = process.env.PAYMENT_DESTINATION;
let atxpMcpServer: any = null;
let atxpTransport: any = null;
if (PAYMENT_DESTINATION && paymentMode === 'atxp') {
console.log('π§ Setting up ATXP MCP server...');
// Create dedicated MCP server for ATXP
atxpMcpServer = new Server(
{
name: "moluabi-atxp-server",
version: "2.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Create ATXP transport for MCP
atxpTransport = new StreamableHTTPServerTransport();
// Register tools on ATXP server with payment requirements
atxpMcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
console.log('π ATXP MCP list_tools called');
return { tools };
});
// Handle tool execution with ATXP payments
atxpMcpServer.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
console.log(`π§ ATXP tool call: ${name}`);
const toolPricing: Record<string, string> = {
"create_agent": "0.05",
"list_agents": "0.001",
"get_agent": "0.001",
"update_agent": "0.02",
"delete_agent": "0.01",
"prompt_agent": "0.01",
"add_user_to_agent": "0.005",
"remove_user_from_agent": "0.005",
"get_usage_report": "0.002",
"get_pricing": "0.001"
};
if (toolPricing[name]) {
console.log(`π° Requiring ATXP payment: $${toolPricing[name]} for ${name}`);
try {
await requirePayment(BigNumber(toolPricing[name]));
console.log('β
ATXP payment validated successfully');
} catch (paymentError) {
console.log(`β ATXP payment failed:`, paymentError);
throw new Error("Payment required - ATXP payment validation failed");
}
}
// Execute tool with dummy API key (ATXP doesn't use API keys)
const atxpArgs = {
...args,
apiKey: 'mab_atxp_dummy_key'
};
switch (name) {
case "create_agent":
return await handleCreateAgent(atxpArgs);
case "list_agents":
return await handleListAgents(atxpArgs);
case "get_agent":
return await handleGetAgent(atxpArgs);
case "prompt_agent":
return await handlePromptAgent(atxpArgs);
case "get_pricing":
return await agentService.getPricing();
default:
throw new Error(`Tool not implemented: ${name}`);
}
});
// Connect ATXP server to transport
await atxpMcpServer.connect(atxpTransport);
// Apply ATXP middleware globally for payment context
app.use(atxpServer({
destination: PAYMENT_DESTINATION,
payeeName: 'MoluAbi MCP Server',
}));
console.log('β
ATXP MCP server and middleware configured');
}
// Health check endpoint
app.get('/', (req, res) => {
res.json({
status: 'healthy',
service: 'MoluAbi MCP Server',
version: '2.0.0',
authentication: 'API Key (mab_...)',
timestamp: new Date().toISOString(),
debug: 'CODE_IS_ACTUALLY_RUNNING_NOW'
});
});
// ATXP SDK Compatibility: Route root POST requests to /mcp/call
app.post('/', express.json(), async (req, res) => {
console.log('π ATXP SDK compatibility: Routing root POST to /mcp/call');
// Accept both "tool" and "name" parameters for backward compatibility
const toolName = req.body.tool || req.body.name;
const args = req.body.arguments;
try {
if (!toolName) {
return res.status(400).json({ error: 'Missing tool/name parameter' });
}
// Validate arguments using the same validation as MCP
validateToolArguments(toolName, args);
// All tools now use API key authentication
if (!args.apiKey) {
return res.status(400).json({
error: 'API key required',
message: 'All operations require a valid MoluAbi API key (format: mab_...)'
});
}
let result;
// Handle tool calls with new API key authentication
switch (toolName) {
case "create_agent":
result = await handleCreateAgent(args);
break;
case "list_agents":
result = await handleListAgents(args);
break;
case "get_agent":
result = await handleGetAgent(args);
break;
case "prompt_agent":
result = await handlePromptAgent(args);
break;
case "update_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
const agent = await platformClient.updateAgent(args.apiKey, args.agentId, {
name: args.name,
description: args.description,
instructions: args.instructions,
type: args.type,
isPublic: args.isPublic,
isShareable: args.isShareable
});
result = {
success: true,
agent,
cost: 0.02,
operation: "update_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'update_agent');
}
break;
case "delete_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.deleteAgent(args.apiKey, args.agentId);
result = {
success: true,
message: "Agent deleted successfully",
cost: 0.01,
operation: "delete_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'delete_agent');
}
break;
case "add_user_to_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.addUserToAgent(args.apiKey, args.agentId, args.userEmail);
result = {
success: true,
message: `Access granted to ${args.userEmail}`,
cost: 0.005,
operation: "add_user_to_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'add_user_to_agent');
}
break;
case "remove_user_from_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.removeUserFromAgent(args.apiKey, args.agentId, args.userEmail);
result = {
success: true,
message: `Access removed for ${args.userEmail}`,
cost: 0.005,
operation: "remove_user_from_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'remove_user_from_agent');
}
break;
case "get_usage_report":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
const report = await platformClient.getUsageReport(args.apiKey, args.days);
result = {
success: true,
report,
cost: 0.002,
operation: "get_usage_report",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'get_usage_report');
}
break;
case "get_pricing":
try {
const pricing = await agentService.getPricing();
result = {
success: true,
pricing,
cost: 0.001,
operation: "get_pricing"
};
} catch (error) {
result = handlePlatformError(error, 'get_pricing');
}
break;
default:
return res.status(400).json({ error: `Unknown tool: ${toolName}` });
}
// Record usage if payment system is enabled and operation was successful
if (result.success && result.cost > 0 && args.apiKey) {
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (keyValidation.valid && keyValidation.userId) {
await paymentManager.recordUsage(keyValidation.userId, toolName, result.cost);
}
} catch (error) {
console.warn('β οΈ Failed to record usage:', error);
}
}
res.json(result);
} catch (error) {
console.error(`β Error executing root endpoint tool ${toolName}:`, error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// MCP HTTP Endpoints for developers
app.get('/tools', (req, res) => {
const tools = createAgentTools();
res.json({
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}))
});
});
app.get('/pricing', async (req, res) => {
try {
const pricing = await agentService.getPricing();
res.json(pricing);
} catch (error) {
res.status(500).json({ error: 'Failed to get pricing', message: error instanceof Error ? error.message : 'Unknown error' });
}
});
// Standard OAuth Resource Server Metadata Endpoint (ATXP SDK expects this path)
app.get('/.well-known/oauth-protected-resource', (req, res) => {
console.log('π OAuth resource metadata requested (standard path)');
res.json({
resource: "https://moluabi-mcp-server.replit.app",
authorization_servers: ["https://auth.atxp.ai"],
authorization_server: "https://auth.atxp.ai",
issuer: "https://auth.atxp.ai",
authorization_endpoint: "https://auth.atxp.ai/oauth/authorize",
token_endpoint: "https://auth.atxp.ai/oauth/token",
scopes_supported: ["mcp:tools", "mcp:read", "mcp:write"],
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
resource_server_metadata: {
name: "MoluAbi ATXP MCP Server",
version: "2.0.0",
supported_operations: ["agent_management", "tool_execution"]
}
});
});
// Alternative OAuth Resource Server Metadata Endpoint for ATXP (backward compatibility)
app.get('/.well-known/oauth-protected-resource/atxp', (req, res) => {
console.log('π OAuth resource metadata requested for ATXP (legacy path)');
res.json({
resource: "https://moluabi-mcp-server.replit.app",
authorization_servers: ["https://auth.atxp.ai"],
authorization_server: "https://auth.atxp.ai",
issuer: "https://auth.atxp.ai",
authorization_endpoint: "https://auth.atxp.ai/oauth/authorize",
token_endpoint: "https://auth.atxp.ai/oauth/token",
scopes_supported: ["mcp:tools", "mcp:read", "mcp:write"],
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
resource_server_metadata: {
name: "MoluAbi ATXP MCP Server",
version: "2.0.0",
supported_operations: ["agent_management", "tool_execution"]
}
});
});
app.post('/mcp/call', express.json(), async (req, res) => {
// Accept both "tool" and "name" parameters for backward compatibility
const toolName = req.body.tool || req.body.name;
const args = req.body.arguments;
try {
if (!toolName) {
return res.status(400).json({ error: 'Missing tool/name parameter' });
}
// Validate arguments using the same validation as MCP
validateToolArguments(toolName, args);
// All tools now use API key authentication
if (!args.apiKey) {
return res.status(400).json({
error: 'API key required',
message: 'All operations require a valid MoluAbi API key (format: mab_...)'
});
}
let result;
// Handle tool calls with new API key authentication
switch (toolName) {
case "create_agent":
result = await handleCreateAgent(args);
break;
case "list_agents":
result = await handleListAgents(args);
break;
case "get_agent":
result = await handleGetAgent(args);
break;
case "prompt_agent":
result = await handlePromptAgent(args);
break;
case "update_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
const agent = await platformClient.updateAgent(args.apiKey, args.agentId, {
name: args.name,
description: args.description,
instructions: args.instructions,
type: args.type,
isPublic: args.isPublic,
isShareable: args.isShareable
});
result = {
success: true,
agent,
cost: 0.02,
operation: "update_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'update_agent');
}
break;
case "delete_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.deleteAgent(args.apiKey, args.agentId);
result = {
success: true,
message: `Agent ${args.agentId} deleted successfully`,
cost: 0.01,
operation: "delete_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'delete_agent');
}
break;
case "add_user_to_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.addUserToAgent(args.apiKey, args.agentId, args.userEmail);
result = {
success: true,
message: `Access granted to ${args.userEmail}`,
cost: 0.005,
operation: "add_user_to_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'add_user_to_agent');
}
break;
case "remove_user_from_agent":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
await platformClient.removeUserFromAgent(args.apiKey, args.agentId, args.userEmail);
result = {
success: true,
message: `Access removed for ${args.userEmail}`,
cost: 0.005,
operation: "remove_user_from_agent",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'remove_user_from_agent');
}
break;
case "get_usage_report":
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (!keyValidation.valid) {
result = { success: false, error: "Invalid API key", cost: 0 };
break;
}
const report = await platformClient.getUsageReport(args.apiKey, args.days);
result = {
success: true,
report,
cost: 0.002,
operation: "get_usage_report",
organizationId: keyValidation.organizationId
};
} catch (error) {
result = handlePlatformError(error, 'get_usage_report');
}
break;
case "get_pricing":
try {
const pricing = await agentService.getPricing();
result = {
success: true,
pricing,
cost: 0.001,
operation: "get_pricing"
};
} catch (error) {
result = handlePlatformError(error, 'get_pricing');
}
break;
default:
return res.status(400).json({ error: `Unknown tool: ${toolName}` });
}
// Record usage if payment system is enabled and operation was successful
if (result.success && result.cost > 0 && args.apiKey) {
try {
const keyValidation = await platformClient.validateAPIKey(args.apiKey);
if (keyValidation.valid && keyValidation.userId) {
await paymentManager.recordUsage(keyValidation.userId, toolName, result.cost);
}
} catch (error) {
console.warn('β οΈ Failed to record usage:', error);
}
}
res.json(result);
} catch (error) {
console.error(`β Error executing HTTP tool ${toolName}:`, error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// ATXP Endpoint - Use transport delegation pattern
if (PAYMENT_DESTINATION && paymentMode === 'atxp' && atxpTransport) {
app.post('/atxp', express.json(), async (req, res) => {
console.log('π ATXP MCP request received:', req.body);
// Set proper headers for MCP protocol
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle specific MCP methods that ATXP SDK requires
const { jsonrpc, method, params, id } = req.body;
if (!jsonrpc || jsonrpc !== "2.0") {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid JSON-RPC request"
},
id: id || null
});
}
try {
if (method === "initialize") {
console.log('π οΈ MCP initialize called on ATXP');
return res.json({
jsonrpc: "2.0",
result: {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
logging: {}
},
serverInfo: {
name: "moluabi-atxp-server",
version: "2.0.0"
}
},
id
});
} else {
// Let the MCP transport handle other methods (tools/list, tools/call)
await atxpTransport.handleRequest(req, res, req.body);
}
} catch (error) {
console.error('β Error handling ATXP MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
}
// Start HTTP server
app.listen(PORT, '0.0.0.0', () => {
console.log(`π HTTP server listening on port ${PORT}`);
console.log('π Available endpoints:');
console.log(' GET / - Health check');
console.log(' POST / - Root tool execution (API Key method)');
console.log(' POST /mcp/call - MCP tool execution (API Key method)');
console.log(' POST /atxp - ATXP MCP endpoint (OAuth2 + Payment)');
console.log('π API Key method: https://moluabi-mcp-server.replit.app/mcp/call');
console.log('π ATXP method: https://moluabi-mcp-server.replit.app/atxp');
});
}
}