Build Your Own MCP Server: From Zero to Published
Build and ship an MCP Server with @modelcontextprotocol/sdk: project setup, declaring tools, choosing stdio vs Streamable HTTP, local testing, OAuth, and a launch checklist.
How to
Init the project and install the SDK
npm init -y then npm i @modelcontextprotocol/sdk; set type=module in package.json, write src/server.ts and src/index.ts, and configure a bin entry.
Register ListTools + CallTool
server.setRequestHandler(ListToolsRequestSchema, ...) returns the tool list (name/description/inputSchema); implement tool logic in CallToolRequestSchema and return a structured result.
Hook up a transport
Use StdioServerTransport for local subprocess; for cloud use StreamableHTTPServerTransport plus OAuth metadata. Support one or both.
Test with MCP Inspector
Run npx @modelcontextprotocol/inspector to open the local debugger and verify the ListTools → CallTool flow, confirming schemas and argument parsing.
Publish and run the security checklist
Before shipping: OAuth, rate limits, a sensitive-tool allowlist, prompt-injection protection; document per-Host setup in the README and publish to npm / GitHub.
MCP (Model Context Protocol) is the open standard for LLM tool calling, and an MCP Server is the side that "exposes your capability". This article walks the whole path in TypeScript, from init to launch.
1. Init from scratch
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm i @modelcontextprotocol/sdk
Add "type": "module" to package.json and declare a bin entry:
{
"type": "module",
"bin": { "my-mcp-server": "./dist/index.js" }
}
2. Register tools: ListTools + CallTool
A minimal server's core is two handlers. Start with src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "my-mcp-server", version: "0.1.0" });
server.registerTool(
"echo",
{
description: "Echo back the input verbatim — a minimal working example",
inputSchema: { message: z.string().describe("The message to echo back") },
},
async ({ message }) => ({
content: [{ type: "text", text: `You said: ${message}` }],
})
);
export default server;
The SDK's McpServer.registerTool auto-generates the ListTools and CallTool JSON-RPC handling — tools carry a Zod schema and input is validated automatically.
3. Pick a transport: stdio or Streamable HTTP?
Choose the transport in src/index.ts:
import server from "./server.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
- Local subprocess →
StdioServerTransport; the Host spawns your binary and talks over stdin/stdout. - Cloud, multi-user →
StreamableHTTPServerTransport, an HTTP endpoint paired with OAuth.
Get stdio working locally first, then add HTTP when you need it.
4. Test locally: MCP Inspector
No Host needed — the official debugger validates the whole flow:
npx @modelcontextprotocol/inspector node dist/index.js
In the Inspector you can: browse tools and schemas, call a tool manually, and watch the JSON-RPC messages. Confirm ListTools → CallTool before wiring up any client.
5. Wire it into ChatGPT / Claude / Cursor
For local development, add the server to the client config. Claude Desktop example (claude_desktop_config.json):
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"]
}
}
}
ChatGPT and Cursor each have their own MCP config entry point — document all of them in the README.
6. Publish: Streamable HTTP + OAuth
For multi-user or cloud access, switch to HTTP:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// Mount the transport in an Express / Hono route and register OAuth metadata
- Register OAuth 2.0 metadata backed by your own auth backend (use
@modelcontextprotocol/sdk/auth). - Add rate limits, request-size caps, and a CORS allowlist.
- Keep sensitive tools on an explicit allowlist with user confirmation.
7. Pre-launch security checklist
- Treat every tool return value as untrusted input (prompt-injection defense).
- Destructive actions (delete, send, pay) require Host confirmation.
- Don't hand credentials to tools; serve sensitive data through OAuth authorized resources.
- Log calls and periodically review who's invoking which tool.
8. Common errors and troubleshooting
- Host can't connect → confirm the stdio command and path; for HTTP check OAuth metadata and CORS.
- CallTool schema validation fails → Zod schema too tight or type mismatch; inspect the args in the Inspector.
- Tools not appearing → ListTools not registered or registerTool threw; check init order.
- Wrong response shape → results must be
{ content: [...] }; use the text type for plain text.
9. What's Next
- The Complete Guide to MCP: How It Works and Practical Builds — protocol primitives and architecture
- Function Calling with the Responses API — using tools in the OpenAI ecosystem
- OpenAI API Getting Started: Your First GPT-5.6 Call
Key points
- A minimal server with @modelcontextprotocol/sdk is ~50-200 lines of TypeScript; the core is registering ListTools + CallTool
- Pick the transport by scenario: stdio for local subprocesses, Streamable HTTP for cloud / multi-user services
- Tools are callable functions with JSON Schema inputs; resources are URI-addressed data; prompts are reusable templates
- Test locally with MCP Inspector (ListTools → CallTool) before wiring up any Host
- Before launch: OAuth, rate limits, a sensitive-tool allowlist, and prompt-injection protection
- Current tool schema version: MCP 2025-03-26 spec; latest stable typescript-sdk
Frequently asked questions
Official references
Related articles
Subscribe to GPTMap Weekly
One email every Monday: curated OpenAI updates, deep dives, and best practices. No ads, unsubscribe anytime.