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