Skip to content

Building MCP Servers

This guide walks through building a production-ready MCP server from scratch.

  1. Create a new project

    Terminal window
    mkdir my-mcp-server
    cd my-mcp-server
    bun init -y
  2. Install dependencies

    Terminal window
    bun add futurity-mcp zod
    bun add -d typescript @types/bun
  3. Create tsconfig.json

    {
    "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
    },
    "include": ["src/**/*"]
    }
  4. Create entry point

    Create src/index.ts:

    import { mcp } from "futurity-mcp";
    const app = mcp({
    name: "my-mcp-server",
    version: "1.0.0",
    });
    app.listen(3000);
    console.log("MCP server running on port 3000");

Good MCP tools follow these principles:

  1. Single responsibility: One tool, one job
  2. Clear descriptions: AI must understand when to use it
  3. Validated inputs: Use Zod for type safety
  4. Meaningful outputs: Return structured, useful data
import { z } from "zod";
import { mcp } from "futurity-mcp";
const app = mcp({
name: "weather-service",
version: "1.0.0",
instructions: `
This service provides weather information.
Use get_weather for current conditions.
Use get_forecast for upcoming weather.
`,
});
// Current weather
app.tool("get_weather", {
description: `Get current weather for a location.
Returns temperature, conditions, humidity, and wind.
Use this when user asks about current weather.`,
input: z.object({
location: z.string().describe("City name or coordinates"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
handler: async ({ location, units }) => {
// Call weather API
const weather = await fetchWeather(location, units);
return {
location: weather.name,
temperature: weather.temp,
units,
conditions: weather.conditions,
humidity: weather.humidity,
wind_speed: weather.wind,
};
},
});
// Forecast
app.tool("get_forecast", {
description: `Get weather forecast for upcoming days.
Returns daily forecasts with high/low temperatures.
Use this when user asks about future weather.`,
input: z.object({
location: z.string(),
days: z.number().min(1).max(7).default(5),
}),
handler: async ({ location, days }) => {
const forecast = await fetchForecast(location, days);
return {
location,
forecast: forecast.map(day => ({
date: day.date,
high: day.high,
low: day.low,
conditions: day.conditions,
})),
};
},
});
// ❌ Bad: Vague description
app.tool("process", {
description: "Process data",
// ...
});
// ✅ Good: Clear and specific
app.tool("analyze_sales_data", {
description: `Analyze sales data to identify trends and insights.
Input: Array of sales records with date, amount, and product.
Output: Summary statistics, top products, and trend analysis.
Use when user wants to understand sales performance.`,
// ...
});
app.tool("get_user", {
description: "Get user by ID",
input: z.object({ id: z.string() }),
handler: async ({ id }) => {
const user = await db.getUser(id);
if (!user) {
return {
error: true,
message: `User with ID ${id} not found`,
};
}
return { user };
},
});

For unexpected errors, throw:

app.tool("risky_operation", {
description: "Perform a risky operation",
handler: async () => {
try {
const result = await externalApi.call();
return { result };
} catch (error) {
throw new Error(`External API failed: ${error.message}`);
}
},
});
app.tool("fetch_data", {
description: "Fetch data from external API",
input: z.object({ endpoint: z.string() }),
handler: async ({ endpoint }) => {
const response = await fetch(`https://api.example.com/${endpoint}`, {
headers: {
"Authorization": `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
return { error: true, message: `API error: ${response.status}` };
}
return await response.json();
},
});
import Database from "bun:sqlite";
const db = new Database("data.db");
app.tool("query_customers", {
description: "Search customers by name",
input: z.object({ name: z.string() }),
handler: async ({ name }) => {
const customers = db
.query("SELECT * FROM customers WHERE name LIKE ?")
.all(`%${name}%`);
return { customers };
},
});
const app = mcp({
name: "secure-server",
version: "1.0.0",
auth: async (req) => {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) {
return false;
}
// Verify token
try {
const payload = await verifyJwt(token);
// Attach user to request context if needed
return true;
} catch {
return false;
}
},
});

For user-authenticated services:

const app = mcp({
name: "oauth-server",
version: "1.0.0",
oauth: {
issuer: process.env.OAUTH_ISSUER,
authorizationEndpoint: `${process.env.OAUTH_ISSUER}/authorize`,
tokenEndpoint: `${process.env.OAUTH_ISSUER}/token`,
scopesSupported: ["read", "write"],
},
auth: async (req) => {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) return false;
// Validate against OAuth provider
return await validateOAuthToken(token);
},
});
import { describe, test, expect } from "bun:test";
describe("weather tools", () => {
test("get_weather returns valid structure", async () => {
const result = await weatherHandler({ location: "London", units: "celsius" });
expect(result).toHaveProperty("temperature");
expect(result).toHaveProperty("conditions");
expect(result.units).toBe("celsius");
});
});
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
let server: ReturnType<typeof app.listen>;
beforeAll(async () => {
server = await app.listen(3001);
});
afterAll(async () => {
await server.stop();
});
test("MCP endpoint responds", async () => {
const response = await fetch("http://localhost:3001/mcp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "tools/list",
id: 1,
}),
});
expect(response.ok).toBe(true);
});
Terminal window
bun run discover http://localhost:3000/mcp
// Use environment variables for configuration
const app = mcp({
name: process.env.SERVER_NAME || "my-server",
version: process.env.SERVER_VERSION || "1.0.0",
});
// API keys and secrets
const API_KEY = process.env.EXTERNAL_API_KEY;
if (!API_KEY) {
throw new Error("EXTERNAL_API_KEY is required");
}
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
// Add a health check tool
app.tool("health", {
description: "Check server health",
handler: async () => {
return {
status: "healthy",
timestamp: new Date().toISOString(),
version: "1.0.0",
};
},
});
  • All tools have clear descriptions
  • Input validation with Zod on all tools
  • Error handling for all external calls
  • Authentication configured
  • CORS configured for production origins
  • Environment variables for all secrets
  • Logging for debugging
  • Health check endpoint
  • Rate limiting considered
  • Documentation for tool usage

See complete examples in the repository:

Terminal window
bun examples/cors.ts # CORS configuration
bun examples/oauth.ts # OAuth metadata
bun examples/stateful.ts # Stateful server
bun examples/todo-app.ts # CRUD operations
bun examples/calculator.ts # Math tools
bun examples/filesystem.ts # Virtual filesystem
bun examples/weather-api.ts # External API integration
bun examples/database.ts # Database operations
bun examples/monday.ts # monday.com integration