Research
Agent Plugins 1.0.0 Cannot Package an Authenticated MCP Server
· 7 min read
On this page
I spent half a day deciding whether to install an MCP server and ended up installing none. Agent Plugins 1.0.0 had shipped the day before — a package format for exactly the thing I had just turned down, with an initial Technical Steering Committee of Core Maintainers from Amazon, Cursor, Microsoft, OpenAI, and Vercel. So I tried to package the server I had rejected and found out I couldn’t, for a reason the specification states outright.
The server was X’s hosted MCP endpoint. It needs a bearer token. Under Agent Plugins 1.0.0 there is no conforming way to ship that.
Seven files, two schemas
The spec publishes JSON Schemas for both files a plugin can contain. I wrote seven of those files and validated them with jsonschema 4.19.2 — A, B, C, and C′ are plugin.json, D, E, and F are mcp.json. The results are the whole argument of this post.
| # | Change | Result |
|---|---|---|
| A | $schema + name only — no version, author, or license | Valid |
| B | author.name set to Cloudflare, Inc., version set to 9.9.9 | Valid |
| C | Adds a top-level signature field | Invalid |
| C′ | Puts the same signature under extensions in a reverse-domain namespace | Valid |
| D | Bearer token written into headers in plaintext | Valid |
| E | Authorization: Bearer ${X_BEARER_TOKEN} | Valid |
| F | Adds a custom secretRef field to the server entry | Invalid |
Cases A and B are what a manifest may omit. Cases C and F are what it may not add. C′ is where case C’s field is actually allowed to live, and it buys nothing. Cases D and E are the trap.
What the manifest requires
Two fields:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "trend-scan"
}
That is a complete, conforming plugin manifest. version, description, author, homepage, repository, license, keywords, and extensions are all optional. Nothing in the format ties a plugin to a person, a build, or a repository, and nothing has to be true. author is an object of name, email, and url — the shape is checked, the contents are not. Case B set author.name to a company I have no relationship with and the schema accepted it, because the spec deliberately declines to validate metadata semantically. It says clients must not reject a manifest merely because version isn’t valid SemVer, repository isn’t a recognized URL, or license isn’t an SPDX identifier.
Provenance has a place to sit and no one to read it
plugin.schema.json sets additionalProperties: false, so the top-level signature field in case C is a schema violation. But the manifest does have a slot for arbitrary data: extensions, an object keyed by reverse-domain namespace, contents unconstrained. Case C′ puts the same signature under com.p4ni and validates.
That is the part worth sitting with, because §8.1 says exactly what a client does with it — ignore entries for namespaces it does not implement, without validating the contents of their values. You can carry a signature. Nobody is obliged to look at it, and anyone who hasn’t implemented your namespace is obliged not to. Provenance under extensions is a private convention wearing a standard’s clothes.
The two rejections also cost different amounts. An unknown top-level manifest field is non-fatal: §5.2 has clients report it, ignore it, and keep loading the plugin. Case F is not — under §7.2.2 a server entry that fails the configuration rules must be skipped, so the whole server vanishes while the rest of the plugin loads fine.
The spec’s FUTURE_CONSIDERATIONS.md lists cryptographic signature verification and attestation chains linking a published plugin to its source repository and build as things a future version may define. Until then there is a place to put a signature and no obligation anywhere to check one.
The authentication gap
Remote MCP servers are configured in mcp.json:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"xapi": {
"type": "streamable-http",
"url": "https://api.x.com/mcp",
"headers": { "Authorization": "Bearer ..." }
}
}
}
Now stack up what the specification says about that headers object. Credentials are forbidden: header values are described as visible package data rather than a portable secret mechanism, and plugins must not embed secrets there. The same prohibition covers env for stdio servers. Placeholder expansion is forbidden too — clients must not perform environment-variable expansion in the URL, header names, or header values. The only placeholders that expand anywhere are ${PLUGIN_ROOT} and ${PLUGIN_DATA}, and they don’t apply to headers at all.
Then the spec closes the loop itself: Agent Plugins v1 defines no OAuth configuration or portable credential-reference fields, and treats authorization discovery, user interaction, and credential storage as client-managed.
So: don’t put the secret in, don’t reference it indirectly, and there is no field for pointing at it. Case F confirmed the last door is shut — invent a secretRef and additionalProperties: false rejects the server entry.
None of this is an oversight. FUTURE_CONSIDERATIONS.md opens its secrets section with “MCP servers often need credentials or API keys at runtime” and lists a secrets manifest field and client-mediated secret injection that avoids plaintext in config files among the things a future version may define. The hole is documented as a hole. That makes it a scoping decision rather than a mistake, and does nothing for the plugin you want to ship today.
The practical consequence is narrow but real. A plugin that bundles skills is fully portable. A plugin that bundles a local stdio MCP server needing no credentials is fully portable. A plugin that bundles an authenticated remote server is a configuration stub that every user must finish by hand, in whatever client-specific way their agent happens to offer. “Build once, run anywhere” holds until the server asks who you are.
Why case E is worse than case D
Case D — a real token committed in plaintext — at least fails loudly in review. Anyone reading the diff sees a credential.
Case E is the one that will cost people time. "Authorization": "Bearer ${X_BEARER_TOKEN}" validates cleanly against the schema, because it is a syntactically valid HTTP header value and the schema has no opinion about its contents. But no client is permitted to expand it. The literal seventeen characters ${X_BEARER_TOKEN} get sent to the origin. The server returns 401, and the spec classifies an authorization failure as a connection failure for that server rather than invalid plugin configuration — so the plugin itself loads fine and reports nothing wrong with the manifest.
A developer coming from .mcp.json in Claude Code, where ${VAR} interpolation does work, will write exactly this and get a silent failure with no schema error to guide them. It validates, it loads, it just doesn’t authenticate.
What the spec gets right
It would be unfair to read the above as sloppiness. The restrictions are consistent with a deliberately minimal contract, and several choices are stronger than what they replace.
The command field must be a single executable token, not a shell string, which removes an entire class of command injection at the format level. Bundled executables must use a plugin-relative ./ path. Component locations are fixed — skills/ and mcp.json, with no way to relocate them from the manifest and no precedence order to reason about — which means a reader can tell what a plugin contains by listing one directory. The containment rules restrict which package files a plugin may reference. And additionalProperties: false, the thing that blocked cases C and F, is what makes typo detection and strict validation possible in the first place.
The spec is also honest about its boundaries in a way vendor announcements usually aren’t. FUTURE_CONSIDERATIONS.md states plainly that v1.0.0 defines no trust model, no permission system, and no sandboxing requirements, then lists graduated trust levels, per-plugin capability restrictions, consent flows, secret injection, allowlists, and audit event schemas as open problems. It does not claim to have solved security. It claims to have standardized packaging and discovery, and that is what it did.
What this changes for me
I audited my own published skills for instruction-level attacks and found that Bandit, Semgrep, and Snyk Code catch none of them. I then measured how hard those instructions land when buried in bundled reference files — 21 compliances out of 30 runs on Haiku 4.5, zero out of 20 on Sonnet 5.
Agent Plugins doesn’t change that attack surface. skills/ holds SKILL.md files in the format the Agent Skills specification already defines, unchanged. What changes is distribution: those files now have a standard package around them, a single manifest to read, and maintainers from five of the companies shipping agents agreeing on where to look. The instructions land exactly as hard as before, and now they travel better.
That is not an argument against the standard. Packaging was a real problem and this solves it cleanly. But the checks that matter for a plugin you did not write — who published it, whether the bytes are what they published, and what it is allowed to touch once loaded — are the three things v1.0.0 explicitly leaves to clients. Until a future version fills those in, “it’s a conforming Agent Plugin” tells you where the files are. It tells you nothing about whether to trust them.
For now I’m still running zero third-party MCP servers. The format didn’t change my reasoning; it just gave me a tidier directory to not install.