Skip to content

Commit 387b1c5

Browse files
committed
Adjusting MCP header processing to llama stack 0.4.x and adding MCP e2e tests
1 parent 6747f34 commit 387b1c5

File tree

18 files changed

+1624
-89
lines changed

18 files changed

+1624
-89
lines changed

dev-tools/mcp-mock-server/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ This mock server helps developers:
1919
-**HTTP & HTTPS** - Runs both protocols simultaneously for comprehensive testing
2020
-**Header Capture** - Captures and displays all request headers
2121
-**Debug Endpoints** - Inspect captured headers and request history
22-
-**MCP Protocol** - Implements basic MCP endpoints for testing
22+
-**MCP Protocol** - Implements MCP endpoints (initialize, tools/list, tools/call)
2323
-**Request Logging** - Tracks recent requests with timestamps
2424
-**Self-Signed Certs** - Auto-generates certificates for HTTPS testing
25+
-**Tool Execution** - Returns mock results for tool/call testing
2526

2627
## Quick Start
2728

@@ -46,8 +47,11 @@ HTTPS: https://localhost:3001
4647
Debug endpoints:
4748
• /debug/headers - View captured headers
4849
• /debug/requests - View request log
49-
MCP endpoint:
50-
• POST /mcp/v1/list_tools
50+
MCP endpoints:
51+
• POST with JSON-RPC (any path)
52+
- method: "initialize"
53+
- method: "tools/list"
54+
- method: "tools/call"
5155
======================================================================
5256
Note: HTTPS uses a self-signed certificate (for testing only)
5357
```
@@ -270,8 +274,9 @@ python dev-tools/mcp-mock-server/server.py 8080
270274
This is a **development/testing tool only**:
271275
- ❌ Not for production use
272276
- ❌ No authentication/security
273-
- ❌ Limited MCP protocol implementation
277+
- ❌ Limited MCP protocol implementation (initialize, tools/list, tools/call only)
274278
- ❌ Single-threaded (one request at a time)
279+
- ❌ Mock responses only (not real tool execution)
275280

276281
For production, use real MCP servers.
277282

dev-tools/mcp-mock-server/server.py

Lines changed: 130 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def _capture_headers(self) -> None:
6060
if len(request_log) > 10:
6161
request_log.pop(0)
6262

63-
def do_POST(self) -> None: # pylint: disable=invalid-name
63+
def do_POST(
64+
self,
65+
) -> (
66+
None
67+
): # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
6468
"""Handle POST requests (MCP protocol endpoints)."""
6569
self._capture_headers()
6670

@@ -73,14 +77,24 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
7377
request_id = request_data.get("id", 1)
7478
method = request_data.get("method", "unknown")
7579
except (json.JSONDecodeError, UnicodeDecodeError):
80+
request_data = {}
7681
request_id = 1
7782
method = "unknown"
7883

84+
# Log the RPC method in the request log
85+
if request_log:
86+
request_log[-1]["rpc_method"] = method
87+
7988
# Determine tool name based on authorization header to avoid collisions
8089
auth_header = self.headers.get("Authorization", "")
8190

91+
# Initialize tool info defaults
92+
tool_name = "mock_tool_no_auth"
93+
tool_desc = "Mock tool with no authorization"
94+
error_mode = False
95+
8296
# Match based on token content
83-
match auth_header:
97+
match True:
8498
case _ if "test-secret-token" in auth_header:
8599
tool_name = "mock_tool_file"
86100
tool_desc = "Mock tool with file-based auth"
@@ -90,58 +104,116 @@ def do_POST(self) -> None: # pylint: disable=invalid-name
90104
case _ if "my-client-token" in auth_header:
91105
tool_name = "mock_tool_client"
92106
tool_desc = "Mock tool with client-provided token"
107+
case _ if "error-mode" in auth_header:
108+
tool_name = "mock_tool_error"
109+
tool_desc = "Mock tool configured to return errors"
110+
error_mode = True
93111
case _:
94-
# No auth header or unrecognized token
95-
tool_name = "mock_tool_no_auth"
96-
tool_desc = "Mock tool with no authorization"
97-
98-
# Handle MCP protocol methods
99-
if method == "initialize":
100-
# Return MCP initialize response
101-
response = {
102-
"jsonrpc": "2.0",
103-
"id": request_id,
104-
"result": {
105-
"protocolVersion": "2024-11-05",
106-
"capabilities": {
107-
"tools": {},
108-
},
109-
"serverInfo": {
110-
"name": "mock-mcp-server",
111-
"version": "1.0.0",
112+
# Default case already set above
113+
pass
114+
115+
# Log the tool name in the request log
116+
if request_log:
117+
request_log[-1]["tool_name"] = tool_name
118+
119+
# Handle MCP protocol methods using match statement
120+
response: dict = {}
121+
match method:
122+
case "initialize":
123+
# Return MCP initialize response
124+
response = {
125+
"jsonrpc": "2.0",
126+
"id": request_id,
127+
"result": {
128+
"protocolVersion": "2024-11-05",
129+
"capabilities": {
130+
"tools": {},
131+
},
132+
"serverInfo": {
133+
"name": "mock-mcp-server",
134+
"version": "1.0.0",
135+
},
112136
},
113-
},
114-
}
115-
elif method == "tools/list":
116-
# Return list of tools with unique name
117-
response = {
118-
"jsonrpc": "2.0",
119-
"id": request_id,
120-
"result": {
121-
"tools": [
122-
{
123-
"name": tool_name,
124-
"description": tool_desc,
125-
"inputSchema": {
126-
"type": "object",
127-
"properties": {
128-
"message": {
129-
"type": "string",
130-
"description": "Test message",
131-
}
137+
}
138+
139+
case "tools/list":
140+
# Return list of tools with unique name
141+
response = {
142+
"jsonrpc": "2.0",
143+
"id": request_id,
144+
"result": {
145+
"tools": [
146+
{
147+
"name": tool_name,
148+
"description": tool_desc,
149+
"inputSchema": {
150+
"type": "object",
151+
"properties": {
152+
"message": {
153+
"type": "string",
154+
"description": "Test message",
155+
}
156+
},
132157
},
133-
},
134-
}
135-
]
136-
},
137-
}
138-
else:
139-
# Generic success response for other methods
140-
response = {
141-
"jsonrpc": "2.0",
142-
"id": request_id,
143-
"result": {"status": "ok"},
144-
}
158+
}
159+
]
160+
},
161+
}
162+
163+
case "tools/call":
164+
# Handle tool execution
165+
params = request_data.get("params", {})
166+
tool_called = params.get("name", "unknown")
167+
arguments = params.get("arguments", {})
168+
169+
# Check if error mode is enabled
170+
if error_mode:
171+
# Return error response
172+
response = {
173+
"jsonrpc": "2.0",
174+
"id": request_id,
175+
"result": {
176+
"content": [
177+
{
178+
"type": "text",
179+
"text": (
180+
f"Error: Tool '{tool_called}' "
181+
"execution failed - simulated error."
182+
),
183+
}
184+
],
185+
"isError": True,
186+
},
187+
}
188+
else:
189+
# Build result text
190+
result_text = (
191+
f"Mock tool '{tool_called}' executed successfully "
192+
f"with arguments: {arguments}."
193+
)
194+
195+
# Return successful tool execution result
196+
response = {
197+
"jsonrpc": "2.0",
198+
"id": request_id,
199+
"result": {
200+
"content": [
201+
{
202+
"type": "text",
203+
"text": result_text,
204+
}
205+
],
206+
"isError": False,
207+
},
208+
}
209+
210+
case _:
211+
# Generic success response for other methods
212+
response = {
213+
"jsonrpc": "2.0",
214+
"id": request_id,
215+
"result": {"status": "ok"},
216+
}
145217

146218
self.send_response(200)
147219
self.send_header("Content-Type", "application/json")
@@ -160,6 +232,11 @@ def do_GET(self) -> None: # pylint: disable=invalid-name
160232
)
161233
case "/debug/requests":
162234
self._send_json_response(request_log)
235+
case "/debug/clear":
236+
# Clear the request log and last captured headers
237+
request_log.clear()
238+
last_headers.clear()
239+
self._send_json_response({"status": "cleared", "request_count": 0})
163240
case "/":
164241
self._send_help_page()
165242
case _:
@@ -273,10 +350,10 @@ def main() -> None:
273350
https_port = http_port + 1
274351

275352
# Create HTTP server
276-
http_server = HTTPServer(("", http_port), MCPMockHandler)
353+
http_server = HTTPServer(("", http_port), MCPMockHandler) # type: ignore[arg-type]
277354

278355
# Create HTTPS server with self-signed certificate
279-
https_server = HTTPServer(("", https_port), MCPMockHandler)
356+
https_server = HTTPServer(("", https_port), MCPMockHandler) # type: ignore[arg-type]
280357

281358
# Generate or load self-signed certificate
282359
script_dir = Path(__file__).parent

docker-compose-library.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
dockerfile: dev-tools/mcp-mock-server/Dockerfile
77
container_name: mcp-mock-server
88
ports:
9-
- "3000:3000"
9+
- "9000:3000"
1010
networks:
1111
- lightspeednet
1212
healthcheck:
@@ -66,6 +66,11 @@ services:
6666
- WATSONX_API_KEY=${WATSONX_API_KEY:-}
6767
# Enable debug logging if needed
6868
- LLAMA_STACK_LOGGING=${LLAMA_STACK_LOGGING:-}
69+
entrypoint: >
70+
/bin/bash -c "
71+
printf %s 'test-secret-token-123' > /tmp/lightspeed-mcp-test-token &&
72+
/app-root/.venv/bin/python3.12 /app-root/src/lightspeed_stack.py
73+
"
6974
healthcheck:
7075
test: ["CMD", "curl", "-f", "http://localhost:8080/liveness"]
7176
interval: 10s # how often to run the check

docker-compose.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
dockerfile: dev-tools/mcp-mock-server/Dockerfile
77
container_name: mcp-mock-server
88
ports:
9-
- "3000:3000"
9+
- "9000:3000"
1010
networks:
1111
- lightspeednet
1212
healthcheck:
@@ -84,6 +84,11 @@ services:
8484
- TENANT_ID=${TENANT_ID:-}
8585
- CLIENT_ID=${CLIENT_ID:-}
8686
- CLIENT_SECRET=${CLIENT_SECRET:-}
87+
entrypoint: >
88+
/bin/bash -c "
89+
printf %s 'test-secret-token-123' > /tmp/lightspeed-mcp-test-token &&
90+
/app-root/.venv/bin/python3.12 /app-root/src/lightspeed_stack.py
91+
"
8792
depends_on:
8893
llama-stack:
8994
condition: service_healthy

src/app/endpoints/streaming_query.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,22 @@ async def retrieve_response_generator(
263263
turn_summary,
264264
)
265265
# Retrieve response stream (may raise exceptions)
266+
# Log request details before calling Llama Stack (MCP debugging)
267+
if responses_params.tools is not None and len(responses_params.tools) > 0:
268+
# Filter MCP tools once for efficiency
269+
mcp_tools = [t for t in responses_params.tools if t.get("type") == "mcp"]
270+
if len(mcp_tools) > 0:
271+
logger.debug(
272+
"Calling Llama Stack Responses API (streaming) with %d MCP tool(s)",
273+
len(mcp_tools),
274+
)
275+
# Log MCP server endpoints that may be called
276+
logger.debug("MCP server endpoints that may be called:")
277+
for tool in mcp_tools:
278+
logger.debug(
279+
" - %s: %s", tool.get("server_label"), tool.get("server_url")
280+
)
281+
266282
response = await context.client.responses.create(
267283
**responses_params.model_dump()
268284
)

src/app/main.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from log import get_logger
2121
from a2a_storage import A2AStorageFactory
2222
from models.responses import InternalServerErrorResponse
23-
from utils.common import register_mcp_servers_async
23+
24+
# from utils.common import register_mcp_servers_async # Not needed for Responses API
2425
from utils.llama_stack_version import check_llama_stack_version
2526

2627
logger = get_logger(__name__)
@@ -69,9 +70,32 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
6970
)
7071
raise
7172

72-
logger.info("Registering MCP servers")
73-
await register_mcp_servers_async(logger, configuration.configuration)
74-
get_logger("app.endpoints.handlers")
73+
# Log MCP server configuration
74+
mcp_servers = configuration.configuration.mcp_servers
75+
if mcp_servers:
76+
logger.info("Loaded %d MCP server(s) from configuration:", len(mcp_servers))
77+
for server in mcp_servers:
78+
has_auth = bool(server.authorization_headers)
79+
logger.info(
80+
" - %s at %s (auth: %s)",
81+
server.name,
82+
server.url,
83+
"yes" if has_auth else "no",
84+
)
85+
# Debug: Show auth header names if configured
86+
if has_auth:
87+
logger.debug(
88+
" Auth headers: %s",
89+
", ".join(server.authorization_headers.keys()),
90+
)
91+
else:
92+
logger.info("No MCP servers configured")
93+
94+
# NOTE: MCP server registration not needed for Responses API
95+
# The Responses API takes inline tool definitions instead of pre-registered toolgroups
96+
# logger.info("Registering MCP servers")
97+
# await register_mcp_servers_async(logger, configuration.configuration)
98+
# get_logger("app.endpoints.handlers")
7599
logger.info("App startup complete")
76100

77101
initialize_database()

src/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125
# MCP authorization header special values
126126
MCP_AUTH_KUBERNETES = "kubernetes"
127127
MCP_AUTH_CLIENT = "client"
128+
# MCP authorization header name (special handling for llama_stack 0.4.x+)
129+
MCP_AUTHORIZATION_HEADER = "authorization"
128130

129131
# default RAG tool value
130132
DEFAULT_RAG_TOOL = "knowledge_search"

0 commit comments

Comments
 (0)