@@ -13,14 +13,14 @@ use std::collections::BTreeMap;
1313
1414use crate :: spec_parser:: {
1515 parse_spec_file, ApiSpec , DispatchConfig , Message , MiddlewareConfig , Parameter , RequestBody ,
16- SpecFormat ,
16+ ResponseContent , SpecFormat ,
1717} ;
1818
1919use crate :: error:: { CompileError , CompileWarning } ;
2020use crate :: manifest:: ProjectManifest ;
2121
2222/// Current artifact format version.
23- pub const ARTIFACT_VERSION : u32 = 2 ;
23+ pub const ARTIFACT_VERSION : u32 = 3 ;
2424
2525/// Options for compilation.
2626#[ derive( Debug , Clone ) ]
@@ -65,6 +65,7 @@ pub const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
6565const KNOWN_EXTENSIONS : & [ & str ] = & [
6666 "x-barbacane-dispatch" , // Operation level - dispatcher config (required)
6767 "x-barbacane-middlewares" , // Root or operation level - middleware chain
68+ "x-barbacane-mcp" , // Root or operation level - MCP server config
6869] ;
6970
7071/// Result of compilation including the manifest and any warnings.
@@ -92,6 +93,22 @@ pub struct Manifest {
9293 pub artifact_hash : String ,
9394 /// Build provenance metadata (git commit, CI source, etc.).
9495 pub provenance : Provenance ,
96+ /// MCP server configuration (from root-level x-barbacane-mcp).
97+ #[ serde( default ) ]
98+ pub mcp : McpConfig ,
99+ }
100+
101+ /// MCP server configuration extracted from `x-barbacane-mcp`.
102+ #[ derive( Debug , Clone , Default , Serialize , Deserialize ) ]
103+ pub struct McpConfig {
104+ /// Whether MCP is enabled globally.
105+ pub enabled : bool ,
106+ /// MCP server name (defaults to info.title).
107+ #[ serde( default ) ]
108+ pub server_name : Option < String > ,
109+ /// MCP server version (defaults to info.version).
110+ #[ serde( default ) ]
111+ pub server_version : Option < String > ,
95112}
96113
97114/// Build provenance metadata embedded in the manifest.
@@ -164,6 +181,12 @@ pub struct CompiledOperation {
164181 /// HTTP method (OpenAPI: "GET", AsyncAPI: "SEND"/"RECEIVE").
165182 pub method : String ,
166183 pub operation_id : Option < String > ,
184+ /// Operation summary (short description).
185+ #[ serde( default ) ]
186+ pub summary : Option < String > ,
187+ /// Operation description (detailed).
188+ #[ serde( default ) ]
189+ pub description : Option < String > ,
167190 /// Parameters for validation (path, query, header).
168191 pub parameters : Vec < Parameter > ,
169192 /// Request body schema for validation.
@@ -186,6 +209,15 @@ pub struct CompiledOperation {
186209 /// Protocol bindings (AsyncAPI: kafka, nats, mqtt, amqp, ws).
187210 #[ serde( default , skip_serializing_if = "BTreeMap::is_empty" ) ]
188211 pub bindings : BTreeMap < String , serde_json:: Value > ,
212+ /// Response definitions keyed by status code.
213+ #[ serde( default , skip_serializing_if = "BTreeMap::is_empty" ) ]
214+ pub responses : BTreeMap < String , ResponseContent > ,
215+ /// Whether this operation is exposed as an MCP tool.
216+ #[ serde( default ) ]
217+ pub mcp_enabled : Option < bool > ,
218+ /// MCP-specific tool description override.
219+ #[ serde( default ) ]
220+ pub mcp_description : Option < String > ,
189221}
190222
191223/// Compile one or more spec files into a .bca artifact.
@@ -430,6 +462,9 @@ fn compile_inner(
430462 let mut seen_structural: HashMap < ( String , String ) , ( String , String ) > = HashMap :: new ( ) ;
431463 let mut seen_operation_ids: HashMap < String , String > = HashMap :: new ( ) ;
432464
465+ // Extract root-level MCP config from first spec that has it
466+ let root_mcp_config = extract_root_mcp_config ( specs) ;
467+
433468 for ( spec, _, _) in specs {
434469 let spec_file = spec. filename . as_deref ( ) . unwrap_or ( "unknown" ) ;
435470
@@ -568,11 +603,38 @@ fn compile_inner(
568603 }
569604 }
570605
606+ // Resolve MCP enabled state for this operation
607+ let ( mcp_enabled, mcp_description) =
608+ resolve_mcp_config ( & root_mcp_config, op. extensions . get ( "x-barbacane-mcp" ) ) ;
609+
610+ // MCP warnings
611+ if mcp_enabled == Some ( true ) {
612+ if op. operation_id . is_none ( ) {
613+ warnings. push ( CompileWarning {
614+ code : "E1060" . to_string ( ) ,
615+ message : "operation without operationId cannot be exposed as MCP tool"
616+ . to_string ( ) ,
617+ location : Some ( location. clone ( ) ) ,
618+ } ) ;
619+ }
620+ if op. summary . is_none ( ) && op. description . is_none ( ) {
621+ warnings. push ( CompileWarning {
622+ code : "E1061" . to_string ( ) ,
623+ message :
624+ "MCP-enabled operation has no summary or description for tool metadata"
625+ . to_string ( ) ,
626+ location : Some ( location. clone ( ) ) ,
627+ } ) ;
628+ }
629+ }
630+
571631 operations. push ( CompiledOperation {
572632 index : operations. len ( ) ,
573633 path : op. path . clone ( ) ,
574634 method : op. method . clone ( ) ,
575635 operation_id : op. operation_id . clone ( ) ,
636+ summary : op. summary . clone ( ) ,
637+ description : op. description . clone ( ) ,
576638 parameters : op. parameters . clone ( ) ,
577639 request_body : op. request_body . clone ( ) ,
578640 dispatch,
@@ -581,6 +643,9 @@ fn compile_inner(
581643 sunset : op. sunset . clone ( ) ,
582644 messages : op. messages . clone ( ) ,
583645 bindings : op. bindings . clone ( ) ,
646+ responses : op. responses . clone ( ) ,
647+ mcp_enabled,
648+ mcp_description,
584649 } ) ;
585650 }
586651 }
@@ -652,6 +717,20 @@ fn compile_inner(
652717 source : options. provenance_source . clone ( ) ,
653718 } ;
654719
720+ // Build MCP config for manifest, defaulting server_name/server_version from spec info
721+ let mcp = {
722+ let mut cfg = root_mcp_config. clone ( ) ;
723+ if cfg. enabled {
724+ if cfg. server_name . is_none ( ) {
725+ cfg. server_name = specs. first ( ) . map ( |( s, _, _) | s. title . clone ( ) ) ;
726+ }
727+ if cfg. server_version . is_none ( ) {
728+ cfg. server_version = specs. first ( ) . map ( |( s, _, _) | s. api_version . clone ( ) ) ;
729+ }
730+ }
731+ cfg
732+ } ;
733+
655734 let manifest = Manifest {
656735 barbacane_artifact_version : ARTIFACT_VERSION ,
657736 compiled_at : now_utc_iso8601 ( ) ,
@@ -662,6 +741,7 @@ fn compile_inner(
662741 plugins : bundled_plugins,
663742 artifact_hash,
664743 provenance,
744+ mcp,
665745 } ;
666746
667747 let manifest_json = serde_json:: to_string_pretty ( & manifest) ?;
@@ -1031,6 +1111,53 @@ mod hex {
10311111 }
10321112}
10331113
1114+ /// Extract root-level `x-barbacane-mcp` config from the first spec that defines it.
1115+ fn extract_root_mcp_config ( specs : & [ ( ApiSpec , String , String ) ] ) -> McpConfig {
1116+ for ( spec, _, _) in specs {
1117+ if let Some ( mcp_value) = spec. extensions . get ( "x-barbacane-mcp" ) {
1118+ let enabled = mcp_value
1119+ . get ( "enabled" )
1120+ . and_then ( |v| v. as_bool ( ) )
1121+ . unwrap_or ( false ) ;
1122+ let server_name = mcp_value
1123+ . get ( "server_name" )
1124+ . and_then ( |v| v. as_str ( ) )
1125+ . map ( |s| s. to_string ( ) ) ;
1126+ let server_version = mcp_value
1127+ . get ( "server_version" )
1128+ . and_then ( |v| v. as_str ( ) )
1129+ . map ( |s| s. to_string ( ) ) ;
1130+ return McpConfig {
1131+ enabled,
1132+ server_name,
1133+ server_version,
1134+ } ;
1135+ }
1136+ }
1137+ McpConfig :: default ( )
1138+ }
1139+
1140+ /// Resolve MCP enabled/description for a single operation from root + operation-level config.
1141+ fn resolve_mcp_config (
1142+ root : & McpConfig ,
1143+ op_extension : Option < & serde_json:: Value > ,
1144+ ) -> ( Option < bool > , Option < String > ) {
1145+ if let Some ( ext) = op_extension {
1146+ let enabled = ext. get ( "enabled" ) . and_then ( |v| v. as_bool ( ) ) ;
1147+ let description = ext
1148+ . get ( "description" )
1149+ . and_then ( |v| v. as_str ( ) )
1150+ . map ( |s| s. to_string ( ) ) ;
1151+ // Operation-level enabled wins; if not set, inherit from root
1152+ let resolved_enabled = enabled. or ( if root. enabled { Some ( true ) } else { None } ) ;
1153+ ( resolved_enabled, description)
1154+ } else if root. enabled {
1155+ ( Some ( true ) , None )
1156+ } else {
1157+ ( None , None )
1158+ }
1159+ }
1160+
10341161#[ cfg( test) ]
10351162mod tests {
10361163 use super :: * ;
@@ -2474,4 +2601,99 @@ paths:
24742601 "Provenance metadata must not affect artifact hash"
24752602 ) ;
24762603 }
2604+
2605+ // --- MCP config tests ---
2606+
2607+ #[ test]
2608+ fn extract_root_mcp_config_enabled ( ) {
2609+ let spec = ApiSpec {
2610+ filename : None ,
2611+ format : SpecFormat :: OpenApi ,
2612+ version : "3.1.0" . to_string ( ) ,
2613+ title : "My API" . to_string ( ) ,
2614+ api_version : "2.0.0" . to_string ( ) ,
2615+ operations : vec ! [ ] ,
2616+ global_middlewares : vec ! [ ] ,
2617+ extensions : BTreeMap :: from ( [ (
2618+ "x-barbacane-mcp" . to_string ( ) ,
2619+ serde_json:: json!( {
2620+ "enabled" : true ,
2621+ "server_name" : "Custom Name"
2622+ } ) ,
2623+ ) ] ) ,
2624+ } ;
2625+ let specs = vec ! [ ( spec, String :: new( ) , String :: new( ) ) ] ;
2626+ let cfg = extract_root_mcp_config ( & specs) ;
2627+ assert ! ( cfg. enabled) ;
2628+ assert_eq ! ( cfg. server_name. as_deref( ) , Some ( "Custom Name" ) ) ;
2629+ assert ! ( cfg. server_version. is_none( ) ) ;
2630+ }
2631+
2632+ #[ test]
2633+ fn extract_root_mcp_config_disabled_by_default ( ) {
2634+ let spec = ApiSpec {
2635+ filename : None ,
2636+ format : SpecFormat :: OpenApi ,
2637+ version : "3.1.0" . to_string ( ) ,
2638+ title : "Test" . to_string ( ) ,
2639+ api_version : "1.0.0" . to_string ( ) ,
2640+ operations : vec ! [ ] ,
2641+ global_middlewares : vec ! [ ] ,
2642+ extensions : BTreeMap :: new ( ) ,
2643+ } ;
2644+ let specs = vec ! [ ( spec, String :: new( ) , String :: new( ) ) ] ;
2645+ let cfg = extract_root_mcp_config ( & specs) ;
2646+ assert ! ( !cfg. enabled) ;
2647+ }
2648+
2649+ #[ test]
2650+ fn resolve_mcp_config_inherits_from_root ( ) {
2651+ let root = McpConfig {
2652+ enabled : true ,
2653+ server_name : None ,
2654+ server_version : None ,
2655+ } ;
2656+ // No operation-level extension → inherits root
2657+ let ( enabled, desc) = resolve_mcp_config ( & root, None ) ;
2658+ assert_eq ! ( enabled, Some ( true ) ) ;
2659+ assert ! ( desc. is_none( ) ) ;
2660+ }
2661+
2662+ #[ test]
2663+ fn resolve_mcp_config_operation_overrides_root ( ) {
2664+ let root = McpConfig {
2665+ enabled : true ,
2666+ server_name : None ,
2667+ server_version : None ,
2668+ } ;
2669+ // Operation opts out
2670+ let ext = serde_json:: json!( { "enabled" : false } ) ;
2671+ let ( enabled, _) = resolve_mcp_config ( & root, Some ( & ext) ) ;
2672+ assert_eq ! ( enabled, Some ( false ) ) ;
2673+ }
2674+
2675+ #[ test]
2676+ fn resolve_mcp_config_operation_description_override ( ) {
2677+ let root = McpConfig {
2678+ enabled : true ,
2679+ server_name : None ,
2680+ server_version : None ,
2681+ } ;
2682+ let ext = serde_json:: json!( { "description" : "Custom tool description" } ) ;
2683+ let ( enabled, desc) = resolve_mcp_config ( & root, Some ( & ext) ) ;
2684+ // enabled not set at operation level → inherits root true
2685+ assert_eq ! ( enabled, Some ( true ) ) ;
2686+ assert_eq ! ( desc. as_deref( ) , Some ( "Custom tool description" ) ) ;
2687+ }
2688+
2689+ #[ test]
2690+ fn resolve_mcp_config_root_disabled_no_inheritance ( ) {
2691+ let root = McpConfig {
2692+ enabled : false ,
2693+ server_name : None ,
2694+ server_version : None ,
2695+ } ;
2696+ let ( enabled, _) = resolve_mcp_config ( & root, None ) ;
2697+ assert ! ( enabled. is_none( ) ) ;
2698+ }
24772699}
0 commit comments