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}