Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/handlers/toolHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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",
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 16 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ 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 <database_file_path>");
logger.error("Usage for SQL Server: node index.js --sqlserver --server <server> --database <database> [--user <user> --password <password>]");
logger.error("Usage for PostgreSQL: node index.js --postgresql --host <host> --database <database> [--user <user> --password <password> --port <port>]");
logger.error("Usage for MySQL: node index.js --mysql --host <host> --database <database> [--user <user> --password <password> --port <port>]");
logger.error("Usage for MySQL with AWS IAM: node index.js --mysql --aws-iam-auth --host <rds-endpoint> --database <database> --user <aws-username> --aws-region <region>");
logger.error("Global options: [--readonly]");
process.exit(1);
}

Expand Down Expand Up @@ -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}`);
}

Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down