Table of Contents

Orleans.Lattice.Api.Mcp.Apps

This page documents Orleans.Lattice.Api.Mcp.Apps, which is unreleased, in the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.

The MCP tool surface for installable apps: every enabled app's tools are advertised on the single Lattice MCP endpoint, namespaced by the app's slug and gated per caller by the app's role bindings: a caller sees a tool when a group it belongs to is bound to the tool's role, unless a deny takes the role away.

What is it?

An app declares its MCP tools in its manifest (mcpTools: an app-local name, a description and the role the tool requires) and supplies their implementations in code through IAppMcpToolProvider. This package pairs the two for every enabled app and adds the resulting tools to each MCP session, alongside the built-in and facade-group tools, through the same per-session tool collection, credential bridge, fail-closed resolution and advertise-and-invoke authorization path.

There is one MCP endpoint for every app. Nothing about the facade groups, their tools or the lattice_capabilities report changes when this package is registered; app tools never appear in that report.

Registration

Register the surface next to the MCP server and the apps engine, then contribute each app's tools:

using ModelContextProtocol.Server;
using Orleans.Lattice.Apps;
using Orleans.Lattice.Api.Mcp.Apps;

public static class CrmMcpTools
{
    public static IServiceCollection Register(
        IServiceCollection services,
        IEnumerable<McpServerTool> tools)
    {
        services.AddAppMcpTools();
        services.AddSingleton<IAppMcpToolProvider>(
            new AppMcpToolProvider(AppSlug.Parse("crm"), tools));
        return services;
    }
}

AddAppMcpTools is idempotent. AppMcpToolProvider is a ready-made IAppMcpToolProvider; implement the interface directly when the tool set is built some other way. The surface needs the app registry projection, the app source and the shared access gate in the container, and at least one registered IAppMcpToolProvider; when any of them is missing it offers no app tools. The host's MCP authorizer must admit the namespaced tool names.

Tool names

Each tool is advertised as {slug}_{tool}, for example crm_find_contact. A provider names its tools by their app-local name (find_contact); the surface applies the prefix. Because slugs are unique and may not contain _, two apps can reuse the same local name without colliding, and a namespaced name parses back to its app and tool unambiguously (AppMcpToolName.Compose and AppMcpToolName.TryParse).

A namespaced name that collides with a tool already in the session - a built-in meta-tool or a facade-group tool - is skipped with a warning, so an app can never shadow a built-in tool. The slugs lattice and repocontext lead the built-in tool namespaces (lattice_* and repocontext_*), so an app with either slug contributes no tools at all: for a caller the built-in tool is withheld from, nothing would collide, and the app's tool would otherwise be advertised under the built-in tool's name.

Exact pairing

For each enabled app the surface pairs the manifest's mcpTools declarations with the union of every provider registered for the app's slug. The pairing must be exact: a declared tool with no implementation, an implementation the manifest does not declare, a local name declared or implemented twice, an implementation with no name, or a declaration naming a role the manifest does not declare fails the whole app's tool activation. The app then contributes no tools at all and the failure is logged. This replaces the warn-and-skip behaviour the facade groups use, because first-wins registration would let a second contribution shadow the first.

Authorization

Like every other Lattice MCP tool, app tools are offered only to an authenticated caller. An app role is held by binding: a caller holds the tool's declared role when its transitive group closure contains a membership group the install binds to that role, and the role's compiled app:{slug}: rules confer at least one operation and one scope. Rights the caller holds through any other rule never make it hold an app role, so a broad operator grant does not reveal an app's tools.

The shared access gate is asked only after the binding holds, and only to take the role away. For each of the role's operations it is asked on each of the role's scopes, in the scope's own shape; the role stays held when on at least one scope the gate refuses none of its operations. A key-filtered answer is tested at a representative key of the scope (the key itself, the prefix itself, or the empty key for a whole tree), so it can only narrow, never grant. An explicit deny on a bound member, on the role's trees or cluster-wide, therefore withholds the tool; a deny narrower than the scope leaves the role held and is enforced by the data path when the tool reads or writes. The scopes are resolved exactly as the role compiler resolves them - the app's own a/{app}/{tree}, an adopted tree, or another app's tree - and composed for the caller's active tenant. Because the session's tool collection serves both tools/list and tools/call, a tool withheld at advertisement is unreachable at invocation, and the decision is checked again at invocation against the current registry state, so disabling an app or revoking a grant takes effect mid-session. The tool itself then runs under the caller's credential, so every data-plane call it makes is authorized again by the gate.

App tools are served in-process by the silo that hosts the app, so a region argument is accepted only when it names the current region; a peer region is rejected rather than served locally under its name.

The tool catalogue is rebuilt whenever the app registry changes, shortly after the change commits; each session selects from the prebuilt lists.

See also