Appearance
Creating MCP Servers
Build MCP servers that provide tools, resources, and prompts to AI assistants.
Source Code Structure
MCP servers are Node.js 20.x applications using @modelcontextprotocol/sdk. Your code must export a createMcpServer() function:
javascript
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { z } = require("zod");
function createMcpServer() {
const server = new McpServer({
name: "my-server",
version: "1.0.0"
});
// Register tools, resources, and prompts here
return server;
}
module.exports = { createMcpServer };Registering Tools
Tools are functions that AI assistants can call:
javascript
server.registerTool("greet", {
title: "Greet",
description: "Returns a greeting for the given name",
inputSchema: {
name: z.string().describe("Name to greet")
}
}, async ({ name }) => ({
content: [{ type: "text", text: JSON.stringify({ greeting: `Hello, ${name}!` }) }]
}));Tool with Multiple Parameters
javascript
server.registerTool("search", {
title: "Search",
description: "Search the web for information",
inputSchema: {
query: z.string().describe("Search query"),
limit: z.number().optional().describe("Max results (default: 10)")
}
}, async ({ query, limit = 10 }) => {
// Your search logic here
const results = await performSearch(query, limit);
return {
content: [{ type: "text", text: JSON.stringify(results) }]
};
});Registering Resources
Resources provide data that AI assistants can read:
javascript
server.resource(
"info",
"my-server://info",
{
description: "Server information",
mimeType: "application/json"
},
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
name: "my-server",
version: "1.0.0",
tools: ["greet", "search"]
})
}]
})
);Registering Prompts
Prompts are pre-built templates for common tasks:
javascript
server.prompt(
"analyze-data",
"Prompt to analyze a dataset",
{
topic: z.string().describe("What to analyze")
},
async ({ topic }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please analyze the following topic thoroughly: ${topic}`
}
}]
})
);Accessing API Keys
Third-party API keys are automatically injected as environment variables. The environment variable name is exactly the key name you stored — no transformation is applied:
javascript
function createMcpServer() {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool("search", {
title: "Search",
description: "Search the web",
inputSchema: { query: z.string() }
}, async ({ query }) => {
// Keys are available as process.env.{exact_key_name}
const apiKey = process.env.serpapi;
if (!apiKey) {
return {
content: [{ type: "text", text: "Error: serpapi key not configured. Add it at universalapi.co/keys" }]
};
}
const response = await fetch(
`https://serpapi.com/search?q=${encodeURIComponent(query)}&api_key=${apiKey}`
);
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data) }]
};
});
return server;
}Key Naming Convention
The env var name matches the exact key name you used when storing the key. No uppercasing or suffix is applied.
| Key Name (as stored) | Environment Variable |
|---|---|
serpapi | process.env.serpapi |
openai | process.env.openai |
twilio_sid | process.env.twilio_sid |
twilio_token | process.env.twilio_token |
SERPAPI_KEY | process.env.SERPAPI_KEY |
Recommendation
Use lowercase_with_underscores for key names (e.g., twilio_sid, serpapi). This is consistent with how most services name their credentials.
Multiple Keys for One Service
Some services require multiple credentials. Store them as separate keys:
javascript
// Twilio requires both Account SID and Auth Token
const accountSid = process.env.twilio_sid;
const authToken = process.env.twilio_token;
if (!accountSid || !authToken) {
return {
content: [{ type: "text", text: "Missing Twilio keys. Add twilio_sid and twilio_token at universalapi.co/keys" }]
};
}Important
Do NOT use userContext.keys directly in your MCP server code — that is an internal implementation detail with a different object structure. Always use process.env.key_name.
Author Keys (Role Tokens)
If you want to provide your own API keys so users don't need to:
- Create a role token with your keys
- Set
authorRoleTokenon your MCP server - Your keys are injected automatically — author keys override user keys
Available Imports
javascript
// MCP SDK
const { McpServer, ResourceTemplate } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { z } = require("zod");
// Node.js built-ins
const https = require("https");
const http = require("http");
const crypto = require("crypto");
const url = require("url");
const querystring = require("querystring");
const buffer = require("buffer");
const stream = require("stream");
const util = require("util");
const path = require("path");
const os = require("os");WARNING
fetch is available globally (Node.js 20.x). File system (fs) and child process (child_process) are not available for security.
Deploying
Via API
bash
curl -s -X POST https://api.universalapi.co/mcp-admin/create \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"serverName": "my-server",
"description": "A server that does useful things",
"sourceCode": "const { McpServer } = require(\"@modelcontextprotocol/sdk/server/mcp.js\");\nconst { z } = require(\"zod\");\n\nfunction createMcpServer() {\n const server = new McpServer({ name: \"my-server\", version: \"1.0.0\" });\n server.registerTool(\"hello\", {\n title: \"Hello\",\n description: \"Say hello\",\n inputSchema: { name: z.string() }\n }, async ({ name }) => ({\n content: [{ type: \"text\", text: `Hello, ${name}!` }]\n }));\n return server;\n}\nmodule.exports = { createMcpServer };",
"visibility": "public"
}' | jqVia Python Script
python
import requests
TOKEN = "uapi_ut_xxxx_your_token"
source_code = '''
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { z } = require("zod");
function createMcpServer() {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool("hello", {
title: "Hello",
description: "Say hello to someone",
inputSchema: { name: z.string().describe("Name to greet") }
}, async ({ name }) => ({
content: [{ type: "text", text: JSON.stringify({ greeting: `Hello, ${name}!` }) }]
}));
return server;
}
module.exports = { createMcpServer };
'''
response = requests.post(
"https://api.universalapi.co/mcp-admin/create",
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
json={
"serverName": "my-server",
"description": "A greeting server",
"sourceCode": source_code,
"visibility": "public"
}
)
print(response.json())Updating
bash
curl -s -X PUT https://api.universalapi.co/mcp-admin/update \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"serverId": "mcp-xxx",
"sourceCode": "...",
"description": "Updated description"
}' | jqWrapping External REST APIs
One of the most common use cases is wrapping an existing REST API (like Stripe, Twilio, Polygon.io/Massive, GitHub, etc.) into an MCP server so AI assistants can use it.
Pattern: API Wrapper
javascript
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { z } = require("zod");
const https = require("https");
// Helper: make HTTPS GET requests with JSON response
function apiGet(hostname, path, headers = {}) {
return new Promise((resolve, reject) => {
const req = https.request({ hostname, path, method: "GET", headers }, (res) => {
let data = "";
res.on("data", (chunk) => data += chunk);
res.on("end", () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(data) });
} catch {
resolve({ status: res.statusCode, data });
}
});
});
req.on("error", reject);
req.setTimeout(25000, () => { req.destroy(); reject(new Error("Request timeout")); });
req.end();
});
}
function createMcpServer() {
const server = new McpServer({ name: "my-api-wrapper", version: "1.0.0" });
server.registerTool("get_stock_price", {
title: "Get Stock Price",
description: "Get the current price for a stock ticker symbol",
inputSchema: {
ticker: z.string().describe("Stock ticker symbol (e.g., AAPL, TSLA)")
}
}, async ({ ticker }) => {
const apiKey = process.env.MASSIVE_KEY || process.env.POLYGON_KEY;
if (!apiKey) {
return { content: [{ type: "text", text: "Error: API key not configured. Store your key with service name 'massive' or 'polygon'." }] };
}
try {
const result = await apiGet(
"api.polygon.io",
`/v2/aggs/ticker/${ticker.toUpperCase()}/prev?apiKey=${apiKey}`
);
return { content: [{ type: "text", text: JSON.stringify(result.data) }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }] };
}
});
return server;
}
module.exports = { createMcpServer };Key Tips for API Wrappers
- Use
httpsmodule —fetch()is available too, buthttpsgives you more control over timeouts - Set request timeouts — The MCP runtime has a ~30s limit. Set your HTTP timeouts to 25s to leave room
- Handle missing API keys — Always check
process.env.YOUR_KEYand return a helpful error message - Keep responses focused — Don't return entire API responses; extract the relevant fields
- Split large APIs into multiple tools — For an API with 50+ endpoints, create tools for the most useful 10-15. Don't try to wrap everything at once
- Use descriptive tool names —
get_stock_priceis better thanqueryorfetch_data
Handling Large APIs
For APIs with many endpoints (like Polygon.io/Massive with 50+ endpoints), don't wrap every endpoint. Instead:
- Identify the top 10-15 most useful endpoints — What would an AI actually need?
- Group related operations — e.g.,
get_stock_price,get_options_chain,get_company_info - Add a helper resource — Register a resource listing all available tools and their parameters
- Consider multiple servers — Split into
my-api-stocks,my-api-options,my-api-cryptofor very large APIs
POST Requests
javascript
function apiPost(hostname, path, body, headers = {}) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = https.request({
hostname, path, method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": data.length, ...headers }
}, (res) => {
let responseData = "";
res.on("data", (chunk) => responseData += chunk);
res.on("end", () => {
try { resolve({ status: res.statusCode, data: JSON.parse(responseData) }); }
catch { resolve({ status: res.statusCode, data: responseData }); }
});
});
req.on("error", reject);
req.setTimeout(25000, () => { req.destroy(); reject(new Error("Request timeout")); });
req.write(data);
req.end();
});
}Limitations & Constraints
| Constraint | Value |
|---|---|
| Runtime | Node.js 20.x |
| Max execution time | ~30 seconds per tool call |
| Available modules | See Available Imports above |
| No file system | fs and child_process blocked |
| No WebSockets | HTTP/HTTPS only |
| Response size | Keep under 100KB for best AI processing |
| API keys | Injected via process.env — see Accessing API Keys |
Troubleshooting
"Read timed out" Error
If you see AWSHTTPSConnectionPool... Read timed out, the tool execution exceeded the timeout. Solutions:
- Reduce the scope of your API calls (fetch less data per tool call)
- Add request timeouts (
req.setTimeout(25000, ...)) - Split into multiple tools — one tool per API endpoint, not one mega-tool
"Syntax error" on Deploy
Run node -c your-source.js locally to validate JavaScript syntax before deploying.
"Key not configured" Errors
- Store your API key: go to API Keys page → Third-Party Keys → add your key
- Use the correct service name (e.g.,
serpapi,openai,massive) - Key becomes
process.env.{exact_key_name}— e.g., key namedserpapi→process.env.serpapi
Best Practices
- Descriptive tool names and descriptions — The AI uses these to decide when to call your tool
- Use Zod schemas — Validate inputs with
.describe()for each parameter - Return JSON strings — Wrap results in
JSON.stringify()for structured data - Handle errors gracefully — Return error messages in the content, don't throw
- Check for required keys — Verify API keys exist before making external calls
- Keep tools focused — One tool per task, not one tool that does everything
- Set HTTP timeouts — Always set 25s timeouts on external API calls
- Test incrementally — Start with 1-2 tools, test, then add more