* feat: improve mcp tools based on endpoint
* feat: improve mcp tools based on endpoint
* test: add integration test for MCP endpoint routing
- Add test_mcp_endpoint_routing.py to verify single-bank vs multi-bank tool exposure
- Verifies /mcp/ exposes all tools with bank_id parameters
- Verifies /mcp/{bank_id}/ only exposes scoped tools without bank_id parameters
- Regression test for issue #317
Related: #317, #318
* test: use StreamableHTTP client for MCP endpoint routing test
Replace httpx AsyncClient SSE parsing with proper MCP StreamableHTTP
client. This correctly tests the MCP server using the actual protocol
that clients will use.
Fixes #317
78 lines
3.8 KiB
Python
78 lines
3.8 KiB
Python
"""Integration test for MCP endpoint routing.
|
|
|
|
This test verifies that /mcp/ and /mcp/{bank_id}/ expose different tool sets.
|
|
"""
|
|
|
|
import httpx
|
|
import pytest
|
|
from mcp.client.session import ClientSession
|
|
from mcp.client.streamable_http import streamable_http_client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_endpoint_routing_integration(memory):
|
|
"""Test that multi-bank and single-bank endpoints expose different tools using StreamableHTTP.
|
|
|
|
This is a regression test for issue #317 where /mcp/{bank_id}/ was incorrectly
|
|
exposing all tools (including list_banks) and bank_id parameters.
|
|
"""
|
|
from hindsight_api.api import create_app
|
|
|
|
# Create app with MCP enabled
|
|
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
|
|
|
# Use the app's lifespan context to properly initialize MCP servers
|
|
async with app.router.lifespan_context(app):
|
|
# Create an HTTPX client that routes to our ASGI app
|
|
from httpx import ASGITransport
|
|
|
|
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
|
# Test 1: Multi-bank endpoint /mcp/
|
|
async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
|
|
read_stream,
|
|
write_stream,
|
|
_,
|
|
):
|
|
async with ClientSession(read_stream, write_stream) as session:
|
|
await session.initialize()
|
|
multi_result = await session.list_tools()
|
|
|
|
multi_tools = {t.name for t in multi_result.tools}
|
|
|
|
# Multi-bank should have all tools including bank management
|
|
assert "retain" in multi_tools
|
|
assert "recall" in multi_tools
|
|
assert "reflect" in multi_tools
|
|
assert "list_banks" in multi_tools, "Multi-bank should expose list_banks"
|
|
assert "create_bank" in multi_tools, "Multi-bank should expose create_bank"
|
|
|
|
# Multi-bank retain should have bank_id parameter
|
|
retain_tool = next((t for t in multi_result.tools if t.name == "retain"), None)
|
|
assert retain_tool is not None
|
|
multi_params = set(retain_tool.inputSchema.get("properties", {}).keys())
|
|
assert "bank_id" in multi_params, "Multi-bank retain should have bank_id parameter"
|
|
|
|
# Test 2: Single-bank endpoint /mcp/test-bank/
|
|
async with streamable_http_client("http://test/mcp/test-bank/", http_client=http_client) as (
|
|
read_stream,
|
|
write_stream,
|
|
_,
|
|
):
|
|
async with ClientSession(read_stream, write_stream) as session:
|
|
await session.initialize()
|
|
single_result = await session.list_tools()
|
|
|
|
single_tools = {t.name for t in single_result.tools}
|
|
|
|
# Single-bank should only have scoped tools (no bank management)
|
|
assert "retain" in single_tools
|
|
assert "recall" in single_tools
|
|
assert "reflect" in single_tools
|
|
assert "list_banks" not in single_tools, "Single-bank should NOT expose list_banks"
|
|
assert "create_bank" not in single_tools, "Single-bank should NOT expose create_bank"
|
|
|
|
# Single-bank retain should NOT have bank_id parameter
|
|
retain_tool = next((t for t in single_result.tools if t.name == "retain"), None)
|
|
assert retain_tool is not None
|
|
single_params = set(retain_tool.inputSchema.get("properties", {}).keys())
|
|
assert "bank_id" not in single_params, "Single-bank retain should NOT have bank_id parameter"
|