1use crate::error::{Error, Result};
4use crate::mcp::protocol::{CallToolResult, Tool};
5use crate::tap_integration::TapIntegration;
6use crate::tools::{error_text_response, success_text_response, ToolHandler};
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10use sqlx::{Column, Connection, Row, SqliteConnection};
11use std::sync::Arc;
12use tracing::{debug, error};
13
14pub struct QueryDatabaseTool {
16 tap_integration: Arc<TapIntegration>,
17}
18
19#[derive(Debug, Deserialize)]
21struct QueryDatabaseParams {
22 agent_did: String,
23 query: String,
24}
25
26#[derive(Debug, Serialize)]
28struct QueryDatabaseResponse {
29 columns: Vec<String>,
30 rows: Vec<Vec<Value>>,
31 row_count: usize,
32 query: String,
33}
34
35impl QueryDatabaseTool {
36 pub fn new(tap_integration: Arc<TapIntegration>) -> Self {
37 Self { tap_integration }
38 }
39
40 fn validate_table_name(name: &str) -> bool {
42 !name.is_empty()
43 && name.len() <= 128
44 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
45 && name
46 .chars()
47 .next()
48 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
49 }
50
51 fn is_read_only_query(query: &str) -> bool {
53 let query_upper = query.trim().to_uppercase();
54 let forbidden_keywords = [
55 "INSERT",
56 "UPDATE",
57 "DELETE",
58 "DROP",
59 "CREATE",
60 "ALTER",
61 "TRUNCATE",
62 "REPLACE",
63 "MERGE",
64 "CALL",
65 "EXECUTE",
66 "EXEC",
67 "BEGIN",
68 "COMMIT",
69 "ROLLBACK",
70 "SAVEPOINT",
71 "GRANT",
72 "REVOKE",
73 "DENY",
74 "ATTACH",
75 "DETACH",
76 ];
77
78 let allowed_starts = ["SELECT", "WITH", "PRAGMA", "EXPLAIN"];
80 let starts_with_allowed = allowed_starts
81 .iter()
82 .any(|&start| query_upper.starts_with(start));
83
84 let contains_forbidden = forbidden_keywords.iter().any(|&keyword| {
86 query_upper.split_whitespace().any(|word| word == keyword)
88 });
89
90 starts_with_allowed && !contains_forbidden
91 }
92}
93
94#[async_trait]
95impl ToolHandler for QueryDatabaseTool {
96 async fn handle(&self, arguments: Option<Value>) -> Result<CallToolResult> {
97 let params: QueryDatabaseParams = match arguments {
98 Some(args) => serde_json::from_value(args)
99 .map_err(|e| Error::invalid_parameter(format!("Invalid parameters: {}", e)))?,
100 None => {
101 return Ok(error_text_response(
102 "Missing required parameters".to_string(),
103 ))
104 }
105 };
106
107 debug!(
108 "Executing query for agent {}: {}",
109 params.agent_did, params.query
110 );
111
112 if !Self::is_read_only_query(¶ms.query) {
114 return Ok(error_text_response(
115 "Only read-only queries are allowed. Query must start with SELECT, WITH, PRAGMA, or EXPLAIN and cannot contain modification keywords.".to_string(),
116 ));
117 }
118
119 let storage = match self
121 .tap_integration
122 .storage_for_agent(¶ms.agent_did)
123 .await
124 {
125 Ok(storage) => storage,
126 Err(e) => {
127 error!(
128 "Failed to get storage for agent {}: {}",
129 params.agent_did, e
130 );
131 return Ok(error_text_response(format!(
132 "Failed to get storage for agent {}: {}",
133 params.agent_did, e
134 )));
135 }
136 };
137
138 let db_path = storage.db_path();
140 let db_url = format!("sqlite://{}?mode=ro", db_path.display());
141
142 let mut conn = match SqliteConnection::connect(&db_url).await {
144 Ok(conn) => conn,
145 Err(e) => {
146 error!("Failed to connect to database: {}", e);
147 return Ok(error_text_response(format!(
148 "Failed to connect to database: {}",
149 e
150 )));
151 }
152 };
153
154 if let Err(e) = sqlx::query("PRAGMA query_only = ON")
156 .execute(&mut conn)
157 .await
158 {
159 error!("Failed to set query_only pragma: {}", e);
160 return Ok(error_text_response(format!(
161 "Failed to set read-only mode: {}",
162 e
163 )));
164 }
165
166 match sqlx::query(¶ms.query).fetch_all(&mut conn).await {
168 Ok(rows) => {
169 let mut columns = Vec::new();
170 let mut result_rows = Vec::new();
171
172 if !rows.is_empty() {
173 let first_row = &rows[0];
175 for column in first_row.columns().iter() {
176 columns.push(column.name().to_string());
177 }
178
179 for row in &rows {
181 let mut row_values = Vec::new();
182 for i in 0..columns.len() {
183 let value = if let Ok(v) = row.try_get::<Option<i64>, _>(i) {
185 v.map(Value::from).unwrap_or(Value::Null)
186 } else if let Ok(v) = row.try_get::<Option<f64>, _>(i) {
187 v.map(Value::from).unwrap_or(Value::Null)
188 } else if let Ok(v) = row.try_get::<Option<String>, _>(i) {
189 v.map(Value::from).unwrap_or(Value::Null)
190 } else if let Ok(v) = row.try_get::<Option<bool>, _>(i) {
191 v.map(Value::from).unwrap_or(Value::Null)
192 } else if let Ok(v) = row.try_get::<Option<Vec<u8>>, _>(i) {
193 v.map(|bytes| {
194 if let Ok(s) = String::from_utf8(bytes.clone()) {
196 Value::String(s)
197 } else {
198 use base64::Engine;
200 Value::String(
201 base64::engine::general_purpose::STANDARD.encode(bytes),
202 )
203 }
204 })
205 .unwrap_or(Value::Null)
206 } else {
207 Value::Null
208 };
209 row_values.push(value);
210 }
211 result_rows.push(row_values);
212 }
213 }
214
215 let response = QueryDatabaseResponse {
216 columns,
217 row_count: result_rows.len(),
218 rows: result_rows,
219 query: params.query,
220 };
221
222 let response_json = serde_json::to_string_pretty(&response).map_err(|e| {
223 Error::tool_execution(format!("Failed to serialize response: {}", e))
224 })?;
225
226 Ok(success_text_response(response_json))
227 }
228 Err(e) => {
229 error!("Failed to execute query: {}", e);
230 Ok(error_text_response(format!(
231 "Failed to execute query: {}",
232 e
233 )))
234 }
235 }
236 }
237
238 fn get_definition(&self) -> Tool {
239 Tool {
240 name: "tap_query_database".to_string(),
241 description: "Executes read-only SQL queries on an agent's database. Only SELECT, WITH, PRAGMA, and EXPLAIN queries are allowed.".to_string(),
242 input_schema: json!({
243 "type": "object",
244 "properties": {
245 "agent_did": {
246 "type": "string",
247 "description": "The DID of the agent whose database to query"
248 },
249 "query": {
250 "type": "string",
251 "description": "The read-only SQL query to execute"
252 }
253 },
254 "required": ["agent_did", "query"],
255 "additionalProperties": false
256 }),
257 }
258 }
259}
260
261pub struct GetDatabaseSchemaTool {
263 tap_integration: Arc<TapIntegration>,
264}
265
266#[derive(Debug, Deserialize)]
268struct GetDatabaseSchemaParams {
269 agent_did: String,
270 #[serde(default)]
271 table_name: Option<String>,
272}
273
274#[derive(Debug, Serialize)]
276struct TableInfo {
277 name: String,
278 columns: Vec<ColumnInfo>,
279 indexes: Vec<IndexInfo>,
280 row_count: i64,
281}
282
283#[derive(Debug, Serialize)]
285struct ColumnInfo {
286 cid: i32,
287 name: String,
288 #[serde(rename = "type")]
289 column_type: String,
290 notnull: bool,
291 dflt_value: Option<String>,
292 pk: bool,
293}
294
295#[derive(Debug, Serialize)]
297struct IndexInfo {
298 name: String,
299 unique: bool,
300 origin: String,
301 partial: bool,
302}
303
304#[derive(Debug, Serialize)]
306struct GetDatabaseSchemaResponse {
307 database_path: String,
308 tables: Vec<TableInfo>,
309}
310
311impl GetDatabaseSchemaTool {
312 pub fn new(tap_integration: Arc<TapIntegration>) -> Self {
313 Self { tap_integration }
314 }
315}
316
317#[async_trait]
318impl ToolHandler for GetDatabaseSchemaTool {
319 async fn handle(&self, arguments: Option<Value>) -> Result<CallToolResult> {
320 let params: GetDatabaseSchemaParams = match arguments {
321 Some(args) => serde_json::from_value(args)
322 .map_err(|e| Error::invalid_parameter(format!("Invalid parameters: {}", e)))?,
323 None => {
324 return Ok(error_text_response(
325 "Missing required parameters".to_string(),
326 ))
327 }
328 };
329
330 debug!("Getting database schema for agent {}", params.agent_did);
331
332 let storage = match self
334 .tap_integration
335 .storage_for_agent(¶ms.agent_did)
336 .await
337 {
338 Ok(storage) => storage,
339 Err(e) => {
340 error!(
341 "Failed to get storage for agent {}: {}",
342 params.agent_did, e
343 );
344 return Ok(error_text_response(format!(
345 "Failed to get storage for agent {}: {}",
346 params.agent_did, e
347 )));
348 }
349 };
350
351 let db_path = storage.db_path();
353 let db_url = format!("sqlite://{}?mode=ro", db_path.display());
354
355 let mut conn = match SqliteConnection::connect(&db_url).await {
357 Ok(conn) => conn,
358 Err(e) => {
359 error!("Failed to connect to database: {}", e);
360 return Ok(error_text_response(format!(
361 "Failed to connect to database: {}",
362 e
363 )));
364 }
365 };
366
367 let mut tables = Vec::new();
368
369 if let Some(ref table_name) = params.table_name {
371 if !QueryDatabaseTool::validate_table_name(table_name) {
372 return Ok(error_text_response(
373 "Invalid table name. Table names must contain only alphanumeric characters and underscores.".to_string(),
374 ));
375 }
376 }
377
378 let table_rows = if let Some(ref table_name) = params.table_name {
380 match sqlx::query(
381 "SELECT name FROM sqlite_master WHERE type='table' AND name=?1 ORDER BY name",
382 )
383 .bind(table_name)
384 .fetch_all(&mut conn)
385 .await
386 {
387 Ok(rows) => rows,
388 Err(e) => {
389 error!("Failed to get tables: {}", e);
390 return Ok(error_text_response(format!("Failed to get tables: {}", e)));
391 }
392 }
393 } else {
394 match sqlx::query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
395 .fetch_all(&mut conn)
396 .await
397 {
398 Ok(rows) => rows,
399 Err(e) => {
400 error!("Failed to get tables: {}", e);
401 return Ok(error_text_response(format!("Failed to get tables: {}", e)));
402 }
403 }
404 };
405
406 for table_row in table_rows {
407 let table_name: String = table_row.try_get("name").unwrap_or_default();
408
409 if !QueryDatabaseTool::validate_table_name(&table_name) {
411 continue;
412 }
413
414 let column_query = format!("PRAGMA table_info('{}')", table_name);
416 let column_rows = match sqlx::query(&column_query).fetch_all(&mut conn).await {
417 Ok(rows) => rows,
418 Err(e) => {
419 error!("Failed to get columns for table {}: {}", table_name, e);
420 continue;
421 }
422 };
423
424 let mut columns = Vec::new();
425 for col_row in column_rows {
426 columns.push(ColumnInfo {
427 cid: col_row.try_get("cid").unwrap_or(0),
428 name: col_row.try_get("name").unwrap_or_default(),
429 column_type: col_row.try_get("type").unwrap_or_default(),
430 notnull: col_row.try_get::<i32, _>("notnull").unwrap_or(0) != 0,
431 dflt_value: col_row.try_get("dflt_value").ok(),
432 pk: col_row.try_get::<i32, _>("pk").unwrap_or(0) != 0,
433 });
434 }
435
436 let index_query = format!("PRAGMA index_list('{}')", table_name);
438 let index_rows = match sqlx::query(&index_query).fetch_all(&mut conn).await {
439 Ok(rows) => rows,
440 Err(e) => {
441 error!("Failed to get indexes for table {}: {}", table_name, e);
442 vec![]
443 }
444 };
445
446 let mut indexes = Vec::new();
447 for idx_row in index_rows {
448 indexes.push(IndexInfo {
449 name: idx_row.try_get("name").unwrap_or_default(),
450 unique: idx_row.try_get::<i32, _>("unique").unwrap_or(0) != 0,
451 origin: idx_row.try_get("origin").unwrap_or_default(),
452 partial: idx_row.try_get::<i32, _>("partial").unwrap_or(0) != 0,
453 });
454 }
455
456 let count_query = format!("SELECT COUNT(*) as count FROM '{}'", table_name);
458 let row_count = match sqlx::query(&count_query).fetch_one(&mut conn).await {
459 Ok(row) => row.try_get::<i64, _>("count").unwrap_or(0),
460 Err(e) => {
461 error!("Failed to get row count for table {}: {}", table_name, e);
462 0
463 }
464 };
465
466 tables.push(TableInfo {
467 name: table_name,
468 columns,
469 indexes,
470 row_count,
471 });
472 }
473
474 let response = GetDatabaseSchemaResponse {
475 database_path: db_path.display().to_string(),
476 tables,
477 };
478
479 let response_json = serde_json::to_string_pretty(&response)
480 .map_err(|e| Error::tool_execution(format!("Failed to serialize response: {}", e)))?;
481
482 Ok(success_text_response(response_json))
483 }
484
485 fn get_definition(&self) -> Tool {
486 Tool {
487 name: "tap_get_database_schema".to_string(),
488 description: "Gets the schema of an agent's database, including all tables, columns, indexes, and row counts. Optionally filter by table name.".to_string(),
489 input_schema: json!({
490 "type": "object",
491 "properties": {
492 "agent_did": {
493 "type": "string",
494 "description": "The DID of the agent whose database schema to retrieve"
495 },
496 "table_name": {
497 "type": "string",
498 "description": "Optional specific table name to get schema for"
499 }
500 },
501 "required": ["agent_did"],
502 "additionalProperties": false
503 }),
504 }
505 }
506}