Skip to content

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

ParameterTypeRequiredDescription
querystringYesSQL SELECT query
bindingsarrayNoQuery parameters (prevents SQL injection)
connectionstringNoDatabase connection name (default: default)
explainboolNoInclude 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
<?php

namespace Tests\Feature\Mcp;

use Tests\TestCase;
use Core\Mcp\Tools\QueryDatabase;

class QueryDatabaseTest extends TestCase
{
    public function test_executes_select_query(): void
    {
        Post::factory()->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

Released under the EUPL-1.2 License.