mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 20:15:42 +00:00
306 lines
11 KiB
Go
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)
|
|
}),
|
|
)
|
|
}
|