Skip to content

Commit 65440bb

Browse files
Add comprehensive MCP tests with mocked API portions
- Updated existing tests to use correct FastMCP API structure - Fixed tool and resource access patterns to use new manager attributes - Added extensive test coverage for MCP commands after server startup - Tests cover tools, resources, configuration, error handling, and protocol communication - All tests validated and working with proper API mocking
1 parent 53af51f commit 65440bb

File tree

5 files changed

+1766
-0
lines changed

5 files changed

+1766
-0
lines changed
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
"""Tests for MCP configuration scenarios."""
2+
3+
import os
4+
import subprocess
5+
import time
6+
from pathlib import Path
7+
from unittest.mock import patch
8+
9+
import pytest
10+
11+
12+
class TestMCPConfiguration:
13+
"""Test MCP configuration scenarios."""
14+
15+
def test_mcp_server_default_configuration(self):
16+
"""Test MCP server with default configuration."""
17+
codegen_path = Path(__file__).parent.parent.parent.parent / "src"
18+
venv_python = Path(__file__).parent.parent.parent.parent / ".venv" / "bin" / "python"
19+
20+
env = os.environ.copy()
21+
env["PYTHONPATH"] = str(codegen_path)
22+
23+
process = subprocess.Popen(
24+
[str(venv_python), "-c", "from codegen.cli.cli import main; main(['mcp'])"],
25+
env=env,
26+
stdin=subprocess.PIPE,
27+
stdout=subprocess.PIPE,
28+
stderr=subprocess.PIPE,
29+
text=True,
30+
)
31+
32+
try:
33+
# Give it time to start
34+
time.sleep(2)
35+
36+
# Server should start with default configuration
37+
assert process.poll() is None, "Server should start with default configuration"
38+
39+
finally:
40+
# Cleanup
41+
if process.poll() is None:
42+
process.terminate()
43+
try:
44+
process.wait(timeout=5)
45+
except subprocess.TimeoutExpired:
46+
process.kill()
47+
process.wait()
48+
49+
def test_mcp_server_stdio_transport_explicit(self):
50+
"""Test MCP server with explicit stdio transport."""
51+
codegen_path = Path(__file__).parent.parent.parent.parent / "src"
52+
venv_python = Path(__file__).parent.parent.parent.parent / ".venv" / "bin" / "python"
53+
54+
env = os.environ.copy()
55+
env["PYTHONPATH"] = str(codegen_path)
56+
57+
process = subprocess.Popen(
58+
[str(venv_python), "-c", "from codegen.cli.cli import main; main(['mcp', '--transport', 'stdio'])"],
59+
env=env,
60+
stdin=subprocess.PIPE,
61+
stdout=subprocess.PIPE,
62+
stderr=subprocess.PIPE,
63+
text=True,
64+
)
65+
66+
try:
67+
# Give it time to start
68+
time.sleep(2)
69+
70+
# Server should start with stdio transport
71+
assert process.poll() is None, "Server should start with stdio transport"
72+
73+
finally:
74+
# Cleanup
75+
if process.poll() is None:
76+
process.terminate()
77+
try:
78+
process.wait(timeout=5)
79+
except subprocess.TimeoutExpired:
80+
process.kill()
81+
process.wait()
82+
83+
def test_mcp_server_http_transport_configuration(self):
84+
"""Test MCP server with HTTP transport configuration."""
85+
codegen_path = Path(__file__).parent.parent.parent.parent / "src"
86+
venv_python = Path(__file__).parent.parent.parent.parent / ".venv" / "bin" / "python"
87+
88+
env = os.environ.copy()
89+
env["PYTHONPATH"] = str(codegen_path)
90+
91+
# Test HTTP transport (should fall back to stdio for now)
92+
process = subprocess.Popen(
93+
[str(venv_python), "-c", "from codegen.cli.cli import main; main(['mcp', '--transport', 'http', '--host', '127.0.0.1', '--port', '8080'])"],
94+
env=env,
95+
stdin=subprocess.PIPE,
96+
stdout=subprocess.PIPE,
97+
stderr=subprocess.PIPE,
98+
text=True,
99+
)
100+
101+
try:
102+
# Give it time to start
103+
time.sleep(2)
104+
105+
# Server should start (even if it falls back to stdio)
106+
assert process.poll() is None, "Server should start with HTTP transport configuration"
107+
108+
finally:
109+
# Cleanup
110+
if process.poll() is None:
111+
process.terminate()
112+
try:
113+
process.wait(timeout=5)
114+
except subprocess.TimeoutExpired:
115+
process.kill()
116+
process.wait()
117+
118+
def test_mcp_server_custom_host_port(self):
119+
"""Test MCP server with custom host and port."""
120+
codegen_path = Path(__file__).parent.parent.parent.parent / "src"
121+
venv_python = Path(__file__).parent.parent.parent.parent / ".venv" / "bin" / "python"
122+
123+
env = os.environ.copy()
124+
env["PYTHONPATH"] = str(codegen_path)
125+
126+
process = subprocess.Popen(
127+
[str(venv_python), "-c", "from codegen.cli.cli import main; main(['mcp', '--host', '0.0.0.0', '--port', '9000'])"],
128+
env=env,
129+
stdin=subprocess.PIPE,
130+
stdout=subprocess.PIPE,
131+
stderr=subprocess.PIPE,
132+
text=True,
133+
)
134+
135+
try:
136+
# Give it time to start
137+
time.sleep(2)
138+
139+
# Server should start with custom host and port
140+
assert process.poll() is None, "Server should start with custom host and port"
141+
142+
finally:
143+
# Cleanup
144+
if process.poll() is None:
145+
process.terminate()
146+
try:
147+
process.wait(timeout=5)
148+
except subprocess.TimeoutExpired:
149+
process.kill()
150+
process.wait()
151+
152+
def test_mcp_server_environment_variables(self):
153+
"""Test MCP server with various environment variables."""
154+
codegen_path = Path(__file__).parent.parent.parent.parent / "src"
155+
venv_python = Path(__file__).parent.parent.parent.parent / ".venv" / "bin" / "python"
156+
157+
env = os.environ.copy()
158+
env["PYTHONPATH"] = str(codegen_path)
159+
env["CODEGEN_API_KEY"] = "test-api-key-123"
160+
env["CODEGEN_API_BASE_URL"] = "https://custom.api.codegen.com"
161+
162+
process = subprocess.Popen(
163+
[str(venv_python), "-c", "from codegen.cli.cli import main; main(['mcp', '--transport', 'stdio'])"],
164+
env=env,
165+
stdin=subprocess.PIPE,
166+
stdout=subprocess.PIPE,
167+
stderr=subprocess.PIPE,
168+
text=True,
169+
)
170+
171+
try:
172+
# Give it time to start
173+
time.sleep(2)
174+
175+
# Server should start with custom environment variables
176+
assert process.poll() is None, "Server should start with custom environment variables"
177+
178+
finally:
179+
# Cleanup
180+
if process.poll() is None:
181+
process.terminate()
182+
try:
183+
process.wait(timeout=5)
184+
except subprocess.TimeoutExpired:
185+
process.kill()
186+
process.wait()
187+
188+
def test_api_client_configuration_with_env_vars(self):
189+
"""Test API client configuration with environment variables."""
190+
from codegen.cli.mcp.server import get_api_client
191+
192+
with patch.dict(os.environ, {
193+
"CODEGEN_API_KEY": "test-key-456",
194+
"CODEGEN_API_BASE_URL": "https://test.api.codegen.com"
195+
}):
196+
try:
197+
api_client, agents_api, orgs_api, users_api = get_api_client()
198+
199+
# Should return configured API clients
200+
assert api_client is not None
201+
assert agents_api is not None
202+
assert orgs_api is not None
203+
assert users_api is not None
204+
205+
# Check that configuration was applied
206+
assert api_client.configuration.host == "https://test.api.codegen.com"
207+
208+
except Exception as e:
209+
# If API client is not available, that's expected in test environment
210+
if "codegen-api-client is not available" not in str(e):
211+
raise
212+
213+
def test_api_client_configuration_defaults(self):
214+
"""Test API client configuration with default values."""
215+
from codegen.cli.mcp.server import get_api_client
216+
217+
# Clear environment variables to test defaults
218+
with patch.dict(os.environ, {}, clear=True):
219+
try:
220+
api_client, agents_api, orgs_api, users_api = get_api_client()
221+
222+
# Should use default base URL
223+
assert api_client.configuration.host == "https://api.codegen.com"
224+
225+
except Exception as e:
226+
# If API client is not available, that's expected in test environment
227+
if "codegen-api-client is not available" not in str(e):
228+
raise
229+
230+
def test_mcp_server_configuration_validation(self):
231+
"""Test MCP server configuration validation."""
232+
from codegen.cli.commands.mcp.main import mcp
233+
import typer
234+
235+
# Test that the function has the expected parameters
236+
import inspect
237+
sig = inspect.signature(mcp)
238+
239+
# Check that all expected parameters are present
240+
expected_params = ["host", "port", "transport"]
241+
for param in expected_params:
242+
assert param in sig.parameters, f"Parameter {param} not found in mcp function signature"
243+
244+
# Check parameter defaults
245+
assert sig.parameters["host"].default == "localhost"
246+
assert sig.parameters["port"].default is None
247+
assert sig.parameters["transport"].default == "stdio"
248+
249+
def test_transport_validation_in_command(self):
250+
"""Test transport validation in the MCP command."""
251+
from codegen.cli.commands.mcp.main import mcp
252+
253+
# This test would ideally call the function with invalid transport
254+
# but since it would try to actually run the server, we'll test the validation logic
255+
# by checking that the function exists and has the right structure
256+
257+
# The function should exist and be callable
258+
assert callable(mcp)
259+
260+
# The validation logic is in the function body, so we can't easily test it
261+
# without actually running the server, which we do in integration tests
262+
263+
def test_server_configuration_object_creation(self):
264+
"""Test server configuration object creation."""
265+
from codegen.cli.mcp.server import mcp
266+
267+
# Check that the FastMCP server was created with correct configuration
268+
assert mcp.name == "codegen-mcp"
269+
assert "MCP server for the Codegen platform" in mcp.instructions
270+
271+
# Check that tools and resources are registered
272+
assert len(mcp._tool_manager._tools) > 0, "Server should have tools registered"
273+
assert len(mcp._resource_manager._resources) > 0, "Server should have resources registered"
274+
275+
def test_server_instructions_configuration(self):
276+
"""Test server instructions configuration."""
277+
from codegen.cli.mcp.server import mcp
278+
279+
instructions = mcp.instructions
280+
281+
# Should contain key information about the server's purpose
282+
assert "MCP server" in instructions
283+
assert "Codegen" in instructions
284+
assert "tools" in instructions
285+
assert "resources" in instructions
286+
287+
def test_global_api_client_singleton_behavior(self):
288+
"""Test global API client singleton behavior."""
289+
from codegen.cli.mcp.server import get_api_client, _api_client
290+
291+
# Reset global state
292+
import codegen.cli.mcp.server
293+
codegen.cli.mcp.server._api_client = None
294+
codegen.cli.mcp.server._agents_api = None
295+
codegen.cli.mcp.server._organizations_api = None
296+
codegen.cli.mcp.server._users_api = None
297+
298+
try:
299+
# First call should create the client
300+
client1 = get_api_client()
301+
302+
# Second call should return the same client
303+
client2 = get_api_client()
304+
305+
# Should be the same objects (singleton behavior)
306+
assert client1[0] is client2[0], "API client should be singleton"
307+
assert client1[1] is client2[1], "Agents API should be singleton"
308+
assert client1[2] is client2[2], "Organizations API should be singleton"
309+
assert client1[3] is client2[3], "Users API should be singleton"
310+
311+
except Exception as e:
312+
# If API client is not available, that's expected in test environment
313+
if "codegen-api-client is not available" not in str(e):
314+
raise
315+
316+
def test_conditional_tool_registration(self):
317+
"""Test conditional tool registration based on available imports."""
318+
from codegen.cli.mcp.server import mcp, LEGACY_IMPORTS_AVAILABLE
319+
320+
tool_names = list(mcp._tool_manager._tools.keys())
321+
322+
if LEGACY_IMPORTS_AVAILABLE:
323+
# Legacy tools should be available
324+
assert "ask_codegen_sdk" in tool_names, "ask_codegen_sdk should be available when legacy imports are available"
325+
assert "improve_codemod" in tool_names, "improve_codemod should be available when legacy imports are available"
326+
else:
327+
# Legacy tools should not be available
328+
assert "ask_codegen_sdk" not in tool_names, "ask_codegen_sdk should not be available when legacy imports are unavailable"
329+
assert "improve_codemod" not in tool_names, "improve_codemod should not be available when legacy imports are unavailable"
330+
331+
# Core tools should always be available
332+
core_tools = ["generate_codemod", "create_agent_run", "get_agent_run", "get_organizations", "get_users", "get_user"]
333+
for tool in core_tools:
334+
assert tool in tool_names, f"Core tool {tool} should always be available"
335+
336+
def test_server_name_and_metadata(self):
337+
"""Test server name and metadata configuration."""
338+
from codegen.cli.mcp.server import mcp
339+
340+
# Check server metadata
341+
assert mcp.name == "codegen-mcp"
342+
343+
# Check that the server has the expected structure
344+
assert hasattr(mcp, '_tool_manager')
345+
assert hasattr(mcp, '_resource_manager')
346+
assert hasattr(mcp, 'instructions')
347+
348+
def test_resource_configuration_consistency(self):
349+
"""Test resource configuration consistency."""
350+
from codegen.cli.mcp.server import mcp
351+
352+
# All resources should have URIs, descriptions, and MIME types
353+
resources = mcp._resource_manager._resources
354+
for uri, resource in resources.items():
355+
assert hasattr(resource, 'description'), f"Resource should have description"
356+
assert hasattr(resource, 'mime_type'), f"Resource should have MIME type"
357+
assert hasattr(resource, 'fn'), f"Resource should have function"
358+
359+
# URI should be a string
360+
assert isinstance(uri, str), f"Resource URI should be string"
361+
assert len(uri) > 0, f"Resource URI should not be empty"
362+
363+
# MIME type should be valid
364+
valid_mime_types = ["text/plain", "application/json", "text/html", "application/xml"]
365+
assert resource.mime_type in valid_mime_types, f"Resource MIME type should be valid: {resource.mime_type}"
366+
367+
def test_tool_configuration_consistency(self):
368+
"""Test tool configuration consistency."""
369+
from codegen.cli.mcp.server import mcp
370+
371+
# All tools should have names and functions
372+
tools = mcp._tool_manager._tools
373+
for name, tool in tools.items():
374+
assert hasattr(tool, 'fn'), f"Tool should have function"
375+
376+
# Name should be a string
377+
assert isinstance(name, str), f"Tool name should be string"
378+
assert len(name) > 0, f"Tool name should not be empty"
379+
380+
# Function should be callable
381+
assert callable(tool.fn), f"Tool function should be callable"

0 commit comments

Comments
 (0)