Introduction
You've seen AI assistants do impressive things: answer questions, write code, explain concepts. But have you noticed their limitation? They can't actually do anything in the real world. They can't fetch live data, access your APIs, or interact with external systems.
That's where MCP comes in.
In this guide, you'll build a Fetch MCP Server, a tool that gives AI assistants (LLMs) the ability to fetch web pages and read their content. By the end, you'll have a production-ready server running locally, ready to connect to VS Code, Cursor, or any MCP-compatible client.
What you'll learn:
- What MCP is and why it matters
- How to scaffold an MCP server in seconds with
create-mcp-server - How to implement a real-world tool (fetching URLs)
- How to test your server
Prerequisites:
- Node.js 20 or later
- Basic TypeScript familiarity
Let's build something useful.
What is MCP?
MCP (Model Context Protocol) is an open standard that lets AI Models (LLMs) discover and use external tools. Think of it as USB for AI: a universal connector that lets AI models plug into any tool or service you create.
The Problem
AI Models (LLMs) are powerful but isolated. They're trained on static data and can't access:
- Real-time information from the web
- Your company's internal APIs
- Databases, file systems, or external services
Every time you need the AI to interact with the outside world, you're stuck copying and pasting data manually.
The Solution
MCP provides a standardized way for AI Models (LLMs) to:
- Discover what tools are available
- Understand how to use them (parameters, descriptions)
- Execute them safely with user oversight
Three Core Concepts
MCP servers expose three types of capabilities:
| Concept | Description | Example |
|---|---|---|
| Tools | Actions the AI can perform | Fetch a URL, send an email, query a database |
| Resources | Data the AI can read | Files, database records, API responses |
| Prompts | Pre-defined templates for common tasks | "Summarize this webpage" |
For this tutorial, we'll focus on Tools, specifically a fetch tool that retrieves web content.
Industry Adoption
MCP isn't just a side project. Originally created by Anthropic, the protocol has been donated to the Agentic AI Foundation — a collaborative effort founded by Anthropic, Google, OpenAI, and others. With support from major players including Docker and Cloudflare, MCP is becoming the standard way AI Models interact with external systems.
For the complete specification, see the official MCP documentation.
MCP Transports: How Clients Talk to Servers
Before we build anything, let's understand how MCP clients and servers communicate. MCP supports two transport mechanisms:
Stdio (Standard Input/Output)
The original transport method. The MCP server runs as a subprocess, and the client communicates through stdin/stdout pipes.
Pros: Simple to implement, no network configuration needed.
Cons: Server must run locally, can't be shared across machines or deployed remotely.
Streamable HTTP
The modern transport method. The MCP server runs as an HTTP service, and clients connect over the network.
Pros: Deploy anywhere, share across teams, scale horizontally, add authentication.
Cons: Slightly more complex setup, requires network configuration.
Which Should You Use?
| Use Case | Recommended Transport |
|---|---|
| Local development, personal tools | Stdio |
| Team sharing, production deployment | Streamable HTTP |
| Adding OAuth/authentication | Streamable HTTP |
| Running in Docker/cloud | Streamable HTTP |
create-mcp-server generates Streamable HTTP servers. This is the right choice for production-ready servers that can be deployed, shared, and secured.
The good news: if you understand Streamable HTTP, stdio is even simpler. The MCP SDK handles both, and your tool implementations stay exactly the same. Only the transport layer changes.
Introducing create-mcp-server
Building an MCP server from scratch means setting up Express, configuring transports, handling sessions, and wiring up the MCP protocol. That's a lot of boilerplate before you write your first tool.
create-mcp-server eliminates that friction. It's a CLI tool that scaffolds production-ready MCP servers in seconds:
npx @agentailor/create-mcp-server
The CLI asks a few questions and generates a complete TypeScript project with everything configured.
Template Options
| Feature | Stateless | Stateful |
|---|---|---|
| Session management | — | Yes |
| SSE support | — | Yes |
| OAuth option | — | Yes |
| Endpoints | POST /mcp | POST, GET, DELETE /mcp |
Stateless: Each request is independent. Simple, but no session persistence.
Stateful: Sessions are maintained across requests. Supports Server-Sent Events for streaming. This is what most production servers need.
For this tutorial, we'll use the Stateful template (without OAuth).
Note: If you're following this tutorial in the future and the CLI prompts you to choose a framework, select Official SDK to match this guide.
Setting Up Your Project
Let's scaffold your MCP server. Run the CLI:
npx @agentailor/create-mcp-server@0.2.1 # specify this version for consistency
When prompted, enter:
-
Project name:
fetch-mcp-server -
Package manager:
npm(or your preference) -
Template type:
Stateful -
Enable OAuth authentication?:
No - Initialize git repository?: Your choice
The CLI generates this structure:
fetch-mcp-server/
├── src/
│ ├── server.ts # MCP server (tools, prompts, resources)
│ └── index.ts # Express app and HTTP transport
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
└── README.md
Navigate to the project and install dependencies:
cd fetch-mcp-server
npm install
Your server is ready to run, but it only has placeholder tools. Let's look at the generated code before adding our own.
Understanding the Scaffolded Code
The CLI generates two key files. Let's understand what each does.
src/index.ts — The HTTP Transport
This file handles all the networking:
import { type Request, type Response } from 'express'
import { randomUUID } from 'node:crypto'
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
import { getServer } from './server.js'
const app = createMcpExpressApp()
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}
// ... Express routes for POST, GET, DELETE /mcp
What it does:
- Creates an Express app with MCP middleware
- Manages sessions using unique IDs
- Handles three endpoints:
-
POST /mcp: Main endpoint for tool calls and messages -
GET /mcp: Server-Sent Events for streaming -
DELETE /mcp: Session cleanup
-
You don't need to modify this file. The transport layer is complete, just focus on server.ts.
src/server.ts — Your MCP Server
This is where the magic happens:
import type {
CallToolResult,
GetPromptResult,
ReadResourceResult,
} from '@modelcontextprotocol/sdk/types.js'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
export function getServer() {
const server = new McpServer(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{ capabilities: { logging: {} } }
)
// Register tools, prompts, and resources here
return server
}
The scaffolded version includes example implementations. We'll replace them with our fetch tool.
Building the Fetch Tool
Now for the core of our server: a tool that fetches URLs and converts HTML to readable markdown.
Install the HTML-to-Markdown Library
We need one additional dependency to convert HTML to markdown:
npm install node-html-markdown
Implement the Fetch Tool
Replace the contents of src/server.ts with:
import type { CallToolResult, GetPromptResult } from '@modelcontextprotocol/sdk/types.js'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { NodeHtmlMarkdown } from 'node-html-markdown'
// User agent for HTTP requests
const DEFAULT_USER_AGENT = 'FetchMCPServer/1.0 (+https://github.com/agentailor/fetch-mcp-server)'
export function getServer() {
const server = new McpServer(
{
name: 'fetch-mcp-server',
version: '1.0.0',
},
{ capabilities: { logging: {} } }
)
// Register the fetch tool
server.registerTool(
'fetch',
{
description: `Fetches a URL from the internet and extracts its contents as markdown.
Use this tool when you need to:
- Read documentation or articles from the web
- Get current information from websites
- Access publicly available web content
The HTML content is automatically converted to clean markdown for easier reading.`,
inputSchema: {
url: z.url().describe('The URL to fetch. Must be a valid HTTP or HTTPS URL.'),
max_length: z
.number()
.int()
.positive()
.max(100000)
.default(5000)
.describe('Maximum number of characters to return. Defaults to 5000.'),
start_index: z
.number()
.int()
.min(0)
.default(0)
.describe(
'Start content from this character index. Use for pagination when content is truncated.'
),
raw: z
.boolean()
.default(false)
.describe('If true, returns raw HTML instead of converting to markdown.'),
},
},
async ({ url, max_length = 5000, start_index = 0, raw = false }): Promise<CallToolResult> => {
try {
// Fetch the URL
const response = await fetch(url, {
headers: {
'User-Agent': DEFAULT_USER_AGENT,
},
redirect: 'follow',
})
if (!response.ok) {
return {
content: [
{
type: 'text',
text: `Failed to fetch ${url}: HTTP ${response.status} ${response.statusText}`,
},
],
isError: true,
}
}
const contentType = response.headers.get('content-type') || ''
const html = await response.text()
// Check if the content is HTML
const isHtml =
contentType.includes('text/html') ||
html.trim().toLowerCase().startsWith('<!doctype') ||
html.trim().toLowerCase().startsWith('<html')
let content: string
let prefix = ''
if (isHtml && !raw) {
// Convert HTML to markdown
content = NodeHtmlMarkdown.translate(html)
} else {
content = html
if (!isHtml) {
prefix = `Content-Type: ${contentType}\n\n`
}
}
// Handle pagination
const originalLength = content.length
if (start_index >= originalLength) {
return {
content: [
{
type: 'text',
text: `No more content available. Total length: ${originalLength} characters, requested start: ${start_index}.`,
},
],
}
}
// Extract the requested portion
const truncatedContent = content.slice(start_index, start_index + max_length)
const remaining = originalLength - (start_index + truncatedContent.length)
let result = `${prefix}Contents of ${url}:\n\n${truncatedContent}`
// Add pagination hint if content was truncated
if (remaining > 0) {
const nextIndex = start_index + truncatedContent.length
result += `\n\n---\n[Content truncated. ${remaining} characters remaining. Use start_index=${nextIndex} to continue.]`
}
return {
content: [
{
type: 'text',
text: result,
},
],
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [
{
type: 'text',
text: `Error fetching ${url}: ${errorMessage}`,
},
],
isError: true,
}
}
}
)
// Register a prompt for fetching URLs
server.registerPrompt(
'fetch',
{
description: 'Fetch a URL and extract its contents as markdown',
argsSchema: {
url: z.url().describe('URL to fetch'),
},
},
async ({ url }): Promise<GetPromptResult> => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please fetch and summarize the content from: ${url}`,
},
},
],
}
}
)
return server
}
Code Breakdown
Let's understand what we built:
Tool Registration: The server.registerTool() method defines our fetch tool with:
- A descriptive name and description (helps AI understand when to use it)
- An input schema using Zod for validation
- An async handler function that does the actual work
Input Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
string | — | The URL to fetch (required) |
max_length |
number | 5000 | Max characters to return |
start_index |
number | 0 | Start position for pagination |
raw |
boolean | false | Skip HTML-to-markdown conversion |
HTML Conversion: We use node-html-markdown to convert HTML to clean markdown. This makes web content much easier for AI to process.
Pagination: Long content is truncated with a helpful message showing how to fetch the next chunk.
Error Handling: Network errors and HTTP failures return user-friendly error messages.
Testing Your Server
Start your server:
npm run dev
You should see:
MCP Stateful HTTP Server listening on port 3000
Your server is now running at http://localhost:3000/mcp.
Using MCP Inspector
The project includes a testing tool. In a new terminal:
npm run inspect
Once the inspector opens in your browser:
- Change the transport from STDIO to HTTP
- Click Connect in the left sidebar
Testing the Prompt
- Go to Prompts → List Prompts (you'll see the
fetchprompt we created) - Click on the fetch prompt
- Paste a URL in the input field
Testing the Tool
- Go to Tools → List Tools
- Click the fetch tool
- You'll see a form with all the parameters we defined (
url,max_length,start_index,raw) - Fill in the url (required) and keep the defaults for other fields
- Click Run Tool
The result will display the fetched content converted to markdown:
Tip: Try fetching
https://modelcontextprotocol.io/docs/getting-started/introto see how the tool converts documentation pages to clean markdown.
Connecting to VS Code or Cursor
To use your server with VS Code or Cursor:
-
Your server URL:
http://localhost:3000/mcp - VS Code setup: See the official VS Code MCP documentation
- Cursor setup: See the official Cursor MCP documentation
Once connected, your AI assistant can use the fetch tool to read web pages directly.
What's Next?
You've built a working MCP server. Here are some ideas to extend it:
Add caching: Store fetched pages to avoid repeated requests for the same URL.
Support more content types: Handle PDFs, images, or JSON APIs.
Add rate limiting: Prevent abuse when deploying publicly.
Secure your server: Add OAuth authentication using our OAuth for MCP Servers guide.
Publish to the registry: Share your server with the community by following our publishing guide.
Deploy with Docker: See our guide on running MCP servers with Docker.
Conclusion
You've just built something that major AI platforms are adopting as a standard. In under 5 minutes, you went from zero to a production-ready MCP server that gives AI assistants the ability to fetch and read web content.
The complete code is available on GitHub: agentailor/fetch-mcp-server
Clone it, extend it, deploy it. The MCP ecosystem is growing fast, and now you're part of it.
Enjoying content like this? Sign up for Agent Briefings, where I share insights and news on building and scaling MCP Servers and AI agents.
Resources
- Fetch MCP Server (GitHub) — Complete code from this tutorial
- create-mcp-server (GitHub) — The CLI tool used in this guide
- MCP Documentation — Official protocol specification
- VS Code MCP Servers — VS Code integration guide
- Cursor MCP Documentation — Cursor integration guide





Top comments (0)