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 modulefetch() 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 namesget_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 serpapiprocess.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