We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/motherduckdb/mcp-server-motherduck'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Fixtures for E2E testing of the MCP server.
These tests treat the MCP server as a black box, spinning it up with various
configurations and making requests via the FastMCP client.
"""
import os
from pathlib import Path
from typing import AsyncGenerator
import pytest
# Load environment variables from .env file
from dotenv import load_dotenv
load_dotenv()
# Map E2E env vars to standard AWS env vars
if os.environ.get("E2E_USER_1_AWS_ACCESS_KEY_ID"):
os.environ["AWS_ACCESS_KEY_ID"] = os.environ["E2E_USER_1_AWS_ACCESS_KEY_ID"]
if os.environ.get("E2E_USER_1_AWS_SECRET_ACCESS_KEY"):
os.environ["AWS_SECRET_ACCESS_KEY"] = os.environ["E2E_USER_1_AWS_SECRET_ACCESS_KEY"]
from fastmcp import Client # noqa: E402
from fastmcp.client.transports import StdioTransport # noqa: E402
# Paths
FIXTURES_DIR = Path(__file__).parent / "fixtures"
TEST_DB_PATH = FIXTURES_DIR / "test.duckdb"
def get_mcp_client(*args: str, env: dict | None = None) -> Client:
"""
Create a FastMCP Client for the MCP server with given arguments.
Args:
*args: Command line arguments to pass to the server
env: Environment variables to set
Returns:
Client configured to launch the server via stdio
"""
# Use uv run to invoke the installed script
# This ensures we're using the local package from the workspace
server_args = ["run", "mcp-server-motherduck"]
server_args.extend(args)
# Merge environment
full_env = os.environ.copy()
# Disable MotherDuck extension logging to prevent stdout pollution in stdio transport
full_env["motherduck_logging"] = "0"
if env:
full_env.update(env)
# Create StdioTransport with proper configuration
# keep_alive=False ensures subprocess is terminated when connection closes
transport = StdioTransport(
command="uv",
args=server_args,
env=full_env,
keep_alive=False,
)
return Client(transport)
def get_result_text(result) -> str:
"""Extract text from a tool call result (CallToolResult)."""
if hasattr(result, "content") and result.content:
return result.content[0].text
return str(result)
@pytest.fixture
def fixtures_dir() -> Path:
"""Return the fixtures directory path."""
return FIXTURES_DIR
@pytest.fixture
def test_db_path() -> Path:
"""Return the test database path."""
if not TEST_DB_PATH.exists():
pytest.skip(
f"Test database not found at {TEST_DB_PATH}. Run 'python tests/e2e/fixtures/create_test_db.py' first."
)
return TEST_DB_PATH
@pytest.fixture
def motherduck_token() -> str:
"""Get the MotherDuck read-write token from environment."""
token = os.environ.get("MOTHERDUCK_TOKEN")
if not token:
pytest.skip("MOTHERDUCK_TOKEN not set")
return token
@pytest.fixture
def motherduck_token_read_scaling() -> str:
"""Get the MotherDuck read-scaling token from environment."""
token = os.environ.get("MOTHERDUCK_TOKEN_READ_SCALING")
if not token:
pytest.skip("MOTHERDUCK_TOKEN_READ_SCALING not set")
return token
@pytest.fixture
async def local_client(test_db_path: Path) -> AsyncGenerator[Client, None]:
"""Create a client connected to a local DuckDB file with write access."""
client = get_mcp_client("--db-path", str(test_db_path), "--read-write")
async with client:
yield client
@pytest.fixture
async def memory_client() -> AsyncGenerator[Client, None]:
"""Create a client connected to an in-memory DuckDB (always writable)."""
client = get_mcp_client("--db-path", ":memory:", "--read-write")
async with client:
yield client
@pytest.fixture
async def memory_client_with_switch() -> AsyncGenerator[Client, None]:
"""Create a client connected to an in-memory DuckDB with switch_database_connection enabled."""
client = get_mcp_client("--db-path", ":memory:", "--read-write", "--allow-switch-databases")
async with client:
yield client
@pytest.fixture
async def readonly_client(test_db_path: Path) -> AsyncGenerator[Client, None]:
"""Create a client connected to a local DuckDB in read-only mode (default)."""
client = get_mcp_client("--db-path", str(test_db_path))
async with client:
yield client
@pytest.fixture
async def motherduck_client(motherduck_token: str) -> AsyncGenerator[Client, None]:
"""Create a client connected to MotherDuck with read-write access."""
client = get_mcp_client(
"--db-path",
"md:",
"--motherduck-token",
motherduck_token,
"--read-write",
)
async with client:
yield client
@pytest.fixture
async def motherduck_saas_client(
motherduck_token_read_scaling: str,
) -> AsyncGenerator[Client, None]:
"""Create a client connected to MotherDuck in SaaS mode."""
client = get_mcp_client(
"--db-path",
"md:",
"--motherduck-token",
motherduck_token_read_scaling,
"--motherduck-saas-mode",
)
async with client:
yield client
def create_limited_client(
db_path: str,
max_rows: int | None = None,
max_chars: int | None = None,
query_timeout: int | None = None,
read_write: bool = False,
) -> Client:
"""
Create a client with custom limits.
This is a factory function, not a fixture, because we need different
limit values for different tests.
"""
args = ["--db-path", db_path]
if read_write:
args.append("--read-write")
if max_rows is not None:
args.extend(["--max-rows", str(max_rows)])
if max_chars is not None:
args.extend(["--max-chars", str(max_chars)])
if query_timeout is not None:
args.extend(["--query-timeout", str(query_timeout)])
return get_mcp_client(*args)