If you've built an MCP server before, you know the drill. You follow a tutorial, get a working "hello world," and then spend the next two weeks bolting on authentication, input validation, error handling, logging, and security hardening before it's anywhere close to deployable.
This post walks through a TypeScript starter kit that handles all of that out of the box, so you can skip straight to writing your tool's actual logic.
What is MCP?
Model Context Protocol is an open standard that lets AI assistants (Claude, Cursor, Windsurf, etc.) call your code as tools. Think of it as a structured API layer between an LLM and your services. You define tools, the assistant discovers them, and calls them with validated inputs. Spec here.
The problem with most MCP tutorials
Every "Build Your First MCP Server" post gets you to a working stdio server in 10 minutes. Great. But then you need to actually ship it, and you're staring at a checklist like this:
- Input validation (not just "trust the LLM")
- Authentication (API keys? JWT?)
- Rate limiting
- SSRF protection (your server can make HTTP requests -- can it be tricked into hitting
169.254.169.254?) - Sandboxed file access (the AI asked to read a file -- should it read
/etc/passwd?) - Structured logging (not
console.log) - Error handling that doesn't crash the process
- Tests. Any tests at all.
Most of us end up copy-pasting from three different repos, Googling "node.js SSRF prevention," and hoping we didn't miss anything. I got tired of this, so I built a starter kit that solves the boring parts.
The starter kit
Repo: github.com/junna-legal/mcp-starter-kit (MIT license, free forever)
It's a TypeScript project with strict mode, ESM, and 250 tests (including 30+ security-focused cases). It supports both stdio and HTTP (SSE) transports, and works with Claude Code, Claude Desktop, Cursor, and Windsurf.
Quick start
git clone https://github.com/junna-legal/mcp-starter-kit.git my-mcp-server
cd my-mcp-server
npm install
cp .env.example .env
npm run db:seed # seeds a sample SQLite database
npm run build
npm run dev # starts with hot-reload
To verify everything works, open the MCP Inspector:
npm run inspector
This launches an interactive debugger in your browser where you can call every tool and see the responses. If you see results from database-query, you're good.
What's included
The kit ships with 6 reference tool implementations you can study, copy, or delete:
- database-query -- SQLite CRUD with parameterized queries (no injection)
- api-connector -- fetch from external REST APIs with SSRF protection
- file-manager -- sandboxed file operations with symlink escape detection
- cache-store -- TTL-based key-value cache with namespaces
- semantic-search -- local RAG with embeddings (3 tools + 2 resources + 1 prompt)
- webhook-notifier -- async webhook delivery with HMAC-SHA256 signatures
Beyond the reference tools, here's what the kit handles for you versus what you'd get from a typical tutorial:
| Starter Kit | Typical Tutorial | |
|---|---|---|
| Transport | HTTP (SSE) + Stdio | Stdio only |
| Validation | Zod schemas | Manual or none |
| Logging | Structured JSON (pino) | console.log |
| Error handling | Graceful + MCP error codes | Process crash |
| Auth | API Key + JWT strategies | None |
| Rate limiting | Token bucket | None |
| CI/CD | GitHub Actions | None |
Security, specifically
This is where most hand-rolled servers fall short. The kit includes:
- SSRF protection that blocks private/internal networks (IPv4 + IPv6), including cloud metadata endpoints. DNS resolution is validated to prevent rebinding attacks.
-
JWT validation that rejects
alg:noneand algorithm downgrade attacks. - Sandboxed file access that prevents path traversal and symlink escapes.
- SQL injection prevention via strict parameter binding.
- 30+ security test cases covering OWASP top threats.
You don't have to think about any of this. It's already tested and wired in.
Adding your own tool
The kit includes a scaffolding script. Say you want to build a send-email tool:
npm run create-tool send-email
This generates a directory with three files:
src/tools/send-email/
index.ts # registration
schema.ts # Zod input schema
handler.ts # your business logic
__tests__/
handler.test.ts # unit test scaffold
Define your input schema:
// src/tools/send-email/schema.ts
import { z } from "zod";
export const sendEmailShape = {
to: z.string().email("Must be a valid email address"),
subject: z.string().min(1).max(200).describe("Email subject line"),
body: z.string().min(1).describe("Plain-text email body"),
priority: z.enum(["low", "normal", "high"]).default("normal"),
};
Write your handler:
// src/tools/send-email/handler.ts
import { logger } from "../../lib/logger.js";
import { ToolError } from "../../lib/errors.js";
import type { SendEmailInput } from "./schema.js";
export async function handleSendEmail(input: SendEmailInput) {
logger.info({ to: input.to, subject: input.subject }, "Sending email");
if (!process.env.SMTP_HOST) {
throw new ToolError("SMTP_HOST environment variable is not configured");
}
// Your actual email logic here (nodemailer, SendGrid, Resend, etc.)
return { status: "sent", timestamp: new Date().toISOString() };
}
That's it. The framework handles validation, error serialization, and registration with the MCP server automatically. Throw ToolError for expected failures; unexpected exceptions are caught and returned as structured MCP error responses.
Run npm test to make sure nothing broke, and you're shipping.
Connecting to your client
For Claude Code, drop this in your project's .mcp.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"LOG_LEVEL": "info",
"DB_PATH": "./data/sample.db",
"SANDBOX_ROOT": "./data/sandbox"
}
}
}
}
Cursor, Windsurf, and Claude Desktop all work too -- see the setup docs for their config formats.
Wrapping up
The repo is MIT-licensed. Clone it, delete the reference tools you don't need, add your own, and ship. If you run into issues, the troubleshooting guide covers the common ones.
Repo: github.com/junna-legal/mcp-starter-kit
If you find it useful, a star on the repo would be appreciated. If you find a bug, open an issue -- or better yet, a PR.
Top comments (0)