Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions katana_mcp_server/src/katana_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,11 @@ async def lifespan(server: FastMCP) -> AsyncIterator[Services]:
# Load environment variables
load_dotenv()

# Get configuration from environment
api_key = os.getenv("KATANA_API_KEY")
# Get configuration from environment. ``.strip()`` so a copy-paste with a
# trailing newline/space doesn't reach the auth layer as a malformed key
# (an all-whitespace value collapses to "" and fails the check below),
# mirroring the handling of ``KATANA_SYNC_API_KEY``.
api_key = (os.getenv("KATANA_API_KEY") or "").strip()
base_url = os.getenv("KATANA_BASE_URL", "https://api.katanamrp.com/v1")

# Validate required configuration
Expand Down
33 changes: 33 additions & 0 deletions katana_mcp_server/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,39 @@ async def test_lifespan_with_valid_credentials(self):
max_pages=100,
)

@pytest.mark.asyncio
async def test_lifespan_strips_whitespace_from_api_key(self):
"""A KATANA_API_KEY with leading/trailing whitespace (a common
copy-paste artifact) is stripped before reaching the client, so it
can't fail auth as a malformed key.
"""
mock_server = MagicMock(spec=FastMCP)

with (
patch.dict(
os.environ,
{
"KATANA_API_KEY": " key-with-spaces \n",
"MCP_DISABLE_CACHE_WARMUP": "1",
},
clear=True,
),
patch("katana_mcp.server.load_dotenv"),
patch("katana_mcp.server.KatanaClient") as mock_client_class,
):
mock_client_instance = AsyncMock(spec=KatanaClient)
mock_client_instance.__aenter__ = AsyncMock(
return_value=mock_client_instance
)
mock_client_instance.__aexit__ = AsyncMock(return_value=None)
mock_client_class.return_value = mock_client_instance

async with lifespan(mock_server) as context:
assert isinstance(context, Services)

# The client was built with the stripped key, not the padded one.
assert mock_client_class.call_args.kwargs["api_key"] == "key-with-spaces"

@pytest.mark.asyncio
async def test_lifespan_with_default_base_url(self):
"""Test lifespan uses default base URL when not provided."""
Expand Down
Loading