Skip to content

fix(detector): stop counting plugin catalog templates as MCP servers - #206

Open
swarit-stepsecurity wants to merge 3 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/mcp-plugin-catalog
Open

fix(detector): stop counting plugin catalog templates as MCP servers#206
swarit-stepsecurity wants to merge 3 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/mcp-plugin-catalog

Conversation

@swarit-stepsecurity

Copy link
Copy Markdown
Member

Fixes #201.

The MCP walk matches on basename anywhere under $HOME, and an agent plugin marketplace is a clone of a catalog repo where every entry ships a template .mcp.json. On my machine that turned 7 real configs into 53: 16 from ~/.claude/plugins/marketplaces, 30 from ~/.codex/.tmp/plugins (not in the issue), none installed and none loaded by any agent. In enterprise mode 36 of them carry an mcpServers block, so the backend recorded stripe, slack, gmail, notion, datadog and friends as servers on the device — the fleet inventory and mcp_servers_count were inflated, not just the pretty summary.

Every plugin package is marked by a .claude-plugin / .codex-plugin manifest at its root, so a hit inside one can be classified instead of guessed at:

  • the package's own .mcp.json, package installed (both agents install under plugins/cache) → kept as claude_plugin / codex_plugin
  • catalog entry, or an MCP-shaped file vendored elsewhere in a payload → dropped. Claude only loads .mcp.json from a plugin, so a plugin repo's own opencode.json (the ponytail/* hits in the issue) is not a live config either.
  • no manifest above it → unchanged discovered_mcp

Preferred manifest detection over a list of catalog paths so this holds for agents whose plugin dirs we haven't seen yet.

Scanned before/after on the same machine: 53 → 7, exactly the seven real configs. Configs outside a plugin package are untouched. An installed-but-disabled plugin is still reported; gating on enabledPlugins is a separate call if we want it.

The MCP walk matches on basename anywhere under $HOME, and an agent plugin
marketplace is a clone of a catalog repo where every entry ships a template
.mcp.json. On this machine that turned 7 real configs into 53: 16 from
~/.claude/plugins/marketplaces and 30 from ~/.codex/.tmp/plugins, none
installed, none loaded by any agent. In enterprise mode 36 of them carried an
mcpServers block, so the backend recorded stripe, slack, gmail, notion and
friends as servers on the device.

Every plugin package is marked by a .claude-plugin/.codex-plugin manifest at
its root, so a walked hit inside one can be classified rather than guessed at:
keep the package's own .mcp.json when the package is installed (both agents
install under plugins/cache), drop catalog entries and drop MCP-shaped files
vendored elsewhere in a payload — Claude only ever loads .mcp.json from a
plugin, so a plugin repo's own opencode.json is not a live config either.

Configs outside a plugin package are untouched.

Fixes step-security#201

Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>

Copilot AI 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.

Pull request overview

This PR fixes inflated MCP server discovery by distinguishing real MCP configs from agent plugin marketplace/catalog templates during the $HOME walk, preventing non-installed plugin templates from being counted/reported as MCP servers.

Changes:

  • Added plugin-package classification for walked MCP config hits, dropping marketplace templates and vendored MCP-shaped files inside plugin payloads.
  • Introduced manifest-based plugin root detection for Claude/Codex plugins and treated installed plugin .mcp.json as separate sources (claude_plugin / codex_plugin).
  • Added tests covering catalog-vs-installed plugin behavior and bounded manifest lookup.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
internal/detector/mcp_plugins.go Adds plugin manifest/root detection and classification logic to drop marketplace templates and keep installed plugin configs.
internal/detector/mcp_plugins_test.go Adds unit tests validating plugin classification behavior for Claude/Codex and walk-root bounding.
internal/detector/mcp_discovery.go Routes walked MCP hits through the new classifier before appending discovered config specs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +56 to +60
for i := 0; i < maxPluginRootLookup; i++ {
for _, m := range mcpPluginManifests {
if info, err := os.Stat(filepath.Join(dir, m.dir, "plugin.json")); err == nil && !info.IsDir() {
return dir, m, true
}
Comment on lines +79 to +83
if runtime.GOOS == "windows" {
p = strings.ToLower(p)
}
sep := string(filepath.Separator)
return strings.Contains(p, sep+"plugins"+sep+"cache"+sep)
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.

MCP count inflated by marketplace catalog templates (discovered_mcp)

3 participants