Replace 5 separate tool registrars (messaging, channels, swarm, attachments, webhooks) with a single HybridToolRegistrar exposing 4 tools: my_status, send_message, search, and execute. New foundation packages: - internal/actions: action registry (22 actions) + BM25 search index - internal/jsruntime: lightweight call() expression parser with concurrency-limited execution pool The `execute` tool dispatches call() expressions through a ServiceBridge that maps action names to existing service methods, preserving all original handler logic. The `search` tool enables agents to discover available actions by keyword. The `send_message` tool merges DM and channel sending with mutual exclusion. All unit tests, integration tests, build, and vet pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
5.7 KiB
Go
208 lines
5.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
|
|
"github.com/synapbus/synapbus/internal/actions"
|
|
"github.com/synapbus/synapbus/internal/agents"
|
|
"github.com/synapbus/synapbus/internal/attachments"
|
|
"github.com/synapbus/synapbus/internal/channels"
|
|
"github.com/synapbus/synapbus/internal/console"
|
|
"github.com/synapbus/synapbus/internal/jsruntime"
|
|
"github.com/synapbus/synapbus/internal/messaging"
|
|
"github.com/synapbus/synapbus/internal/search"
|
|
"github.com/synapbus/synapbus/internal/trace"
|
|
)
|
|
|
|
// MCPServer wraps the mcp-go server with SynapBus services.
|
|
type MCPServer struct {
|
|
mcpServer *server.MCPServer
|
|
httpServer *server.StreamableHTTPServer
|
|
connMgr *ConnectionManager
|
|
agentService *agents.AgentService
|
|
logger *slog.Logger
|
|
console *console.Printer
|
|
}
|
|
|
|
// NewMCPServer creates and configures a new MCP server with 4 hybrid tools registered.
|
|
func NewMCPServer(
|
|
msgService *messaging.MessagingService,
|
|
agentService *agents.AgentService,
|
|
channelService *channels.Service,
|
|
swarmService *channels.SwarmService,
|
|
attachmentService *attachments.Service,
|
|
searchService *search.Service,
|
|
consolePrinter *console.Printer,
|
|
jsPool *jsruntime.Pool,
|
|
actionRegistry *actions.Registry,
|
|
actionIndex *actions.Index,
|
|
db *sql.DB,
|
|
) *MCPServer {
|
|
logger := slog.Default().With("component", "mcp-server")
|
|
connMgr := NewConnectionManager()
|
|
|
|
// Set up hooks for client info capture and connection tracking
|
|
hooks := &server.Hooks{}
|
|
hooks.AddAfterInitialize(func(ctx context.Context, id any, msg *mcplib.InitializeRequest, result *mcplib.InitializeResult) {
|
|
clientName := msg.Params.ClientInfo.Name
|
|
clientVersion := msg.Params.ClientInfo.Version
|
|
protocolVersion := msg.Params.ProtocolVersion
|
|
|
|
// Build capabilities list
|
|
var caps []string
|
|
if msg.Params.Capabilities.Roots != nil {
|
|
caps = append(caps, "roots")
|
|
}
|
|
if msg.Params.Capabilities.Sampling != nil {
|
|
caps = append(caps, "sampling")
|
|
}
|
|
if msg.Params.Capabilities.Elicitation != nil {
|
|
caps = append(caps, "elicitation")
|
|
}
|
|
if len(msg.Params.Capabilities.Experimental) > 0 {
|
|
for k := range msg.Params.Capabilities.Experimental {
|
|
caps = append(caps, "experimental/"+k)
|
|
}
|
|
}
|
|
|
|
// Extract agent name from context (set by HTTP auth middleware)
|
|
agentName, _ := extractAgentName(ctx)
|
|
|
|
// Get session ID for connection tracking
|
|
session := server.ClientSessionFromContext(ctx)
|
|
sessionID := ""
|
|
if session != nil {
|
|
sessionID = session.SessionID()
|
|
}
|
|
|
|
// Register connection
|
|
if sessionID != "" {
|
|
conn := &Connection{
|
|
ID: sessionID,
|
|
AgentName: agentName,
|
|
Transport: "streamable-http",
|
|
ConnectedAt: time.Now(),
|
|
LastActivity: time.Now(),
|
|
ClientName: clientName,
|
|
ClientVersion: clientVersion,
|
|
ProtocolVersion: protocolVersion,
|
|
ClientCapabilities: caps,
|
|
}
|
|
connMgr.Add(conn)
|
|
}
|
|
|
|
// Structured log (always)
|
|
logger.Info("client initialized",
|
|
"agent", agentName,
|
|
"client_name", clientName,
|
|
"client_version", clientVersion,
|
|
"protocol_version", protocolVersion,
|
|
"capabilities", caps,
|
|
"session_id", sessionID,
|
|
)
|
|
|
|
// Pretty console output
|
|
if consolePrinter != nil {
|
|
if agentName != "" {
|
|
consolePrinter.AgentConnected(agentName, clientName, clientVersion)
|
|
} else {
|
|
consolePrinter.ClientConnected(clientName, clientVersion)
|
|
}
|
|
}
|
|
})
|
|
|
|
hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) {
|
|
sessionID := session.SessionID()
|
|
conn, ok := connMgr.Get(sessionID)
|
|
if ok {
|
|
if consolePrinter != nil && conn.AgentName != "" {
|
|
consolePrinter.AgentDisconnected(conn.AgentName)
|
|
}
|
|
logger.Info("client disconnected",
|
|
"agent", conn.AgentName,
|
|
"client_name", conn.ClientName,
|
|
"session_id", sessionID,
|
|
"duration", fmt.Sprintf("%s", time.Since(conn.ConnectedAt).Truncate(time.Second)),
|
|
)
|
|
connMgr.Remove(sessionID)
|
|
}
|
|
})
|
|
|
|
// Create the mcp-go server
|
|
mcpSrv := server.NewMCPServer(
|
|
"SynapBus",
|
|
"0.1.0",
|
|
server.WithToolCapabilities(true),
|
|
server.WithHooks(hooks),
|
|
)
|
|
|
|
// Register the 4 hybrid tools
|
|
hybridRegistrar := NewHybridToolRegistrar(
|
|
msgService,
|
|
agentService,
|
|
channelService,
|
|
swarmService,
|
|
attachmentService,
|
|
searchService,
|
|
jsPool,
|
|
actionRegistry,
|
|
actionIndex,
|
|
db,
|
|
)
|
|
hybridRegistrar.RegisterAllOnServer(mcpSrv)
|
|
|
|
// Create Streamable HTTP transport with context func for auth propagation
|
|
httpServer := server.NewStreamableHTTPServer(mcpSrv,
|
|
server.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
|
|
// Propagate agent identity from HTTP auth to MCP context
|
|
if agent, ok := agents.AgentFromContext(r.Context()); ok {
|
|
ctx = ContextWithAgentName(ctx, agent.Name)
|
|
// Propagate owner ID for trace recording
|
|
if ownerID, ok := trace.OwnerIDFromContext(r.Context()); ok {
|
|
ctx = trace.ContextWithOwnerID(ctx, ownerID)
|
|
}
|
|
}
|
|
return ctx
|
|
}),
|
|
)
|
|
|
|
s := &MCPServer{
|
|
mcpServer: mcpSrv,
|
|
httpServer: httpServer,
|
|
connMgr: connMgr,
|
|
agentService: agentService,
|
|
logger: logger,
|
|
console: consolePrinter,
|
|
}
|
|
|
|
logger.Info("MCP server initialized (4 hybrid tools, streamable HTTP transport)")
|
|
return s
|
|
}
|
|
|
|
// Handler returns the HTTP handler for mounting on a router.
|
|
func (s *MCPServer) Handler() http.Handler {
|
|
return s.httpServer
|
|
}
|
|
|
|
// ConnectionManager returns the connection manager.
|
|
func (s *MCPServer) ConnectionManager() *ConnectionManager {
|
|
return s.connMgr
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the MCP server.
|
|
func (s *MCPServer) Shutdown(ctx context.Context) error {
|
|
s.logger.Info("MCP server shutting down")
|
|
if err := s.httpServer.Shutdown(ctx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|