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