feat: add embedded MCP server and Makefile

Add an MCP (Model Context Protocol) server that starts on 127.0.0.1:9427
at app launch, sharing connection pools with the Tauri IPC layer. This
lets Claude (or any MCP client) query PostgreSQL through Tusk's existing
connections.

MCP tools: list_connections, execute_query, list_schemas, list_tables,
describe_table.

Also add a Makefile with targets for dev, build (cross-platform),
install/uninstall, lint, and formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 13:24:25 +03:00
parent ded35d8c40
commit 32486b0524
10 changed files with 756 additions and 71 deletions

View File

@@ -3,11 +3,12 @@ use crate::models::schema::{ColumnDetail, ColumnInfo, ConstraintInfo, IndexInfo,
use crate::state::AppState;
use sqlx::Row;
use std::collections::HashMap;
use std::sync::Arc;
use tauri::State;
#[tauri::command]
pub async fn list_databases(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
) -> TuskResult<Vec<String>> {
let pools = state.pools.read().await;
@@ -27,15 +28,14 @@ pub async fn list_databases(
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
}
#[tauri::command]
pub async fn list_schemas(
state: State<'_, AppState>,
connection_id: String,
pub async fn list_schemas_core(
state: &AppState,
connection_id: &str,
) -> TuskResult<Vec<String>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let rows = sqlx::query(
"SELECT schema_name FROM information_schema.schemata \
@@ -50,15 +50,22 @@ pub async fn list_schemas(
}
#[tauri::command]
pub async fn list_tables(
state: State<'_, AppState>,
pub async fn list_schemas(
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<String>> {
list_schemas_core(&state, &connection_id).await
}
pub async fn list_tables_core(
state: &AppState,
connection_id: &str,
schema: &str,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let rows = sqlx::query(
"SELECT t.table_name, \
@@ -70,7 +77,7 @@ pub async fn list_tables(
WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' \
ORDER BY t.table_name",
)
.bind(&schema)
.bind(schema)
.fetch_all(pool)
.await
.map_err(TuskError::Database)?;
@@ -80,16 +87,25 @@ pub async fn list_tables(
.map(|r| SchemaObject {
name: r.get(0),
object_type: "table".to_string(),
schema: schema.clone(),
schema: schema.to_string(),
row_count: r.get::<Option<i64>, _>(1),
size_bytes: r.get::<Option<i64>, _>(2),
})
.collect())
}
#[tauri::command]
pub async fn list_tables(
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
list_tables_core(&state, &connection_id, &schema).await
}
#[tauri::command]
pub async fn list_views(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
@@ -122,7 +138,7 @@ pub async fn list_views(
#[tauri::command]
pub async fn list_functions(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
@@ -155,7 +171,7 @@ pub async fn list_functions(
#[tauri::command]
pub async fn list_indexes(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
@@ -188,7 +204,7 @@ pub async fn list_indexes(
#[tauri::command]
pub async fn list_sequences(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
@@ -219,17 +235,16 @@ pub async fn list_sequences(
.collect())
}
#[tauri::command]
pub async fn get_table_columns(
state: State<'_, AppState>,
connection_id: String,
schema: String,
table: String,
pub async fn get_table_columns_core(
state: &AppState,
connection_id: &str,
schema: &str,
table: &str,
) -> TuskResult<Vec<ColumnInfo>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let rows = sqlx::query(
"SELECT \
@@ -254,8 +269,8 @@ pub async fn get_table_columns(
WHERE c.table_schema = $1 AND c.table_name = $2 \
ORDER BY c.ordinal_position",
)
.bind(&schema)
.bind(&table)
.bind(schema)
.bind(table)
.fetch_all(pool)
.await
.map_err(TuskError::Database)?;
@@ -274,9 +289,19 @@ pub async fn get_table_columns(
.collect())
}
#[tauri::command]
pub async fn get_table_columns(
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
table: String,
) -> TuskResult<Vec<ColumnInfo>> {
get_table_columns_core(&state, &connection_id, &schema, &table).await
}
#[tauri::command]
pub async fn get_table_constraints(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
table: String,
@@ -317,7 +342,7 @@ pub async fn get_table_constraints(
#[tauri::command]
pub async fn get_table_indexes(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
table: String,
@@ -359,7 +384,7 @@ pub async fn get_table_indexes(
#[tauri::command]
pub async fn get_completion_schema(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
let pools = state.pools.read().await;
@@ -396,7 +421,7 @@ pub async fn get_completion_schema(
#[tauri::command]
pub async fn get_column_details(
state: State<'_, AppState>,
state: State<'_, Arc<AppState>>,
connection_id: String,
schema: String,
table: String,