diff --git a/src/handlers/toolHandlers.ts b/src/handlers/toolHandlers.ts index 463bcc3..cdaf93a 100644 --- a/src/handlers/toolHandlers.ts +++ b/src/handlers/toolHandlers.ts @@ -5,13 +5,25 @@ import { readQuery, writeQuery, exportQuery } from '../tools/queryTools.js'; import { createTable, alterTable, dropTable, listTables, describeTable } from '../tools/schemaTools.js'; import { appendInsight, listInsights } from '../tools/insightTools.js'; +/** + * Tools that perform write operations on the database. + * These are hidden and blocked when the server runs in readonly mode. + */ +const WRITE_TOOLS: ReadonlySet = new Set([ + "write_query", + "create_table", + "alter_table", + "drop_table", + "append_insight", +]); + /** * Handle listing available tools + * @param readonly Whether the server is in readonly mode * @returns List of available tools */ -export function handleListTools() { - return { - tools: [ +export function handleListTools(readonly: boolean = false) { + const allTools = [ { name: "read_query", description: "Execute SELECT queries to read data from the database", @@ -118,18 +130,30 @@ export function handleListTools() { properties: {}, }, }, - ], - }; + ]; + + const tools = readonly + ? allTools.filter(tool => !WRITE_TOOLS.has(tool.name)) + : allTools; + + return { tools }; } /** * Handle tool call requests * @param name Name of the tool to call * @param args Arguments for the tool + * @param readonly Whether the server is in readonly mode * @returns Tool execution result */ -export async function handleToolCall(name: string, args: any) { +export async function handleToolCall(name: string, args: any, readonly: boolean = false) { try { + if (readonly && WRITE_TOOLS.has(name)) { + return formatErrorResponse( + `Tool "${name}" is disabled in readonly mode. The server was started with --readonly, which prevents all write operations.` + ); + } + switch (name) { case "read_query": return await readQuery(args.query); diff --git a/src/index.ts b/src/index.ts index 9bf7fda..c53bd65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,8 @@ const server = new Server( // Parse command line arguments const args = process.argv.slice(2); +const readonlyMode = args.includes('--readonly'); + if (args.length === 0) { logger.error("Please provide database connection information"); logger.error("Usage for SQLite: node index.js "); @@ -47,6 +49,7 @@ if (args.length === 0) { logger.error("Usage for PostgreSQL: node index.js --postgresql --host --database [--user --password --port ]"); logger.error("Usage for MySQL: node index.js --mysql --host --database [--user --password --port ]"); logger.error("Usage for MySQL with AWS IAM: node index.js --mysql --aws-iam-auth --host --database --user --aws-region "); + logger.error("Global options: [--readonly]"); process.exit(1); } @@ -185,7 +188,12 @@ else if (args.includes('--mysql')) { } else { // SQLite mode (default) dbType = 'sqlite'; - connectionInfo = args[0]; // First argument is the SQLite file path + const dbPath = args.find((arg: string) => !arg.startsWith('--')); + if (!dbPath) { + logger.error("Please provide a database file path for SQLite mode"); + process.exit(1); + } + connectionInfo = dbPath; logger.info(`Using SQLite database at path: ${connectionInfo}`); } @@ -199,11 +207,11 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { }); server.setRequestHandler(ListToolsRequestSchema, async () => { - return handleListTools(); + return handleListTools(readonlyMode); }); server.setRequestHandler(CallToolRequestSchema, async (request) => { - return await handleToolCall(request.params.name, request.params.arguments); + return await handleToolCall(request.params.name, request.params.arguments, readonlyMode); }); // Handle shutdown gracefully @@ -249,7 +257,11 @@ async function runServer() { const dbInfo = getDatabaseMetadata(); logger.info(`Connected to ${dbInfo.name} database`); - + + if (readonlyMode) { + logger.warn('READONLY MODE ENABLED: All write operations are disabled'); + } + logger.info('Starting MCP server...'); const transport = new StdioServerTransport(); await server.connect(transport);