feat(mcp): emit channel events for brain recall/remember
Some checks failed
CI / test (push) Failing after 3s

DirectSubsystem pushes brain.recall.complete and brain.remember.complete
events via OnChannel callback. Avoids circular import by using func-based
wiring instead of interface-based SubsystemWithNotifier.

New() auto-detects subsystems with OnChannel() and wires them to
ChannelSend via closure.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-21 15:16:34 +00:00
parent bc3c7184e9
commit 1ac4ff731d
2 changed files with 43 additions and 3 deletions

View file

@ -18,13 +18,27 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// channelSender is the callback for pushing channel events.
type channelSender func(ctx context.Context, channel string, data any)
// DirectSubsystem implements mcp.Subsystem for OpenBrain via direct HTTP calls.
// Unlike Subsystem (which uses the IDE WebSocket bridge), this calls the
// Laravel API directly — suitable for standalone core-mcp usage.
type DirectSubsystem struct {
apiURL string
apiKey string
client *http.Client
apiURL string
apiKey string
client *http.Client
onChannel channelSender
}
// OnChannel sets a callback for channel event broadcasting.
// Called by the MCP service after creation to wire up notifications.
//
// brain.OnChannel(func(ctx context.Context, ch string, data any) {
// mcpService.ChannelSend(ctx, ch, data)
// })
func (s *DirectSubsystem) OnChannel(fn func(ctx context.Context, channel string, data any)) {
s.onChannel = fn
}
// NewDirect creates a brain subsystem that calls the OpenBrain API directly.
@ -132,6 +146,13 @@ func (s *DirectSubsystem) remember(ctx context.Context, _ *mcp.CallToolRequest,
}
id, _ := result["id"].(string)
if s.onChannel != nil {
s.onChannel(ctx, "brain.remember.complete", map[string]any{
"id": id,
"type": input.Type,
"project": input.Project,
})
}
return nil, RememberOutput{
Success: true,
MemoryID: id,
@ -185,6 +206,12 @@ func (s *DirectSubsystem) recall(ctx context.Context, _ *mcp.CallToolRequest, in
}
}
if s.onChannel != nil {
s.onChannel(ctx, "brain.recall.complete", map[string]any{
"query": input.Query,
"count": len(memories),
})
}
return nil, RecallOutput{
Success: true,
Count: len(memories),

View file

@ -107,6 +107,19 @@ func New(opts Options) (*Service, error) {
for _, sub := range s.subsystems {
sub.RegisterTools(s.server)
if sn, ok := sub.(SubsystemWithNotifier); ok {
sn.SetNotifier(s)
}
// Wire channel callback for subsystems that use func-based notification
type channelWirer interface {
OnChannel(func(ctx context.Context, channel string, data any))
}
if cw, ok := sub.(channelWirer); ok {
svc := s // capture for closure
cw.OnChannel(func(ctx context.Context, channel string, data any) {
svc.ChannelSend(ctx, channel, data)
})
}
}
return s, nil