Skip to content

v0.16.0

Choose a tag to compare

@github-actions github-actions released this 27 Jul 12:31
ee2cc7c

Added

  • Tools link to interactive views. ui=UIToolMeta(resource_uri="ui://…")
    on register_service_tool / register_selector_tool /
    register_chain_tool (and the @service_tool / @selector_tool
    decorators) emits _meta.ui on the tool's tools/list entry, so an MCP
    host renders that tool's result inside the view instead of showing raw JSON.
    This closes the MCP Apps round trip started by register_ui_resource.

    • The render payload is the structuredContent the tool already emits
      no second serialisation path, pagination envelope included. A tools/call
      the view makes arrives at the ordinary endpoint, so permission_classes,
      per-binding MCPPermissions and rate limits all apply unchanged.
    • visibility (UIVisibility.MODEL / APP) declares who may call the tool.
      Host-enforced — a host must not offer the model a tool whose visibility
      omits MODEL — so this server declares the field and does not filter
      tools/list on it.
    • Three ways a link can be wrong are refused at registration, because all
      three fail identically at runtime: a view that silently never renders.
      (1) resource_uri names no view on this server — so a view must be
      registered before the tool linking to it; (2) the tool doesn't emit
      structuredContent, checked against the effective value so a project
      that disabled it globally is caught too; (3) both ui= and a "ui" key in
      meta=, which would silently overwrite each other.
  • ClientCapabilities.extensions — a client's initialize now round-trips
    the protocol extensions it advertises (MCP Apps arrives as
    io.modelcontextprotocol/ui). Parsed for introspection only: advertisement
    is one-directional, client → server
    , the spec defines no matching server
    capability, and nothing gates on it — remembering it per session would mean a
    breaking change to the pluggable SessionStore Protocol, for metadata
    non-supporting clients are required to ignore anyway.

  • Interactive views — the server half of
    MCP Apps.
    A tool can
    declare an HTML view that an MCP host renders inline in the chat.
    MCPServer.register_ui_resource(name=…, uri="ui://…", template_name=…)
    serves the document with the text/html;profile=mcp-app mime type and a
    _meta bundle built from the typed UIResourceMeta — CSP origins (UICsp),
    browser UIPermissions, publisher domain, border preference.

    • We declare; hosts render. The sandboxed iframe, CSP enforcement and
      the ui/* postMessage bridge belong to the host and are deliberately not
      implemented here. Apps is an extension over base MCP 2025-11-25, which
      this package already speaks, so there is no protocol bump, no transport
      change and no new capability to advertise.
    • A view is an ordinary resource. It shares one URI namespace with data
      resources (a collision raises as always), appears in resources/list, and
      honours permissions= / always_listed. Views are unguarded by
      default
      : the MCP session is already authenticated, a view is a static
      asset rather than tenant data, and hosts may prefetch one before any tool
      call.
    • Exactly one content source — template_name= (a Django template, rendered
      per read so an edit needs no restart), html=, or a zero-argument
      selector=. The template renders with no context: because hosts
      prefetch and cache views, a view is a shell that hydrates itself from tool
      results, and rendering a queryset into it would leak data across that
      cache.
    • meta= stays available for other extensions. Passing both the typed ui=
      and that same key inside meta= raises at registration rather than letting
      one silently win — the symptom would be a view that never renders.
  • ResourceEncoding — a resource declares how its body is encoded.
    register_resource(..., encoding=ResourceEncoding.TEXT) returns the
    selector's value verbatim instead of JSON-encoding it. This fixes a live
    bug for any non-JSON resource:
    mime_type= has always accepted anything,
    but both read handlers JSON-encoded unconditionally, so a Markdown, CSV,
    plain-text or HTML resource came back as a quoted JSON string literal
    rather than the document. Declared rather than sniffed from mime_type, so
    advertising a new type never silently changes the body. A TEXT resource
    whose selector returns a non-str comes back as a JSON-RPC error on the read
    rather than raising through the transport.

  • Generic _meta passthrough on the wire types and every registration
    surface.
    The base MCP protocol gives most wire objects a free-form _meta
    object — the open extension namespace, distinct from the closed
    annotations hint bundle — and the package had no way to populate it. Pass
    meta= to register_service_tool / register_selector_tool /
    register_chain_tool / register_resource / register_prompt, to the
    @service_tool / @selector_tool / @resource / @prompt decorators, or
    to a ToolDefinition / SelectorDefaults / ServiceDefaults, and the
    bundle lands on the binding (binding.meta) and is emitted verbatim under
    the "_meta" key of that binding's listing entry (tools/list,
    resources/list, resources/templates/list, prompts/list) and — for a
    resource — on the contents block resources/read returns, on both the sync
    and async transports. Omitted entirely when empty, so existing payloads are
    byte-identical. Keys are passed through untouched: nothing is validated,
    reserved, or rewritten. Contributions are combined by merge_meta (shallow,
    later wins), the single seam a future feature injects its own key through.
    On the tools/call result envelope _meta is per-call rather than
    per-binding, so it is a build_tool_result(..., meta=…) argument.

Changed

  • resources/read renders and encodes through one shared builder. The sync
    and async read handlers are full parallel implementations, not wrappers, so
    the serializer/encoding step now lives in
    output.build_resource_contents.build_resource_contents and the two
    transports cannot drift apart on it. No wire change.