-
Notifications
You must be signed in to change notification settings - Fork 979
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
from fastmcp import Client
from fastmcp.client.transports import (
SSETransport
)
import os
from dotenv import load_dotenv
import asyncio
import httpx
load_dotenv()
API_KEY = os.getenv('API_KEY')
async def main():
base_url = "http://127.0.0.1:8080/function/mcp/sse"
# Connect to a server over SSE (common for web-based MCP servers)
transport = SSETransport(
f"{base_url}"
)
async with Client(transport) as client:
await client.ping()
print(await client.call_tool("list_functions"))
asyncio.run(main())
I'm running this command against a function deployed to OpenFaaS. It supports SSE, and is running the sample / demo for SSE transport with uvicorn/Starlette.
The connection starts - however the client immediately tries to access the wrong base path for the messages endpoint:
Error in post_writer: Client error '404 Not Found' for url 'http://127.0.0.1:8080/messages/?session_id=2c2a505b5f40401db6bef7b72d1794a7'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
When I add debugging to the server, and use Cursor as a client instead, it still fails and I get:
2025/04/14 11:53:31 stdout: === SSE Event ===
2025/04/14 11:53:31 stdout: event: endpoint
2025/04/14 11:53:31 stdout: data: /messages/?session_id=fd599c32ea5d4f25ba434b9f13e291bf
Clearly, there is nothing mounted at /messages, it needs to respect the base path of "http://127.0.0.1:8080/function/mcp
#!/usr/bin/env python
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from starlette.requests import Request
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse, Response, StreamingResponse
import json
import asyncio
import os
from function.handler import Handler
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
print(f"\n=== Request ===")
print(f"Path: {request.url.path}")
print(f"Method: {request.method}")
print(f"Headers: {dict(request.headers)}")
response = await call_next(request)
print(f"\n=== Response ===")
print(f"Status: {response.status_code}")
print(f"Headers: {dict(response.headers)}")
# Log SSE events if it's an event stream
if response.headers.get('content-type', '').startswith('text/event-stream'):
print("\n=== SSE Stream Start ===")
async def logged_streaming():
async for chunk in response.body_iterator:
# Log each chunk of the SSE stream
try:
decoded = chunk.decode('utf-8')
if decoded.strip(): # Only log non-empty chunks
print(f"\n=== SSE Event ===\n{decoded}")
except Exception as e:
print(f"Error decoding SSE chunk: {e}")
yield chunk
return StreamingResponse(
logged_streaming(),
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.media_type
)
return response
async def handle_messages(request):
session_id = request.query_params.get('session_id')
if request.method == "GET":
return JSONResponse({
"status": "ok",
"messages": [],
"session_id": session_id
})
else: # POST
data = await request.json()
print(f"\n=== Message POST Data ===\n{json.dumps(data, indent=2)}")
return JSONResponse({
"status": "ok",
"session_id": session_id
})
# Initialize FastMCP with our API name
mcp = FastMCP("OpenFaaS API")
# Initialize handler with mcp instance
h = Handler(mcp)
# Get SSE app once to reuse
sse_app = mcp.sse_app()
# Create Starlette app with both SSE and messages endpoints
app = Starlette(
debug=True,
middleware=[
Middleware(LoggingMiddleware)
],
routes=[
Route('/sse', endpoint=sse_app, methods=['GET']),
Route('/sse/', endpoint=sse_app, methods=['GET']),
Route('/messages', endpoint=handle_messages, methods=['GET', 'POST']),
Route('/messages/', endpoint=handle_messages, methods=['GET', 'POST'])
]
)
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5000)
I've tried mounting/routing at various paths and combinations. The messages
route was a suggestion from Cursor, which I don't think is being used.
Is this an oversight? Do you need anything else from me for a fix?
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working