33# stdlib imports
44import json
55import os
6+ import re
67import shlex
78from pathlib import Path
89from typing import Annotated , Any , Literal
1516from pydantic import BaseModel , Field , field_validator , model_validator
1617
1718
19+ class InputDefinition (BaseModel ):
20+ """Definition of an input parameter."""
21+
22+ type : Literal ["promptString" ] = "promptString"
23+ id : str
24+ description : str | None = None
25+ password : bool = False
26+
27+
1828class MCPServerConfig (BaseModel ):
1929 """Base class for MCP server configurations."""
2030
@@ -83,6 +93,7 @@ class MCPServersConfig(BaseModel):
8393 """Configuration for multiple MCP servers."""
8494
8595 servers : dict [str , ServerConfigUnion ]
96+ inputs : list [InputDefinition ] | None = None
8697
8798 @model_validator (mode = "before" )
8899 @classmethod
@@ -115,14 +126,80 @@ def infer_server_types(cls, servers_data: dict[str, Any]) -> dict[str, Any]:
115126
116127 return servers_data
117128
129+ def get_required_inputs (self ) -> list [str ]:
130+ """Get list of input IDs that are defined in the inputs section."""
131+ if not self .inputs :
132+ return []
133+ return [input_def .id for input_def in self .inputs ]
134+
135+ def validate_inputs (self , provided_inputs : dict [str , str ]) -> list [str ]:
136+ """Validate provided inputs against input definitions.
137+
138+ Returns list of missing required input IDs.
139+ """
140+ if not self .inputs :
141+ return []
142+
143+ required_input_ids = self .get_required_inputs ()
144+ missing_inputs = []
145+
146+ for input_id in required_input_ids :
147+ if input_id not in provided_inputs :
148+ missing_inputs .append (input_id )
149+
150+ return missing_inputs
151+
152+ def get_input_description (self , input_id : str ) -> str | None :
153+ """Get the description for a specific input ID."""
154+ if not self .inputs :
155+ return None
156+
157+ for input_def in self .inputs :
158+ if input_def .id == input_id :
159+ return input_def .description
160+
161+ return None
162+
163+ @classmethod
164+ def _substitute_inputs (cls , data : Any , inputs : dict [str , str ]) -> Any :
165+ """Recursively substitute ${input:key} placeholders with values from inputs dict."""
166+ if isinstance (data , str ):
167+ # Replace ${input:key} patterns with values from inputs
168+ def replace_input (match : re .Match [str ]) -> str :
169+ key = match .group (1 )
170+ if key in inputs :
171+ return inputs [key ]
172+ else :
173+ raise ValueError (f"Missing input value for key: '{ key } '" )
174+
175+ return re .sub (r"\$\{input:([^}]+)\}" , replace_input , data )
176+
177+ elif isinstance (data , dict ):
178+ result = {} # type: ignore
179+ for k , v in data .items (): # type: ignore
180+ result [k ] = cls ._substitute_inputs (v , inputs ) # type: ignore
181+ return result
182+
183+ elif isinstance (data , list ):
184+ result = [] # type: ignore
185+ for item in data : # type: ignore
186+ result .append (cls ._substitute_inputs (item , inputs )) # type: ignore
187+ return result
188+
189+ else :
190+ return data
191+
118192 @classmethod
119- def from_file (cls , config_path : Path | str , use_pyyaml : bool = False ) -> "MCPServersConfig" :
193+ def from_file (
194+ cls , config_path : Path | str , use_pyyaml : bool = False , inputs : dict [str , str ] | None = None
195+ ) -> "MCPServersConfig" :
120196 """Load configuration from a JSON or YAML file.
121197
122198 Args:
123199 config_path: Path to the configuration file
124200 use_pyyaml: If True, force use of PyYAML parser. Defaults to False.
125201 Also automatically used for .yaml/.yml files.
202+ inputs: Dictionary of input values to substitute for ${input:key} placeholders
126203 """
127204
128205 config_path = os .path .expandvars (config_path ) # Expand environment variables like $HOME
@@ -136,6 +213,26 @@ def from_file(cls, config_path: Path | str, use_pyyaml: bool = False) -> "MCPSer
136213 if should_use_yaml :
137214 if not yaml :
138215 raise ImportError ("PyYAML is required to parse YAML files. " )
139- return cls . model_validate ( yaml .safe_load (config_file ) )
216+ data = yaml .safe_load (config_file )
140217 else :
141- return cls .model_validate (json .load (config_file ))
218+ data = json .load (config_file )
219+
220+ # Create a preliminary config to validate inputs if they're defined
221+ preliminary_config = cls .model_validate (data )
222+
223+ # Validate inputs if provided and input definitions exist
224+ if inputs is not None and preliminary_config .inputs :
225+ missing_inputs = preliminary_config .validate_inputs (inputs )
226+ if missing_inputs :
227+ descriptions = []
228+ for input_id in missing_inputs :
229+ desc = preliminary_config .get_input_description (input_id )
230+ descriptions .append (f" - { input_id } : { desc or 'No description' } " )
231+
232+ raise ValueError (f"Missing required input values:\n " + "\n " .join (descriptions ))
233+
234+ # Substitute input placeholders if inputs provided
235+ if inputs :
236+ data = cls ._substitute_inputs (data , inputs )
237+
238+ return cls .model_validate (data )
0 commit comments