Skip to content

Commit 41a7dd6

Browse files
committed
feat(mcp): add a feature to start MCP server from config file
1 parent 962ac1c commit 41a7dd6

File tree

3 files changed

+202
-1
lines changed

3 files changed

+202
-1
lines changed

src/strands/tools/mcp/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .mcp_agent_tool import MCPAgentTool
1010
from .mcp_client import MCPClient
11+
from .mcp_from_config import MCPServerConfig
1112
from .mcp_types import MCPTransport
1213

13-
__all__ = ["MCPAgentTool", "MCPClient", "MCPTransport"]
14+
__all__ = ["MCPAgentTool", "MCPClient", "MCPServerConfig", "MCPTransport"]
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""MCP server configuration loading utilities."""
2+
3+
import json
4+
from pathlib import Path
5+
from typing import Dict, List, Optional
6+
7+
from mcp import StdioServerParameters
8+
from mcp.client.stdio import stdio_client
9+
10+
from .mcp_client import MCPClient
11+
from .mcp_types import MCPTransport
12+
13+
14+
class MCPServerConfig:
15+
"""Configuration for an MCP server following MCP standards."""
16+
17+
def __init__(
18+
self,
19+
name: str,
20+
command: str,
21+
args: Optional[List[str]] = None,
22+
env: Optional[Dict[str, str]] = None,
23+
timeout: Optional[int] = None,
24+
):
25+
"""Initialize MCP server configuration.
26+
27+
Args:
28+
name: Server name
29+
command: Command to run the server
30+
args: Command arguments
31+
env: Environment variables
32+
timeout: Timeout in milliseconds
33+
"""
34+
self.name = name
35+
self.command = command
36+
self.args = args or []
37+
self.env = env or {}
38+
self.timeout = timeout or 60000
39+
40+
def create_client(self) -> MCPClient:
41+
"""Create an MCPClient from this configuration."""
42+
43+
def transport_callable() -> MCPTransport:
44+
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
45+
return stdio_client(server_params)
46+
47+
return MCPClient(transport_callable)
48+
49+
@classmethod
50+
def from_config(cls, config_path: str) -> List["MCPServerConfig"]:
51+
"""Load MCP server configurations from standard mcp.json format.
52+
53+
Args:
54+
config_path: Path to the MCP configuration file
55+
56+
Returns:
57+
List of MCPServerConfig instances
58+
59+
Config file examples:
60+
Anthropic MCP Server Config Examples: (https://modelcontextprotocol.io/examples)
61+
AmazonQ MCP Server Config Examples: (https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-understanding-config.html)
62+
63+
Expected format:
64+
{
65+
"mcpServers": {
66+
"server-name": {
67+
"command": "command-to-run",
68+
"args": ["arg1", "arg2"],
69+
"env": {
70+
"ENV_VAR1": "value1",
71+
"ENV_VAR2": "value2"
72+
},
73+
"timeout": 60000
74+
}
75+
}
76+
}
77+
78+
"""
79+
config_file = Path(config_path)
80+
if not config_file.exists():
81+
raise FileNotFoundError(f"Config file not found: {config_path}")
82+
83+
with open(config_file) as f:
84+
config_data = json.load(f)
85+
86+
servers = []
87+
mcp_server_name = set()
88+
mcp_servers = config_data.get("mcpServers", {})
89+
expected_attrs = {"command", "args", "env", "timeout"}
90+
91+
for name, server_config in mcp_servers.items():
92+
if len(name) == 0 or len(name) > 250 or server_config.get("command") is None or name in mcp_server_name:
93+
raise ValueError(f"Invalid server configuration for {name}")
94+
if set(server_config.keys()) - expected_attrs:
95+
raise ValueError(f"Invalid server configuration for {name}")
96+
97+
servers.append(
98+
cls(
99+
name=name,
100+
command=server_config["command"],
101+
args=server_config.get("args"),
102+
env=server_config.get("env"),
103+
timeout=server_config.get("timeout"),
104+
)
105+
)
106+
mcp_server_name.add(name)
107+
108+
return servers
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import json
2+
from unittest.mock import patch
3+
4+
import pytest
5+
6+
from strands.tools.mcp.mcp_from_config import MCPServerConfig
7+
8+
9+
@pytest.fixture
10+
def mcp_config_data():
11+
"""Valid MCP configuration data."""
12+
return {
13+
"mcpServers": {
14+
"filesystem": {
15+
"command": "npx",
16+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
17+
"env": {"NODE_ENV": "production"},
18+
"timeout": 60000,
19+
},
20+
"calculator": {"command": "python", "args": ["calculator_server.py"]},
21+
}
22+
}
23+
24+
25+
@pytest.fixture
26+
def mcp_config_file(tmp_path, mcp_config_data):
27+
"""Create a temporary MCP config file."""
28+
config_path = tmp_path / "mcp_config.json"
29+
with open(config_path, "w") as f:
30+
json.dump(mcp_config_data, f)
31+
return str(config_path)
32+
33+
34+
@pytest.fixture
35+
def invalid_mcp_config_data():
36+
"""Invalid MCP configuration data for testing validation."""
37+
return {
38+
"mcpServers": {
39+
"": { # Empty name
40+
"command": "echo"
41+
},
42+
"no_command": { # Missing command
43+
"args": ["test"]
44+
},
45+
}
46+
}
47+
48+
49+
class TestMCPServerConfig:
50+
"""Test MCPServerConfig core functionality."""
51+
52+
def test_init_defaults(self):
53+
"""Test MCPServerConfig initialization with defaults."""
54+
config = MCPServerConfig(name="test", command="echo")
55+
56+
assert config.name == "test"
57+
assert config.command == "echo"
58+
assert config.args == []
59+
assert config.env == {}
60+
assert config.timeout == 60000
61+
62+
@patch("strands.tools.mcp.mcp_from_config.MCPClient")
63+
def test_create_client(self, mock_mcp_client):
64+
"""Test create_client method creates MCPClient."""
65+
config = MCPServerConfig(name="test", command="echo")
66+
67+
result = config.create_client()
68+
69+
mock_mcp_client.assert_called_once()
70+
assert result == mock_mcp_client.return_value
71+
72+
def test_from_config_valid_file(self, mcp_config_file):
73+
"""Test from_config with valid config file."""
74+
servers = MCPServerConfig.from_config(mcp_config_file)
75+
76+
assert len(servers) == 2
77+
server_names = {s.name for s in servers}
78+
assert server_names == {"filesystem", "calculator"}
79+
80+
def test_from_config_file_not_found(self):
81+
"""Test from_config with nonexistent file."""
82+
with pytest.raises(FileNotFoundError):
83+
MCPServerConfig.from_config("/nonexistent/path.json")
84+
85+
def test_from_config_validation_error(self, tmp_path, invalid_mcp_config_data):
86+
"""Test from_config with validation errors."""
87+
config_path = tmp_path / "invalid.json"
88+
with open(config_path, "w") as f:
89+
json.dump(invalid_mcp_config_data, f)
90+
91+
with pytest.raises(ValueError):
92+
MCPServerConfig.from_config(str(config_path))

0 commit comments

Comments
 (0)