mcp_execution_core/metadata.rs
1//! Structured sidecar metadata describing a server's generated tools.
2//!
3//! `mcp-execution-codegen` emits a `_meta.json` file alongside the generated
4//! TypeScript tool files for each server. `mcp-execution-skill` (and
5//! `mcp-execution-server`) read that file back to build `SKILL.md` and
6//! runtime tool listings, instead of re-parsing the generated `.ts` source.
7//!
8//! This module is the shared wire contract between the two sides: the
9//! producer (codegen) and the consumer (skill/server) both depend on
10//! `mcp-execution-core`, so the schema lives here rather than in either
11//! crate directly.
12//!
13//! # Examples
14//!
15//! ```
16//! use mcp_execution_core::metadata::{ServerMetadata, ToolMetadata, METADATA_SCHEMA_VERSION};
17//! use mcp_execution_core::{ServerId, ToolName};
18//!
19//! let meta = ServerMetadata {
20//! schema_version: METADATA_SCHEMA_VERSION,
21//! server_id: ServerId::new("github").unwrap(),
22//! server_name: "GitHub".to_string(),
23//! server_version: "1.0.0".to_string(),
24//! tools: vec![ToolMetadata {
25//! name: ToolName::new("create_issue").unwrap(),
26//! typescript_name: "createIssue".to_string(),
27//! category: Some("issues".to_string()),
28//! keywords: vec!["create".to_string(), "issue".to_string()],
29//! description: Some("Creates a new issue".to_string()),
30//! parameters: vec![],
31//! }],
32//! };
33//!
34//! let json = serde_json::to_string_pretty(&meta).unwrap();
35//! let round_tripped: ServerMetadata = serde_json::from_str(&json).unwrap();
36//! assert_eq!(round_tripped, meta);
37//! ```
38
39use crate::{ServerId, ToolName};
40use serde::{Deserialize, Serialize};
41
42/// Current schema version of the `_meta.json` sidecar format.
43///
44/// Bump this when making a breaking change to [`ServerMetadata`] or its
45/// nested types, so that a consumer built against an older schema fails
46/// loudly (via a schema-version mismatch check) instead of silently
47/// misinterpreting the new shape.
48///
49/// # Examples
50///
51/// ```
52/// use mcp_execution_core::metadata::METADATA_SCHEMA_VERSION;
53///
54/// assert_eq!(METADATA_SCHEMA_VERSION, 1);
55/// ```
56pub const METADATA_SCHEMA_VERSION: u32 = 1;
57
58/// Filename of the sidecar metadata file emitted alongside generated tool files.
59///
60/// Shared between the producer (`mcp-execution-codegen`) and the consumer
61/// (`mcp-execution-skill`) to avoid a stringly-typed filename duplicated in
62/// two crates.
63///
64/// # Examples
65///
66/// ```
67/// use mcp_execution_core::metadata::METADATA_FILE_NAME;
68///
69/// assert_eq!(METADATA_FILE_NAME, "_meta.json");
70/// ```
71pub const METADATA_FILE_NAME: &str = "_meta.json";
72
73/// Structured sidecar describing one server's generated tools.
74///
75/// Serialized as `_meta.json` by `mcp-execution-codegen` and deserialized by
76/// `mcp-execution-skill` / `mcp-execution-server`, replacing a fragile
77/// regex-based re-parse of the generated TypeScript files.
78///
79/// # Examples
80///
81/// ```
82/// use mcp_execution_core::metadata::{ServerMetadata, METADATA_SCHEMA_VERSION};
83/// use mcp_execution_core::ServerId;
84///
85/// let meta = ServerMetadata {
86/// schema_version: METADATA_SCHEMA_VERSION,
87/// server_id: ServerId::new("github").unwrap(),
88/// server_name: "GitHub".to_string(),
89/// server_version: "1.0.0".to_string(),
90/// tools: vec![],
91/// };
92///
93/// assert_eq!(meta.tools.len(), 0);
94/// ```
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96pub struct ServerMetadata {
97 /// Schema version this sidecar was produced with.
98 ///
99 /// Consumers should compare this against [`METADATA_SCHEMA_VERSION`] and
100 /// fail loudly on a mismatch rather than risk misinterpreting an
101 /// incompatible future shape.
102 pub schema_version: u32,
103
104 /// MCP server identifier (e.g. `github`).
105 ///
106 /// [`ServerId`]'s derived `Serialize`/`Deserialize` round-trip through a plain JSON string
107 /// (single-field newtype structs serialize transparently), so this field's on-the-wire
108 /// shape is unchanged from when it was a bare `String`.
109 pub server_id: ServerId,
110
111 /// Human-readable server name.
112 pub server_name: String,
113
114 /// Server version string, as reported by the MCP server.
115 pub server_version: String,
116
117 /// Metadata for every generated tool, in generation order.
118 pub tools: Vec<ToolMetadata>,
119}
120
121/// Structured metadata for a single generated tool.
122///
123/// # Examples
124///
125/// ```
126/// use mcp_execution_core::metadata::ToolMetadata;
127/// use mcp_execution_core::ToolName;
128///
129/// let tool = ToolMetadata {
130/// name: ToolName::new("create_issue").unwrap(),
131/// typescript_name: "createIssue".to_string(),
132/// category: Some("issues".to_string()),
133/// keywords: vec!["create".to_string(), "issue".to_string()],
134/// description: Some("Creates a new issue".to_string()),
135/// parameters: vec![],
136/// };
137///
138/// assert_eq!(tool.name.as_str(), "create_issue");
139/// ```
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct ToolMetadata {
142 /// Original MCP tool name (the call identifier), unmodified.
143 ///
144 /// [`ToolName`]'s derived `Serialize`/`Deserialize` round-trip through a plain JSON string
145 /// (see [`ServerMetadata::server_id`]'s doc comment), so this field's on-the-wire shape is
146 /// unchanged from when it was a bare `String`.
147 pub name: ToolName,
148
149 /// TypeScript-friendly name (camelCase), matching the generated file's
150 /// basename (e.g. `createIssue` for `createIssue.ts`).
151 pub typescript_name: String,
152
153 /// Optional category for tool grouping.
154 pub category: Option<String>,
155
156 /// Keywords for discovery, split from the source comma-separated string.
157 pub keywords: Vec<String>,
158
159 /// Human-readable tool description, as reported by the MCP server.
160 pub description: Option<String>,
161
162 /// Metadata for each of the tool's input parameters.
163 pub parameters: Vec<ParameterMetadata>,
164}
165
166/// Structured metadata for a single tool parameter.
167///
168/// # Examples
169///
170/// ```
171/// use mcp_execution_core::metadata::ParameterMetadata;
172///
173/// let param = ParameterMetadata {
174/// name: "title".to_string(),
175/// typescript_type: "string".to_string(),
176/// required: true,
177/// description: Some("Issue title".to_string()),
178/// };
179///
180/// assert!(param.required);
181/// ```
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183pub struct ParameterMetadata {
184 /// Parameter name.
185 pub name: String,
186
187 /// TypeScript type (e.g. `string`, `number`, `boolean`).
188 pub typescript_type: String,
189
190 /// Whether the parameter is required.
191 pub required: bool,
192
193 /// Parameter description, sourced from the tool's input JSON Schema.
194 pub description: Option<String>,
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{METADATA_SCHEMA_VERSION, ParameterMetadata, ServerMetadata, ToolMetadata};
200 use crate::{ServerId, ToolName};
201
202 #[test]
203 fn round_trips_through_json() {
204 let meta = ServerMetadata {
205 schema_version: METADATA_SCHEMA_VERSION,
206 server_id: ServerId::new("github").unwrap(),
207 server_name: "GitHub".to_string(),
208 server_version: "1.0.0".to_string(),
209 tools: vec![ToolMetadata {
210 name: ToolName::new("create_issue").unwrap(),
211 typescript_name: "createIssue".to_string(),
212 category: Some("issues".to_string()),
213 keywords: vec!["create".to_string(), "issue".to_string()],
214 description: Some("Creates a new issue".to_string()),
215 parameters: vec![ParameterMetadata {
216 name: "title".to_string(),
217 typescript_type: "string".to_string(),
218 required: true,
219 description: Some("Issue title".to_string()),
220 }],
221 }],
222 };
223
224 let json = serde_json::to_string_pretty(&meta).unwrap();
225 let round_tripped: ServerMetadata = serde_json::from_str(&json).unwrap();
226
227 assert_eq!(round_tripped, meta);
228 }
229
230 #[test]
231 fn deserializes_minimal_tool() {
232 let json = r#"{
233 "schema_version": 1,
234 "server_id": "github",
235 "server_name": "GitHub",
236 "server_version": "1.0.0",
237 "tools": [{
238 "name": "get_user",
239 "typescript_name": "getUser",
240 "category": null,
241 "keywords": [],
242 "description": null,
243 "parameters": []
244 }]
245 }"#;
246
247 let meta: ServerMetadata = serde_json::from_str(json).unwrap();
248
249 assert_eq!(meta.tools.len(), 1);
250 assert!(meta.tools[0].category.is_none());
251 assert!(meta.tools[0].keywords.is_empty());
252 }
253}