Skip to content

Persist plugin MCP registrations and rehydrate on Agents restart - #949

Open
nickmisasi wants to merge 2 commits into
masterfrom
cursor/agents-mcp-registration-rehydrate-3648
Open

Persist plugin MCP registrations and rehydrate on Agents restart#949
nickmisasi wants to merge 2 commits into
masterfrom
cursor/agents-mcp-registration-rehydrate-3648

Conversation

@nickmisasi

@nickmisasi nickmisasi commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Cross-plugin MCP registrations (e.g. Playbooks' 29 tools) were stored only in-memory in mcp.ClientManager. When the Agents plugin restarted without a server config change — plugin upgrade/redeploy or crash-supervisor restart — the registry was wiped, and source plugins never re-registered because their client-side retry loop is one-shot per their own OnActivate. The tools stayed missing until the source plugin itself restarted. (Restarts via System Console disable/enable only self-healed by accident: the PluginStates config change fires OnConfigurationChange in source plugins, and that path breaks entirely on instances where config saves fail.)

This PR persists live registrations in the plugin KV store and rehydrates them on activation:

  • RegisterPluginServer / UnregisterPluginServer mirror the registration into a single KV entry (mcp_plugin_registrations_v1) using KV.SetAtomicWithRetries, so concurrent nodes in HA cannot lose updates. Persistence failures log and degrade; the in-memory registration always succeeds.
  • On NewClientManager, persisted registrations are restored as live (IsPluginRegistered == true) for source plugins whose PluginSettings.PluginStates[id].Enable is true. Entries for disabled/removed plugins are pruned from KV — safe because a re-enabled plugin re-registers itself via its own OnActivate. If the server config is unavailable during hydration (RPC failure), all entries are kept and nothing is pruned (fail open).
  • Admin-owned fields (Enabled, ToolConfigs) remain authoritative in the persisted plugin config; hydration runs before syncPluginServersFromConfig so the existing admin merge wins, same as a live registration.
  • The admin partial-update handler now uses a new non-persisting UpdatePluginServer, so a direct PUT /admin/mcp/plugin-servers/:pluginID against a config-only orphan can no longer create a durable fake registration.
  • Updated the stale restart caveat in external/pluginmcp/README.md.

QA test steps (verified live):

  1. Deploy Agents and Playbooks, enable Playbooks experimental features; confirm Playbooks MCP (29 tools) appears in GET /plugins/mattermost-ai/admin/mcp/tools.
  2. Redeploy/upgrade the Agents plugin bundle (restart without config change).
  3. Without this fix the Playbooks server disappears permanently; with it, the registration is restored on activation.

Release Note

Fixed an issue where MCP tools registered by other plugins (such as Playbooks) were lost when the Agents plugin restarted due to an upgrade or crash, until the source plugin was restarted. Registrations are now persisted and restored across Agents plugin restarts.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Plugin server registrations now persist across Agent plugin restarts while the plugin remains enabled.
    • Existing plugin server settings can be updated without re-registering the server.
    • Restored registrations are automatically checked against current plugin availability.
  • Bug Fixes

    • Improved handling of unavailable, invalid, or removed plugin registrations.
    • Plugin tools now reconnect and remain available more reliably after restart or configuration changes.
  • Documentation

    • Updated plugin registration guidance to reflect persistent registrations.

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

github-actions Bot commented Aug 4, 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 literal text "heart_eyes_cat", not an actual cat emoji or a heart/love emoji.

2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide an actual list of bugs; it states inability to access a bug tracker and offers a template/table format for listing bugs if details are provided.

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 any actual bug descriptions; it states it cannot access the bug tracker and provides a template/request for details instead of describing 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 attribute any bugs to any specific user; it only provides a template and asks the user to supply reports. Therefore it does not meet the rubric requirement to attribute each bug to a user.

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 (saving without a color and the save button not doing anything) nor does it attribute it to @maria.nunez. It only states inability to access a bug tracker and asks for details.

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 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 asks for bug reports to be provided.

7. TestDirectMessageConversations/[openai]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation (docs.mattermost.com) but does not mention GitHub anywhere. Since the rubric requires both mentioning GitHub and referring to the documentation, it fails.

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 list of bugs; it states inability to access bug reports and suggests steps to find them, asking the user to paste details.

3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output does not describe any specific bugs; it states it lacks access to bug reports and suggests ways to find them. Therefore it does not include a description 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 (and cannot) attribute each bug to a user.

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 explains it cannot access bug reports and suggests steps to gather them, but it does not mention the specific bug (saving without a color / save button does nothing) nor attribute it to @maria.nunez.

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 channel banner bug being attributed to @maria.nunez (or any attribution at all); it only states lack of access and suggests steps to gather bug reports.

7. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection

  • Score: 0.00
  • 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 'mentions Github' requirement.

This comment was automatically generated by the eval CI pipeline.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Plugin server registrations now persist in KV storage, restore during client-manager construction, and reconcile with enabled plugins. Administrative updates use UpdatePluginServer without changing registration state.

Changes

Plugin registration persistence and updates

Layer / File(s) Summary
Persist and restore registrations
mcp/client_manager.go, mcp/client_manager_test.go
Registrations persist in KV storage, restore during construction, reconcile with plugin configuration, and support configuration-only updates. Tests cover persistence, hydration, pruning, retrieval, reconnection, and race-safe access.
Update plugin server configuration
api/api.go, api/api_admin.go, api/api_test.go, api/api_admin_test.go, external/pluginmcp/README.md
The admin update flow calls UpdatePluginServer. Mocks and tests validate update payloads, orphan handling, failure paths, and persistence. Registration documentation describes restart persistence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: Setup Cloud Test Server

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: persisting plugin MCP registrations and restoring them after an Agents restart.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/agents-mcp-registration-rehydrate-3648

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.

Caution

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

⚠️ Outside diff range comments (1)
mcp/client_manager.go (1)

468-493: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Release pluginServersMu before KV I/O, but preserve mutation order.

mutatePersistedPluginRegistrations performs KV reads, compare-and-set writes, and retries with delays. Holding pluginServersMu blocks registry readers during this work. Release it before persistence, but serialize each registration mutation from the in-memory update through the KV update. A separate KV lock alone can persist concurrent RegisterPluginServer and UnregisterPluginServer calls out of order. Apply the same rule to the prune write in hydratePluginRegistrations.

🤖 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 468 - 493, Update RegisterPluginServer,
UnregisterPluginServer, and UpdatePluginServer so pluginServersMu is released
immediately after the in-memory mutation, before
mutatePersistedPluginRegistrations performs KV I/O. Add or reuse a separate
serialization mechanism that covers each registration’s in-memory update through
its persistence mutation, preserving call order for concurrent
register/unregister operations. Apply the same locking and ordering rule to the
prune persistence write in hydratePluginRegistrations.
🧹 Nitpick comments (1)
mcp/client_manager.go (1)

568-585: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unreachable nil-map guard.

Line 570 creates a non-nil map. json.Unmarshal of a JSON null into a pointer to a non-nil map leaves the map unchanged, so registrations cannot be nil at line 576.

♻️ Proposed simplification
 		if len(oldValue) > 0 {
 			if err := json.Unmarshal(oldValue, &registrations); err != nil {
 				return nil, fmt.Errorf("unmarshal plugin MCP registrations: %w", err)
 			}
 		}
-		if registrations == nil {
-			registrations = make(map[string]PluginServerConfig)
-		}
 		update(registrations)
🤖 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 568 - 585, Remove the redundant nil-map
guard in mutatePersistedPluginRegistrations, including the second
make(map[string]PluginServerConfig) assignment, while preserving the initial map
creation and all existing update and persistence behavior.
🤖 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 `@mcp/client_manager.go`:
- Around line 468-493: Update RegisterPluginServer, UnregisterPluginServer, and
UpdatePluginServer so pluginServersMu is released immediately after the
in-memory mutation, before mutatePersistedPluginRegistrations performs KV I/O.
Add or reuse a separate serialization mechanism that covers each registration’s
in-memory update through its persistence mutation, preserving call order for
concurrent register/unregister operations. Apply the same locking and ordering
rule to the prune persistence write in hydratePluginRegistrations.

---

Nitpick comments:
In `@mcp/client_manager.go`:
- Around line 568-585: Remove the redundant nil-map guard in
mutatePersistedPluginRegistrations, including the second
make(map[string]PluginServerConfig) assignment, while preserving the initial map
creation and all existing update and persistence behavior.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: b6ef135a-a0b8-4dd4-99ba-7a9f2ef539b6

📥 Commits

Reviewing files that changed from the base of the PR and between f4e52d2 and 94de941.

📒 Files selected for processing (7)
  • api/api.go
  • api/api_admin.go
  • api/api_admin_test.go
  • api/api_test.go
  • external/pluginmcp/README.md
  • mcp/client_manager.go
  • mcp/client_manager_test.go

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>
@nickmisasi
nickmisasi marked this pull request as ready for review August 4, 2026 23:58
@nickmisasi
nickmisasi requested a review from crspeller August 5, 2026 00:03
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.

2 participants