InitalCommit
This commit is contained in:
247
mssql-mcp-readonly.js
Normal file
247
mssql-mcp-readonly.js
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env node
|
||||
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} = require("@modelcontextprotocol/sdk/types.js");
|
||||
const sql = require("mssql");
|
||||
|
||||
// Read DB config from environment variables
|
||||
const config = {
|
||||
server: process.env.MSSQL_SERVER,
|
||||
port: parseInt(process.env.MSSQL_PORT || "1433"),
|
||||
user: process.env.MSSQL_USER,
|
||||
password: process.env.MSSQL_PASSWORD,
|
||||
database: process.env.MSSQL_DATABASE,
|
||||
options: {
|
||||
encrypt: process.env.MSSQL_ENCRYPT === "true",
|
||||
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === "true"
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to allow only SELECT queries (strict read-only enforcement)
|
||||
function validateQuery(query) {
|
||||
const trimmed = query.trim().toUpperCase();
|
||||
|
||||
// Must start with SELECT
|
||||
if (!trimmed.startsWith("SELECT")) {
|
||||
throw new Error("Only SELECT queries are allowed!");
|
||||
}
|
||||
|
||||
// Block dangerous keywords that could modify data
|
||||
const dangerousKeywords = [
|
||||
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
|
||||
'TRUNCATE', 'EXEC', 'EXECUTE', 'SP_', 'XP_', 'MERGE',
|
||||
'GRANT', 'REVOKE', 'DENY'
|
||||
];
|
||||
|
||||
for (const keyword of dangerousKeywords) {
|
||||
if (trimmed.includes(keyword)) {
|
||||
throw new Error(`Forbidden keyword detected: ${keyword}. Only SELECT queries are allowed!`);
|
||||
}
|
||||
}
|
||||
|
||||
// Block semicolons (prevents query chaining like "SELECT 1; DROP TABLE")
|
||||
if (query.includes(';')) {
|
||||
throw new Error("Multiple statements not allowed. Only single SELECT queries permitted!");
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
async function runQuery(query) {
|
||||
validateQuery(query);
|
||||
const pool = await sql.connect(config);
|
||||
const result = await pool.request().query(query);
|
||||
await pool.close();
|
||||
return result.recordset;
|
||||
}
|
||||
|
||||
// Create MCP server instance
|
||||
const server = new Server(
|
||||
{
|
||||
name: "mssql-readonly-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "execute_sql",
|
||||
description: "Execute read-only SQL queries on MS SQL Server. Only SELECT queries are allowed. Use this to fetch data from the Infra database tables such as TEILE.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "SQL SELECT query to execute. Must start with SELECT. No INSERT, UPDATE, DELETE, or other write operations allowed.",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_tables",
|
||||
description: "List all available tables in the Infra database",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "describe_table",
|
||||
description: "Get the schema/structure of a specific table including column names, data types, and constraints",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
table_name: {
|
||||
type: "string",
|
||||
description: "Name of the table to describe (e.g., 'TEILE')",
|
||||
},
|
||||
},
|
||||
required: ["table_name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search_parts",
|
||||
description: "Search for parts in the TEILE table by term. Searches in Teil, Bez, Bez2, Hersteller, and other fields.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
search_term: {
|
||||
type: "string",
|
||||
description: "Term to search for (e.g., manufacturer name, part number, description)",
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Maximum number of results to return (default: 50)",
|
||||
default: 50,
|
||||
},
|
||||
},
|
||||
required: ["search_term"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool execution
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
switch (name) {
|
||||
case "execute_sql": {
|
||||
const { query } = args;
|
||||
const rows = await runQuery(query);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(rows, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_tables": {
|
||||
const listQuery = `
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME
|
||||
`;
|
||||
const rows = await runQuery(listQuery);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(rows, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "describe_table": {
|
||||
const { table_name } = args;
|
||||
const describeQuery = `
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
CHARACTER_MAXIMUM_LENGTH,
|
||||
IS_NULLABLE,
|
||||
COLUMN_DEFAULT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = '${table_name.replace(/'/g, "''")}'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
`;
|
||||
const rows = await runQuery(describeQuery);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(rows, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_parts": {
|
||||
const { search_term, limit = 50 } = args;
|
||||
const searchQuery = `
|
||||
SELECT TOP ${parseInt(limit)}
|
||||
'TEILE' AS TableName, Teil, Art, Gruppe, Bez, Bez2, Hersteller, WarenNr
|
||||
FROM TEILE
|
||||
WHERE
|
||||
LOWER(CAST(Teil AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
|
||||
OR LOWER(CAST(Bez AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
|
||||
OR LOWER(CAST(Bez2 AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
|
||||
OR LOWER(CAST(Hersteller AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
|
||||
`;
|
||||
const rows = await runQuery(searchQuery);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(rows, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("MS SQL Read-only MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Server error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user