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:
138
Makefile
Normal file
138
Makefile
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
APP_NAME := tusk
|
||||||
|
VERSION := $(shell grep '"version"' src-tauri/tauri.conf.json | head -1 | sed 's/.*: "//;s/".*//')
|
||||||
|
PREFIX := /usr/local
|
||||||
|
BINDIR := $(PREFIX)/bin
|
||||||
|
DATADIR := $(PREFIX)/share
|
||||||
|
BUNDLE_DIR := src-tauri/target/release/bundle
|
||||||
|
|
||||||
|
# Rust cross-compilation target (override: make build TARGET=aarch64-unknown-linux-gnu)
|
||||||
|
TARGET :=
|
||||||
|
TARGET_FLAG := $(if $(TARGET),--target $(TARGET),)
|
||||||
|
TARGET_DIR := $(if $(TARGET),src-tauri/target/$(TARGET)/release,src-tauri/target/release)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Development
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
.PHONY: dev
|
||||||
|
dev: node_modules ## Run app in dev mode (Vite HMR + Rust backend)
|
||||||
|
npm run tauri dev
|
||||||
|
|
||||||
|
.PHONY: dev-frontend
|
||||||
|
dev-frontend: node_modules ## Run only the Vite frontend dev server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
.PHONY: check
|
||||||
|
check: ## Check Rust compilation without building
|
||||||
|
cd src-tauri && cargo check $(TARGET_FLAG)
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: node_modules ## Run ESLint + cargo clippy
|
||||||
|
npm run lint
|
||||||
|
cd src-tauri && cargo clippy $(TARGET_FLAG) -- -D warnings
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: ## Format Rust code
|
||||||
|
cd src-tauri && cargo fmt
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check: ## Check Rust formatting
|
||||||
|
cd src-tauri && cargo fmt --check
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Build
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: node_modules ## Build release binary + bundles for current platform
|
||||||
|
npm run tauri build $(TARGET_FLAG)
|
||||||
|
|
||||||
|
.PHONY: build-debug
|
||||||
|
build-debug: node_modules ## Build debug binary
|
||||||
|
npm run tauri build -- --debug $(TARGET_FLAG)
|
||||||
|
|
||||||
|
.PHONY: build-linux
|
||||||
|
build-linux: node_modules ## Build for Linux (deb + AppImage + rpm)
|
||||||
|
npm run tauri build
|
||||||
|
|
||||||
|
.PHONY: build-macos
|
||||||
|
build-macos: node_modules ## Build for macOS (dmg + app bundle) — run on macOS
|
||||||
|
npm run tauri build
|
||||||
|
|
||||||
|
.PHONY: build-macos-universal
|
||||||
|
build-macos-universal: node_modules ## Build universal macOS binary (x86_64 + aarch64)
|
||||||
|
npm run tauri build -- --target universal-apple-darwin
|
||||||
|
|
||||||
|
.PHONY: build-windows
|
||||||
|
build-windows: node_modules ## Build for Windows (msi + nsis) — run on Windows
|
||||||
|
npm run tauri build
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Install / Uninstall (Linux)
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
define DESKTOP_ENTRY
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Tusk
|
||||||
|
Comment=PostgreSQL Database Management GUI
|
||||||
|
Exec=$(BINDIR)/$(APP_NAME)
|
||||||
|
Icon=$(APP_NAME)
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Development;Database;
|
||||||
|
Keywords=postgresql;database;sql;
|
||||||
|
endef
|
||||||
|
export DESKTOP_ENTRY
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: build ## Build release and install to system (PREFIX=/usr/local)
|
||||||
|
install -Dm755 $(TARGET_DIR)/$(APP_NAME) $(DESTDIR)$(BINDIR)/$(APP_NAME)
|
||||||
|
@mkdir -p $(DESTDIR)$(DATADIR)/applications
|
||||||
|
@echo "$$DESKTOP_ENTRY" > $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
|
||||||
|
@for size in 32x32 128x128; do \
|
||||||
|
if [ -f src-tauri/icons/$$size.png ]; then \
|
||||||
|
install -Dm644 src-tauri/icons/$$size.png \
|
||||||
|
$(DESTDIR)$(DATADIR)/icons/hicolor/$$size/apps/$(APP_NAME).png; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
@echo "Installed $(APP_NAME) $(VERSION) to $(DESTDIR)$(BINDIR)/$(APP_NAME)"
|
||||||
|
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall: ## Remove installed files
|
||||||
|
rm -f $(DESTDIR)$(BINDIR)/$(APP_NAME)
|
||||||
|
rm -f $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
|
||||||
|
rm -f $(DESTDIR)$(DATADIR)/icons/hicolor/32x32/apps/$(APP_NAME).png
|
||||||
|
rm -f $(DESTDIR)$(DATADIR)/icons/hicolor/128x128/apps/$(APP_NAME).png
|
||||||
|
@echo "Uninstalled $(APP_NAME)"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Utilities
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Remove build artifacts
|
||||||
|
rm -rf dist
|
||||||
|
cd src-tauri && cargo clean
|
||||||
|
|
||||||
|
.PHONY: clean-frontend
|
||||||
|
clean-frontend: ## Remove only frontend build output
|
||||||
|
rm -rf dist node_modules/.vite
|
||||||
|
|
||||||
|
.PHONY: deps
|
||||||
|
deps: node_modules ## Install all dependencies
|
||||||
|
@echo "Dependencies ready"
|
||||||
|
|
||||||
|
node_modules: package.json
|
||||||
|
npm install
|
||||||
|
@touch node_modules
|
||||||
|
|
||||||
|
.PHONY: version
|
||||||
|
version: ## Show app version
|
||||||
|
@echo "$(APP_NAME) $(VERSION)"
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show this help
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
|
||||||
|
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
259
src-tauri/Cargo.lock
generated
259
src-tauri/Cargo.lock
generated
@@ -53,6 +53,17 @@ version = "1.0.101"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -97,6 +108,58 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
@@ -546,8 +609,18 @@ version = "0.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
"darling_macro",
|
"darling_macro 0.21.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.23.0",
|
||||||
|
"darling_macro 0.23.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -564,13 +637,37 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||||
|
dependencies = [
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.23.0",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
@@ -927,6 +1024,21 @@ dependencies = [
|
|||||||
"new_debug_unreachable",
|
"new_debug_unreachable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1000,6 +1112,7 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -1439,6 +1552,12 @@ version = "1.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -1452,6 +1571,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
@@ -1932,6 +2052,12 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -2431,6 +2557,12 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pastey"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathdiff"
|
name = "pathdiff"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -2798,6 +2930,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2818,6 +2960,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -2836,6 +2988,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3010,6 +3171,51 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"pastey",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"rmcp-macros",
|
||||||
|
"schemars 1.2.1",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sse-stream",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp-macros"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.23.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_json",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -3102,7 +3308,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"schemars_derive",
|
"schemars_derive 0.8.22",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
@@ -3127,8 +3333,10 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
|
"schemars_derive 1.2.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -3145,6 +3353,18 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars_derive"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -3245,6 +3465,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -3311,7 +3542,7 @@ version = "3.16.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
|
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.21.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
@@ -3741,6 +3972,19 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sse-stream"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -4479,6 +4723,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4575,11 +4820,14 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|||||||
name = "tusk"
|
name = "tusk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"axum",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"chrono",
|
"chrono",
|
||||||
"csv",
|
"csv",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
|
"rmcp",
|
||||||
|
"schemars 1.2.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -4589,6 +4837,7 @@ dependencies = [
|
|||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,7 @@ csv = "1"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
bigdecimal = { version = "0.4", features = ["serde"] }
|
bigdecimal = { version = "0.4", features = ["serde"] }
|
||||||
|
rmcp = { version = "0.15", features = ["server", "macros", "transport-streamable-http-server"] }
|
||||||
|
axum = "0.8"
|
||||||
|
schemars = "1"
|
||||||
|
tokio-util = "0.7"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::state::AppState;
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
|
|
||||||
fn get_connections_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
fn get_connections_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||||
@@ -50,7 +51,7 @@ pub async fn save_connection(app: AppHandle, config: ConnectionConfig) -> TuskRe
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_connection(
|
pub async fn delete_connection(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
id: String,
|
id: String,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
let path = get_connections_path(&app)?;
|
let path = get_connections_path(&app)?;
|
||||||
@@ -91,7 +92,7 @@ pub async fn test_connection(config: ConnectionConfig) -> TuskResult<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn connect(state: State<'_, AppState>, config: ConnectionConfig) -> TuskResult<()> {
|
pub async fn connect(state: State<'_, Arc<AppState>>, config: ConnectionConfig) -> TuskResult<()> {
|
||||||
let pool = PgPool::connect(&config.connection_url())
|
let pool = PgPool::connect(&config.connection_url())
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database)?;
|
.map_err(TuskError::Database)?;
|
||||||
@@ -113,7 +114,7 @@ pub async fn connect(state: State<'_, AppState>, config: ConnectionConfig) -> Tu
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn switch_database(
|
pub async fn switch_database(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
config: ConnectionConfig,
|
config: ConnectionConfig,
|
||||||
database: String,
|
database: String,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -139,7 +140,7 @@ pub async fn switch_database(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn disconnect(state: State<'_, AppState>, id: String) -> TuskResult<()> {
|
pub async fn disconnect(state: State<'_, Arc<AppState>>, id: String) -> TuskResult<()> {
|
||||||
let mut pools = state.pools.write().await;
|
let mut pools = state.pools.write().await;
|
||||||
if let Some(pool) = pools.remove(&id) {
|
if let Some(pool) = pools.remove(&id) {
|
||||||
pool.close().await;
|
pool.close().await;
|
||||||
@@ -153,7 +154,7 @@ pub async fn disconnect(state: State<'_, AppState>, id: String) -> TuskResult<()
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_read_only(
|
pub async fn set_read_only(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -164,7 +165,7 @@ pub async fn set_read_only(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_read_only(
|
pub async fn get_read_only(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<bool> {
|
) -> TuskResult<bool> {
|
||||||
Ok(state.is_read_only(&connection_id).await)
|
Ok(state.is_read_only(&connection_id).await)
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ use crate::state::AppState;
|
|||||||
use crate::utils::escape_ident;
|
use crate::utils::escape_ident;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::{Column, Row, TypeInfo};
|
use sqlx::{Column, Row, TypeInfo};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_table_data(
|
pub async fn get_table_data(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -95,7 +96,7 @@ pub async fn get_table_data(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_row(
|
pub async fn update_row(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -142,7 +143,7 @@ pub async fn update_row(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn insert_row(
|
pub async fn insert_row(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -182,7 +183,7 @@ pub async fn insert_row(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_rows(
|
pub async fn delete_rows(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ use crate::models::management::*;
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::utils::escape_ident;
|
use crate::utils::escape_ident;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_database_info(
|
pub async fn get_database_info(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<Vec<DatabaseInfo>> {
|
) -> TuskResult<Vec<DatabaseInfo>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
@@ -54,7 +55,7 @@ pub async fn get_database_info(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_database(
|
pub async fn create_database(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
params: CreateDatabaseParams,
|
params: CreateDatabaseParams,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -95,7 +96,7 @@ pub async fn create_database(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn drop_database(
|
pub async fn drop_database(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -129,7 +130,7 @@ pub async fn drop_database(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_roles(
|
pub async fn list_roles(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<Vec<RoleInfo>> {
|
) -> TuskResult<Vec<RoleInfo>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
@@ -193,7 +194,7 @@ pub async fn list_roles(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_role(
|
pub async fn create_role(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
params: CreateRoleParams,
|
params: CreateRoleParams,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -271,7 +272,7 @@ pub async fn create_role(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn alter_role(
|
pub async fn alter_role(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
params: AlterRoleParams,
|
params: AlterRoleParams,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -370,7 +371,7 @@ pub async fn alter_role(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn drop_role(
|
pub async fn drop_role(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -394,7 +395,7 @@ pub async fn drop_role(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_table_privileges(
|
pub async fn get_table_privileges(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -433,7 +434,7 @@ pub async fn get_table_privileges(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn grant_revoke(
|
pub async fn grant_revoke(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
params: GrantRevokeParams,
|
params: GrantRevokeParams,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -478,7 +479,7 @@ pub async fn grant_revoke(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn manage_role_membership(
|
pub async fn manage_role_membership(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
params: RoleMembershipParams,
|
params: RoleMembershipParams,
|
||||||
) -> TuskResult<()> {
|
) -> TuskResult<()> {
|
||||||
@@ -510,7 +511,7 @@ pub async fn manage_role_membership(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_sessions(
|
pub async fn list_sessions(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<Vec<SessionInfo>> {
|
) -> TuskResult<Vec<SessionInfo>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
@@ -550,7 +551,7 @@ pub async fn list_sessions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cancel_query(
|
pub async fn cancel_query(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
pid: i32,
|
pid: i32,
|
||||||
) -> TuskResult<bool> {
|
) -> TuskResult<bool> {
|
||||||
@@ -570,7 +571,7 @@ pub async fn cancel_query(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn terminate_backend(
|
pub async fn terminate_backend(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
pid: i32,
|
pid: i32,
|
||||||
) -> TuskResult<bool> {
|
) -> TuskResult<bool> {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::state::AppState;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::postgres::PgRow;
|
use sqlx::postgres::PgRow;
|
||||||
use sqlx::{Column, Row, TypeInfo};
|
use sqlx::{Column, Row, TypeInfo};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
@@ -68,18 +69,17 @@ pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
pub async fn execute_query_core(
|
||||||
pub async fn execute_query(
|
state: &AppState,
|
||||||
state: State<'_, AppState>,
|
connection_id: &str,
|
||||||
connection_id: String,
|
sql: &str,
|
||||||
sql: String,
|
|
||||||
) -> TuskResult<QueryResult> {
|
) -> TuskResult<QueryResult> {
|
||||||
let read_only = state.is_read_only(&connection_id).await;
|
let read_only = state.is_read_only(connection_id).await;
|
||||||
|
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
let pool = pools
|
let pool = pools
|
||||||
.get(&connection_id)
|
.get(connection_id)
|
||||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let rows = if read_only {
|
let rows = if read_only {
|
||||||
@@ -88,14 +88,14 @@ pub async fn execute_query(
|
|||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database)?;
|
.map_err(TuskError::Database)?;
|
||||||
let result = sqlx::query(&sql)
|
let result = sqlx::query(sql)
|
||||||
.fetch_all(&mut *tx)
|
.fetch_all(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database);
|
.map_err(TuskError::Database);
|
||||||
tx.rollback().await.map_err(TuskError::Database)?;
|
tx.rollback().await.map_err(TuskError::Database)?;
|
||||||
result?
|
result?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query(&sql)
|
sqlx::query(sql)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database)?
|
.map_err(TuskError::Database)?
|
||||||
@@ -127,3 +127,12 @@ pub async fn execute_query(
|
|||||||
execution_time_ms,
|
execution_time_ms,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn execute_query(
|
||||||
|
state: State<'_, Arc<AppState>>,
|
||||||
|
connection_id: String,
|
||||||
|
sql: String,
|
||||||
|
) -> TuskResult<QueryResult> {
|
||||||
|
execute_query_core(&state, &connection_id, &sql).await
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ use crate::models::schema::{ColumnDetail, ColumnInfo, ConstraintInfo, IndexInfo,
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_databases(
|
pub async fn list_databases(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<Vec<String>> {
|
) -> TuskResult<Vec<String>> {
|
||||||
let pools = state.pools.read().await;
|
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())
|
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
pub async fn list_schemas_core(
|
||||||
pub async fn list_schemas(
|
state: &AppState,
|
||||||
state: State<'_, AppState>,
|
connection_id: &str,
|
||||||
connection_id: String,
|
|
||||||
) -> TuskResult<Vec<String>> {
|
) -> TuskResult<Vec<String>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
let pool = pools
|
let pool = pools
|
||||||
.get(&connection_id)
|
.get(connection_id)
|
||||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT schema_name FROM information_schema.schemata \
|
"SELECT schema_name FROM information_schema.schemata \
|
||||||
@@ -50,15 +50,22 @@ pub async fn list_schemas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_tables(
|
pub async fn list_schemas(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
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>> {
|
) -> TuskResult<Vec<SchemaObject>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
let pool = pools
|
let pool = pools
|
||||||
.get(&connection_id)
|
.get(connection_id)
|
||||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT t.table_name, \
|
"SELECT t.table_name, \
|
||||||
@@ -70,7 +77,7 @@ pub async fn list_tables(
|
|||||||
WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' \
|
WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' \
|
||||||
ORDER BY t.table_name",
|
ORDER BY t.table_name",
|
||||||
)
|
)
|
||||||
.bind(&schema)
|
.bind(schema)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database)?;
|
.map_err(TuskError::Database)?;
|
||||||
@@ -80,16 +87,25 @@ pub async fn list_tables(
|
|||||||
.map(|r| SchemaObject {
|
.map(|r| SchemaObject {
|
||||||
name: r.get(0),
|
name: r.get(0),
|
||||||
object_type: "table".to_string(),
|
object_type: "table".to_string(),
|
||||||
schema: schema.clone(),
|
schema: schema.to_string(),
|
||||||
row_count: r.get::<Option<i64>, _>(1),
|
row_count: r.get::<Option<i64>, _>(1),
|
||||||
size_bytes: r.get::<Option<i64>, _>(2),
|
size_bytes: r.get::<Option<i64>, _>(2),
|
||||||
})
|
})
|
||||||
.collect())
|
.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]
|
#[tauri::command]
|
||||||
pub async fn list_views(
|
pub async fn list_views(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
) -> TuskResult<Vec<SchemaObject>> {
|
) -> TuskResult<Vec<SchemaObject>> {
|
||||||
@@ -122,7 +138,7 @@ pub async fn list_views(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_functions(
|
pub async fn list_functions(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
) -> TuskResult<Vec<SchemaObject>> {
|
) -> TuskResult<Vec<SchemaObject>> {
|
||||||
@@ -155,7 +171,7 @@ pub async fn list_functions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_indexes(
|
pub async fn list_indexes(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
) -> TuskResult<Vec<SchemaObject>> {
|
) -> TuskResult<Vec<SchemaObject>> {
|
||||||
@@ -188,7 +204,7 @@ pub async fn list_indexes(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_sequences(
|
pub async fn list_sequences(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
) -> TuskResult<Vec<SchemaObject>> {
|
) -> TuskResult<Vec<SchemaObject>> {
|
||||||
@@ -219,17 +235,16 @@ pub async fn list_sequences(
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
pub async fn get_table_columns_core(
|
||||||
pub async fn get_table_columns(
|
state: &AppState,
|
||||||
state: State<'_, AppState>,
|
connection_id: &str,
|
||||||
connection_id: String,
|
schema: &str,
|
||||||
schema: String,
|
table: &str,
|
||||||
table: String,
|
|
||||||
) -> TuskResult<Vec<ColumnInfo>> {
|
) -> TuskResult<Vec<ColumnInfo>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
let pool = pools
|
let pool = pools
|
||||||
.get(&connection_id)
|
.get(connection_id)
|
||||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT \
|
"SELECT \
|
||||||
@@ -254,8 +269,8 @@ pub async fn get_table_columns(
|
|||||||
WHERE c.table_schema = $1 AND c.table_name = $2 \
|
WHERE c.table_schema = $1 AND c.table_name = $2 \
|
||||||
ORDER BY c.ordinal_position",
|
ORDER BY c.ordinal_position",
|
||||||
)
|
)
|
||||||
.bind(&schema)
|
.bind(schema)
|
||||||
.bind(&table)
|
.bind(table)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(TuskError::Database)?;
|
.map_err(TuskError::Database)?;
|
||||||
@@ -274,9 +289,19 @@ pub async fn get_table_columns(
|
|||||||
.collect())
|
.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]
|
#[tauri::command]
|
||||||
pub async fn get_table_constraints(
|
pub async fn get_table_constraints(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -317,7 +342,7 @@ pub async fn get_table_constraints(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_table_indexes(
|
pub async fn get_table_indexes(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
@@ -359,7 +384,7 @@ pub async fn get_table_indexes(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_completion_schema(
|
pub async fn get_completion_schema(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
|
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
|
||||||
let pools = state.pools.read().await;
|
let pools = state.pools.read().await;
|
||||||
@@ -396,7 +421,7 @@ pub async fn get_completion_schema(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_column_details(
|
pub async fn get_column_details(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, Arc<AppState>>,
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
schema: String,
|
schema: String,
|
||||||
table: String,
|
table: String,
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod mcp;
|
||||||
mod models;
|
mod models;
|
||||||
mod state;
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
let shared_state = Arc::new(AppState::new());
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.manage(AppState::new())
|
.manage(shared_state)
|
||||||
|
.setup(|app| {
|
||||||
|
let state = app.state::<Arc<AppState>>().inner().clone();
|
||||||
|
let connections_path = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.expect("failed to resolve app data dir")
|
||||||
|
.join("connections.json");
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = mcp::start_mcp_server(state, connections_path, 9427).await {
|
||||||
|
log::error!("MCP server error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// connections
|
// connections
|
||||||
commands::connections::get_connections,
|
commands::connections::get_connections,
|
||||||
|
|||||||
236
src-tauri/src/mcp/mod.rs
Normal file
236
src-tauri/src/mcp/mod.rs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
use crate::commands::queries::execute_query_core;
|
||||||
|
use crate::commands::schema::{get_table_columns_core, list_schemas_core, list_tables_core};
|
||||||
|
use crate::models::connection::ConnectionConfig;
|
||||||
|
use crate::state::AppState;
|
||||||
|
use rmcp::handler::server::tool::ToolRouter;
|
||||||
|
use rmcp::handler::server::wrapper::Parameters;
|
||||||
|
use rmcp::model::*;
|
||||||
|
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
||||||
|
use rmcp::transport::streamable_http_server::StreamableHttpServerConfig;
|
||||||
|
use rmcp::transport::streamable_http_server::StreamableHttpService;
|
||||||
|
use rmcp::{tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// --- Tool parameter types ---
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
struct ConnectionIdParam {
|
||||||
|
/// ID of the connection to use (from list_connections)
|
||||||
|
connection_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
struct ExecuteQueryParam {
|
||||||
|
/// ID of the connection to use
|
||||||
|
connection_id: String,
|
||||||
|
/// SQL query to execute
|
||||||
|
sql: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
struct ListTablesParam {
|
||||||
|
/// ID of the connection to use
|
||||||
|
connection_id: String,
|
||||||
|
/// Schema name (e.g. "public")
|
||||||
|
schema: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
struct DescribeTableParam {
|
||||||
|
/// ID of the connection to use
|
||||||
|
connection_id: String,
|
||||||
|
/// Schema name (e.g. "public")
|
||||||
|
schema: String,
|
||||||
|
/// Table name
|
||||||
|
table: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MCP Server ---
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TuskMcpServer {
|
||||||
|
state: Arc<AppState>,
|
||||||
|
connections_path: PathBuf,
|
||||||
|
tool_router: ToolRouter<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ConnectionStatus {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
database: String,
|
||||||
|
active: bool,
|
||||||
|
read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_router]
|
||||||
|
impl TuskMcpServer {
|
||||||
|
fn new(state: Arc<AppState>, connections_path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
connections_path,
|
||||||
|
tool_router: Self::tool_router(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "List all configured database connections with their active/read-only status")]
|
||||||
|
async fn list_connections(&self) -> Result<CallToolResult, McpError> {
|
||||||
|
let configs: Vec<ConnectionConfig> = if self.connections_path.exists() {
|
||||||
|
let data = std::fs::read_to_string(&self.connections_path).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Failed to read connections file: {}", e), None)
|
||||||
|
})?;
|
||||||
|
serde_json::from_str(&data).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let pools = self.state.pools.read().await;
|
||||||
|
let read_only_map = self.state.read_only.read().await;
|
||||||
|
|
||||||
|
let statuses: Vec<ConnectionStatus> = configs
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
let active = pools.contains_key(&c.id);
|
||||||
|
let read_only = read_only_map.get(&c.id).copied().unwrap_or(true);
|
||||||
|
ConnectionStatus {
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
host: c.host,
|
||||||
|
port: c.port,
|
||||||
|
database: c.database,
|
||||||
|
active,
|
||||||
|
read_only,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&statuses).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Serialization error: {}", e), None)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Execute a SQL query on a database connection. Respects read-only mode.")]
|
||||||
|
async fn execute_query(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<ExecuteQueryParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match execute_query_core(&self.state, ¶ms.connection_id, ¶ms.sql).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let json = serde_json::to_string_pretty(&result).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Serialization error: {}", e), None)
|
||||||
|
})?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||||
|
}
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "List all schemas in the current database (excludes system schemas)")]
|
||||||
|
async fn list_schemas(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<ConnectionIdParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match list_schemas_core(&self.state, ¶ms.connection_id).await {
|
||||||
|
Ok(schemas) => {
|
||||||
|
let json = serde_json::to_string_pretty(&schemas).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Serialization error: {}", e), None)
|
||||||
|
})?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||||
|
}
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "List all tables in a schema with row count and size information")]
|
||||||
|
async fn list_tables(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<ListTablesParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match list_tables_core(&self.state, ¶ms.connection_id, ¶ms.schema).await {
|
||||||
|
Ok(tables) => {
|
||||||
|
let json = serde_json::to_string_pretty(&tables).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Serialization error: {}", e), None)
|
||||||
|
})?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||||
|
}
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Describe table columns: name, type, nullable, primary key, default value")]
|
||||||
|
async fn describe_table(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<DescribeTableParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match get_table_columns_core(
|
||||||
|
&self.state,
|
||||||
|
¶ms.connection_id,
|
||||||
|
¶ms.schema,
|
||||||
|
¶ms.table,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(columns) => {
|
||||||
|
let json = serde_json::to_string_pretty(&columns).map_err(|e| {
|
||||||
|
McpError::internal_error(format!("Serialization error: {}", e), None)
|
||||||
|
})?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||||
|
}
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_handler]
|
||||||
|
impl ServerHandler for TuskMcpServer {
|
||||||
|
fn get_info(&self) -> ServerInfo {
|
||||||
|
ServerInfo {
|
||||||
|
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||||
|
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||||
|
server_info: Implementation {
|
||||||
|
name: "tusk-mcp".to_string(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
title: Some("Tusk PostgreSQL MCP Server".to_string()),
|
||||||
|
description: None,
|
||||||
|
website_url: None,
|
||||||
|
icons: None,
|
||||||
|
},
|
||||||
|
instructions: Some(
|
||||||
|
"Tusk MCP server provides access to PostgreSQL databases through the Tusk GUI app. \
|
||||||
|
Use list_connections to see available connections (must be connected in the Tusk GUI first). \
|
||||||
|
Use the connection_id from list_connections in other tools. \
|
||||||
|
Read-only mode is respected — write queries will fail if a connection is in read-only mode."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_mcp_server(
|
||||||
|
state: Arc<AppState>,
|
||||||
|
connections_path: PathBuf,
|
||||||
|
port: u16,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let service = StreamableHttpService::new(
|
||||||
|
move || Ok(TuskMcpServer::new(state.clone(), connections_path.clone())),
|
||||||
|
Arc::new(LocalSessionManager::default()),
|
||||||
|
StreamableHttpServerConfig::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let router = axum::Router::new().nest_service("/mcp", service);
|
||||||
|
let addr = format!("127.0.0.1:{}", port);
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
|
|
||||||
|
log::info!("MCP server listening on http://{}/mcp", addr);
|
||||||
|
|
||||||
|
axum::serve(listener, router).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user