Microsoft SQL Server MCP Server (MSSQL)
by dperussina
Verified
// lib/resources.js - Database resource implementations
import { executeQuery, sanitizeSqlIdentifier, formatSqlError } from './database.mjs';
import { logger } from './logger.mjs';
import { createJsonRpcError } from './errors.mjs';
/**
* Register all database-related resources with the MCP server
* @param {object} server - MCP server instance
*/
export function registerDatabaseResources(server) {
logger.info('Registering database resources');
// Wrap the original resource method to add logging and error handling
const originalResource = server.resource.bind(server);
server.resource = function(name, uriPattern, handler) {
const wrappedHandler = async function(...args) {
logger.info(`Reading resource: ${name}`);
logger.debug(`URI: ${args[0]?.href}`);
try {
const result = await handler(...args);
logger.info(`Resource ${name} read successfully`);
return result;
} catch (err) {
logger.error(`Resource ${name} read failed: ${err.message}`);
// Format error for response
const errorMessage = formatSqlError(err);
return {
contents: [{
uri: args[0]?.href || `${name}://error`,
text: `Error reading resource: ${errorMessage}`
}]
};
}
};
return originalResource(name, uriPattern, wrappedHandler);
};
// Register all database resources
registerDatabaseSchemaResource(server);
registerTablesListResource(server);
registerProceduresListResource(server);
registerFunctionsListResource(server);
registerViewsListResource(server);
registerIndexesListResource(server);
registerAiSchemaResource(server);
registerDiscoveryResource(server);
logger.info('Database resources registered successfully');
}
/**
* Register the database schema resource
* @param {object} server - MCP server instance
*/
function registerDatabaseSchemaResource(server) {
server.resource(
"schema",
"schema://database",
async (uri) => {
try {
logger.info('Fetching database schema...');
const result = await executeQuery(`
SELECT
TABLE_NAME,
COLUMN_NAME,
DATA_TYPE,
IS_NULLABLE,
CHARACTER_MAXIMUM_LENGTH,
COLUMN_DEFAULT
FROM
INFORMATION_SCHEMA.COLUMNS
ORDER BY
TABLE_NAME, ORDINAL_POSITION
`);
// Format schema data into human-readable text
const formattedSchema = formatSchemaData(result.recordset);
logger.info('Schema retrieved successfully');
return {
contents: [{
uri: uri.href,
text: formattedSchema
}]
};
} catch (err) {
logger.error(`Error retrieving schema: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the tables list resource
* @param {object} server - MCP server instance
*/
function registerTablesListResource(server) {
server.resource(
"tables",
"tables://list",
async (uri) => {
try {
logger.info('Fetching tables list...');
const result = await executeQuery(`
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_TYPE
FROM
INFORMATION_SCHEMA.TABLES
WHERE
TABLE_TYPE = 'BASE TABLE'
ORDER BY
TABLE_SCHEMA, TABLE_NAME
`);
// Format as markdown list grouped by schema
let markdown = `# Database Tables\n\n`;
// Group by schema
const tablesBySchema = {};
result.recordset.forEach(table => {
if (!tablesBySchema[table.TABLE_SCHEMA]) {
tablesBySchema[table.TABLE_SCHEMA] = [];
}
tablesBySchema[table.TABLE_SCHEMA].push(table.TABLE_NAME);
});
// Add tables by schema
for (const [schema, tables] of Object.entries(tablesBySchema)) {
markdown += `## ${schema} Schema\n\n`;
tables.forEach(table => {
markdown += `- ${table}\n`;
});
markdown += '\n';
}
logger.info(`Retrieved ${result.recordset.length} tables`);
return {
contents: [{
uri: uri.href,
text: markdown
}]
};
} catch (err) {
logger.error(`Error retrieving tables: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the stored procedures list resource
* @param {object} server - MCP server instance
*/
function registerProceduresListResource(server) {
server.resource(
"procedures",
"procedures://list",
async (uri) => {
try {
logger.info('Fetching stored procedures list...');
const result = await executeQuery(`
SELECT
ROUTINE_SCHEMA,
ROUTINE_NAME
FROM
INFORMATION_SCHEMA.ROUTINES
WHERE
ROUTINE_TYPE = 'PROCEDURE'
ORDER BY
ROUTINE_SCHEMA, ROUTINE_NAME
`);
// Format as markdown list grouped by schema
let markdown = `# Database Stored Procedures\n\n`;
// Group by schema
const procsBySchema = {};
result.recordset.forEach(proc => {
if (!procsBySchema[proc.ROUTINE_SCHEMA]) {
procsBySchema[proc.ROUTINE_SCHEMA] = [];
}
procsBySchema[proc.ROUTINE_SCHEMA].push(proc.ROUTINE_NAME);
});
// Add procedures by schema
for (const [schema, procs] of Object.entries(procsBySchema)) {
markdown += `## ${schema} Schema\n\n`;
procs.forEach(proc => {
markdown += `- ${proc}\n`;
});
markdown += '\n';
}
logger.info(`Retrieved ${result.recordset.length} stored procedures`);
return {
contents: [{
uri: uri.href,
text: markdown
}]
};
} catch (err) {
logger.error(`Error retrieving stored procedures: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the functions list resource
* @param {object} server - MCP server instance
*/
function registerFunctionsListResource(server) {
server.resource(
"functions",
"functions://list",
async (uri) => {
try {
logger.info('Fetching functions list...');
const result = await executeQuery(`
SELECT
ROUTINE_SCHEMA,
ROUTINE_NAME,
DATA_TYPE AS RETURN_TYPE
FROM
INFORMATION_SCHEMA.ROUTINES
WHERE
ROUTINE_TYPE = 'FUNCTION'
ORDER BY
ROUTINE_SCHEMA, ROUTINE_NAME
`);
// Format as markdown list grouped by schema
let markdown = `# Database Functions\n\n`;
// Group by schema
const funcsBySchema = {};
result.recordset.forEach(func => {
if (!funcsBySchema[func.ROUTINE_SCHEMA]) {
funcsBySchema[func.ROUTINE_SCHEMA] = [];
}
funcsBySchema[func.ROUTINE_SCHEMA].push({
name: func.ROUTINE_NAME,
returnType: func.RETURN_TYPE
});
});
// Add functions by schema
for (const [schema, funcs] of Object.entries(funcsBySchema)) {
markdown += `## ${schema} Schema\n\n`;
markdown += '| Function | Return Type |\n';
markdown += '|----------|------------|\n';
funcs.forEach(func => {
markdown += `| ${func.name} | ${func.returnType} |\n`;
});
markdown += '\n';
}
logger.info(`Retrieved ${result.recordset.length} functions`);
return {
contents: [{
uri: uri.href,
text: markdown
}]
};
} catch (err) {
logger.error(`Error retrieving functions: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the views list resource
* @param {object} server - MCP server instance
*/
function registerViewsListResource(server) {
server.resource(
"views",
"views://list",
async (uri) => {
try {
logger.info('Fetching views list...');
const result = await executeQuery(`
SELECT
TABLE_SCHEMA,
TABLE_NAME
FROM
INFORMATION_SCHEMA.VIEWS
ORDER BY
TABLE_SCHEMA, TABLE_NAME
`);
// Format as markdown list grouped by schema
let markdown = `# Database Views\n\n`;
// Group by schema
const viewsBySchema = {};
result.recordset.forEach(view => {
if (!viewsBySchema[view.TABLE_SCHEMA]) {
viewsBySchema[view.TABLE_SCHEMA] = [];
}
viewsBySchema[view.TABLE_SCHEMA].push(view.TABLE_NAME);
});
// Add views by schema
for (const [schema, views] of Object.entries(viewsBySchema)) {
markdown += `## ${schema} Schema\n\n`;
views.forEach(view => {
markdown += `- ${view}\n`;
});
markdown += '\n';
}
logger.info(`Retrieved ${result.recordset.length} views`);
return {
contents: [{
uri: uri.href,
text: markdown
}]
};
} catch (err) {
logger.error(`Error retrieving views: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the indexes list resource
* @param {object} server - MCP server instance
*/
function registerIndexesListResource(server) {
server.resource(
"indexes",
"indexes://list",
async (uri) => {
try {
logger.info('Fetching indexes list...');
const result = await executeQuery(`
SELECT
s.name AS SchemaName,
t.name AS TableName,
i.name AS IndexName,
i.type_desc AS IndexType,
i.is_unique AS IsUnique,
i.is_primary_key AS IsPrimaryKey
FROM
sys.indexes i
INNER JOIN
sys.tables t ON i.object_id = t.object_id
INNER JOIN
sys.schemas s ON t.schema_id = s.schema_id
WHERE
i.name IS NOT NULL
ORDER BY
s.name, t.name, i.name
`);
// Format as markdown table
let markdown = `# Database Indexes\n\n`;
// Group by table
const indexesByTable = {};
result.recordset.forEach(idx => {
const tableKey = `${idx.SchemaName}.${idx.TableName}`;
if (!indexesByTable[tableKey]) {
indexesByTable[tableKey] = [];
}
indexesByTable[tableKey].push({
name: idx.IndexName,
type: idx.IndexType,
isUnique: idx.IsUnique,
isPrimaryKey: idx.IsPrimaryKey
});
});
// Add indexes by table
for (const [table, indexes] of Object.entries(indexesByTable)) {
markdown += `## ${table}\n\n`;
markdown += '| Index Name | Type | Unique | Primary Key |\n';
markdown += '|------------|------|--------|------------|\n';
indexes.forEach(idx => {
markdown += `| ${idx.name} | ${idx.type} | ${idx.isUnique ? 'Yes' : 'No'} | ${idx.isPrimaryKey ? 'Yes' : 'No'} |\n`;
});
markdown += '\n';
}
logger.info(`Retrieved ${result.recordset.length} indexes`);
return {
contents: [{
uri: uri.href,
text: markdown
}]
};
} catch (err) {
logger.error(`Error retrieving indexes: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the AI schema resource
* @param {object} server - MCP server instance
*/
function registerAiSchemaResource(server) {
server.resource(
"ai-schema",
"ai-schema://database",
async (uri) => {
try {
logger.info('Generating AI-friendly database schema...');
// Get tables
const tablesResult = await executeQuery(`
SELECT
TABLE_SCHEMA,
TABLE_NAME
FROM
INFORMATION_SCHEMA.TABLES
WHERE
TABLE_TYPE = 'BASE TABLE'
ORDER BY
TABLE_SCHEMA, TABLE_NAME
`);
// Generate a comprehensive schema description for AI
let aiSchemaText = '# AI Assistant Database Guide\n\n';
aiSchemaText += 'This is a guide for AI assistants to interact with this SQL Server database.\n\n';
// Add tables section
aiSchemaText += '## Available Tables\n\n';
// Group by schema
const tablesBySchema = {};
tablesResult.recordset.forEach(table => {
if (!tablesBySchema[table.TABLE_SCHEMA]) {
tablesBySchema[table.TABLE_SCHEMA] = [];
}
tablesBySchema[table.TABLE_SCHEMA].push(table.TABLE_NAME);
});
// Add tables by schema
for (const [schema, tables] of Object.entries(tablesBySchema)) {
aiSchemaText += `### ${schema} Schema\n\n`;
aiSchemaText += '```\n';
tables.forEach(table => {
aiSchemaText += `${table}\n`;
});
aiSchemaText += '```\n\n';
}
// Add usage examples
aiSchemaText += '## MCP Usage Examples\n\n';
aiSchemaText += '### Listing Tables\n';
aiSchemaText += 'To list tables, use the `tables://list` resource:\n';
aiSchemaText += '```javascript\n';
aiSchemaText += 'mcp__resources_read("tables://list")\n';
aiSchemaText += '```\n\n';
aiSchemaText += '### Executing Queries\n';
aiSchemaText += 'To execute a SQL query, use the `execute-query` tool:\n';
aiSchemaText += '```javascript\n';
aiSchemaText += 'mcp__execute_query({ sql: "SELECT TOP 100 * FROM [table_name]" })\n';
aiSchemaText += '```\n\n';
aiSchemaText += '### Getting Table Details\n';
aiSchemaText += 'To get details about a specific table, use the `table-details` tool:\n';
aiSchemaText += '```javascript\n';
aiSchemaText += 'mcp__table_details({ tableName: "table_name" })\n';
aiSchemaText += '```\n\n';
aiSchemaText += '## Best Practices for AI Assistants\n\n';
aiSchemaText += '1. Always check table existence before querying\n';
aiSchemaText += '2. Use `SELECT TOP N` for safety when exploring large tables\n';
aiSchemaText += '3. Explore table schema with `table-details` before constructing complex queries\n';
aiSchemaText += '4. Use `discover-database()` to get a comprehensive overview\n';
logger.info('AI-friendly schema generated');
return {
contents: [{
uri: uri.href,
text: aiSchemaText
}]
};
} catch (err) {
logger.error(`Error generating AI schema: ${err.message}`);
throw err;
}
}
);
}
/**
* Register the discovery resource
* @param {object} server - MCP server instance
*/
function registerDiscoveryResource(server) {
server.resource(
"discovery",
"discovery://tables",
async (uri) => {
try {
logger.info('Generating table discovery guide...');
// Get tables with sample data for better understanding
const tablesResult = await executeQuery(`
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_TYPE
FROM
INFORMATION_SCHEMA.TABLES
WHERE
TABLE_TYPE = 'BASE TABLE'
ORDER BY
TABLE_SCHEMA, TABLE_NAME
`);
// Get a sample of common tables with row counts for context
const sampleTablesWithRowCounts = [];
// Get row counts for the first 5 tables (limited to avoid performance issues)
for (let i = 0; i < Math.min(5, tablesResult.recordset.length); i++) {
const tableSchema = tablesResult.recordset[i].TABLE_SCHEMA;
const tableName = tablesResult.recordset[i].TABLE_NAME;
try {
const countResult = await executeQuery(`
SELECT
COUNT(*) AS TotalRows
FROM
[${tableSchema}].[${tableName}]
`, {
schemaName: tableSchema,
tableName: tableName
});
const rowCount = countResult.recordset[0].TotalRows || 0;
sampleTablesWithRowCounts.push({
schema: tableSchema,
name: tableName,
rowCount
});
} catch (err) {
logger.error(`Error getting row count for ${tableSchema}.${tableName}: ${err.message}`);
sampleTablesWithRowCounts.push({
schema: tableSchema,
name: tableName,
rowCount: "Unknown"
});
}
}
// Generate a comprehensive table discovery guide
let discoveryText = '# Table Discovery Guide\n\n';
discoveryText += 'This guide will help you discover and explore tables in this SQL Server database.\n\n';
// Step 1: List all tables
discoveryText += '## Step 1: List All Tables\n\n';
discoveryText += 'To get a complete list of all tables in the database, use this command:\n\n';
discoveryText += '```javascript\n';
discoveryText += 'mcp__execute_query({ sql: "SELECT TOP 100 TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\' ORDER BY TABLE_SCHEMA, TABLE_NAME" })\n';
discoveryText += '```\n\n';
// Step 2: Explore table structure
discoveryText += '## Step 2: Explore Table Structure\n\n';
discoveryText += 'Once you have table names, explore their structure using either table-details or SQL:\n\n';
discoveryText += '```javascript\n';
discoveryText += '// Option 1: Using the dedicated tool\n';
if (sampleTablesWithRowCounts.length > 0) {
discoveryText += `mcp__table_details({ tableName: "${sampleTablesWithRowCounts[0].name}" })\n\n`;
} else {
discoveryText += `mcp__table_details({ tableName: "example_table_name" })\n\n`;
}
discoveryText += '// Option 2: Using SQL query\n';
discoveryText += 'mcp__execute_query({ sql: "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = \'example_table_name\' ORDER BY ORDINAL_POSITION" })\n';
discoveryText += '```\n\n';
// Step 3: Query with example
discoveryText += '## Step 3: Execute Safe Queries\n\n';
discoveryText += 'After discovering tables and their structure, execute queries with TOP clause for safety:\n\n';
discoveryText += '```javascript\n';
discoveryText += `// Example query for a sample table\n`;
if (sampleTablesWithRowCounts.length > 0) {
discoveryText += `mcp__execute_query({ sql: "SELECT TOP 100 * FROM [${sampleTablesWithRowCounts[0].schema}].[${sampleTablesWithRowCounts[0].name}]" })\n`;
} else {
discoveryText += `mcp__execute_query({ sql: "SELECT TOP 100 * FROM [schema].[table_name]" })\n`;
}
discoveryText += '```\n\n';
// Sample information about tables
discoveryText += '## Sample Tables Information\n\n';
discoveryText += 'Here are some tables in this database with approximate row counts:\n\n';
discoveryText += '| Schema | Table Name | Approximate Row Count |\n';
discoveryText += '|--------|------------|----------------------|\n';
sampleTablesWithRowCounts.forEach(table => {
discoveryText += `| ${table.schema} | ${table.name} | ${table.rowCount} |\n`;
});
discoveryText += '\n## Total Tables Count\n\n';
discoveryText += `This database contains ${tablesResult.recordset.length} tables in total.\n\n`;
discoveryText += '## Best Practices for Table Discovery\n\n';
discoveryText += '1. Always start with listing available tables\n';
discoveryText += '2. Examine table structure before querying\n';
discoveryText += '3. Use TOP clauses for initial queries to avoid performance issues\n';
discoveryText += '4. For large tables, filter with WHERE clauses when possible\n';
logger.info('Table discovery guide generated');
return {
contents: [{
uri: uri.href,
text: discoveryText
}]
};
} catch (err) {
logger.error(`Error generating table discovery guide: ${err.message}`);
throw err;
}
}
);
}
/**
* Format schema data into human-readable text
* @param {Array} records - Records from INFORMATION_SCHEMA.COLUMNS
* @returns {string} - Formatted markdown
*/
function formatSchemaData(records) {
const tables = {};
// Group columns by table
records.forEach(record => {
if (!tables[record.TABLE_NAME]) {
tables[record.TABLE_NAME] = [];
}
tables[record.TABLE_NAME].push({
name: record.COLUMN_NAME,
type: record.DATA_TYPE,
length: record.CHARACTER_MAXIMUM_LENGTH,
nullable: record.IS_NULLABLE === 'YES',
default: record.COLUMN_DEFAULT
});
});
// Format as text
let output = '# Database Schema\n\n';
for (const [tableName, columns] of Object.entries(tables)) {
output += `## Table: ${tableName}\n\n`;
output += '| Column | Type | Length | Nullable | Default |\n';
output += '|--------|------|--------|----------|--------|\n';
columns.forEach(col => {
const length = col.length !== null ? col.length : 'N/A';
const defaultVal = col.default !== null ? col.default : 'N/A';
output += `| ${col.name} | ${col.type} | ${length} | ${col.nullable ? 'Yes' : 'No'} | ${defaultVal} |\n`;
});
output += '\n';
}
return output;
}