Skip to content

Commit c0e1f00

Browse files
its-mashclaude
andcommitted
feat: support default values for input definitions
Add `default` field to domain InputDefinition so server definitions can specify fallback values for inputs not provided by the user. Transport resolution now merges defaults before resolving placeholders, with user-provided values always taking priority. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9bfd6a4 commit c0e1f00

3 files changed

Lines changed: 434 additions & 8 deletions

File tree

crates/mcpmux-core/src/domain/config.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ impl UserServerEntry {
254254
required: true,
255255
secret: true,
256256
description: None,
257+
default: None,
257258
placeholder: None,
258259
obtain_url: None,
259260
obtain_instructions: None,
@@ -444,6 +445,7 @@ mod tests {
444445
required: false,
445446
secret: false,
446447
description: Some("Custom description".to_string()),
448+
default: None,
447449
placeholder: None,
448450
obtain_url: None,
449451
obtain_instructions: None,
@@ -649,4 +651,133 @@ mod tests {
649651
// Explicit OAuth should not be overridden
650652
assert!(matches!(def.auth, Some(AuthConfig::Oauth)));
651653
}
654+
655+
#[test]
656+
fn test_input_default_value_parsed_from_json() {
657+
let json = r#"{
658+
"mcpServers": {
659+
"test-server": {
660+
"command": "node",
661+
"args": ["server.js"],
662+
"env": {
663+
"LOG_LEVEL": "${input:LOG_LEVEL}"
664+
},
665+
"metadata": {
666+
"inputs": [
667+
{
668+
"id": "LOG_LEVEL",
669+
"label": "Log Level",
670+
"type": "text",
671+
"required": false,
672+
"secret": false,
673+
"default": "info"
674+
}
675+
]
676+
}
677+
}
678+
}
679+
}"#;
680+
681+
let config: UserSpaceConfig = serde_json::from_str(json).unwrap();
682+
let definitions =
683+
config.to_server_definitions("test-space", PathBuf::from("/test/path.json"));
684+
685+
assert_eq!(definitions.len(), 1);
686+
let inputs = &definitions[0].transport.metadata().inputs;
687+
assert_eq!(inputs.len(), 1);
688+
assert_eq!(inputs[0].id, "LOG_LEVEL");
689+
assert_eq!(inputs[0].default, Some("info".to_string()));
690+
}
691+
692+
#[test]
693+
fn test_explicit_input_with_default_takes_precedence_over_autodiscovery() {
694+
let entry = UserServerEntry {
695+
command: Some("node".to_string()),
696+
args: None,
697+
env: Some(HashMap::from([(
698+
"LOG_LEVEL".to_string(),
699+
"${input:LOG_LEVEL}".to_string(),
700+
)])),
701+
url: None,
702+
headers: None,
703+
name: None,
704+
description: None,
705+
icon: None,
706+
alias: None,
707+
auth: None,
708+
metadata: Some(UserServerMetadata {
709+
inputs: Some(vec![InputDefinition {
710+
id: "LOG_LEVEL".to_string(),
711+
label: "Log Level".to_string(),
712+
r#type: "text".to_string(),
713+
required: false,
714+
secret: false,
715+
description: None,
716+
default: Some("info".to_string()),
717+
placeholder: None,
718+
obtain_url: None,
719+
obtain_instructions: None,
720+
}]),
721+
publisher: None,
722+
}),
723+
};
724+
725+
let (_, inputs) = entry.resolve_transport_and_inputs();
726+
727+
assert_eq!(inputs.len(), 1);
728+
assert_eq!(inputs[0].id, "LOG_LEVEL");
729+
assert_eq!(inputs[0].default, Some("info".to_string()));
730+
// Should use explicit definition's type, not auto-discovered "password"
731+
assert_eq!(inputs[0].r#type, "text");
732+
assert!(!inputs[0].required);
733+
assert!(!inputs[0].secret);
734+
}
735+
736+
#[test]
737+
fn test_auto_discovered_inputs_have_no_default() {
738+
let entry = UserServerEntry {
739+
command: Some("node".to_string()),
740+
args: None,
741+
env: Some(HashMap::from([(
742+
"API_KEY".to_string(),
743+
"${input:API_KEY}".to_string(),
744+
)])),
745+
url: None,
746+
headers: None,
747+
name: None,
748+
description: None,
749+
icon: None,
750+
alias: None,
751+
auth: None,
752+
metadata: None,
753+
};
754+
755+
let (_, inputs) = entry.resolve_transport_and_inputs();
756+
757+
assert_eq!(inputs.len(), 1);
758+
assert_eq!(inputs[0].id, "API_KEY");
759+
assert_eq!(inputs[0].default, None);
760+
}
761+
762+
#[test]
763+
fn test_input_default_serializes_roundtrip() {
764+
let input = InputDefinition {
765+
id: "PORT".to_string(),
766+
label: "Port".to_string(),
767+
r#type: "number".to_string(),
768+
required: false,
769+
secret: false,
770+
description: None,
771+
default: Some("8080".to_string()),
772+
placeholder: None,
773+
obtain_url: None,
774+
obtain_instructions: None,
775+
};
776+
777+
let json = serde_json::to_string(&input).unwrap();
778+
let deserialized: InputDefinition = serde_json::from_str(&json).unwrap();
779+
780+
assert_eq!(deserialized.id, "PORT");
781+
assert_eq!(deserialized.default, Some("8080".to_string()));
782+
}
652783
}

crates/mcpmux-core/src/domain/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub struct InputDefinition {
146146
#[serde(default)]
147147
pub secret: bool,
148148
pub description: Option<String>,
149+
pub default: Option<String>,
149150
pub placeholder: Option<String>,
150151

151152
// Additional helpful metadata for acquiring credentials

0 commit comments

Comments
 (0)