Skip to content

feat(build): pin an MCP server's npx invocation to the plugin's own version #56

Description

@unional

Problem

A plugin whose MCP server runs its own npm package writes an unpinned invocation into mcpServers:

{ "command": "npx", "args": ["-y", "cyber-asana", "mcp"] }

Nothing pins that. A consumer installs plugin 0.10.0, gets 0.10.0's skills, and then npx resolves the latest published server. Skills and server drift apart silently, and the drift widens with every release the consumer does not reinstall.

This is the same defect class that makes unpinned invocations in skill prose a problem — an agent mid-workflow silently gets a different version than the instructions were written against.

Why the build should own it

  • The version is known at build time and nowhere else. A runtime placeholder cannot work: only ${PLUGIN_ROOT} and ${PLUGIN_DATA} expand in mcp.json, so a ${PLUGIN_VERSION} in args would arrive at the client literally.
  • There is already a precedent for exactly this shape. Hook casing is authored canonically and derived per vendor. "Author abstractly, stamp concretely at build" is the established pattern.
  • build already owns every derived manifest. It composes each vendor manifest from the canonical one plus harnesses.<vendor> overrides.
  • It replaces per-repo scripts. cyberuni/cyber-asana hand-maintains five manifests and textually rewrites the version line across them with a bespoke scripts/sync-plugin-version.mjs. That script cannot pin args, and every adopting repo would otherwise write its own.

Current state

mcpServers appears nowhere in packages/universal-plugin/src — the block rides through the build as opaque JSON. build special-cases only hooks.

plugin version moves the version through the manifests that carry a version field; it has no notion of an invocation embedded in args.

Proposed behaviour

At build, rewrite the package specifier in a mcpServers entry's args to <pkg>@<plugin version> when — and only when — the package is the plugin's own.

The build must not stamp its version onto an unrelated tool. npx -y some-other-cli is not the plugin, and guessing by name match is not sound for a plugin published under a different name than its server package. This needs an explicit marker in the canonical manifest, e.g.:

{
  "mcpServers": {
    "cyber-asana": {
      "command": "npx",
      "args": ["-y", "cyber-asana", "mcp"],
      "pinToPluginVersion": true
    }
  }
}

...with the marker stripped from every derived manifest, since it is a build directive and not part of any vendor's schema.

Open questions:

  • Should an already-pinned specifier be left alone, overwritten, or rejected as a conflict?
  • Does plugin doctor warn on an unpinned self-invocation that carries no marker?

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions