Skip to main content

Build Your Own Server

Create a custom MCP server that integrates with CodeSpar's session model, billing, and tool routing.

Build Your Own Server

CodeSpar's 57 MCP servers cover the most common Latin American commerce APIs. But if you need to integrate a proprietary API, internal service, or unsupported provider, you can build your own MCP server and plug it into the CodeSpar ecosystem.

What you're building

An MCP server is a process that exposes tools over the Model Context Protocol. Each tool is a function with:

  • A name (e.g., myapi_create_order)
  • A description (natural language, shown to LLMs)
  • An input_schema (JSON Schema for parameters)
  • A handler (the function that calls your API)

When your agent calls the tool, CodeSpar routes the request to your server, which executes the handler and returns the result.

Prerequisites

npm install @modelcontextprotocol/sdk

Step-by-step

Define your tools

Start by listing the operations your API supports. Each operation becomes a tool:

tools.ts
import { z } from "zod";

export const tools = {
  create_order: {
    description: "Create a new order in MyAPI",
    inputSchema: z.object({
      customer_id: z.string().describe("Customer identifier"),
      items: z.array(z.object({
        product_id: z.string(),
        quantity: z.number().int().positive(),
      })).describe("Order line items"),
      notes: z.string().optional().describe("Optional order notes"),
    }),
  },
  get_order: {
    description: "Get order details by ID",
    inputSchema: z.object({
      order_id: z.string().describe("Order ID to look up"),
    }),
  },
  list_orders: {
    description: "List orders with optional filters",
    inputSchema: z.object({
      status: z.enum(["pending", "paid", "shipped", "delivered"]).optional(),
      from: z.string().optional().describe("Start date (ISO 8601)"),
      to: z.string().optional().describe("End date (ISO 8601)"),
      limit: z.number().int().max(100).default(50),
    }),
  },
};

Implement the handlers

Each handler receives the validated input and calls your API:

handlers.ts
import { MyAPIClient } from "./client";

const client = new MyAPIClient({
  baseUrl: process.env.MYAPI_BASE_URL!,
  apiKey: process.env.MYAPI_API_KEY!,
});

export async function createOrder(input: {
  customer_id: string;
  items: { product_id: string; quantity: number }[];
  notes?: string;
}) {
  const order = await client.orders.create({
    customerId: input.customer_id,
    items: input.items,
    notes: input.notes,
  });

  return {
    order_id: order.id,
    status: order.status,
    total: order.total,
    created_at: order.createdAt,
  };
}

export async function getOrder(input: { order_id: string }) {
  return client.orders.get(input.order_id);
}

export async function listOrders(input: {
  status?: string;
  from?: string;
  to?: string;
  limit: number;
}) {
  return client.orders.list({
    status: input.status,
    dateFrom: input.from,
    dateTo: input.to,
    limit: input.limit,
  });
}

Create the MCP server

Wire tools and handlers together into an MCP server:

server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { tools } from "./tools";
import { createOrder, getOrder, listOrders } from "./handlers";

const server = new Server(
  { name: "myapi", version: "1.0.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler("tools/list", async () => ({
  tools: [
    {
      name: "myapi_create_order",
      description: tools.create_order.description,
      inputSchema: tools.create_order.inputSchema,
    },
    {
      name: "myapi_get_order",
      description: tools.get_order.description,
      inputSchema: tools.get_order.inputSchema,
    },
    {
      name: "myapi_list_orders",
      description: tools.list_orders.description,
      inputSchema: tools.list_orders.inputSchema,
    },
  ],
}));

server.setRequestHandler("tools/call", async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "myapi_create_order":
      return { content: [{ type: "text", text: JSON.stringify(await createOrder(args)) }] };
    case "myapi_get_order":
      return { content: [{ type: "text", text: JSON.stringify(await getOrder(args)) }] };
    case "myapi_list_orders":
      return { content: [{ type: "text", text: JSON.stringify(await listOrders(args)) }] };
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);

Package and publish

Create a package.json and publish to npm:

package.json
{
  "name": "@yourorg/mcp-myapi",
  "version": "1.0.0",
  "type": "module",
  "bin": { "mcp-myapi": "./dist/server.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  }
}
npm publish --access public

Use with CodeSpar

Once published, your server can be used in CodeSpar sessions:

const session = await codespar.sessions.create({
  servers: ["myapi", "stripe", "correios"],
});

const result = await session.execute("myapi_create_order", {
  customer_id: "cus_123",
  items: [{ product_id: "prod_abc", quantity: 2 }],
});

To register your server in the CodeSpar catalog so other users can discover it, open an issue on GitHub or contact us at enterprise@codespar.dev.

Best practices for LLM-friendly tools

  1. Write clear descriptions. The LLM reads description to decide when to call the tool. Be specific: "Create a new order with line items and shipping address" is better than "Create order".

  2. Use descriptive parameter names. customer_id is better than cid. The LLM maps natural language to parameter names.

  3. Add .describe() to every field. Zod's .describe() becomes the description field in JSON Schema, which the LLM uses to understand what to pass.

  4. Return structured data. Return objects with clear field names, not raw strings. The LLM can reason better about { "order_id": "ord_123", "status": "paid" } than "Order created successfully".

  5. Prefix tool names with your server name. Use myapi_create_order, not create_order. This avoids collisions when multiple servers are loaded in the same session.

  6. Handle errors gracefully. Return error objects instead of throwing. The LLM can reason about { "error": "Customer not found" } and try a different approach.

  7. Keep input schemas simple. Avoid deeply nested objects. Flat schemas with clear types produce better LLM tool-calling accuracy.

Using the MCP Generator instead

If you have an existing REST API with Express, Fastify, or Next.js routes, the MCP Generator can automatically scan your source code and generate the tool definitions and server code. This is faster than writing tools by hand for large APIs.

Next steps