Files
synapbus/internal/mcp/server.go
T
Algis DumbrisandClaude Opus 4.6 bd1bccc692 feat(015): SQL query interface for agents + split read/write pools
Split Connection Pools:
- writeDB: MaxOpenConns=1, serializes all writes (no SQLITE_BUSY)
- readDB: MaxOpenConns=8, query_only=ON, for all SELECTs
- QueryDB() helper returns read pool when available

SQL Query Interface:
- New 'query' action via execute MCP tool
- Read-only enforcement (PRAGMA query_only=ON + SQL validation)
- Curated views: my_messages, my_channels, channel_messages
- Per-agent access control via CTE injection
- Auto LIMIT 100, 5s timeout, SELECT-only validation
- Blocks: INSERT, UPDATE, DELETE, DROP, PRAGMA, etc.
- 12 new tests (access control, validation, limits, CTEs)

Migration 016: agent query views (v_agent_messages, etc.)
Action registry: 30 actions (was 29, added 'query')
All 29 test packages pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:17:23 +02:00

230 lines
6.6 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/agentquery"
"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/reactions"
"github.com/synapbus/synapbus/internal/search"
"github.com/synapbus/synapbus/internal/trace"
"github.com/synapbus/synapbus/internal/trust"
)
// MCPServer wraps the mcp-go server with SynapBus services.
type MCPServer struct {
mcpServer *server.MCPServer
httpServer *server.StreamableHTTPServer
connMgr *ConnectionManager
agentService *agents.AgentService
hybridRegistrar *HybridToolRegistrar
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,
reactionService *reactions.Service,
trustService *trust.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.WithPromptCapabilities(true),
server.WithHooks(hooks),
)
// Register the 4 hybrid tools
hybridRegistrar := NewHybridToolRegistrar(
msgService,
agentService,
channelService,
swarmService,
attachmentService,
searchService,
reactionService,
trustService,
jsPool,
actionRegistry,
actionIndex,
db,
)
hybridRegistrar.RegisterAllOnServer(mcpSrv)
// Register the 4 MCP prompts
traceStore := trace.NewSQLiteTraceStore(db)
promptRegistrar := NewPromptRegistrar(db, agentService, channelService, traceStore)
promptRegistrar.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,
hybridRegistrar: hybridRegistrar,
logger: logger,
console: consolePrinter,
}
logger.Info("MCP server initialized (4 hybrid tools, 4 prompts, streamable HTTP transport)")
return s
}
// SetQueryExecutor sets the SQL query executor for agent queries via the execute tool.
func (s *MCPServer) SetQueryExecutor(exec *agentquery.Executor) {
if s.hybridRegistrar != nil {
s.hybridRegistrar.SetQueryExecutor(exec)
}
}
// 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
}