Skip to content

Example: Shipping Tracker

This guide builds a complete MCP server for shipping and delivery tracking. The server exposes three tools that let oHallo’s agents answer customer questions like “Where is my order?”, “What shipments are pending for my account?”, and “How long will delivery take?”

By the end, you will have a working server with realistic mock data, proper error handling, and instructions for testing.

Three tools:

ToolPurpose
get_shipmentLook up a shipment by tracking number
list_shipmentsList shipments for an account, with optional status filter
estimate_deliveryEstimate delivery time between two locations
Terminal window
mkdir shipping-tracker && cd shipping-tracker
npm init -y
npm install @standfast/mcp-server @hono/node-server
npm install -D typescript @types/node

Create tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}

Create src/server.ts with the full implementation:

import { createMcpServer, McpError } from '@standfast/mcp-server'
import type { McpToolDef } from '@standfast/mcp-server'
import { serve } from '@hono/node-server'
// ---------------------------------------------------------------------------
// Mock data -- replace with real database or API calls in production
// ---------------------------------------------------------------------------
interface Shipment {
trackingNumber: string
accountId: string
status: 'processing' | 'in_transit' | 'out_for_delivery' | 'delivered' | 'exception'
origin: string
destination: string
carrier: string
weight: number
weightUnit: string
shippedAt: string | null
estimatedDelivery: string | null
deliveredAt: string | null
events: Array<{ timestamp: string; location: string; description: string }>
}
const shipments: Shipment[] = [
{
trackingNumber: 'SHP-2026-00142',
accountId: 'acc_acme',
status: 'in_transit',
origin: 'Rotterdam, NL',
destination: 'Munich, DE',
carrier: 'EuroExpress',
weight: 12.5,
weightUnit: 'kg',
shippedAt: '2026-03-22T08:00:00Z',
estimatedDelivery: '2026-03-27T18:00:00Z',
deliveredAt: null,
events: [
{ timestamp: '2026-03-22T08:00:00Z', location: 'Rotterdam, NL', description: 'Shipment picked up from warehouse' },
{ timestamp: '2026-03-23T14:30:00Z', location: 'Duisburg, DE', description: 'Arrived at sorting facility' },
{ timestamp: '2026-03-24T06:15:00Z', location: 'Duisburg, DE', description: 'Departed sorting facility' },
],
},
{
trackingNumber: 'SHP-2026-00143',
accountId: 'acc_acme',
status: 'delivered',
origin: 'Amsterdam, NL',
destination: 'Berlin, DE',
carrier: 'EuroExpress',
weight: 3.2,
weightUnit: 'kg',
shippedAt: '2026-03-18T10:00:00Z',
estimatedDelivery: '2026-03-21T18:00:00Z',
deliveredAt: '2026-03-21T14:22:00Z',
events: [
{ timestamp: '2026-03-18T10:00:00Z', location: 'Amsterdam, NL', description: 'Shipment picked up' },
{ timestamp: '2026-03-19T16:00:00Z', location: 'Hannover, DE', description: 'In transit' },
{ timestamp: '2026-03-21T09:00:00Z', location: 'Berlin, DE', description: 'Out for delivery' },
{ timestamp: '2026-03-21T14:22:00Z', location: 'Berlin, DE', description: 'Delivered -- signed by M. Weber' },
],
},
{
trackingNumber: 'SHP-2026-00144',
accountId: 'acc_globex',
status: 'exception',
origin: 'Brussels, BE',
destination: 'Vienna, AT',
carrier: 'AlpineLogistics',
weight: 45.0,
weightUnit: 'kg',
shippedAt: '2026-03-20T07:00:00Z',
estimatedDelivery: '2026-03-25T18:00:00Z',
deliveredAt: null,
events: [
{ timestamp: '2026-03-20T07:00:00Z', location: 'Brussels, BE', description: 'Shipment picked up' },
{ timestamp: '2026-03-22T11:00:00Z', location: 'Stuttgart, DE', description: 'Held at customs -- documentation required' },
],
},
{
trackingNumber: 'SHP-2026-00145',
accountId: 'acc_acme',
status: 'processing',
origin: 'Rotterdam, NL',
destination: 'Paris, FR',
carrier: 'EuroExpress',
weight: 8.0,
weightUnit: 'kg',
shippedAt: null,
estimatedDelivery: null,
deliveredAt: null,
events: [
{ timestamp: '2026-03-25T16:00:00Z', location: 'Rotterdam, NL', description: 'Order received -- awaiting pickup' },
],
},
]
// Delivery time estimates in hours between city pairs
const deliveryEstimates: Record<string, Record<string, { standardHours: number; expressHours: number }>> = {
'rotterdam': {
'munich': { standardHours: 96, expressHours: 48 },
'berlin': { standardHours: 72, expressHours: 36 },
'paris': { standardHours: 48, expressHours: 24 },
'vienna': { standardHours: 120, expressHours: 60 },
},
'amsterdam': {
'munich': { standardHours: 96, expressHours: 48 },
'berlin': { standardHours: 72, expressHours: 36 },
'paris': { standardHours: 48, expressHours: 24 },
'vienna': { standardHours: 120, expressHours: 60 },
},
'brussels': {
'munich': { standardHours: 72, expressHours: 36 },
'berlin': { standardHours: 96, expressHours: 48 },
'paris': { standardHours: 24, expressHours: 12 },
'vienna': { standardHours: 120, expressHours: 60 },
},
}
// ---------------------------------------------------------------------------
// Tool definitions
// ---------------------------------------------------------------------------
const tools: McpToolDef[] = [
{
name: 'get_shipment',
description:
'Look up a shipment by its tracking number. Returns the current status, origin, destination, carrier, shipping events timeline, and estimated or actual delivery date. Use this when a customer asks about a specific shipment or tracking number.',
inputSchema: {
type: 'object',
properties: {
trackingNumber: {
type: 'string',
description: 'The shipment tracking number, e.g. SHP-2026-00142',
},
},
required: ['trackingNumber'],
},
handler: async (args) => {
const trackingNumber = args.trackingNumber as string
const shipment = shipments.find((s) => s.trackingNumber === trackingNumber)
if (!shipment) {
throw new McpError('not_found', `Shipment with tracking number ${trackingNumber} not found`)
}
return {
trackingNumber: shipment.trackingNumber,
status: shipment.status,
origin: shipment.origin,
destination: shipment.destination,
carrier: shipment.carrier,
weight: { value: shipment.weight, unit: shipment.weightUnit },
shippedAt: shipment.shippedAt,
estimatedDelivery: shipment.estimatedDelivery,
deliveredAt: shipment.deliveredAt,
events: shipment.events,
}
},
},
{
name: 'list_shipments',
description:
'List all shipments for a given account. Optionally filter by shipment status. Returns a summary of each shipment including tracking number, status, origin, destination, and dates. Use this when a customer asks about their recent shipments or pending deliveries.',
inputSchema: {
type: 'object',
properties: {
accountId: {
type: 'string',
description: 'The customer account ID, e.g. acc_acme',
},
status: {
type: 'string',
enum: ['processing', 'in_transit', 'out_for_delivery', 'delivered', 'exception'],
description: 'Filter shipments by status. Omit to return all statuses.',
},
},
required: ['accountId'],
},
handler: async (args) => {
const accountId = args.accountId as string
const statusFilter = args.status as string | undefined
let results = shipments.filter((s) => s.accountId === accountId)
if (results.length === 0) {
throw new McpError('not_found', `No shipments found for account ${accountId}`)
}
if (statusFilter) {
results = results.filter((s) => s.status === statusFilter)
}
return {
accountId,
shipments: results.map((s) => ({
trackingNumber: s.trackingNumber,
status: s.status,
origin: s.origin,
destination: s.destination,
carrier: s.carrier,
shippedAt: s.shippedAt,
estimatedDelivery: s.estimatedDelivery,
deliveredAt: s.deliveredAt,
})),
totalCount: results.length,
}
},
},
{
name: 'estimate_delivery',
description:
'Estimate the delivery time between two cities for both standard and express shipping. Returns estimated hours and a human-readable timeframe. Use this when a customer asks how long a delivery will take or wants to compare shipping options.',
inputSchema: {
type: 'object',
properties: {
originCity: {
type: 'string',
description: 'The origin city name, e.g. Rotterdam, Amsterdam, Brussels',
},
destinationCity: {
type: 'string',
description: 'The destination city name, e.g. Munich, Berlin, Paris, Vienna',
},
},
required: ['originCity', 'destinationCity'],
},
handler: async (args) => {
const origin = (args.originCity as string).toLowerCase()
const destination = (args.destinationCity as string).toLowerCase()
const originRoutes = deliveryEstimates[origin]
if (!originRoutes) {
throw new McpError(
'validation_error',
`Origin city "${args.originCity}" is not in our service area. Available origins: Rotterdam, Amsterdam, Brussels.`
)
}
const estimate = originRoutes[destination]
if (!estimate) {
throw new McpError(
'validation_error',
`Destination city "${args.destinationCity}" is not available from ${args.originCity}. Available destinations: Munich, Berlin, Paris, Vienna.`
)
}
const formatHours = (hours: number): string => {
if (hours < 24) return `${hours} hours`
const days = Math.round(hours / 24)
return `${days} business day${days > 1 ? 's' : ''}`
}
return {
origin: args.originCity,
destination: args.destinationCity,
standard: {
estimatedHours: estimate.standardHours,
estimatedTimeframe: formatHours(estimate.standardHours),
},
express: {
estimatedHours: estimate.expressHours,
estimatedTimeframe: formatHours(estimate.expressHours),
},
}
},
},
]
// ---------------------------------------------------------------------------
// Start the server
// ---------------------------------------------------------------------------
async function main() {
const app = await createMcpServer({
name: 'shipping-tracker',
version: '1.0.0',
authType: 'api_key',
tools,
})
serve({ fetch: app.fetch, port: 4200 }, (info) => {
console.log(`Shipping tracker MCP server listening on port ${info.port}`)
})
}
main()

Here is how a real conversation flows when a customer asks about their shipment:

Customer: “Hi, can you check on my shipment SHP-2026-00142? It was supposed to arrive yesterday.”

What happens behind the scenes:

  1. The Orchestrator classifies the intent as a shipping inquiry and routes to the specialist agent that has the shipping tracker tools.
  2. The Specialist Agent calls get_shipment with trackingNumber: "SHP-2026-00142".
  3. The tool returns the shipment details, including the status in_transit, the event timeline, and the estimated delivery date.
  4. The Message Agent composes a response using the returned data:

Agent response: “Your shipment SHP-2026-00142 is currently in transit. It departed the sorting facility in Duisburg, Germany on March 24th and is on its way to Munich. The estimated delivery is March 27th by 6:00 PM. I apologise for the delay — it looks like the shipment is running slightly behind the original schedule.”

If the customer then asks “What other shipments do I have pending?”, the agent calls list_shipments with the customer’s account ID and returns a summary.

Start the server:

Terminal window
MCP_API_KEY=test-key npx tsx src/server.ts

List tools:

Terminal window
curl -s http://localhost:4200/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-key" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}' | jq '.result.tools[].name'

Expected output:

"get_shipment"
"list_shipments"
"estimate_delivery"

Look up a shipment:

Terminal window
curl -s http://localhost:4200/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-key" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_shipment",
"arguments": {
"trackingNumber": "SHP-2026-00142"
}
}
}' | jq .

List shipments with a status filter:

Terminal window
curl -s http://localhost:4200/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-key" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_shipments",
"arguments": {
"accountId": "acc_acme",
"status": "in_transit"
}
}
}' | jq .

Estimate delivery time:

Terminal window
curl -s http://localhost:4200/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-key" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "estimate_delivery",
"arguments": {
"originCity": "Rotterdam",
"destinationCity": "Munich"
}
}
}' | jq .

Test error handling (invalid tracking number):

Terminal window
curl -s http://localhost:4200/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-key" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "get_shipment",
"arguments": {
"trackingNumber": "INVALID-123"
}
}
}' | jq .

When you are ready to deploy:

  1. Replace the mock data with real database queries or API calls to your shipping provider.
  2. Set a strong MCP_API_KEY environment variable.
  3. Deploy to any hosting that supports Node.js and HTTP (Docker, AWS ECS, Railway, Fly.io, a VM).
  4. Register the server in oHallo under Settings then MCP Hub.
  5. Assign the tools to the appropriate specialist agent.