Skip to content

tools: a typed refusal for a tool outside the consumer's MCP access profile #312

Open

Description

@amirkhan-ak-sf

What this is

A typed refusal for "this tool is not in your access profile", distinct from
"this call broke a content policy", plus the discovery-side behaviour that goes
with least-privileged MCP access.

Why it is needed

The roadmap adds "Least-Privileged MCP Access Profiles" — each consumer gets a
scoped subset of tools instead of all-or-nothing MCP access. That produces a new
class of refusal the SDK cannot currently express.

classify() today has no branch for it, so an out-of-profile tool call lands in
the generic PolicyViolation fall-through. The agent is told a governance policy
refused its call. That is technically true and operationally useless, because the
two situations demand opposite reactions:

  • out of profile — a configuration problem. Nothing the agent does at
    runtime fixes it. It should stop, and a human should widen the profile.
  • content policy — a runtime problem. Rephrasing, redacting or escalating to
    a human may well succeed.

An agent that retries an out-of-profile call is burning budget on something that
can never succeed; an agent that gives up on a content refusal it could have
rephrased is failing unnecessarily. BG §1.2's whole premise is that the refusal
type drives the reaction, and this is a refusal type with no type.

Depends on

#303 — whether discovery is profile-scoped server-side, and the exact denial
shape. The typed exception cannot be written against a guessed discriminator
(§0.3). #221 (discovery itself) is also _verify.blocked.

Scope

  • A typed ToolNotInProfile (name to settle at implementation) under
    PolicyViolation, carrying the tool name and, if the gateway supplies it, the
    profile identity.
  • Remediation text that names the fix — widen the access profile in API Manager —
    rather than suggesting a retry.
  • Discovery behaviour follows verify: the MCP access-profile denial shape #303's answer: if the gateway scopes discovery
    server-side, ToolSet simply reflects it; if not, the SDK filters after
    resolution and the difference is documented rather than hidden.
  • A ToolSet accessor for what was filtered out and why, so "the agent can't see
    the tool" is diagnosable without a gateway log.
  • Origin-agnostic. The denial is produced by the gateway, so it must
    classify the same whether the server behind it is Mule-hosted (MCP Bridge, a
    Mule app) or framework-built (e.g. FastMCP). A tool failure returned by the
    server itself (an MCP tool result with isError: true) is not a refusal and
    must not classify as ToolNotInProfile.

Sketch

from donkey_kit import Donkey
from donkey_kit.core.errors import ToolNotInProfile, PolicyViolation

donkey = Donkey.from_env()
tools = await donkey.tools.discover(query="customer-360")

# Deep adapter: the same typed refusal surfaces through LangGraph's tool node.
graph = build_graph(
    model=donkey.langgraph.chat_model("gpt-4o"),
    tools=donkey.langgraph.tools(tools),
)

try:
    await graph.ainvoke(state)
except ToolNotInProfile as exc:
    log.error("not provisioned: %s (profile %s)", exc.tool_name, exc.profile)
    raise                        # configuration problem: stop, don't retry
except PolicyViolation as exc:
    return await rephrase_and_retry(exc)   # runtime problem: recoverable

Diagnosing the empty-toolset case:

tools.excluded          # [ToolExclusion(name="refund.issue", reason="out-of-profile")]

Raw client — MCP sessions come from donkey.tools, so the no-framework path
raises the same type:

async with donkey.tools.session(tools) as session:
    await session.call("refund.issue", {...})    # -> ToolNotInProfile

TypeScript parity (planned, Phase 5):

catch (e) {
  if (e instanceof ToolNotInProfile) { /* configuration, not runtime */ }
}

Acceptance

Cross-surface impact

  • core: errors.py (classify() and the new type).
  • tools: session.py, filter.py, ToolSet.
  • registry: the discovery query, if profile scoping is a parameter.
  • integrations: LangGraph tool binding surfaces it; the shallow frameworks get
    it through the shared transport.
  • TypeScript parity (planned): flag.
  • Provisioning CLI: not affected.
  • Nextra docs site: the tool-access section and the refusal table.
  • docs/verified-apis.md: consumes the row verify: the MCP access-profile denial shape #303 adds.

Related

Activity

  1. added
    enhancementNew feature or request
    blocked-on-verificationCannot be closed by writing code (build plan §0.3)
    area:toolstools/ — MCP session, ToolSet, filtering (Pillar 2)
    priority/p1Must-have: blocks a real adapter or CI gate
    size/m1-2 days, multiple files
    area:refusalsclassify(), the typed error taxonomy, handlers (1.2, 2.1, 2.2)
    on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:refusalsclassify(), the typed error taxonomy, handlers (1.2, 2.1, 2.2)area:toolstools/ — MCP session, ToolSet, filtering (Pillar 2)blocked-on-verificationCannot be closed by writing code (build plan §0.3)enhancementNew feature or requestpriority/p1Must-have: blocks a real adapter or CI gatesize/m1-2 days, multiple files

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions