Skip to content

Can't invoke Streamable HTTP MCP server implemented with Micronaut MCP #582

Closed

Description

@glaforge

I implemented a Streamable HTTP MCP server with the Micronaut MCP module:
https://github.com/glaforge/mn-mcp-server

It exposes a couple methods:

  • current-moon-phase(): to get the current phase of the moon
  • moon-phase-at-date("2025-09-28"): to get the phase at a certain date

You can call that server here with the MCP Inspector for testing:
https://mn-mcp-server-1029513523185.europe-west1.run.app/mcp/
The MCP inspector is totally fine with it, no error, I can list all the tools, call all the methods, etc.

I tried with the LangChain4j MCP module as well (which is a reimplementation of the protocol), with the following code:
https://gist.github.com/glaforge/809ccbad223886b0db03e679adcb7e85
And it also works totally fine with my server.

However, when I can from the Agent Development Kit for Java (ADK), it fails.
As ADK is using the official MCP SDK, I tried to reproduce the problem with the SDK directly, to remove ADK from the equation, and it fails in the same way.

I reproduced the problem with the following code:
https://gist.github.com/glaforge/741edcad883b27268ab10249400ce6d6

I'm seeing this stacktrace (same as within ADK for Java):
https://gist.github.com/glaforge/afd1194ece6bc909b091a946c030dc27

i.e.

io.modelcontextprotocol.spec.McpTransportException: Invalid SSE response. Status code: 405 Line: {"message":"Method Not Allowed","_links":{"self":{"href":"/mcp","templated":false}},"_embedded":{"errors":[{"message":"Method [GET] not allowed for URI [/mcp]. Allowed methods: [POST]"}]}}
	at io.modelcontextprotocol.client.transport.ResponseSubscribers$SseLineSubscriber.hookOnNext(ResponseSubscribers.java:185)
	at io.modelcontextprotocol.client.transport.ResponseSubscribers$SseLineSubscriber.hookOnNext(ResponseSubscribers.java:72)
...
21:04:44.389 [HttpClient-1-Worker-0] WARN io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport -- Unknown media type text/html returned for POST in session [missing_session_id]
21:04:44.389 [HttpClient-1-Worker-0] WARN io.modelcontextprotocol.client.LifecycleInitializer -- Handling exception
java.lang.RuntimeException: Unknown media type returned: text/html
	at io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport.lambda$sendMessage$27(HttpClientStreamableHttpTransport.java:522)
...
Caused by: java.lang.RuntimeException: Unknown media type returned: text/html
	at io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport.lambda$sendMessage$27(HttpClientStreamableHttpTransport.java:522)

So I'm not sure if it's a problem on Micronaut's side or on the MCP SDK's side.
Maybe one is too strict with the spec, or not tolerant enough, I don't know.

But when I read the spec:

The server MUST provide a single HTTP endpoint path (hereafter referred to as the MCP endpoint) 
that supports both POST and GET methods. For example, this could be a URL like 
https://example.com/mcp.
The client MAY issue an HTTP GET to the MCP endpoint. This can be used to open an SSE stream, 
allowing the server to communicate to the client, without the client first sending data via HTTP POST.
The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, 
or else return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE 
stream at this endpoint.

It seems that Micronaut does the right thing: if a client tries to do a GET to do an SSE connection,
it gets a 405, which is the error Micronaut is returning.

Any idea what could be going wrong here @tzolov ?

/cc @sdelamo @graemerocher @Poggecci

Activity

  1. Kehrlann commented on Sep 30, 2025

    @Kehrlann
    Contributor

    @glaforge
    The McpSyncClient + HttpClientStreamableHttpTransport works against our "stateless server implementation".

    When trying against your server, I'm seeing a 404 when the client sends notifications/initialized (see MCP Initialization phase), but we expect a successful HTTP 2xx (we send 202).
    Since the language says the client MUST send this notification, I assume it's implied the server MUST accept it.

    Now the spec seems to indicate that 202 is the "correct" response, see #4 in the streamable HTTP transport spec, unless the server cannot accept. What would "cannot accept" mean in this context, I am unsure.

    Using the following script to test:

    #!/usr/bin/env bash
    
    server="https://mn-mcp-server-1029513523185.europe-west1.run.app/mcp"
    
    init_request='{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "initialize",
      "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {
          "roots": {
            "listChanged": true
          },
          "sampling": {},
          "elicitation": {}
        },
        "clientInfo": {
          "name": "ExampleClient",
          "title": "Example Client Display Name",
          "version": "1.0.0"
        }
      }
    }'
    
    initialized_notif='{
      "jsonrpc": "2.0",
      "method": "notifications/initialized"
    }'
    
    echo "
    
    -------------
    
    Sending initialize ...
    
    "
    
    curl -XPOST -v \
      --json "${init_request}" \
      -H "Accept: application/json, text/event-stream" \
      "$server"
    
    echo "
    
    -------------
    
    Sending notifications/initialized ...
    
    "
    
    curl -XPOST -v \
      --json "${initialized_notif}" \
      -H "Accept: application/json, text/event-stream" \
      "$server"
  2. Kehrlann commented on Sep 30, 2025

    @Kehrlann
    Contributor

    And just now I see @sdelamo has fixed it 🤦 Could you please try again with the latest micronaut build @glaforge ?

  3. self-assigned this
    on Sep 30, 2025
  4. glaforge commented on Oct 1, 2025

    @glaforge
    Author

    Hi @Kehrlann ! Good to see you here 😄
    I've just redeployed my Micronaut MCP server (still at the same address) with the new 0.0.8 version from @sdelamo from yesterday.
    However, I'm still seeing the same errors right now.

    io.modelcontextprotocol.spec.McpTransportException: Invalid SSE response. Status code: 405 Line: {"_links":{"self":[{"href":"/mcp","templated":false}]},"_embedded":{"errors":[{"message":"Method [GET] not allowed for URI [/mcp]. Allowed methods: [POST]"}]},"message":"Method Not Allowed"}
    	at io.modelcontextprotocol.client.transport.ResponseSubscribers$SseLineSubscriber.hookOnNext(ResponseSubscribers.java:185)
    	at io.modelcontextprotocol.client.transport.ResponseSubscribers$SseLineSubscriber.hookOnNext(ResponseSubscribers.java:72)
    	at reactor.core.publisher.BaseSubscriber.onNext(BaseSubscriber.java:160)
    	at org.reactivestreams.FlowAdapters$FlowToReactiveSubscriber.onNext(FlowAdapters.java:211)
    	at java.net.http/jdk.internal.net.http.LineSubscriberAdapter$LineSubscription.loop(LineSubscriberAdapter.java:447)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:280)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:233)
    	at java.net.http/jdk.internal.net.http.LineSubscriberAdapter$LineSubscription.signalComplete(LineSubscriberAdapter.java:193)
    	at java.net.http/jdk.internal.net.http.LineSubscriberAdapter.onComplete(LineSubscriberAdapter.java:114)
    	at java.net.http/jdk.internal.net.http.common.HttpBodySubscriberWrapper.complete(HttpBodySubscriberWrapper.java:293)
    	at java.net.http/jdk.internal.net.http.common.HttpBodySubscriberWrapper.onComplete(HttpBodySubscriberWrapper.java:401)
    	at java.net.http/jdk.internal.net.http.ResponseContent$FixedLengthBodyParser.accept(ResponseContent.java:629)
    	at java.net.http/jdk.internal.net.http.ResponseContent$FixedLengthBodyParser.accept(ResponseContent.java:540)
    	at java.net.http/jdk.internal.net.http.Http1Response$BodyReader.handle(Http1Response.java:707)
    	at java.net.http/jdk.internal.net.http.Http1Response$BodyReader.handle(Http1Response.java:635)
    	at java.net.http/jdk.internal.net.http.Http1Response$Receiver.accept(Http1Response.java:527)
    	at java.net.http/jdk.internal.net.http.Http1Response$BodyReader.tryAsyncReceive(Http1Response.java:665)
    	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.flush(Http1AsyncReceiver.java:233)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$TryEndDeferredCompleter.complete(SequentialScheduler.java:324)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:151)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
    	at java.net.http/jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(HttpClientImpl.java:177)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:282)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:251)
    	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.asyncReceive(Http1AsyncReceiver.java:468)
    	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$Http1TubeSubscriber.onNext(Http1AsyncReceiver.java:589)
    	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$Http1TubeSubscriber.onNext(Http1AsyncReceiver.java:546)
    	at java.net.http/jdk.internal.net.http.common.SSLTube$DelegateWrapper.onNext(SSLTube.java:210)
    	at java.net.http/jdk.internal.net.http.common.SSLTube$SSLSubscriberWrapper.onNext(SSLTube.java:492)
    	at java.net.http/jdk.internal.net.http.common.SSLTube$SSLSubscriberWrapper.onNext(SSLTube.java:295)
    	at java.net.http/jdk.internal.net.http.common.SubscriberWrapper$DownstreamPusher.run1(SubscriberWrapper.java:316)
    	at java.net.http/jdk.internal.net.http.common.SubscriberWrapper$DownstreamPusher.run(SubscriberWrapper.java:259)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:280)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:233)
    	at java.net.http/jdk.internal.net.http.common.SubscriberWrapper.outgoing(SubscriberWrapper.java:232)
    	at java.net.http/jdk.internal.net.http.common.SubscriberWrapper.outgoing(SubscriberWrapper.java:198)
    	at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:451)
    	at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:269)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
    	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    	at java.base/java.lang.Thread.run(Thread.java:1583)
    10:12:24.228 [HttpClient-1-Worker-0] WARN io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport -- Unknown media type text/html returned for POST in session [missing_session_id]
    10:12:24.228 [HttpClient-1-Worker-0] WARN io.modelcontextprotocol.client.LifecycleInitializer -- Handling exception
    java.lang.RuntimeException: Unknown media type returned: text/html
    	at io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport.lambda$sendMessage$27(HttpClientStreamableHttpTransport.java:514)
    	at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:388)
    	at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:251)
    	at reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:880)
    	at reactor.core.publisher.FluxCreate$BufferAsyncSink.next(FluxCreate.java:805)
    	at reactor.core.publisher.FluxCreate$SerializedFluxSink.next(FluxCreate.java:163)
    	at io.modelcontextprotocol.client.transport.ResponseSubscribers$BodilessResponseLineSubscriber.hookOnComplete(ResponseSubscribers.java:315)
    	at reactor.core.publisher.BaseSubscriber.onComplete(BaseSubscriber.java:197)```
    
  5. glaforge commented on Oct 1, 2025

    @glaforge
    Author

    And what's interesting is that other clients like LangChain4j's MCP support, Gemini CLI's MCP support, are all happy with the Micronaut server I deployed. The MCP SDK is quite strict compared to other clients.

  6. Kehrlann commented on Oct 1, 2025

    @Kehrlann
    Contributor

    Ah, I see.

    So now the response is fixed, it'd not 404 anymore. However, we get:

    HTTP/2 202 
    x-cloud-trace-context: 7c1b401773491e4d5bd6bc4b300d1f1c
    date: Wed, 01 Oct 2025 09:04:43 GMT
    content-type: text/html
    server: Google Frontend
    content-length: 0
    alt-svc: h3=":443"; ma=2592000,h3-29=":443"; m
    

    So we have a content-type: text/html with no body. I think this is valid. The SDK gets tripped by content-type not being either text/event-stream or application/json ; because even the spec mandates those return types for JSONRPC-requests (see transports).
    We could look at the content-length and avoid being tripped up.

    In the meantime, you can update either Micronaut or your frontend to strip that header.

  7. added a commit that references this issue on Oct 1, 2025
    c4bf139
  8. glaforge commented on Oct 1, 2025

    @glaforge
    Author

    @Kehrlann any change that you're going to make a release before Devoxx with the change in c4bf139 ? 😄

  9. glaforge commented on Oct 1, 2025

    @glaforge
    Author

    On the ADK integration of the MCP SDK, I noticed we use an outdated version of the SDK: 0.11.3 instead of 0.13.1.
    I'll try to get that version upgraded rapidly as well.

  10. tzolov commented on Oct 1, 2025

    @tzolov
    Contributor

    @Kehrlann any change that you're going to make a release before Devoxx with the change in c4bf139 ? 😄

    Hi @glaforge , we plan to have 0.14.0 release today or tomorrow. Will make sure that #588 is in

  11. added a commit that references this issue on Oct 1, 2025
    bb125d8
  12. glaforge commented on Oct 1, 2025

    @glaforge
    Author

    Awesome! Thank you both!

  13. added a commit that references this issue on Oct 1, 2025
    080098e
  14. added a commit that references this issue on Jun 25, 2026
    7f007e3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions