MCP for Beginners: Build a Server and Client, Then Take It to Enterprise
MCP for beginners: the N x M problem, core ideas in plain terms, a TypeScript helpdesk server and client in Node.js, and the step up to secure HTTP deploys.
This article is published in English.
Sources: MCP Tools Inside Enterprise Applications: A Beginner-Friendly Deep Dive
Anyone who has worked with AI agents recently has run into MCP, the Model Context Protocol. It appears in Claude Desktop, in IDE plugins, in company-internal "AI portal" projects and, more and more, in production enterprise software.
This is a beginner-level walk through it, with no prior MCP experience assumed. It starts from the problem the protocol solves, covers the architecture, builds a real server and client in TypeScript on Node.js, and then looks at the security, governance and observability that enterprises wrap around all of it. The diagrams are plain-text ASCII, so they can be pasted straight into notes or code documentation.
1. The problem MCP addresses
Before MCP, getting an agent to do something real, such as reading a CRM record, opening an ITSM ticket or querying a data warehouse, meant exactly one thing: writing custom, one-off integration code for that agent and that system.
That's tolerable until there are several agents (a support bot, a coding assistant, an internal knowledge helper) and several systems (Salesforce, Jira, an HR database, a warehouse). The result is a combinatorial pile of hand-built connectors.
BEFORE MCP — the N x M integration problem
┌───────────┐ ┌─────────────┐
│ Agent A │───────▶│ CRM API │ (custom connector #1)
└───────────┘ └─────────────┘
┌───────────┐ ┌─────────────┐
│ Agent A │───────▶│ Ticketing │ (custom connector #2)
└───────────┘ └─────────────┘
┌───────────┐ ┌─────────────┐
│ Agent B │───────▶│ CRM API │ (custom connector #3 -
└───────────┘ └─────────────┘ yes, AGAIN, for a different agent)
┌───────────┐ ┌─────────────┐
│ Agent B │───────▶│ Data │ (custom connector #4)
└───────────┘ │ Warehouse │
└─────────────┘
N agents x M systems = N x M custom, non-reusable integrations.
Every new agent re-implements auth, retries, schemas, error handling.
USB solved the same shape of problem for hardware peripherals, and ODBC and JDBC did it for databases: too many custom connectors and no shared standard. MCP is Anthropic's answer, published as an open protocol and not a Claude-only feature, so any AI application and any backend can speak a common language.
AFTER MCP — one protocol, many servers, many clients
┌───────────┐ ┌───────────────────┐
│ Agent A │──┐ ┌─▶│ MCP Server: CRM │
└───────────┘ │ ┌───────────┐ │ └───────────────────┘
├───▶│ MCP │───┤ ┌───────────────────┐
┌───────────┐ │ │ (shared │ ├─▶│ MCP Server: Ticket │
│ Agent B │──┘ │ protocol)│ │ └───────────────────┘
└───────────┘ └───────────┘ │ ┌──────────────────┐
└─▶│ MCP Server: DW │
└──────────────────┘
Any MCP-compatible agent can now talk to any MCP server.
Build the connector once, reuse it everywhere.
The pitch fits in one sentence. MCP gives AI applications one common way to find tools, call them, read data and use prompt templates, so an integration gets written once and any compliant client can reuse it.
2. Core concepts in plain terms
Four terms get used loosely, so they're pinned down before any code.
- Host. The AI-powered application the end user actually touches, such as an internal chat assistant, an IDE plugin or a support copilot, and it's the part that contains the LLM.
- MCP client. A component inside the host that manages a single connection to a single MCP server. A host that talks to three backends usually runs three clients, one per server.
- MCP server. A lightweight program that exposes one system's capabilities (tools, data, prompts) following the MCP specification. As an enterprise developer, this is typically the piece you build.
- Transport. The channel client and server communicate through. Two dominate: stdio, in which the client starts the server as a local child process and exchanges messages on standard input and output (handy for local development and desktop apps), and Streamable HTTP, in which the server lives remotely behind HTTPS (the usual choice for shared, centrally run servers).
Underneath, all messages use JSON-RPC 2.0, a simple and well-established format for requests, responses and notifications. The SDK writes it for you, but knowing the wire format explains why MCP debugging tools resemble the API tools you already use, with a request ID, method name, params and a result or error.
What a server can offer: three primitives
- Tools are callable functions that let the model act or fetch computed results, and it is the model that chooses when to invoke them, depending on what the user asks. Examples:
create_support_ticket,get_employee_details. - Resources are read-only data the host can attach as context, such as files or records. The application or user decides what to attach. Examples: a knowledge-base article, a policy PDF, a log file.
- Prompts are reusable, parameterised instruction templates that the user picks, much like a slash command. Example: "Summarise this incident for a postmortem."
In most enterprise integrations, Tools carry about 90% of the interesting work, so they're built first.
3. The architecture, three layers together
┌─────────────────────────── HOST APPLICATION ───────────────────────────┐
│ e.g. an internal AI assistant, IDE plugin, support copilot │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ MCP Client 1 │ │ MCP Client 2 │ │ MCP Client 3 │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
└───────────┼────────────────────────┼───────────────────────┼───────────┘
│ JSON-RPC over │ JSON-RPC over │ JSON-RPC over
│ stdio / HTTPS │ stdio / HTTPS │ stdio / HTTPS
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ MCP Server │ │ MCP Server │ │ MCP Server │
│ wraps HR system │ │ wraps Ticketing │ │ wraps Data │
│ (tools: lookup, │ │ (tools: create, │ │ Warehouse │
│ update) │ │ status, close) │ │ (tools: query) │
└──────────────────┘ └──────────────────┘ └─────────────────┘
Each MCP server is meant to be small and focused, with a separate server for each backend system and no single giant one covering everything. The thinking is borrowed from microservices, with a small blast radius, independent deployment, independent scaling and ownership by whichever team owns that backend.
4. Building a first MCP server (Node.js and TypeScript)
The working example is an IT Helpdesk MCP server with two tools, one to create a support ticket and one to check a ticket's status. It mirrors a very common enterprise pattern, wrapping an internal ticketing or ITSM API.
4.1 Project setup
mkdir helpdesk-mcp-server && cd helpdesk-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init
@modelcontextprotocol/sdk is the official package. It takes care of JSON-RPC on the wire, the transports and the primitives, so raw JSON-RPC is never touched by hand. zod is a schema validation library. MCP tool inputs are checked against a schema before handler code ever runs, which matters a great deal in enterprise settings because malformed or malicious calls are stopped at the door.
4.2 The server code
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// --- A stand-in for a real internal ticketing API client ---
// In a real enterprise server this would call your ITSM system
// (ServiceNow, Jira Service Management, Zendesk, an internal API, etc.)
const ticketStore = new Map<string, { status: string; subject: string }>();
let nextId = 1000;
// 1. Create the server instance.
// "name" and "version" identify this server to any client that connects.
const server = new McpServer({
name: "helpdesk-mcp-server",
version: "1.0.0",
});
// 2. Register a tool: create_support_ticket
server.registerTool(
"create_support_ticket",
{
title: "Create Support Ticket",
description:
"Creates a new IT helpdesk ticket for the requesting employee.",
inputSchema: {
subject: z.string().describe("Short summary of the issue"),
priority: z.enum(["low", "medium", "high", "urgent"]),
employeeId: z.string().describe("Requesting employee's ID"),
},
outputSchema: {
ticketId: z.string(),
status: z.string(),
},
},
async ({ subject, priority, employeeId }) => {
const ticketId = `TCK-${nextId++}`;
ticketStore.set(ticketId, { status: "open", subject });
const output = { ticketId, status: "open" };
// MCP tool results return a "content" array (what a human/LLM reads)
// and, optionally, "structuredContent" (typed data other code can use).
return {
content: [
{
type: "text",
text: `Created ticket ${ticketId} (priority: ${priority}) for employee ${employeeId}.`,
},
],
structuredContent: output,
};
}
);
// 3. Register a second tool: get_ticket_status
server.registerTool(
"get_ticket_status",
{
title: "Get Ticket Status",
description: "Looks up the current status of an existing support ticket.",
inputSchema: {
ticketId: z.string(),
},
outputSchema: {
status: z.string(),
},
},
async ({ ticketId }) => {
const ticket = ticketStore.get(ticketId);
if (!ticket) {
// Returning isError lets the model know the call failed
// WITHOUT crashing the whole conversation.
return {
content: [{ type: "text", text: `No ticket found with ID ${ticketId}.` }],
isError: true,
};
}
return {
content: [{ type: "text", text: `Ticket ${ticketId} is currently "${ticket.status}".` }],
structuredContent: { status: ticket.status },
};
}
);
// 4. Wire the server to a transport and start listening.
// stdio is perfect for local development and desktop-hosted tools.
const transport = new StdioServerTransport();
await server.connect(transport);
4.3 What each part does
- The
McpServerinstance. This object is the heart of the server. It tracks every tool, resource and prompt registered, and it handles the MCP handshake (capability negotiation) when a client connects. No list of tools is ever written by hand, because the SDK builds it from theregisterToolcalls. inputSchemawith Zod. More than documentation. The SDK uses it to generate a JSON Schema that tells the client and LLM which arguments are valid, and to check every call as it arrives. A client that passespriority: "banana"gets turned away before the handler is reached. For an enterprise, this schema-first validation is a real security control and not a nicety.outputSchemaandstructuredContent. A tool can return a human-readable summary (content) and also a machine-readable object (structuredContent). The text shows up in a chat transcript, while a calling application can parse the structured data to drive a UI or further logic.isError: true. MCP has a first-class notion of a tool-level error, distinct from a protocol-level one. It means the operation failed but the conversation should carry on, so the model can read the error and decide whether to retry, ask the user or try another tool. You don't want a whole agent session to crash because one ticket ID didn't exist.- The transport (
StdioServerTransport). The piece that moves bytes. For local tools such as a developer's IDE or a desktop assistant, stdio is simplest, with the host spawning the server as a child process. Anything running centrally in an enterprise would use the HTTP-based transport, covered in Section 6.
4.4 Running it
npx tsx src/server.ts
On its own the server shows nothing, since it's waiting for a client to connect over stdin and stdout. The client comes next.
5. A client inside an enterprise application
Beginners often build a server and then wonder what actually talks to it. Here's a minimal client, imagined as part of an internal Node.js backend that powers a company's AI assistant.
// src/client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function main() {
// 1. Describe how to launch the server. Here we spawn it as a
// local subprocess - in production you'd more commonly point
// this at a remote HTTP-based server instead (see Section 6).
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", "src/server.ts"],
});
// 2. Create a client and connect. This performs the MCP
// handshake and capability negotiation automatically.
const client = new Client({ name: "internal-ai-assistant", version: "1.0.0" });
await client.connect(transport);
// 3. Discover what tools this server offers - this is the same
// mechanism an LLM uses to "learn" what it can do.
const { tools } = await client.listTools();
console.log("Available tools:", tools.map((t) => t.name));
// 4. Call a tool, just like the LLM would.
const result = await client.callTool({
name: "create_support_ticket",
arguments: {
subject: "VPN keeps disconnecting",
priority: "high",
employeeId: "E-4821",
},
});
console.log(result.content);
await client.close();
}
main();
Why this matters conceptually
The central idea is discovery. The client calls listTools() and receives a machine-readable catalogue of everything the server offers, each tool with its schema. That is what makes MCP dynamic: an LLM-powered host doesn't need tools baked into its prompt. At connection time it asks the server what it can do, and the answer becomes part of what the model is told it may do in that conversation.
Enterprise governance sits on exactly this mechanism. If a tool shouldn't be available to a particular user or environment, the cleanest control is to leave it out of what listTools() returns, and not to hope the model chooses not to call it.
6. Moving from a local prototype to a company deployment
The stdio setup suits learning and local developer tools, but it doesn't match how most enterprises run infrastructure: hosted centrally, behind authentication, logged, and reachable by many host applications at once. That's where the Streamable HTTP transport and an API gateway come in.
Here is the same helpdesk server adapted to run over HTTP and sit behind a plain Express app:
// src/httpServer.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
// In a real enterprise deployment, authentication middleware would
// run BEFORE this point - verifying a bearer token, checking scopes,
// and attaching the caller's identity to the request.
const server = buildHelpdeskServer(); // same registerTool calls as before
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode: simplest to scale horizontally
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log("MCP server listening on :3000"));
The significant change is less about code than about where responsibility lives. Three things that were implicit with stdio become explicit enterprise concerns over HTTP.
- Authentication goes in front of the server, typically in middleware or a gateway, which lets the server assume
reqalready holds a verified identity. - Statelessness is a conscious decision. With
sessionIdGenerator: undefinedthe transport keeps no per-connection session, so whichever instance sits behind the load balancer can answer any request, and you can scale out like any stateless API. - Observability stops being optional. Every tool invocation is a genuine network request, so it should be logged, traced and rate-limited the way any internal API call is.
7. Enterprise considerations checklist
Getting an MCP server to run is the simple fifth of the job. The remaining four fifths is making it safe against production systems that hold real employee or customer data, and that part looks like this.
- Authentication and authorisation. Use OAuth2 or OIDC at the gateway. Map the calling user's or service account's identity to specific scoped permissions, so that not every tool is callable by every caller.
- Least-privilege tool design. Prefer narrow tools such as
get_ticket_statusover broad ones such asrun_sql_query. A tool that does one well-defined thing is far easier to reason about and audit. - Input validation. Treat schema validation, as with Zod above, as a genuine security boundary and not just type-checking convenience.
- A human in the loop for destructive actions. For anything irreversible, such as deleting a record, sending external email or approving a payment, ask for explicit user confirmation before the tool executes.
- Audit logging. Record who called each tool, with which arguments and with what result. That's what lets an AI-driven action be explained afterwards, a hard requirement in regulated areas like healthcare and finance.
- Rate limits and timeouts. Apply the same controls as for other internal API traffic, so that an agent stuck in a loop can't flood a backend.
- Idempotency. Make tools with side effects safe to call twice with the same input, since agents sometimes retry.
- Versioning. As tool schemas change, version them deliberately so older hosts don't silently break.
8. Where this appears in real enterprises
- IT and internal support. The helpdesk example: create and query tickets, reset passwords, check system status.
- Sales and CRM copilots. Pull account history and draft follow-up emails grounded in real CRM data and not the model's guesses.
- Data and analytics assistants. A read-only, pre-approved set of query tools over a warehouse, in place of unrestricted SQL.
- Healthcare workflows. Tools that look up appointment slots, check insurance eligibility or flag care-coordination gaps, always behind strict role-based access and audit trails because of how sensitive the data is. Here the narrow-tool, human-in-the-loop, full-audit-log principles of Section 7 are often a compliance requirement.
- Developer tooling. IDE assistants that query internal documentation, start a CI pipeline or check who is on call for a service.
What these have in common: MCP doesn't make a model any smarter. What changes is that the AI gets grounded, auditable access to real systems and no longer has to invent plausible-sounding answers.
9. Pitfalls beginners hit
- One giant "do everything" server. Split by backend system or domain. That mirrors good microservice boundaries and keeps the blast radius small.
- Skipping
descriptionfields. The model leans heavily on tool and parameter descriptions to decide when and how to call something. Vague descriptions lead to the wrong tool, or the right tool with the wrong arguments. - Forgetting
isError. A raw exception thrown out of a tool handler may take down the entire client connection, where the model should instead be told that this failed and to try something else. - Shipping stdio to production. Stdio is fine for local development and desktop hosts. Multi-tenant production needs the HTTP transport behind a real gateway.
- No audit trail. Unless you can say who asked the AI to act and what it did, keep it away from production data.
10. The short version
Once the buzzwords are stripped away, MCP's core idea is refreshingly plain: one shared method for AI applications to find and invoke tools, pull in data and reuse prompt templates, which means integration work done a single time serves every compliant client. For enterprises, that standardisation is what moves AI agents from flashy demos to something that can be governed, secured and run in production next to everything else.
For anyone starting out, a sensible path is:
- Build one small MCP server locally over stdio, like the helpdesk example.
- Add a client that calls those tools, so the whole round trip is visible.
- Put the server behind HTTP with authentication or a gateway in front.
- Only then turn to audit logging, rate limits and human approval for anything destructive.
Begin small and local, and bring in the enterprise concerns one at a time. Trying to design the perfect production server on day one is a much shakier way to get there.