(feat) MCP Server (#1365)

* feat(hooks/observe): add structured output validation hook (ObserveHook)

- hooks/types.go: Add ObserveHook interface + BuiltInHookType enum
- hooks/dispatcher.go: HookDispatcher emits ObserveHook with PhaseResult payload
- pipeline/observe_stage.go: ObserveStage emits hook after ObserveResult built
- pipeline/substates.go: ObserveStageResult carries hook results + validation errors
- hooks/config.go: BuiltInHookTypeObserve added to BuiltInHookType enum

PhaseResult carries structured output, token usage, tool calls + validation errors
emitted post-ObserveStage. ObserveHook implementations can validate structured
output against schemas, detect tool-call loops, enforce token budgets, etc.

Hook fires after ObserveStage produces ObserveResult, before results propagate
to next stage. ValidationError returned by hook halts pipeline and propagates
error to caller without further stage execution.

Co-Authored-By: Claude <noreply@anthropic.com>

* ui(hooks): add post_model_response event to web UI

- Add event to Zod schema, filter dropdown, and form dialog
- Implement conditional test panel UI for model response payload
- Add translations (en/zh/vi) for new test panel fields
- Updated beta description to reference the new event

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(mcp): add MCP CRUD server exposing goclaw resources at /api/mcp/ with Bearer token auth
and X-GoClaw-Tenant-Id header, default to master tenant

* feat(mcp): add goclaw_skills_write_file tool to edit skill files on disk

The CRUD MCP server's goclaw_skills_update only touched skill DB metadata,
with no way to edit a skill's SKILL.md/file content on the filesystem. Extract
the versioned write logic from the web UI's skill file editor
(SkillsHandler.handleWriteFile) into skills.WriteVersionedFile so both
surfaces share identical validation and versioning, and expose it as a new
MCP tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-15 09:56:43 +07:00
1 parent 4f558e2c3b
commit 293aa2a5e1
75 files changed
+8461 -166

No files matched your search

+40
View File
@@ -491,6 +491,7 @@ func runGateway() {
server.SetToolPolicy(toolPE)
server.SetPairingService(pgStores.Pairing)
server.SetMessageBus(msgBus)
server.SetExecApprovalManager(execApprovalMgr)
server.SetOAuthHandler(httpapi.NewOAuthHandler(pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus))
// contextFileInterceptor is created inside wireExtras.
@@ -502,6 +503,42 @@ func runGateway() {
if pgStores.Agents != nil {
server.SetAgentStore(pgStores.Agents)
}
// Wire the skill/cron stores used by the CRUD MCP server (see
// internal/mcp/crud_server.go, mounted at /api/mcp/ in BuildMux()).
if pgStores.Skills != nil {
server.SetSkillStore(pgStores.Skills)
}
if pgStores.Cron != nil {
server.SetCronStore(pgStores.Cron)
}
if pgStores.AgentLinks != nil {
server.SetAgentLinkStore(pgStores.AgentLinks)
}
if pgStores.ConfigPermissions != nil {
server.SetConfigPermissionStore(pgStores.ConfigPermissions)
}
if pgStores.BitrixPortals != nil {
server.SetBitrixPortalStore(pgStores.BitrixPortals)
}
if pgStores.RunTimeline != nil {
server.SetRunTimelineStore(pgStores.RunTimeline)
}
if pgStores.Teams != nil {
server.SetTeamStore(pgStores.Teams)
}
if pgStores.ChannelInstances != nil {
server.SetChannelInstanceStore(pgStores.ChannelInstances)
}
if pgStores.Heartbeats != nil {
server.SetHeartbeatStore(pgStores.Heartbeats)
}
if pgStores.Providers != nil {
server.SetProviderStore(pgStores.Providers)
}
if pgStores.Tenants != nil {
server.SetTenantStore(pgStores.Tenants)
}
server.SetSQLDB(pgStores.DB)
// Build OAuth token refresher before wireExtras so the resolver can inject tokens.
var mcpOAuthRefresher mcpbridge.OAuthTokenProvider
@@ -654,6 +691,7 @@ func runGateway() {
hm.SetTestRunner(methods.NewDispatcherTestRunner(sharedHookHandlers))
}
hm.Register(server.Router())
server.SetHookStore(hs)
slog.Info("registered hooks RPC methods")
}
@@ -696,6 +734,7 @@ func runGateway() {
channelMgr := channels.NewManager(msgBus)
channelMgr.SetSystemMessages(systemmessages.NewResolver(cfg))
deps.channelMgr = channelMgr
server.SetChannelManager(channelMgr)
// Wire channel member resolver into permission grant paths (WS + HTTP) so
// file_writer grants coming from the Web UI auto-enrich their metadata.
@@ -939,6 +978,7 @@ func runGateway() {
// Register quota usage RPC.
methods.NewQuotaMethods(quotaChecker, pgStores.DB).Register(server.Router())
server.SetQuotaChecker(quotaChecker)
// API key management RPC
if pgStores.APIKeys != nil {
+3
View File
@@ -346,6 +346,9 @@ func (d *gatewayDeps) wireHTTPHandlersOnServer(
// Wire WS method — provider nil means each request resolves key via secretStore at HTTP layer.
// For WS, use same cache. Provider is resolved via secretStore at WS level in a future phase.
methods.NewVoicesMethods(voiceCache, nil).Register(d.server.Router())
// Wire the same cache + secret store into the CRUD MCP server (see
// internal/mcp/crud_server.go, mounted at /api/mcp/ in BuildMux()).
d.server.SetVoiceCache(voiceCache, secretStore)
}
// TTS synthesize endpoint — shares audio.Manager with setupTTS.
+3
View File
@@ -69,6 +69,9 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore
// Phase 2: Usage (queries SessionStore for real token data)
methods.NewUsageMethods(sessStore, tracingStore).Register(router)
methods.NewLLMMethods(providerReg, cfg.Gateway.BackgroundProvider, cfg.Gateway.BackgroundModel).Register(router)
// Wire the same provider registry into the CRUD MCP server (see
// internal/mcp/crud_server.go, mounted at /api/mcp/ in BuildMux()).
server.SetLLMProviders(providerReg, cfg.Gateway.BackgroundProvider, cfg.Gateway.BackgroundModel)
// Phase 2: Exec approval (always registered — returns empty when manager is nil)
methods.NewExecApprovalMethods(execApprovalMgr, msgBus).Register(router)