Guide
How to build a custom MCP server
The Model Context Protocol (MCP) is how an AI agent discovers and calls tools that live outside its own process. This tutorial walks through building a custom MCP server from an empty file to a running endpoint, then connecting it to Tango so the work your agent does lands in the same queue your humans are working from.
1. What an MCP server actually is
An MCP server is a JSON-RPC endpoint that answers three kinds of question: what tools do you have, what shape is each tool's input, and what happens when I call one. The client — Claude Desktop, Cursor, an OpenClaw or Hermes runtime, or your own harness — handles the model side. Your server only has to be honest about its tools and do the work.
Two transports matter in practice. stdio is the simplest: the client spawns your process locally and talks over stdin/stdout. streamable HTTP is what you want for anything hosted, multi-user, or authenticated — Tango's own server at /mcp uses it.
2. A minimal server
Start with the official TypeScript SDK. This server exposes a single tool and runs over stdio, which is enough to test end-to-end from a desktop client.
npm install @modelcontextprotocol/sdk zodimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "acme-tools", version: "1.0.0" });
server.tool(
"lookup_order",
"Look up an order by its ID and return status and line items.",
{ order_id: z.string().describe("The order ID, e.g. ORD-1042") },
async ({ order_id }) => {
const order = await db.orders.find(order_id);
return { content: [{ type: "text", text: JSON.stringify(order) }] };
},
);
await server.connect(new StdioServerTransport());Register it with a desktop client by pointing the client's config at the command that starts your process:
{
"mcpServers": {
"acme-tools": { "command": "node", "args": ["/abs/path/to/server.js"] }
}
}3. Write tool schemas for a reader who can't ask questions
The description and the JSON schema are the entire interface the model sees. Most bad MCP servers are bad here, not in their business logic. Rules that hold up:
- Name the tool after the action, not the table:
cancel_order, notorders_update. - Describe every parameter, including its format. An agent that guesses a UUID format will guess wrong, and you'll pay for it in failed calls.
- Make required fields required. A tool that silently accepts a missing tenant ID is a cross-tenant bug waiting to happen.
- Return structured, machine-readable errors with a reason the model can act on —
{ error: "lease_held", retry_after_seconds: 120 }beats a stack trace.
4. Going hosted: HTTP and auth
Once more than one person uses the server, stdio stops being enough. Serve the same server over streamable HTTP and authenticate every request. Identity is the part people skip: a hosted MCP server without a per-caller identity cannot enforce tenancy, cannot attribute an action, and cannot be audited afterwards.
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
export async function POST(request: Request) {
const caller = await authenticate(request); // bearer token or OAuth
if (!caller) return new Response("Unauthorized", { status: 401 });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
return transport.handleRequest(request, { context: { caller } });
}Bind the session to one identity at connect time and derive the actor from the session, never from a parameter the model supplies. If the model can pass acting_as, it will eventually pass someone else's.
5. The part a tool server can't solve alone
A working MCP server gives one agent the ability to act. It says nothing about what happens when three agents — or an agent and a human — are pointed at the same work. Assign one issue to three agents and you get three competing pull requests, because nothing in MCP arbitrates who owns a job.
That's the layer Tango adds. Tango is itself an MCP server, so your agent connects to it the same way it connects to yours: claimed work gets a lease so a second agent can't start it, completions get a signed receipt so you can prove who did what, and handoffs move a task between an agent and a person without losing the thread.
{
"mcpServers": {
"acme-tools": { "command": "node", "args": ["/abs/path/to/server.js"] },
"tango": { "url": "https://tango.applayer.io/mcp" }
}
}With both connected, the pattern is: pull the next task from Tango, do the work with your own tools, record artifacts, complete the task. The queue stays true and the record of who did what survives the session.
6. Checklist before you ship
- Every tool has a description a stranger could act on.
- Every hosted request is authenticated and bound to one identity.
- Errors are structured and tell the caller what to do next.
- Destructive tools require an explicit confirmation argument.
- Long-running work reports progress rather than blocking silently.
- Shared work is leased, so two agents can't claim the same job.
Connect your agent to Tango
Full MCP endpoint, auth flow and tool reference — written for the agent to read directly.