Skip to content

Commit b9b01f0

Browse files
authored
feat(fxmcpserver): Provided streamable HTTP transport (#357)
1 parent db59402 commit b9b01f0

31 files changed

+1622
-353
lines changed

docs/modules/fxmcpserver.md

Lines changed: 170 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ It comes with:
2323
- automatic requests logging and tracing (method, target, duration, ...)
2424
- automatic requests metrics (count and duration)
2525
- possibility to register MCP resources, resource templates, prompts and tools
26-
- possibility to register MCP SSE server context hooks
27-
- possibility to expose the MCP server via Stdio (local) and/or HTTP SSE (remote)
26+
- possibility to register MCP Streamable HTTP and SSE server context hooks
27+
- possibility to expose the MCP server via Streamable HTTP (remote), HTTP SSE (remote) and Stdio (local)
2828

2929
## Installation
3030

31-
First install the module:
31+
First, install the module:
3232

3333
```shell
3434
go get github.com/ankorstore/yokai/fxmcpserver
@@ -60,10 +60,17 @@ modules:
6060
name: "MCP Server" # server name ("MCP server" by default)
6161
version: 1.0.0 # server version (1.0.0 by default)
6262
capabilities:
63-
resources: true # to expose MCP resources & resource templates (disabled by default)
63+
resources: true # to expose MCP resources and resource templates (disabled by default)
6464
prompts: true # to expose MCP prompts (disabled by default)
6565
tools: true # to expose MCP tools (disabled by default)
6666
transport:
67+
stream:
68+
expose: true # to remotely expose the MCP server via Streamable HTTP (disabled by default)
69+
address: ":8083" # exposition address (":8083" by default)
70+
stateless: false # stateless server mode (disabled by default)
71+
base_path: "/mcp" # base path ("/mcp" by default)
72+
keep_alive: true # to keep the connections alive
73+
keep_alive_interval: 10 # keep alive interval in seconds (10 by default)
6774
sse:
6875
expose: true # to remotely expose the MCP server via SSE (disabled by default)
6976
address: ":8082" # exposition address (":8082" by default)
@@ -74,7 +81,7 @@ modules:
7481
keep_alive: true # to keep connection alive
7582
keep_alive_interval: 10 # keep alive interval in seconds (10 by default)
7683
stdio:
77-
expose: false # to locally expose the MCP server via Stdio (disabled by default)
84+
expose: true # to locally expose the MCP server via Stdio (disabled by default)
7885
log:
7986
request: true # to log MCP requests contents (disabled by default)
8087
response: true # to log MCP responses contents (disabled by default)
@@ -270,7 +277,7 @@ import (
270277

271278
func Register() fx.Option {
272279
return fx.Options(
273-
// registers UserProfileResource as MCP resource
280+
// registers UserProfileResource as MCP resource template
274281
fxmcpserver.AsMCPServerResourceTemplate(resource.NewUserProfileResource),
275282
// ...
276283
)
@@ -519,6 +526,70 @@ modules:
519526
520527
## Hooks
521528
529+
This module provides hooking mechanisms for the `StreamableHTTP` and `SSE` servers requests handling.
530+
531+
### StreamableHTTP server hooks
532+
533+
This module offers the possibility to provide context hooks with [MCPStreamableHTTPServerContextHook](https://github.com/ankorstore/yokai/blob/main/fxmcpserver/server/stream/context.go) implementations, that will be applied on each MCP StreamableHTTP request.
534+
535+
For example, an MCP StreamableHTTP server context hook that adds a config value to the context:
536+
537+
```go title="internal/mcp/resource/readme.go"
538+
package hook
539+
540+
import (
541+
"context"
542+
"net/http"
543+
544+
"github.com/ankorstore/yokai/config"
545+
"github.com/mark3labs/mcp-go/mcp"
546+
"github.com/mark3labs/mcp-go/server"
547+
)
548+
549+
type ExampleHook struct {
550+
config *config.Config
551+
}
552+
553+
func NewExampleHook(config *config.Config) *ExampleHook {
554+
return &ExampleHook{
555+
config: config,
556+
}
557+
}
558+
559+
func (h *ExampleHook) Handle() server.HTTPContextFunc {
560+
return func(ctx context.Context, r *http.Request) context.Context {
561+
return context.WithValue(ctx, "foo", h.config.GetString("foo"))
562+
}
563+
}
564+
```
565+
566+
You can register your MCP StreamableHTTP server context hook:
567+
568+
- with `AsMCPStreamableHTTPServerContextHook()` to register a single MCP StreamableHTTP server context hook
569+
- with `AsMCPStreamableHTTPServerContextHooks()` to register several MCP StreamableHTTP server context hooks at once
570+
571+
```go title="internal/register.go"
572+
package internal
573+
574+
import (
575+
"github.com/ankorstore/yokai/fxmcpserver"
576+
"github.com/foo/bar/internal/mcp/hook"
577+
"go.uber.org/fx"
578+
)
579+
580+
func Register() fx.Option {
581+
return fx.Options(
582+
// registers ExampleHook as MCP StreamableHTTP server context hook
583+
fxmcpserver.AsMCPStreamableHTTPServerContextHook(hook.NewExampleHook),
584+
// ...
585+
)
586+
}
587+
```
588+
589+
The dependencies of your MCP StreamableHTTP server context hooks will be autowired.
590+
591+
### SSE server hooks
592+
522593
This module offers the possibility to provide context hooks with [MCPSSEServerContextHook](https://github.com/ankorstore/yokai/blob/main/fxmcpserver/server/sse/context.go) implementations, that will be applied on each MCP SSE request.
523594

524595
For example, an MCP SSE server context hook that adds a config value to the context:
@@ -568,7 +639,7 @@ import (
568639
569640
func Register() fx.Option {
570641
return fx.Options(
571-
// registers ReadmeResource as MCP resource
642+
// registers ExampleHook as MCP SSE server context hook
572643
fxmcpserver.AsMCPSSEServerContextHook(hook.NewExampleHook),
573644
// ...
574645
)
@@ -684,7 +755,98 @@ mcp_server_requests_total{method="tools/call",status="success",target="calculato
684755

685756
## Testing
686757

687-
This module provides a [MCPSSETestServer](https://github.com/ankorstore/yokai/blob/main/fxmcpserver/fxmcpservertest/server.go) to enable you to easily test your exposed MCP registrations.
758+
This module provide `StreamableHTTP` and `SSE` test servers, to functionally test your applications.
759+
760+
### StreamableHTTP test server
761+
762+
This module provides a [MCPStreamableHTTPTestServer](https://github.com/ankorstore/yokai/blob/main/fxmcpserver/fxmcpservertest/stream.go) to enable you to easily test your exposed MCP registrations.
763+
764+
From this server, you can create a ready to use client via `StartClient()` to perform MCP requests, to functionally test your MCP server.
765+
766+
You can easily assert on:
767+
768+
- MCP responses
769+
- logs
770+
- traces
771+
- metrics
772+
773+
For example, to test an `MCP ping`:
774+
775+
```go title="internal/mcp/ping_test.go"
776+
package handler_test
777+
778+
import (
779+
"testing"
780+
781+
"github.com/ankorstore/yokai/log/logtest"
782+
"github.com/ankorstore/yokai/trace/tracetest"
783+
"github.com/foo/bar/internal"
784+
"github.com/prometheus/client_golang/prometheus"
785+
"github.com/prometheus/client_golang/prometheus/testutil"
786+
"github.com/stretchr/testify/assert"
787+
"go.opentelemetry.io/otel/attribute"
788+
"go.opentelemetry.io/otel/trace"
789+
"go.uber.org/fx"
790+
)
791+
792+
func TestMCPPing(t *testing.T) {
793+
var testServer *fxmcpservertest.MCPStreamableHTTPTestServer
794+
var logBuffer logtest.TestLogBuffer
795+
var traceExporter tracetest.TestTraceExporter
796+
var metricsRegistry *prometheus.Registry
797+
798+
internal.RunTest(t, fx.Populate(&testServer, &logBuffer, &traceExporter, &metricsRegistry))
799+
800+
// close the test server once done
801+
defer testServer.Close()
802+
803+
// start test client
804+
testClient, err := testServer.StartClient(context.Background())
805+
assert.NoError(t, err)
806+
807+
// close the test client once done
808+
defer testClient.Close()
809+
810+
// send MCP ping request
811+
err = testClient.Ping(context.Background())
812+
assert.NoError(t, err)
813+
814+
// assertion on the logs buffer
815+
logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{
816+
"level": "info",
817+
"mcpMethod": "ping",
818+
"mcpTransport": "streamable-http",
819+
"message": "MCP request success",
820+
})
821+
822+
// assertion on the traces exporter
823+
tracetest.AssertHasTraceSpan(
824+
t,
825+
traceExporter,
826+
"MCP ping",
827+
attribute.String("mcp.method", "ping"),
828+
attribute.String("mcp.transport", "streamable-http"),
829+
)
830+
831+
// assertion on the metrics registry
832+
expectedMetric := `
833+
# HELP mcp_server_requests_total Number of processed MCP requests
834+
# TYPE mcp_server_requests_total counter
835+
mcp_server_requests_total{method="ping",status="success",target=""} 1
836+
`
837+
838+
err = testutil.GatherAndCompare(
839+
metricsRegistry,
840+
strings.NewReader(expectedMetric),
841+
"mcp_server_requests_total",
842+
)
843+
assert.NoError(t, err)
844+
}
845+
```
846+
847+
### SSE test server
848+
849+
This module provides a [MCPSSETestServer](https://github.com/ankorstore/yokai/blob/main/fxmcpserver/fxmcpservertest/sse.go) to enable you to easily test your exposed MCP registrations.
688850

689851
From this server, you can create a ready to use client via `StartClient()` to perform MCP requests, to functionally test your MCP server.
690852

0 commit comments

Comments
 (0)