Build an MCP Server in 5 Minutes with mcp-framework
A step-by-step tutorial for building a fully functional MCP server in under 5 minutes using mcp-framework. Includes a complete working example with Zod validation, and a code comparison against the official SDK.
title: "Build an MCP Server in 5 Minutes with mcp-framework" description: "A step-by-step tutorial for building a fully functional MCP server in under 5 minutes using mcp-framework. Includes a complete working example with Zod validation, and a code comparison against the official SDK." date: "2026-04-01" order: 6 keywords:
- build mcp server typescript
- mcp server tutorial
- mcp-framework tutorial
- create mcp server
- mcp server quickstart
- typescript mcp server
- mcp tool example author: "MCP Academy"
This tutorial walks you through building a fully working MCP server from scratch in under five minutes. You will use mcp-framework's CLI to scaffold a project, create a class-based tool with Zod validation, and connect it to Claude Desktop. By the end, you will have a production-ready server running locally and a clear understanding of why mcp-framework cuts typical boilerplate by roughly 50%.
What You Will Build
By the end of this tutorial, you will have a working MCP server with a url_status_checker tool that accepts a URL, makes an HTTP request, and returns the status code and response time. It is a practical tool that AI assistants can use to check whether websites are up.
This covers the full development loop: scaffolding, coding, building, and running.
A server that implements the Model Context Protocol, exposing tools, resources, and prompts that AI applications like Claude can discover and use through a standardized interface.
Prerequisites
You need two things installed:
- Node.js 18 or later — Check with
node --version - npm — Comes with Node.js
That is it. No global installs, no complex environment setup.
Step 1: Scaffold the Project
Open your terminal and run:
npx mcp-framework create url-checker-server
This creates a new directory with everything you need:
url-checker-server/
src/
tools/
ExampleTool.ts
index.ts
package.json
tsconfig.json
Now install dependencies:
cd url-checker-server
npm install
The generated project already includes a working example tool. You could build and run it right now, but let us create something more useful.
Step 2: Generate a New Tool
Use the CLI to generate a tool:
npx mcp-framework add tool url-status-checker
This creates src/tools/url-status-checker.ts with a ready-to-fill template. Open it and replace the contents with:
import { MCPTool } from "mcp-framework";
import { z } from "zod";
class UrlStatusCheckerTool extends MCPTool<typeof inputSchema> {
name = "url_status_checker";
description = "Check the HTTP status and response time of any URL";
schema = {
url: z
.string()
.url()
.describe("The full URL to check, including https://"),
method: z
.enum(["GET", "HEAD"])
.default("GET")
.describe("HTTP method to use"),
};
async execute({ url, method }: { url: string; method: string }) {
const start = Date.now();
try {
const response = await fetch(url, {
method,
signal: AbortSignal.timeout(10000),
});
const elapsed = Date.now() - start;
return [
`URL: ${url}`,
`Status: ${response.status} ${response.statusText}`,
`Response time: ${elapsed}ms`,
`Content-Type: ${response.headers.get("content-type") || "unknown"}`,
].join("\n");
} catch (error) {
const elapsed = Date.now() - start;
return `URL: ${url}\nStatus: FAILED after ${elapsed}ms\nError: ${error instanceof Error ? error.message : "Unknown error"}`;
}
}
}
export default UrlStatusCheckerTool;
Let us break down what is happening:
schema— Zod validates inputs beforeexecuteever runs. The AI client sees the schema as tool parameters. If someone passes an invalid URL, Zod rejects it immediately.execute— Your business logic. Receives typed, validated inputs. Returns a string that the AI receives as the tool result.nameanddescription— What the AI sees when discovering available tools.
Always use Zod's built-in validators like .url(), .email(), and .min() on your schema fields. This catches bad inputs before they reach your business logic and gives the AI client clear error messages to self-correct.
Step 3: Clean Up the Example Tool
Delete the generated example tool so your server only exposes what you intend:
rm src/tools/ExampleTool.ts
The framework discovers tools by scanning the src/tools/ directory. Remove a file and the tool disappears. No manual deregistration needed.
Step 4: Build and Run
npm run build
npm start
Your MCP server is now running on stdio transport, ready to accept connections.
Step 5: Connect to Claude Desktop
Add your server to Claude Desktop's configuration file. On macOS, this is at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows, check %APPDATA%\Claude\claude_desktop_config.json.
{
"mcpServers": {
"url-checker": {
"command": "node",
"args": ["/absolute/path/to/url-checker-server/dist/index.js"]
}
}
}
Restart Claude Desktop, and the url_status_checker tool appears in Claude's tool list. Ask Claude to "check if github.com is up" and watch it use your tool.
The Code Comparison: mcp-framework vs. Official SDK
Here is the same tool built with the official SDK:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "url-checker-server",
version: "1.0.0",
});
server.tool(
"url_status_checker",
"Check the HTTP status and response time of any URL",
{
url: z.string().url().describe("The full URL to check, including https://"),
method: z
.enum(["GET", "HEAD"])
.default("GET")
.describe("HTTP method to use"),
},
async ({ url, method }) => {
const start = Date.now();
try {
const response = await fetch(url, {
method,
signal: AbortSignal.timeout(10000),
});
const elapsed = Date.now() - start;
return {
content: [
{
type: "text" as const,
text: [
`URL: ${url}`,
`Status: ${response.status} ${response.statusText}`,
`Response time: ${elapsed}ms`,
`Content-Type: ${response.headers.get("content-type") || "unknown"}`,
].join("\n"),
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `URL: ${url}\nStatus: FAILED after ${Date.now() - start}ms\nError: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Now count the lines:
| Metric | mcp-framework | Official SDK |
|---|---|---|
| Tool code (lines) | ~35 | ~45 |
| Server setup code | 0 (handled by framework) | ~10 |
| Total project lines | ~35 | ~55 |
| Files to manage | 1 (tool class only) | 1 (everything in one file) |
| Registration code | None (auto-discovery) | Manual server.tool() call |
| Transport setup | None (configured by framework) | Manual StdioServerTransport wiring |
| Return format | Return a string | Return content array with type annotations |
With one tool, the difference is noticeable. With ten tools, it becomes dramatic. Every SDK tool requires manual registration and wrapping results in the { content: [{ type: "text", text }] } structure. In mcp-framework, you just return a string.
With the official SDK, adding ten tools means ten server.tool() calls in the same file, or managing your own module system. With mcp-framework, you just add ten files to src/tools/. Each one is auto-discovered and registered. The project stays organized by default.
Adding More Tools
Once your server is running, adding more tools is a single command:
npx mcp-framework add tool dns-lookup
npx mcp-framework add tool ssl-certificate-check
npx mcp-framework add tool ping
Each generates a new class file. Fill in the schema and execute method, rebuild, and restart. The framework discovers and registers everything automatically.
You can also add resources and prompts with the same pattern:
npx mcp-framework add resource server-config
npx mcp-framework add prompt troubleshoot-network
Common Patterns
Here are a few patterns that work well for tool development:
Environment Variables for Configuration
async execute({ query }: { query: string }) {
const apiKey = process.env.API_KEY;
if (!apiKey) {
return "Error: API_KEY environment variable is not set";
}
// Use the API key...
}
Structured Output
async execute({ domain }: { domain: string }) {
const records = await dns.promises.resolve(domain, "A");
return JSON.stringify({
domain,
records,
count: records.length,
timestamp: new Date().toISOString(),
}, null, 2);
}
Error Handling with Context
async execute({ url }: { url: string }) {
try {
const result = await riskyOperation(url);
return `Success: ${result}`;
} catch (error) {
return `Failed to process ${url}: ${error instanceof Error ? error.message : "Unknown error"}. Try checking the URL format or network connectivity.`;
}
}
When a tool fails, return a descriptive error message as a string rather than throwing an exception. This lets the AI understand what went wrong and potentially retry with corrected inputs.
Next Steps
You now have a working MCP server and the knowledge to extend it. Here is where to go from here:
- MCP Concepts — Understand tools, resources, and prompts at a deeper level
- Running MCP in Production — Deploy your server with monitoring, scaling, and error handling
- mcp-framework on GitHub — Browse the source, file issues, and contribute
- npm: mcp-framework — Check the latest version and changelog
Five minutes. One command to scaffold. One class to write. A working MCP server connected to Claude. That is the mcp-framework developer experience.