diff --git a/Cargo.lock b/Cargo.lock index 7f330c2256..e0905be9bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,6 +588,20 @@ dependencies = [ "utoipa", ] +[[package]] +name = "api-research" +version = "0.1.0" +dependencies = [ + "axum 0.8.8", + "exa", + "rmcp", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", +] + [[package]] name = "api-subscription" version = "0.1.0" @@ -625,6 +639,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "sqlx", "thiserror 2.0.18", "tokio", "tokio-util", @@ -1169,6 +1184,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -3768,6 +3792,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -5038,6 +5071,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -5294,6 +5330,17 @@ dependencies = [ "cc", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "etcetera" version = "0.10.0" @@ -5728,6 +5775,17 @@ dependencies = [ "serde", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -5953,6 +6011,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -10566,6 +10635,16 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "libwayshot-xcap" version = "0.3.2" @@ -16282,6 +16361,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smallvec" @@ -16501,6 +16583,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spin" @@ -16540,6 +16625,196 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink 0.10.0", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.36", + "serde", + "serde_json", + "sha2", + "smallvec 1.15.1", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.114", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.10.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec 1.15.1", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.10.0", + "byteorder", + "crc", + "dotenvy", + "etcetera 0.8.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec 1.15.1", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + [[package]] name = "sse-stream" version = "0.2.1" @@ -19153,7 +19428,7 @@ dependencies = [ "bytes", "docker_credential", "either", - "etcetera", + "etcetera 0.10.0", "futures", "log", "memchr", @@ -19496,7 +19771,7 @@ dependencies = [ "socket2 0.6.2", "tokio", "tokio-util", - "whoami", + "whoami 2.1.0", ] [[package]] @@ -21421,6 +21696,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasite" version = "1.0.2" @@ -21884,6 +22165,16 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite 0.1.0", +] + [[package]] name = "whoami" version = "2.1.0" @@ -21891,7 +22182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fae98cf96deed1b7572272dfc777713c249ae40aa1cf8862e091e8b745f5361" dependencies = [ "libredox", - "wasite", + "wasite 1.0.2", "web-sys", ] @@ -22252,6 +22543,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -22303,6 +22603,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -22369,6 +22684,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -22387,6 +22708,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -22405,6 +22732,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -22435,6 +22768,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -22453,6 +22792,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -22471,6 +22816,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -22489,6 +22840,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 8800c8b276..69cecaac8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,6 +315,7 @@ whichlang = "0.1" swift-rs = { git = "https://github.com/yujonglee/swift-rs", rev = "41a1605" } rmcp = "0.14" +sqlx = { version = "0.8", default-features = false } sysinfo = "0.38" [patch.crates-io] diff --git a/apps/ai/src/env.rs b/apps/ai/src/env.rs index f3c485ff23..c0e6c1fc2b 100644 --- a/apps/ai/src/env.rs +++ b/apps/ai/src/env.rs @@ -24,6 +24,8 @@ pub struct Env { pub stripe: hypr_api_subscription::StripeEnv, #[serde(flatten)] pub github_app: hypr_api_support::GitHubAppEnv, + #[serde(flatten)] + pub supabase_db: hypr_api_support::SupabaseDbEnv, #[serde(flatten)] pub llm: hypr_llm_proxy::Env, diff --git a/apps/ai/src/main.rs b/apps/ai/src/main.rs index 8016365eb1..5c4f9591d2 100644 --- a/apps/ai/src/main.rs +++ b/apps/ai/src/main.rs @@ -23,7 +23,7 @@ use env::env; pub const DEVICE_FINGERPRINT_HEADER: &str = "x-device-fingerprint"; -fn app() -> Router { +async fn app() -> Router { let env = env(); let analytics = { @@ -45,7 +45,8 @@ fn app() -> Router { let nango_config = hypr_api_nango::NangoConfig::new(&env.nango); let subscription_config = hypr_api_subscription::SubscriptionConfig::new(&env.supabase, &env.stripe); - let support_config = hypr_api_support::SupportConfig::new(&env.github_app, &env.llm); + let support_config = + hypr_api_support::SupportConfig::new(&env.github_app, &env.llm, &env.supabase_db); let webhook_routes = Router::new().nest( "/nango", @@ -80,7 +81,7 @@ fn app() -> Router { .route("/health", axum::routing::get(|| async { "ok" })) .route("/v", axum::routing::get(version)) .route("/openapi.json", axum::routing::get(openapi_json)) - .nest("/support", hypr_api_support::router(support_config)) + .nest("/support", hypr_api_support::router(support_config).await) .merge(webhook_routes) .merge(pro_routes) .merge(auth_routes) @@ -224,7 +225,7 @@ fn main() -> std::io::Result<()> { tracing::info!(addr = %addr, "server_listening"); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app()) + axum::serve(listener, app().await) .with_graceful_shutdown(shutdown_signal()) .await .unwrap(); diff --git a/crates/api-support/Cargo.toml b/crates/api-support/Cargo.toml index 599fe678bf..38138c2c3d 100644 --- a/crates/api-support/Cargo.toml +++ b/crates/api-support/Cargo.toml @@ -16,6 +16,7 @@ tracing = { workspace = true } jsonwebtoken = { workspace = true } octocrab = "0.49" +sqlx = { workspace = true, features = ["runtime-tokio", "tls-rustls", "postgres", "json"] } askama = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/api-support/src/config.rs b/crates/api-support/src/config.rs index 3bfa4b809e..1fabf26687 100644 --- a/crates/api-support/src/config.rs +++ b/crates/api-support/src/config.rs @@ -1,16 +1,22 @@ -use crate::env::{GitHubAppEnv, OpenRouterEnv}; +use crate::env::{GitHubAppEnv, OpenRouterEnv, SupabaseDbEnv}; #[derive(Clone)] pub struct SupportConfig { pub github: GitHubAppEnv, pub openrouter: OpenRouterEnv, + pub supabase_db: SupabaseDbEnv, } impl SupportConfig { - pub fn new(github: &GitHubAppEnv, openrouter: &OpenRouterEnv) -> Self { + pub fn new( + github: &GitHubAppEnv, + openrouter: &OpenRouterEnv, + supabase_db: &SupabaseDbEnv, + ) -> Self { Self { github: github.clone(), openrouter: openrouter.clone(), + supabase_db: supabase_db.clone(), } } } diff --git a/crates/api-support/src/env.rs b/crates/api-support/src/env.rs index a0385cd768..45a16fa749 100644 --- a/crates/api-support/src/env.rs +++ b/crates/api-support/src/env.rs @@ -9,4 +9,9 @@ pub struct GitHubAppEnv { pub github_discussion_category_id: String, } +#[derive(Clone, Deserialize)] +pub struct SupabaseDbEnv { + pub supabase_db_url: String, +} + pub use hypr_api_env::OpenRouterEnv; diff --git a/crates/api-support/src/lib.rs b/crates/api-support/src/lib.rs index 76de7fe87f..6d81166a40 100644 --- a/crates/api-support/src/lib.rs +++ b/crates/api-support/src/lib.rs @@ -9,6 +9,6 @@ mod routes; mod state; pub use config::SupportConfig; -pub use env::{GitHubAppEnv, OpenRouterEnv}; +pub use env::{GitHubAppEnv, OpenRouterEnv, SupabaseDbEnv}; pub use openapi::openapi; pub use routes::router; diff --git a/crates/api-support/src/mcp/server.rs b/crates/api-support/src/mcp/server.rs index de60b06c2b..cd621f5fa6 100644 --- a/crates/api-support/src/mcp/server.rs +++ b/crates/api-support/src/mcp/server.rs @@ -5,7 +5,7 @@ use rmcp::{ use crate::state::AppState; -use super::tools::{self, SubmitBugReportParams, SubmitFeatureRequestParams}; +use super::tools::{self, ReadGitHubDataParams, SubmitBugReportParams, SubmitFeatureRequestParams}; #[derive(Clone)] pub(crate) struct SupportMcpServer { @@ -41,6 +41,16 @@ impl SupportMcpServer { ) -> Result { tools::submit_feature_request(&self.state, params).await } + + #[tool( + description = "Read GitHub data (issues, pull requests, comments, tags) from the database. Data is synced from GitHub via Airbyte." + )] + async fn read_github_data( + &self, + Parameters(params): Parameters, + ) -> Result { + tools::read_github_data(&self.state, params).await + } } #[tool_handler] diff --git a/crates/api-support/src/mcp/tools/mod.rs b/crates/api-support/src/mcp/tools/mod.rs index 4a45fbbaa1..bdba04b8cc 100644 --- a/crates/api-support/src/mcp/tools/mod.rs +++ b/crates/api-support/src/mcp/tools/mod.rs @@ -1,5 +1,7 @@ mod bug_report; mod feature_request; +mod read_github_data; pub(crate) use bug_report::{SubmitBugReportParams, submit_bug_report}; pub(crate) use feature_request::{SubmitFeatureRequestParams, submit_feature_request}; +pub(crate) use read_github_data::{ReadGitHubDataParams, read_github_data}; diff --git a/crates/api-support/src/mcp/tools/read_github_data.rs b/crates/api-support/src/mcp/tools/read_github_data.rs new file mode 100644 index 0000000000..24718d2a79 --- /dev/null +++ b/crates/api-support/src/mcp/tools/read_github_data.rs @@ -0,0 +1,123 @@ +use rmcp::{ + ErrorData as McpError, + model::*, + schemars::{self, JsonSchema}, +}; +use serde::Deserialize; + +use crate::state::AppState; + +#[derive(Debug, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum GitHubTable { + #[schemars(description = "GitHub issues")] + Issues, + #[schemars(description = "GitHub pull requests")] + PullRequests, + #[schemars(description = "GitHub comments")] + Comments, + #[schemars(description = "GitHub tags")] + Tags, +} + +impl GitHubTable { + fn as_str(&self) -> &'static str { + match self { + Self::Issues => "issues", + Self::PullRequests => "pull_requests", + Self::Comments => "comments", + Self::Tags => "tags", + } + } +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct ReadGitHubDataParams { + #[schemars(description = "The table to read from")] + pub table: GitHubTable, + #[schemars(description = "Maximum number of rows to return (default: 50, max: 500)")] + pub limit: Option, + #[schemars(description = "Number of rows to skip (default: 0)")] + pub offset: Option, + #[schemars( + description = "Filter by state (e.g. 'open', 'closed'). Applicable to issues and pull_requests." + )] + pub state: Option, +} + +pub(crate) async fn read_github_data( + state: &AppState, + params: ReadGitHubDataParams, +) -> Result { + let table_name = params.table.as_str(); + let limit = params.limit.unwrap_or(50).max(0).min(500); + let offset = params.offset.unwrap_or(0).max(0); + + if params.state.is_some() { + match params.table { + GitHubTable::Comments | GitHubTable::Tags => { + return Err(McpError::invalid_params( + "The 'state' filter is only applicable to 'issues' and 'pull_requests' tables", + None, + )); + } + _ => {} + } + } + + let query = if let Some(ref state_filter) = params.state { + let q = format!( + "SELECT to_jsonb(t.*) AS row_data FROM hyprnote_github.{} t WHERE t.state = $1 ORDER BY t._airbyte_extracted_at DESC LIMIT $2 OFFSET $3", + table_name + ); + sqlx::query_scalar::<_, serde_json::Value>(&q) + .bind(state_filter) + .bind(limit) + .bind(offset) + .fetch_all(&state.db_pool) + .await + } else { + let q = format!( + "SELECT to_jsonb(t.*) AS row_data FROM hyprnote_github.{} t ORDER BY t._airbyte_extracted_at DESC LIMIT $1 OFFSET $2", + table_name + ); + sqlx::query_scalar::<_, serde_json::Value>(&q) + .bind(limit) + .bind(offset) + .fetch_all(&state.db_pool) + .await + }; + + let rows = query.map_err(|e| McpError::internal_error(e.to_string(), None))?; + + let total_count: i64 = if let Some(ref state_filter) = params.state { + let count_query = format!( + "SELECT COUNT(*) FROM hyprnote_github.{} t WHERE t.state = $1", + table_name + ); + sqlx::query_scalar(&count_query) + .bind(state_filter) + .fetch_one(&state.db_pool) + .await + .unwrap_or(0) + } else { + let count_query = format!("SELECT COUNT(*) FROM hyprnote_github.{}", table_name); + sqlx::query_scalar(&count_query) + .fetch_one(&state.db_pool) + .await + .unwrap_or(0) + }; + + let result = serde_json::json!({ + "table": table_name, + "total_count": total_count, + "returned_count": rows.len(), + "limit": limit, + "offset": offset, + "rows": rows, + }); + + Ok(CallToolResult::success(vec![Content::text( + result.to_string(), + )])) +} diff --git a/crates/api-support/src/routes/mod.rs b/crates/api-support/src/routes/mod.rs index ef4a18678e..2d1211df80 100644 --- a/crates/api-support/src/routes/mod.rs +++ b/crates/api-support/src/routes/mod.rs @@ -8,8 +8,8 @@ use crate::state::AppState; pub use feedback::{FeedbackRequest, FeedbackResponse}; -pub fn router(config: SupportConfig) -> Router { - let state = AppState::new(config); +pub async fn router(config: SupportConfig) -> Router { + let state = AppState::new(config).await; let mcp = mcp_service(state.clone()); Router::new() diff --git a/crates/api-support/src/state.rs b/crates/api-support/src/state.rs index ce543def85..eda568a29b 100644 --- a/crates/api-support/src/state.rs +++ b/crates/api-support/src/state.rs @@ -1,4 +1,6 @@ use octocrab::Octocrab; +use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; use crate::config::SupportConfig; @@ -6,10 +8,11 @@ use crate::config::SupportConfig; pub(crate) struct AppState { pub(crate) config: SupportConfig, pub(crate) octocrab: Octocrab, + pub(crate) db_pool: PgPool, } impl AppState { - pub(crate) fn new(config: SupportConfig) -> Self { + pub(crate) async fn new(config: SupportConfig) -> Self { let key = jsonwebtoken::EncodingKey::from_rsa_pem( config .github @@ -24,7 +27,17 @@ impl AppState { .build() .expect("failed to build octocrab client"); - Self { config, octocrab } + let db_pool = PgPoolOptions::new() + .max_connections(5) + .connect(&config.supabase_db.supabase_db_url) + .await + .expect("failed to connect to Supabase Postgres"); + + Self { + config, + octocrab, + db_pool, + } } pub(crate) async fn installation_client(&self) -> Result {