Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e84e2ce
feat(trace): Disabled otlp-grpc block mode (#321)
ekkinox Mar 5, 2025
a2d1117
chore(main): release trace 1.4.0 (#322)
github-actions[bot] Mar 5, 2025
b2dd342
feat(httpclient): Added test server helper (#323)
ekkinox Mar 6, 2025
b818870
chore(main): release httpclient 1.5.0 (#324)
github-actions[bot] Mar 6, 2025
9e3a6d0
doc(fxhttpclient): Updated documentation (#325)
ekkinox Mar 6, 2025
ae58a8f
feat(fxcore): Added core tasks system (#326)
ekkinox Mar 13, 2025
8262d66
chore(main): release fxcore 1.11.0 (#327)
github-actions[bot] Mar 13, 2025
e4574e5
doc(main): Updated documentation (#328)
ekkinox Mar 13, 2025
f5b741b
feat(fxcore): Added core server exposition config (#329)
ekkinox Mar 18, 2025
c3c627a
chore(main): release fxcore 1.12.0 (#330)
github-actions[bot] Mar 18, 2025
286a454
doc(main): Updated documentation (#331)
ekkinox Mar 18, 2025
2930493
feat(fxclock): Provided module (#332)
MonkeyDDude Mar 31, 2025
4704444
chore(main): release fxclock 1.0.0 (#333)
github-actions[bot] Mar 31, 2025
9ddf337
doc(fxclock): Updated documentation (#334)
ekkinox Mar 31, 2025
233a5f5
feat(fxmcpserver): Provided module (#335)
ekkinox May 6, 2025
3b6e16f
chore(main): release fxmcpserver 1.0.0 (#336)
github-actions[bot] May 6, 2025
27f3460
doc(fxmcpserver): Updated documentation (#337)
ekkinox May 6, 2025
f117843
doc(fxmcpserver): Updated documentation (#338)
ekkinox May 6, 2025
ab30269
doc(main): Updated documentation (#339)
ekkinox May 7, 2025
66811ff
feat(fxmcpserver): Updated context handling (#340)
ekkinox May 7, 2025
50eae94
chore(main): release fxmcpserver 1.1.0 (#341)
github-actions[bot] May 7, 2025
119bd6c
feat(fxmcpserver): Updated SSE test client (#342)
ekkinox May 7, 2025
77157ca
chore(main): release fxmcpserver 1.2.0 (#343)
github-actions[bot] May 7, 2025
e126b4f
doc(main): Updated documentation (#344)
ekkinox May 8, 2025
2c6a111
doc(main): Updated documentation (#345)
ekkinox May 8, 2025
a178d88
doc(main): Updated documentation (#346)
ekkinox May 8, 2025
17dabfe
feat(fxmcpserver): Added MCP SSE server context hooks (#347)
ekkinox May 8, 2025
3265e3a
chore(main): release fxmcpserver 1.3.0 (#348)
github-actions[bot] May 8, 2025
dfc8362
doc(fxmcpserver): Updated documentation (#349)
ekkinox May 8, 2025
dfc463e
feat(fxmcpserver): Added tracing remote propagation (#350)
ekkinox May 14, 2025
dd07f38
chore(main): release fxmcpserver 1.4.0 (#351)
github-actions[bot] May 14, 2025
bd74d10
feat(fxmcpserver): Added autoconfiguration of the SSE test server end…
ekkinox May 14, 2025
b449b0f
chore(main): release fxmcpserver 1.5.0 (#353)
github-actions[bot] May 14, 2025
d9e879a
fix(fxmcpserver): Fixed MCP SSE server tracing to accept remote conte…
ekkinox May 26, 2025
4bc9276
chore(main): release fxmcpserver 1.5.1 (#355)
github-actions[bot] May 26, 2025
db59402
doc(main): Fixed tutorials documentation (#356)
ekkinox May 28, 2025
b9b01f0
feat(fxmcpserver): Provided streamable HTTP transport (#357)
ekkinox Jun 5, 2025
4176884
chore(main): release fxmcpserver 1.6.0 (#358)
github-actions[bot] Jun 5, 2025
eaa80e2
doc(fxmcpserver): Updated documentation (#359)
ekkinox Jun 5, 2025
9e24793
doc(fxmcpserver): Updated documentation (#360)
ekkinox Jun 17, 2025
50dbd83
chore(main): Adapted linter version (#363)
ekkinox Sep 1, 2025
2042e33
fix(fxhttpserver): Fix HTTP handlers/middlewares dependency injection…
damnedest Sep 1, 2025
37e8c19
chore(main): release fxhttpserver 1.7.1 (#364)
github-actions[bot] Sep 1, 2025
d318ece
fix(fxhealthcheck): Fix resolution collision by using full import pat…
damnedest Sep 2, 2025
69c137b
chore(main): release fxhealthcheck 1.1.1 (#369)
github-actions[bot] Sep 2, 2025
2cdca0b
fix(fxgrpcserver): Fix resolution collision by using full import path…
damnedest Sep 2, 2025
4977596
chore(main): release fxgrpcserver 1.3.1 (#370)
github-actions[bot] Sep 2, 2025
8db4440
fix(fxcron): Fix resolution collision by using full import path in ty…
damnedest Sep 2, 2025
e2c0482
chore(main): release fxcron 1.1.1 (#371)
github-actions[bot] Sep 2, 2025
bb92938
fix(fxworker): Fix resolution collision by using full import path in …
damnedest Sep 2, 2025
65ec792
chore(main): release fxworker 1.1.1 (#372)
github-actions[bot] Sep 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(fxmcpserver): Added MCP SSE server context hooks (ankorstore#347)
  • Loading branch information
ekkinox authored May 8, 2025
commit 17dabfebe23951215ead3a2efdb502eafe2b7751
59 changes: 59 additions & 0 deletions fxmcpserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* [Resource templates](#resource-templates)
* [Prompts](#prompts)
* [Tools](#tools)
* [Hooks](#hooks)
* [Testing](#testing)
<!-- TOC -->

Expand All @@ -37,6 +38,7 @@ This module provides an [MCP server](https://modelcontextprotocol.io/introductio
- automatic requests logging and tracing (method, target, duration, ...)
- automatic requests metrics (count and duration)
- possibility to register MCP resources, resource templates, prompts and tools
- possibility to register MCP SSE server context hooks
- possibility to expose the MCP server via Stdio (local) and/or HTTP SSE (remote)

## Documentation
Expand Down Expand Up @@ -549,6 +551,63 @@ modules:
capabilities:
tools: true # to expose MCP tools (disabled by default)
```
### Hooks

This module offers the possibility to provide context hooks with [MCPSSEServerContextHook](server/sse/context.go) implementations, applied on each MCP SSE request.

You can use the `AsMCPSSEServerMiddleware()` function to register an MCP SSE server middleware, or `AsMCPSSEServerMiddlewares()` to register several MCP SSE server middlewares at once.

The dependencies of your MCP SSE server middlewares will be autowired.

```go
package main

import (
"context"
"net/http"

"github.com/ankorstore/yokai/config"
"github.com/ankorstore/yokai/fxconfig"
"github.com/ankorstore/yokai/fxgenerate"
"github.com/ankorstore/yokai/fxlog"
"github.com/ankorstore/yokai/fxmcpserver"
"github.com/ankorstore/yokai/fxmetrics"
"github.com/ankorstore/yokai/fxtrace"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"go.uber.org/fx"
)

type ExampleHook struct {
config *config.Config
}

func NewExampleHook(config *config.Config) *ExampleHook {
return &ExampleHook{
config: config,
}
}

func (r *ExampleHook) Handle() server.SSEContextFunc {
return func(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, "foo", "bar")
}
}

func main() {
fx.New(
fxconfig.FxConfigModule,
fxlog.FxLogModule,
fxtrace.FxTraceModule,
fxmetrics.FxMetricsModule,
fxgenerate.FxGenerateModule,
fxmcpserver.FxMCPServerModule,
fx.Options(
fxmcpserver.AsMCPSSEServerContextHook(NewExampleHook), // registers the NewExampleHook as MCP SSE server context hook
),
).Run()
}
```

### Testing

Expand Down
14 changes: 10 additions & 4 deletions fxmcpserver/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,20 @@ func ProvideMCPServer(p ProvideMCPServerParam) *server.MCPServer {
// ProvideDefaultMCPSSEContextHandlerParam allows injection of the required dependencies in ProvideDefaultMCPSSEServerContextHandler.
type ProvideDefaultMCPSSEContextHandlerParam struct {
fx.In
Generator uuid.UuidGenerator
TracerProvider trace.TracerProvider
Logger *log.Logger
Generator uuid.UuidGenerator
TracerProvider trace.TracerProvider
Logger *log.Logger
MCPSSEServerContextHooks []sse.MCPSSEServerContextHook `group:"mcp-sse-server-context-hooks"`
}

// ProvideDefaultMCPSSEServerContextHandler provides the default sse.MCPSSEServerContextHandler instance.
func ProvideDefaultMCPSSEServerContextHandler(p ProvideDefaultMCPSSEContextHandlerParam) *sse.DefaultMCPSSEServerContextHandler {
return sse.NewDefaultMCPSSEServerContextHandler(p.Generator, p.TracerProvider, p.Logger)
return sse.NewDefaultMCPSSEServerContextHandler(
p.Generator,
p.TracerProvider,
p.Logger,
p.MCPSSEServerContextHooks...,
)
}

// ProvideDefaultMCPSSEServerFactoryParams allows injection of the required dependencies in ProvideDefaultMCPSSEServerFactory.
Expand Down
6 changes: 4 additions & 2 deletions fxmcpserver/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/ankorstore/yokai/fxmcpserver"
"github.com/ankorstore/yokai/fxmcpserver/fxmcpservertest"
fs "github.com/ankorstore/yokai/fxmcpserver/server"
"github.com/ankorstore/yokai/fxmcpserver/testdata/hook"
"github.com/ankorstore/yokai/fxmcpserver/testdata/prompt"
"github.com/ankorstore/yokai/fxmcpserver/testdata/resource"
"github.com/ankorstore/yokai/fxmcpserver/testdata/resourcetemplate"
Expand Down Expand Up @@ -56,6 +57,7 @@ func TestMCPServerModule(t *testing.T) {
fxmcpserver.AsMCPServerPrompts(prompt.NewSimpleTestPrompt),
fxmcpserver.AsMCPServerResources(resource.NewSimpleTestResource),
fxmcpserver.AsMCPServerResourceTemplates(resourcetemplate.NewSimpleTestResourceTemplate),
fxmcpserver.AsMCPSSEServerContextHooks(hook.NewSimpleMCPSSEServerContextHook),
fxhealthcheck.AsCheckerProbe(fs.NewMCPServerProbe),
),
fx.Supply(fx.Annotate(context.Background(), fx.As(new(context.Context)))),
Expand Down Expand Up @@ -174,15 +176,15 @@ func TestMCPServerModule(t *testing.T) {

// send success prompts/get request
expectedRequest = `{"method":"prompts/get","params":{"name":"simple-test-prompt"}}`
expectedResponse = `{"description":"ok","messages":[{"role":"assistant","content":{"type":"text","text":"simple test prompt"}}]}`
expectedResponse = `{"description":"ok","messages":[{"role":"assistant","content":{"type":"text","text":"context hook value: bar"}}]}`

getPromptRequest := mcp.GetPromptRequest{}
getPromptRequest.Params.Name = "simple-test-prompt"

getPromptResult, err := testClient.GetPrompt(ctx, getPromptRequest)
assert.NoError(t, err)
assert.Equal(t, mcp.RoleAssistant, getPromptResult.Messages[0].Role)
assert.Equal(t, "simple test prompt", getPromptResult.Messages[0].Content.(mcp.TextContent).Text)
assert.Equal(t, "context hook value: bar", getPromptResult.Messages[0].Content.(mcp.TextContent).Text)

logtest.AssertHasLogRecord(t, logBuffer, map[string]any{
"level": "info",
Expand Down
23 changes: 23 additions & 0 deletions fxmcpserver/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fxmcpserver

import (
"github.com/ankorstore/yokai/fxmcpserver/server"
"github.com/ankorstore/yokai/fxmcpserver/server/sse"
"go.uber.org/fx"
)

Expand Down Expand Up @@ -92,3 +93,25 @@ func AsMCPServerResourceTemplates(constructors ...any) fx.Option {

return fx.Options(options...)
}

// AsMCPSSEServerContextHook registers an MCP SSE server context hook.
func AsMCPSSEServerContextHook(constructor any) fx.Option {
return fx.Provide(
fx.Annotate(
constructor,
fx.As(new(sse.MCPSSEServerContextHook)),
fx.ResultTags(`group:"mcp-sse-server-context-hooks"`),
),
)
}

// AsMCPSSEServerContextHooks registers several MCP SSE server context hook.
func AsMCPSSEServerContextHooks(constructors ...any) fx.Option {
options := []fx.Option{}

for _, constructor := range constructors {
options = append(options, AsMCPSSEServerContextHook(constructor))
}

return fx.Options(options...)
}
19 changes: 19 additions & 0 deletions fxmcpserver/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/ankorstore/yokai/fxmcpserver"
"github.com/ankorstore/yokai/fxmcpserver/testdata/hook"
"github.com/ankorstore/yokai/fxmcpserver/testdata/prompt"
"github.com/ankorstore/yokai/fxmcpserver/testdata/resource"
"github.com/ankorstore/yokai/fxmcpserver/testdata/resourcetemplate"
Expand Down Expand Up @@ -84,3 +85,21 @@ func TestAsMCPServerResourceTemplates(t *testing.T) {
assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", reg))
assert.Implements(t, (*fx.Option)(nil), reg)
}

func TestAsMCPSSEServerContextHook(t *testing.T) {
t.Parallel()

reg := fxmcpserver.AsMCPSSEServerContextHook(hook.NewSimpleMCPSSEServerContextHook)

assert.Equal(t, "fx.provideOption", fmt.Sprintf("%T", reg))
assert.Implements(t, (*fx.Option)(nil), reg)
}

func TestAsMCPSSEServerContextHooks(t *testing.T) {
t.Parallel()

reg := fxmcpserver.AsMCPSSEServerContextHooks(hook.NewSimpleMCPSSEServerContextHook)

assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", reg))
assert.Implements(t, (*fx.Option)(nil), reg)
}
19 changes: 17 additions & 2 deletions fxmcpserver/server/sse/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (

var _ MCPSSEServerContextHandler = (*DefaultMCPSSEServerContextHandler)(nil)

// MCPSSEServerContextHook is the interface for MCP SSE server context hooks.
type MCPSSEServerContextHook interface {
Handle() server.SSEContextFunc
}

// MCPSSEServerContextHandler is the interface for MCP SSE server context handlers.
type MCPSSEServerContextHandler interface {
Handle() server.SSEContextFunc
Expand All @@ -26,18 +31,21 @@ type DefaultMCPSSEServerContextHandler struct {
generator uuid.UuidGenerator
tracerProvider ot.TracerProvider
logger *log.Logger
contextHooks []MCPSSEServerContextHook
}

// NewDefaultMCPSSEServerContextHandler returns a new DefaultMCPSSEServerContextHandler instance.
func NewDefaultMCPSSEServerContextHandler(
generator uuid.UuidGenerator,
tracerProvider ot.TracerProvider,
logger *log.Logger,
contextHooks ...MCPSSEServerContextHook,
) *DefaultMCPSSEServerContextHandler {
return &DefaultMCPSSEServerContextHandler{
generator: generator,
tracerProvider: tracerProvider,
logger: logger,
contextHooks: contextHooks,
}
}

Expand Down Expand Up @@ -91,7 +99,14 @@ func (h *DefaultMCPSSEServerContextHandler) Handle() server.SSEContextFunc {

ctx = logger.WithContext(ctx)

// cancellation removal
return context.WithoutCancel(ctx)
// cancellation removal propagation
ctx = context.WithoutCancel(ctx)

// hooks propagation
for _, hook := range h.contextHooks {
ctx = hook.Handle()(ctx, req)
}

return ctx
}
}
34 changes: 20 additions & 14 deletions fxmcpserver/server/sse/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

servercontext "github.com/ankorstore/yokai/fxmcpserver/server/context"
"github.com/ankorstore/yokai/fxmcpserver/server/sse"
"github.com/ankorstore/yokai/fxmcpserver/testdata/hook"
"github.com/ankorstore/yokai/log"
"github.com/ankorstore/yokai/log/logtest"
"github.com/stretchr/testify/assert"
Expand All @@ -27,11 +28,11 @@ func (m *generatorMock) Generate() string {
func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {
t.Parallel()

t.Run("with provided session id and request id", func(t *testing.T) {
t.Run("with defaults", func(t *testing.T) {
t.Parallel()

gm := new(generatorMock)
gm.AssertNotCalled(t, "Generate")
gm.On("Generate").Return("test-request-id")

tp := trace.NewTracerProvider()

Expand All @@ -41,12 +42,11 @@ func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {

handler := sse.NewDefaultMCPSSEServerContextHandler(gm, tp, lg)

req := httptest.NewRequest(http.MethodGet, "/sse?sessionId=test-session-id", nil)
req.Header.Set("X-Request-Id", "test-request-id")
req := httptest.NewRequest(http.MethodGet, "/sse", nil)

ctx := handler.Handle()(context.Background(), req)

assert.Equal(t, "test-session-id", servercontext.CtxSessionID(ctx))
assert.Equal(t, "", servercontext.CtxSessionID(ctx))
assert.Equal(t, "test-request-id", servercontext.CtxRequestId(ctx))

span, ok := servercontext.CtxRootSpan(ctx).(trace.ReadWriteSpan)
Expand All @@ -62,7 +62,7 @@ func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {
assert.Equal(t, "sse", attr.Value.AsString())
}
if attr.Key == "mcp.sessionID" {
assert.Equal(t, "test-session-id", attr.Value.AsString())
assert.Equal(t, "", attr.Value.AsString())
}
if attr.Key == "mcp.requestID" {
assert.Equal(t, "test-request-id", attr.Value.AsString())
Expand All @@ -75,33 +75,36 @@ func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {
"level": "info",
"system": "mcpserver",
"mcpTransport": "sse",
"mcpSessionID": "test-session-id",
"mcpSessionID": "",
"mcpRequestID": "test-request-id",
"message": "test log",
})

gm.AssertExpectations(t)
})

t.Run("without provided session id and request id", func(t *testing.T) {
t.Run("with provided session id and request id and middleware", func(t *testing.T) {
t.Parallel()

gm := new(generatorMock)
gm.On("Generate").Return("test-request-id")
gm.AssertNotCalled(t, "Generate")

tp := trace.NewTracerProvider()

lb := logtest.NewDefaultTestLogBuffer()
lg, err := log.NewDefaultLoggerFactory().Create(log.WithOutputWriter(lb))
assert.NoError(t, err)

handler := sse.NewDefaultMCPSSEServerContextHandler(gm, tp, lg)
hk := hook.NewSimpleMCPSSEServerContextHook()

req := httptest.NewRequest(http.MethodGet, "/sse", nil)
handler := sse.NewDefaultMCPSSEServerContextHandler(gm, tp, lg, hk)

req := httptest.NewRequest(http.MethodGet, "/sse?sessionId=test-session-id", nil)
req.Header.Set("X-Request-Id", "test-request-id")

ctx := handler.Handle()(context.Background(), req)

assert.Equal(t, "", servercontext.CtxSessionID(ctx))
assert.Equal(t, "test-session-id", servercontext.CtxSessionID(ctx))
assert.Equal(t, "test-request-id", servercontext.CtxRequestId(ctx))

span, ok := servercontext.CtxRootSpan(ctx).(trace.ReadWriteSpan)
Expand All @@ -117,7 +120,7 @@ func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {
assert.Equal(t, "sse", attr.Value.AsString())
}
if attr.Key == "mcp.sessionID" {
assert.Equal(t, "", attr.Value.AsString())
assert.Equal(t, "test-session-id", attr.Value.AsString())
}
if attr.Key == "mcp.requestID" {
assert.Equal(t, "test-request-id", attr.Value.AsString())
Expand All @@ -130,11 +133,14 @@ func TestDefaultMCPSSEServerContextHandler_Handle(t *testing.T) {
"level": "info",
"system": "mcpserver",
"mcpTransport": "sse",
"mcpSessionID": "",
"mcpSessionID": "test-session-id",
"mcpRequestID": "test-request-id",
"message": "test log",
})

//nolint:forcetypeassert
assert.Equal(t, "bar", ctx.Value("foo").(string))

gm.AssertExpectations(t)
})
}
2 changes: 1 addition & 1 deletion fxmcpserver/server/stdio/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (h *DefaultMCPStdioServerContextHandler) Handle() server.StdioContextFunc {

ctx = logger.WithContext(ctx)

// cancellation removal
// cancellation removal propagation
return context.WithoutCancel(ctx)
}
}
20 changes: 20 additions & 0 deletions fxmcpserver/testdata/hook/simple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package hook

import (
"context"
"net/http"

"github.com/mark3labs/mcp-go/server"
)

type SimpleMCPSSEServerContextHook struct{}

func NewSimpleMCPSSEServerContextHook() *SimpleMCPSSEServerContextHook {
return &SimpleMCPSSEServerContextHook{}
}

func (p *SimpleMCPSSEServerContextHook) Handle() server.SSEContextFunc {
return func(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, "foo", "bar")
}
}
Loading