@@ -188,108 +188,39 @@ def test_servers_field_takes_precedence():
188188 assert len (config .servers ) == 1
189189
190190
191- def test_input_substitution ():
192- """Test that ${input:key} placeholders are substituted correctly."""
193- config_data = {
194- "servers" : {
195- "azure_server" : {
196- "type" : "sse" ,
197- "url" : "https://${input:app-name}.azurewebsites.net/mcp/sse" ,
198- "headers" : {"Authorization" : "Bearer ${input:api-token}" , "X-Custom-Header" : "${input:custom-value}" },
199- },
200- "stdio_server" : {
201- "type" : "stdio" ,
202- "command" : "python -m ${input:module-name}" ,
203- "args" : ["--config" , "${input:config-file}" ],
204- "env" : {"API_KEY" : "${input:api-key}" , "ENV" : "${input:environment}" },
205- "isActive" : True ,
206- },
207- }
208- }
209-
210- inputs = {
211- "app-name" : "my-function-app" ,
212- "api-token" : "abc123token" ,
213- "custom-value" : "custom-header-value" ,
214- "module-name" : "my_server" ,
215- "config-file" : "/path/to/config.json" ,
216- "api-key" : "secret-api-key" ,
217- "environment" : "production" ,
218- }
219-
220- # Test substitution
221- substituted = MCPServersConfig ._substitute_inputs (config_data , inputs )
222- config = MCPServersConfig .model_validate (substituted )
223-
224- # Test SSE server substitution
225- sse_server = config .servers ["azure_server" ]
226- assert isinstance (sse_server , SSEServerConfig )
227- assert sse_server .url == "https://my-function-app.azurewebsites.net/mcp/sse"
228- assert sse_server .headers == {"Authorization" : "Bearer abc123token" , "X-Custom-Header" : "custom-header-value" }
229-
230- # Test stdio server substitution
231- stdio_server = config .servers ["stdio_server" ]
232- assert isinstance (stdio_server , StdioServerConfig )
233- assert stdio_server .command == "python -m my_server"
234- assert stdio_server .args == ["--config" , "/path/to/config.json" ]
235- assert stdio_server .env == {"API_KEY" : "secret-api-key" , "ENV" : "production" }
236-
237-
238- def test_input_substitution_missing_key ():
239- """Test that missing input keys raise appropriate errors."""
240- config_data = {"servers" : {"test_server" : {"type" : "sse" , "url" : "https://${input:missing-key}.example.com" }}}
241-
242- inputs = {"other-key" : "value" }
243-
244- with pytest .raises (ValueError , match = "Missing input value for key: 'missing-key'" ):
245- MCPServersConfig ._substitute_inputs (config_data , inputs )
246-
247-
248- def test_input_substitution_partial ():
249- """Test that only specified placeholders are substituted."""
250- config_data = {
251- "servers" : {
252- "test_server" : {
253- "type" : "sse" ,
254- "url" : "https://${input:app-name}.example.com/api/${input:version}" ,
255- "headers" : {"Static-Header" : "static-value" , "Dynamic-Header" : "${input:token}" },
256- }
257- }
258- }
259-
260- inputs = {
261- "app-name" : "myapp" ,
262- "token" : "secret123" ,
263- # Note: 'version' is intentionally missing
264- }
265-
266- with pytest .raises (ValueError , match = "Missing input value for key: 'version'" ):
267- MCPServersConfig ._substitute_inputs (config_data , inputs )
268-
269-
270191def test_from_file_with_inputs (tmp_path : Path ):
271192 """Test loading config from file with input substitution."""
272193 # Create test config file
273194 config_content = {
195+ "inputs" : [
196+ {"id" : "host" , "description" : "Server hostname" },
197+ {"id" : "token" , "description" : "API token" },
198+ ],
274199 "servers" : {
275200 "dynamic_server" : {
276201 "type" : "streamable_http" ,
277202 "url" : "https://${input:host}/mcp/api" ,
278203 "headers" : {"Authorization" : "Bearer ${input:token}" },
279204 }
280- }
205+ },
281206 }
282207
283208 config_file = tmp_path / "test_config.json"
284209 with open (config_file , "w" ) as f :
285210 json .dump (config_content , f )
286211
287- inputs = {"host" : "api.example.com" , "token" : "test-token-123" }
212+ config = MCPServersConfig .from_file (config_file )
213+
214+ assert config .get_required_inputs () == ["host" , "token" ]
215+ assert config .inputs is not None
216+ assert config .inputs [0 ].id == "host"
217+ assert config .inputs [1 ].id == "token"
218+ assert config .inputs [0 ].description == "Server hostname"
219+ assert config .inputs [1 ].description == "API token"
288220
289- # Load with input substitution
290- config = MCPServersConfig . from_file ( config_file , inputs = inputs )
221+ input_values = { "host" : "api.example.com" , "token" : "test-token-123" }
222+ server = config . server ( "dynamic_server" , input_values = input_values )
291223
292- server = config .servers ["dynamic_server" ]
293224 assert isinstance (server , StreamableHTTPServerConfig )
294225 assert server .url == "https://api.example.com/mcp/api"
295226 assert server .headers == {"Authorization" : "Bearer test-token-123" }
@@ -324,6 +255,16 @@ def test_from_file_without_inputs(tmp_path: Path):
324255def test_input_substitution_yaml_file (tmp_path : Path ):
325256 """Test input substitution with YAML files."""
326257 yaml_content = """
258+ inputs:
259+ - type: promptString
260+ id: module
261+ description: Python module to run
262+ - type: promptString
263+ id: port
264+ description: Port to run the server on
265+ - type: promptString
266+ id: debug
267+ description: Debug mode
327268servers:
328269 yaml_server:
329270 type: stdio
@@ -336,13 +277,23 @@ def test_input_substitution_yaml_file(tmp_path: Path):
336277"""
337278
338279 config_file = tmp_path / "test_config.yaml"
339- config_file .write_text (yaml_content )
280+ assert config_file .write_text (yaml_content )
281+
282+ config = MCPServersConfig .from_file (config_file )
340283
341- inputs = {"module" : "test_server" , "port" : "8080" , "debug" : "true" }
284+ assert config .get_required_inputs () == ["module" , "port" , "debug" ]
285+ assert config .inputs is not None
286+ assert len (config .inputs ) == 3
287+ assert config .inputs [0 ].id == "module"
288+ assert config .inputs [0 ].description == "Python module to run"
289+ assert config .inputs [1 ].id == "port"
290+ assert config .inputs [1 ].description == "Port to run the server on"
291+ assert config .inputs [2 ].id == "debug"
292+ assert config .inputs [2 ].description == "Debug mode"
342293
343- config = MCPServersConfig .from_file (config_file , inputs = inputs )
294+ input_values = {"module" : "test_server" , "port" : "8080" , "debug" : "true" }
295+ server = config .server ("yaml_server" , input_values = input_values )
344296
345- server = config .servers ["yaml_server" ]
346297 assert isinstance (server , StdioServerConfig )
347298 assert server .command == "python -m test_server"
348299 assert server .args == ["--port" , "8080" ]
@@ -373,15 +324,14 @@ def test_input_definitions_parsing():
373324 config = MCPServersConfig .model_validate (config_data )
374325
375326 # Test input definitions are parsed correctly
327+ assert config .get_required_inputs () == ["functionapp-name" , "api-token" ]
376328 assert config .inputs is not None
377329 assert len (config .inputs ) == 2
378-
379330 app_name_input = config .inputs [0 ]
380331 assert app_name_input .id == "functionapp-name"
381332 assert app_name_input .description == "Azure Functions App Name"
382333 assert app_name_input .password is False
383334 assert app_name_input .type == "promptString"
384-
385335 api_token_input = config .inputs [1 ]
386336 assert api_token_input .id == "api-token"
387337 assert api_token_input .description == "API Token for authentication"
@@ -401,19 +351,17 @@ def test_get_required_inputs():
401351 }
402352
403353 config = MCPServersConfig .model_validate (config_data )
404- required_inputs = config .get_required_inputs ()
405354
406- assert required_inputs == ["input1" , "input2" , "input3" ]
355+ assert config . get_required_inputs () == ["input1" , "input2" , "input3" ]
407356
408357
409358def test_get_required_inputs_no_inputs_defined ():
410359 """Test getting required inputs when no inputs are defined."""
411360 config_data = {"servers" : {"test_server" : {"type" : "stdio" , "command" : "python test.py" }}}
412361
413362 config = MCPServersConfig .model_validate (config_data )
414- required_inputs = config .get_required_inputs ()
415363
416- assert required_inputs == []
364+ assert config . get_required_inputs () == []
417365
418366
419367def test_get_required_inputs_empty_inputs_list ():
@@ -424,10 +372,9 @@ def test_get_required_inputs_empty_inputs_list():
424372 }
425373
426374 config = MCPServersConfig .model_validate (config_data )
427- required_inputs = config .get_required_inputs ()
428- assert config .validate_inputs ({}) == []
429375
430- assert required_inputs == []
376+ assert config .validate_inputs ({}) == []
377+ assert config .get_required_inputs () == []
431378 assert config .inputs == [] # Verify inputs is actually an empty list, not None
432379
433380
@@ -513,12 +460,19 @@ def test_from_file_with_input_validation_success(tmp_path: Path):
513460 with open (config_file , "w" ) as f :
514461 json .dump (config_content , f )
515462
516- inputs = {"app-name" : "myapp" , "env" : "prod" }
463+ config = MCPServersConfig .from_file (config_file )
464+
465+ assert config .get_required_inputs () == ["app-name" , "env" ]
466+ assert config .inputs is not None
467+ assert len (config .inputs ) == 2
468+ assert config .inputs [0 ].id == "app-name"
469+ assert config .inputs [0 ].description == "Application name"
470+ assert config .inputs [1 ].id == "env"
471+ assert config .inputs [1 ].description == "Environment (dev/prod)"
517472
518- # Should load successfully with all required inputs provided
519- config = MCPServersConfig . from_file ( config_file , inputs = inputs )
473+ input_values = { "app-name" : "myapp" , "env" : "prod" }
474+ server = config . server ( "app_server" , input_values = input_values )
520475
521- server = config .servers ["app_server" ]
522476 assert isinstance (server , StreamableHTTPServerConfig )
523477 assert server .url == "https://myapp-prod.example.com/mcp/api"
524478
@@ -537,13 +491,15 @@ def test_from_file_with_input_validation_failure(tmp_path: Path):
537491 with open (config_file , "w" ) as f :
538492 json .dump (config_content , f )
539493
540- inputs = {
494+ inputs : dict [ str , str ] = {
541495 # Missing 'required-key' and 'optional-host'
542496 }
543497
544498 # Should raise ValueError with helpful error message
545499 with pytest .raises (ValueError , match = "Missing required input values" ):
546- MCPServersConfig .from_file (config_file , inputs = inputs )
500+ config = MCPServersConfig .from_file (config_file )
501+ server = config .server ("test_server" , input_values = inputs )
502+ assert server
547503
548504
549505def test_from_file_without_input_definitions_no_validation (tmp_path : Path ):
@@ -556,10 +512,11 @@ def test_from_file_without_input_definitions_no_validation(tmp_path: Path):
556512 with open (config_file , "w" ) as f :
557513 json .dump (config_content , f )
558514
515+ config = MCPServersConfig .from_file (config_file )
516+
559517 # Even with empty inputs, should load fine since no input definitions exist
560- config = MCPServersConfig . from_file ( config_file , inputs = {})
518+ server = config . server ( "test_server" , input_values = {})
561519
562- server = config .servers ["test_server" ]
563520 assert isinstance (server , StdioServerConfig )
564521 # Placeholder should remain unchanged
565522 assert server .command == "python -m server --token ${input:token}"
@@ -586,20 +543,20 @@ def test_input_definition_with_yaml_file(tmp_path: Path):
586543"""
587544
588545 config_file = tmp_path / "test_config.yaml"
589- config_file .write_text (yaml_content )
590-
591- inputs = {"module-name" : "test_module" , "config-path" : "/etc/config.json" }
546+ assert config_file .write_text (yaml_content )
592547
593- config = MCPServersConfig .from_file (config_file , inputs = inputs )
548+ config = MCPServersConfig .from_file (config_file )
594549
595550 # Verify input definitions were parsed
551+ assert config .get_required_inputs () == ["module-name" , "config-path" ]
596552 assert config .inputs is not None
597553 assert len (config .inputs ) == 2
598554 assert config .inputs [0 ].id == "module-name"
599555 assert config .inputs [1 ].id == "config-path"
600556
601- # Verify substitution worked
602- server = config .servers ["yaml_server" ]
557+ input_values = {"module-name" : "test_module" , "config-path" : "/etc/config.json" }
558+ server = config .server ("yaml_server" , input_values = input_values )
559+
603560 assert isinstance (server , StdioServerConfig )
604561 assert server .command == "python -m test_module"
605562 assert server .args == ["--config" , "/etc/config.json" ]
@@ -711,20 +668,18 @@ def test_from_file_with_jsonc_comments(tmp_path: Path):
711668"""
712669
713670 config_file = tmp_path / "test_config.json"
714- config_file .write_text (jsonc_content )
715-
716- inputs = {"api-key" : "secret123" }
671+ assert config_file .write_text (jsonc_content )
717672
718673 # Should load successfully despite comments
719- config = MCPServersConfig .from_file (config_file , inputs = inputs )
674+ config = MCPServersConfig .from_file (config_file )
720675
721676 # Verify input definitions were parsed
722677 assert config .inputs is not None
723678 assert len (config .inputs ) == 1
724679 assert config .inputs [0 ].id == "api-key"
725680
726681 # Verify server configuration and input substitution
727- server = config .servers [ "main_server" ]
682+ server = config .server ( "main_server" , input_values = { "api-key" : "secret123" })
728683 assert isinstance (server , SSEServerConfig )
729684 assert server .url == "https://api.example.com/mcp/sse"
730685 assert server .headers == {"Authorization" : "Bearer secret123" }
0 commit comments