Appearance
Creating MCP Servers
This guide walks you through creating MCP servers on Universal API. You can create servers through the web UI or API.
Web UI (Recommended)
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 SDKzod- Schema validationnode-fetch- HTTP requests (or use built-inhttps)
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:
- Fetches the caller's stored API keys from their account
- Flattens each key into an individual environment variable
- If the server has an
authorRoleToken, also injects the author's keys (author keys override user keys on conflict) - 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:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | Author keys (from authorRoleToken) | Server owner's AWS credentials |
| 2 | User keys (from caller's stored keys) | Caller's own API keys |
Available Environment Variables
| Variable | Description |
|---|---|
process.env.KEY_NAME | Individual flattened key (e.g., SERPAPI_KEY, OPENAI_API_KEY) |
process.env.UAPI_KEYS_JSON | JSON blob of all user keys (backward compat) |
process.env.UAPI_AUTHOR_KEYS_JSON | JSON blob of all author keys (backward compat) |
process.env.NODE_ENV | Always "production" |
process.env.AWS_REGION | AWS 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:
| Field | Description | Default |
|---|---|---|
serverName | Display name | Required |
description | What the server does | Required |
visibility | public or private | private |
memoryMb | Memory allocation | 512 |
timeoutSec | Execution timeout | 60 |
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:
| Dimension | Field | Description | Example |
|---|---|---|---|
| Per Invocation | pricePerInvocation | Flat fee per tool call | 0.001 ($0.001) |
| Per GB-Second | pricePerGbSecond | Compute-based pricing | 0.00001667 |
| Per Input Token | pricePerInputToken | LLM input token pricing | 0.000003 |
| Per Output Token | pricePerOutputToken | LLM output token pricing | 0.000015 |
Data Transfer Pricing:
| Dimension | Field | Description | Example |
|---|---|---|---|
| Per MB Ingress | pricePerMbIngress | API request payload (user → platform) | 0.01 ($0.01/MB) |
| Per MB Egress | pricePerMbEgress | API response payload (platform → user) | 0.01 ($0.01/MB) |
| Per MB External Egress | pricePerMbExternalEgress | Bytes sent to external APIs (e.g., PDF → Textract) | 0.05 ($0.05/MB) |
| Per MB External Ingress | pricePerMbExternalIngress | Bytes 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
- Using with Claude - Connect to Claude
- Examples - More code templates
- API Reference - Full API documentation