The smokesignal.events web application
1//! MCP client storage operations.
2//!
3//! Stores dynamically registered MCP clients for OAuth flow.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use sqlx::FromRow;
8
9use super::{StoragePool, errors::StorageError};
10
11/// An MCP client registered for OAuth.
12#[derive(Clone, Debug, FromRow, Serialize, Deserialize)]
13pub struct McpClient {
14 /// Unique internal ID
15 pub id: String,
16 /// OAuth client_id (URL format)
17 pub client_id: String,
18 /// OAuth client_secret
19 pub client_secret: String,
20 /// OAuth redirect_uri
21 pub redirect_uri: String,
22 /// Human-readable client name
23 pub client_name: Option<String>,
24 /// DID of the authenticated user (after OAuth completes)
25 pub did: Option<String>,
26 /// DPoP JWK for token binding
27 pub dpop_jwk: String,
28 /// ATProto authorization server issuer
29 pub issuer: Option<String>,
30 /// When the client was registered
31 pub created_at: DateTime<Utc>,
32 /// When the client was last updated
33 pub updated_at: DateTime<Utc>,
34}
35
36/// Create a new MCP client registration.
37pub async fn mcp_client_create(
38 pool: &StoragePool,
39 id: &str,
40 client_id: &str,
41 client_secret: &str,
42 redirect_uri: &str,
43 client_name: Option<&str>,
44 dpop_jwk: &str,
45) -> Result<McpClient, StorageError> {
46 let now = Utc::now();
47
48 sqlx::query_as::<_, McpClient>(
49 r#"
50 INSERT INTO mcp_clients (id, client_id, client_secret, redirect_uri, client_name, dpop_jwk, created_at, updated_at)
51 VALUES ($1, $2, $3, $4, $5, $6, $7, $7)
52 RETURNING *
53 "#,
54 )
55 .bind(id)
56 .bind(client_id)
57 .bind(client_secret)
58 .bind(redirect_uri)
59 .bind(client_name)
60 .bind(dpop_jwk)
61 .bind(now)
62 .fetch_one(pool)
63 .await
64 .map_err(StorageError::UnableToExecuteQuery)
65}
66
67/// Get an MCP client by its client_id.
68pub async fn mcp_client_get_by_client_id(
69 pool: &StoragePool,
70 client_id: &str,
71) -> Result<McpClient, StorageError> {
72 sqlx::query_as::<_, McpClient>("SELECT * FROM mcp_clients WHERE client_id = $1")
73 .bind(client_id)
74 .fetch_one(pool)
75 .await
76 .map_err(|err| match err {
77 sqlx::Error::RowNotFound => StorageError::RowNotFound("mcp_client".to_string(), err),
78 other => StorageError::UnableToExecuteQuery(other),
79 })
80}
81
82/// Get an MCP client by its internal ID.
83pub async fn mcp_client_get_by_id(pool: &StoragePool, id: &str) -> Result<McpClient, StorageError> {
84 sqlx::query_as::<_, McpClient>("SELECT * FROM mcp_clients WHERE id = $1")
85 .bind(id)
86 .fetch_one(pool)
87 .await
88 .map_err(|err| match err {
89 sqlx::Error::RowNotFound => StorageError::RowNotFound("mcp_client".to_string(), err),
90 other => StorageError::UnableToExecuteQuery(other),
91 })
92}
93
94/// Update an MCP client with authentication info after OAuth completes.
95pub async fn mcp_client_update_auth(
96 pool: &StoragePool,
97 client_id: &str,
98 did: &str,
99 issuer: &str,
100) -> Result<(), StorageError> {
101 let now = Utc::now();
102
103 sqlx::query(
104 r#"
105 UPDATE mcp_clients
106 SET did = $1, issuer = $2, updated_at = $3
107 WHERE client_id = $4
108 "#,
109 )
110 .bind(did)
111 .bind(issuer)
112 .bind(now)
113 .bind(client_id)
114 .execute(pool)
115 .await
116 .map_err(StorageError::UnableToExecuteQuery)?;
117
118 Ok(())
119}
120
121/// Delete an MCP client by its client_id.
122pub async fn mcp_client_delete(pool: &StoragePool, client_id: &str) -> Result<(), StorageError> {
123 sqlx::query("DELETE FROM mcp_clients WHERE client_id = $1")
124 .bind(client_id)
125 .execute(pool)
126 .await
127 .map_err(StorageError::UnableToExecuteQuery)?;
128
129 Ok(())
130}
131
132/// Delete an MCP client by its internal ID.
133pub async fn mcp_client_delete_by_id(pool: &StoragePool, id: &str) -> Result<(), StorageError> {
134 sqlx::query("DELETE FROM mcp_clients WHERE id = $1")
135 .bind(id)
136 .execute(pool)
137 .await
138 .map_err(StorageError::UnableToExecuteQuery)?;
139
140 Ok(())
141}