Skip to content

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
serpapiprocess.env.serpapi
openaiprocess.env.openai
twilio_sidprocess.env.twilio_sid
twilio_tokenprocess.env.twilio_token
SERPAPI_KEYprocess.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:

  1. Create a role token with your keys
  2. Set authorRoleToken on your MCP server
  3. 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"
  }' | jq

Via 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"
  }' | jq

Wrapping 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 ​

  1. Use https module — fetch() is available too, but https gives you more control over timeouts
  2. Set request timeouts — The MCP runtime has a ~30s limit. Set your HTTP timeouts to 25s to leave room
  3. Handle missing API keys — Always check process.env.YOUR_KEY and return a helpful error message
  4. Keep responses focused — Don't return entire API responses; extract the relevant fields
  5. 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
  6. Use descriptive tool names — get_stock_price is better than query or fetch_data

Handling Large APIs ​

For APIs with many endpoints (like Polygon.io/Massive with 50+ endpoints), don't wrap every endpoint. Instead:

  1. Identify the top 10-15 most useful endpoints — What would an AI actually need?
  2. Group related operations — e.g., get_stock_price, get_options_chain, get_company_info
  3. Add a helper resource — Register a resource listing all available tools and their parameters
  4. Consider multiple servers — Split into my-api-stocks, my-api-options, my-api-crypto for 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 ​

ConstraintValue
RuntimeNode.js 20.x
Max execution time~30 seconds per tool call
Available modulesSee Available Imports above
No file systemfs and child_process blocked
No WebSocketsHTTP/HTTPS only
Response sizeKeep under 100KB for best AI processing
API keysInjected 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 ​

  1. Store your API key: go to API Keys page → Third-Party Keys → add your key
  2. Use the correct service name (e.g., serpapi, openai, massive)
  3. Key becomes process.env.{exact_key_name} — e.g., key named serpapi → process.env.serpapi

Best Practices ​

  1. Descriptive tool names and descriptions — The AI uses these to decide when to call your tool
  2. Use Zod schemas — Validate inputs with .describe() for each parameter
  3. Return JSON strings — Wrap results in JSON.stringify() for structured data
  4. Handle errors gracefully — Return error messages in the content, don't throw
  5. Check for required keys — Verify API keys exist before making external calls
  6. Keep tools focused — One tool per task, not one tool that does everything
  7. Set HTTP timeouts — Always set 25s timeouts on external API calls
  8. Test incrementally — Start with 1-2 tools, test, then add more

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