Skip to content

Commit 68b1a2e

Browse files
committed
Add YAML support
1 parent 71adf90 commit 68b1a2e

File tree

5 files changed

+180
-4
lines changed

5 files changed

+180
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies = [
3737
rich = ["rich>=13.9.4"]
3838
cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"]
3939
ws = ["websockets>=15.0.1"]
40+
yaml = ["pyyaml>=6.0.2"]
4041

4142
[project.scripts]
4243
mcp = "mcp.cli:app [cli]"

src/mcp/client/config/mcp_servers_config.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from typing import Annotated, Any, Literal
77

88
# third party imports
9+
try:
10+
import yaml
11+
except ImportError:
12+
yaml = None # type: ignore
913
from pydantic import BaseModel, Field, field_validator
1014

1115

@@ -81,7 +85,21 @@ def infer_server_types(cls, servers_data: dict[str, Any]) -> dict[str, Any]:
8185
return servers_data
8286

8387
@classmethod
84-
def from_file(cls, config_path: Path) -> "MCPServersConfig":
85-
"""Load configuration from a JSON file."""
88+
def from_file(cls, config_path: Path, use_pyyaml: bool = False) -> "MCPServersConfig":
89+
"""Load configuration from a JSON or YAML file.
90+
91+
Args:
92+
config_path: Path to the configuration file
93+
use_pyyaml: If True, force use of PyYAML parser. Defaults to False.
94+
Also automatically used for .yaml/.yml files.
95+
"""
8696
with open(config_path) as config_file:
87-
return cls.model_validate(json.load(config_file))
97+
# Check if YAML parsing is requested
98+
should_use_yaml = use_pyyaml or config_path.suffix.lower() in (".yaml", ".yml")
99+
100+
if should_use_yaml:
101+
if not yaml:
102+
raise ImportError("PyYAML is required to parse YAML files. ")
103+
return cls.model_validate(yaml.safe_load(config_file))
104+
else:
105+
return cls.model_validate(json.load(config_file))

tests/client/config/mcp.yaml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# MCP Servers Configuration (YAML format)
2+
# This file demonstrates the same server configurations as mcp.json but in YAML format
3+
4+
mcpServers:
5+
time:
6+
command: uvx mcp-server-time
7+
8+
# Stdio server with full command string
9+
# The library will automatically parse this.
10+
# The effective_command will be "python" and effective_args will be ["-m", "my_server"]
11+
stdio_server_with_full_command:
12+
command: python -m my_server
13+
14+
# Streamable HTTP server with headers
15+
streamable_http_server:
16+
url: https://api.example.com/mcp
17+
18+
# Streamable HTTP server with headers
19+
streamable_http_server_with_headers:
20+
url: https://api.example.com/mcp
21+
headers:
22+
Authorization: Bearer token123
23+
24+
# stdio server with explicit command and args like typically done in mcp.json
25+
# files
26+
# I would expect this to not be used much with YAML files, but it's here for
27+
# completeness.
28+
stdio_server:
29+
command: python
30+
args:
31+
- -m
32+
- my_server
33+
env:
34+
DEBUG: "true"
35+
36+
# Stdio server with full command string AND explicit args
37+
# The effective_args will combine parsed command args with explicit args
38+
stdio_server_with_full_command_and_explicit_args:
39+
command: python -m my_server # Will be parsed to: command="python", args=["-m", "my_server"]
40+
args:
41+
- --debug # Will be appended to parsed args: ["-m", "my_server", "--debug"]
42+
43+
# Streamable HTTP server with headers
44+
streamable_http_server_with_headers:
45+
url: https://api.example.com/mcp
46+
headers:
47+
Authorization: Bearer token123
48+
49+
# Servers with explicit types - these demonstrate that type inference
50+
# can be overridden by explicitly specifying the "type" field
51+
52+
# Stdio server with explicit type specification
53+
stdio_server_with_explicit_type:
54+
type: stdio # Explicitly specified type
55+
command: python
56+
args:
57+
- -m
58+
- my_server
59+
env:
60+
DEBUG: "true"
61+
62+
# Streamable HTTP server with explicit type specification
63+
streamable_http_server_with_explicit_type:
64+
type: streamable_http # Explicitly specified type
65+
url: https://api.example.com/mcp
66+
67+
# SSE (Server-Sent Events) server with explicit type specification
68+
sse_server_with_explicit_type:
69+
type: sse # Explicitly specified type
70+
url: https://api.example.com/sse
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# stdlib imports
2+
from pathlib import Path
3+
4+
# third party imports
5+
import pytest
6+
7+
# local imports
8+
from mcp.client.config.mcp_servers_config import MCPServersConfig, StdioServerConfig, StreamableHTTPServerConfig
9+
10+
11+
@pytest.fixture
12+
def mcp_yaml_config_file() -> Path:
13+
"""Return path to the mcp.yaml config file."""
14+
return Path(__file__).parent / "mcp.yaml"
15+
16+
17+
def test_yaml_extension_auto_detection(mcp_yaml_config_file: Path):
18+
"""Test that .yaml files are automatically parsed with PyYAML."""
19+
config = MCPServersConfig.from_file(mcp_yaml_config_file)
20+
21+
# Should successfully load the YAML file with all 9 servers
22+
assert "stdio_server" in config.servers
23+
assert "streamable_http_server_with_headers" in config.servers
24+
assert "sse_server_with_explicit_type" in config.servers
25+
26+
# Verify a specific server
27+
stdio_server = config.servers["stdio_server"]
28+
assert isinstance(stdio_server, StdioServerConfig)
29+
assert stdio_server.command == "python"
30+
assert stdio_server.args == ["-m", "my_server"]
31+
assert stdio_server.env == {"DEBUG": "true"}
32+
33+
34+
def test_use_pyyaml_parameter_with_json_file():
35+
"""Test that use_pyyaml=True forces PyYAML parsing even for JSON files."""
36+
json_file = Path(__file__).parent / "mcp.json"
37+
38+
# Load with PyYAML explicitly
39+
config = MCPServersConfig.from_file(json_file, use_pyyaml=True)
40+
41+
# Should work fine - PyYAML can parse JSON
42+
assert len(config.servers) == 7
43+
assert "stdio_server" in config.servers
44+
45+
# Verify it produces the same result as normal JSON parsing
46+
config_json = MCPServersConfig.from_file(json_file, use_pyyaml=False)
47+
assert len(config.servers) == len(config_json.servers)
48+
assert list(config.servers.keys()) == list(config_json.servers.keys())
49+
50+
51+
def test_time_server(mcp_yaml_config_file: Path):
52+
"""Test the time server configuration with uvx command."""
53+
config = MCPServersConfig.from_file(mcp_yaml_config_file)
54+
55+
# Should have the time server
56+
assert "time" in config.servers
57+
58+
# Verify the server configuration
59+
time_server = config.servers["time"]
60+
assert isinstance(time_server, StdioServerConfig)
61+
assert time_server.type == "stdio" # Should be auto-inferred from command field
62+
assert time_server.command == "uvx mcp-server-time"
63+
assert time_server.args is None # No explicit args
64+
assert time_server.env is None # No environment variables
65+
66+
# Test the effective command parsing
67+
assert time_server.effective_command == "uvx"
68+
assert time_server.effective_args == ["mcp-server-time"]
69+
70+
71+
def test_streamable_http_server(mcp_yaml_config_file: Path):
72+
"""Test the new streamable HTTP server configuration without headers."""
73+
config = MCPServersConfig.from_file(mcp_yaml_config_file)
74+
75+
# Should have the new streamable_http_server
76+
assert "streamable_http_server" in config.servers
77+
78+
# Verify the server configuration
79+
http_server = config.servers["streamable_http_server"]
80+
assert isinstance(http_server, StreamableHTTPServerConfig)
81+
assert http_server.type == "streamable_http" # Should be auto-inferred
82+
assert http_server.url == "https://api.example.com/mcp"
83+
assert http_server.headers is None # No headers specified

uv.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)