Skip to main content

fraiseql_cli/config/toml_schema/
server_settings.rs

1//! Server settings configuration for TOML schema (validation, debug, MCP).
2
3use serde::{Deserialize, Serialize};
4
5/// MCP (Model Context Protocol) server configuration.
6///
7/// Enables AI/LLM tools to interact with FraiseQL queries and mutations
8/// through the standardized Model Context Protocol.
9#[derive(Debug, Clone, Deserialize, Serialize)]
10#[serde(default, deny_unknown_fields)]
11pub struct McpConfig {
12    /// Enable MCP server endpoint.
13    pub enabled:       bool,
14    /// Transport mode: "http", "stdio", or "both".
15    pub transport:     String,
16    /// HTTP path for MCP endpoint (e.g., "/mcp").
17    pub path:          String,
18    /// Require authentication for MCP requests.
19    pub require_auth:  bool,
20    /// Whitelist of query/mutation names to expose (empty = all).
21    #[serde(default)]
22    pub include:       Vec<String>,
23    /// Blacklist of query/mutation names to hide.
24    #[serde(default)]
25    pub exclude:       Vec<String>,
26    /// Read-only exposure: when `true`, no mutation is ever exposed as an MCP tool,
27    /// regardless of `include`/`exclude` (fail-closed for AI callers). Recommended
28    /// unless you deliberately expose mutations.
29    #[serde(default)]
30    pub read_only:     bool,
31    /// Keep per-thread working state across an agent's tool calls, in the
32    /// `[session_state]` store (#967). Off by default; requires an authenticated
33    /// caller and a configured `[session_state]` backend.
34    #[serde(default)]
35    pub session_state: bool,
36}
37
38impl Default for McpConfig {
39    fn default() -> Self {
40        Self {
41            enabled:       false,
42            transport:     "http".to_string(),
43            path:          "/mcp".to_string(),
44            require_auth:  true,
45            include:       Vec::new(),
46            exclude:       Vec::new(),
47            read_only:     false,
48            session_state: false,
49        }
50    }
51}
52
53/// Query validation limits (depth and complexity).
54///
55/// ```toml
56/// [validation]
57/// max_query_depth = 10
58/// max_query_complexity = 100
59/// ```
60#[derive(Debug, Clone, Default, Deserialize, Serialize)]
61#[serde(default, deny_unknown_fields)]
62pub struct ValidationConfig {
63    /// Maximum allowed query nesting depth. `None` uses the server default (10).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub max_query_depth: Option<u32>,
66
67    /// Maximum allowed query complexity score. `None` uses the server default (100).
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub max_query_complexity: Option<u32>,
70
71    /// Maximum bytes a single read may deliver, measured on the rows returned.
72    ///
73    /// Depth and complexity score the request; this bounds the answer, which is
74    /// the only cost a read from a materialised view actually has. `None` leaves
75    /// reads unbounded by size.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub max_response_bytes: Option<u64>,
78
79    /// Maximum rows a top-level `first`/`last`/`limit` argument may request,
80    /// guarding against unbounded-pagination denial of service (#421). `None`
81    /// uses the server default (1000); the server also honours the
82    /// `FRAISEQL_MAX_PAGE_SIZE` environment variable as an override.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub max_page_size: Option<u32>,
85
86    /// The deepest offset a client may page to (#1306): a GraphQL `offset:`, REST
87    /// `?offset=` or `?rel.offset=` beyond it is refused before any statement, naming the
88    /// relay connection as the path that pages at any depth. `None` (the default) refuses
89    /// none; the server also honours `FRAISEQL_MAX_OFFSET` as an override.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub max_offset: Option<u32>,
92
93    /// The most (bucket, entity) cells an aggregate carrying a semi-additive measure forward
94    /// may read (#1459): each is one index seek, and a larger request is refused before the
95    /// measure is read. `None` uses the runtime default (250,000).
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub max_semi_additive_cells: Option<u64>,
98}
99
100impl ValidationConfig {
101    /// Refuse a `max_offset` of 0: it would refuse every offset page, which is not a
102    /// ceiling. Leave the key unset for none. Likewise `max_semi_additive_cells` (#1459).
103    ///
104    /// # Errors
105    ///
106    /// When `max_offset` or `max_semi_additive_cells` is 0.
107    pub fn validate(&self) -> anyhow::Result<()> {
108        if self.max_offset == Some(0) {
109            anyhow::bail!(
110                "[validation] max_offset = 0 would refuse every offset page. Set the deepest \
111                 offset clients may page to, or leave the key unset for no ceiling."
112            );
113        }
114        if self.max_semi_additive_cells == Some(0) {
115            anyhow::bail!(
116                "[validation] max_semi_additive_cells = 0 would refuse every aggregate over a \
117                 semi-additive measure. Set the most cells such an aggregate may read, or leave \
118                 the key unset for the default."
119            );
120        }
121        Ok(())
122    }
123}
124
125/// Debug/development configuration.
126///
127/// Controls features that should only be enabled during development or
128/// in trusted environments. All flags default to off.
129///
130/// ```toml
131/// [debug]
132/// enabled = true
133/// database_explain = true
134/// expose_sql = true
135/// ```
136#[derive(Debug, Clone, Deserialize, Serialize)]
137#[serde(default, deny_unknown_fields)]
138pub struct DebugConfig {
139    /// Master switch — all debug features require this to be `true`.
140    pub enabled: bool,
141
142    /// When `true`, the explain endpoint will also run `EXPLAIN` against the
143    /// database and include the query plan in the response.
144    pub database_explain: bool,
145
146    /// When `true`, the explain endpoint includes the generated SQL in the
147    /// response. Defaults to `true` (SQL is shown even without
148    /// `database_explain`).
149    pub expose_sql: bool,
150}
151
152impl Default for DebugConfig {
153    fn default() -> Self {
154        Self {
155            enabled:          false,
156            database_explain: false,
157            expose_sql:       true,
158        }
159    }
160}