Both official SDKs reduce building an MCP server to writing plain functions with type annotations, the SDK handles JSON-RPC routing, schema generation, and the initialization handshake. This lesson scaffolds a minimal server in each language so you can pick whichever matches your team’s stack, examines precisely where the SDK’s responsibility ends and yours begins, and establishes a project layout that stays sane as the server grows past a handful of tools; the rest of the course uses Python for full examples with TypeScript equivalents noted where they diverge meaningfully.
Python: FastMCP Project Setup
mkdir enterprise-mcp-server && cd enterprise-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]" pydantic
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
name="enterprise-mcp-server",
version="0.1.0",
description="Starter enterprise MCP server",
)
@mcp.tool()
async def ping(message: str) -> str:
"""Simplest possible tool: echoes the input back. Useful as a smoke test."""
return f"pong: {message}"
if __name__ == "__main__":
mcp.run(transport="stdio") # Local development transport, see Lesson 7
# Run it directly, or launch through the MCP Inspector for an interactive UI
npx @modelcontextprotocol/inspector python server.py
What FastMCP Actually Generates for You
It is worth being concrete about the boundary between what the decorator gives you for free and what remains your responsibility, since misunderstanding this boundary is a common source of early confusion. From the ping function above, FastMCP inspects the function signature (message: str) at import time and generates a JSON Schema ({"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}) that gets advertised verbatim in tools/list. When a tools/call request arrives with {"name": "ping", "arguments": {"message": "hi"}}, FastMCP looks up the registered function by name, validates the incoming arguments against the generated schema (rejecting the call with a structured JSON-RPC error before your function body ever executes if they do not match, the mechanism explored fully in Lesson 6), calls your async function with the validated arguments, and wraps whatever your function returns into the MCP response content format. What FastMCP does not do: it does not know anything about your business logic, your downstream systems, your authentication scheme, or how to handle a downstream timeout, those are all things you write, starting in Lesson 5 for the first two and Module 4 for the third. Keeping this boundary clear early prevents a common mistake of assuming the SDK provides safety guarantees (like authorization) that it does not.
TypeScript: SDK Setup
mkdir enterprise-mcp-server-ts && cd enterprise-mcp-server-ts
npm init -y && npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "enterprise-mcp-server", version: "0.1.0" });
server.registerTool(
"ping",
{
description: "Simplest possible tool: echoes the input back.",
inputSchema: { message: z.string() },
},
async ({ message }) => ({
content: [{ type: "text", text: `pong: ${message}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
The TypeScript SDK’s registerTool plays the identical role to FastMCP’s decorator, converting a Zod schema into the JSON Schema advertised over the wire, and routing incoming calls to your handler function, the two SDKs are deliberately symmetrical in what they automate so that the mental model you build in one transfers directly to the other.
Running and Verifying with the MCP Inspector
The Inspector is worth using from the very first tool you write, not just at the end of a build, since it catches schema and routing mistakes immediately rather than after they are buried under ten more tools.
npx @modelcontextprotocol/inspector python server.py
# Opens a local web UI: connect, call tools/list, manually invoke `ping`
# with a test message, and inspect the raw JSON-RPC request/response pairs.
Use the Inspector’s raw request/response view specifically to confirm the generated input schema matches what you intended, a common early mistake is an Optional[str] = None type hint generating a schema that technically allows null but the downstream code does not actually handle a None value gracefully, something obvious once you see the raw schema and try it, easy to miss otherwise.
Project Layout for This Course
The rest of this course builds out a single enterprise MCP server incrementally. A clean layout separates protocol wiring from business logic, this matters once you have dozens of tools across multiple internal systems, and it maps directly onto the ownership model from Module 5 and Module 10, where different teams own different integration modules within (or across) servers.
flowchart TD
Root["enterprise-mcp-server/"]
Root --> ServerPy["server.py\n(FastMCP instance, imports + registers\ntools from each module below)"]
Root --> Tools["tools/\ncrm.py, warehouse.py, ticketing.py\n(one module per wrapped system)"]
Root --> Schemas["schemas/\n(Pydantic input/output models,\nshared across tools/ modules)"]
Root --> Clients["clients/\n(typed wrappers around internal APIs, DBs)"]
Root --> Auth["auth/\n(middleware: API keys, OAuth)"]
Root --> Tests["tests/\n(contract tests per tool, Lesson 26)"]
style Root fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style ServerPy fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Tools fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Schemas fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Clients fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Auth fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Tests fill:#EEF0F7,stroke:#6366F1,color:#0F172A
# server.py: composition root, imports tool modules and registers them
from mcp.server.fastmcp import FastMCP
from tools import crm, warehouse, ticketing
mcp = FastMCP(name="enterprise-mcp-server", version="0.1.0")
crm.register(mcp) # tools/crm.py defines its own @mcp.tool()-style registration
warehouse.register(mcp)
ticketing.register(mcp)
# tools/crm.py: one team's tools, isolated from every other system's code
def register(mcp):
@mcp.tool(name="crm.search_accounts")
async def search_accounts(query: str) -> dict:
"""Owned by the CRM/RevOps team; see clients/crm_client.py for the API wrapper."""
from clients.crm_client import crm_client
return await crm_client.search(query)
With the scaffold running and verified in the Inspector, and a project layout that will not collapse under its own weight, the next lesson adds real tools, resources, and prompts backed by typed schemas rather than a placeholder ping.