fastmcp/tests/integration_tests/test_github_mcp_remote.py
Jeremiah Lowin 1a788cf349
Merge remote-tracking branch 'origin/main' into feature/client-auto-default
# Conflicts:
#	tests/client/test_streamable_http.py
#	tests/server/middleware/test_initialization_middleware.py
#	tests/server/tasks/test_task_status_notifications.py
2026-07-20 12:02:38 -04:00

156 lines
6.1 KiB
Python

import json
import os
import pytest
from mcp import MCPError
from mcp_types import Resource, TextContent, 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(
not FASTMCP_GITHUB_TOKEN,
reason="The FASTMCP_GITHUB_TOKEN environment variable is not set or empty",
)
@pytest.fixture(name="streamable_http_client")
def fixture_streamable_http_client() -> Client[StreamableHttpTransport]:
"""A default client, so this suite exercises `mode="auto"` against a real peer.
GitHub answers `server/discover` but has not adopted result tagging, so its
result envelope is not conformant with the modern version it advertises. The
client's conformance check catches that at connect time and degrades to the
initialize handshake, which is why these tests behave as they always have.
"""
assert FASTMCP_GITHUB_TOKEN is not None
return Client(
StreamableHttpTransport(
url=GITHUB_REMOTE_MCP_URL,
auth=BearerAuth(FASTMCP_GITHUB_TOKEN),
)
)
@pytest.fixture(name="legacy_client")
def fixture_legacy_client() -> Client[StreamableHttpTransport]:
"""A handshake-pinned client, for capabilities that exist only in that era."""
assert FASTMCP_GITHUB_TOKEN is not None
return Client(
StreamableHttpTransport(
url=GITHUB_REMOTE_MCP_URL,
auth=BearerAuth(FASTMCP_GITHUB_TOKEN),
),
mode="legacy",
)
class TestGithubMCPRemote:
async def test_connect_disconnect(
self,
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(self, legacy_client: Client[StreamableHttpTransport]):
"""Test pinging the server.
`ping` is defined only in the handshake era — the modern protocol version
does not carry the method at all — so this pins `mode="legacy"` rather
than relying on the default negotiation landing there.
"""
async with legacy_client:
assert legacy_client.is_connected() is True
result = await legacy_client.ping()
assert result is True
async def test_list_tools(
self, 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.input_schema, dict)
assert len(tool.input_schema) > 0
async def test_list_resources(
self, 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)
for resource in resources:
assert isinstance(resource, Resource)
assert resource.name
assert str(resource.uri)
async def test_list_prompts(
self, 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(
self, 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=r"unknown tool|tool not found"):
await streamable_http_client.call_tool("foo")
async def test_call_tool_list_commits(
self,
streamable_http_client: Client[StreamableHttpTransport],
):
"""Test calling a list_commit tool"""
async with streamable_http_client:
assert streamable_http_client.is_connected()
# On a modern connection the client derives `Mcp-Param-*` headers from
# the tool's schema, which it only holds once the tool has been listed
# in this session. Listing first keeps the call correct in either era.
await streamable_http_client.list_tools()
result = await streamable_http_client.call_tool(
"list_commits", {"owner": "prefecthq", "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
assert isinstance(result.content[0], TextContent)
commits = json.loads(result.content[0].text)
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