Skip to content

WebMCP loses important context when tools appear or disappear #262

Open

Description

@heyitischris-web

Problem

WebMCP currently gives agents a very simple view of capability state: a tool is either available or it is not.

That works for simple cases, but real applications have much richer state behind those tools.

A tool may become unavailable because:

  • the user does not have permission
  • their subscription does not include the feature
  • they switched organizations or workspaces
  • the current workflow is not ready for that action
  • a resource is still processing
  • the feature is temporarily unavailable

The site usually knows exactly why.

But when the site responds by simply registering or unregistering the tool, that meaning is lost. The agent only sees that the tool disappeared.

I think this creates a form of semantic context blindness.

Example

Imagine a reporting application.

A user asks an agent:

"Export this report."

Earlier, the agent could see:

export_report

The user then switches to another workspace where exports are not included in their plan.

The application unregisters export_report.

Now the agent only knows:

export_report no longer exists.

It cannot tell whether:

  • the user lacks permission
  • the workspace plan does not support exports
  • the report is not ready yet
  • exports are temporarily unavailable
  • or something else changed

Those situations should lead to very different agent behavior.

If the agent knew:

Export is unavailable because this workspace does not include report exports.

it could explain that to the user and continue helping with the parts of the workflow that are still available.

Developer-side problem

Leaving this entirely to application code also makes developers responsible for keeping the WebMCP tool registry synchronized with constantly changing application state. It also poses security concerns, due to constant changes that would need to happen, and coding changes that need to be maintained by a developer.

For example:
.js

if (canExport) {
registerTool(exportReport)
} else {
unregisterTool("export_report")
}

That seems simple until an application has dozens of tools and permissions can change because of login/logout, organization switching, subscription changes, role changes, feature flags, or async workflow state.

It becomes easy to:

  • forget to remove a tool when access changes
  • repeatedly register/unregister tools unnecessarily
  • create races where an agent plans around a tool just as it disappears
  • hide an entire tool even though only one operation inside it is restricted

Async/service-style tools make this even more noticeable.

For example, one manage_report tool might allow a user to view and edit a report, while exporting requires an additional permission. Removing the entire tool because export is unavailable would also hide the operations the user is still allowed to perform.

What I think is missing

I am not suggesting that WebMCP should become an OAuth or authorization system, or that the browser should understand every site’s permission model.

The site should remain responsible for authentication and authorization.

But it may be useful for WebMCP to have a standard way for a site to preserve some of the meaning behind capability changes instead of reducing everything to:

tool exists / tool does not exist

For example, a site could communicate that a capability is currently unavailable and optionally provide a reason such as:

  • permission required
  • entitlement required
  • authentication required
  • invalid current state
  • temporarily unavailable

The exact API is less important to me than preserving that semantic context.

Related discussions

This seems related to:

I think this is slightly different from those discussions because the core problem is not specifically scopes.

The broader issue is that rich application capability state is currently being projected into a binary tool-presence signal, and useful context is lost between the site and the agent.

MCP naturally sits at the backend authorization boundary; WebMCP does not. WebMCP therefore needs a cleaner way to project backend capability state into the agent-visible tool surface, without reducing everything to syncing tools with register/unregister.

Activity

  1. zekariasasaminew commented on Sep 2, 2026

    @zekariasasaminew

    Implementation note from a WebMCP Challenge incident-response simulator: dynamic, human-driven tool scoping makes this issue very concrete.

    In Incident Command, the human can revoke a capability or remove a service from the agent's scope mid-incident. We unregister/re-register tools with AbortController signals and narrow service enums so the browser agent's surface reflects the human boundary. That works, but from the agent side a removed tool or removed enum value carries very little semantic context: it cannot distinguish 'phase passed', 'human revoked this capability', 'service intentionally out of scope', or 'registration failed/recovering' unless the app also exposes a separate state/audit tool and the agent chooses to call it.

    The pattern is useful for safety, but it pushes apps toward building their own explanatory side channel for unavailable capabilities. A standard way to preserve optional unavailable reasons, especially across toolchange, would make human-controlled dynamic scopes much easier for agents to handle well without reintroducing dangerous tools just to explain why they are blocked.

  2. victorhuangwq commented on Sep 3, 2026

    @victorhuangwq
    Contributor

    One thing I’m curious about here: is there a reason the agent needs to know that a tool is unavailable at the tool definition / discovery layer?

    In the current situation, it seems like one option would be to keep the tool registered, and if it can’t currently be used, have execute() return a structured result explaining why. That would avoid some of the register/unregister churn while still giving the agent the semantic context once it tries to use the tool. Something along the lines of:

    {
      status: "unavailable",
      reason: "authentication_required"
    }

    I was thinking, for cases like authentication, the tool invocation could also trigger the site’s existing auth flow, for example by directing the user to sign in. That makes me wonder whether keeping the tool discoverable might actually be useful in some cases, since invoking it can naturally surface whatever prerequisite the user needs to complete.

    Let me know if this doesn’t track. The main difference I can see is that the agent wouldn’t know the tool is unavailable until invocation. If that pre-invocation knowledge is important here, I would be interested in your thoughts on what cases require it.

  3. minjikim89 commented on Sep 6, 2026

    @minjikim89

    One mitigation that worked for us, in case it is useful while a spec-level answer is being worked out.

    The read tool that stays registered reports the tools that are not currently registered, with the reason and the way back. In our case list_slides returns:

    "unavailableTools": [
      { "name": "set_chart_form",
        "because": "no visualize note is open",
        "how": "circle a chart and mark it visualize" }
    ]

    inside the output budget. The effect is that the agent stops hunting for a tool it remembers and instead asks the person for the state that enables it.

    Our toolset is authored by page state on purpose (before a deck is open only open_deck exists, the review set appears when a deck opens, and conditional tools follow the open notes), so tools appearing and disappearing is the normal case here rather than an edge. That made the missing-context problem load-bearing early.

    This does not replace a spec-level answer; a toolchange payload carrying reasons would be better, since it would work for every site without each one inventing a field. It closes the gap today with no host support.

    Implementation: https://github.com/minjikim89/redline/blob/main/docs/pattern.md · live: https://minjikim89.github.io/redline/

  4. zekariasasaminew commented on Sep 6, 2026

    @zekariasasaminew

    Thanks, this tracks for recoverable prerequisites, and I think it exposes the distinction our original comment was missing.

    Keeping a tool registered and returning { status: "unavailable", reason: "authentication_required" } is a good fit when invocation can help the user satisfy the prerequisite. Authentication, entitlement upgrades, and “resource still processing” often belong in that category.

    The Incident Command case is different in two ways:

    1. The absence is itself a human policy boundary. execute_approved_action does not appear until two humans approve the exact action, and a commander can revoke a capability or service mid-run. If the dangerous tool remains discoverable, the agent can continue planning around and attempting a capability the human explicitly removed. The execution handler must still re-authorize every call, so unregistration is not the security check; it keeps the planning surface aligned with current human intent.
    2. The input domain changes before invocation. Removing a service from scope also removes that service from the registered schema enum. Learning only after a failed call is late: the agent may have already selected a revoked target and built a plan around it.

    So I would split unavailable states into two broad classes:

    • Recoverable prerequisite: keep the tool discoverable; return a structured reason and a path to resolution.
    • Policy/workflow boundary: remove or narrow it before planning; preserve an explanation for the capability change.

    The latter matters especially for consequential tools and for keeping the active tool surface small. It also avoids turning expected human revocation into repeated failed tool calls and noisy audit events.

    The Redline pattern is a useful bridge. Incident Command already keeps get_incident_state registered and returns humanPolicy plus recent audit events, but the agent has to infer which tools disappeared and why. An explicit bounded unavailableTools list with because and how would be better, and we can adopt that application-level pattern.

    At the platform level, a toolchange detail describing removals/narrowing (at least for tools the agent had already observed) would preserve the semantics without keeping a revoked tool callable. Something like { name, change: "removed", reason: "user_revoked" } would be enough; sites could keep richer prose in their stable state tool.

    The important safety caveat is unchanged: discovery state is guidance, not authorization. The execute path must fail closed against current policy even if registration and invocation race.

  5. minjikim89 commented on Sep 7, 2026

    @minjikim89

    Thanks for taking the split apart properly. The two classes are the right cut, and I would keep the line where you drew it.

    unavailableTools only covers your first class. Every entry names something the person can turn on in the page, like "circle a chart and mark it visualize". It saves the agent a round trip, and that is all it does.

    The boundary the person actually owns sits in the execute path, and it runs on every write regardless of what is registered. While they keep edits scoped to the slides they marked, a write elsewhere returns OUT_OF_SCOPE. A write over a hand edit the agent has not read returns STALE_READ with the diff. Registration decides what is offered, not what is permitted, and offering less never grants more. That is your closing caveat, seen from the page side.

    It is also why the pattern is safe to copy. unavailableTools can stay advisory because nothing rests on it. If a site expressed a policy boundary only by removing a tool, the removal would become the check, and a discovery/invocation race would turn into a security bug rather than a stale hint.

    On the platform-level toolchange detail: agreed, and it would help outside the security case too. We register from page state, so the set changes whenever a deck opens or a note resolves, and the page's own UI has to work out what changed because the event carries no detail. { name, change: "removed", reason: "user_revoked" } would cover both consumers.

    I filed the execute-path side as #298, page-enforced write boundaries as a §6.4 mitigation, with the eval numbers behind it. Might be worth pinning "discovery is guidance, not authorization" somewhere normative.

  6. rohit-binaried commented on Sep 17, 2026

    @rohit-binaried

    One implementation data point: in RCIP, which I maintain, a declared capability can remain in the application catalog while its live state reports unavailable or unbound. Consumers can inspect the snapshot before planning, and invocation still checks current conditions. Contract/lifecycle description.

    That supports the distinction already made here: “temporarily unusable” and “the app intentionally no longer offers this operation” need not become the same signal. An external projection would also need a way to omit sensitive capabilities entirely; an unavailable reason must not become an information leak.

    RCIP is not a WebMCP implementation. For a future mapping, I would want both a current snapshot and change notifications, with execution re-checking policy even when the consumer holds an older snapshot. The existing unavailableTools examples seem useful input for that mapping.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions