Building an MCP Server
Hands-on: building a custom MCP server that exposes your domain tools to agents
Overview
Pre-built MCP servers cover common services like GitHub and PostgreSQL, but every team has domain-specific tools that no generic server provides -- internal APIs, custom deployment pipelines, proprietary data stores, or company-specific workflows. This lesson walks through building a custom MCP server from scratch using the official TypeScript SDK. You will build a server that exposes project-specific tools an agent can use, and you will understand every layer of the process.
Why Build a Custom Server
You should build a custom MCP server when:
- You have an internal API that agents need to access (deployment, monitoring, feature flags)
- You want to give agents domain-specific operations that do not map to a generic tool (e.g., "create a migration for our ORM" or "validate against our schema registry")
- You need to wrap complex workflows as simple tool calls (e.g., a single "deploy to staging" tool that runs five steps behind the scenes)
- You want to enforce business rules at the tool level (e.g., ensuring agents cannot deploy to production without a ticket number)
Think of an MCP server as a domain expert translator -- it understands both the MCP protocol and your specific systems, and it bridges the gap between them.
Project Setup
We will build a project management MCP server that exposes three tools: listing team members, getting project status, and creating task summaries. This is a realistic pattern -- many teams have internal APIs that agents should be able to query.
Start by initializing a new Node.js project:
This lesson uses unpinned dependencies for exploration. For production work, record the exact package versions validated by your project in package.json and commit the lockfile. Do not rely on an unpinned install command for reproducible builds. If you encounter breaking changes in the SDK, check the MCP SDK documentation for migration guidance.
The key dependencies are:
@modelcontextprotocol/sdk-- the official MCP SDK that handles protocol details, transport, and message routingzod-- used for defining and validating tool input schemas
Configure TypeScript for ES modules:
The Server Skeleton
Every MCP server follows the same basic structure: create a server instance, register capabilities, and start listening on a transport. Here is the minimal skeleton:
This is the frame around which everything else is built. The McpServer class handles all the protocol details -- JSON-RPC parsing, capability negotiation, message routing. You focus on defining what your tools do.
Custom MCP server architecture: the project-tools server contains a server skeleton (McpServer + StdioTransport) that exposes three tools (list_team, project_status, create_summary) and one resource (team-directory). The MCP client in Claude Code communicates with the server via stdio.
Defining Tools
Tools are the heart of most MCP servers. Each tool has a name, a description, an input schema, and a handler function. The SDK uses Zod for schema definition, which gives you both runtime validation and TypeScript type inference.
Let us add our first tool -- listing team members:
Let us break down the server.tool() call:
- First argument (
"list_team") -- the tool name the agent will use to call it - Second argument -- a human-readable description that helps the model understand when to use this tool
- Third argument -- a Zod schema defining the input parameters. Each field can have a
.describe()annotation that the model uses to understand what to pass - Fourth argument -- the async handler function that executes when the tool is called
The handler returns a content array of content blocks. The most common type is text, but you can also return image blocks with base64-encoded images.
Adding More Tools
Let us add a project status tool that aggregates information from multiple sources:
And a tool that creates a summary across all projects:
Adding Resources
Resources provide read-only data that agents can access without making a tool call. They are useful for reference information that the agent might need as context.
Resources use URIs to identify themselves. The URI scheme is up to you -- use something descriptive like team:// or project:// for your domain.
The Complete Server
Here is the full server assembled into a single file:
Notice the console.error at the end -- MCP servers must write diagnostic output to stderr, not stdout. Stdout is reserved for JSON-RPC messages.
Building and Testing
Compile and test your server:
Connecting Your Server to Claude Code
To use your server with Claude Code, add it to your project's MCP configuration. Since this is a local server, point the command at the compiled JavaScript file:
After restarting Claude Code, the agent will discover your three tools (list_team, project_status, create_summary) and the team-directory resource during initialization.
Server startup and usage sequence: Claude Code starts the server process, sends initialize, receives capabilities (tools and resources), discovers available tools and resources, then during usage calls project_status with a project name and gets back a formatted status string.
Now you can ask your agent "What is the status of the payments project?" and it will call your custom MCP tool to get the answer.
Patterns for Real-World Servers
The example above uses hardcoded data, but real servers call actual APIs. Here are patterns you will use in production:
Wrapping REST APIs
Note the isError: true flag in the error case. This tells the MCP client that the tool call failed, which helps the model decide whether to retry or report the error.
Handling Authentication via Environment Variables
Always validate required environment variables at startup. Failing fast with a clear error message is better than failing silently when the first tool call arrives.
Writing Effective Tool Descriptions
The quality of your tool descriptions directly affects how well the agent uses your tools. The model reads these descriptions to decide which tool to call and what arguments to pass.
Good descriptions:
- State what the tool does in one sentence
- Mention when to use it (and when not to)
- Use domain language the model will encounter in user requests
Compare these two descriptions:
- Weak:
"Get data"-- the model has no idea what data, when to use it, or what to expect - Strong:
"Get the current deployment status and health metrics for a service. Use when the user asks about uptime, deployment history, or service health."-- the model knows exactly when this tool is relevant
The same principle applies to parameter descriptions. Each .describe() annotation should tell the model what values are valid and what format to use.
Summary
Building a custom MCP server lets you expose your team's domain-specific tools and internal APIs to any MCP-compatible agent. The process follows a consistent pattern: create a server with the SDK, define tools with Zod schemas and handler functions, optionally add resources for reference data, and connect via stdio transport. The key to effective servers is writing clear tool and parameter descriptions that help the model understand when and how to use each capability. In the next lesson, we will cover the production concerns -- security, authentication, performance, and deployment -- that matter when your MCP servers handle real data and real users.