Skip to content

Commit 8c8e657

Browse files
committed
Support VS Code "inputs" key
1 parent 66e0e0e commit 8c8e657

File tree

2 files changed

+502
-3
lines changed

2 files changed

+502
-3
lines changed

src/mcp/client/config/mcp_servers_config.py

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# stdlib imports
44
import json
55
import os
6+
import re
67
import shlex
78
from pathlib import Path
89
from typing import Annotated, Any, Literal
@@ -15,6 +16,15 @@
1516
from 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+
1828
class 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

Comments
 (0)