dbmcp_postgres/tools/
read_query.rs1use std::borrow::Cow;
4
5use dbmcp_pii::MaybeRedact as _;
6use dbmcp_server::pagination::{Cursor, Pager};
7use dbmcp_server::types::{PinnedReadQueryRequest, ReadQueryResponse, UnpinnedReadQueryRequest};
8use dbmcp_sql::Connection as _;
9use dbmcp_sql::StatementKind;
10use dbmcp_sql::pagination::with_limit_offset;
11use dbmcp_sql::validation::validate_read_only;
12use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
13use rmcp::model::{ErrorData, ToolAnnotations};
14
15use crate::PostgresHandler;
16
17const NAME: &str = "readQuery";
18const TITLE: &str = "Read Query";
19const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/read_query/pinned.md");
20const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/read_query/unpinned.md");
21
22fn annotations() -> ToolAnnotations {
23 ToolAnnotations::new()
24 .read_only(true)
25 .destructive(false)
26 .idempotent(true)
27 .open_world(true)
28}
29
30pub(crate) struct PinnedReadQueryTool;
32
33impl ToolBase for PinnedReadQueryTool {
34 type Parameter = PinnedReadQueryRequest;
35 type Output = ReadQueryResponse;
36 type Error = ErrorData;
37
38 fn name() -> Cow<'static, str> {
39 NAME.into()
40 }
41
42 fn title() -> Option<String> {
43 Some(TITLE.into())
44 }
45
46 fn description() -> Option<Cow<'static, str>> {
47 Some(DESCRIPTION_PINNED.into())
48 }
49
50 fn annotations() -> Option<ToolAnnotations> {
51 Some(annotations())
52 }
53}
54
55impl AsyncTool<PostgresHandler> for PinnedReadQueryTool {
56 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
57 handler.read_query(params.query, None, params.cursor).await
58 }
59}
60
61pub(crate) struct UnpinnedReadQueryTool;
63
64impl ToolBase for UnpinnedReadQueryTool {
65 type Parameter = UnpinnedReadQueryRequest;
66 type Output = ReadQueryResponse;
67 type Error = ErrorData;
68
69 fn name() -> Cow<'static, str> {
70 NAME.into()
71 }
72
73 fn title() -> Option<String> {
74 Some(TITLE.into())
75 }
76
77 fn description() -> Option<Cow<'static, str>> {
78 Some(DESCRIPTION_UNPINNED.into())
79 }
80
81 fn annotations() -> Option<ToolAnnotations> {
82 Some(annotations())
83 }
84}
85
86impl AsyncTool<PostgresHandler> for UnpinnedReadQueryTool {
87 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
88 handler
89 .read_query(params.inner.query, params.database, params.inner.cursor)
90 .await
91 }
92}
93
94impl PostgresHandler {
95 pub async fn read_query(
109 &self,
110 query: String,
111 database: Option<String>,
112 cursor: Option<Cursor>,
113 ) -> Result<ReadQueryResponse, ErrorData> {
114 let kind = validate_read_only(&query, &sqlparser::dialect::PostgreSqlDialect {})?;
115 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
116
117 let (rows, next_cursor) = match kind {
118 StatementKind::Select => {
119 let pager = Pager::new(cursor, self.config.page_size);
120 let wrapped = with_limit_offset(&query, pager.limit(), pager.offset());
121 let rows = self.connection.fetch_json(wrapped.as_str(), database).await?;
122 pager.paginate(rows)
123 }
124 StatementKind::NonSelect => {
125 let rows = self.connection.fetch_json(query.as_str(), database).await?;
126 (rows, None)
127 }
128 };
129 let rows = self.redactor.redact_rows(rows).await?;
130 Ok(ReadQueryResponse { rows, next_cursor })
131 }
132}