diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cf4805e87..bb9c7fb36 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -48,3 +48,5 @@ jobs: - name: Run tests run: uv run pytest tests + env: + FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }} diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_tests/test_github_mcp_remote.py b/tests/integration_tests/test_github_mcp_remote.py new file mode 100644 index 000000000..99ee869fb --- /dev/null +++ b/tests/integration_tests/test_github_mcp_remote.py @@ -0,0 +1,116 @@ +import json +import os + +import pytest +from mcp import McpError +from mcp.types import Tool + +from fastmcp import Client +from fastmcp.client import StreamableHttpTransport +from fastmcp.client.auth.bearer import BearerAuth + +GITHUB_REMOTE_MCP_URL = "https://api.githubcopilot.com/mcp/" + +HEADER_AUTHORIZATION = "Authorization" +FASTMCP_GITHUB_TOKEN = os.getenv("FASTMCP_GITHUB_TOKEN") + +# Skip tests if no GitHub token is available +pytestmark = pytest.mark.xfail( + FASTMCP_GITHUB_TOKEN is None, + reason="The FASTMCP_GITHUB_TOKEN environment variable is not set", +) + + +@pytest.fixture(name="streamable_http_client") +def fixture_streamable_http_client() -> Client[StreamableHttpTransport]: + assert FASTMCP_GITHUB_TOKEN is not None + + return Client( + StreamableHttpTransport( + url=GITHUB_REMOTE_MCP_URL, + auth=BearerAuth(FASTMCP_GITHUB_TOKEN), + ) + ) + + +async def test_connect_disconnect( + streamable_http_client: Client[StreamableHttpTransport], +): + async with streamable_http_client: + assert streamable_http_client.is_connected() is True + await streamable_http_client._disconnect() # pylint: disable=W0212 (protected-access) + assert streamable_http_client.is_connected() is False + + +async def test_ping(streamable_http_client: Client[StreamableHttpTransport]): + """Test pinging the server.""" + async with streamable_http_client: + assert streamable_http_client.is_connected() is True + result = await streamable_http_client.ping() + assert result is True + + +async def test_list_tools(streamable_http_client: Client[StreamableHttpTransport]): + """Test listing the MCP tools""" + async with streamable_http_client: + assert streamable_http_client.is_connected() + tools = await streamable_http_client.list_tools() + assert isinstance(tools, list) + assert len(tools) > 0 # Ensure the tools list is non-empty + for tool in tools: + assert isinstance(tool, Tool) + assert len(tool.name) > 0 + assert tool.description is not None and len(tool.description) > 0 + assert isinstance(tool.inputSchema, dict) + assert len(tool.inputSchema) > 0 + + +async def test_list_resources(streamable_http_client: Client[StreamableHttpTransport]): + """Test listing the MCP resources""" + async with streamable_http_client: + assert streamable_http_client.is_connected() + resources = await streamable_http_client.list_resources() + assert isinstance(resources, list) + assert len(resources) == 0 + + +async def test_list_prompts(streamable_http_client: Client[StreamableHttpTransport]): + """Test listing the MCP prompts""" + async with streamable_http_client: + assert streamable_http_client.is_connected() + prompts = await streamable_http_client.list_prompts() + # there is at least one prompt (as of July 2025) + assert len(prompts) >= 1 + + +async def test_call_tool_ko(streamable_http_client: Client[StreamableHttpTransport]): + """Test calling a non-existing tool""" + async with streamable_http_client: + assert streamable_http_client.is_connected() + with pytest.raises(McpError, match="tool not found"): + await streamable_http_client.call_tool("foo") + + +async def test_call_tool_list_commits( + streamable_http_client: Client[StreamableHttpTransport], +): + """Test calling a list_commit tool""" + async with streamable_http_client: + assert streamable_http_client.is_connected() + result = await streamable_http_client.call_tool( + "list_commits", {"owner": "jlowin", "repo": "fastmcp"} + ) + + # at this time, the github server does not support structured content + assert result.structured_content is None + assert isinstance(result.content, list) + assert len(result.content) == 1 + commits = json.loads(result.content[0].text) # type: ignore[attr-defined] + for commit in commits: + assert isinstance(commit, dict) + assert "sha" in commit + assert "commit" in commit + assert "author" in commit["commit"] + assert len(commit["commit"]["author"]["date"]) > 0 + assert len(commit["commit"]["author"]["name"]) > 0 + assert len(commit["commit"]["author"]["email"]) > 0 diff --git a/tests/test_servers/fastmcp_server.py b/tests/test_servers/fastmcp_server.py deleted file mode 100644 index 1bc942295..000000000 --- a/tests/test_servers/fastmcp_server.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Any - -from fastmcp import FastMCP - -USERS = [ - {"id": "1", "name": "Alice", "active": True}, - {"id": "2", "name": "Bob", "active": True}, - {"id": "3", "name": "Charlie", "active": False}, -] - - -server = FastMCP("TestServer") - -# --- Tools --- - - -@server.tool -def greet(name: str) -> str: - """Greet someone by name.""" - return f"Hello, {name}!" - - -@server.tool -def add(a: int, b: int) -> int: - """Add two numbers together.""" - return a + b - - -@server.tool -def error_tool(): - """This tool always raises an error.""" - raise ValueError("This is a test error") - - -# --- Resources --- - - -@server.resource(uri="resource://wave") -def wave() -> str: - return "👋" - - -@server.resource(uri="data://users") -async def get_users() -> list[dict[str, Any]]: - return USERS - - -@server.resource(uri="data://user/{user_id}") -async def get_user(user_id: str) -> dict[str, Any] | None: - return next((user for user in USERS if user["id"] == user_id), None) - - -# --- Prompts --- - - -@server.prompt -def welcome(name: str) -> str: - return f"Welcome to FastMCP, {name}!" diff --git a/tests/test_servers/sse.py b/tests/test_servers/sse.py deleted file mode 100644 index 6ada18c33..000000000 --- a/tests/test_servers/sse.py +++ /dev/null @@ -1,6 +0,0 @@ -import asyncio - -import fastmcp_server - -if __name__ == "__main__": - asyncio.run(fastmcp_server.server.run_sse_async()) diff --git a/tests/test_servers/stdio.py b/tests/test_servers/stdio.py deleted file mode 100644 index 00f3796cf..000000000 --- a/tests/test_servers/stdio.py +++ /dev/null @@ -1,6 +0,0 @@ -import asyncio - -import fastmcp_server - -if __name__ == "__main__": - asyncio.run(fastmcp_server.server.run_stdio_async())