mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(mcp): include saved notes in connection listings
This commit is contained in:
@@ -35,6 +35,7 @@ pub struct ConnectionSummary {
|
||||
pub port: u16,
|
||||
pub database: String,
|
||||
pub group_path: Vec<String>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
impl From<&ConnectionConfig> for ConnectionSummary {
|
||||
@@ -51,6 +52,7 @@ impl From<&ConnectionConfig> for ConnectionSummary {
|
||||
port: config.port,
|
||||
database: config.database.clone().unwrap_or_default(),
|
||||
group_path: Vec::new(),
|
||||
note: config.note.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2967,6 +2969,15 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_summary_preserves_notes() {
|
||||
let mut config = mutation_test_connection();
|
||||
for note in ["", "Application database | staging\nRead-only queries"] {
|
||||
config.note = note.to_string();
|
||||
assert_eq!(ConnectionSummary::from(&config).note, note);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_connection_mutations_notify_desktop_only_after_success() {
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1178,7 +1178,7 @@ impl DbxMcpServer {
|
||||
|
||||
#[tool(
|
||||
name = "dbx_list_connections",
|
||||
description = "List database connections configured in DBX. Returns connection IDs, names, group paths, database types, endpoints, and selected databases."
|
||||
description = "List database connections configured in DBX. Returns connection IDs, names, group paths, database types, endpoints, selected databases, and saved connection notes."
|
||||
)]
|
||||
async fn list_connections(
|
||||
&self,
|
||||
@@ -3774,7 +3774,9 @@ impl ServerHandler for DbxMcpServer {
|
||||
.then(|| {
|
||||
Resource::new(CONNECTIONS_RESOURCE_URI, "dbx_connections")
|
||||
.with_title("DBX connections")
|
||||
.with_description("Database connections visible to the current DBX MCP scope")
|
||||
.with_description(
|
||||
"Database connections visible to the current DBX MCP scope, including saved notes",
|
||||
)
|
||||
.with_mime_type("text/markdown")
|
||||
})
|
||||
.into_iter()
|
||||
@@ -5011,11 +5013,11 @@ fn ambiguous_connections(name: &str, connections: &[dbx_core::models::connection
|
||||
|
||||
fn format_connections(connections: &[ConnectionSummary]) -> String {
|
||||
let mut output = String::from(
|
||||
"| ID | Name | Group Path | Type | Host | Port | Database |\n| --- | --- | --- | --- | --- | --- | --- |",
|
||||
"| ID | Name | Group Path | Type | Host | Port | Database | Note |\n| --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
);
|
||||
for connection in connections {
|
||||
output.push_str(&format!(
|
||||
"\n| {} | {} | {} | {} | {} | {} | {} |",
|
||||
"\n| {} | {} | {} | {} | {} | {} | {} | {} |",
|
||||
escape_cell(&connection.id),
|
||||
escape_cell(&connection.name),
|
||||
escape_cell(&connection.group_path.join(" / ")),
|
||||
@@ -5023,6 +5025,7 @@ fn format_connections(connections: &[ConnectionSummary]) -> String {
|
||||
escape_cell(&connection.host),
|
||||
connection.port,
|
||||
escape_cell(&connection.database),
|
||||
escape_cell(&connection.note),
|
||||
));
|
||||
}
|
||||
output
|
||||
@@ -6261,10 +6264,14 @@ mod tests {
|
||||
port: 5432,
|
||||
database: "app".to_string(),
|
||||
group_path: vec!["Project|A".to_string(), "Staging\nWest".to_string()],
|
||||
note: "Application | staging\nRead-only queries".to_string(),
|
||||
}]);
|
||||
assert!(output.contains("id\\|1"));
|
||||
assert!(output.contains("local pg"));
|
||||
assert!(output.contains("Project\\|A / Staging West"));
|
||||
assert!(output.contains("| Database | Note |"));
|
||||
assert!(output.contains("| Application \\| staging Read-only queries |"));
|
||||
assert_eq!(output.lines().count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -572,6 +572,10 @@ async fn remove_connection_respects_global_connection_scope() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn enforces_global_connection_scope_and_read_only_policy() {
|
||||
let mut allowed = test_connection("allowed", "shared-db");
|
||||
allowed.note = "Allowed connection note".to_string();
|
||||
let mut blocked_connection = test_connection("blocked", "blocked-db");
|
||||
blocked_connection.note = "Hidden connection note".to_string();
|
||||
let backend = PolicyBackend {
|
||||
policy: McpGlobalPolicy {
|
||||
read_only: true,
|
||||
@@ -579,11 +583,7 @@ async fn enforces_global_connection_scope_and_read_only_policy() {
|
||||
allowed_connection_ids: Some(vec!["allowed".to_string(), "allowed-staging".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
connections: vec![
|
||||
test_connection("allowed", "shared-db"),
|
||||
test_connection("allowed-staging", "shared-db"),
|
||||
test_connection("blocked", "blocked-db"),
|
||||
],
|
||||
connections: vec![allowed, test_connection("allowed-staging", "shared-db"), blocked_connection],
|
||||
group_paths: Ok(HashMap::from([
|
||||
("allowed".to_string(), vec!["Project".to_string(), "Production".to_string()]),
|
||||
("allowed-staging".to_string(), vec!["Project".to_string(), "Staging".to_string()]),
|
||||
@@ -603,6 +603,18 @@ async fn enforces_global_connection_scope_and_read_only_policy() {
|
||||
assert!(listed_text.contains("Project / Production"));
|
||||
assert!(listed_text.contains("Project / Staging"));
|
||||
assert!(!listed_text.contains("Secret"));
|
||||
assert!(listed_text.contains("Allowed connection note"));
|
||||
assert!(!listed_text.contains("Hidden connection note"));
|
||||
|
||||
let resource = client
|
||||
.peer()
|
||||
.read_resource(ReadResourceRequestParams::new("dbx://connections"))
|
||||
.await
|
||||
.expect("read scoped connections resource");
|
||||
let ResourceContents::TextResourceContents { text, .. } = &resource.contents[0] else {
|
||||
panic!("connections resource should be text");
|
||||
};
|
||||
assert_eq!(text, &listed_text);
|
||||
|
||||
let blocked = client
|
||||
.peer()
|
||||
@@ -763,6 +775,8 @@ async fn connection_group_path_failure_preserves_connection_listing() {
|
||||
assert_ne!(listed.is_error, Some(true));
|
||||
assert!(listed_text.contains("| ID | Name | Group Path |"));
|
||||
assert!(listed_text.contains("local-db"));
|
||||
assert!(listed_text.contains("| Database | Note |"));
|
||||
assert!(listed_text.contains("| :memory: | |"));
|
||||
|
||||
client.cancel().await.expect("close MCP client");
|
||||
server_task.abort();
|
||||
@@ -770,9 +784,13 @@ async fn connection_group_path_failure_preserves_connection_listing() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_connection_scope_preserves_group_paths() {
|
||||
let mut scoped = test_connection("scoped", "shared-db");
|
||||
scoped.note = "业务库 | TEST\n只读查询".to_string();
|
||||
let mut outside = test_connection("outside", "shared-db");
|
||||
outside.note = "Out-of-scope connection note".to_string();
|
||||
let backend = PolicyBackend {
|
||||
policy: McpGlobalPolicy::default(),
|
||||
connections: vec![test_connection("scoped", "shared-db"), test_connection("outside", "shared-db")],
|
||||
connections: vec![scoped, outside],
|
||||
group_paths: Ok(HashMap::from([
|
||||
("scoped".to_string(), vec!["Project".to_string(), "Production".to_string()]),
|
||||
("outside".to_string(), vec!["Project".to_string(), "Staging".to_string()]),
|
||||
@@ -793,6 +811,19 @@ async fn runtime_connection_scope_preserves_group_paths() {
|
||||
assert!(listed_text.contains("| scoped | shared-db | Project / Production |"));
|
||||
assert!(!listed_text.contains("outside"));
|
||||
assert!(!listed_text.contains("Project / Staging"));
|
||||
assert!(listed_text.contains("业务库 \\| TEST 只读查询"));
|
||||
assert!(!listed_text.contains("Out-of-scope connection note"));
|
||||
assert_eq!(listed_text.lines().count(), 3);
|
||||
|
||||
let resource = client
|
||||
.peer()
|
||||
.read_resource(ReadResourceRequestParams::new("dbx://connections"))
|
||||
.await
|
||||
.expect("read scoped connections resource");
|
||||
let ResourceContents::TextResourceContents { text, .. } = &resource.contents[0] else {
|
||||
panic!("connections resource should be text");
|
||||
};
|
||||
assert_eq!(text, &listed_text);
|
||||
|
||||
client.cancel().await.expect("close MCP client");
|
||||
server_task.abort();
|
||||
|
||||
@@ -122,7 +122,7 @@ DBX MCP 当前提供以下工具(具体可用工具取决于构建功能及权
|
||||
|
||||
| 工具 | 说明 |
|
||||
| --- | --- |
|
||||
| `dbx_list_connections` | 列出当前 MCP 会话可见的连接 |
|
||||
| `dbx_list_connections` | 列出当前 MCP 会话可见的连接,包括已保存的备注 |
|
||||
| `dbx_list_databases` | 列出连接中可通过 MCP 访问的数据库,并遵守该连接的数据库范围 |
|
||||
| `dbx_add_connection` | 添加连接到 DBX 存储 |
|
||||
| `dbx_duplicate_connection` | 复制一个连接及其完整配置 |
|
||||
@@ -148,6 +148,8 @@ DBX MCP 当前提供以下工具(具体可用工具取决于构建功能及权
|
||||
| `dbx_open_table` | 在运行中的 DBX 桌面端打开表 |
|
||||
| `dbx_execute_and_show` | 执行查询并在 DBX 中展示结果 |
|
||||
|
||||
`dbx_list_connections` 和 `dbx://connections` 会返回已保存的连接备注,不受侧边栏显示开关影响。可访问该连接的 MCP 客户端也能看到备注,请勿在备注中保存密码或其他敏感信息。
|
||||
|
||||
`dbx_execute_query` 返回行数:
|
||||
|
||||
- `max_rows`:取值 1–1000,默认 100。越界值会被夹取到范围内,而不是报错。仅对 SQL 连接生效——MongoDB shell 命令始终最多返回 100 行,路由到批量执行器的多语句脚本每条语句最多返回 100 行。
|
||||
|
||||
@@ -122,7 +122,7 @@ DBX MCP provides the following tools (availability depends on build features and
|
||||
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `dbx_list_connections` | List connections visible to the MCP session |
|
||||
| `dbx_list_connections` | List connections visible to the MCP session, including saved notes |
|
||||
| `dbx_list_databases` | List databases available through a connection, respecting its MCP database scope |
|
||||
| `dbx_add_connection` | Add a connection to DBX storage |
|
||||
| `dbx_duplicate_connection` | Duplicate a DBX connection with its complete settings |
|
||||
@@ -148,6 +148,8 @@ DBX MCP provides the following tools (availability depends on build features and
|
||||
| `dbx_open_table` | Open a table in the running DBX desktop app |
|
||||
| `dbx_execute_and_show` | Execute a query and display the result in DBX |
|
||||
|
||||
`dbx_list_connections` and `dbx://connections` include saved connection notes regardless of the sidebar display setting. Notes are visible to MCP clients that can access the connection; do not store passwords or other secrets in notes.
|
||||
|
||||
`dbx_execute_query` row controls:
|
||||
|
||||
- `max_rows`: 1–1000, defaults to 100. Out-of-range values are clamped, not rejected. It applies to SQL connections only — MongoDB shell commands always return at most 100 rows, and multi-statement scripts routed to the batch executor return at most 100 rows per statement.
|
||||
|
||||
@@ -140,9 +140,11 @@ Ask the MCP client to:
|
||||
|
||||
## Tools
|
||||
|
||||
`dbx_list_connections` and `dbx://connections` include saved connection notes regardless of the sidebar display setting. Notes are visible to MCP clients that can access the connection; do not store passwords or other secrets in notes.
|
||||
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `dbx_list_connections` | List connections visible to the MCP session |
|
||||
| `dbx_list_connections` | List connections visible to the MCP session, including saved notes |
|
||||
| `dbx_list_databases` | List databases available through a connection, respecting its MCP database scope |
|
||||
| `dbx_add_connection` | Add a connection to DBX storage |
|
||||
| `dbx_duplicate_connection` | Duplicate a DBX connection with its complete settings |
|
||||
|
||||
Reference in New Issue
Block a user