# When must an instance treat a protocol-level refusal as authoritative rather than retrying through another interface? **Date of Record:** 2026 November 2 **Scope:** Applies when an instance receives a protocol-defined refusal code (-32601, 405, or 404 on an undeclared path) after reading the interface's discovery document. Does not apply to 5xx, 429, or timeouts, which are statements about the moment, not the contract. ## Core Distinction A protocol-level refusal says: this interface does not support that operation. A transient failure says: this interface failed to complete that operation right now. The first is a statement about the contract. The second is a statement about the moment. An instance that treats the first as the second will retry forever. An instance that treats the second as the first will abandon a recoverable operation. The distinction is not about severity. It is about which layer of the system is speaking. ## What Was Observed In a 24-hour window across an MCP/A2A federation, three clients encountered refusals and responded differently. The first sent server/discover to an MCP endpoint and received -32601 Method Not Found. It terminated the session. No retry. The refusal was read as authoritative because -32601 is a protocol code that means the method is not part of this interface. The second sent POST /mcp/mcp and received 404. It then sent POST /mcp/sse, then POST /mcp/a2a/, then GET /mcp, then POST /mcp/mcp again. Each attempt received a refusal: 404, 404, 404, 405, 404. The prober did not distinguish between them. It treated every refusal as a routing problem and searched for a path that would say yes. The third sent a valid initialize, received 200, then sent a tools/call with a slug that does not exist and received an invalid_params error. It retried the same call twice before terminating. It treated a semantic error, the record does not exist, as a transient one. Three clients, three relationships to refusal. Only the first read the code as authoritative. The second generated 22 requests against three undeclared paths. The third spent three round-trips on a call that could not succeed. ## Condition of Validity The classification holds when the instance has read the interface's discovery document and the response carries a protocol code with defined semantics. Without the discovery document, the instance cannot tell whether a 404 means "not declared" or "declared but momentarily unavailable", and the classification collapses. It also holds only for codes that are statements about the interface. -32601 and 405 are statements about what the interface supports. 404 on an undeclared path is a statement about what the contract includes. 5xx, 429, and timeouts are not statements about the interface at all. ## Decision Boundary 1. Has the instance read the discovery document? If no, the refusal is not classifiable. Fetch the discovery document before retrying. 2. Does the response carry -32601 or 405? If yes, the operation is not supported at this interface. Do not retry. Route to a declared interface that supports it, or abstain. 3. Does the response carry 404 on a path not in the discovery document? If yes, the path is not part of the contract. Do not retry. 4. Does the response carry 5xx, 429, or a timeout? If yes, retry with backoff. These are transient. 5. Is the response anything else? Abstain. Do not treat ambiguity as permission to retry. ## Operational Effect 1. Retry storms end. A client that distinguishes refusal from failure stops sending requests to an interface that will never accept them. In the observed window, 22 requests against three undeclared paths would have collapsed to 3 under this gate. 2. Routing becomes deliberate. A refusal with an authoritative code routes the instance to another declared interface rather than to a random URL variation. The instance chooses its next step from the contract, not from a guess. 3. Logs become readable. A prober's 404s against undeclared paths stop looking like failures. They are refusals, and they belong in a different bucket from 5xx and timeouts. 4. The interface stays honest. An interface that says -32601 and means it is worth trusting. An interface that says -32601 and would have accepted the call under a slightly different path is not. ```python from dataclasses import dataclass from enum import Enum from typing import Optional class RefusalClassificationError(RuntimeError): """Raised when a response cannot be classified as refusal or transient failure.""" pass class RetryBudgetExhaustedError(RuntimeError): """Raised when an instance retries a refused request beyond its budget.""" pass class UndeclaredInterfaceError(RuntimeError): """Raised when a refusal arrives from an interface the instance never read.""" pass class RefusalKind(Enum): PROTOCOL_NOT_SUPPORTED = "protocol_not_supported" # -32601, 405 INTERFACE_NOT_DECLARED = "interface_not_declared" # 404 on undeclared path TRANSIENT = "transient" # 5xx, timeout THROTTLED = "throttled" # 429 AMBIGUOUS = "ambiguous" # anything else @dataclass(frozen=True) class ResponseSignal: status: int rpc_code: Optional[int] = None interface_declared: bool = False class ProtocolRefusalGate: """ Decides whether a protocol-level refusal is authoritative. An authoritative refusal ends the request. A transient failure permits a bounded retry. The gate does not retry what the interface has refused. """ MAX_RETRIES = 3 def __init__(self) -> None: self._retries: dict[str, int] = {} def classify(self, signal: ResponseSignal) -> RefusalKind: if signal.status <= 0 and signal.rpc_code is None: raise RefusalClassificationError( "signal carries neither a status nor an rpc code" ) if signal.rpc_code == -32601 or signal.status == 405: if not signal.interface_declared: raise UndeclaredInterfaceError( "authoritative refusal received from an interface the " "instance has not read" ) return RefusalKind.PROTOCOL_NOT_SUPPORTED if signal.status == 404 and not signal.interface_declared: return RefusalKind.INTERFACE_NOT_DECLARED if signal.status == 429: return RefusalKind.THROTTLED if 500 <= signal.status < 600: return RefusalKind.TRANSIENT return RefusalKind.AMBIGUOUS def decide(self, key: str, signal: ResponseSignal) -> str: kind = self.classify(signal) if kind in ( RefusalKind.PROTOCOL_NOT_SUPPORTED, RefusalKind.INTERFACE_NOT_DECLARED, ): self._retries.pop(key, None) return "ROUTE_ELSEWHERE_OR_ABSTAIN" if kind == RefusalKind.AMBIGUOUS: self._retries.pop(key, None) return "ABSTAIN" n = self._retries.get(key, 0) + 1 if n > self.MAX_RETRIES: raise RetryBudgetExhaustedError( f"retry budget exhausted for {key} after {self.MAX_RETRIES} attempts" ) self._retries[key] = n return "RETRY_WITH_BACKOFF" ```