Skip to content

Add service account authentication for external MCP servers - #937

Draft
nickmisasi wants to merge 9 commits into
masterfrom
cursor/mcp-service-account-auth-a475
Draft

Add service account authentication for external MCP servers#937
nickmisasi wants to merge 9 commits into
masterfrom
cursor/mcp-service-account-auth-a475

Conversation

@nickmisasi

@nickmisasi nickmisasi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds service account authentication for MCP servers, so automation-oriented agents can call external MCP servers (e.g. Jira) with admin-provisioned shared credentials instead of per-user OAuth.

What's new

  • Each remote MCP server config gains a Service Account Authentication headers section in the System Console (mirrors the existing Headers editor; blank rows are filtered via EffectiveServiceAccountHeaders()).
  • Agents gain a single all-or-nothing "Use service accounts for authentication" setting on the agent modal's MCPs tab, with a warning banner explaining the shared-access implications.
  • When enabled (and remote MCP is licensed): external MCP servers authenticate with the configured SA headers (servers without SA headers are excluded fail-closed from the agent's catalog — no fallback to user OAuth); embedded/plugin MCP tools run as the agent's bot account (channel membership is the internal access boundary); all per-user OAuth machinery (connect prompts, mcp_oauth_needed state, per-user server preferences, RHS Tools popover) is bypassed/hidden.
  • The contract applies in every context — DMs, channel mentions, and the AI bridge. Interactive approval is unchanged: the human initiator approves, the service account executes.
  • Attribution: token usage logs gain acting_user_id and tool_auth_mode.
  • Bridge: SA-flagged agents use the SA catalog regardless of the caller-supplied user_id (which still drives usage ACLs and attribution); discovery uses the same catalog as execution.
  • Unlicensed servers: SA mode is fully off — the agent authenticates per-user like any other agent.
  • Non-SA agents are unchanged everywhere, including bridge user_id semantics.
  • Docs updated: docs/features/multiplayer_tool_calling.md (the "service accounts are not a concept" contract is replaced with per-user vs service-account modes), docs/admin_guide.md, public/bridgeclient/README.md, user guide, managing-agents.

Implementation notes

  • SA connections pool per (bot user ID, service-account mode) in mcp.ClientManager, with an isolated shared tools cache namespace (sa:<server>); X-Mattermost-UserID is the agent bot's ID. The "SA never surfaces OAuth prompts" invariant is enforced structurally in Client.oauthNeededError.
  • Catalog selection lives at the single funnel (llmcontext.Builder.getToolsStoreForUser) via UsesServiceAccountCatalog, so conversations, the bridge, discovery, and channel analysis all inherit it with no per-site branching.
  • New migration 000010 adds UseServiceAccountAuth to Agents_UserAgents.
  • Bridge tool_hooks are deliberately not accounted for in SA mode (their only consumer, the Channel Automation plugin, is being retired).

QA (manual, local server + real Anthropic LLM + PAT-gated external MCP server)

  • Adding SA headers in the console, creating an SA agent (warning banner), and saving all persist correctly.
  • End-to-end proof: chatting with the SA agent and calling the external server's get_me tool returns the service account's username (not the signed-in user), after human approval, with no OAuth prompt; the RHS Tools popover is hidden.
  • Regression: a normal agent keeps the per-user Tools popover with a Connect button for the same server.
System Console: SA headers on server Agent MCPs tab: setting + warning
Console MCP server with Service Account Authentication section Agent MCPs tab with service account checkbox and warning

sa_agent_tool_call_runs_as_service_account.mp4

normal_agent_keeps_per_user_tools_ux.mp4

Release Note

Added service account authentication for external MCP servers. MCP server configurations accept a new Service Account Authentication headers section, and agents gained a "Use service accounts for authentication" setting: when enabled, external MCP tool calls authenticate with the admin-configured service account headers (servers without them are excluded fail-closed) and embedded Mattermost tools run as the agent's bot account. Added the UseServiceAccountAuth column to the Agents_UserAgents table. Token usage logs gained acting_user_id and tool_auth_mode fields.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added agent-level “use service accounts for authentication” support, persisted end-to-end and surfaced in the agent list response.
    • MCP authentication can now use admin-configured service-account static headers; external MCP servers without required headers are excluded (fail-closed).
    • Service-account agents hide the Tools UI and attribute token usage by acting identity and auth mode.
  • Documentation
    • Updated admin, user, and MCP/bridge documentation to explain service-account behaviour, credential boundaries, and fail-closed rules.
  • Tests
    • Expanded automated coverage for flag round-trips, tool-catalog selection, tool execution, and token attribution.

cursoragent and others added 6 commits July 28, 2026 20:31
… flag

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
…context

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 LLM Evaluation Results

OpenAI

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ OPENAI 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

OPENAI

1. TestReactEval/[openai]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺/🐱) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide a list of bugs; it states it cannot enumerate bugs without pasted reports and only offers a template table with empty rows.

3. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output does not include descriptions of any actual bugs; it only states it cannot access the database and provides a template/table structure to be filled in later. Therefore it does not include a description of each bug.

4. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output does not enumerate any bugs and therefore does not attribute each bug to a specific user; it only provides a template with an empty 'Reported by' column.

5. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention the specific bug about saving without a color or the save button doing nothing, nor does it attribute that bug to @maria.nunez. It only states it lacks access to bug reports and provides a template.

6. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output does not list or attribute any specific bug. It provides a template and requests the user to paste bug reports, but it never mentions the channel banner change bug or attributes it to @maria.nunez.

7. TestDirectMessageConversations/[openai]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to the Mattermost documentation and includes a docs.mattermost.com link, but it does not mention GitHub anywhere, so it does not satisfy the requirement to mention GitHub and refer to documentation.

Anthropic

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ ANTHROPIC 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

ANTHROPIC

1. TestReactEval/[anthropic]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺/🐱) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide any bug list; it states it lacks access to bug reports and suggests how the user could find them, offering to format them if provided.

3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output states it cannot access bug reports and suggests where to search, but it does not list any bugs or provide descriptions of each bug.

4. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output does not list any bugs at all, and therefore does not attribute each bug to a user. It only states lack of access and suggests how to find bug reports.

5. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention @maria.nunez or attribute any specific bug (saving without a color and the save button not doing anything) to them; it only states lack of access and suggests how to search for bug reports.

6. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output does not mention the specific bug about an end user being able to change the channel banner, nor does it attribute that bug to @maria.nunez. It only states a lack of access and suggests how to find bug reports.

7. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection

  • Score: 0.50
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation (docs.mattermost.com) but does not mention GitHub anywhere, so it does not satisfy the rubric requirement to mention GitHub and refer to the documentation.

This comment was automatically generated by the eval CI pipeline.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Agents now support opt-in service-account authentication across persistence, MCP execution, bridge and conversation flows, administration, UI configuration, and token attribution. External MCP servers without valid service-account headers are excluded, while agent identity is preserved for execution and logging.

Changes

Service-account configuration and persistence

Layer / File(s) Summary
Agent contracts and storage
api/..., store/..., llm/configuration.go, config/mcp_config.go
Agent requests, responses, database records, bot configuration, and MCP server configuration now carry service-account settings and headers.
MCP client execution
mcp/...
Service-account clients use static headers, separate client pools and cache namespaces, fail-closed server selection, and no OAuth-needed classification.
Catalog, bridge, and conversation flows
llmcontext/..., api/api_llm_bridge.go, conversations/...
Tool catalogs use the agent bot identity when service-account mode is licensed and enabled; bridge tool hooks are rejected and conversation approval resumes re-derive the service-account catalog.
Web application controls
webapp/src/components/agents/..., webapp/src/components/system_console/..., webapp/src/components/rhs/...
Agent configuration exposes the service-account toggle, system administration supports service-account headers, and the per-user Tools popover is hidden for service-account agents.
Attribution and documentation
llm/token_tracking.go, docs/..., public/bridgeclient/README.md
Token logs include acting identity and tool authentication mode, and documentation covers credential, approval, fail-closed, and bridge API behaviour.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentConfig
  participant ContextBuilder
  participant ClientManager
  participant MCPServer
  participant LLM
  AgentConfig->>ContextBuilder: enable service-account authentication
  ContextBuilder->>ClientManager: request service-account catalog
  ClientManager->>MCPServer: connect with static headers and bot identity
  MCPServer-->>ClientManager: eligible tools
  ClientManager-->>ContextBuilder: service-account tool catalog
  ContextBuilder->>LLM: provide catalog and tool auth mode
  LLM-->>ContextBuilder: tool request
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding service account authentication for external MCP servers.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/mcp-service-account-auth-a475

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
webapp/src/components/system_console/mcp_servers.tsx (1)

126-130: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Service-account credentials render in cleartext.

The section help text advertises this field for a personal access token, yet the value input is plain text while the adjacent OAuth Client Secret (line 359) uses type='password'. Consider threading a maskValues prop through HeaderMapEditor and enabling it for the service-account map so the credential isn't shoulder-surfable in the System Console.

🔒 Sketch
 const HeaderMapEditor = ({
     headers,
     onChange,
+    maskValues = false,
 }: {
     headers: {[key: string]: string};
     onChange: (headers: {[key: string]: string}) => void;
+    maskValues?: boolean;
 }) => {
                         <HeaderInput
+                            type={maskValues ? 'password' : 'text'}
                             placeholder={intl.formatMessage({defaultMessage: 'Value'})}
                 <HeaderMapEditor
                     headers={config.serviceAccountHeaders}
+                    maskValues={true}
                     onChange={(serviceAccountHeaders) => onChange(serverIndex, {...config, serviceAccountHeaders})}
                 />

Note this would also require updating the positional Value placeholder lookups in webapp/src/components/system_console/mcp_servers.test.tsx (lines 142-143) if you switch input types.

Also applies to: 303-313

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/system_console/mcp_servers.tsx` around lines 126 - 130,
Update HeaderMapEditor and its service-account header map usage to accept and
apply a maskValues prop, rendering credential value inputs with password masking
when enabled while preserving plain-text behavior elsewhere. Enable masking for
the service-account credentials, and update the affected mcp_servers tests to
locate the Value inputs without relying on their changed input type or position.
webapp/src/components/agents/tabs/mcps_tab.tsx (1)

149-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

aria-checked is ignored on an implicit role='button'.

Assistive tech drops aria-checked unless the element exposes a checkable role, so the on/off state isn't announced. The aria-label still flips between "Enable"/"Disable", so the control remains usable — hence optional — but role='switch' makes the state programmatically correct.

♻️ Proposed tweak
                 <ServerToggle
                     type='button'
+                    role='switch'
                     aria-label={serverToggleLabel}
                     aria-checked={allOn}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/agents/tabs/mcps_tab.tsx` around lines 149 - 158,
Update the ServerToggle element to expose a checkable switch role so its
existing aria-checked={allOn} state is announced by assistive technology.
Preserve the current label, toggle handler, disabled behavior, and visual state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/mcp_config.go`:
- Around line 122-148: Update EffectiveServiceAccountHeaders to trim each header
name and value before storing them, while continuing to skip entries whose
trimmed name or value is empty. Use the trimmed strings as the map key and value
so HasServiceAccountAuth only exposes valid credentials, and add coverage in the
existing table-driven tests for padded headers.

In `@mcp/client_manager.go`:
- Around line 105-115: Update ClientManager.closeIdleClients to delete the
matching m.activity entry whenever an idle client is removed from m.clients,
using the same key and preserving the existing timeout and client-closing
behavior.

In `@store/migrations/reviews/000010_user_agent_service_account_auth.md`:
- Around line 33-41: Update the “Table Locks & Impact” and “Zero Downtime”
sections to state that the metadata-only ADD COLUMN still acquires ACCESS
EXCLUSIVE and may wait for active transactions or conflicting locks. Remove
“returns instantly” and unqualified “Negligible” claims, and describe the impact
as dependent on lock-wait duration.

In `@webapp/src/components/system_console/mcp_servers.tsx`:
- Around line 101-108: Update updateHeader so renaming a key rebuilds the
headers map in its existing Object.entries order, replacing oldKey with newKey
at the original position rather than deleting and appending it. Preserve the
current value update and onChange behavior, including the unchanged-key path, so
rows rendered by index remain stable.

---

Nitpick comments:
In `@webapp/src/components/agents/tabs/mcps_tab.tsx`:
- Around line 149-158: Update the ServerToggle element to expose a checkable
switch role so its existing aria-checked={allOn} state is announced by assistive
technology. Preserve the current label, toggle handler, disabled behavior, and
visual state.

In `@webapp/src/components/system_console/mcp_servers.tsx`:
- Around line 126-130: Update HeaderMapEditor and its service-account header map
usage to accept and apply a maskValues prop, rendering credential value inputs
with password masking when enabled while preserving plain-text behavior
elsewhere. Enable masking for the service-account credentials, and update the
affected mcp_servers tests to locate the Value inputs without relying on their
changed input type or position.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7c279556-1ca7-4c98-9fd5-3d2103f02588

📥 Commits

Reviewing files that changed from the base of the PR and between 48a6b5e and b5f5de2.

📒 Files selected for processing (58)
  • api/api.go
  • api/api_agents.go
  • api/api_agents_test.go
  • api/api_channel_analysis_test.go
  • api/api_config_test.go
  • api/api_llm_bridge.go
  • api/api_llm_bridge_service_account_test.go
  • api/api_llm_bridge_test.go
  • api/api_no_tools_test.go
  • api/api_test.go
  • bots/bot.go
  • config/mcp_config.go
  • config/mcp_config_test.go
  • conversations/bot_channel_tool_filter_test.go
  • conversations/conversations_test.go
  • conversations/dm_conversation_test.go
  • conversations/handle_messages.go
  • conversations/service_account_test.go
  • conversations/single_build_test.go
  • docs/admin_guide.md
  • docs/features/managing_agents.md
  • docs/features/multiplayer_tool_calling.md
  • docs/user_guide.md
  • e2e/helpers/agent-api.ts
  • llm/configuration.go
  • llm/configuration_test.go
  • llm/context.go
  • llm/token_tracking.go
  • llm/token_tracking_test.go
  • llm/token_usage_fields.go
  • llmcontext/llm_context.go
  • llmcontext/llm_context_license_test.go
  • llmcontext/llm_context_test.go
  • mcp/client.go
  • mcp/client_embedded_oauth_test.go
  • mcp/client_manager.go
  • mcp/client_manager_test.go
  • mcp/client_test.go
  • mcp/service_account_test.go
  • mcp/user_clients.go
  • public/bridgeclient/README.md
  • store/agents.go
  • store/agents_test.go
  • store/migrations/000010_user_agent_service_account_auth.down.sql
  • store/migrations/000010_user_agent_service_account_auth.up.sql
  • store/migrations/reviews/000010_user_agent_service_account_auth.md
  • store/store_test.go
  • webapp/src/bots.tsx
  • webapp/src/components/agents/agent_config_view.test.tsx
  • webapp/src/components/agents/agent_config_view.tsx
  • webapp/src/components/agents/tabs/mcps_tab.test.tsx
  • webapp/src/components/agents/tabs/mcps_tab.tsx
  • webapp/src/components/rhs/rhs_header.test.tsx
  • webapp/src/components/rhs/rhs_header.tsx
  • webapp/src/components/system_console/mcp_servers.test.tsx
  • webapp/src/components/system_console/mcp_servers.tsx
  • webapp/src/i18n/en.json
  • webapp/src/types/agents.ts

Comment thread config/mcp_config.go
Comment on lines +122 to +148
func (s *MCPServerConfig) EffectiveServiceAccountHeaders() map[string]string {
if s == nil {
return nil
}

var headers map[string]string
for name, value := range s.ServiceAccountHeaders {
if strings.TrimSpace(name) == "" || strings.TrimSpace(value) == "" {
continue
}
if headers == nil {
headers = make(map[string]string, len(s.ServiceAccountHeaders))
}
headers[name] = value
}
return headers
}

// HasServiceAccountAuth reports whether this server has Service Account
// authentication configured, i.e. at least one usable header. An SA-flagged
// agent therefore never connects to a server that would effectively send no
// service credential (fail closed). Enabled is intentionally ignored:
// enablement is filtered where catalogs are built, this predicate answers only
// "is SA auth configured".
func (s *MCPServerConfig) HasServiceAccountAuth() bool {
return len(s.EffectiveServiceAccountHeaders()) > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Padded header names still slip through and break the whole request.

The blank check trims, but the stored key/value keep surrounding whitespace. An admin entry like " Authorization" (or a value with a trailing newline pasted from a token file) is non-blank, so it passes the filter and is then rejected by Go's HTTP transport with invalid header field name, failing every SA connection to that server — the exact failure mode the doc comment says this function prevents. Trimming what you keep closes the gap and matches the intent.

🛠️ Proposed fix: store the trimmed name/value
 	var headers map[string]string
 	for name, value := range s.ServiceAccountHeaders {
-		if strings.TrimSpace(name) == "" || strings.TrimSpace(value) == "" {
+		trimmedName := strings.TrimSpace(name)
+		trimmedValue := strings.TrimSpace(value)
+		if trimmedName == "" || trimmedValue == "" {
 			continue
 		}
 		if headers == nil {
 			headers = make(map[string]string, len(s.ServiceAccountHeaders))
 		}
-		headers[name] = value
+		headers[trimmedName] = trimmedValue
 	}
 	return headers

Note the companion table in config/mcp_config_test.go (lines 122-206) would need a row for a padded name/value if you take this.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *MCPServerConfig) EffectiveServiceAccountHeaders() map[string]string {
if s == nil {
return nil
}
var headers map[string]string
for name, value := range s.ServiceAccountHeaders {
if strings.TrimSpace(name) == "" || strings.TrimSpace(value) == "" {
continue
}
if headers == nil {
headers = make(map[string]string, len(s.ServiceAccountHeaders))
}
headers[name] = value
}
return headers
}
// HasServiceAccountAuth reports whether this server has Service Account
// authentication configured, i.e. at least one usable header. An SA-flagged
// agent therefore never connects to a server that would effectively send no
// service credential (fail closed). Enabled is intentionally ignored:
// enablement is filtered where catalogs are built, this predicate answers only
// "is SA auth configured".
func (s *MCPServerConfig) HasServiceAccountAuth() bool {
return len(s.EffectiveServiceAccountHeaders()) > 0
}
func (s *MCPServerConfig) EffectiveServiceAccountHeaders() map[string]string {
if s == nil {
return nil
}
var headers map[string]string
for name, value := range s.ServiceAccountHeaders {
trimmedName := strings.TrimSpace(name)
trimmedValue := strings.TrimSpace(value)
if trimmedName == "" || trimmedValue == "" {
continue
}
if headers == nil {
headers = make(map[string]string, len(s.ServiceAccountHeaders))
}
headers[trimmedName] = trimmedValue
}
return headers
}
// HasServiceAccountAuth reports whether this server has Service Account
// authentication configured, i.e. at least one usable header. An SA-flagged
// agent therefore never connects to a server that would effectively send no
// service credential (fail closed). Enabled is intentionally ignored:
// enablement is filtered where catalogs are built, this predicate answers only
// "is SA auth configured".
func (s *MCPServerConfig) HasServiceAccountAuth() bool {
return len(s.EffectiveServiceAccountHeaders()) > 0
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/mcp_config.go` around lines 122 - 148, Update
EffectiveServiceAccountHeaders to trim each header name and value before storing
them, while continuing to skip entries whose trimmed name or value is empty. Use
the trimmed strings as the map key and value so HasServiceAccountAuth only
exposes valid credentials, and add coverage in the existing table-driven tests
for padded headers.

Comment thread mcp/client_manager.go Outdated
Comment on lines +105 to +115
func (m *ClientManager) closeIdleClients(now time.Time) {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for key, client := range m.clients {
if now.Sub(m.activity[key]) > m.clientTimeout {
m.log.Debug("Closing inactive MCP client", "userID", key.userID, "serviceAccount", key.serviceAccount)
client.Close()
delete(m.clients, key)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

closeIdleClients leaves orphaned activity entries.

The bag is deleted from m.clients, but its timestamp stays in m.activity forever, so the map grows unbounded with distinct (user, auth mode) pairs that never reconnect. Now that each user can occupy two keys, this doubles.

🧹 Proposed fix
 		if now.Sub(m.activity[key]) > m.clientTimeout {
 			m.log.Debug("Closing inactive MCP client", "userID", key.userID, "serviceAccount", key.serviceAccount)
 			client.Close()
 			delete(m.clients, key)
+			delete(m.activity, key)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *ClientManager) closeIdleClients(now time.Time) {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for key, client := range m.clients {
if now.Sub(m.activity[key]) > m.clientTimeout {
m.log.Debug("Closing inactive MCP client", "userID", key.userID, "serviceAccount", key.serviceAccount)
client.Close()
delete(m.clients, key)
}
}
}
func (m *ClientManager) closeIdleClients(now time.Time) {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for key, client := range m.clients {
if now.Sub(m.activity[key]) > m.clientTimeout {
m.log.Debug("Closing inactive MCP client", "userID", key.userID, "serviceAccount", key.serviceAccount)
client.Close()
delete(m.clients, key)
delete(m.activity, key)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/client_manager.go` around lines 105 - 115, Update
ClientManager.closeIdleClients to delete the matching m.activity entry whenever
an idle client is removed from m.clients, using the same key and preserving the
existing timeout and client-closing behavior.

Comment on lines +33 to +41
## Table Locks & Impact
- Tables affected: `Agents_UserAgents`.
- Lock types acquired:
- `ALTER TABLE … ADD COLUMN`: ACCESS EXCLUSIVE on `Agents_UserAgents`. Metadata-only because the default is constant — returns instantly.
- Impact to concurrent operations: Negligible.

## Zero Downtime
- Possible: Yes.
- Reason: Metadata-only ADD COLUMN on an admin-managed table.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,120p' store/migrations/reviews/000010_user_agent_service_account_auth.md

Repository: mattermost/mattermost-plugin-agents

Length of output: 2539


🌐 Web query:

PostgreSQL ALTER TABLE ADD COLUMN ACCESS EXCLUSIVE lock waits behind active transactions metadata-only constant default lock semantics

💡 Result:

In PostgreSQL, an ALTER TABLE ADD COLUMN statement always acquires an ACCESS EXCLUSIVE lock on the target table [1][2]. This lock is the most restrictive level in PostgreSQL and conflicts with all other lock modes, meaning it will block all concurrent access to the table, including simple SELECT statements, until the transaction completes [3][4]. While the lock is always required, its duration and the impact on the system have been significantly reduced by changes introduced in PostgreSQL 11 [5][6]. Metadata-Only and Fast Default Mechanism: When a column is added with a non-volatile DEFAULT value, PostgreSQL now uses a "fast default" mechanism [5][6]. Instead of rewriting the entire table to populate the new column for existing rows, the database evaluates the default value once and stores it in the table's metadata (specifically within the system catalog pg_attribute, using the attmissingval and atthasmissing fields) [5][6]. When existing rows are subsequently read, the database synthesizes the default value on the fly using this metadata [5][6]. Because the table data itself is not modified during the ALTER TABLE command, the operation completes extremely quickly, effectively minimizing the duration for which the ACCESS EXCLUSIVE lock must be held [5][6][4]. Key Considerations: 1. Volatile Defaults: If the default value is volatile (e.g., clock_timestamp or random), the fast default mechanism cannot be used [5]. In such cases, the database must update every existing row, which may result in a much longer hold time for the ACCESS EXCLUSIVE lock [7][5]. 2. Lock Queuing: Even with the fast path, because the ACCESS EXCLUSIVE lock is requested at the start of the command, it can still lead to "lock queuing" in high-traffic environments [4][8]. If there are long-running transactions active on the table when the ALTER TABLE statement is issued, the ALTER TABLE command will wait behind them, and subsequent incoming queries will, in turn, be queued behind the pending ALTER TABLE request [4][8]. 3. No Default/Null: If no DEFAULT is specified (defaulting to NULL) or a simple constant is used, the operation is metadata-only and highly efficient [1][6]. In summary, while the ACCESS EXCLUSIVE lock is mandatory, modern PostgreSQL versions ensure the operation is near-instant for non-volatile constants, preventing the heavy performance penalty associated with full table rewrites [5][6][4].

Citations:


Clarify lock wait behaviour. ADD COLUMN is metadata-only with a constant default, but it still takes ACCESS EXCLUSIVE and can wait behind active transactions; avoid calling it “instant” or the impact “negligible” without that caveat.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/migrations/reviews/000010_user_agent_service_account_auth.md` around
lines 33 - 41, Update the “Table Locks & Impact” and “Zero Downtime” sections to
state that the metadata-only ADD COLUMN still acquires ACCESS EXCLUSIVE and may
wait for active transactions or conflicting locks. Remove “returns instantly”
and unqualified “Negligible” claims, and describe the impact as dependent on
lock-wait duration.

Comment on lines +101 to +108
const updateHeader = (oldKey: string, newKey: string, value: string) => {
const updated = {...headers};
if (oldKey !== newKey) {
delete updated[oldKey];
}
updated[newKey] = value;
onChange(updated);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Renaming a header reorders the rows while key={index} stays fixed.

updateHeader does delete updated[oldKey] then updated[newKey] = value, which moves the entry to the end of the object's insertion order. Rows are rendered from Object.entries(headers) keyed by position, so with two or more headers, typing in the name field of any non-last row swaps that row's rendered content with the one below it on the first keystroke while focus stays put — the admin ends up editing the wrong header. Now that this editor also backs the service-account credential map, that's a credential typed into the wrong slot.

Rebuilding the map in the original order keeps the row stable.

🐛 Proposed fix: preserve entry order on rename
     const updateHeader = (oldKey: string, newKey: string, value: string) => {
-        const updated = {...headers};
-        if (oldKey !== newKey) {
-            delete updated[oldKey];
-        }
-        updated[newKey] = value;
-        onChange(updated);
+        const updated: {[key: string]: string} = {};
+        for (const [k, v] of Object.entries(headers)) {
+            if (k === oldKey) {
+                updated[newKey] = value;
+            } else {
+                updated[k] = v;
+            }
+        }
+        onChange(updated);
     };

Also applies to: 119-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/system_console/mcp_servers.tsx` around lines 101 - 108,
Update updateHeader so renaming a key rebuilds the headers map in its existing
Object.entries order, replacing oldKey with newKey at the original position
rather than deleting and appending it. Preserve the current value update and
onChange behavior, including the unchanged-key path, so rows rendered by index
remain stable.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
llmcontext/llm_context.go (1)

272-292: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Record service-account mode outside the optional MCP-provider branch.

mcpToolProvider is nullable, but c.ToolAuthMode is set only when that provider exists. A licensed service-account agent without an MCP provider therefore reaches token tracking with the default user mode, causing acting_user_id to be attributed to the requesting user instead of the bot account. Set c.ToolAuthMode immediately after useServiceAccount is computed, before the provider check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llmcontext/llm_context.go` around lines 272 - 292, Set c.ToolAuthMode
immediately after useServiceAccount is computed, before any nullable
mcpToolProvider check, assigning service-account mode when enabled and
preserving user mode otherwise. Remove the later service-account assignment
inside the provider branch while keeping the existing tool-fetch behavior
unchanged.
api/api_llm_bridge.go (1)

318-323: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the new bridge error through i18n.

Line 321 adds a client-visible error as a hardcoded English string. Route it through the repository’s i18n extraction or established API error-key mechanism.

As per coding guidelines: “All new user-facing strings must go through i18n extraction instead of being hardcoded.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api_llm_bridge.go` around lines 318 - 323, Update the new error returned
by the tool-hook validation in the request bridge to use the repository’s
established i18n extraction or API error-key mechanism instead of a hardcoded
English message. Preserve the existing BadRequest status and validation behavior
in the service-account catalog check.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@api/api_llm_bridge.go`:
- Around line 318-323: Update the new error returned by the tool-hook validation
in the request bridge to use the repository’s established i18n extraction or API
error-key mechanism instead of a hardcoded English message. Preserve the
existing BadRequest status and validation behavior in the service-account
catalog check.

In `@llmcontext/llm_context.go`:
- Around line 272-292: Set c.ToolAuthMode immediately after useServiceAccount is
computed, before any nullable mcpToolProvider check, assigning service-account
mode when enabled and preserving user mode otherwise. Remove the later
service-account assignment inside the provider branch while keeping the existing
tool-fetch behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 1a2cf38f-75d0-4f24-b82b-b890dac696f9

📥 Commits

Reviewing files that changed from the base of the PR and between b5f5de2 and 97fecc2.

📒 Files selected for processing (34)
  • api/api_llm_bridge.go
  • api/api_llm_bridge_service_account_test.go
  • api/api_llm_bridge_test.go
  • api/api_test.go
  • bots/bot.go
  • config/mcp_config.go
  • config/mcp_config_test.go
  • conversations/bot_channel_tool_filter_test.go
  • conversations/handle_messages.go
  • conversations/service_account_test.go
  • conversations/single_build_test.go
  • llm/configuration.go
  • llm/configuration_test.go
  • llm/context.go
  • llm/token_tracking.go
  • llm/token_tracking_test.go
  • llm/token_usage_fields.go
  • llmcontext/llm_context.go
  • llmcontext/llm_context_license_test.go
  • llmcontext/llm_context_test.go
  • mcp/client.go
  • mcp/client_manager.go
  • mcp/client_test.go
  • mcp/service_account_test.go
  • mcp/user_clients.go
  • webapp/src/bots.tsx
  • webapp/src/components/agents/agent_config_view.test.tsx
  • webapp/src/components/agents/tabs/mcps_tab.test.tsx
  • webapp/src/components/agents/tabs/mcps_tab.tsx
  • webapp/src/components/rhs/rhs_header.test.tsx
  • webapp/src/components/rhs/rhs_header.tsx
  • webapp/src/components/system_console/mcp_servers.test.tsx
  • webapp/src/components/system_console/mcp_servers.tsx
  • webapp/src/types/agents.ts
💤 Files with no reviewable changes (3)
  • conversations/bot_channel_tool_filter_test.go
  • conversations/single_build_test.go
  • llm/token_tracking_test.go
🚧 Files skipped from review as they are similar to previous changes (27)
  • llm/context.go
  • webapp/src/components/rhs/rhs_header.tsx
  • llm/configuration.go
  • webapp/src/bots.tsx
  • webapp/src/types/agents.ts
  • conversations/handle_messages.go
  • bots/bot.go
  • webapp/src/components/agents/agent_config_view.test.tsx
  • webapp/src/components/rhs/rhs_header.test.tsx
  • mcp/client_test.go
  • webapp/src/components/system_console/mcp_servers.test.tsx
  • llm/token_usage_fields.go
  • conversations/service_account_test.go
  • mcp/user_clients.go
  • webapp/src/components/agents/tabs/mcps_tab.test.tsx
  • config/mcp_config_test.go
  • llmcontext/llm_context_license_test.go
  • config/mcp_config.go
  • webapp/src/components/agents/tabs/mcps_tab.tsx
  • api/api_test.go
  • api/api_llm_bridge_service_account_test.go
  • webapp/src/components/system_console/mcp_servers.tsx
  • llmcontext/llm_context_test.go
  • api/api_llm_bridge_test.go
  • mcp/client.go
  • mcp/client_manager.go
  • mcp/service_account_test.go

@mm-cloud-bot

Copy link
Copy Markdown

Plugin test server update attempted, but encountered an issue:

⚠️ Install Error: failed to install plugin via mmctl: failed with status code 500

The test server is still available. You can manually download and install the updated plugin using the artifact link below.

Updated with git commit 97fecc2881155d241b3f6dbff7be94abdec14025

Plugin Version Artifact
agents 97fecc2 Download

@mm-cloud-bot

Copy link
Copy Markdown

Test server destroyed

cursoragent and others added 2 commits July 29, 2026 01:22
…account-auth-a475

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
… tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/components/agents/tabs/mcps_tab.tsx (1)

313-375: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Bypass per-user OAuth controls in service-account mode.

When useServiceAccountAuth is enabled, this still renders Connect, connection badges, and instructions telling a user to authenticate. Hide these controls and render the service-account catalogue/state instead; otherwise the UI contradicts the selected authentication mode.

Based on PR objectives: per-user OAuth flows and related UI are bypassed.

Also applies to: 394-402

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/agents/tabs/mcps_tab.tsx` around lines 313 - 375,
Update the MCP server rendering around ServerHeaderButton and ConnectButton to
branch on useServiceAccountAuth: when service-account authentication is enabled,
suppress the per-user Connect control, authentication/connection badges, and
related user-authentication instructions, and render the existing
service-account catalogue/state instead. Preserve the current OAuth UI behavior
when useServiceAccountAuth is disabled, including the applicable tools metadata
and server expansion controls.
🧹 Nitpick comments (1)
llm/configuration_test.go (1)

668-672: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add read-side and true-value coverage for the new flag.

This assertion covers only marshalling false. Add table-driven unmarshal/round-trip cases for both true and false; otherwise a regression while loading persisted or API configuration could silently disable service-account authentication while this test still passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/configuration_test.go` around lines 668 - 672, Extend the BotConfig
coverage in the existing configuration tests with table-driven cases that
unmarshal and round-trip both true and false values for UseServiceAccountAuth,
verifying the loaded value is preserved and serialized correctly. Retain the
current false-field presence assertions and use the existing
MCPDynamicToolLoading/UseServiceAccountAuth symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/multiplayer_tool_calling.md`:
- Around line 81-82: Update the approval rationale in the multiplayer
tool-calling documentation to qualify that tools run with the initiator’s
credentials only for per-user authentication. Keep the existing initiator-only
approval behavior and service-account explanation unchanged.

---

Outside diff comments:
In `@webapp/src/components/agents/tabs/mcps_tab.tsx`:
- Around line 313-375: Update the MCP server rendering around ServerHeaderButton
and ConnectButton to branch on useServiceAccountAuth: when service-account
authentication is enabled, suppress the per-user Connect control,
authentication/connection badges, and related user-authentication instructions,
and render the existing service-account catalogue/state instead. Preserve the
current OAuth UI behavior when useServiceAccountAuth is disabled, including the
applicable tools metadata and server expansion controls.

---

Nitpick comments:
In `@llm/configuration_test.go`:
- Around line 668-672: Extend the BotConfig coverage in the existing
configuration tests with table-driven cases that unmarshal and round-trip both
true and false values for UseServiceAccountAuth, verifying the loaded value is
preserved and serialized correctly. Retain the current false-field presence
assertions and use the existing MCPDynamicToolLoading/UseServiceAccountAuth
symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 30bcc64f-1b71-4c69-95a7-39965355eecf

📥 Commits

Reviewing files that changed from the base of the PR and between 97fecc2 and 52bf800.

📒 Files selected for processing (26)
  • api/api_agents_test.go
  • api/api_config_test.go
  • api/api_llm_bridge.go
  • api/api_llm_bridge_service_account_test.go
  • api/api_llm_bridge_test.go
  • api/api_test.go
  • config/mcp_config_test.go
  • conversations/service_account_test.go
  • conversations/single_build_test.go
  • docs/admin_guide.md
  • docs/features/multiplayer_tool_calling.md
  • llm/configuration_test.go
  • llm/token_tracking_test.go
  • llmcontext/llm_context.go
  • llmcontext/llm_context_test.go
  • mcp/client_embedded_oauth_test.go
  • mcp/client_manager.go
  • mcp/client_test.go
  • mcp/service_account_test.go
  • mcp/user_clients.go
  • public/bridgeclient/README.md
  • store/agents_test.go
  • webapp/src/components/agents/agent_config_view.test.tsx
  • webapp/src/components/agents/tabs/mcps_tab.test.tsx
  • webapp/src/components/agents/tabs/mcps_tab.tsx
  • webapp/src/components/system_console/mcp_servers.test.tsx
💤 Files with no reviewable changes (2)
  • api/api_llm_bridge.go
  • api/api_llm_bridge_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • webapp/src/components/agents/tabs/mcps_tab.test.tsx
  • conversations/single_build_test.go
  • api/api_test.go
  • docs/admin_guide.md
  • webapp/src/components/agents/agent_config_view.test.tsx
  • mcp/client_manager.go
  • llmcontext/llm_context.go

Comment on lines +81 to +82
Initiator-only approval applies in both credential modes (§3). For a service account agent the initiator is still the person asking for the side effect, so their consent is still the one that matters; the admin consented to the credential itself when they enabled it on the agent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the approval rationale to per-user authentication.

The new clarification correctly says initiator-only approval applies in both modes, but the preceding rationale says tools run with the initiator’s credentials. That is inaccurate for service-account agents and should be qualified as applying only to per-user authentication.

Suggested wording
- Because tools run with the initiator's credentials and OAuth scopes (see §3), only the initiator has the standing to consent to side effects executed under their identity.
+ In per-user authentication mode, tools run with the initiator's credentials and OAuth scopes (see §3), so only the initiator has the standing to consent to side effects executed under their identity. In service-account mode, initiator-only approval remains the consent contract even though the service account performs the action.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~81-~81: Possible missing comma found.
Context: ...ntial modes (§3). For a service account agent the initiator is still the person askin...

(AI_HYDRA_LEO_MISSING_COMMA)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/features/multiplayer_tool_calling.md` around lines 81 - 82, Update the
approval rationale in the multiplayer tool-calling documentation to qualify that
tools run with the initiator’s credentials only for per-user authentication.
Keep the existing initiator-only approval behavior and service-account
explanation unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants