Skip to content
Merged
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
11 changes: 10 additions & 1 deletion lib/core/mcp/mcp_query_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:querya_desktop/core/database/table_mutation_engine.dart';
import 'package:querya_desktop/core/mcp/mcp_access_store.dart';
import 'package:querya_desktop/core/mcp/mcp_redaction.dart';
import 'package:querya_desktop/core/mcp/mcp_sql_guard.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/erd/erd_catalog.dart';
Expand Down Expand Up @@ -293,9 +294,17 @@ class McpQueryService {
) async {
final row = await _readableConnection(connectionId);
final dialect = dialectOf(row.type)!;
final delegate = _createDelegate(row, dialect);
final SqlExecutionDelegate delegate;
try {
delegate = _createDelegate(row, dialect);
} catch (e) {
throw McpToolException(McpRedaction.redact('$e', row: row));
}
try {
return await body(delegate, dialect);
} on McpToolException catch (e) {
// Driver errors may quote connection strings or options.
throw McpToolException(McpRedaction.redact(e.message, row: row));
} finally {
delegate.dispose();
}
Expand Down
49 changes: 49 additions & 0 deletions lib/core/mcp/mcp_redaction.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:querya_desktop/core/storage/local_db.dart';

/// Removes credentials from text that goes back to an MCP client.
///
/// Driver errors can quote a connection string or an option list; the model
/// must never see them. Removes the connection's own secrets when they are
/// loaded, then any `scheme://user:password@` user info and
/// `password=...`-style options.
abstract final class McpRedaction {
static const mask = '***';

static final _uriUserInfo =
RegExp(r'([a-zA-Z][a-zA-Z0-9+.\-]*://)[^/@\s]*@');
static final _keyValue = RegExp(
r'\b(password|passwd|pwd|passphrase|secret|token|api[_-]?key)(\s*[=:]\s*)'
r'''("[^"]*"|'[^']*'|[^\s,;&)]+)''',
caseSensitive: false,
);
static final _pemBlock = RegExp(
r'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(-----END [A-Z0-9 ]*PRIVATE KEY-----|$)',
);

static String redact(String text, {ConnectionRow? row}) {
var out = text;
for (final secret in _secretsOf(row)) {
out = out.replaceAll(secret, mask);
}
out = out.replaceAll(_pemBlock, '[private key removed]');
out = out.replaceAllMapped(_uriUserInfo, (m) => '${m[1]}$mask@');
out = out.replaceAllMapped(_keyValue, (m) => '${m[1]}${m[2]}$mask');
return out;
}

static Iterable<String> _secretsOf(ConnectionRow? row) sync* {
if (row == null) return;
final ssh = row.sshSecrets;
for (final s in [
row.connectionString,
row.password,
ssh?.password,
ssh?.passphrase,
ssh?.jumpPassword,
ssh?.privateKey,
]) {
// Very short values would mask ordinary words.
if (s != null && s.length >= 4) yield s;
}
}
}
6 changes: 6 additions & 0 deletions lib/core/mcp/mcp_sql_guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ abstract final class McpSqlGuard {
return 'Only one statement per call is allowed; send them separately.';
}
final statement = statements.single;
// MySQL / MariaDB execute the body of `/*! ... */` and `/*M! ... */`
// comments, which the comment stripper below would hide from the checks.
if (dialect == SqlDialect.mysql &&
RegExp(r'/\*M?!').hasMatch(statement)) {
return 'MySQL executable comments (/*! ... */) are not allowed.';
}
final upper = DestructiveSqlDetector.stripCommentsAndStrings(statement)
.trim()
.replaceAll(RegExp(r';\s*$'), '')
Expand Down
3 changes: 2 additions & 1 deletion lib/core/mcp/querya_mcp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';

import 'package:dart_mcp/server.dart';
import 'package:querya_desktop/core/mcp/mcp_query_service.dart';
import 'package:querya_desktop/core/mcp/mcp_redaction.dart';

/// One finished tool call, for the activity log.
class McpCallRecord {
Expand Down Expand Up @@ -217,7 +218,7 @@ base class QueryaMcpServer extends MCPServer with ToolsSupport, ResourcesSupport
return CallToolResult(
isError: true, content: [TextContent(text: e.message)]);
} catch (e) {
final message = 'Internal error: $e';
final message = McpRedaction.redact('Internal error: $e');
_report(tool, started, sw.elapsed, connectionId, rawSql, null, message);
return CallToolResult(
isError: true, content: [TextContent(text: message)]);
Expand Down
Loading
Loading