The Model Context Protocol is a thin JSON-RPC over stdio or SSE that lets a language model discover and call tools. You define tools. The client runs them on behalf of the agent. The wire format is documented and the SDKs are small.
Building one is not hard. Building one that an agent actually handles well, and that does not leak data or mislead the caller, takes more care. This guide walks both paths: hand-writing a server with FastMCP or the TypeScript SDK, and generating one from an existing REST API or repo. Either way, finish with the security checklist.
You can type every tool by hand, or you can generate the server from an existing surface. Both are valid; the right choice depends on how much custom logic the server needs.
You write the tool definitions and their implementations. Best for small, opinionated tool surfaces where the shape matters more than completeness.
Point SmeltSec's CLI at an OpenAPI spec or a repo and it produces the server. Best for wrapping an existing API with dozens of endpoints, or for a first pass you plan to edit down.
FastMCP is the Python SDK. It uses decorators and type hints to derive tool schemas automatically. The full working minimum:
from fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> dict:
"""Return a weather forecast for a city."""
# call your real API here
return {"city": city, "days": days, "summary": "sunny"}
if __name__ == "__main__":
mcp.run()The TypeScript equivalent uses the official @modelcontextprotocol/sdk package and needs a few more lines of ceremony but behaves the same way:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new Server({ name: "weather-server", version: "1.0.0" }, {
capabilities: { tools: {} },
});
server.setRequestHandler("tools/list", async () => ({
tools: [{
name: "get_forecast",
description: "Return a weather forecast for a city.",
inputSchema: z.object({
city: z.string(),
days: z.number().default(3),
}),
}],
}));
server.setRequestHandler("tools/call", async (req) => {
if (req.params.name === "get_forecast") {
const { city, days } = req.params.arguments;
return { content: [{ type: "text", text: JSON.stringify({ city, days, summary: "sunny" }) }] };
}
throw new Error("unknown tool");
});
await server.connect(new StdioServerTransport());Both snippets are complete. They will run against Claude Desktop or any stdio-aware MCP client once registered.
The published CLI 0.1.0 installs successfully, but its analyzer fails with `require is not defined` in fresh Node.js 22.23.3 tests. Use the SDK path above to build your server, then run the supported test and CI checks below:
npm install -g @smeltsec/cli@0.1.0 smeltsec test ./existing-mcp-server/server.js --categories connectivity,invocation,schema,security,permissions smeltsec ci ./existing-mcp-server --output ./smeltsec-ci.json
Replace the example path with your reviewed JavaScript MCP server. The test command starts that local server. The CI command writes a security, quality and schema report, and returns a nonzero exit when a configured gate fails. Passing these checks does not establish that the server is safe.
Review the tool descriptions, tighten parameter schemas, and remove tools that agents do not need. Check scanner coverage and retained evidence before release. Analysis and generation should be re-tested after a fixed CLI release.
A tool is a name, a description, and a JSON Schema. Agents are surprisingly sensitive to all three.
get_user, create_invoice, search_tickets. Noun-only names confuse planners.The fastest feedback loop is the MCP Inspector. Point it at the server binary and it lists every tool, renders the parameter forms, and sends calls. Three commands:
# Python npx @modelcontextprotocol/inspector python server.py # TypeScript npx @modelcontextprotocol/inspector node build/server.js # or via a source-built SmeltSec CLI node apps/cli/dist/index.js test ./my-mcp-server
Write integration tests that exercise each tool. Simulate the model by calling tools/list and tools/call directly; the protocol is JSON-RPC, which is trivial to mock. If a tool has side effects, give it a dry-run parameter and assert the dry-run behavior in CI.
Every MCP server should pass this list before it reaches a production client. The MCP Server Security guide covers each item in depth.
Each client has its own config file. The structure is similar across all of them: a name, a command, and arguments. Register the server, restart the client, and the tools appear in the agent's tool list.
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/abs/path/to/server.py"]
}
}
}Setup instructions for each supported client live under /docs. Paths and file names differ per OS; the config shape is stable.
The questions developers ask when they sit down to write their first server.
require is not defined. Directory generation is not a verified path in that release. Build an existing server with the SDK examples below, then use smeltsec test and smeltsec ci to review it. The published CLI has no --openapi flag.The SmeltSec CLI takes an OpenAPI spec or a repo and returns an MCP server baseline. Review and test the generated code before configuring it in Claude Desktop or Cursor.