In this guide, you will build a production-ready Model Context Protocol (MCP) server from scratch using TypeScript and Node.js. You will expose custom database tools and secure resources to local AI agents, configure Cursor and VS Code to consume them, and optimize contextual token efficiency for your everyday workflows.
- The core architecture of the Model Context Protocol (MCP) specification: Tools, Resources, and Prompts
- How to implement a typed MCP server with the official TypeScript SDK and Zod schema validation
- Methods to safely bridge local PostgreSQL and SQLite databases directly to Cursor and VS Code agents
- Context optimization techniques that prevent context window exhaustion and protect developer secrets
Introduction
Your AI coding agent is hallucinating code because it is blind to your actual runtime infrastructure. Pasting schema definitions, tailing terminal logs manually, and exporting JSON payloads into chat windows is a tedious, error-prone chore. This hands-on build custom mcp server tutorial shows you how to eliminate manual context-dumping entirely by giving your editor native access to your development state.
In October 2026, the Model Context Protocol (MCP) has solidified as the standard protocol for giving local AI coding agents secure, real-time access to developer databases, logs, and internal APIs. Instead of building brittle, custom IDE plugins for every new language model, MCP offers a unified client-host-server specification. Claude, Cursor, and VS Code now treat MCP as the foundational pipeline for tool use and dynamic prompt context.
We are going to build an end-to-end server that connects your active database schema, runtime query logs, and safe query runners directly into your editor's agent loop. By the time you finish this guide, your IDE will execute contextual diagnostics against live data using real-world safeguards.
Why Context Protocol Beats Naive System Prompts
Traditional agent context relies on static files like .cursorrules or monolithic system prompts. While helpful, these techniques fail the moment your database schema drifts, an undocumented migration executes, or your team provisions a dynamic staging replica. Static documentation rots quickly, and feeding raw text snapshots blows out your token budget.
MCP replaces static prompt injection with dynamic client-server orchestration. Think of MCP as the Language Server Protocol (LSP), but designed for large language models instead of compilers. Where LSP gives an IDE auto-complete and jump-to-definition capabilities, MCP gives an LLM typed tools, pullable resources, and dynamic system templates over standard I/O pipes.
stdio (standard input/output) or server-sent events (SSE). This keeps latency negligible and prevents sensitive credentials from leaking to external cloud bridges.Engineering teams at high-velocity organizations use this paradigm to streamline their debugging workflows. When an agent can inspect table structures, analyze live query performance, and read error traces on demand, developers spend less time context-switching. You stop writing repetitive glue queries and focus directly on core product logic.
Deconstructing the Model Context Protocol Architecture
To build an effective server, you must master the three core primitives defined by the MCP specification: Resources, Tools, and Prompts. Each primitive serves a dedicated role in how the host orchestrates context collection.
Resources are passive, read-only data streams that mirror file paths or URIs (like db://schema/users). The agent reads them to ingest background knowledge without triggering external mutations. They function identically to targeted documentation lookups.
Tools are callable functions that allow the model to take action. When you expose an analytical query tool, the agent drafts parameters against a JSON schema, waits for the client runtime to invoke it, and reads the raw result. Tools modify state or execute computational logic.
Prompts are parameterized, reusable workflows exposed by the server. They provide predefined slash-commands or instructional patterns that guide the agent through multi-step operations like migration generation or anomaly diagnosis.
Now that the core protocol concepts are clear, let's explore the key technical capabilities we will bake into our custom implementation.
Key Features and Concepts
Typed Schema Validation via Zod
Model Context Protocol relies on deterministic JSON-RPC contracts. In an mcp server typescript implementation, we enforce argument types using Zod to ensure the model cannot run malformed queries against our infrastructure.
Safe Read-Only Database Introspection
Exposing database access directly to an autonomous LLM requires strict guardrails. We isolate schema queries from write operations, using a safe SQL runner to connect local database to cursor ai without exposing destructive mutations.
Context Token Window Optimization
Returning an entire ten-thousand-row table dump degrades response quality and exhausts memory. We truncate and aggregate result sets directly inside the server logic to optimize ai coding agent context before payloads reach the editor.
Implementation Guide
We will construct an operational MCP server that extracts live database schemas and executes guarded queries against a local database. Our project uses TypeScript, Node.js (v22+), and the official @modelcontextprotocol/sdk package.
# Initialize the project workspace
mkdir dev-context-mcp && cd dev-context-mcp
npm init -y
# Install the official MCP SDK, Zod, and SQLite driver
npm install @modelcontextprotocol/sdk zod better-sqlite3
# Install dev dependencies for TypeScript execution
npm install -D typescript @types/node @types/better-sqlite3 tsx
# Generate tsconfig.json
npx tsc --init
This command sequence scaffolds our directory, installs the standardized protocol libraries, and sets up a modern TypeScript environment. The better-sqlite3 driver serves as our local database engine for this guide, but the identical architecture applies to PostgreSQL or MySQL connection pools.
Configure your tsconfig.json file to emit modern ES modules with NodeNext resolution so the MCP SDK handles imports cleanly.
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2024"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
We configure ES2024 and NodeNext to guarantee full compatibility with the official SDK's asynchronous streaming primitives. Using strict compilation prevents subtle undefined schema arguments when mapping parameters from the model's generated tool calls.
Now, let's create a dummy database utility with some seed data so our server has meaningful application data to inspect. Create a file named src/db.ts:
// src/db.ts
import Database from "better-sqlite3";
export const db = new Database("app_dev.db");
// Seed demo tables and data for our agent to query
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
tier TEXT CHECK(tier IN ('free', 'pro', 'enterprise')) DEFAULT 'free',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
status_code INTEGER NOT NULL,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`);
// Insert initial demo records if table is empty
const checkCount = db.prepare("SELECT count(*) as count FROM users").get() as { count: number };
if (checkCount.count === 0) {
db.exec(`
INSERT INTO users (email, tier) VALUES
('sarah@acme.corp', 'enterprise'),
('alex@startup.io', 'pro'),
('dave@hobby.dev', 'free');
INSERT INTO audit_logs (user_id, action, status_code) VALUES
(1, 'workspace.export', 200),
(1, 'api_token.generate', 201),
(2, 'billing.upgrade', 402),
(3, 'project.delete', 403);
`);
}
This module sets up an SQLite database containing real-world relational structures: a users directory and an audit_logs stream. We include edge cases like foreign keys and mixed HTTP response statuses to give our AI agent practical debugging hooks.
Now, let's build the primary MCP server file. Create src/index.ts and initialize the protocol instance using standard I/O transports.
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { db } from "./db.js";
// Initialize server metadata
const server = new Server(
{
name: "dev-context-engine",
version: "1.0.0"
},
{
capabilities: {
resources: {},
tools: {}
}
}
);
// Register passive database schema resource
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "db://schema/tables",
name: "Application Database Schema",
mimeType: "application/json",
description: "Live schema definition of SQLite tables and column structures."
}
]
};
});
// Resolve passive schema resource content
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "db://schema/tables") {
const tables = db.prepare(
"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
).all();
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(tables, null, 2)
}
]
};
}
throw new Error(`Resource not found: ${request.params.uri}`);
});
Here, we declare a compliant server instance that runs on local stdio channels and advertises its capabilities to the client host. We register a dedicated resource handler under db://schema/tables that fetches current table definitions straight from the database engine, giving the agent live context whenever this URI is queried.
Next, append the tools layer to the bottom of src/index.ts. This exposes interactive query tools that the agent can execute during active conversations.
// Define our tool validation schemas
const RunReadOnlyQueryArgs = z.object({
query: z.string().describe("A safe SELECT SQL query to run against the database")
});
// Expose available tools to the LLM client
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "run_read_query",
description: "Executes a SELECT query on local database. Mutations (DROP, UPDATE, INSERT) are strictly forbidden.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The SQL SELECT statement."
}
},
required: ["query"]
}
}
]
};
});
// Implement runtime tool execution with safety boundaries
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "run_read_query") {
const parseResult = RunReadOnlyQueryArgs.safeParse(request.params.arguments);
if (!parseResult.success) {
return {
content: [{ type: "text", text: `Validation Error: ${parseResult.error.message}` }],
isError: true
};
}
const { query } = parseResult.data;
const cleanQuery = query.trim();
// Guard against destructive mutations
if (!cleanQuery.toUpperCase().startsWith("SELECT")) {
return {
content: [{ type: "text", text: "Security violation: Only SELECT queries are permitted." }],
isError: true
};
}
try {
const rows = db.prepare(cleanQuery).all();
// Cap results to prevent token window overflow
const limited = rows.slice(0, 25);
return {
content: [
{
type: "text",
text: JSON.stringify({ returned: limited.length, total: rows.length, data: limited }, null, 2)
}
]
};
} catch (err: any) {
return {
content: [{ type: "text", text: `SQL Execution Error: ${err.message}` }],
isError: true
};
}
}
throw new Error(`Tool not found: ${request.params.name}`);
});
// Boot transport loop
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Context MCP Server operational on stdio");
}
main().catch((err) => {
console.error("Fatal startup error:", err);
process.exit(1);
});
This implementation handles the second half of the MCP lifecycle: dynamic tool invocation. The server validates incoming JSON structures using Zod, enforces a strict read-only policy by verifying that statements begin with SELECT, truncates payloads to 25 records to protect context limits, and routes debug messages to stderr so the stdio communication channel remains clean.
console.log() for diagnostic logging inside an MCP server using stdio transport. Standard output is reserved strictly for raw JSON-RPC messages; writing arbitrary text there will corrupt the communication pipe and crash your editor's client extension. Always use console.error() instead.With our server code in place, let's wire it up to your daily editor environment.
Connecting Your Server to Cursor and VS Code
Modern editors run your MCP server as a local background process whenever the IDE opens your workspace. Here is how you complete your model context protocol cursor setup and configure modern VS Code environments.
In Cursor, navigate to Cursor Settings > Features > MCP, click + Add New MCP Server, and configure your parameters. Alternatively, create a configuration file at ~/.cursor/mcp.json or directly in your project root at .cursor/mcp.json:
{
"mcpServers": {
"local-db-context": {
"command": "npx",
"args": ["tsx", "/ABSOLUTE/PATH/TO/dev-context-mcp/src/index.ts"]
}
}
}
This JSON block tells Cursor how to spawn our server process whenever an agent session starts. Note the absolute path: Cursor spawns daemon processes outside your active terminal session, so relative file paths will fail to resolve.
For modern VS Code setups, configure the official MCP extension manifest in your project workspace under .vscode/mcp.json:
{
"servers": {
"local-db-context": {
"type": "stdio",
"command": "node",
"args": ["--import", "tsx", "/ABSOLUTE/PATH/TO/dev-context-mcp/src/index.ts"],
"env": {
"NODE_ENV": "development"
}
}
}
}
This snippet standardizes your mcp integration vs code 2026 workflow across your team. Once saved, VS Code automatically prompts you to approve the local server transport, making your tools and schema resources available to the Copilot agent interface.
Best Practices and Common Pitfalls
Enforce Bounded Token Truncation
Large query outputs degrade agent reasoning and trigger token limit errors. Never return raw SQL dumps with thousands of records. Cap result sets using a deterministic LIMIT, summarize arrays with high-level statistics, and return targeted summaries so the model only consumes what it needs to solve the problem.
total_rows_matching: 1840 with just the top 5 records. This preserves complete context for reasoning without flooding the model with redundant tokens.Enforce Sandbox Boundaries on Queries
Basic string prefix checking (like matching SELECT) works for simple local scripts, but production environments require stronger isolation. Developers often forget that nested SQL statements like SELECT * FROM users WHERE id IN (DELETE FROM ...) can trigger side effects.
Always connect your MCP servers using read-only database roles, or run your queries inside read-only transactions (BEGIN TRANSACTION READ ONLY;). This prevents catastrophic data loss if an agent attempts an unexpected mutation.
Isolate Protocol Stdio from Third-Party Logs
If an imported library writes banner art, version notices, or debug logs to process.stdout, your MCP client will fail with JSON parse errors. Audit all dependencies and redirect their log output to stderr to keep the transport clean.
Real-World Example
Let's examine how a fintech engineering team uses these developer productivity mcp tools to triage failed webhook transactions in local development.
A billing developer at a financial services startup notices their local webhook endpoint is rejecting incoming test events with 402 Payment Required errors. Instead of manually launching an external database GUI, running manual joins across audit tables, and copying IDs into their prompt, they ask their editor's agent directly:
# Developer prompt inside Cursor Composer / VS Code Agent:
"Audit why our seed tests failed on billing upgrades. Cross-reference the audit_logs table with user tiers."
Under the hood, the AI agent executes a coordinated sequence of operations:
- It pulls the active schema resource (
db://schema/tables) and inspects the foreign keys betweenusersandaudit_logs. - It autonomously calls the tool:
run_read_query({"query": "SELECT u.email, u.tier, a.action, a.status_code FROM users u JOIN audit_logs a ON u.id = a.user_id WHERE a.status_code = 402"}). - It receives the bounded JSON payload:
alex@startup.iofailed because the account is on theprotier, but the simulated endpoint requires enterprise scopes. - The agent suggests a code fix in the test handler and crafts the appropriate migration without any manual context switching.
This automated flow turns a ten-minute debugging session into an instantaneous, single-prompt fix.
Future Outlook and What's Coming Next
The Model Context Protocol ecosystem is moving quickly beyond simple local standard input/output connections. Over the next 12 to 18 months, look for several key improvements to land across the ecosystem:
The MCP working group is standardizing native bi-directional SSE streams with distributed mutual TLS (mTLS). This will allow teams to share internal observability and staging databases across remote development containers without relying on raw local standard streams.
We are also seeing the rollout of native binary framing protocols like CBOR, which drastically reduce serialization overhead when streaming large profiles, heap dumps, and telemetry into local models. Mastering the protocol's core primitives today positions your team to adopt these upcoming agent capabilities with minimal rework.
Conclusion
Context rot is the single biggest bottleneck to effective AI-assisted software development. Building your own custom MCP server bridges the gap between your editor's coding models and your actual local runtime, turning generic autocomplete suggestions into precise, system-aware edits.
By implementing typed validation schemas, enforcing read-only query boundaries, and optimizing payload sizes, you build a dependable development tool that saves your team hours of manual triage every week.
Do not wait for third parties to release generic extensions that half-fit your stack. Run the setup commands above, wire up your local database or API logs to your editor, and see how much faster your AI agent moves when it can finally see your entire system.
- The Model Context Protocol (MCP) replaces fragile static documentation prompts with typed, real-time client-server communication.
- MCP servers structure agent interactions using three core primitives: Resources (passive context), Tools (active execution), and Prompts (workflow orchestration).
- Always keep
stdoutclean for JSON-RPC messages and log diagnostic info tostderrto prevent transport errors. - Protect token windows and prevent database damage by enforcing read-only transactions and bounding result sizes.