Skip to content

Client doesn't use bath path for SSE transport? #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alexellis opened this issue Apr 14, 2025 · 5 comments
Closed

Client doesn't use bath path for SSE transport? #150

alexellis opened this issue Apr 14, 2025 · 5 comments
Labels
bug Something isn't working

Comments

@alexellis
Copy link

alexellis commented Apr 14, 2025

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?

@jlowin
Copy link
Owner

jlowin commented Apr 14, 2025

Thanks for the report - definitely feels like something off, though that endpoint hasn't changed from v1 so it might be unintentional. I'll have to investigate.

@jlowin jlowin added the bug Something isn't working label Apr 14, 2025
@alexellis
Copy link
Author

Thanks for taking a look. It could be something in the configuration, I also found this:

modelcontextprotocol/python-sdk#412 (comment)

What's the relationship between the two projects with what looks like the same name i.e. "FastMCP"?

@jlowin
Copy link
Owner

jlowin commented Apr 14, 2025

It looks like this is in the server config -- when I run the first code block (with the Client), I do see it trying to use the base URL correctly (and failing, because I'm not running any server).

I will take a look at patching that issue here. Version 1 of FastMCP was absorbed into the official SDK a few months ago; FastMCP v2 lives here.

@jlowin
Copy link
Owner

jlowin commented Apr 14, 2025

Actually if this is a client issue, I think maybe it will be fixed by modelcontextprotocol/python-sdk#386 since the client inherits that transport. Still looking at the server config

@jlowin
Copy link
Owner

jlowin commented Apr 14, 2025

After testing, I do think this is an issue in the low-level server and is addressed by modelcontextprotocol/python-sdk#386. I'm going to close it here as FastMCP is re-exposing the low-level server and therefore we don't have a place to fix it. As far as I can tell the FastMCP client is respecting the base path for its initial call, but the server itself is sending it to the wrong endpoint. If that's not the case, happy to reopen as a client bug.

@jlowin jlowin closed this as completed Apr 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants