Complete "MCP Fundamentals and Architecture" first to unlock this lesson.Go to previous lesson

Goal and Architecture

In this lesson, you will build a local MCP server that exposes one structured capability: calculating a shipping quote from a package weight, destination zone, and service level. The capability is implemented as an MCP tool because a client actively invokes it with arguments and receives a result. By the end, an MCP-compatible client can discover the tool, inspect its input schema, validate a call, and receive both human-readable text and structured data.

The server will use TypeScript, Node.js 20 or later, and the official MCP TypeScript server package. It will communicate through the stdio transport. In this model, an MCP client starts the server as a child process and exchanges JSON-RPC messages through standard input and standard output. This transport is appropriate for local AI applications, command-line hosts, and development tools. A remotely deployed shared server would normally use Streamable HTTP instead.

The request flow is simple. The client starts the process, completes the MCP initialization handshake, lists available tools, and calls the selected tool. The SDK validates the supplied arguments before running your handler. Your handler performs the business calculation and returns an MCP result. This separation is important: MCP defines discovery and communication, while your application code owns the shipping rules.

flowchart showing an AI application containing an MCP client, a stdio JSON-RPC connection, an MCP server, input-schema validation, the shipping quote handler, and a structured result returning to the application

Project Setup

Create a new project and install the server SDK, Zod, TypeScript, and the tsx runner. The package is configured as an ES module because the current SDK uses ES module imports. The tsx runner executes the TypeScript entry point directly, which keeps this first exercise focused on MCP rather than build tooling.

bash
mkdir shipping-mcp-server
cd shipping-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install --save-dev typescript tsx @types/node
mkdir src

Add scripts to package.json so the launch command is stable. The server script runs the stdio process, while the inspect script starts the MCP Inspector and tells it how to launch that same process.

json
{
"name": "shipping-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"tsx": "^4.0.0",
"typescript": "^6.0.0"
}
}

The exact resolved dependency versions may be newer when you install them. The important architectural detail is that new server projects use the dedicated server package rather than the older monolithic SDK package. If package installation fails, confirm that Node.js and npm are available and that your Node.js version is at least 20.

Implementing the Server

Create src/index.ts with the following implementation. The createServer function constructs a fresh MCP server and registers the calculate-shipping tool. The input schema accepts a positive package weight, one of three destination zones, and one of two service levels. The output schema describes the structured result that clients can consume without parsing prose.

typescript
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const inputSchema = z.object({
weightKg: z.number().positive().max(50),
zone: z.enum(['domestic', 'regional', 'international']),
service: z.enum(['standard', 'express'])
});
const outputSchema = z.object({
currency: z.literal('USD'),
baseCost: z.number(),
serviceSurcharge: z.number(),
totalCost: z.number(),
estimatedDays: z.number().int()
});
function createServer(): McpServer {
const server = new McpServer({
name: 'shipping-quote-server',
version: '1.0.0'
});
server.registerTool(
'calculate-shipping',
{
title: 'Calculate Shipping Quote',
description: 'Calculate a shipping price and delivery estimate for a package',
inputSchema,
outputSchema
},
async ({ weightKg, zone, service }) => {
const ratePerKg = {
domestic: 2.5,
regional: 5,
international: 9
}[zone];
const standardDays = {
domestic: 4,
regional: 7,
international: 14
}[zone];
const baseCost = Number((5 + weightKg * ratePerKg).toFixed(2));
const serviceSurcharge = service === 'express'
? Number((baseCost * 0.6).toFixed(2))
: 0;
const totalCost = Number((baseCost + serviceSurcharge).toFixed(2));
const estimatedDays = service === 'express'
? Math.max(1, Math.ceil(standardDays / 2))
: standardDays;
const result = {
currency: 'USD' as const,
baseCost,
serviceSurcharge,
totalCost,
estimatedDays
};
return {
content: [
{
type: 'text',
text: `Shipping costs USD ${totalCost.toFixed(2)} and takes approximately ${estimatedDays} days.`
}
],
structuredContent: result
};
}
);
return server;
}
void serveStdio(createServer);
console.error('Shipping MCP server is running on stdio');

The tool description should state what the capability does, because an AI agent uses the name, description, and generated JSON Schema when deciding whether to call it. The schema is also an executable boundary. A negative weight, an unsupported zone, or an unknown service is rejected before the calculation handler executes.

Understanding the Result

The handler returns two representations of the same outcome. The content array contains a text block that a model or user interface can read directly. The structuredContent value contains predictable fields for applications that need to display a price, compare options, save the result, or pass it to another tool. Because an output schema is registered, the structured value must match that schema.

For an input containing a weight of 3 kilograms, the regional zone, and express service, the base cost is 20 dollars. The express surcharge is 12 dollars, producing a total of 32 dollars and an estimated delivery time of four days. A successful result has this logical shape.

json
{
"content": [
{
"type": "text",
"text": "Shipping costs USD 32.00 and takes approximately 4 days."
}
],
"structuredContent": {
"currency": "USD",
"baseCost": 20,
"serviceSurcharge": 12,
"totalCost": 32,
"estimatedDays": 4
}
}

Structured output is preferable to embedding all information in one sentence. A client should not need regular expressions to recover the total price or delivery estimate. The text remains useful for model context, while the structured object provides a stable application interface.

Running and Testing

Start the server directly to verify that the process loads without a syntax or import error. It should print its status message and continue waiting because an stdio server needs an MCP client to send requests. Stop it with the terminal interrupt command after confirming that it remains active.

bash
npm start

Next, launch the MCP Inspector. The Inspector starts your server, performs the connection handshake, and provides a browser interface for listing and calling tools. Connect to the server, open the Tools view, select calculate-shipping, and submit valid values such as 3, regional, and express.

bash
npm run inspect

Verify three behaviors rather than checking only the happy path. First, confirm that the tool appears with the expected description and input fields. Second, call it with valid data and compare the structured result with the calculation rules. Third, try an invalid weight such as minus 2 or an unsupported zone. The invalid request should fail schema validation instead of reaching the handler.

testing sequence showing tool discovery, a valid calculate-shipping call with weightKg 3, zone regional, service express, the structured USD 32 result, and an invalid negative-weight call stopped by schema validation

Common Failures and Next Steps

The most destructive stdio mistake is writing application logs to standard output. Standard output is reserved for protocol messages, so an ordinary log line can corrupt the JSON-RPC stream and disconnect the client. Use standard error for diagnostics, as shown by the status message. This restriction applies to your own code and to libraries that may print unexpected output.

If the Inspector cannot start the process, run the start command directly from the project root. Import errors usually indicate incorrect package names, an old Node.js version, or a missing ES module configuration. If the tool is visible but calls fail, inspect the submitted argument names and types. The schema expects a numeric weight and exact enum values; a numeric string is not automatically equivalent to a number.

A server that starts successfully is not automatically a useful MCP server. Its tool needs a precise description, constrained inputs, deterministic behavior, and results that are easy for both models and applications to consume. You now have the complete local path: create a server, register a structured tool, serve it over stdio, and exercise it with a real MCP client. The next practical step is connecting the same launch command to an AI host or replacing the local calculation with a database or external service while preserving the tool contract.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1