Skip to content

Commit b2b43bb

Browse files
authored
Merge pull request #58 from barbacane-dev/feat/mcp-server
feat: add MCP server support (ADR-0025)
2 parents d9bca3a + ad16f7f commit b2b43bb

27 files changed

Lines changed: 3250 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/barbacane-compiler/src/artifact.rs

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ use std::collections::BTreeMap;
1313

1414
use crate::spec_parser::{
1515
parse_spec_file, ApiSpec, DispatchConfig, Message, MiddlewareConfig, Parameter, RequestBody,
16-
SpecFormat,
16+
ResponseContent, SpecFormat,
1717
};
1818

1919
use crate::error::{CompileError, CompileWarning};
2020
use 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");
6565
const 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)]
10351162
mod 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
}

crates/barbacane-compiler/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pub mod spec_parser;
1313
pub use artifact::{
1414
compile, compile_with_manifest, load_manifest, load_plugins, load_routes, load_specs,
1515
BundledPlugin, CompileOptions, CompileResult, CompiledOperation, CompiledRoutes, LoadedPlugin,
16-
Manifest, PluginBundle, PluginCapabilities, Provenance, SourceSpec, ARTIFACT_VERSION,
17-
COMPILER_VERSION,
16+
Manifest, McpConfig, PluginBundle, PluginCapabilities, Provenance, SourceSpec,
17+
ARTIFACT_VERSION, COMPILER_VERSION,
1818
};
1919
pub use error::{CompileError, CompileWarning};
2020
pub use manifest::{
@@ -23,5 +23,6 @@ pub use manifest::{
2323
// Re-export spec-parser types for convenience
2424
pub use spec_parser::{
2525
parse_spec, parse_spec_file, ApiSpec, AsyncAction, Channel, ContentSchema, DispatchConfig,
26-
Message, MiddlewareConfig, Operation, Parameter, ParseError, RequestBody, SpecFormat,
26+
Message, MiddlewareConfig, Operation, Parameter, ParseError, RequestBody, ResponseContent,
27+
SpecFormat,
2728
};

0 commit comments

Comments
 (0)