Files
bilogic c09a72a3bf notify channel of pairing approval (#1508)
* notify channel of pairing approval

* purge cache when revoked
2026-08-07 14:35:59 +07:00

306 lines
11 KiB
Go

// Package mcp exposes goclaw's Model Context Protocol bridge/server surface.
// crud_server.go implements a second, distinct MCP server (separate from the
// tool bridge in bridge_server.go) that exposes goclaw's CRUD-style resource
// management surface — agents, sessions, skills, cron, config, agent links,
// API keys, config permissions, Bitrix24 portals, run timelines, teams,
// teams tasks, teams workspace, channels, channel instances, hooks,
// heartbeat, pairing, exec approval, usage, quota, chat/chat-behavior, LLM
// completion, runtime logs, outbound send, and TTS voices — as MCP tools
// backed directly by the real store/subsystem implementations used by the
// gateway's own WebSocket RPC methods.
//
// Tenant scope: the CRUD MCP server is gated by a single shared bearer
// secret (gateway.mcp_server_token) with no per-caller identity, so it is
// treated like the gateway-token/owner path in internal/http/auth.go —
// callers may optionally scope a request to a tenant via the
// "X-GoClaw-Tenant-Id" header (UUID or slug), with no membership check
// (the token itself is the full-trust boundary), falling back to
// store.MasterTenantID when the header is absent or unresolvable. This is
// applied once per request via mcpserver.WithHTTPContextFunc in
// NewCRUDServer, so every tool handler in this package can rely on
// store.TenantIDFromContext(ctx) already carrying a concrete value.
package mcp
import (
"context"
"database/sql"
"log/slog"
"net/http"
mcpserver "github.com/mark3labs/mcp-go/server"
"github.com/nextlevelbuilder/goclaw/internal/audio"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/channels"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/hooks"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/tools"
)
// CRUDDeps bundles the store dependencies the CRUD MCP server needs.
// All fields are optional — tool groups whose backing store is nil are
// simply not registered, so this server degrades gracefully across editions
// (e.g. SQLite/lite builds that omit certain stores).
type CRUDDeps struct {
Agents store.AgentStore
AgentRuntime AgentRuntimeLookup // enables goclaw_agent_{get,wait}; agent identity/files work without it
Sessions store.SessionStore
Skills store.SkillStore
Cron store.CronStore
Config *config.Config
AgentLinks store.AgentLinkStore
APIKeys store.APIKeyStore
ConfigPermissions store.ConfigPermissionStore
Bitrix store.BitrixPortalStore
RunTimeline store.RunTimelineStore
// Phase 2: subsystem-backed tool families.
Teams store.TeamStore
ChannelInstances store.ChannelInstanceStore
ChannelManager *channels.Manager
Hooks hooks.HookStore
Heartbeats store.HeartbeatStore
Providers store.ProviderStore
Pairing store.PairingStore
ExecApproval *tools.ExecApprovalManager
Quota *channels.QuotaChecker
DB *sql.DB // for quota.usage today's trace summary
// Phase 3: live-runtime-backed tool families (chat, LLM, logs, send, voices).
ChatRunner ChatRunner // enables goclaw_chat_{send,abort,session_status}
LLMProviders *providers.Registry // enables goclaw_llm_complete
LLMDefaults LLMDefaults // background provider/model fallback for goclaw_llm_complete
MessageBus *bus.MessageBus // enables goclaw_send
RuntimeLogs RuntimeLogSnapshotter // enables goclaw_logs_tail
VoiceCache *audio.VoiceCache // enables goclaw_voices_{list,refresh}
VoiceSecretsStore store.ConfigSecretsStore // per-tenant TTS provider API key lookup for goclaw_voices_*
// Tenants resolves the optional "X-GoClaw-Tenant-Id" request header (UUID
// or slug) to a concrete tenant for every CRUD MCP call — see
// resolveMCPTenantID. Every tenant-scoped tool handler in this package
// relies on store.TenantIDFromContext(ctx) already carrying a resolved
// value by the time it runs. When nil, all requests are treated as
// store.MasterTenantID (same fail-safe default resolveMCPTenantID uses
// when the header is absent/unresolvable).
Tenants store.TenantStore
// Phase 4: read-heavy/admin surfaces (memory, knowledge graph, tracing,
// tenants, providers) closing the CLI-vs-MCP coverage gap.
Memory store.MemoryStore
KnowledgeGraph store.KnowledgeGraphStore
Tracing store.TracingStore
Contacts store.ContactStore
PendingMessages store.PendingMessageStore
Activity store.ActivityStore
SystemConfigs store.SystemConfigStore
SecureCLI store.SecureCLIStore
}
// NewCRUDServer builds a StreamableHTTPServer exposing goclaw's CRUD
// resources (agents, sessions, skills, cron, config, agent links, API keys,
// config permissions, Bitrix24 portals, run timelines) as MCP tools. Callers
// are expected to gate access to the returned handler with a bearer-token
// middleware (see gateway.tokenAuthMiddleware / Server.BuildMux) before
// mounting it — this server performs no authentication of its own.
func NewCRUDServer(deps CRUDDeps, version string) *mcpserver.StreamableHTTPServer {
srv := mcpserver.NewMCPServer("goclaw-crud", version,
mcpserver.WithToolCapabilities(false),
)
var registered int
if deps.Agents != nil {
registerAgentCRUDTools(srv, deps.Agents)
registered += 5
registerAgentSkillPinCRUDTools(srv, deps.Agents)
registered += 2
}
if deps.Agents != nil && deps.AgentRuntime != nil {
registerAgentRuntimeCRUDTools(srv, deps.Agents, deps.AgentRuntime)
registered += 5
}
if deps.Sessions != nil {
registerSessionCRUDTools(srv, deps.Sessions)
registered += 7
}
if deps.Skills != nil {
registerSkillCRUDTools(srv, deps.Skills)
registered += 2
if manage, ok := deps.Skills.(store.SkillManageStore); ok {
registerSkillUpdateCRUDTool(srv, deps.Skills, manage)
registered++
registerSkillGrantCRUDTools(srv, deps.Skills, manage)
registered += 2
if deps.Config != nil {
registerSkillWriteFileCRUDTool(srv, deps.Skills, manage, deps.Config)
registered++
registerSkillCreateCRUDTool(srv, manage, deps.Config)
registered++
}
}
}
if deps.Cron != nil {
registerCronCRUDTools(srv, deps.Cron)
registered += 8
}
if deps.Config != nil {
registerConfigCRUDTools(srv, deps.Config)
registered++
}
if deps.AgentLinks != nil {
registerAgentLinkCRUDTools(srv, deps.AgentLinks)
registered += 4
}
if deps.APIKeys != nil {
registerAPIKeyCRUDTools(srv, deps.APIKeys)
registered += 3
}
if deps.ConfigPermissions != nil {
registerConfigPermissionCRUDTools(srv, deps.ConfigPermissions)
registered += 4
}
if deps.Bitrix != nil {
registerBitrixCRUDTools(srv, deps.Bitrix)
registered += 3
}
if deps.RunTimeline != nil {
registerRunTimelineCRUDTools(srv, deps.RunTimeline)
registered++
}
if deps.Teams != nil {
registerTeamsCRUDTools(srv, deps.Teams, deps.Agents)
registered += 10
registerTeamsTasksCRUDTools(srv, deps.Teams, deps.Agents)
registered += 13
if deps.Config != nil {
registerTeamsWorkspaceCRUDTools(srv, deps.Teams, deps.Config)
registered += 3
}
}
if deps.ChannelManager != nil {
registerChannelsCRUDTools(srv, deps.ChannelManager)
registered += 3
}
if deps.ChannelInstances != nil {
registerChannelInstancesCRUDTools(srv, deps.ChannelInstances, deps.Agents)
registered += 5
}
if deps.Hooks != nil {
registerHooksCRUDTools(srv, deps.Hooks)
registered += 7
}
if deps.Heartbeats != nil {
registerHeartbeatCRUDTools(srv, deps.Heartbeats, deps.Agents, deps.Providers)
registered += 8
}
if deps.Pairing != nil {
registerPairingCRUDTools(srv, pairingCRUDDeps{
pairing: deps.Pairing,
channelMgr: deps.ChannelManager,
msgBus: deps.MessageBus,
cfg: deps.Config,
})
registered += 6
}
if deps.ExecApproval != nil {
registerExecApprovalCRUDTools(srv, deps.ExecApproval)
registered += 3
}
if deps.Sessions != nil {
registerUsageCRUDTools(srv, deps.Sessions)
registered += 2
}
registerQuotaCRUDTools(srv, deps.Quota, deps.DB)
registered++
// Phase 3: live-runtime-backed tool families.
if deps.ChatRunner != nil && deps.Sessions != nil {
registerChatCRUDTools(srv, deps.ChatRunner, deps.Sessions)
registered += 5
}
if deps.Config != nil {
registerChatBehaviorCRUDTool(srv, deps.Config, deps.ChannelManager)
registered++
}
if deps.LLMProviders != nil {
registerLLMCRUDTool(srv, deps.LLMProviders, deps.LLMDefaults)
registered++
}
if deps.RuntimeLogs != nil {
registerLogsCRUDTool(srv, deps.RuntimeLogs)
registered++
}
if deps.MessageBus != nil {
registerSendCRUDTool(srv, deps.MessageBus)
registered++
}
if deps.VoiceCache != nil {
registerVoicesCRUDTools(srv, deps.VoiceCache, deps.VoiceSecretsStore)
registered += 2
}
// Phase 4: read-heavy/admin surfaces closing CLI-vs-MCP coverage gaps.
if deps.Memory != nil {
registerMemoryCRUDTools(srv, deps.Memory)
registered += 5
}
if deps.KnowledgeGraph != nil {
registerKnowledgeGraphCRUDTools(srv, deps.KnowledgeGraph)
registered += 14
}
if deps.Tenants != nil {
registerTenantsCRUDTools(srv, deps.Tenants)
registered += 6
}
if deps.Providers != nil {
registerProvidersCRUDTools(srv, deps.Providers)
registered += 5
}
if deps.Tracing != nil {
registerTracesCRUDTools(srv, deps.Tracing)
registered += 2
}
if deps.Contacts != nil {
registerContactsCRUDTools(srv, deps.Contacts)
registered += 4
}
if deps.PendingMessages != nil {
registerPendingMessagesCRUDTools(srv, deps.PendingMessages)
registered += 3
}
if deps.Activity != nil {
registerActivityCRUDTools(srv, deps.Activity)
registered++
}
if deps.SystemConfigs != nil {
registerSystemConfigCRUDTools(srv, deps.SystemConfigs)
registered += 4
}
if deps.Config != nil {
registerStorageCRUDTools(srv, deps.Config)
registered += 4
}
if deps.Agents != nil {
registerAgentExportCRUDTools(srv, deps.Agents)
registered += 2
}
if deps.SecureCLI != nil {
registerSecureCLICRUDTools(srv, deps.SecureCLI)
registered += 5
}
registerHealthCRUDTool(srv, deps.DB, version)
registered++
slog.Info("mcp.crud: tools registered", "count", registered)
tenants := deps.Tenants
return mcpserver.NewStreamableHTTPServer(srv,
mcpserver.WithStateLess(true),
mcpserver.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
tenantID := resolveMCPTenantID(ctx, tenants, r.Header.Get(mcpTenantHeader))
return store.WithTenantID(ctx, tenantID)
}),
)
}