Skip to main content

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}