UNPKG

mcp-turso-cloud

Version:

MCP server for integrating Turso with LLMs

446 lines (445 loc) 19.4 kB
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import * as database_client from '../clients/database.js'; import * as organization_client from '../clients/organization.js'; import { resolve_database_name, set_current_database, } from './context.js'; /** * Register all tools with the server */ export function register_tools(server) { // Register the list of available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Organization tools { name: 'list_databases', description: 'List all databases in your Turso organization', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'create_database', description: `✓ SAFE: Create a new database in your Turso organization. Database name must be unique.`, inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the database to create - Must be unique within organization', }, group: { type: 'string', description: 'Optional group name for the database (defaults to "default")', }, regions: { type: 'array', items: { type: 'string', }, description: 'Optional list of regions to deploy the database to (affects latency and compliance)', }, }, required: ['name'], }, }, { name: 'delete_database', description: `⚠️ DESTRUCTIVE: Permanently deletes a database and ALL its data. Cannot be undone. Always confirm with user before proceeding and verify correct database name.`, inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the database to permanently delete - WARNING: ALL DATA WILL BE LOST FOREVER', }, }, required: ['name'], }, }, { name: 'generate_database_token', description: 'Generate a new token for a specific database', inputSchema: { type: 'object', properties: { database: { type: 'string', description: 'Name of the database to generate a token for', }, permission: { type: 'string', enum: ['full-access', 'read-only'], description: 'Permission level for the token', }, }, required: ['database'], }, }, // Database tools { name: 'list_tables', description: 'Lists all tables in a database', inputSchema: { type: 'object', properties: { database: { type: 'string', description: 'Database name (optional, uses context if not provided)', }, }, required: [], }, }, { name: 'execute_read_only_query', description: `✓ SAFE: Execute read-only SQL queries (SELECT, PRAGMA, EXPLAIN). Automatically rejects write operations.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Read-only SQL query to execute (SELECT, PRAGMA, EXPLAIN only)', }, params: { type: 'object', description: 'Query parameters (optional) - Use parameterized queries for security', }, database: { type: 'string', description: 'Database name (optional, uses context if not provided) - Specify target database', }, }, required: ['query'], }, }, { name: 'execute_query', description: `⚠️ DESTRUCTIVE: Execute SQL that can modify/delete data (INSERT, UPDATE, DELETE, DROP, ALTER). Always confirm with user before destructive operations.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'SQL query to execute - WARNING: Can permanently modify or delete data. Use with extreme caution.', }, params: { type: 'object', description: 'Query parameters (optional) - Use parameterized queries to prevent SQL injection', }, database: { type: 'string', description: 'Database name (optional, uses context if not provided) - Verify correct database before destructive operations', }, }, required: ['query'], }, }, { name: 'describe_table', description: 'Gets schema information for a table', inputSchema: { type: 'object', properties: { table: { type: 'string', description: 'Table name', }, database: { type: 'string', description: 'Database name (optional, uses context if not provided)', }, }, required: ['table'], }, }, { name: 'vector_search', description: 'Performs vector similarity search', inputSchema: { type: 'object', properties: { table: { type: 'string', description: 'Table name', }, vector_column: { type: 'string', description: 'Column containing vectors', }, query_vector: { type: 'array', items: { type: 'number', }, description: 'Query vector for similarity search', }, limit: { type: 'number', description: 'Maximum number of results (optional, default 10)', }, database: { type: 'string', description: 'Database name (optional, uses context if not provided)', }, }, required: ['table', 'vector_column', 'query_vector'], }, }, ], })); // Register the unified tool handler server.setRequestHandler(CallToolRequestSchema, async (request) => { try { // Organization tools // Handle list_databases tool if (request.params.name === 'list_databases') { const databases = await organization_client.list_databases(); return { content: [ { type: 'text', text: JSON.stringify({ databases }, null, 2), }, ], }; } // Handle create_database tool if (request.params.name === 'create_database') { const { name, group, regions } = request.params.arguments; const database = await organization_client.create_database(name, { group, regions, }); return { content: [ { type: 'text', text: JSON.stringify({ database }, null, 2), }, ], }; } // Handle delete_database tool if (request.params.name === 'delete_database') { const { name } = request.params.arguments; await organization_client.delete_database(name); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Database '${name}' deleted successfully`, }, null, 2), }, ], }; } // Handle generate_database_token tool if (request.params.name === 'generate_database_token') { const { database, permission = 'full-access' } = request .params.arguments; const jwt = await organization_client.generate_database_token(database, permission); return { content: [ { type: 'text', text: JSON.stringify({ success: true, database, token: { jwt, permission, database, }, message: `Token generated successfully for database '${database}' with '${permission}' permissions`, }, null, 2), }, ], }; } // Database tools // Handle list_tables tool if (request.params.name === 'list_tables') { const { database } = request.params.arguments; const database_name = resolve_database_name(database); // Update context if database is explicitly provided if (database) { set_current_database(database); } const tables = await database_client.list_tables(database_name); return { content: [ { type: 'text', text: JSON.stringify({ database: database_name, tables, }, null, 2), }, ], }; } // Handle execute_read_only_query tool if (request.params.name === 'execute_read_only_query') { const { query, params = {}, database, } = request.params.arguments; // Validate that this is a read-only query const normalized_query = query.trim().toLowerCase(); if (!normalized_query.startsWith('select') && !normalized_query.startsWith('pragma')) { throw new Error('Only SELECT and PRAGMA queries are allowed with execute_read_only_query'); } const database_name = resolve_database_name(database); // Update context if database is explicitly provided if (database) { set_current_database(database); } const result = await database_client.execute_query(database_name, query, params); // Format the result for better readability const formatted_result = format_query_result(result); return { content: [ { type: 'text', text: JSON.stringify({ database: database_name, query, result: formatted_result, }, null, 2), }, ], }; } // Handle execute_query tool if (request.params.name === 'execute_query') { const { query, params = {}, database, } = request.params.arguments; // Validate that this is not a read-only query const normalized_query = query.trim().toLowerCase(); if (normalized_query.startsWith('select') || normalized_query.startsWith('pragma')) { throw new Error('SELECT and PRAGMA queries should use execute_read_only_query'); } const database_name = resolve_database_name(database); // Update context if database is explicitly provided if (database) { set_current_database(database); } const result = await database_client.execute_query(database_name, query, params); // Format the result for better readability const formatted_result = format_query_result(result); return { content: [ { type: 'text', text: JSON.stringify({ database: database_name, query, result: formatted_result, }, null, 2), }, ], }; } // Handle describe_table tool if (request.params.name === 'describe_table') { const { table, database } = request.params.arguments; const database_name = resolve_database_name(database); // Update context if database is explicitly provided if (database) { set_current_database(database); } const columns = await database_client.describe_table(database_name, table); return { content: [ { type: 'text', text: JSON.stringify({ database: database_name, table, columns: columns.map((col) => ({ name: col.name, type: col.type, nullable: col.notnull === 0, default_value: col.dflt_value, primary_key: col.pk === 1, })), }, null, 2), }, ], }; } // Handle vector_search tool if (request.params.name === 'vector_search') { const { table, vector_column, query_vector, limit = 10, database, } = request.params.arguments; const database_name = resolve_database_name(database); // Update context if database is explicitly provided if (database) { set_current_database(database); } // Construct a vector search query using SQLite's vector functions // This assumes the vector column is stored in a format compatible with SQLite's vector1 extension const vector_string = query_vector.join(','); const query = ` SELECT *, vector_distance(${vector_column}, vector_from_json(?)) as distance FROM ${table} ORDER BY distance ASC LIMIT ? `; const params = { 1: `[${vector_string}]`, 2: limit, }; const result = await database_client.execute_query(database_name, query, params); // Format the result for better readability const formatted_result = format_query_result(result); return { content: [ { type: 'text', text: JSON.stringify({ database: database_name, table, vector_column, query_vector, results: formatted_result, }, null, 2), }, ], }; } // If we get here, it's not a recognized tool throw new Error(`Unknown tool: ${request.params.name}`); } catch (error) { console.error('Error executing tool:', error); return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, }; } }); } /** * Format a query result for better readability * Handles BigInt serialization */ function format_query_result(result) { // Convert BigInt to string to avoid serialization issues const lastInsertRowid = result.lastInsertRowid !== null && typeof result.lastInsertRowid === 'bigint' ? result.lastInsertRowid.toString() : result.lastInsertRowid; return { rows: result.rows, rowsAffected: result.rowsAffected, lastInsertRowid: lastInsertRowid, columns: result.columns, }; }