MCP (Model Context Protocol) is an open protocol that defines how applications communicate context to LLMs. Think of it as a standardized interface — instead of writing ad-hoc integrations for every tool, MCP gives language models a unified way to connect with external systems like APIs, databases, and document repositories.
How MCP Works
MCP operates on a client-server model:
- MCP Client — lives inside the AI application (e.g. Cursor, Claude Desktop)
- MCP Server — exposes tools and resources, either as a local process or a remote service
This separation lets you swap components without redesigning your entire prompt strategy. You build a server once, and any MCP-compatible client can use it.
Building a Todo MCP Server
Let's build a Node.js MCP server that gives an LLM access to a PostgreSQL todo database. The server will expose four tools: list, create, update title, and update completion status.
Database Schema
CREATE TABLE todos (
id integer NOT NULL,
title character varying(255) NOT NULL,
description text,
is_completed boolean DEFAULT false,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
);Setup
npm init -y
npm install @modelcontextprotocol/sdk pg zod
npm install -D typescript ts-node @types/nodepackage.json
{
"name": "test",
"version": "1.0.0",
"main": "build/index.js",
"scripts": {
"dev": "npx ts-node index.ts",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"pg": "^8.14.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.17",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
}
}tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"rootDir": "./",
"outDir": "./build",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"exclude": ["node_modules"]
}Server Implementation
index.ts
const {
McpServer,
} = require("@modelcontextprotocol/sdk/server/mcp.js");
const {
StdioServerTransport,
} = require("@modelcontextprotocol/sdk/server/stdio.js");
const { Pool } = require("pg");
const { z } = require("zod");
const server = new McpServer(
{ name: "demo", version: "1.0.0" },
{ capabilities: { resources: {} } }
);
const pool = new Pool({
user: "postgres",
password: "postgres",
host: "localhost",
database: "todo",
port: "5432",
ssl: { rejectUnauthorized: false },
});
const getDb = async () => {
const dbClient = await pool.connect();
return dbClient;
};
// Tool 1: List all todos
server.tool(
"list-todos",
"List all todos",
{},
async () => {
const db = await getDb();
const todoRows = await db.query("SELECT * FROM todos;");
const todos = todoRows.rows;
if (todos.length === 0) {
return { content: [{ type: "text", text: "No todos found!" }] };
}
const todoItems = todos.map(
(todo: { title: string; is_completed: boolean }) => `
Title: ${todo.title}
isCompleted: ${todo.is_completed}`
);
return {
content: [{ type: "text", text: `TODO list:\n${todoItems}` }],
};
}
);
// Tool 2: Create a todo
server.tool(
"createTodo",
{ title: z.string() },
async ({ title }: { title: string }) => {
const db = await getDb();
try {
await db.query('INSERT into todos ("title") VALUES ($1);', [title]);
return { content: [{ type: "text", text: `Todo created:\n\n${title}` }] };
} finally {
await db.release();
}
}
);
// Tool 3: Update todo title
server.tool(
"updateTodo",
{ oldTitle: z.string(), newTitle: z.string() },
async ({ oldTitle, newTitle }: { oldTitle: string; newTitle: string }) => {
const db = await getDb();
try {
await db.query(
'UPDATE todos SET "title" = $2 WHERE LOWER(title) = $1;',
[oldTitle.toLowerCase(), newTitle]
);
return { content: [{ type: "text", text: `Todo updated:\n\n${newTitle}` }] };
} finally {
await db.release();
}
}
);
// Tool 4: Update completion status
server.tool(
"updateCompletedStatus",
{ title: z.string(), status: z.boolean() },
async ({ title, status }: { title: string; status: boolean }) => {
const db = await getDb();
try {
await db.query(
'UPDATE todos SET "is_completed" = $2 WHERE LOWER(title) = $1;',
[title.toLowerCase(), status]
);
return {
content: [{ type: "text", text: `Todo "${title}" status updated:\n\n${status}` }],
};
} finally {
await db.release();
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("todo app is running");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});Registering with a Client (Cursor)
Build the server first:
npm run buildThen register it in Cursor by creating an mcp.json configuration:
{
"mcpServers": {
"todos": {
"command": "node",
"args": ["{SOURCE_DIR_PATH}\\build\\index.js"]
}
}
}Cursor will launch the server process and make all four tools available to the LLM automatically.
What You Can Now Ask the LLM
Once connected, you can talk to your database in plain English:
- "Show me all my todos"
- "Create a todo called Buy groceries"
- "Mark Buy groceries as completed"
- "Rename Buy groceries to Buy organic groceries"
The LLM calls the appropriate tool, executes the query, and returns the result — no extra prompt engineering needed.
Conclusion
MCP removes the friction of one-off LLM integrations. You define your tools once with clear schemas, and any MCP-compatible client can discover and use them. As more editors and AI applications adopt the protocol, a single MCP server becomes reusable across your entire toolchain.