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 priorDefinitionHash linking 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:

VerdictMeaning
continuousthe new version references the pinned version's hash and bumps its semver — a legitimate upgrade
broken_chainthe new version does not reference the version it replaces
version_not_incrementedthe 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.