# Query Database Tool The MCP package provides a secure SQL query execution tool with validation, connection management, and EXPLAIN plan analysis. ## Overview The Query Database tool allows AI agents to: - Execute SELECT queries safely - Analyze query performance - Access multiple database connections - Prevent destructive operations - Enforce workspace context ## Basic Usage ```php use Core\Mcp\Tools\QueryDatabase; $tool = new QueryDatabase(); $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE status = ?', 'bindings' => ['published'], 'connection' => 'mysql', ]); // Returns: // [ // 'rows' => [...], // 'count' => 10, // 'execution_time_ms' => 5.23 // ] ``` ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `query` | string | Yes | SQL SELECT query | | `bindings` | array | No | Query parameters (prevents SQL injection) | | `connection` | string | No | Database connection name (default: default) | | `explain` | bool | No | Include EXPLAIN plan analysis | ## Security Validation ### Allowed Operations ✅ Only SELECT queries are allowed: ```php // ✅ Allowed 'SELECT * FROM posts' 'SELECT id, title FROM posts WHERE status = ?' 'SELECT COUNT(*) FROM users' // ❌ Blocked 'DELETE FROM posts' 'UPDATE posts SET status = ?' 'DROP TABLE posts' 'TRUNCATE posts' ``` ### Forbidden Keywords The following are automatically blocked: - `DROP` - `TRUNCATE` - `DELETE` - `UPDATE` - `INSERT` - `ALTER` - `CREATE` - `GRANT` - `REVOKE` ### Required WHERE Clauses Queries on large tables must include WHERE clauses: ```php // ✅ Good - has WHERE clause 'SELECT * FROM posts WHERE user_id = ?' // ⚠️ Warning - no WHERE clause 'SELECT * FROM posts' // Returns warning if table has > 10,000 rows ``` ### Connection Validation Only whitelisted connections are accessible: ```php // config/mcp.php 'query_database' => [ 'allowed_connections' => ['mysql', 'pgsql', 'analytics'], ], ``` ## EXPLAIN Plan Analysis Enable query optimization insights: ```php $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE status = ?', 'bindings' => ['published'], 'explain' => true, ]); // Returns additional 'explain' key: // [ // 'rows' => [...], // 'explain' => [ // 'type' => 'ref', // 'key' => 'idx_status', // 'rows_examined' => 150, // 'analysis' => 'Query uses index. Performance: Good', // 'recommendations' => [] // ] // ] ``` ### Performance Analysis The EXPLAIN analyzer provides human-readable insights: **Good Performance:** ``` "Query uses index. Performance: Good" ``` **Index Missing:** ``` "Warning: Full table scan detected. Consider adding an index on 'status'" ``` **High Row Count:** ``` "Warning: Query examines 50,000 rows. Consider adding WHERE clause to limit results" ``` ## Examples ### Basic SELECT ```php $result = $tool->execute([ 'query' => 'SELECT id, title, created_at FROM posts LIMIT 10', ]); foreach ($result['rows'] as $row) { echo "{$row['title']}\n"; } ``` ### With Parameters ```php $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE user_id = ? AND status = ?', 'bindings' => [42, 'published'], ]); ``` ### Aggregation ```php $result = $tool->execute([ 'query' => 'SELECT status, COUNT(*) as count FROM posts GROUP BY status', ]); // Returns: [ // ['status' => 'draft', 'count' => 15], // ['status' => 'published', 'count' => 42], // ] ``` ### Join Query ```php $result = $tool->execute([ 'query' => ' SELECT posts.title, users.name FROM posts JOIN users ON posts.user_id = users.id WHERE posts.status = ? LIMIT 10 ', 'bindings' => ['published'], ]); ``` ### Date Filtering ```php $result = $tool->execute([ 'query' => ' SELECT * FROM posts WHERE created_at >= ? AND created_at < ? ORDER BY created_at DESC ', 'bindings' => ['2024-01-01', '2024-02-01'], ]); ``` ## Multiple Connections Query different databases: ```php // Main application database $posts = $tool->execute([ 'query' => 'SELECT * FROM posts', 'connection' => 'mysql', ]); // Analytics database $stats = $tool->execute([ 'query' => 'SELECT * FROM page_views', 'connection' => 'analytics', ]); // PostgreSQL database $data = $tool->execute([ 'query' => 'SELECT * FROM logs', 'connection' => 'pgsql', ]); ``` ## Error Handling ### Forbidden Query ```php $result = $tool->execute([ 'query' => 'DELETE FROM posts WHERE id = 1', ]); // Returns: // [ // 'success' => false, // 'error' => 'Forbidden query: DELETE operations not allowed', // 'code' => 'FORBIDDEN_QUERY' // ] ``` ### Invalid Connection ```php $result = $tool->execute([ 'query' => 'SELECT * FROM posts', 'connection' => 'unknown', ]); // Returns: // [ // 'success' => false, // 'error' => 'Connection "unknown" not allowed', // 'code' => 'INVALID_CONNECTION' // ] ``` ### SQL Error ```php $result = $tool->execute([ 'query' => 'SELECT * FROM nonexistent_table', ]); // Returns: // [ // 'success' => false, // 'error' => 'Table "nonexistent_table" doesn\'t exist', // 'code' => 'SQL_ERROR' // ] ``` ## Configuration ```php // config/mcp.php 'query_database' => [ // Allowed database connections 'allowed_connections' => [ 'mysql', 'pgsql', 'analytics', ], // Forbidden SQL keywords 'forbidden_keywords' => [ 'DROP', 'TRUNCATE', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'CREATE', 'GRANT', 'REVOKE', ], // Maximum execution time (milliseconds) 'max_execution_time' => 5000, // Enable EXPLAIN plan analysis 'enable_explain' => true, // Warn on queries without WHERE clause for tables larger than: 'warn_no_where_threshold' => 10000, ], ``` ## Workspace Context Queries are automatically scoped to the current workspace: ```php // When workspace context is set $result = $tool->execute([ 'query' => 'SELECT * FROM posts', ]); // Equivalent to: // 'SELECT * FROM posts WHERE workspace_id = ?' // with workspace_id automatically added ``` Disable automatic scoping: ```php $result = $tool->execute([ 'query' => 'SELECT * FROM global_settings', 'ignore_workspace_scope' => true, ]); ``` ## Best Practices ### 1. Always Use Bindings ```php // ✅ Good - prevents SQL injection $tool->execute([ 'query' => 'SELECT * FROM posts WHERE user_id = ?', 'bindings' => [$userId], ]); // ❌ Bad - vulnerable to SQL injection $tool->execute([ 'query' => "SELECT * FROM posts WHERE user_id = {$userId}", ]); ``` ### 2. Limit Results ```php // ✅ Good - limits results 'SELECT * FROM posts LIMIT 100' // ❌ Bad - could return millions of rows 'SELECT * FROM posts' ``` ### 3. Use EXPLAIN for Optimization ```php // ✅ Good - analyze slow queries $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE status = ?', 'bindings' => ['published'], 'explain' => true, ]); if (isset($result['explain']['recommendations'])) { foreach ($result['explain']['recommendations'] as $rec) { error_log("Query optimization: {$rec}"); } } ``` ### 4. Handle Errors Gracefully ```php // ✅ Good - check for errors $result = $tool->execute([...]); if (!($result['success'] ?? true)) { return [ 'error' => $result['error'], 'code' => $result['code'], ]; } return $result['rows']; ``` ## Testing ```php create(['title' => 'Test Post']); $tool = new QueryDatabase(); $result = $tool->execute([ 'query' => 'SELECT * FROM posts WHERE title = ?', 'bindings' => ['Test Post'], ]); $this->assertTrue($result['success'] ?? true); $this->assertCount(1, $result['rows']); } public function test_blocks_delete_query(): void { $tool = new QueryDatabase(); $result = $tool->execute([ 'query' => 'DELETE FROM posts WHERE id = 1', ]); $this->assertFalse($result['success']); $this->assertEquals('FORBIDDEN_QUERY', $result['code']); } public function test_validates_connection(): void { $tool = new QueryDatabase(); $result = $tool->execute([ 'query' => 'SELECT 1', 'connection' => 'invalid', ]); $this->assertFalse($result['success']); $this->assertEquals('INVALID_CONNECTION', $result['code']); } } ``` ## Learn More - [SQL Security →](/packages/mcp/security) - [Workspace Context →](/packages/mcp/workspace) - [Tool Analytics →](/packages/mcp/analytics)