1#![allow(missing_docs)] use serde::{Deserialize, Serialize};
19
20pub const API_VERSION: &str = "v1-beta";
22
23pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "lowercase")]
32pub enum ApiStability {
33 Experimental,
35 Beta,
37 Stable,
39 Deprecated,
41}
42
43#[derive(Debug, Clone)]
45pub struct ApiEndpoint {
46 pub method: &'static str,
47 pub stability: ApiStability,
48 pub since_version: &'static str,
49 pub deprecated_in: Option<&'static str>,
50 pub removed_in: Option<&'static str>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VersionInfo {
56 pub api_version: String,
57 pub server_version: String,
58 pub protocol_version: String,
59 pub stability: String,
60}
61
62impl Default for VersionInfo {
63 fn default() -> Self {
64 Self {
65 api_version: API_VERSION.to_string(),
66 server_version: SERVER_VERSION.to_string(),
67 protocol_version: MCP_PROTOCOL_VERSION.to_string(),
68 stability: "beta".to_string(),
69 }
70 }
71}
72
73pub struct ApiRegistry;
75
76impl ApiRegistry {
77 pub fn endpoints() -> Vec<ApiEndpoint> {
79 vec![
80 ApiEndpoint {
82 method: "initialize",
83 stability: ApiStability::Stable,
84 since_version: "0.9.1",
85 deprecated_in: None,
86 removed_in: None,
87 },
88 ApiEndpoint {
89 method: "initialized",
90 stability: ApiStability::Stable,
91 since_version: "0.9.1",
92 deprecated_in: None,
93 removed_in: None,
94 },
95 ApiEndpoint {
96 method: "shutdown",
97 stability: ApiStability::Stable,
98 since_version: "0.9.1",
99 deprecated_in: None,
100 removed_in: None,
101 },
102 ApiEndpoint {
103 method: "tools/list",
104 stability: ApiStability::Stable,
105 since_version: "0.9.1",
106 deprecated_in: None,
107 removed_in: None,
108 },
109 ApiEndpoint {
110 method: "tools/call",
111 stability: ApiStability::Stable,
112 since_version: "0.9.1",
113 deprecated_in: None,
114 removed_in: None,
115 },
116 ApiEndpoint {
117 method: "resources/list",
118 stability: ApiStability::Stable,
119 since_version: "0.9.1",
120 deprecated_in: None,
121 removed_in: None,
122 },
123 ApiEndpoint {
124 method: "resources/read",
125 stability: ApiStability::Stable,
126 since_version: "0.9.1",
127 deprecated_in: None,
128 removed_in: None,
129 },
130 ApiEndpoint {
131 method: "prompts/list",
132 stability: ApiStability::Beta,
133 since_version: "0.9.1",
134 deprecated_in: None,
135 removed_in: None,
136 },
137 ApiEndpoint {
139 method: "security/status",
140 stability: ApiStability::Experimental,
141 since_version: "0.9.1",
142 deprecated_in: None,
143 removed_in: None,
144 },
145 ApiEndpoint {
146 method: "security/threats",
147 stability: ApiStability::Experimental,
148 since_version: "0.9.1",
149 deprecated_in: None,
150 removed_in: None,
151 },
152 ApiEndpoint {
153 method: "security/rate_limit_status",
154 stability: ApiStability::Experimental,
155 since_version: "0.9.1",
156 deprecated_in: None,
157 removed_in: None,
158 },
159 ApiEndpoint {
161 method: "admin/update_config",
162 stability: ApiStability::Experimental,
163 since_version: "0.9.1",
164 deprecated_in: None,
165 removed_in: None,
166 },
167 ]
168 }
169
170 pub fn is_stable(method: &str) -> bool {
172 Self::endpoints()
173 .iter()
174 .find(|e| e.method == method)
175 .is_some_and(|e| e.stability == ApiStability::Stable)
176 }
177
178 pub fn get_stability(method: &str) -> Option<ApiStability> {
180 Self::endpoints()
181 .iter()
182 .find(|e| e.method == method)
183 .map(|e| e.stability)
184 }
185
186 pub fn experimental_enabled() -> bool {
188 std::env::var("KINDLYGUARD_EXPERIMENTAL")
190 .map(|v| v == "1" || v.to_lowercase() == "true")
191 .unwrap_or(false)
192 }
193}
194
195pub fn add_version_metadata(response: &mut serde_json::Value) {
197 if let Some(obj) = response.as_object_mut() {
198 let version_info = VersionInfo::default();
199 obj.insert(
200 "_meta".to_string(),
201 serde_json::json!({
202 "api_version": version_info.api_version,
203 "server_version": version_info.server_version,
204 "timestamp": chrono::Utc::now().to_rfc3339(),
205 }),
206 );
207 }
208}
209
210pub struct VersionNegotiator;
212
213impl VersionNegotiator {
214 pub const SUPPORTED_PROTOCOLS: &'static [&'static str] = &[
216 "2024-11-05", "2024-10-01", ];
219
220 pub fn is_supported(version: &str) -> bool {
222 Self::SUPPORTED_PROTOCOLS.contains(&version)
223 }
224
225 pub fn negotiate(requested: &str) -> Option<&'static str> {
227 Self::SUPPORTED_PROTOCOLS
229 .iter()
230 .find(|&&v| v == requested)
231 .copied()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_api_registry() {
241 assert!(ApiRegistry::is_stable("initialize"));
242 assert!(ApiRegistry::is_stable("tools/list"));
243 assert!(!ApiRegistry::is_stable("security/status"));
244
245 assert_eq!(
246 ApiRegistry::get_stability("security/status"),
247 Some(ApiStability::Experimental)
248 );
249 }
250
251 #[test]
252 fn test_version_negotiation() {
253 assert!(VersionNegotiator::is_supported("2024-11-05"));
254 assert!(!VersionNegotiator::is_supported("2023-01-01"));
255
256 assert_eq!(
257 VersionNegotiator::negotiate("2024-11-05"),
258 Some("2024-11-05")
259 );
260 }
261
262 #[test]
263 fn test_version_metadata() {
264 let mut response = serde_json::json!({
265 "result": "test"
266 });
267
268 add_version_metadata(&mut response);
269
270 assert!(response.get("_meta").is_some());
271 let meta = response.get("_meta").unwrap();
272 assert!(meta.get("api_version").is_some());
273 assert!(meta.get("server_version").is_some());
274 }
275}