Skip to content

Creating MCP Servers

This guide walks you through creating MCP servers on Universal API. You can create servers through the web UI or API.

Navigate to universalapi.co/mcp-servers and click "Create MCP Server".

Two Creation Modes

1. Write Code Mode

Choose from 4 templates:

  • Basic - Simple single-tool server
  • Multi-Tool - Multiple tools (time, calculator)
  • API Wrapper - External API integration pattern
  • Resources & Prompts - Full MCP features demo

2. Select APIs Mode

  • Multi-select existing Universal API actions
  • Auto-generates MCP tool code wrapping selected actions
  • Customize the generated code before saving

Code Structure

Every MCP server must export a createMcpServer function:

javascript
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { z } = require("zod");

function createMcpServer() {
  const mcpServer = new McpServer({
    name: "my-server",
    version: "1.0.0"
  });

  // Register your tools here
  mcpServer.registerTool("tool_name", {
    title: "Tool Title",
    description: "What this tool does",
    inputSchema: {
      param1: z.string().describe("Parameter description"),
      param2: z.number().optional()
    }
  }, async ({ param1, param2 }) => {
    // Tool implementation
    return {
      content: [{ type: "text", text: "Result" }]
    };
  });

  return mcpServer;
}

module.exports = { createMcpServer };

Available Dependencies

The MCP runtime includes these packages:

  • @modelcontextprotocol/sdk - MCP SDK
  • zod - Schema validation
  • node-fetch - HTTP requests (or use built-in https)

Accessing API Keys

The UniversalAPI runtime automatically injects your stored third-party API keys as environment variables. No JSON parsing needed — just read process.env.KEY_NAME.

How It Works

When a user (or the frontend "Try It" feature) calls your MCP server, the runtime:

  1. Fetches the caller's stored API keys from their account
  2. Flattens each key into an individual environment variable
  3. If the server has an authorRoleToken, also injects the author's keys (author keys override user keys on conflict)
  4. Your code reads process.env.SERPAPI_KEY, process.env.OPENAI_API_KEY, etc.
javascript
// ✅ Simple — just read the env var directly
const apiKey = process.env.SERPAPI_KEY;

// ❌ Don't do this — no need to parse JSON
// const keys = JSON.parse(process.env.UAPI_KEYS_JSON);

Priority Chain

When both the user and the author have the same key name, the author's key wins:

PrioritySourceExample
1 (highest)Author keys (from authorRoleToken)Server owner's AWS credentials
2User keys (from caller's stored keys)Caller's own API keys

Available Environment Variables

VariableDescription
process.env.KEY_NAMEIndividual flattened key (e.g., SERPAPI_KEY, OPENAI_API_KEY)
process.env.UAPI_KEYS_JSONJSON blob of all user keys (backward compat)
process.env.UAPI_AUTHOR_KEYS_JSONJSON blob of all author keys (backward compat)
process.env.NODE_ENVAlways "production"
process.env.AWS_REGIONAWS region (default: us-east-1)

Example: API Wrapper with Key Injection

javascript
const https = require("https");

function createMcpServer() {
  const mcpServer = new McpServer({ name: "my-api", version: "1.0.0" });

  mcpServer.registerTool("search", {
    title: "Search",
    description: "Search using SerpAPI",
    inputSchema: { query: z.string() }
  }, async ({ query }) => {
    // Key is automatically available — no setup needed by the caller
    const apiKey = process.env.SERPAPI_KEY;
    if (!apiKey) {
      return { content: [{ type: "text", text: "Error: SERPAPI_KEY not configured" }], isError: true };
    }

    const data = await callApi(`https://serpapi.com/search.json?api_key=${apiKey}&q=${query}`);
    return { content: [{ type: "text", text: JSON.stringify(data) }] };
  });

  return mcpServer;
}

TIP

Users store their API keys on the Credentials page under "Third-Party Keys". The key's serviceName becomes the environment variable name.

Registering Tools

Writing Effective Tool Descriptions

Good tool descriptions help AI agents understand when and how to use your tools. Follow this format for comprehensive documentation:

javascript
mcpServer.registerTool("tool_name", {
  title: "Human-Readable Title",
  description: `Brief description of what the tool does.

Use this tool when [specific use case].

Example response:
    [Show what the output looks like]

Notes:
    - Important caveat or limitation
    - Authentication requirements
    - Rate limits or quotas

Args:
    param1: Description of parameter (required)
        Example: "example_value"
    param2: Description of optional parameter (optional)
        Default: default_value
        Options: "option1", "option2"

Returns:
    Description of what the tool returns`,
  inputSchema: {
    param1: z.string().describe("Brief param description"),
    param2: z.string().optional().describe("Optional param description")
  }
}, async ({ param1, param2 }) => {
  // Implementation
});

Basic Tool

javascript
mcpServer.registerTool("greet", {
  title: "Greet",
  description: `Greet a person by name.

Use this tool to generate a friendly greeting message.

Example response:
    "Hello, Alice!"

Args:
    name: The name of the person to greet (required)
        Example: "Alice"

Returns:
    A greeting message`,
  inputSchema: {
    name: z.string().describe("Name of the person to greet")
  }
}, async ({ name }) => ({
  content: [{ type: "text", text: `Hello, ${name}!` }]
}));

Tool with Multiple Parameters

javascript
mcpServer.registerTool("calculate", {
  title: "Calculator",
  description: `Perform basic arithmetic operations.

Use this tool for mathematical calculations.

Example response:
    "10 add 5 = 15"

Notes:
    - Division by zero returns an error message
    - Results are returned as strings

Args:
    a: First number in the calculation (required)
        Example: 10
    b: Second number in the calculation (required)
        Example: 5
    operation: The arithmetic operation to perform (required)
        Options: "add", "subtract", "multiply", "divide"

Returns:
    The calculation result as a formatted string`,
  inputSchema: {
    a: z.number().describe("First number"),
    b: z.number().describe("Second number"),
    operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("Operation to perform")
  }
}, async ({ a, b, operation }) => {
  let result;
  switch (operation) {
    case "add": result = a + b; break;
    case "subtract": result = a - b; break;
    case "multiply": result = a * b; break;
    case "divide": result = b !== 0 ? a / b : "Error: Division by zero"; break;
  }
  return {
    content: [{ type: "text", text: `${a} ${operation} ${b} = ${result}` }]
  };
});

Tool with API Call

javascript
const https = require("https");

function apiRequest(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let data = "";
      res.on("data", chunk => data += chunk);
      res.on("end", () => {
        try { resolve(JSON.parse(data)); }
        catch { resolve({ raw: data }); }
      });
    }).on("error", reject);
  });
}

mcpServer.registerTool("get_weather", {
  title: "Get Weather",
  description: `Get current weather for a city.

Use this tool to fetch weather information from an external API.

Example response:
    {
        "city": "San Francisco",
        "temperature": 65,
        "conditions": "Partly cloudy"
    }

Notes:
    - Requires valid city name
    - Returns temperature in Fahrenheit
    - API may have rate limits

Args:
    city: Name of the city to get weather for (required)
        Example: "San Francisco"

Returns:
    JSON object with weather data including temperature and conditions`,
  inputSchema: {
    city: z.string().describe("City name (e.g., 'San Francisco')")
  }
}, async ({ city }) => {
  const data = await apiRequest(`https://api.example.com/weather?city=${city}`);
  return {
    content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
  };
});

Server Configuration

When creating via UI or API, you can configure:

FieldDescriptionDefault
serverNameDisplay nameRequired
descriptionWhat the server doesRequired
visibilitypublic or privateprivate
memoryMbMemory allocation512
timeoutSecExecution timeout60

Author Monetization

If you build a public MCP server that calls external paid services (AWS Textract, OpenAI, Stripe, etc.), you can recover your costs — and earn revenue — by setting author pricing on your resource.

Author Pricing

Set pricing when creating or updating your server. There are 8 pricing dimensions — use any combination:

Compute & Token Pricing:

DimensionFieldDescriptionExample
Per InvocationpricePerInvocationFlat fee per tool call0.001 ($0.001)
Per GB-SecondpricePerGbSecondCompute-based pricing0.00001667
Per Input TokenpricePerInputTokenLLM input token pricing0.000003
Per Output TokenpricePerOutputTokenLLM output token pricing0.000015

Data Transfer Pricing:

DimensionFieldDescriptionExample
Per MB IngresspricePerMbIngressAPI request payload (user → platform)0.01 ($0.01/MB)
Per MB EgresspricePerMbEgressAPI response payload (platform → user)0.01 ($0.01/MB)
Per MB External EgresspricePerMbExternalEgressBytes sent to external APIs (e.g., PDF → Textract)0.05 ($0.05/MB)
Per MB External IngresspricePerMbExternalIngressBytes received from external APIs (e.g., Textract results)0.02 ($0.02/MB)

The data transfer dimensions are ideal for MCP servers that proxy external paid APIs — the author pays the external service and recovers costs proportional to data volume.

bash
# Set author pricing on your MCP server (simple per-invocation)
curl -X PUT "https://api.universalapi.co/mcp-admin/update" \
  -H "Authorization: Bearer uapi_ut_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": "your-server-id",
    "authorPricing": {
      "pricePerInvocation": 0.001
    }
  }'

# Set data transfer pricing (for external API proxies like Textract)
curl -X PUT "https://api.universalapi.co/mcp-admin/update" \
  -H "Authorization: Bearer uapi_ut_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": "your-server-id",
    "authorPricing": {
      "pricePerInvocation": 0.01,
      "pricePerMbExternalEgress": 0.05,
      "pricePerMbExternalIngress": 0.02
    }
  }'

Authors receive 100% of the price they set. A 20% marketplace fee is charged separately to the invoking user. See Credit System for details.

Author Credentials (Role Tokens)

If your MCP server calls external APIs using your own credentials (e.g., you pay for AWS Textract and charge users via authorPricing), attach a role token to inject your keys at runtime:

bash
# 1. Create a role token with your service credentials
curl -X POST "https://api.universalapi.co/user/token/create" \
  -H "Authorization: Bearer uapi_ut_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "tokenName": "my-textract-keys",
    "tokenType": "role",
    "keys": {
      "aws_access_key_id": "AKIA...",
      "aws_secret_access_key": "wJal...",
      "aws_region": "us-east-1"
    }
  }'

# 2. Attach the role token to your MCP server
curl -X PUT "https://api.universalapi.co/mcp-admin/update" \
  -H "Authorization: Bearer uapi_ut_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": "your-server-id",
    "authorRoleToken": "uapi_rt_your_role_token"
  }'

At runtime, your author keys are available alongside the invoking user's keys. The invoking user's keys take priority on conflicts — your author keys fill in any keys the user doesn't have.

Example: AWS Textract MCP Server

The built-in AWS Textract OCR server demonstrates this pattern: the author provides AWS credentials via a role token, sets pricePerInvocation to recover Textract costs, and users invoke the tool without needing their own AWS account.

See Role Tokens for full documentation on creating and managing role tokens.

Creating via API

bash
curl -X POST "https://api.universalapi.co/mcp-admin/create" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer uapi_ut_your_token" \
  -d '{
    "serverName": "my-tools",
    "description": "My custom MCP tools",
    "visibility": "private",
    "sourceCode": "const { McpServer } = require(\"@modelcontextprotocol/sdk/server/mcp.js\");\nconst { z } = require(\"zod\");\n\nfunction createMcpServer() {\n  const mcpServer = new McpServer({ name: \"my-tools\", version: \"1.0.0\" });\n  mcpServer.registerTool(\"hello\", {\n    title: \"Hello\",\n    description: \"Says hello\",\n    inputSchema: {}\n  }, async () => ({ content: [{ type: \"text\", text: \"Hello!\" }] }));\n  return mcpServer;\n}\n\nmodule.exports = { createMcpServer };"
  }'

Testing Your Server

After creating, test with curl:

bash
# Initialize the server
curl -X POST "https://api.universalapi.co/mcp/your-server-id" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'

# List available tools
curl -X POST "https://api.universalapi.co/mcp/your-server-id" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# Call a tool
curl -X POST "https://api.universalapi.co/mcp/your-server-id" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Hello"}}}'

Next Steps

Universal API - The agentic entry point to the universe of APIs