Building an MCP server is mostly about capability design and operational discipline, not just SDK syntax. A good server has a narrow purpose, explicit contracts, strong validation, clear logging, and a predictable deployment shape.
This page walks through an end-to-end implementation using the official TypeScript SDK because it is the most common starting point and tracks the current spec closely.
A first production-worthy server should be small, read-only where possible, and easy to inspect. Your goal is not to expose everything. Your goal is to expose one domain well.
Keep protocol wiring, backend adapters, and validation logic in separate files so the server stays reviewable as it grows.
mcp-policy-server/
package.json
tsconfig.json
src/
index.ts
policies.ts
auth.ts
logging.ts
test/
policies.test.ts
protocol.test.ts
Start by choosing one domain, not one technology. "Company policy server" is a useful boundary. "A random set of internal endpoints" is not. The domain tells you what tools, resources, and prompts make sense together.
For a first server, prefer read-only capabilities. They are easier to secure, easier to test with Inspector, and easier to reason about when the model makes mistakes.
Use the SDK to create a server instance, then register capabilities deliberately. Add descriptions that help a host and model understand what each capability is for, and design resource URIs before you implement loaders.
Keep your server bootstrap boring. Most production value will come from the handlers and backend modules, not from fancy startup logic.
Every tool and resource read should validate inputs and then call backend logic through small adapter functions. That keeps protocol concerns separated from business logic and makes testing easier.
If a backend call needs policy, approval, or identity-aware filtering, encode it there instead of letting the model decide what looks safe.
Do not stop at unit tests. An MCP server is an integration surface, so you need startup tests, capability listing tests, valid call tests, invalid call tests, and auth-denied tests.
The MCP Inspector should become part of your development loop. If Inspector cannot explain your server, most hosts will struggle too.
An MCP server should be designed from its capability contracts outward. Start by writing the tools, resources, and prompts the host should discover, then implement the backend adapters behind them. This prevents the server from becoming a thin wrapper around internal APIs that were never designed for model use.
For each capability, document the user intent, inputs, output shape, authorization requirement, failure modes, and observability fields. If a tool can write data, also document idempotency, approval expectations, and rollback behavior. If a resource can expose sensitive data, document discovery filtering and read authorization separately.
Keep the server startup path clean. For stdio servers, stdout must be reserved for protocol messages; logs should go to stderr or a file. For HTTP servers, initialize authentication, routing, request limits, and structured logging before exposing capability handlers. Many confusing MCP failures are actually process, framing, or environment problems.
Treat schemas as part of the public API. Strong descriptions, bounded enums, required fields, and structured outputs help both the model and the host. Loose schemas shift ambiguity into runtime behavior, which is harder to debug and easier to misuse.
Server tests should cover more than happy-path handler functions. Test initialize negotiation, capability listing, valid calls, invalid schemas, unauthorized calls, backend timeouts, oversized results, and host-visible error messages. A server that passes unit tests but fails capability discovery is still broken from the host perspective.
Create fixture requests that mirror the real JSON-RPC messages a host sends. This catches serialization, naming, and response-shape problems early. For tools, test both the model-facing schema and the backend-facing adapter. For resources, test pagination, not-found behavior, and permission filtering. For prompts, test argument validation and generated message structure.
Use the MCP Inspector or an equivalent protocol-level client before trying a full AI host. The Inspector narrows the problem: can the server start, negotiate, list capabilities, execute a minimal call, and return a valid result? Once that works, host integration issues become much easier to diagnose.
Finally, test operational behavior. Simulate cancelled requests, duplicate writes, expired tokens, missing environment variables, and process restarts. Production MCP servers fail in ordinary infrastructure ways, and the tutorial should prepare readers for those realities.
Before calling a server finished, review it capability by capability. For each tool, resource, and prompt, write the user intent, allowed caller, input schema, output shape, authorization rule, failure classes, and audit fields. This turns the server from a demo into a maintainable protocol component.
Then run the same server through three clients: a protocol inspector, a real host, and an automated test fixture. Each client catches different problems. The inspector catches protocol shape, the host catches integration and UX issues, and tests catch regressions.
Finally, simulate bad inputs. Send missing fields, extra fields, unauthorized targets, oversized requests, backend timeouts, and malformed resource identifiers. A server that fails safely is much closer to production than one that only handles perfect calls.
This is a compact but executable TypeScript server using the official SDK and stdio transport.
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: 'policy-hub', version: '1.0.0' });
server.registerTool(
'search_policies',
{
title: 'Search Policies',
description: 'Search approved policy documents by keyword.',
inputSchema: {
query: z.string().min(3),
},
},
async ({ query }) => {
const results = [
{
uri: 'policy://security/password-rotation',
title: 'Password Rotation Policy',
snippet: `Matched on query: ${query}`,
},
];
return {
content: [
{ type: 'text', text: `Found ${results.length} matching policy document.` },
],
structuredContent: { results },
};
}
);
server.registerResource(
'password-policy',
'policy://security/password-rotation',
{
title: 'Password Rotation Policy',
mimeType: 'text/markdown',
},
async (uri) => ({
contents: [
{
uri: uri.href,
text: '# Password Rotation\n\nRotate privileged credentials every 90 days.',
},
],
})
);
server.registerPrompt(
'summarize_policy',
{
title: 'Summarize Policy',
description: 'Summarize a policy for a specific audience.',
argsSchema: {
audience: z.string(),
policyText: z.string(),
},
},
({ audience, policyText }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Summarize this policy for ${audience}:\n\n${policyText}`,
},
},
],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init
npx tsx src/index.ts
TypeScript is a strong starting point because the official SDK is mature and well documented, but choose the language that fits your runtime and team.
Only if you actually need remote deployment. A local stdio server is often the better learning and prototyping path.
Explore 500+ free tutorials across 20+ languages and frameworks.