InitalCommit

This commit is contained in:
2026-02-17 16:00:34 +01:00
commit ce89fccdb5
20 changed files with 6296 additions and 0 deletions

247
mssql-mcp-readonly.js Normal file
View 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);
});