MCP Proposals.

Informing the user

MCP servers can encapsulate data that the user might not have easy access to. This means informing the user of what will be done, what has been done and the overall state of the data can be a challenge.

So I've suggested a few extensions which are inspired by MCP Apps, stay MCP-compatible, opt-in, and do not change normal operations or inflate LLM token use.

At a glance
  • Human visualization of tool calls
  • Human access to encapsulated data
  • Standardized dry-run
  • Runtime policies
  • Scalable permissions

UI Proposal

There are two challenges: informing a user what a tool call did or will do. And informing the user of the overall state of the system.

It's important to note that the user typically needs different information for this than the LLM.

P(review)

The first challenge in MCP is informing the user of the impact a particular tool action has. If the user just looks at tool input or output, it might not tell the whole story.

The LLM has a context built up over the conversation which may consist of different tool calls and interactions. Based on that context the LLM decides to perform a tool call to "replace this line with this other line". For the user however, looking just at the input might not be enough. He may want to see the broader context of that line to understands what the replacement means.

At the same time we don't want to add unnecessary tools to the MCP server that the LLM might get confused by. Instead we want to generate side effects that are aimed solely at the user and invisible to the LLM.

In the file server I added to this project (the mcp-fs project), the edit_file tool call will always generate a visual diff (both unified and side-by-side) for human consumption and a structured diff for automated processing. If the tool call is simply allowed to pass, it can be stored for later viewing, however the server also supports a _meta.preview boolean toggle. This makes use of the MCP v2 addition of _meta keys to standardize a "dry run". The tool will generate the exact same output but not actually apply the change. The artifacts it generates can then be used to inform the user of what would happen were he to allow it. Once allowed, the tool call can be rerun without the preview parameter.

The screenshots are taken in the mcp-test tool which is also available and makes it easy to test the proposed extensions. Note that tools advertise annotations.preview = true to indicate support.

Intent templates

Tool descriptions tend to be written for the LLM. To inform the user before a tool is invoked, tools can optionally publish annotations.intentTemplate: a short, human-focused template filled with the call input.

The template uses a lightweight EBNF-style format. Placeholders use braces ({path}), optional segments use brackets ([in {root}]), and dotted keys allow field access ({paths.path}). Missing values drop the optional segment they appear in. For example: Search for {pattern} [in {root}] [with glob {glob}].

An extended expansion mode treats bracketed groups that contain array placeholders as repeatable. The group is rendered once per array element and placeholders are resolved by index. Scalars repeat across each entry. If multiple array roots appear, each root group is expanded independently. Missing entries are treated as null so optional subgroups drop. If all arrays in the group are empty, the group is omitted. Whitespace is normalized by collapsing multiple spaces after optional groups drop.

Example: Read files [{paths.path} [from line {paths.start_line}] [limit {paths.limit}]]

  • Discouraged: mixing different array roots in the same group.
  • Discouraged: placeholders that resolve to non-primitive values.
  • Discouraged: required keys without brackets (missing keys stay as {key}).

How the UI layer is delivered

Inspired by MCP apps, there is an MCP bridge (available in mcp-ui): a native web component that renders HTML inside a sandboxed iframe. It exposes a standard set of CSS variables that can be configured on the mcp-view, allowing the MCP UI to blend into the host application.

For the file server, each edit-file tool call generates a temporary dynamic resource. If you can inform the user based solely on input/output or existing tools, static resources are also possible. The link is currently added to the content array.

Context beyond a single tool call

MCP servers are frequently black boxes. They manage datasets the user might not be able to easily access, such as a remote or containerized instance.

Sometimes the user needs more information regarding the state of the data. This can help him decide which path to take or understand a larger set of changes.

In the example file server I added this takes the form of a "file browser". It uses the same mcp-ui bridge to inject resources and call its own mcp tools.

Each application is tagged in resource annotations so the host can detect them.

UI layer details

The file browser itself is a Vue 3 app. The standard build is about 75kb raw javascript, and an initial Vue Vapor build drops it to 57kb with more room left to reduce. The goal is to ship lightweight applications.

The host application that embeds the bridge must register a resolver for resources and tool calls to make the flow work.

Projects

mcp-fs

A file server that demonstrates the model

I implemented a rust-based file MCP server that showcases the UI extensions end-to-end. It includes diff views for every file edit and a broader file browser application.

mcp-ui

MCP Bridge

A MCP-Apps inspired native web component to securely render the HTML artifacts while allowing resolving of additional resources and tool calls. It also has a starting point for standardizing css across tools.

mcp-test

Testing it all

A tool to test MCP servers and specifically the UI extensions. It will dynamically detect the applications and content responses.

Configuration and policies

Building on SEP-1596, I have introduced a way to dynamically send the actual configuration in the initialize exchange. After that, runtime policies can be used for each tool call to create restricted sandboxes on the fly.

Schema-first configuration

Servers can publish their requirements during the initialize phase (SEP-1596) via capabilities.configSchema. This allows middleware to dynamically generate configuration UIs for the user. In the provided mcp-fs server, you can also retrieve this schema directly via the --print-config-schema CLI flag.

I've added an extension where the middleware can then take that user-provided configuration and send it in the initialize phase via capabilities.experimental.configuration. While the included mcp-fs server supports traditional environment variables and CLI flags, these initialization-time configurations take precedence, allowing for more flexible, stateful management..

Runtime policies

Once a server is active, Runtime Policies allow for granular, per-call restrictions. This is vital when scoping an MCP server to a narrow domain for a specific task, or when an LLM needs to delegate a sub-task to a worker with limited exposure. The mcp-fs tool implements this by accepting a _meta.policy fragment in the call. These fragments act as a restrictive overlay on the primary configuration.

Crucial Security Rule: A policy can only restrict configuration, never expand it. For example, a policy can narrow a file root to a specific subfolder or add stricter "deny" globs, but it cannot grant access to a new root. Any attempt to expand permissions results in a JSON-RPC error.

To manage this, I've introduced two attributes for configuration fields:

  • scope: this is configuration, policy or any (default). Defines if a setting is set at startup, at runtime, or both.
  • audience: this is human, llm or any (default). Determines if a policy change can be requested by the LLM itself or must be configured by a human.

The included mcp-fs and mcp-test tools provide full reference implementations of these concepts. Auto-detection is explicit: servers advertise policy support via capabilities.experimental.policy = true.

Permissions

I wanted to use a set of permissions that is easy for the user to manage at a high level while also allowing the user to dig deeper and configure it in detail when needed.

Scopes that scale across tools

The goal is to let users tune permissions per session or per agent without drowning them in hundreds of tool-specific toggles. A flat permission list does not scale when you have dozens of MCP tools.

A hierarchical scope layout solves this. Across all tools, the root capabilities should collapse into a small, clear set like read, write and execute. That lets users quickly tune an agent at a broad level before deciding whether they need finer control.

For example you might easily set up an agent that has read:* but disallowed write:* for a high level distinction.

Granularity when you actually need it

Scopes can carry detail, e.g. write:file:/path/to. This keeps the base policy simple while still allowing fine-grained control when a specific tool needs it. It also lets you express cases like write:file allowed but write:database denied, or vice-versa.

Dynamic permission requests

Example: a file server starts with configured roots. If the LLM tries to write outside those roots, the server returns an error by default, but also includes requested_scopes in _meta such as write:file:/new/path .

The agent UI can ask the user to allow or deny the request. If the user approves, the call is replayed with granted_scopes . The grant can be stored for the session or only for that request.

Permission flow lives in the agent layer

Permission checks and prompts are not handled by the MCP server and do not pass through the LLM. The agent framework mediates approval and decides when to persist grants per session or per agent, based on what the workflow needs.