Skip to content

Qualifying one third-party MCP tool

Opaque can admit a signed, pinned HTTPS MCP tool under a finite argument contract. The broker owns its credential, requires native full review, reserves one durable attempt and checks current authority before dispatch. A route may disclose a small signed selection of typed result fields; arbitrary upstream text remains withheld.

This is a bounded MCP 2025-06-18 Streamable HTTP implementation, not universal MCP interoperability. Qualify the selected server, credential type, catalog and schema before enrolling it. No bundled example establishes compatibility with a live service.

Discovery in the MCP adapter

The discovery gate below is available from v0.6.0. opaque-mcp from v0.6.0 advertises opaque_mcp_tool_<alias> routes only when the authenticated daemon returns them, and opaque_mcp_invocation_get and opaque_mcp_invocation_revoke only when the daemon's mcp_catalog reply reports gateway.availability as enabled or fixture_only. A daemon without an [mcp] section reports disabled, and daemons through 0.5.0 omit the field; in both cases the list holds the 22 built-in tools and no opaque_mcp_* tool. The 0.5.0 adapter listed the two invocation tools unconditionally. The recorded reply is in MCP integration.

Separate the advertised schema from admitted input

Registry version 1 is unchanged: input_schema is both the finite admitted schema and the exact upstream pin; output is withhold. Existing serialized records and prepared-call v2 digests retain their prior meaning. Its legacy response hash and byte count remain observable metadata: v1 withholding is not a confidentiality guarantee for values inferable from predictable responses. Use v2 to omit those fields. Legacy response metadata is also withheld after authority loss; receipt reads apply current policy, identity, registry, expiry and invocation-revocation checks before returning that metadata. Control-state receipts remain available to their owner.

Registry version 2 requires an upstream_input_schema for every route. That is the exact advertised inputSchema, including supported annotations. input_schema separately describes the narrower arguments an agent may submit. Each call must pass both validators. An upstream number can accept an admitted bounded integer; it cannot enable fractions, expand integer bounds or permit unlisted arguments. Version 2 routes use domain-separated prepared-call v3 digests.

The upstream subset permits objects, arrays, strings, integers, numbers, booleans, enums, ordinary length/range bounds, and string description/title annotations. It may omit string/array upper bounds and object closure. Those constraints remain mandatory in the admitted schema. References, regexes, combinators, defaults, unknown keywords and unsupported types are rejected before schema compilation. There is no remote schema retrieval. Each v2 schema is at most 64 KiB, with at most 1,024 schema nodes and eight nesting levels; annotation strings are at most 4 KiB.

The runtime still requires exact advertised schema equality. A changed description inside the pinned schema is drift, even if it does not alter argument validation. Reenroll and sign a new registry version deliberately; never strip catalog fields to make a changed pin appear equal. Descriptions outside the input schema are not agent instructions and do not become authority or override the enrolled tool alias.

Offline qualification

The contract utility reads bounded regular files and never connects to a daemon, upstream endpoint, credential store or schema server.

From the repository root, this complete example uses the bundled synthetic v2 fixtures. It respects a custom Cargo target directory:

cargo build --locked -p opaque-mcp --bin opaque-mcp-contract
mcp_contract_bin="${CARGO_TARGET_DIR:-target}/debug/opaque-mcp-contract"
mcp_fixture_dir="crates/opaque-mcp/tests/fixtures"
"$mcp_contract_bin" validate "$mcp_fixture_dir/gateway-registry-v2.json"
"$mcp_contract_bin" qualify "$mcp_fixture_dir/gateway-registry-v2.json" \
  "$mcp_fixture_dir/gateway-catalog-v2.json"
"$mcp_contract_bin" prepare "$mcp_fixture_dir/gateway-registry-v2.json" \
  "$mcp_fixture_dir/gateway-call-v2.json"

All three commands should exit 0. Validation reports registry_valid: true; qualification reports scope: "catalog_pin_match_only" and diagnostic: "pinned_schema_matches"; preparation reports status: "prepared_not_authorized", prepared_contract_version: 3 and output_policy: "typed_fields". Every command reports runtime_gateway_enabled: false. Replace the fixture paths with the selected registry, captured catalog and bounded call to check another candidate. Repeating prepare does not execute or reserve an invocation; runtime replay protection is provided by the broker's durable invocation ledger, not this offline utility.

The catalog is a captured tools/list result or JSON-RPC result envelope. Qualification accepts at most 256 KiB and 128 tools, with no pagination. Diagnostics are pinned_schema_matches, tool_missing, duplicate_tool or upstream_schema_drift; rejected upstream text is not echoed. Unsupported catalogs fail closed. Exit 0 means every selected pin matches; exit 2 means failure. This compares a supplied file, not authenticated endpoint provenance. Successful qualification neither authorizes a call nor proves every admitted input satisfies the upstream schema. prepare validates one concrete input against both schemas.

The synthetic fixtures demonstrate a GitHub-style comment schema containing unbounded strings, descriptions and an upstream number with stricter admitted input:

These are illustrative source-shape fixtures at mcp.example.com. They are not captured GitHub schemas or a ready-to-run GitHub integration. In particular, a real tool may not return the synthetic structuredContent fields used in this example.

validate accepts the unsigned registry document for inspection. It does not enroll that document, validate issuer custody or replace runtime signature, tenant, registry sequence and expiry checks. prepare accepts only route and arguments; callers cannot add an endpoint, credential, approval or output-projection override.

Signed typed result disclosure

A v2 route with output_policy: "typed_fields" must contain output_projection:

{
  "fields": [
    {
      "source": "id",
      "name": "resource_id",
      "value_type": { "kind": "integer_id", "maximum": 1000000 }
    },
    {
      "source": "status",
      "name": "status",
      "value_type": { "kind": "status", "values": ["created", "queued"] }
    }
  ]
}

Only top-level fields of the successful tool result's structuredContent are read. The projection permits at most eight fields and 1,024 serialized output bytes. Numeric IDs must be integers between one and the signed maximum, capped at 2^53−1. Status values must match one of at most sixteen signed tokens, each at most 32 bytes. Source/destination names are fixed signed identifiers, not JSON pointers or caller input. There are no arbitrary string, URL, nested object or raw content projections. Unknown fields are ignored. A missing, malformed or out-of-range selected field withholds the entire projection. Server-reported tool errors never disclose values.

The complete projection, upstream schema digest and admitted schema digest are part of canonical review and action binding. Changing them requires a new approved action. Policy rules can match output_policy, output_projection_digest and upstream_schema_digest; these static fields are available during discovery too. Use the projection digest for matching, since policy field strings use glob syntax and serialized projection JSON contains characters with special glob meaning. The projection contract is signed; returned values are not separately signed evidence. Typed values remain untrusted provider claims; shape validation does not prove their truth, nor that the provider performed a business effect. Choose fields appropriate for the receiving principal and the workflow's disclosure policy.

After the effect, policy, requester identity, registry, expiry and invocation revocation are checked again. The broker repeats its final disclosure fence after asynchronous identity/audit work. Authority lost before that fence removes all projected values while preserving the charged outcome. Revocation after an authorized dispatch cannot undo the provider effect; revocation after authorized disclosure cannot recall bytes already released.

An authorized response adds output and disclosure: "projected" beside receipt. Otherwise disclosure explains withheld_invalid_projection, withheld_tool_error or withheld_authority_changed when applicable. No raw response body enters a receipt. Version 2 receipts retain only projection-validation control state, not projected values, response hashes or response sizes. A digest of a small enum or predictable identifier can reveal its value by enumeration, so it is not safe withholding. mcp_get never rediscloses prior values, including after restart. Losing the response does not authorize retry. Use independent readback before proposing any new work.

Outcome interpretation and production limits

accepted means a valid tool result was observed. rejected can follow a server error after an effect occurred. The v2 receipt's dispatch_status distinguishes not_attempted, attempted and crash-recovery attempt_uncertain; none establishes provider business success. attempt_charged remains true after reservation, including failed admission during handshake, error, expiry, revocation or timeout. An interrupted dispatched call becomes unknown; it is never resumed or refunded. Every invocation UUID is single-use; a new UUID requires separate approval.

An accepted receipt can accompany withheld_invalid_projection or withheld_authority_changed: the transport result was observed, but no useful values were disclosed. Check both receipt and disclosure before reporting success to the user. The MCP adapter sets isError from receipt acceptance, so isError: false alone is not evidence that an output was disclosed or a business effect succeeded.

Production still requires a sealed tenant, separate broker custody, admitted delegated identity, enforced agent sessions, public HTTPS DNS on port 443 and actual native review. Private-address destinations, ambient proxies, redirects, retries, OAuth negotiation, server callbacks and open-ended streams are unsupported. Remote task approval receipts do not authorize MCP calls. A real pilot must verify that its agent cannot bypass the broker using another credential or dispatch path.