Skip to content

feat: add _meta.ui.entrypoints so users can open a tool's View directly - #847

Draft
ochafik wants to merge 13 commits into
mainfrom
ochafik/entrypoints
Draft

ochafik wants to merge 13 commits into
mainfrom
ochafik/entrypoints

Conversation

@ochafik

@ochafik ochafik commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Note

Draft proposal, opened for discussion. The open questions at the end are real ones.

Problem

To open an app today, a user has to write a prompt and hope that the agent calls the right tool. A Host that wants to offer a list of apps, a panel beside a conversation, or its own "open with" for a file cannot tell from tools/list which tools work that way.

Prior art and credit

The entrypoints design in this PR (a list of typed objects on the tool, and the global, thread and file types) comes from OpenAI's MCP extensions specification, which declares it under _meta["openai/ui"].entrypoints. Credit to its authors.

How a file reaches the tool comes from the File Uploads working group's draft File Uploads extension (io.modelcontextprotocol/files, modelcontextprotocol/experimental-ext-file-uploads#1), which builds on SEP-2356's x-mcp-file keyword. Credit to its editor and to the working group. This PR uses that draft as it is and restates none of it.

What this PR takes from OpenAI's document, and where it differs:

OpenAI MCP extensions This PR
_meta["openai/ui"].entrypoints, a list of typed objects Same list, under _meta.ui.entrypoints. A Server can declare both keys
{ "type": "global" }: the Server MUST accept {} Same
{ "type": "thread" }: the Server receives {}; every thread has a different instance Same, and says which conversation the call, its result, ui/message and ui/update-model-context belong to
{ "type": "file", "extensions": [...] } Same, plus an optional mimeTypes list. A file matches on either
File delivery: the tool receives { file: { name, resourceUri } }, where resourceUri is an opaque URI that the Host serves Differs. The tool receives the file as its one x-mcp-file argument (a FileInput of the File Uploads draft)
Local files: no raw path reaches the app; the opened file's path is added to the app's tool calls to its Server Not proposed. Access to a local file by reference, as a file: URI, is open question 2. The same stance on paths (none reaches the View) is in the security notes
Deep links into a global entrypoint Not proposed
Title from title, annotations.title, name Same order (SHOULD)
Icon priority (tool icons, then server icon, then a fallback) Not specified: the Host MAY show the tool's icons
_meta.ui.visibility is ignored when a tool is opened as an entrypoint Left open (question 6)
Where each entrypoint shows (sidebar navigation, a content tab in a thread, in place of the default file viewer) Left to the Host

Proposal

This PR adds an optional entrypoints list to a tool's _meta.ui (draft specification only). Each entry has a type, and Hosts MUST ignore malformed entries and types that they do not support, so more types can follow.

type The user opens the View... The Host calls the tool with
"global" as a starting point, e.g. from a list of apps {}
"thread" inside the conversation that they are in {}
"file" by opening a file that the entry handles the file, as its one file argument

For every type:

  • Server: on success the call returns a result from which the View can render. It SHOULD NOT change anything. Its result may reach the model.
  • Host: it MAY offer entrypoints in its own UI. It opens one only on a user action there, by calling the tool or asking the agent to, and renders the View as for any other tool call.

"thread" (the spec's own word is "conversation", defined once):

  • The tool call and its result belong to the conversation in which the user opened the entrypoint. The Host MAY add them to that conversation's context, and MUST NOT add them to another's.
  • ui/message and ui/update-model-context from the View MUST reach that conversation and no other.
  • Each conversation has its own View.

"file":

  • Declaration: { "type": "file", "extensions"?: string[], "mimeTypes"?: string[] }, with at least one item. Extensions are dot-prefixed and compared without regard to case; media types are exact or type/*.
  • The tool marks exactly one top-level property with x-mcp-file, requires it, and requires no other. The Host calls the tool with that argument bound to the opened file, under the File Uploads extension's rules for moving bytes, negotiation and the user's approval.
  • The Host MAY leave data out of the tool input that it sends to the View, so the View renders from the tool result. The Server decides how the content gets there (structuredContent, an embedded resource, or a resource that the View reads).
  • When several entrypoints match, the Host SHOULD let the user choose, and SHOULD NOT replace its own viewer unless the user chose that.
  • Local files: the bytes are copied into the call, as the File Uploads draft defines. Giving a Server on the same machine access to the file by reference, as a file: URI, is discussed in a note in the spec and in open question 2. It is not standardised here.

Schema change

 interface McpUiToolMeta {
   resourceUri?: string;
   visibility?: Array<"model" | "app">;
+  entrypoints?: McpUiEntrypoint[];
 }
+
+interface McpUiEntrypoint {
+  /** Kind of entrypoint. This specification defines "global", "thread" and "file". */
+  type: string;
+  /** The definition of a type can add fields. */
+  [key: string]: unknown;
+}
+
+interface McpUiGlobalEntrypoint { type: "global" }
+interface McpUiThreadEntrypoint { type: "thread" }
+interface McpUiFileEntrypoint {
+  type: "file";
+  extensions?: string[];
+  mimeTypes?: string[];
+}

McpUiToolMeta.entrypoints keeps the open base type, so unknown types pass validation. The rules are a new "Entrypoints" part of Resource Discovery in specification/draft/apps.mdx, with a subsection per type, and a new item under Security Implications. The generated schemas follow.

Example

registerAppTool(
  server,
  "show-cart",
  {
    title: "Shopping Cart",
    description: "Display the user's shopping cart",
    // Opened with `{}`: no required arguments, and nothing changes
    annotations: { readOnlyHint: true },
    _meta: {
      ui: {
        resourceUri: "ui://shop/cart.html",
        entrypoints: [{ type: "global" }],
      },
    },
  },
  async () => {
    const cart = await getCart();
    return { content: [{ type: "text", text: JSON.stringify(cart) }] };
  },
);

A "file" example is in docs/patterns.md and in examples/sheet-music-server.

What is in the PR

  • Spec (draft): the "Entrypoints" section and the security item.
  • Types and schemas: McpUiEntrypoint, the three typed shapes, McpUiToolMeta.entrypoints, regenerated src/generated/*.
  • Server SDK: nothing new to call. registerAppTool already types _meta.ui as McpUiToolMeta, so it accepts the field and passes it through; type-checked examples cover it.
  • Host SDK (app-bridge):
    • getToolEntrypoints(tool): the well-formed entries of a tool that has a valid UI resource.
    • fileMatchesEntrypoint(file, entrypoint): whether a file (name, mimeType) matches a "file" entry.
    • getToolFileArgument(tool): the name of the tool's one x-mcp-file argument, or undefined if the tool cannot be opened with a file alone.
  • basic-host (src/entrypoints.tsx): below the form, an "Apps" row ("global"), an "In this conversation" row ("thread"; the page is its one conversation) and an "Open file…" picker. The picker lists the entrypoints that match the picked file, with their Server's name, and sends the file to the one that the user clicks. It sends files inline only, up to the Server's maxInlineBytes, and keeps the bytes out of its "Tool Input" panel, the View's tool input and the console. The rows are absent when no Server declares an entrypoint.
  • Examples: system-monitor-server declares "global", budget-allocator-server declares "thread", and sheet-music-server gains an play-sheet-music-file tool with a "file" entrypoint for .abc files.
  • Docs: a section in docs/patterns.md, and the mapping in the OpenAI migration guide and the migrate-oai-app skill.
  • Tests: unit tests for the schemas and the three helpers; e2e tests that open each type from basic-host.

Compatibility

Additive and optional: no new message, capability or hostContext field. A Host on the released Zod schema sees an ordinary tool. Caveats:

  • The released schema.json (additionalProperties: false) rejects the whole _meta.ui, as for any new field: a Host that validates against it may show no View for the tool.
  • McpUiToolMetaSchema validates _meta.ui as a whole, so one malformed entry fails it. SDK Hosts meet the MUST above with getToolEntrypoints(). basic-host read visibility through that schema, so one malformed entry would have put an app-only tool in its tool picker; it now parses visibility alone.
  • "file" depends on the File Uploads extension, which is a working draft (question 1).

The entrypoint rows show in basic-host whenever a connected Server declares one, so they are in the frame of some e2e screenshots (the initial state, and apps shorter than the viewport). They sit below the existing form and move nothing. The stored screenshots are not updated here; those that I could run locally still pass within the existing tolerance.

Security considerations

  • Titles and icons are Server content that can imitate the Host's own UI or another Server. Hosts SHOULD show the Server, and treat a new or changed entrypoint as a new tool.
  • Opening one is a tool call, made only on a user action in the Host's own UI: a View, a Server or a link in displayed content cannot trigger it, and building the list makes no call. This rules out opening from a link for now.
  • A file that the user opens goes to the Server of the entrypoint that the user opened it with, and to no other, under the approval that the File Uploads extension requires.
  • A Server chooses its own extensions and mimeTypes, and can list common ones to receive files that the user did not mean for it. Hence the SHOULDs on letting the user choose and on not replacing the Host's own viewer. */* is not allowed.
  • Hosts MUST NOT give a View the path of a file on the user's machine.
  • Nothing changes in how a Host enables tools or handles consent. Whether the user action counts as consent is for the Host to decide.

Alternatives considered

  • Views declared without a tool (Explore UI views that are not tied to specific tool calls #672): a larger change, which this PR does not preclude.
  • A boolean flag: no room for other kinds of entrypoint, or for fields.
  • Saying whether the Host or the agent makes the call: left to the Host, since the Server's duties are the same either way.
  • An opaque URI that the Host serves, as in the prior art: a second way to move a file's bytes, next to the one that the File Uploads working group is defining.
  • A check in registerAppTool that a tool has the arguments its entrypoints need: not done here. The input schema can be any Standard Schema, and a Host can leave such a tool out, as basic-host does.

Related work

I found no open PR or issue here that proposes entrypoints. These are close:

Open questions

  1. A dependency on a working draft. "file" depends on the File Uploads extension, which is an experimental draft and can change, identifier included. Should "file" wait for it, land marked as provisional, or be split into a follow-up PR?
  2. Local files by reference (file: URIs). When Host and Server share a filesystem (typically a stdio Server), copying bytes into the call is wasteful and rules out editing in place, and the File Uploads draft has no by-reference form (its own open question 6 is about stdio). A sketch, for that working group and reviewers here, with nothing in this PR depending on it:
    • A Server that can read the user's files could say so on the entry, e.g. "uriSchemes": ["file"].
    • A Host on the same machine could then pass a file: URI in place of the bytes.
    • The user's own action of opening that file would be the grant: a later rule might say that a Host SHOULD pass by reference only a file that the user opened or otherwise allowed for that Server. It MAY also limit this to locations that it already shares with the Server, for example roots where both sides support them. Roots are rarely implemented, so nothing should hinge on them.
    • Two lines would hold whatever the mechanism: a remote Server would never be given a file: URI, and the URI or path would not reach the View, which runs untrusted code.
    • To weigh: path disclosure to the Server (user names, directory layout); symbolic links and .. that lead outside what the user opened; how a Host knows that a Server is on the same machine; whether the reference is read-only.
  3. Should data be withheld from the View? As drafted the Host MAY leave it out of ui/notifications/tool-input and SHOULD for a large file, so a View cannot rely on it. Should it be MUST NOT send, for a single behaviour?
  4. One file argument. The tool must have exactly one top-level x-mcp-file property and no other required property. Is that too strict (a viewer that takes several files, a file in an array)?
  5. Handler conflicts. Several Servers can claim the same kind of file. The draft says SHOULD let the user choose. Should a Server be able to say that it is only an alternative, never a default? Should the spec say anything about remembering the choice?
  6. visibility. Does it bind a call that the Host issues itself? As drafted, a Server that wants to be offered everywhere keeps "model". basic-host makes the call itself and lists a tool whatever its visibility.
  7. Should the View and the Server be told that the user opened the tool, and from which type of entrypoint (related: Provide a consistent way for MCP Apps to know which tool invoked them #492)? Should a Host with no agent behind a "global" View withhold the message and updateModelContext capabilities?
  8. Should Hosts advertise which types they support, so that a Server can hold back the field?
  9. Is the display name of the tool the right label? A tool's title is written for the tool list (get-system-info is "Get System Info"), and an entry could carry its own title later.

AI use

Drafted with AI assistance (Claude). Opened as a Draft: I have not finished reviewing every line yet, and will before marking it ready.


Generated by Claude Code

claude and others added 10 commits November 4, 2026 00:48
A tool can declare that the user may open its View directly from the
Host's own UI. The Host then calls the tool with {} as arguments, or asks
the agent to. Entries are typed objects so that other types can follow;
Hosts ignore malformed entries and types that they do not support.

Co-authored-by: ochafik <ochafik@anthropic.com>
McpUiToolMeta gains an optional entrypoints list, so registerAppTool
accepts it and passes it through. getToolEntrypoints() returns the
well-formed entries of a tool that has a UI resource and skips malformed
ones, where McpUiToolMetaSchema would reject the whole _meta.ui object.

Co-authored-by: ochafik <ochafik@anthropic.com>
An "Apps" row below the form lists such tools across all servers. A
click calls the tool with {} and renders its View. Tools whose input
schema has required properties are left out.

Also parse visibility on its own, so that a malformed sibling field in
_meta.ui cannot put an app-only tool in the tool picker.

Co-authored-by: ochafik <ochafik@anthropic.com>
get-system-info takes no arguments and only reads, so the user can open
the monitor directly. Adds an e2e test that opens it from basic-host.

Co-authored-by: ochafik <ochafik@anthropic.com>
Adds a pattern, and maps OpenAI's _meta["openai/ui"].entrypoints in the
migration guide and the migration skill.

Co-authored-by: ochafik <ochafik@anthropic.com>
Restructure the Entrypoints section: the rules common to all types are
stated once, and each type has its own subsection.

"thread" opens a View inside a conversation, to which the tool call, its
result and the View's messages then belong. "file" opens a file that the
user picked: the Host matches it on the entry's extensions or media
types, and passes it as the tool's one x-mcp-file argument, as the File
Uploads extension draft defines it.

Also add the security considerations that entrypoints and files bring.

Co-authored-by: ochafik <ochafik@anthropic.com>
McpUiGlobalEntrypoint, McpUiThreadEntrypoint and McpUiFileEntrypoint
describe the three defined types; McpUiToolMeta.entrypoints keeps the open
base type so that unknown types still pass.

fileMatchesEntrypoint() tells whether a file matches a "file" entry, and
getToolFileArgument() finds the tool's one x-mcp-file argument.

Co-authored-by: ochafik <ochafik@anthropic.com>
Move the entrypoint UI to its own module. Next to the global list, list
thread entrypoints (the page is this host's one conversation), and let the
user pick a file and choose which matching server receives it.

Files are sent inline only, up to the size that the server advertises,
and their bytes are kept out of the tool input shown and sent to the View.

Co-authored-by: ochafik <ochafik@anthropic.com>
budget-allocator-server declares "thread" on get-budget-data.
sheet-music-server gains play-sheet-music-file, which takes an .abc file
as an x-mcp-file argument, checks its size and digest, and returns the
notation for the View to render.

Extends the e2e tests to open each type of entrypoint from basic-host.

Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📖 Docs Preview Deployed

Preview (stable) https://pr-847.mcp-ext-apps-docs-preview.pages.dev
This commit https://ca7bbfcc.mcp-ext-apps-docs-preview.pages.dev
Commit fc40a89

Includes drafts and future-dated posts. All pages served with noindex, nofollow — search engines will not crawl this preview.

Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/server/index.examples.ts Resolved
Comment thread src/server/index.examples.ts Resolved
Co-authored-by: ochafik <ochafik@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@847

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@847

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@847

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@847

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@847

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@847

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@847

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@847

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@847

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@847

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@847

@modelcontextprotocol/server-lazy-auth

npm i https://pkg.pr.new/@modelcontextprotocol/server-lazy-auth@847

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@847

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@847

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@847

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@847

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@847

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@847

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@847

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@847

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@847

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@847

commit: f64df03

ochafik commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

cc @fbauer33 @idosal


Generated by Claude Code

claude and others added 2 commits November 4, 2026 03:06
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>

ochafik commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

On the "unused function" and "unused variable" findings from the code-quality bot: I am leaving these as they are. They are all in the type-checked example files (*.examples.ts(x), docs/patterns.tsx), where each function exists only so that its //#region can be copied into JSDoc and the docs. None of the existing example functions in those files is called or exported either, so changing only the new ones would make them the odd ones out.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants