rustapi_mcp/config.rs
1//! Configuration for the MCP server / integration.
2
3use std::collections::HashSet;
4
5/// Configuration for the native MCP server.
6///
7/// This is the primary way users control what gets exposed as tools,
8/// authentication for MCP clients, transport behavior, etc.
9#[derive(Debug, Clone)]
10pub struct McpConfig {
11 /// Human-friendly name of this MCP server (shown to agents).
12 pub name: String,
13 /// Version string.
14 pub version: String,
15 /// Optional description.
16 pub description: Option<String>,
17
18 /// Whether tool discovery and calling is enabled.
19 pub tools_enabled: bool,
20
21 /// Explicitly allowed tags. Only routes that have at least one of these tags
22 /// (via OpenAPI `tags` or future route metadata) will be exposed as tools.
23 ///
24 /// Empty set + no other allow rules = nothing is exposed (safe default).
25 pub allowed_tags: HashSet<String>,
26
27 /// Explicit path prefixes that are allowed to become tools.
28 /// Example: `["/api/public", "/agent"]`
29 pub allowed_path_prefixes: Vec<String>,
30
31 /// Admin / MCP client token.
32 ///
33 /// When set, MCP clients must present this (via header or query param,
34 /// transport dependent) to use discovery or invocation.
35 pub admin_token: Option<String>,
36
37 /// Whether to include detailed error information in tool responses.
38 /// In production you usually want this `false` (similar to RUSTAPI_ENV=production).
39 pub expose_detailed_errors: bool,
40
41 /// Maximum number of tools to advertise in one `tools/list` response.
42 /// Helps protect against very large route sets.
43 pub max_tools: usize,
44
45 /// How `tools/call` should be executed.
46 /// Proxy (default) always goes over HTTP (correct and works for external targets).
47 /// InProcess / Auto are for when an in-process RustApi instance is available.
48 pub invocation_mode: InvocationMode,
49
50 /// Permission policy for which operations are exposed as MCP tools.
51 ///
52 /// Framework-native guardrail. By default we are conservative for agent use:
53 /// ReadOnly (only safe methods like GET are exposed unless you opt into writes).
54 ///
55 /// This addresses the blast radius concern when agents can call destructive endpoints.
56 pub tool_policy: ToolPolicy,
57}
58
59/// Controls which operations (by HTTP semantics) are turned into MCP tools.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum ToolPolicy {
62 /// Expose everything (subject to `allowed_tags` / prefixes).
63 /// Use with care — agents can trigger writes/deletes.
64 All,
65
66 /// Only expose read-only operations (GET, HEAD, OPTIONS).
67 /// Strongly recommended default when giving tools to AI agents.
68 #[default]
69 ReadOnly,
70 // Future: fully custom allow-list + confirmation requirements.
71 // Custom { ... },
72}
73
74/// Controls whether tool invocation goes through the normal HTTP path or
75/// a direct in-memory call (when available).
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub enum InvocationMode {
78 /// Always proxy via the configured `http_base` (safest, works everywhere).
79 #[default]
80 Proxy,
81 /// Use direct in-process invocation when a RustApi runtime is attached.
82 InProcess,
83 /// Choose automatically (InProcess if runtime available, else Proxy).
84 Auto,
85}
86
87impl Default for McpConfig {
88 fn default() -> Self {
89 Self {
90 name: "rustapi-mcp".to_string(),
91 version: "0.0.0".to_string(),
92 description: None,
93 tools_enabled: true,
94 allowed_tags: HashSet::new(),
95 allowed_path_prefixes: vec![],
96 admin_token: None,
97 expose_detailed_errors: false,
98 max_tools: 256,
99 invocation_mode: InvocationMode::Proxy,
100 tool_policy: ToolPolicy::ReadOnly, // Safe default for agent-facing use
101 }
102 }
103}
104
105impl McpConfig {
106 /// Create a new config with reasonable defaults.
107 pub fn new() -> Self {
108 Self::default()
109 }
110
111 /// Set the name advertised to MCP clients.
112 pub fn name(mut self, name: impl Into<String>) -> Self {
113 self.name = name.into();
114 self
115 }
116
117 /// Set the version advertised to MCP clients.
118 pub fn version(mut self, version: impl Into<String>) -> Self {
119 self.version = version.into();
120 self
121 }
122
123 /// Set a human description.
124 pub fn description(mut self, desc: impl Into<String>) -> Self {
125 self.description = Some(desc.into());
126 self
127 }
128
129 /// Enable or disable the tools capability entirely.
130 pub fn enable_tools(mut self, enabled: bool) -> Self {
131 self.tools_enabled = enabled;
132 self
133 }
134
135 /// Allow tools only for routes that carry at least one of the given tags.
136 ///
137 /// This is the recommended way to safely expose a curated surface to agents.
138 pub fn allowed_tags<I, S>(mut self, tags: I) -> Self
139 where
140 I: IntoIterator<Item = S>,
141 S: Into<String>,
142 {
143 self.allowed_tags = tags.into_iter().map(Into::into).collect();
144 self
145 }
146
147 /// Add a path prefix that is allowed to be exposed as tools.
148 pub fn allow_path_prefix(mut self, prefix: impl Into<String>) -> Self {
149 self.allowed_path_prefixes.push(prefix.into());
150 self
151 }
152
153 /// Require this token for MCP clients (discovery + calls).
154 pub fn admin_token(mut self, token: impl Into<String>) -> Self {
155 self.admin_token = Some(token.into());
156 self
157 }
158
159 /// Control whether tool responses include full internal error details.
160 pub fn expose_detailed_errors(mut self, expose: bool) -> Self {
161 self.expose_detailed_errors = expose;
162 self
163 }
164
165 /// Set the maximum number of tools to list.
166 pub fn max_tools(mut self, max: usize) -> Self {
167 self.max_tools = max;
168 self
169 }
170
171 /// Choose invocation strategy for tool calls.
172 pub fn invocation_mode(mut self, mode: InvocationMode) -> Self {
173 self.invocation_mode = mode;
174 self
175 }
176
177 /// Set the permission policy for exposing tools.
178 ///
179 /// `ReadOnly` is the safe default when agents will call your tools.
180 /// Only GET/HEAD/OPTIONS operations are turned into tools.
181 ///
182 /// Use `All` if you explicitly want agents to perform writes (and you have
183 /// strong `allowed_tags` + confirmation flows).
184 pub fn tool_policy(mut self, policy: ToolPolicy) -> Self {
185 self.tool_policy = policy;
186 self
187 }
188}