File size: 8,281 Bytes
ef1072a
b90e63c
8e64604
ef1072a
ca11c0a
b90e63c
ca11c0a
 
ef1072a
ca11c0a
 
 
 
ef1072a
 
8e64604
ef1072a
ca11c0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef1072a
 
 
8e64604
ef1072a
 
 
 
 
 
 
 
 
 
 
 
 
 
8e64604
ef1072a
 
 
8e64604
 
 
 
 
 
 
ef1072a
 
 
 
 
 
 
 
 
b90e63c
 
 
 
 
 
 
 
 
 
 
 
ef1072a
ca11c0a
b476cc6
ca11c0a
b476cc6
 
ca11c0a
 
 
b476cc6
ca11c0a
b476cc6
 
 
 
ca11c0a
 
a4ec518
b476cc6
 
 
 
 
 
ca11c0a
 
 
b476cc6
 
 
 
 
 
 
 
 
ca11c0a
 
a4ec518
b476cc6
 
 
 
a4ec518
b476cc6
 
 
 
 
 
 
 
a4ec518
b476cc6
 
 
 
 
 
 
 
 
a4ec518
b476cc6
 
 
 
 
 
 
 
 
a4ec518
b476cc6
 
 
 
ca11c0a
b476cc6
 
ca11c0a
b476cc6
ca11c0a
b476cc6
 
 
 
ca11c0a
a4ec518
b476cc6
 
 
ca11c0a
a4ec518
b476cc6
ca11c0a
b476cc6
 
 
 
 
 
 
a4ec518
b476cc6
 
a4ec518
b476cc6
 
 
 
 
 
 
ca11c0a
 
a4ec518
ca11c0a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import inspect
import json
from pathlib import Path

import pytest
from pydantic import ValidationError

from fastmcp.cli.run import (
    create_mcp_config_server,
    import_server,
    is_url,
    parse_file_path,
)
from fastmcp.client.client import Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.mcp_config import MCPConfig, StdioMCPServer
from fastmcp.server.server import FastMCP


class TestUrlDetection:
    """Test URL detection functionality."""

    def test_is_url_valid_http(self):
        """Test detection of valid HTTP URLs."""
        assert is_url("http://example.com")
        assert is_url("http://localhost:8080")
        assert is_url("http://127.0.0.1:3000/path")

    def test_is_url_valid_https(self):
        """Test detection of valid HTTPS URLs."""
        assert is_url("https://example.com")
        assert is_url("https://api.example.com/mcp")
        assert is_url("https://localhost:8443")

    def test_is_url_invalid(self):
        """Test detection of non-URLs."""
        assert not is_url("server.py")
        assert not is_url("/path/to/server.py")
        assert not is_url("server.py:app")
        assert not is_url("ftp://example.com")  # Not http/https
        assert not is_url("file:///path/to/file")


class TestFilePathParsing:
    """Test file path parsing functionality."""

    def test_parse_file_path_simple(self, tmp_path):
        """Test parsing simple file path without object."""
        test_file = tmp_path / "server.py"
        test_file.write_text("# test server")

        file_path, server_object = parse_file_path(str(test_file))
        assert file_path == test_file.resolve()
        assert server_object is None

    def test_parse_file_path_with_object(self, tmp_path):
        """Test parsing file path with object specification."""
        test_file = tmp_path / "server.py"
        test_file.write_text("# test server")

        file_path, server_object = parse_file_path(f"{test_file}:app")
        assert file_path == test_file.resolve()
        assert server_object == "app"

    def test_parse_file_path_complex_object(self, tmp_path):
        """Test parsing file path with complex object specification."""
        test_file = tmp_path / "server.py"
        test_file.write_text("# test server")

        # The current implementation splits on the last colon, so file:module:app
        # becomes file_path="file:module" and server_object="app"
        # We need to create a file with a colon in the name for this test
        complex_file = tmp_path / "server:module.py"
        complex_file.write_text("# test server")

        file_path, server_object = parse_file_path(f"{complex_file}:app")
        assert file_path == complex_file.resolve()
        assert server_object == "app"

    def test_parse_file_path_nonexistent(self):
        """Test parsing nonexistent file path exits."""
        with pytest.raises(SystemExit) as exc_info:
            parse_file_path("nonexistent.py")
        assert exc_info.value.code == 1

    def test_parse_file_path_directory(self, tmp_path):
        """Test parsing directory path exits."""
        with pytest.raises(SystemExit) as exc_info:
            parse_file_path(str(tmp_path))
        assert exc_info.value.code == 1


class TestMCPConfig:
    """Test MCPConfig functionality."""

    async def test_run_mcp_config(self, tmp_path: Path):
        """Test creating a server from an MCPConfig file."""
        server_script = inspect.cleandoc("""
            from fastmcp import FastMCP

            mcp = FastMCP()

            @mcp.tool
            def add(a: int, b: int) -> int:
                return a + b

            if __name__ == '__main__':
                mcp.run()
            """)

        script_path: Path = tmp_path / "test.py"
        script_path.write_text(server_script)

        mcp_config_path = tmp_path / "mcp_config.json"

        mcp_config = MCPConfig(
            mcpServers={
                "test_server": StdioMCPServer(command="python", args=[str(script_path)])
            }
        )
        mcp_config.write_to_file(mcp_config_path)

        server: FastMCP[None] = create_mcp_config_server(mcp_config_path)

        client = Client[FastMCPTransport](server)

        async with client:
            tools = await client.list_tools()
            assert len(tools) == 1

    async def test_validate_mcp_config(self, tmp_path: Path):
        """Test creating a server from an MCPConfig file."""

        mcp_config_path = tmp_path / "mcp_config.json"

        mcp_config = {"mcpServers": {"test_server": dict(x=1, y=2)}}
        with mcp_config_path.open("w") as f:
            json.dump(mcp_config, f)

        with pytest.raises(ValidationError, match="validation errors for MCPConfig"):
            create_mcp_config_server(mcp_config_path)


class TestServerImport:
    """Test server import functionality using real files."""

    async def test_import_server_basic_mcp(self, tmp_path):
        """Test importing server with basic FastMCP server."""
        test_file = tmp_path / "server.py"
        test_file.write_text("""
import fastmcp

mcp = fastmcp.FastMCP("TestServer")

@mcp.tool
def greet(name: str) -> str:
    return f"Hello, {name}!"
""")

        server = await import_server(test_file)
        assert server.name == "TestServer"
        tools = await server.get_tools()
        assert "greet" in tools

    async def test_import_server_with_main_block(self, tmp_path):
        """Test importing server with if __name__ == '__main__' block."""
        test_file = tmp_path / "server.py"
        test_file.write_text("""
import fastmcp

app = fastmcp.FastMCP("MainServer")

@app.tool
def calculate(x: int, y: int) -> int:
    return x + y

if __name__ == "__main__":
    app.run()
""")

        server = await import_server(test_file)
        assert server.name == "MainServer"
        tools = await server.get_tools()
        assert "calculate" in tools

    async def test_import_server_standard_names(self, tmp_path):
        """Test automatic detection of standard names (mcp, server, app)."""
        # Test with 'mcp' name
        mcp_file = tmp_path / "mcp_server.py"
        mcp_file.write_text("""
import fastmcp
mcp = fastmcp.FastMCP("MCPServer")
""")

        server = await import_server(mcp_file)
        assert server.name == "MCPServer"

        # Test with 'server' name
        server_file = tmp_path / "server_server.py"
        server_file.write_text("""
import fastmcp
server = fastmcp.FastMCP("ServerServer")
""")

        server = await import_server(server_file)
        assert server.name == "ServerServer"

        # Test with 'app' name
        app_file = tmp_path / "app_server.py"
        app_file.write_text("""
import fastmcp
app = fastmcp.FastMCP("AppServer")
""")

        server = await import_server(app_file)
        assert server.name == "AppServer"

    async def test_import_server_nonstandard_name(self, tmp_path):
        """Test importing server with non-standard object name."""
        test_file = tmp_path / "server.py"
        test_file.write_text("""
import fastmcp

my_custom_server = fastmcp.FastMCP("CustomServer")

@my_custom_server.tool
def custom_tool() -> str:
    return "custom"
""")

        server = await import_server(test_file, "my_custom_server")
        assert server.name == "CustomServer"
        tools = await server.get_tools()
        assert "custom_tool" in tools

    async def test_import_server_no_standard_names_fails(self, tmp_path):
        """Test importing server when no standard names exist fails."""
        test_file = tmp_path / "server.py"
        test_file.write_text("""
import fastmcp

other_name = fastmcp.FastMCP("OtherServer")
""")

        with pytest.raises(SystemExit) as exc_info:
            await import_server(test_file)
        assert exc_info.value.code == 1

    async def test_import_server_nonexistent_object_fails(self, tmp_path):
        """Test importing nonexistent server object fails."""
        test_file = tmp_path / "server.py"
        test_file.write_text("""
import fastmcp

mcp = fastmcp.FastMCP("TestServer")
""")

        with pytest.raises(SystemExit) as exc_info:
            await import_server(test_file, "nonexistent")
        assert exc_info.value.code == 1