Repository navigation
feat: run plugin MCP servers discovered by agentcontext - #29326
jscottmiller wants to merge 1 commit into
Conversation
4a5f2c5 to
d8f7100
Compare
d8f7100 to
7d71863
Compare
UAT run on this tip (
|
7d71863 to
bf74037
Compare
bf74037 to
311b7fd
Compare
311b7fd to
3511ebb
Compare
e7dca4c to
7e655a8
Compare
7e655a8 to
250b756
Compare
250b756 to
5ea3704
Compare
c279941 to
f9e9502
Compare
f9e9502 to
4951489
Compare
4951489 to
a5c91c5
Compare
a5c91c5 to
9461fb6
Compare
|
/coder-agents-review |
|
Chat: Review posted | View chat Review historydeep-review v0.13.0 | Round 4 | Last posted: Round 4, 50 findings (3 P2, 19 P3, 20 P4, 8 Nit), COMMENT. Review Finding inventoryFindings
Contested and acknowledgedCRF-6 (P2, agent/agent.go:555) - plugin command created after mcp.json stays failed
CRF-50 (P4, agent/x/agentmcp/manager.go:659) - plugin server ID and legacy-collision rule in two packages
CRF-54 (P4, agent/agentcontext/mcp.go:92) - legacy-collision warning hides the plugin row's own error
Round logRound 1Round type: round 1 (full panel). Netero: 1 P2, 2 P3, 1 Note; no P0, so the panel ran. Law not run (517 effective additions). Panel of 13: specialists ging-go, takumi, ryosuke, kurapika, melody, luffy, killua (7 triggered: Go files, goroutines and shutdown, Go package boundaries, path containment and subprocess launch, paired source lists and wire format, new feature, reload resource lifecycle); core pariston, mafuuu; pool bisky (tests changed), gon, leorio (once-per-PR floor); wildcard meruem (shuf). Size target 9-12 for security-sensitive code exceeded by one because the 7 triggered specialists plus core, Bisky, and the Gon/Leorio floor reach 12 before the wildcard. Cross-check decisions:
Round 2 updateBLOCKED. Churn guard: 28 of 29 open findings addressed in 22cc66d; CRF-6 (P2) deferred by the author ("a follow-up ticket is being filed") with no ticket linked and no stated reason the failed-server state is acceptable. No reviewers, no Netero. Status cells updated from churn-guard classifications; fixes are author claims, not panel-verified. Reviewed against 9461fb6..22cc66d. Round 3Churn guard PROCEED: CRF-6 deferred to CODAGT-1391 with explicit acceptance. Round type: restructure (exported API changed: ReloadFrom removed; SetSources, ReloadCurrent, PluginSource added; PluginInfo.HasMCPConfig removed), so full panel. Netero (advisory, post-panel): CRF-46 to CRF-51. Law not run (783 effective additions). Panel of 13: same 7 triggered specialists as R1 (ging-go, takumi, ryosuke, kurapika, melody, luffy, killua), core pariston and mafuuu, bisky, gon and leorio (mandatory on restructure), wildcard razor (shuf). Exceeds 12 by one for the same reason as R1. Verified fixed by reviewers: CRF-1 addition half (Netero mutation), CRF-5 (Takumi, Pariston, Mafuuu), CRF-7, CRF-12, CRF-17 (Bisky), CRF-8, CRF-13 (Luffy), CRF-9, CRF-18 (Leorio), CRF-11 (Netero), CRF-14 (Mafuuu, Gon), CRF-16, CRF-20, CRF-21, CRF-22, CRF-23, CRF-26, CRF-27, CRF-30 (Gon), CRF-24, CRF-25 (Ging-Go), CRF-2 (Netero, code removed). CRF-3 code fixed, no test (CRF-46). Cross-check decisions:
Round 4 updateBLOCKED. Branch rebased (base 1cb2cc2 to f76e6bf, head d731736). Churn guard: 15 addressed, 2 acknowledged without ticket (CRF-50, CRF-54), 1 deferred (CRF-6), 6 silent: CRF-15, CRF-29, CRF-52, CRF-53 (R3 replies on threads whose unresolve failed with "Resource not accessible by integration", so the threads still show resolved) and CRF-64, CRF-65 (folded into the R3 review body). Orchestrator confirmed CRF-52, CRF-53, CRF-64 unchanged in code. No reviewers, no Netero. Human approval from ethanndickson on the PR. Reviewed against f76e6bf..d731736. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
This PR starts and stops plugin MCP servers from the agentcontext plugin snapshot and attributes their resources to the plugin. Findings: 3 P2, 12 P3, 7 P4, 7 Nit.
Out of scope (needs a ticket or explicit acceptance by a human):
coderd/agentapi/context.go:483: coderd requires a plugin MCP server source of<plugin>/<server>, and legacy server names may contain/, so plugin and legacy sources can collide; a separator that cannot collide needs a coderd change.agent/agentcontext/mcp.go:193: a legacy.mcp.jsonserver name over 1024 bytes makes coderd reject the whole context push; this existed before the PR.agent/x/agentmcp/manager.go:922: the raw ListTools error is stored and shown to users; for HTTP servers it may include the request URL with query-string credentials (unverified).agent/agentcontext/plugin.go:478: an escapingplugin.jsonsymlink is also reported as escaping a "scan root" that is the plugin root.
Notes:
- Plugin
mcp.jsonstdio commands under<working dir>/plugins/*and<working dir>/.agents/plugins/*now start automatically when the workspace supports plugins. This is the trust level the working-directory.mcp.jsonalready has. - A plugin
mcp.jsonthat fails to parse appears as a KindMCPServer resource namedmcp.jsonwith source<plugin>/mcp.json; #30577 has to show it as a file error. Manager.Reload(paths)has no production caller after this PR.TestReloadSources_ConcurrentCallerAppliesItsSourcesdoes not force the secondReloadto join the in-flight body beforeclose(release). It failed 20 of 20 runs against the basemanager.go, but under heavy scheduler load it can pass against broken code.
🤖 This review was automatically generated with Coder Agents.
8fa7b32 to
22cc66d
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Re-review of 22cc66d. The author reports fixes for 28 of the 29 round 1 findings; these are not verified yet, because this round ran no reviewers.
The finding below is deferred without a ticket, so further review is blocked until it has a response:
agent/agent.go:555, P2: a plugin whose./-relative command or cwd is created after itsmcp.jsonkeeps a failed server untilmcp.jsonchanges or the agent restarts. The reply says a follow-up ticket is being filed, but no ticket is linked, and the reply does not say why the stuck failed server is acceptable. This needs a human decision: link the ticket in that thread, or state explicitly that the gap is accepted.
The out-of-scope items and notes from round 1 are unchanged and do not block.
🤖 This review was automatically generated with Coder Agents.
|
/coder-agents-review |
22cc66d to
55133e9
Compare
There was a problem hiding this comment.
Re-review of 22cc66d. This round: 7 P3, 13 P4, 1 Nit, and two prior findings re-raised on their threads.
Reviewers verified these fixes: the watcher now uses the registered source function; the plugin-set guard is gone; the legacy-name collision no longer starts the plugin server; the collision warning text; the attempt-cap error; the per-attempt source test; the orphaned-server test; PluginSource; the removed HasMCPConfig layer; and the round 1 comment and naming fixes, apart from the two re-raised on their threads. The ./-command retry is deferred to CODAGT-1391 with the gap accepted. Two fixes introduced new problems: the 1024-byte source fix and the removal test (replies on those threads).
Out of scope (needs a ticket or explicit acceptance by a human):
agent/x/agentmcp/manager.go:457:SnapshotChangedignores the order of legacy.mcp.jsonsources, so reorderingMCPConfigFileswithout a file change keeps the old first-wins winner for a shared legacy name until a file changes or the agent restarts.- The four round 1 out-of-scope items are unchanged.
agent/x/agentmcp/manager.go:367
P3 [CRF-48] The handleWatchedConfigChange doc still says it reloads "using the most recently observed path set" through "a singleflight Reload", but it now calls reloadFrom with the function registered by SetSources. (Netero)
A maintainer reading it will expect the watcher to reuse the last reload's list, which is the stale-list behavior the round 1 fix removed. The lastSources field comment at line 116 also says it holds "sources passed to ReloadSources", while line 347 records the sources of every reload. Update both comments.
🤖
agent/x/agentmcp/manager.go:396
P4 [CRF-64] A failed watcher reload logs watched reload failed, while the two agent triggers now log failed to reload MCP servers. (Leorio)
All three call reloadFrom and can return the same new attempt-cap error, so an operator who greps for failed to reload MCP servers misses failures from config file edits. Use failed to reload MCP servers here too; the logger already carries trigger=fsnotify (line 386).
🤖
agent/x/agentmcp/manager.go:192
P4 [CRF-65] The ReloadSources doc says it waits for one reconnect and does not mention the attempt-cap error, and ReloadCurrent refers to it. (Leorio)
Only the unexported reloadFrom doc says a call can reload up to maxReloadAttempts times and then return an error while the Manager is still open. Add that to the ReloadSources doc, including that servers from the last completed reload keep running.
🤖
🤖 This review was automatically generated with Coder Agents.
55133e9 to
1d40c18
Compare
ethanndickson
left a comment
There was a problem hiding this comment.
pr lgtm in relative isolation (i havent spent much time on the rest of the stack) but maybe a $comment-cleanup is due
|
@ethanndickson Ran a comment-cleanup pass across the whole stack; see the CRF-62 thread. (Coder Agents generated) |
|
/coder-agents-review |
There was a problem hiding this comment.
Re-review of d731736. The author reports fixes for 15 of the round 3 findings. They are not verified yet, because this round ran no reviewers.
Further review is blocked until each item below has a fix or a reply. The first four were replies on review threads that the reviewer could not reopen, so those threads still show as resolved. Expand the resolved threads to find them.
agent/agentcontext/mcp.go:111, P3: a plugin server whose<plugin>/<server>is over 1024 bytes is still started, and only its resource is dropped. agentmcp has no length check, so the command runs while chatd never receives its tools. Reject the entry inparseAndDedup, next to the legacy-name check. Reply on the thread for the 1024-byte source finding.agent/agent_context_test.go:281, P3: the removal step ofTestAgent_PluginMCPServersFollowPluginChangesstill callsos.RemoveAll(pluginDir)and checks only the push. It passes whenmcpConfigSourceskeeps returning the removed plugin. Delete onlyplugin.jsonand check that the server stopped, not only the pushed resources. Reply on the thread about testing plugin changes after startup.agent/agentcontext/mcp_internal_test.go:160, P3 follow-up: the legacy test server is still namedworkspace.agent/agent_context_test.go:159and the doc onTestReloadCurrent_ReadsSourcesOnEachAttemptinreload_internal_test.go, Nit follow-up: both comments still restate the code.agent/x/agentmcp/manager.go:370, P4 (round 3 review body): a failed watcher reload still logswatched reload failed, while the agent triggers logfailed to reload MCP servers.ReloadCurrentdoc inagent/x/agentmcp/manager.go, P4 (round 3 review body): it still does not say that the call can reload up tomaxReloadAttemptstimes and then return an error.
Accepted without a ticket, so these need a human decision (file a ticket or explicitly accept the gap):
agent/x/agentmcp/manager.go:632: the<plugin>/<server>ID and the legacy-collision rule are implemented in both agentmcp and agentcontext, and no test ties them together.agent/agentcontext/mcp.go:87: the legacy-collision warning replaces a plugin row's own error, such as an invalid entry, whatever caused that row.
The out-of-scope items from rounds 1 and 3 are unchanged and do not block.
🤖 This review was automatically generated with Coder Agents.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
nickvigilante
left a comment
There was a problem hiding this comment.
Approving the docs in docs/ai-coder/agents/extending-agents.md; I did not review the agent code.
I checked the "Plugin MCP servers" section against the agent code at c3570ad.
The claims match the implementation, and markdownlint and Vale are clean.
The inline comments are non-blocking.
The main gaps are the plugin mcp.json rules that differ from .mcp.json, the start and stop behavior without a workspace restart, and the known limitation for late-appearing ./ commands, which matters now that agent-plugins is a safe experiment.
The remaining comments are wording and link nits.
🤖 Review generated with Coder Agents on behalf of @nickvigilante.
| A plugin declares MCP servers in an `mcp.json` file at its root, with `"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"`. | ||
| Each server requires `type`, set to `stdio`, `streamable-http`, or `sse`. | ||
| A stdio server runs with `PLUGIN_ROOT` set to the plugin directory and `PLUGIN_DATA` set to a data directory for the plugin under `~/.coder/plugin-data`. | ||
| The agent expands `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` in `args`, `env` values, and `cwd`. |
There was a problem hiding this comment.
This section tells readers how to declare servers but leaves out the rules that make a plugin mcp.json stricter than .mcp.json. A reader copying the .mcp.json example above will hit these errors without explanation:
- The top level must contain exactly
$schemaandmcpServers, or the whole file is rejected (agent/x/agentmcp/plugin.go:52). typeisn't inferred fromcommandorurl, and"http"is rejected in favor ofstreamable-http(plugin.go:174-181). Line 339 listshttpas valid for.mcp.json, so the contrast is worth stating.commandmust be a bare name onPATHor a./-relative path inside the plugin (plugin.go:257).cwddefaults to the plugin root, not the workspace working directory, and must start with./,${PLUGIN_ROOT}, or${PLUGIN_DATA}(plugin.go:303-323).urlmust behttps, orhttpon a loopback host (plugin.go:459).envcan't setPLUGIN_ROOTorPLUGIN_DATA, and fields from anothertypeare rejected (plugin.go:98-100,plugin.go:239).- An invalid entry is skipped while its siblings still load, unlike
.mcp.json, which fails as a whole.
A short list or a minimal example mcp.json would cover most of this.
| Setting `CODER_AGENT_EXP_MCP_CONFIG_FILES` to a non-empty value, including `.mcp.json`, turns off plugin MCP servers. | ||
| A template that points it at a file outside the project, as in [network isolation](./platform-controls/agent-isolation.md), also keeps plugins in the project from starting servers. | ||
| Plugins in `~/.coder` or in a declared source contribute skills only, even when the declared source is inside the working directory. | ||
| If the template doesn't set `dir` on the `coder_agent` resource, the workspace has no working directory, so no plugin starts MCP servers. |
There was a problem hiding this comment.
Two gaps after this paragraph:
- Live start and stop. The agent starts and stops plugin servers as plugins and their
mcp.jsonfiles appear or disappear, without a workspace restart. "How discovery works" says this only for.mcp.json. One sentence here, possibly pointing to the existing out-of-date-chat note, would cover it. - Known limitation. Per the PR description, a server whose
./command file appears after itsmcp.jsonisn't retried until a config or snapshot change. Since this PR addsagent-pluginstoExperimentsSafe, the content guidelines call for documenting known limitations. That change also means--experiments='*'turns the feature on, which "Enable the experiment" could mention.
| If the template doesn't set `dir` on the `coder_agent` resource, the workspace has no working directory, so no plugin starts MCP servers. | ||
|
|
||
| Plugin servers and `.mcp.json` servers share one set of server names, which prefix their [tool names](#tool-naming). | ||
| A plugin server doesn't start when a `.mcp.json` server or another plugin's server already uses its name, and its context entry shows the conflict. |
There was a problem hiding this comment.
As written, "already" doesn't say who wins. parseAndDedup (manager.go:638-672) gives .mcp.json servers priority over every plugin server, and between plugins the first to claim the name (resolver name-claim order) wins. This also uses "context list" to match line 411 instead of introducing "context entry".
| A plugin server doesn't start when a `.mcp.json` server or another plugin's server already uses its name, and its context entry shows the conflict. | |
| A plugin server doesn't start when another server already uses its name, and it appears in the chat's context list with the conflict. | |
| A `.mcp.json` server always keeps its name over a plugin server, and between plugins, the first plugin the agent discovers keeps the name. |
| - `CODER_AGENT_EXP_MCP_CONFIG_FILES` is not set on the agent. | ||
|
|
||
| Setting `CODER_AGENT_EXP_MCP_CONFIG_FILES` to a non-empty value, including `.mcp.json`, turns off plugin MCP servers. | ||
| A template that points it at a file outside the project, as in [network isolation](./platform-controls/agent-isolation.md), also keeps plugins in the project from starting servers. |
There was a problem hiding this comment.
Nit: the env var is set in the "1. Configure the template" section, so a deep link lands readers on it.
| A template that points it at a file outside the project, as in [network isolation](./platform-controls/agent-isolation.md), also keeps plugins in the project from starting servers. | |
| A template that points it at a file outside the project, as in [Network isolation](./platform-controls/agent-isolation.md#1-configure-the-template), also keeps plugins in the project from starting servers. |
| - The plugin is the workspace working directory, or is in its `plugins/` or `.agents/plugins/` directory. | ||
| - `CODER_AGENT_EXP_MCP_CONFIG_FILES` is not set on the agent. | ||
|
|
||
| Setting `CODER_AGENT_EXP_MCP_CONFIG_FILES` to a non-empty value, including `.mcp.json`, turns off plugin MCP servers. |
There was a problem hiding this comment.
Nit: the second bullet and line 432 say nearly the same thing, and "The plugin is the workspace working directory" reads oddly. Possible tightening:
| - The plugin is the workspace working directory, or is in its `plugins/` or `.agents/plugins/` directory. | |
| - `CODER_AGENT_EXP_MCP_CONFIG_FILES` is not set on the agent. | |
| Setting `CODER_AGENT_EXP_MCP_CONFIG_FILES` to a non-empty value, including `.mcp.json`, turns off plugin MCP servers. | |
| - The plugin is the working directory itself, or is in its `plugins/` or `.agents/plugins/` directory. | |
| - `CODER_AGENT_EXP_MCP_CONFIG_FILES` is unset on the agent. | |
| Setting `CODER_AGENT_EXP_MCP_CONFIG_FILES` to any non-empty value, even `.mcp.json`, turns off plugin MCP servers. |
The agent now feeds the mcp.json of each valid plugin discovered at the working directory (the directory itself, plugins/*, and .agents/plugins/*) into the MCP engine, but only while CODER_AGENT_EXP_MCP_CONFIG_FILES is unset. Setting it, even to .mcp.json, starts no plugin servers; agentcontextconfig.Config.MCPConfigFilesSet records whether it was set. Plugins found through other scan roots, including declared sources nested under the working directory, start no servers. mcpConfigSources composes the env-configured .mcp.json files followed by one plugin-scoped source per plugin, built by agentmcp.PluginSource with PLUGIN_DATA under ~/.coder/plugin-data. A plugin without an mcp.json contributes no servers, and the engine's config watcher picks the file up when it appears. When the home directory or a plugin's data directory cannot be determined, the plugin is still passed as a ConfigSource with LoadError set, which an existing mcp.json reports on the plugin's <plugin>/mcp.json resource; only the first such failure logs at warn level. LoadError is kept out of PluginScope, so it is not part of server identity; the file snapshot records it, and a change to it reloads. Plugin sources come only from the context snapshot, so no plugin server starts while Manifest.AgentPluginsEnabled is off. The agent registers mcpConfigSources with Manager.SetSources, and the startup reload, a context snapshot subscriber, and config watcher reloads all compute the current sources from it. ReloadCurrent is the only reload entry point besides Reconnect: Reload, ReloadSources, and the watcher's last-sources fallback are removed. Reconnect, which a context resync calls to retry failed servers, reloads the current sources and does nothing until the first reload settles. ReloadCurrent calls the function on every attempt and re-checks the snapshot after joining a concurrent reload, so one trigger cannot drop another's servers. It returns an error if the sources keep changing after three reloads. The subscriber reloads on every snapshot change once the first resolve has finished; a reload with unchanged sources is a no-op. The subscription is taken before the context manager's Run loop starts, and the current snapshot is examined before the first wait. Snapshot.Plugins() returns the valid plugins in the order the resolver claimed their names, so the first plugin to claim a server name wins deterministically. Snapshot.PluginsAt keeps the ones a scan root at a given directory discovers. The engine's snapshot records source order, so a reorder alone triggers a reload. MCPServerStatus gains PluginName, which buildMCPServerResources copies onto the resource. MCP server resources declared by a plugin carry the plugin name and use <plugin>/<server> as their source, so a plugin entry that lost a name collision with a legacy server still appears with its error. A plugin server whose <plugin>/<server> ID equals a legacy server name is not started, and the legacy server's resource carries a warning naming it. A plugin server whose source exceeds the 1024-byte source limit is left out and counted on an unreadable <plugin>/mcp.json resource, or on the plugin's server named mcp.json when there is one. agent-plugins joins ExperimentsSafe, so --experiments='*' enables it. The extending-agents docs gain a Plugin MCP servers section covering the plugin mcp.json, when the agent starts plugin servers, name conflicts with .mcp.json servers, and repository trust.
tl;dr: The agent runs the MCP servers of plugins in the workspace working directory, and starts and stops them as plugins appear and disappear. The frontend follows in #30577.
Plugin servers start only for plugins discovered at the working directory (the directory itself,
plugins/*, and.agents/plugins/*), and only whileCODER_AGENT_EXP_MCP_CONFIG_FILESis unset. Setting it, even to.mcp.json, starts none, so a template that points it outside the project, as the network isolation example does, also keeps plugins in the project from starting servers. The rule is path based, so plugins at other paths, such as~/.coder, contribute skills only, as does every plugin when the template sets no agentdir.agent-pluginsjoinsExperimentsSafe, so--experiments='*'enables it, and the extending-agents docs describe plugin MCP servers, theirmcp.jsonfields, thePLUGIN_DATAdirectory, this rule, and repository trust.mcpConfigSourcesbuilds the MCP source list from the env-configured.mcp.jsonfiles followed by one plugin-scopedmcp.jsonsource per such plugin (Snapshot.PluginsAt). The agent registers it withManager.SetSources, so the startup reload, a snapshot subscriber, the engine's config watcher, andReconnecton a context resync all compute the current list from one place.ReloadCurrentre-reads the list on every attempt, so concurrent reloads cannot drop each other's servers, and it returns an error if the list is still changing after three reloads. The subscriber reloads on every snapshot change; reloads with unchanged sources are no-ops. Plugins are ordered by name claim, so name collisions resolve deterministically.Plugin MCP server resources carry the plugin name and a
<plugin>/<server>source, so a server that lost a name collision still appears with its error. A plugin server whose<plugin>/<server>equals a legacy server name is not started, and the legacy resource names it in a warning; plugin servers whose source exceeds the 1024-byte limit are counted on a<plugin>/mcp.jsonrow instead of failing the push. When the home directory or a plugin's data directory cannot be determined, an existing pluginmcp.jsonshows that error on its<plugin>/mcp.jsonrow instead of loading. A plugin server whose./command appears after itsmcp.jsonis not retried until a config or snapshot change. Agent tests cover adding a plugin, then itsmcp.json, and removing it after startup, plus the working-directory, config-file, and experiment rules.PR map
scott/agent-plugins/6-agentmcp-plugins..a9f87cef08agent/x/agentmcp/manager.go(+119/-66):SetSourcesandReloadCurrent, which replaceReloadandReloadSources, re-read the registered sources on every attempt and error aftermaxReloadAttempts. The snapshot records each source's position andLoadError, so a plugin reorder or a changed load error reloads.parseAndDedupdoes not start a plugin server whose ID equals a legacy name.Reconnectreloads the registered sources and does nothing until the first reload settles.agent/agentcontext/mcp.go(+113/-11):buildMCPServerResourcesuses<plugin>/<server>sources and copiesPluginName. It omits a plugin server whose ID equals a legacy server name, with a warning on the legacy row, and counts plugin servers whose source is over 1024 bytes on a<plugin>/mcp.jsonrow.agent/agent.go(+86/-4, concurrency):mcpConfigSourcesadds one source per plugin at the working directory, and none when the MCP config file list was set explicitly or the manifest has no directory. A tracked goroutine,runPluginMCPSync, subscribes to snapshot changes beforeRunstarts and callsReloadCurrent.agent/agentcontext/plugin.go(+54/-0):Snapshot.Plugins()returns OK plugins in name-claim order, andSnapshot.PluginsAt(dir)keeps those a scan root atdirdiscovers.agent/agentcontextconfig/api.go(+16/-5):Config.MCPConfigFilesSet, set byReadEnvConfigwhenCODER_AGENT_EXP_MCP_CONFIG_FILESis non-empty, andAPI.MCPConfigFilesSet().agent/x/agentmcp/config.go,plugindata.go(+26/-3):ConfigSource.LoadError, reported for an existing pluginmcp.jsoninstead of loading it, andPluginSource.codersdk/deployment.go(+1, API):agent-pluginsjoinsExperimentsSafe.This PR was generated by Coder Agents on behalf of @jscottmiller.