Build a Production MCP Server in TypeScript for Next.js & Node.js (2026 Guide)
Step-by-step guide to building a production-ready Model Context Protocol (MCP) server with the official TypeScript SDK. Cover tools, resources, stdio and Streamable HTTP transports, security, and how to wire your Next.js or Node.js backend so AI agents can safely call your APIs and data.
By Mussawar Hayat
Why MCP Servers Matter for Full-Stack Teams in 2026
The Model Context Protocol (MCP) has become the standard way AI agents and coding tools connect to external data and actions. Instead of writing one-off integrations for Claude, Cursor, Codex, or your own agent runtime, you expose tools and resources once through an MCP server. Any compliant client can discover and call them.
For Next.js and Node.js teams this is practical, not theoretical. You already own Postgres, Prisma models, Stripe webhooks, and internal APIs. Packaging those capabilities as MCP tools lets coding agents and internal chatbots operate against real project context instead of guessing from a prompt.
What You Will Learn
- What MCP is and how hosts, clients, and servers interact
- How to scaffold a TypeScript MCP server with the official v2 SDK
- Registering tools and resources with Zod schemas
- Stdio vs Streamable HTTP transports and when to use each
- Production patterns for Next.js / Node backends, auth, and least privilege
- Security, performance, common mistakes, and a real use-case walkthrough
1. MCP Architecture in One Page
MCP is an open protocol (JSON-RPC 2.0) that standardises how LLM applications obtain context and invoke capabilities. The three roles are:
- Host — the AI application (Claude Desktop, Claude Code, Cursor, your own agent UI)
- Client — the connector inside the host that speaks MCP to one or more servers
- Server — the process you build that exposes tools, resources, and prompts
Servers offer three main feature types:
- Tools — model-controlled actions (query DB, create issue, run a report)
- Resources — read-only context the model or user can load (file contents, schema docs, config)
- Prompts — reusable prompt templates with arguments
Transports move messages between client and server. The two you will use most:
- stdio — process stdin/stdout; ideal for local CLI and desktop agents
- Streamable HTTP — HTTP endpoint for remote or multi-user servers
Official docs and the 2026-07-28 specification live at modelcontextprotocol.io. The TypeScript SDK is maintained at modelcontextprotocol/typescript-sdk.
2. Project Setup (TypeScript + Official SDK v2)
Use Node.js 20+ and ES modules. The v2 packages are split: @modelcontextprotocol/server for servers and @modelcontextprotocol/client for clients.
mkdir project-mcp-server && cd project-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install -D typescript tsx @types/node
# Optional: Express/Hono adapters for HTTP later
# npm install @modelcontextprotocol/express expressMinimal tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src/**/*"]
}Create src/index.ts with a single tool over stdio:
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio.js';
import * as z from 'zod/v4';
const server = new McpServer({
name: 'project-tools',
version: '1.0.0',
});
server.registerTool(
'greet',
{
description: 'Greet a user by name',
inputSchema: z.object({ name: z.string().min(1) }),
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }],
})
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});Run it:
npx tsx src/index.tsImportant: log only with console.error. Anything written to stdout becomes part of the JSON-RPC stream and will break the protocol.
3. Production Tools for a Next.js / Prisma Stack
A useful MCP server exposes the same operations your app already performs — through a controlled, schema-validated interface. Below is a realistic pattern: list projects for an authenticated user and create a project, both backed by a server-only data-access layer.
3.1 Shared Prisma client and DAL
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// src/data/projects.ts
import 'server-only'; // if this module is ever imported from a Next.js boundary
import { prisma } from '../lib/prisma.js';
export async function listProjectsForUser(userId: string) {
return prisma.project.findMany({
where: { ownerId: userId },
select: { id: true, name: true, updatedAt: true },
orderBy: { updatedAt: 'desc' },
take: 50,
});
}
export async function createProjectForUser(userId: string, name: string) {
return prisma.project.create({
data: { name, ownerId: userId },
select: { id: true, name: true, createdAt: true },
});
}3.2 Register tools with ownership checks
import { listProjectsForUser, createProjectForUser } from './data/projects.js';
function requireUserId(userId: string | undefined): string {
if (!userId || typeof userId !== 'string') {
throw new Error('Authenticated user id is required');
}
return userId;
}
server.registerTool(
'list_projects',
{
description: 'List projects owned by the current user. Returns id, name, updatedAt.',
inputSchema: z.object({
userId: z.string().uuid().describe('Authenticated user id from the host session'),
}),
},
async ({ userId }) => {
const id = requireUserId(userId);
const projects = await listProjectsForUser(id);
return {
content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }],
};
}
);
server.registerTool(
'create_project',
{
description: 'Create a new project for the current user.',
inputSchema: z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(120),
}),
},
async ({ userId, name }) => {
const id = requireUserId(userId);
const project = await createProjectForUser(id, name);
return {
content: [{ type: 'text', text: JSON.stringify(project, null, 2) }],
};
}
);Never accept a raw connection string or admin token from the model. Pass only the authenticated user id that the host already verified, and scope every query to that id.
3.3 Resources for schema and docs
server.registerResource(
'schema://projects',
{
description: 'JSON Schema description of the Project model the tools operate on',
mimeType: 'application/json',
},
async () => ({
contents: [{
uri: 'schema://projects',
mimeType: 'application/json',
text: JSON.stringify({
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
ownerId: { type: 'string', format: 'uuid' },
updatedAt: { type: 'string', format: 'date-time' },
},
required: ['id', 'name', 'ownerId'],
}, null, 2),
}],
})
);4. Transports: Stdio for Local Agents, HTTP for Shared Servers
4.1 Stdio (local Claude Code / Cursor)
Stdio is the default for desktop and CLI hosts. Point the host at your entry file:
// Example Claude Desktop / Claude Code MCP config snippet
{
"mcpServers": {
"project-tools": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/project-mcp-server/src/index.ts"],
"env": {
"DATABASE_URL": "postgresql://..."
}
}
}
}4.2 Streamable HTTP (remote or multi-user)
For a service that multiple users or agents can call over the network, use Streamable HTTP. The official middleware packages (@modelcontextprotocol/express, @modelcontextprotocol/hono, @modelcontextprotocol/node) wrap the transport so you can mount it on an existing Node server.
Sketch with Express:
import express from 'express';
import { McpServer } from '@modelcontextprotocol/server';
// Use the official Express helper package in real code; pattern is:
// create the McpServer, register tools, then mount the Streamable HTTP handler
// on a path such as /mcp with proper Host header validation and auth middleware.
const app = express();
// app.use('/mcp', authMiddleware, mcpHttpHandler);
app.listen(process.env.PORT ?? 3100);Always put authentication in front of the HTTP transport. The host must present a token or session that maps to a real user id before any tool runs.
5. Security Considerations
- Least privilege — each tool should do one thing and only for the authenticated principal. Never expose admin or cross-tenant operations without an explicit, separate gate.
- Validate every argument — Zod (or another Standard Schema library) is mandatory. Reject unexpected keys and out-of-range values.
- No secrets in tool results — strip connection strings, API keys, and PII before returning content to the model.
- Destructive tools — mark or gate delete/update operations. Prefer human-in-the-loop approval for irreversible actions when the host supports it.
- Transport isolation — stdio servers inherit the process environment; HTTP servers must not trust the network. Use TLS, short-lived tokens, and Host header validation.
- Audit logging — log tool name, user id, argument hashes, latency, and success/failure. Do not log full argument payloads if they can contain sensitive data.
- Dependency hygiene — pin the SDK version, review changes on upgrade, and treat third-party MCP servers the same way you treat third-party npm packages.
6. Performance and Operational Notes
- Keep tool handlers fast. Agents often call several tools in sequence; a 2-second DB query becomes painful under multi-step workflows.
- Return compact JSON. Large dumps waste tokens and degrade model focus. Prefer summaries plus an optional resource URI for full detail.
- Reuse connection pools (Prisma, Redis). A new client per tool call will exhaust connections under concurrent agent sessions.
- For HTTP servers, prefer horizontal scale with sticky sessions only if the transport requires them; otherwise keep the server stateless where the protocol allows.
- Monitor error rates and p95 latency per tool. Spikes often mean schema drift or a missing index, not an MCP problem.
7. Real Use Cases
- Internal coding agent — expose read-only tools that list open issues, recent deploys, and failing CI jobs so Claude Code or Cursor can reason about the real project state.
- Support triage bot — tools that look up a customer by id, list recent tickets, and draft a reply draft that a human still sends.
- Ops runbook agent — resources that load runbook markdown and tools that trigger approved, non-destructive health checks.
- SaaS product feature — customers connect their own MCP clients to your remote server so their agents can query only their tenant’s data.
8. Common Mistakes
- Writing to stdout with
console.logand breaking the stdio protocol. - Passing the entire request body or raw DB row back to the model without filtering.
- Trusting a userId supplied by the model without verifying it against the host session.
- Registering one mega-tool that does everything instead of small, well-described tools.
- Running an HTTP MCP endpoint without authentication or Host validation.
- Hard-coding production database credentials in the MCP process environment used for local agent experiments.
- Ignoring the official specification and SDK examples when the API surface changes between major versions.
9. FAQ
Is MCP only for Anthropic products?
No. MCP is an open standard. Claude, Cursor, VS Code, ChatGPT (where supported), and many agent frameworks implement clients. Build the server once and reuse it.
Should I use the v1 or v2 TypeScript SDK?
Prefer v2 (@modelcontextprotocol/server / @modelcontextprotocol/client) which targets the 2026-07-28 specification. v1 remains available for legacy projects with a documented migration path.
Can I mount an MCP server inside a Next.js app?
Yes for HTTP transports: run a dedicated Node process or Route Handler that speaks Streamable HTTP, and keep Prisma and secrets server-side only. Stdio is better as a separate process the host launches.
How do I test tools without a full AI host?
Use the SDK client packages or the official example clients to list tools and call them with fixed arguments. Treat tools like any other API: unit-test the handler logic and integration-test the wire format.
What about authorization for multi-tenant SaaS?
Authenticate at the transport layer, map the token to a tenant and user, and pass only those identifiers into tools. Every data access must filter by tenant (and user where required). Never let the model choose a tenant id.
10. Summary
MCP turns your existing TypeScript backend into a first-class context source for AI agents. Start with the official v2 server package, register small Zod-validated tools that call a proper data-access layer, choose stdio for local agents and Streamable HTTP for remote ones, and enforce authentication and least privilege from day one.
Key Takeaway
Treat an MCP server like a public API: strict schemas, scoped credentials, audit logs, and no raw database or secret exposure. The protocol is the easy part; production safety is the real work.
Need help wiring MCP or AI agents into a production Next.js stack?
I help teams design secure Server Actions, Prisma data layers, and agent-friendly backends in TypeScript. Get in touch or explore full-stack and AI development services.
Related reading: Agent Skills for TypeScript & Next.js and OpenAI Agents SDK Multi-Agent Workflows.
Related guides
How to run Octomind AI agents against a Next.js 16 app in production. Covers agent-generated Playwright tests, TypeScript config, CI integration on preview deploys, auth handling, flake control, and when autonomous e2e agents beat hand-written suites.
Claude Code Mods in TypeScript: Production Guardrails for Next.js Teams (2026)Anthropic shipped Claude Code mods on October 1, 2026. Build a TypeScript plugin that blocks force-pushes, redacts secrets from tool output, and asks before destructive shell commands, without replacing human review.
Production Evals for Coding Agents in TypeScript and Next.js (2026)Code generation is cheap. Knowing the agent is right is not. This guide shows how to score TypeScript and Next.js coding agents with fixture tasks, deterministic checks, LLM judges used only where needed, merge gates, and a CI harness you can run without a human watching every diff.
