DEV Community

Cover image for Create Your First MCP Server in 5 Minutes with create-mcp-server
Ali Ibrahim
Ali Ibrahim

Posted on • Originally published at blog.agentailor.com

Create Your First MCP Server in 5 Minutes with create-mcp-server

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:

  1. Discover what tools are available
  2. Understand how to use them (parameters, descriptions)
  3. 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.

MCP STDIO

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.

MCP Streamable HTTP

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When prompted, enter:

  1. Project name: fetch-mcp-server
  2. Package manager: npm (or your preference)
  3. Template type: Stateful
  4. Enable OAuth authentication?: No
  5. 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
Enter fullscreen mode Exit fullscreen mode

Navigate to the project and install dependencies:

cd fetch-mcp-server
npm install
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should see:

MCP Stateful HTTP Server listening on port 3000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Once the inspector opens in your browser:

  1. Change the transport from STDIO to HTTP
  2. Click Connect in the left sidebar

MCP Inspector Home

Testing the Prompt

  1. Go to PromptsList Prompts (you'll see the fetch prompt we created)
  2. Click on the fetch prompt
  3. Paste a URL in the input field

MCP Inspector Fetch Prompt

Testing the Tool

  1. Go to ToolsList Tools
  2. Click the fetch tool
  3. You'll see a form with all the parameters we defined (url, max_length, start_index, raw)
  4. Fill in the url (required) and keep the defaults for other fields
  5. Click Run Tool

The result will display the fetched content converted to markdown:

MCP Inspector Tool Result

Tip: Try fetching https://modelcontextprotocol.io/docs/getting-started/intro to 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:

  1. Your server URL: http://localhost:3000/mcp
  2. VS Code setup: See the official VS Code MCP documentation
  3. 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

Related Articles

Top comments (0)