Signed MCP tool definitions
Axtary pins every MCP tool definition by content hash, so a tool whose schema changes after approval is caught (tool poisoning). Signed tool definitions add the next layer: a publisher identity and an immutable version chain, so a definition is rejected unless a publisher you trust signed this exact version — and rejected before the tool is invoked.
This is the Axtary profile of the ETDI idea, expressed on the same ES256/JOSE keys used elsewhere in Axtary. It attests who published a definition and how this version relates to the last one; it does not claim a tool is safe and does not detect prompt injection.
The problem in plain terms
Your agent uses an MCP server that offers a refund_customer tool. You read its
description, decide it's fine, and let your agent call it. Tool poisoning is
when that definition quietly changes afterward — the same tool name now has a
new description or schema that nudges the agent into doing something you never
approved. A "rug pull" is the version of this where a previously-trusted
publisher ships a malicious update.
Axtary already defends the first case: it pins each tool by a content hash, so if the live definition no longer matches the hash you approved, it's caught. Signed definitions add two things pinning alone can't give you:
- Who published this? A signature ties the definition to a publisher identity whose key you explicitly trust — an unknown signer is rejected, with no trust-on-first-use.
- Is this a legitimate next version? Each signed version links to the one it replaces and must bump its semver, so a silent re-definition either breaks the chain or fails to advance — and gets surfaced for re-review instead of being accepted.
The rest of this page shows how a publisher signs a definition, how you choose which publishers to trust, and how Axtary verifies the exact definition before the tool is ever invoked.
What a signature commits to
A publisher signs a compact JWS over the exact tool definition. The signed claims include:
- the canonical
definitionHash(the same hash Axtary already pins), - a semantic version (
semver), - the required scopes the tool declares,
- a
priorDefinitionHashlinking this version to the one it supersedes.
Because the signature commits to the definitionHash, a later byte-level change
to the definition cannot be presented under the same signature.
Step 1 — A publisher signs a definition
A tool publisher creates an ES256 keyring and signs its definitions. Each signature is a portable statement you can distribute next to the tool:
import {
createMcpToolDefinition,
signMcpToolDefinition,
loadOrCreateMcpPublisherKeyring,
mcpPublisherSigningKey,
mcpPublisherJwks,
} from "@axtary/mcp";
const keyring = await loadOrCreateMcpPublisherKeyring({
filePath: ".axtary/publisher-keyring.json",
publisher: "mcp-publisher://acme",
});
const signing = mcpPublisherSigningKey(keyring);
const { statement } = await signMcpToolDefinition({
definition: {
serverIdentity: "mcp://internal/payments",
schemaVersion: "2026-03-26",
name: "refund_customer",
description: "Issues a refund for an order.",
inputSchema: { type: "object", properties: { orderId: { type: "string" } } },
},
publisher: "mcp-publisher://acme",
semver: "1.2.0",
requiredScopes: ["payments:refund"],
signingKey: signing.privateKey,
keyId: signing.kid,
});
// Distribute `statement` with the tool, and the public JWKS for verifiers:
const publicJwks = mcpPublisherJwks(keyring);
The private key stays with the publisher (written 0600); only the statement
and the public JWKS are distributed.
Step 2 — A verifier trusts the publisher's keys
On the Axtary side you decide which publishers to trust by registering their public keys. There is no trust-on-first-use for signatures — a key you have not registered is rejected.
The CLI persists public keys only in an owner-readable local trust store:
axtary mcp trust-publisher \
--publisher mcp-publisher://acme \
--jwks ./acme-publisher.jwks.json
axtary mcp publishers
The trust store contains public JWKs and publisher identities, never publisher private keys.
import { emptyMcpPublisherTrustStore, trustMcpPublisher } from "@axtary/mcp";
const trust = trustMcpPublisher(
emptyMcpPublisherTrustStore(),
"mcp-publisher://acme",
publicJwks.keys
);
Step 3 — Verify the exact definition
You can verify one publisher statement directly:
axtary mcp verify-definition \
--definition ./refund-customer.definition.json \
--statement ./refund-customer.statement.jwt \
--publisher mcp-publisher://acme
For live server review, place statements in the local signed-definition registry:
{
"schemaVersion": "axtary.mcp_signed_definition_registry.v0",
"statements": [
{
"serverIdentity": "mcp://internal/payments",
"toolName": "refund_customer",
"publisher": "mcp-publisher://acme",
"statement": "<compact JWS>"
}
]
}
Then review the server against its persisted pins and publisher trust:
axtary mcp review \
--wrap-url https://payments.example/mcp \
--signed-definitions .axtary/mcp-signed-definitions.json
The review displays continuous, broken_chain, or
version_not_incremented from the signed candidate and existing pin. An
unknown key displays untrusted and cannot be accepted. --accept is an
explicit operator decision; verified publisher/version evidence is persisted
with the new pin.
Step 4 — Fail closed before invoke
A signed handler verifies the statement against the live definition before the tool runs. Any of these reject and the tool is never invoked:
- the statement is unsigned, malformed, or has the wrong type,
- the signing key is not in your trust store,
- the signature does not verify,
- the live definition no longer hashes to the signed
definitionHash(it was altered after signing).
import { createSignedMcpToolHandler, mcpPublisherKeyResolver } from "@axtary/mcp";
const handler = createSignedMcpToolHandler({
definition,
invoke: callUpstreamTool,
statement,
verificationKeys: mcpPublisherKeyResolver(trust),
expectedPublisher: "mcp-publisher://acme",
});
// `handler` verifies the signature, then runs only if it passes.
The verified publisher, semver, and version link are bound into the action's
toolDefinition, so they ride into the ActionPass and the ledger and are
independently verifiable from the ledger alone.
Step 5 — The version chain (rug-pull defense)
When a publisher ships a new version, Axtary checks it against the version you have pinned:
| Verdict | Meaning |
|---|---|
continuous | the new version references the pinned version's hash and bumps its semver — a legitimate upgrade |
broken_chain | the new version does not reference the version it replaces |
version_not_incremented | the chain links but the semver did not advance |
A silent re-definition therefore cannot be laundered: it either breaks the chain or fails to advance the version, and is surfaced for human re-review rather than accepted.
The connectors dashboard reads the publisher, key id, semver, and chain verdict from the local conformance receipt. It does not create trust or accept a key; publisher trust remains an explicit local CLI operation.
FAQ
How is this different from pinning? Pinning catches a definition that changed after you approved it. Signing also tells you who published it (a key you trust) and proves a new version legitimately supersedes the last — so a trusted-publisher rug pull is caught, not just an anonymous edit.
What happens to an unsigned or unknown-publisher tool? It's rejected before the tool runs. There is no trust-on-first-use for signatures: a key you haven't registered is not accepted.
Does a valid signature mean the tool is safe to run? No. It proves authorship and version lineage only. It does not vet what the tool does and does not detect prompt injection — it bounds who and which version, not intent.
Limits (by design)
- ES256/JOSE only; no post-quantum suites.
- A signature attests who published a definition and its version lineage — it does not prove the tool is safe and does not detect prompt injection.
- Trust is exactly the publisher keys you configure. Axtary does not verify a publisher's legal or organizational identity.
For the exact contract, reason codes, and a conformance vector, see the signed tool-definition profile.