Implementing MCP Servers with TypeScript
Introduction
In our previous guide, we explored building an MCP server using Python. Today, we'll implement the same functionality using TypeScript, showcasing how to build a robust MCP server that connects Claude (or any other MCP-compatible AI assistant) directly to your database.
TypeScript offers several advantages for MCP server implementation, including:
- Strong typing that helps catch errors at compile time
- Better code organization and maintainability
- Native integration with Node.js ecosystem
- Excellent tooling and IDE support
By the end of this guide, you'll have a fully functional TypeScript-based MCP server that connects to a SQLite database, allowing Claude to query your data while maintaining full control over your information.
Prerequisites
Before we begin, make sure you have the following:
- Node.js 18+ installed
- npm or yarn package manager
- Basic knowledge of TypeScript and JavaScript
- Familiarity with SQL and database concepts
- Claude Desktop or another MCP-compatible client for testing
Don't worry if you're new to TypeScript - we'll walk through each step carefully.
The TypeScript MCP SDK
The MCP TypeScript SDK provides all the building blocks you need to create an MCP server. Released by the Model Context Protocol team, it handles the protocol details, connection management, and communication patterns so you can focus on implementing your database integration.
The core components of the SDK include:
MCPServer
: The main server class that manages connections and routes requestsResource
: For making static or semi-static data available to the AITool
: For implementing functions that can be called by the AI- Type definitions for all protocol messages and objects
Project Setup
Let's start by creating a new TypeScript project and installing the necessary dependencies:
# Create a new directory for your project
mkdir mcp-sqlite-server
cd mcp-sqlite-server
# Initialize your project
npm init -y
# Install required dependencies
npm install @mcp/sdk@latest sqlite3 better-sqlite3 typescript ts-node @types/node
# Initialize TypeScript configuration
npx tsc --init
Next, let's update the tsconfig.json
file to configure TypeScript properly:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
Let's create a basic folder structure for our project:
mkdir -p src/tools src/resources
touch src/index.ts src/database.ts
Setting Up Database Connection
First, let's create a sample SQLite database that we'll use for our example. Create a file named
src/database.ts
with the following content:
import Database from 'better-sqlite3';
import { existsSync } from 'fs';
import { join } from 'path';
// Database configuration
const DB_PATH = join(process.cwd(), 'sample.db');
// Initialize the database
export function initializeDatabase(): Database.Database {
const isNewDatabase = !existsSync(DB_PATH);
const db = new Database(DB_PATH);
// Create tables if this is a new database
if (isNewDatabase) {
console.log('Creating new database with sample data...');
// Create products table
db.exec(`
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
inventory INTEGER NOT NULL
)
`);
// Create customers table
db.exec(`
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
signup_date TEXT NOT NULL
)
`);
// Create orders table
db.exec(`
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TEXT NOT NULL,
total_amount REAL NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers (id)
)
`);
// Create order_items table
db.exec(`
CREATE TABLE order_items (
id INTEGER PRIMARY KEY,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (id),
FOREIGN KEY (product_id) REFERENCES products (id)
)
`);
// Insert sample data
insertSampleData(db);
}
return db;
}
// Insert sample data into the database
function insertSampleData(db: Database.Database) {
// Insert products
const products = [
{ name: 'Laptop', category: 'Electronics', price: 999.99, inventory: 45 },
{ name: 'Smartphone', category: 'Electronics', price: 699.99, inventory: 120 },
{ name: 'Headphones', category: 'Accessories', price: 149.99, inventory: 75 },
{ name: 'Monitor', category: 'Electronics', price: 349.99, inventory: 30 },
{ name: 'Keyboard', category: 'Accessories', price: 79.99, inventory: 65 }
];
const insertProduct = db.prepare(`
INSERT INTO products (name, category, price, inventory)
VALUES (?, ?, ?, ?)
`);
products.forEach(product => {
insertProduct.run(product.name, product.category, product.price, product.inventory);
});
// Insert customers
const customers = [
{ name: 'John Doe', email: 'john.doe@example.com', signup_date: '2024-01-15' },
{ name: 'Jane Smith', email: 'jane.smith@example.com', signup_date: '2024-02-20' },
{ name: 'Michael Brown', email: 'michael.brown@example.com', signup_date: '2024-03-05' }
];
const insertCustomer = db.prepare(`
INSERT INTO customers (name, email, signup_date)
VALUES (?, ?, ?)
`);
customers.forEach(customer => {
insertCustomer.run(customer.name, customer.email, customer.signup_date);
});
// Insert orders and order items
const orders = [
{ customer_id: 1, order_date: '2024-04-10', total_amount: 1149.98, status: 'Delivered' },
{ customer_id: 2, order_date: '2024-04-15', total_amount: 699.99, status: 'Shipped' },
{ customer_id: 3, order_date: '2024-04-20', total_amount: 429.98, status: 'Processing' }
];
const insertOrder = db.prepare(`
INSERT INTO orders (customer_id, order_date, total_amount, status)
VALUES (?, ?, ?, ?)
`);
orders.forEach(order => {
const result = insertOrder.run(order.customer_id, order.order_date, order.total_amount, order.status);
const orderId = result.lastInsertRowid;
if (orderId === 1) {
// Add items to first order
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 1, 1, 999.99);
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 3, 1, 149.99);
} else if (orderId === 2) {
// Add items to second order
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 2, 1, 699.99);
} else if (orderId === 3) {
// Add items to third order
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 3, 1, 149.99);
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 5, 1, 79.99);
db.prepare(`
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)
`).run(orderId, 4, 1, 349.99);
}
});
console.log('Sample data inserted successfully');
}
// Export database functions
export function getDatabaseSchema(): string {
const db = new Database(DB_PATH);
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`).all();
let schema = '';
for (const table of tables) {
const tableInfo = db.prepare(`PRAGMA table_info(${table.name})`).all();
schema += `Table: ${table.name}\n`;
schema += 'Columns:\n';
for (const column of tableInfo) {
schema += ` - ${column.name} (${column.type})`;
if (column.pk === 1) schema += ' PRIMARY KEY';
if (column.notnull === 1) schema += ' NOT NULL';
schema += '\n';
}
schema += '\n';
}
db.close();
return schema;
}
// Function to execute a SQL query
export function executeQuery(query: string, params?: any[]): any {
const db = new Database(DB_PATH);
let result;
try {
if (query.trim().toLowerCase().startsWith('select')) {
const stmt = params ? db.prepare(query).bind(...params) : db.prepare(query);
result = stmt.all();
} else {
const stmt = params ? db.prepare(query).bind(...params) : db.prepare(query);
result = stmt.run();
}
return result;
} catch (error) {
throw error;
} finally {
db.close();
}
}
Implementing the MCP Server
Now let's create the MCP server implementation. We'll start by implementing the resources that provide information about our database schema.
Create a file named src/resources/schema.ts
:
import { Resource } from '@mcp/sdk';
import { getDatabaseSchema } from '../database';
export class SchemaResource extends Resource {
constructor() {
super({
name: 'database_schema',
description: 'The schema of the database including tables and columns',
content_type: 'text/plain',
is_stream: false
});
}
async read(): Promise {
return getDatabaseSchema();
}
}
Now, let's implement the database query tool in src/tools/query.ts
:
import { Tool, ToolResponse, ToolResponseFormat } from '@mcp/sdk';
import { executeQuery } from '../database';
export class QueryDatabaseTool extends Tool {
constructor() {
super({
name: 'query_database',
description: 'Execute a SQL query against the database',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The SQL query to execute'
},
params: {
type: 'array',
description: 'Optional parameters for the query',
items: {
type: 'string'
}
}
},
required: ['query']
}
});
}
async invoke(params: any): Promise {
const { query, params: queryParams } = params;
try {
const result = executeQuery(query, queryParams);
// For SELECT queries, return the results
if (query.trim().toLowerCase().startsWith('select')) {
return {
format: ToolResponseFormat.JSON,
data: result
};
}
// For other queries (INSERT, UPDATE, DELETE), return success message
return {
format: ToolResponseFormat.STRING,
data: `Query executed successfully. Rows affected: ${result.changes}`
};
} catch (error: any) {
return {
format: ToolResponseFormat.ERROR,
error: `Error executing query: ${error.message}`
};
}
}
}
Let's also implement a tool to list the tables in the database in src/tools/list-tables.ts
:
import { Tool, ToolResponse, ToolResponseFormat } from '@mcp/sdk';
import { executeQuery } from '../database';
export class ListTablesTool extends Tool {
constructor() {
super({
name: 'list_tables',
description: 'List all tables in the database',
parameters: {
type: 'object',
properties: {},
required: []
}
});
}
async invoke(): Promise {
try {
const result = executeQuery(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`);
return {
format: ToolResponseFormat.JSON,
data: result.map((row: any) => row.name)
};
} catch (error: any) {
return {
format: ToolResponseFormat.ERROR,
error: `Error listing tables: ${error.message}`
};
}
}
}
Now, let's add a tool to get table information in src/tools/table-info.ts
:
import { Tool, ToolResponse, ToolResponseFormat } from '@mcp/sdk';
import { executeQuery } from '../database';
export class TableInfoTool extends Tool {
constructor() {
super({
name: 'get_table_info',
description: 'Get information about a specific table',
parameters: {
type: 'object',
properties: {
table_name: {
type: 'string',
description: 'The name of the table'
}
},
required: ['table_name']
}
});
}
async invoke(params: any): Promise {
const { table_name } = params;
try {
// Validate table exists
const tableExists = executeQuery(`
SELECT name FROM sqlite_master
WHERE type='table' AND name = ?
`, [table_name]);
if (tableExists.length === 0) {
return {
format: ToolResponseFormat.ERROR,
error: `Table '${table_name}' does not exist`
};
}
// Get table columns info
const columns = executeQuery(`PRAGMA table_info(${table_name})`);
// Get sample data (limited to 5 rows)
const sampleData = executeQuery(`SELECT * FROM ${table_name} LIMIT 5`);
return {
format: ToolResponseFormat.JSON,
data: {
table_name,
columns,
sample_data: sampleData
}
};
} catch (error: any) {
return {
format: ToolResponseFormat.ERROR,
error: `Error getting table info: ${error.message}`
};
}
}
}
Defining Resources
In addition to the schema resource we've already implemented, you can define additional resources to provide more context to the AI assistant. For example, you might want to create a resource that provides sample queries or documentation about your database schema.
Here's how you could implement a sample queries resource:
import { Resource } from '@mcp/sdk';
export class SampleQueriesResource extends Resource {
constructor() {
super({
name: 'sample_queries',
description: 'Example SQL queries for the database',
content_type: 'text/plain',
is_stream: false
});
}
async read(): Promise {
return `
# Sample Queries for the E-commerce Database
## Product Queries
- List all products:
SELECT * FROM products;
## Customer Queries
- Find customers who signed up in 2024:
SELECT * FROM customers WHERE signup_date LIKE '2024-%';
## Order Queries
- Get total revenue:
SELECT SUM(total_amount) AS total_revenue FROM orders;
## Analysis Queries
- Find top-selling products:
SELECT p.name, SUM(oi.quantity) as total_sold
FROM order_items oi
JOIN products p ON oi.product_id = p.id
GROUP BY p.name
ORDER BY total_sold DESC;
`;
}
}
Running Your Server
Now let's put everything together and create our main server file. Create src/index.ts
:
import { MCPServer } from '@mcp/sdk';
import { initializeDatabase } from './database';
import { SchemaResource } from './resources/schema';
import { QueryDatabaseTool } from './tools/query';
import { ListTablesTool } from './tools/list-tables';
import { TableInfoTool } from './tools/table-info';
async function main() {
// Initialize the database
initializeDatabase();
// Create MCP server
const server = new MCPServer({
name: 'SQLite Database Server',
description: 'A simple MCP server that provides access to a SQLite database',
version: '1.0.0'
});
// Register resources
server.registerResource(new SchemaResource());
// Register tools
server.registerTool(new QueryDatabaseTool());
server.registerTool(new ListTablesTool());
server.registerTool(new TableInfoTool());
// Start the server
await server.start({
host: 'localhost',
port: 8000
});
console.log('MCP Server started on http://localhost:8000');
console.log('Connect from Claude Desktop using the URL: ws://localhost:8000');
}
main().catch(console.error);
Now let's add a script to our package.json
file to run the server:
{
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
}
}
To run your server, execute the following command:
npm start
Connecting to Claude Desktop
Now that your server is running, you can connect it to Claude Desktop:
- Open Claude Desktop application
- Click on the settings icon
- Select "Connections"
- Click "Add Connection"
- Enter the server address:
ws://localhost:8000
- Click "Connect"
Once connected, you can start a conversation with Claude and ask it to interact with your database:
- "What tables are in the database?"
- "Show me the schema of the customers table."
- "How many products are in the Electronics category?"
- "What's the total value of all products in inventory?"
- "Show me all orders with a total amount over $700."
Claude will use your MCP server to execute the appropriate queries and return the results.
For large query results, you can implement streaming to handle the data more efficiently:
export class StreamingQueryTool extends Tool {
constructor() {
super({
name: 'streaming_query',
description: 'Execute a SQL query with streaming results',
parameters: {
// Same as QueryDatabaseTool
}
});
}
async *stream(params: any): AsyncGenerator {
const { query } = params;
// Implementation using streams for large results
// ...
}
}
Conclusion
In this guide, we've walked through building a complete MCP server in TypeScript that connects to a SQLite database. This same approach can be extended to work with other databases like PostgreSQL, MySQL, or MongoDB by changing the database adapter.
The Model Context Protocol enables powerful integrations between AI models and your data while maintaining full control over access and permissions. By building custom MCP servers, you can create specialized tools that allow AI assistants like Claude to work with your specific use cases.