Skip to content

Commit 0796cf7

Browse files
nickmisasiclaudemattermost-build
authored
MM-65671: Agents CRUD, legacy bot migration, and bot account sync (#589)
* Add prescriptive Phase 1 plan: Database & Store Layer Detailed implementation plan for Agents_UserAgents table, Morph migration, CRUD store methods, and integration tests. Includes exact file paths, line numbers, code snippets, and verification commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add UserAgent model, store CRUD, migration, and AgentStore interface Phase 1 of self-service agent creation: Agents_UserAgents table with Morph migration 000004, full CRUD store methods with JSON slice field marshaling, AgentStore interface in api package, and comprehensive integration tests (12 test functions covering round-trips, soft delete, edge cases, and concurrency). Also fixes setupTestStore to set search_path via connection string so concurrent test goroutines use the correct schema, and updates migration count assertion from 3 to 4. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add agent CRUD API endpoints, bot lifecycle, and license gating Implements Phase 2 of self-service agent creation: - Agent CRUD handlers (create, list, get, update, delete) with license gating - Bot account lifecycle (create on agent create, deactivate on delete) - Avatar upload endpoint for agent profile images - Services listing endpoint (secrets stripped) - Permission-based access control (create_agent permission, creator/admin checks) - Partial update support via pointer fields in UpdateAgentRequest - Comprehensive test coverage (9 test functions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add runtime integration for DB-backed agents with HA cluster support Implements Phase 3 of self-service agent creation: - AgentStore interface in bots package for loading user agents from DB - userAgentToBotConfig converter maps UserAgent to llm.BotConfig - EnsureBots merges DB-backed agents into the live bot registry - forceRefresh flag bypasses optimistic config-equality cache for agent CRUD - clusterEventAgentUpdate event propagates agent changes to HA nodes - ClusterAgentNotifier interface + refreshBotsAndNotify helper in API - Agent create/update/delete handlers trigger registry refresh + cluster notify - Runtime tests: converter, registry lookup, force-refresh flag - Nil config guard in EnsureBots for test safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Phase 4: Add agent listing page, API client, and product registration - Add TypeScript types for UserAgent, CreateAgentRequest, UpdateAgentRequest, ServiceInfo - Add agent CRUD API client functions (getAgents, createAgent, updateAgent, deleteAgent, uploadAgentAvatar, getServices) - Add Redux agents reducer for state caching - Create AgentsPage root component with URL-based visibility at /plug/mattermost-ai/agents - Create AgentRow component with avatar, name, tool badge, and actions menu - Create AgentsList component with All/Your agents tabs, loading/error/empty states, and delete flow - Create DeleteAgentDialog confirmation modal - Create AgentsLicenseGate for E20+ license enforcement - Register root component and main menu navigation entry point Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix getServices() URL and align EnabledTool TS type with Go struct 1. getServices() was calling /agents/services but the backend route is /services on the base router — the gin router would match /agents/services against /:agentid with agentid="services" → 404. 2. EnabledTool TS type had mismatched fields (type/id/name/serverName) vs Go struct (server_origin/tool_name). Aligned to match backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add agent config modal with Configuration, Access, and MCPs tabs Phase 5 implementation: three-tab modal for creating/editing agents. - Config tab: display name, username, avatar, service selection, custom instructions - Access tab: channel access, user access, agent admin controls - MCPs tab: per-server and per-tool toggle switches with search - Wire modal into agents_list.tsx for create and edit flows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix UserAgent TS type to use snake_case matching Go JSON tags The Go UserAgent struct uses snake_case JSON tags (bot_user_id, creator_id, display_name, etc.) but the TS type used camelCase, causing JSON deserialization to silently drop all fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add per-agent MCP tool filtering and access control wiring Wire UserAgent.EnabledTools into the tool discovery pipeline so DB-backed agents only expose their selected MCP tools. Adds EnabledMCPTool type to BotConfig, convertEnabledTools mapping, RetainOnlyMCPTools filter on ToolStore, and the wiring call in getToolsStoreForUser. Nil = all tools allowed (backward compat), empty = no MCP tools, populated = allowlist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add E2E test infrastructure and specs for agent CRUD, access control, and MCP tools Phase 7: Creates agent container factory, API helper, page object, and three test spec files covering create/edit/delete flows, user access level enforcement, and MCP tool selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add search/filter, form validation, and edge case handling (Phase 8) - Search input on listing page filters by display name and username - Field-level validation errors in config modal with clear-on-edit - Server-side 409 (username taken) mapped to inline field error - Service deleted edge case: warning badge in agent row, fallback option in service dropdown - MCP server removed edge case: orphaned tools warning banner with auto-cleanup on tab load Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix ServiceID column VARCHAR(26) too small for UUIDs ServiceID stores UUIDs (36 chars) but was defined as VARCHAR(26), causing POST /agents to fail with a Postgres truncation error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix agents page navigation — add team prefix to route navigateToAgentsPage() was pushing /plug/mattermost-ai/agents without the team prefix, causing Mattermost's router to treat it as an unknown team and redirect to "Team Not Found". Now extracts the current team name from the URL path and builds the full team-scoped route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move Agents entry from team menu to product switcher Use registerProduct (internal MM API) instead of registerMainMenuAction so "Agents" appears in the product switcher grid icon rather than the team dropdown menu. Simplify AgentsPage to a normal component — routing is handled by registerProduct, so no URL-matching overlay is needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert registerProduct, restore overlay approach with team-prefix fix Reverts the registerProduct change (internal API not reliable) and restores the original rootComponent overlay + registerMainMenuAction approach. Fixes two issues in the original code: 1. navigateToAgentsPage() now extracts the current team name from the URL and navigates to /${teamName}/plug/mattermost-ai/agents 2. AgentsPage visibility check uses includes() instead of endsWith() to match team-prefixed URLs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert "Revert registerProduct, restore overlay approach with team-prefix fix" This reverts commit 7f7ba76. * Validate username format and return proper status codes for agent creation errors Add pre-validation for username format (must match ^[a-z][a-z0-9._-]*$) returning 400 for invalid usernames. Detect duplicate username errors from bot creation and return 409 Conflict instead of 500. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix global header losing dark theme on Agents product page Add app__body and channel-view CSS classes on mount in AgentsPage, matching what Playbooks and Boards do in their product components. ChannelController normally sets these classes but isn't loaded in product views, causing the header to render with a white background. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix MCP tool filtering bypass for agents with no tools enabled SetEnabledToolsFromJSON was treating "[]" the same as "" (empty string), mapping both to nil. This destroyed the nil-vs-empty distinction: - nil EnabledTools = all tools allowed (config-defined bots) - [] EnabledTools = no tools allowed (user disabled all toggles) When an agent was saved with no MCP tools enabled, the DB stored "[]" but on read it was converted to nil, bypassing the RetainOnlyMCPTools filter in llm_context.go and allowing all tools at runtime. Fix: only map "" (empty DB column from pre-feature agents) to nil. Let json.Unmarshal handle "[]" → empty slice and "null" → nil correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Filter RHS tools dropdown to show only agent's enabled MCP tools The Tools popover in the RHS chat header was showing ALL available MCP tool providers regardless of the selected agent's configured tools. Changes: - Backend: Add EnabledMCPTools to AIBotInfo so /ai_bots exposes each bot's allowed MCP tools to the frontend - Frontend: Add enabledMCPTools to LLMBot interface, pass it to ToolProviderPopover, and filter the server list so only servers with at least one enabled tool are shown - Config-defined bots (null enabledMCPTools) continue to show all tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * E2E: expand mocked agents coverage, CI shard registration, RHS MCP harness - Register tests/agents/*.spec.ts in e2e-shard-3; validate ci-test-groups - crud: duplicate username text, search no-results, regular user listing, unprivileged 403 UI - access-control: allowlist negative, UserAccessLevel None listing, delegated admin edit - mcp-tools: RHS tool provider cases; longer container beforeAll timeouts - api: RHS MCP filtering + tests; ai-plugin helpers for Tools menu Made-with: Cursor * E2E: stronger agent access + MCP assertions (API posts, Smocker body rules) - MattermostPage: assert DM outcomes via bot posts (Client4) instead of thread reply UI - access-control: use expectNoBotDmReplyFromApi / expectBotDmReplyFromApi - openai-mock: buildChatCompletionMockRule for ordered body matchers - mcp-tools: layered Smocker rules to tie responses to read_post in completion payload Made-with: Cursor * E2E: hold full negative DM window for no-expected-bot-reply (45s) - expectNoBotDmReplyFromApi: poll until observeDurationMs elapses (default 45s, matches positive reply timeout) - Remove early return after minObserve; extend block/allowlist-negative tests to 120s Made-with: Cursor * E2E: tighten MCP enabled tool assertions Ensure the positive enabled-tools flow only passes after a mocked tool-call round trip, so the suite proves runtime MCP execution instead of just mocked response text. Made-with: Cursor * MM-65671: Self-service agents parity, system console cleanup, legacy migration - Persist model, vision, tools, native tools, reasoning, structured output on user agents (DB, API, runtime bot mapping). - Expose ServiceInfo fields and POST /agents/models/fetch for server-side model listing. - Agents modal Configuration tab: full parity with legacy bot form; MCPs tab disabled when tools off. - Structured output vs extended thinking: mutual exclusion, restore reasoning when structured off, inline note. - Modal scroll fixes (min-height) for long config forms; service selection hint for advanced options. - System Console: replace AI Bots editor with moved notice + link; default bot from getAIBots(). - Idempotent startup migration from config.bots to DB agents; sysadmin can manage migrated agents (empty creator). - Tests: API/store, E2E crud and system console; skip legacy bot UI specs pending agents coverage. - Export NativeToolsItem from bot.tsx for reuse. Made-with: Cursor * MM-65671: restore migrated agent coverage and config refresh Add agent-builder coverage for the provider-specific settings that moved out of the system console, and rerun legacy config-bot migration after later config updates so seeded bots still appear in the UI. Made-with: Cursor * MM-65671: address code review findings and cleanup Apply fixes from CodeRabbit review: add username validation on agent update, guard against nil config in service validation, bound avatar upload size (10MB / 413), align SetEnabledNativeToolsFromJSON empty-slice semantics, remove legacy bot-reasoning-config spec (coverage moved to provider-config), add optional chaining in e2e afterAll teardown, and improve webapp accessibility (avatar alt text, dialog ARIA attributes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * MM-65671: Agents CRUD, legacy bot migration, and bot account sync - Extend user agent model, API, store, and bots integration - Migrate legacy config bots to user agents on plugin enable - Sync Mattermost bot display name and deactivate on agent update/delete - Update agents UI, system console config, and i18n strings - Add permission helper and e2e agent CRUD coverage - Fix golangci errcheck and shadow issues in agent handlers and migration Made-with: Cursor * Stop tracking .planning; ignore planning directory Remove .planning/phase-1/PLAN.md from version control (keep local copy). .gitignore: use .planning pattern for the planning directory. Made-with: Cursor * MM-65671: Address PR review (API auth, bots, UI, i18n) - Gate service list and model fetch with canConfigureAgentServices (own or manage-others agents) - Reject agent username changes after create; validate service_id on update; log rollback bot deactivation failures - EnsureBots fails closed when ListAgents errors; defer legacy bot migration when bots are missing - Partial unique index on BotUserID for active agents; clone mock store agents in API tests - Webapp: services error handling, delete in-flight guard, dialog focus, stale model fetch, MCP a11y, system console bots - E2E: neutral agents page ready check; resilient access-control teardown - i18n extract for new FormattedMessage strings Made-with: Cursor * MM-65671: Address follow-up CodeRabbit (MCP auth filter, HA notify, e2e locators) Made-with: Cursor * e2e: scope agent delete action to row menu Use row-scoped locator for Delete in agent actions (match Edit pattern) and pass display name from crud test. Made-with: Cursor * e2e: scope delete confirm button to delete-agent dialog Use role dialog + Delete agent name; chain Delete button from dialog locator. Made-with: Cursor * MM-65671: Fix E2E for migrated agents and product route - BotConfigHelper: resolve bots via GET /agents when legacy config bots are empty after migration; update migrated agents via PUT /agents/:id. - Grant manage_own_agent and manage_others_agent via mmctl in plugincontainer (and manage_others_agent in agent-container) so admin and tests can call agent APIs after legacy migration. - Use a leading slash on AGENTS_ROUTE for registerProduct routing. Made-with: Cursor * MM-65671: Self-service agents E2E and agent builder draft sync - Allow system admins to configure services, create agents, and manage migrated legacy bots (empty CreatorID) via API. - E2E: centralize mmctl agent permissions; grant after system-console admin setup; live-service spec grants permissions for default admin. - Agents list: show actions for migrated agents when user has manage_system. - System console bot: data-testid on native tool checkboxes for E2E. - Agent builder config tab: skip false service-change resets when the modal hydrates from an empty serviceId; when switching services of the same LLM type, preserve enabledNativeTools and reasoning fields (clear them only when the provider type changes). Made-with: Cursor * MM-65671: Gate Create agent on canCreate; fix permission E2E - Show Create agent only when the user has manage_own_agent or manage_system, matching POST /agents authorization (manage_others alone cannot create). - Add mmctl helper to revoke manage_own_agent from system_user for tests. - Rewrite denied-create E2E: users without service list access cannot fill the modal; assert the Create button is hidden after revoking the role permission. Made-with: Cursor * e2e: stabilize smart-reactions wait on plugin /react response - Use Promise.all to register waitForResponse with the menu click. - Match completed POST responses to the plugin react URL without requiring response.ok() so Chromium still resolves when the handler returns a non-2xx status. - Bump test timeout and reaction visibility wait for slower CI. Made-with: Cursor * fix: eslint lines-around-comment and operator-linebreak in agents UI Made-with: Cursor * e2e: retry Smart Reactions basic tests once; extend reaction wait CI sometimes completes /react before the reaction UI appears; align with flake policy and give the reactions container up to 60s. Made-with: Cursor * e2e: fix smart reactions mock and default plugin LLM routing - Set useResponsesAPI false and enabledNativeTools [] on default mock services/bots so react flow uses chat completions (Smocker mocks) like other E2E containers. - Use buildTextResponse for thumbsup SSE to match proven streaming shape. Made-with: Cursor * fix: prefer configured default bot for post actions Use the configured default bot when post action endpoints are called without a bot username so migrated agent ordering does not route requests to the wrong service. Add a regression test that proves we honor config over in-memory bot slice order. Made-with: Cursor * e2e: give follow-ups login more headroom Allow the first action-item extraction test more time to reach the channel view on slower CI containers so shard-1 does not fail before the scenario starts. Made-with: Cursor * Adjust product row style * Address crspeller review: agent API dedupe, EnsureBots snapshot, MCP tool types, shared confirm dialog Made-with: Cursor * Port PR 617 MCP and OpenAI defaults. Keep the self-serve agent flow aligned with the merged master changes by making MCP always available, preserving OpenAI responses defaults across the UI and API, and covering the updated behavior in tests. Made-with: Cursor * Add refetch for agents list on creation, fix defaults for structured json conflicting with extended reasoning * Fix MCP tool tests for native tool defaults. Keep the new default web search behavior intact by making the MCP-focused e2e agents explicitly disable native tools, and align bifrost expectations with the current empty-slice filtering behavior. Made-with: Cursor * Address remaining PR feedback. Deduplicate agent service validation, add a Manage agents action, and remove low-signal bot tests. Made-with: Cursor * Refactor UserAgent to use old BotConfig * Make the UI the source of truth for agent defaults and use explicit full-object create/update payloads. - Remove the unshipped SelfServiceAgentDefaults config and its create-time default injection so the create modal owns all defaults. - Convert CreateAgentRequest / UpdateAgentRequest to full-object shapes; update is now a full replacement rather than a pointer-based patch. - Make enabledMCPTools a required tri-state field on both endpoints (null = all, [] = none, [items] = allowlist; omission is rejected). - Drop defaultControlledFieldsTouched plumbing from the create modal so both create and update send the full draft on every save. - Update tests and e2e helpers for the new contract; e2e updateAgent helper fetches + merges to preserve partial-override ergonomics. Made-with: Cursor * e2e: fix BotConfigHelper agent updates for full-object PUT contract Export mergeAgentIntoUpdate and use GET+merge+PUT for migrated bots so partial displayName/service updates match the agents API after the pointer-patch removal. Made-with: Cursor * Replace per-agent MCP tri-state with explicit auto-enable boolean The per-agent `enabledMCPTools` field was overloaded with tri-state semantics (nil=all, []=none, [..]=allowlist) to preserve the pre-allowlist "all MCP tools" behavior for migrated config bots. That coupling required a custom JSON unmarshaler, a presence-tracked `enabledMCPToolsProvided` bool, special marshal/unmarshal in the store, and `null` vs `"[]"` distinctions in SQL — all to encode a single user intent: "let this agent pick up every MCP tool, now and in the future." Replaces the tri-state with an explicit `AutoEnableNewMCPTools bool` on BotConfig. When true, the agent gets every MCP tool (existing and future) and `EnabledMCPTools` is ignored. When false, only tools listed in `EnabledMCPTools` are available. Key changes: - BotConfig: add AutoEnableNewMCPTools; drop tri-state doc on EnabledMCPTools. - Store: extend migration 000005 with the new column; simplify marshal/unmarshal to drop nil/"null" handling. - API: remove custom UnmarshalJSON + enabledMCPToolsProvided and the 400 guard that required the field to be present. Expose the new bool in AIBotInfo for the RHS popover. - Legacy bot migration: flip AutoEnableNewMCPTools=true for config-migrated agents to preserve their historical "all tools" behavior. - Frontend: add a checkbox to the MCPs tab ("Automatically enable all MCP tools"), default new agents to auto-enable on (fixes the prior UX quirk where new agents defaulted to zero tools), and surface "All MCP tools" as an agent row badge. - E2E: update types and tests; the "implicit-all" case now asserts the bool directly instead of a null allowlist. - Makefile: quote the i18n-extract glob so formatjs recurses into src/components/**/tabs/ (macOS bash 3.2 lacks globstar). None of the tri-state behavior has shipped, so no backfill migration is needed — the original 000005 migration is edited directly. Made-with: Cursor * MM-65671: Agent API hardening and themed react-select - Cap agent create/update JSON bodies and validate config before bot creation. - Allow one self-service agent without multi-LLM license; keep enterprise unlimited. - Add active agent count for quota checks and extend API tests. - Portal async multi-select menus to #root with fixed positioning and high z-index so agent Access pickers are not clipped by the modal footer. - Theme SelectUser/SelectChannel and system console ComboboxItem with center-channel tokens for dark/light consistency. - Align reasoning config and related LLM configuration with branch work. Made-with: Cursor * Address Cursor Bugbot: EnsureBots DB agents, bot refresh fallbacks, DRY Responses API - Skip deactivating plugin-owned Mattermost bots that still map to active DB agents when EnsureBots cannot reconcile them (missing/invalid service). - Centralize ServiceUsesResponsesAPI on llm.ServiceConfig; bifrost and API use it. - refreshBotsAndNotify returns EnsureBots error; update/delete handlers only apply explicit Bot.Patch / UpdateActive when EnsureBots did not run, failed, or no bot registry is configured. GitHub: replied on PR #589 to Cursor auth threads (handlers use canConfigureAgentServices). Made-with: Cursor * Fix nil a.bots in canUserAccessAgent via UsageRestrictionsForUserConfig Agent list/get routes call canUserAccessAgent without aiBotRequired; dereferencing a.bots for usage checks could panic when the bot registry is nil. Evaluate restrictions with the plugin client instead; MMBots delegates to the same helper. Made-with: Cursor * Fix eslint lines-around-comment in select.tsx Made-with: Cursor * Fix Bugbot: clone config on GET admin/config; ShouldBindJSON for models/fetch - Avoid mutating cached Services when normalizing OpenAI UseResponsesAPI for the response. - Use ShouldBindJSON in handleFetchModelsForService for consistent error handling. - Add regression test for GET not mutating stored service config. Made-with: Cursor * store: monotonic UpdateAt on agent update (fix CI flake) plugin-tests failed on TestAgentUpdate when CreateAgent and UpdateAgent shared the same millisecond: assert.Greater(UpdateAt) failed. Bump UpdateAt to at least previous+1 when the clock has not advanced. Made-with: Cursor * e2e: align live service agent tool settings Update the live service flow to reset agent tool defaults after creation so the DM verification exercises plain replies instead of hanging on pending tool calls. Made-with: Cursor * style: gofmt api_agents.go Normalize formatting in api_agents.go so the Go lint/style checks pass cleanly. Made-with: Cursor --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Mattermost Build <build@mattermost.com>
1 parent fa4910a commit 0796cf7

97 files changed

Lines changed: 9457 additions & 1291 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ specs/
5151

5252
opencode.json
5353

54-
.planning/
54+
.planning

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ i18n-extract: i18n-extract-webapp i18n-extract-server
444444

445445
.PHONY: i18n-extract-webapp
446446
i18n-extract-webapp:
447-
cd webapp && $(NPM) run i18n-extract -- --out-file src/i18n/en.json --id-interpolation-pattern '[sha512:contenthash:base64:8]' --format simple src/index.tsx src/components/**/*.{ts,tsx}
447+
cd webapp && $(NPM) run i18n-extract -- --out-file src/i18n/en.json --id-interpolation-pattern '[sha512:contenthash:base64:8]' --format simple src/index.tsx 'src/components/**/*.{ts,tsx}'
448448

449449
.PHONY: i18n-extract-server
450450
i18n-extract-server:

api/api.go

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ type ConfigStore interface {
6767
SaveConfig(cfg config.Config) error
6868
}
6969

70+
// AgentStore provides CRUD access to user-created agents in the database.
71+
type AgentStore interface {
72+
CreateAgent(cfg *llm.BotConfig) error
73+
GetAgent(id string) (*llm.BotConfig, error)
74+
ListAgents() ([]*llm.BotConfig, error)
75+
ListAgentsByCreator(creatorID string) ([]*llm.BotConfig, error)
76+
CountActiveAgents() (int, error)
77+
UpdateAgent(cfg *llm.BotConfig) error
78+
DeleteAgent(id string) error
79+
}
80+
7081
// ConfigUpdater updates the in-memory plugin configuration.
7182
type ConfigUpdater interface {
7283
Update(cfg *config.Config)
@@ -77,6 +88,11 @@ type ClusterNotifier interface {
7788
PublishConfigUpdate() error
7889
}
7990

91+
// ClusterAgentNotifier broadcasts agent update events to other cluster nodes.
92+
type ClusterAgentNotifier interface {
93+
PublishAgentUpdate() error
94+
}
95+
8096
// API represents the HTTP API functionality for the plugin
8197
type API struct {
8298
bots *bots.MMBots
@@ -99,8 +115,10 @@ type API struct {
99115
mcpHandlers *mcpserver.PluginMCPHandlers
100116
llmUpstreamHTTPClient *http.Client
101117
configStore ConfigStore
118+
agentStore AgentStore
102119
configUpdater ConfigUpdater
103120
clusterNotifier ClusterNotifier
121+
clusterAgentNotifier ClusterAgentNotifier
104122
getSearchInitError func() string
105123
customPromptsStore *customprompts.Store
106124
}
@@ -126,8 +144,10 @@ func New(
126144
mcpHandlers *mcpserver.PluginMCPHandlers,
127145
llmUpstreamHTTPClient *http.Client,
128146
configStore ConfigStore,
147+
agentStore AgentStore,
129148
configUpdater ConfigUpdater,
130149
clusterNotifier ClusterNotifier,
150+
clusterAgentNotifier ClusterAgentNotifier,
131151
getSearchInitError func() string,
132152
customPromptsStore *customprompts.Store,
133153
) *API {
@@ -152,8 +172,10 @@ func New(
152172
mcpHandlers: mcpHandlers,
153173
llmUpstreamHTTPClient: llmUpstreamHTTPClient,
154174
configStore: configStore,
175+
agentStore: agentStore,
155176
configUpdater: configUpdater,
156177
clusterNotifier: clusterNotifier,
178+
clusterAgentNotifier: clusterAgentNotifier,
157179
getSearchInitError: getSearchInitError,
158180
customPromptsStore: customPromptsStore,
159181
}
@@ -212,6 +234,22 @@ func (a *API) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Reques
212234
router.PUT("/mcp/user-preferences", a.handlePutUserPreferences)
213235
router.DELETE("/mcp/oauth/:serverName", a.handleDeleteUserMCPOAuth)
214236

237+
// Agent routes — authenticated. Free-tier instances (no multi-LLM license)
238+
// can CRUD up to one self-service agent; the quota is enforced inside
239+
// handleCreateAgent so reads, updates, deletes, and avatar uploads remain
240+
// available even after a license downgrade.
241+
agentRouter := router.Group("/agents")
242+
agentRouter.POST("", a.handleCreateAgent)
243+
agentRouter.GET("", a.handleListAgents)
244+
// Register /models/fetch before /:agentid routes so "models" is never captured as :agentid.
245+
agentRouter.POST("/models/fetch", a.handleFetchModelsForService)
246+
agentRouter.GET("/:agentid", a.handleGetAgent)
247+
agentRouter.PUT("/:agentid", a.handleUpdateAgent)
248+
agentRouter.DELETE("/:agentid", a.handleDeleteAgent)
249+
agentRouter.POST("/:agentid/avatar", a.handleUploadAgentAvatar)
250+
251+
router.GET("/services", a.handleListServices)
252+
215253
// Raw search endpoint returns enriched semantic search results without LLM processing.
216254
// Used by the MCP server for external search callbacks.
217255
router.POST("/search/raw", a.handleRawSearch)
@@ -295,8 +333,11 @@ func (a *API) metricsMiddleware(c *gin.Context) {
295333
}
296334

297335
func (a *API) aiBotRequired(c *gin.Context) {
298-
// We should integreate LLM here
299336
botUsername := c.Query("botUsername")
337+
if botUsername == "" {
338+
botUsername = a.config.GetDefaultBotName()
339+
}
340+
300341
bot := a.bots.GetBotByUsernameOrFirst(botUsername)
301342
if bot == nil {
302343
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("failed to get bot: %s", botUsername))
@@ -351,15 +392,17 @@ func (a *API) handleGetAIThreads(c *gin.Context) {
351392
}
352393

353394
type AIBotInfo struct {
354-
ID string `json:"id"`
355-
DisplayName string `json:"displayName"`
356-
Username string `json:"username"`
357-
LastIconUpdate int64 `json:"lastIconUpdate"`
358-
DMChannelID string `json:"dmChannelID"`
359-
ChannelAccessLevel llm.ChannelAccessLevel `json:"channelAccessLevel"`
360-
ChannelIDs []string `json:"channelIDs"`
361-
UserAccessLevel llm.UserAccessLevel `json:"userAccessLevel"`
362-
UserIDs []string `json:"userIDs"`
395+
ID string `json:"id"`
396+
DisplayName string `json:"displayName"`
397+
Username string `json:"username"`
398+
LastIconUpdate int64 `json:"lastIconUpdate"`
399+
DMChannelID string `json:"dmChannelID"`
400+
ChannelAccessLevel llm.ChannelAccessLevel `json:"channelAccessLevel"`
401+
ChannelIDs []string `json:"channelIDs"`
402+
UserAccessLevel llm.UserAccessLevel `json:"userAccessLevel"`
403+
UserIDs []string `json:"userIDs"`
404+
EnabledMCPTools []llm.EnabledMCPTool `json:"enabledMCPTools"`
405+
AutoEnableNewMCPTools bool `json:"autoEnableNewMCPTools"`
363406
}
364407

365408
type AIBotsResponse struct {
@@ -392,15 +435,17 @@ func (a *API) getAIBotsForUser(userID string) ([]AIBotInfo, error) {
392435
}
393436

394437
bots = append(bots, AIBotInfo{
395-
ID: bot.GetMMBot().UserId,
396-
DisplayName: bot.GetMMBot().DisplayName,
397-
Username: bot.GetMMBot().Username,
398-
LastIconUpdate: bot.GetMMBot().LastIconUpdate,
399-
DMChannelID: dmChannelID,
400-
ChannelAccessLevel: bot.GetConfig().ChannelAccessLevel,
401-
ChannelIDs: bot.GetConfig().ChannelIDs,
402-
UserAccessLevel: bot.GetConfig().UserAccessLevel,
403-
UserIDs: bot.GetConfig().UserIDs,
438+
ID: bot.GetMMBot().UserId,
439+
DisplayName: bot.GetMMBot().DisplayName,
440+
Username: bot.GetMMBot().Username,
441+
LastIconUpdate: bot.GetMMBot().LastIconUpdate,
442+
DMChannelID: dmChannelID,
443+
ChannelAccessLevel: bot.GetConfig().ChannelAccessLevel,
444+
ChannelIDs: bot.GetConfig().ChannelIDs,
445+
UserAccessLevel: bot.GetConfig().UserAccessLevel,
446+
UserIDs: bot.GetConfig().UserIDs,
447+
EnabledMCPTools: bot.GetConfig().EnabledMCPTools,
448+
AutoEnableNewMCPTools: bot.GetConfig().AutoEnableNewMCPTools,
404449
})
405450
if bot.GetMMBot().Username == defaultBotName {
406451
bots[0], bots[i] = bots[i], bots[0]

api/api_admin.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -242,28 +242,26 @@ func (a *API) handleGetMCPTools(c *gin.Context) {
242242
Servers: make([]MCPServerInfo, 0, len(mcpConfig.Servers)+1),
243243
}
244244

245-
// Discover tools from embedded server
246-
{
247-
embeddedServer := a.mcpClientManager.GetEmbeddedServer()
248-
if embeddedServer != nil {
249-
serverInfo := MCPServerInfo{
250-
Name: mcp.EmbeddedServerName,
251-
URL: mcp.EmbeddedClientKey,
252-
Tools: []MCPToolInfo{},
253-
Error: nil,
254-
}
255-
256-
// Try to discover tools from embedded server
257-
tools, err := a.discoverEmbeddedServerTools(c.Request.Context(), userID, mcpConfig.EmbeddedServer, embeddedServer)
258-
if err != nil {
259-
errMsg := err.Error()
260-
serverInfo.Error = &errMsg
261-
} else {
262-
serverInfo.Tools = tools
263-
}
245+
embeddedServer := a.mcpClientManager.GetEmbeddedServer()
246+
if embeddedServer != nil {
247+
serverInfo := MCPServerInfo{
248+
Name: mcp.EmbeddedServerName,
249+
URL: mcp.EmbeddedClientKey,
250+
Tools: []MCPToolInfo{},
251+
Error: nil,
252+
}
264253

265-
response.Servers = append(response.Servers, serverInfo)
254+
// Embedded MCP is always available after PR #617, even if older configs still
255+
// have the legacy toggle stored as false.
256+
tools, err := a.discoverEmbeddedServerTools(c.Request.Context(), userID, mcpConfig.EmbeddedServer, embeddedServer)
257+
if err != nil {
258+
errMsg := err.Error()
259+
serverInfo.Error = &errMsg
260+
} else {
261+
serverInfo.Tools = tools
266262
}
263+
264+
response.Servers = append(response.Servers, serverInfo)
267265
}
268266

269267
// Discover tools from each configured remote server

api/api_admin_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func setupAdminTestEnvironment(t *testing.T) (*API, *plugintest.API) {
3434
cfg := &testConfigImpl{}
3535
noopMetrics := &metrics.NoopMetrics{}
3636

37-
api := New(nil, nil, nil, nil, nil, client, noopMetrics, nil, cfg, nil, nil, nil, nil, nil, nil, &mockMCPClientManager{}, nil, nil, nil, nil, nil, nil, nil)
37+
api := New(nil, nil, nil, nil, nil, client, noopMetrics, nil, cfg, nil, nil, nil, nil, nil, nil, &mockMCPClientManager{}, nil, nil, nil, nil, nil, nil, nil, nil, nil)
3838

3939
return api, mockAPI
4040
}

0 commit comments

Comments
 (0)