---
title: "McpServer source"
url: "https://nsta1.github.io/Orleans.Lattice/samples/McpServer/source.html"
source: "https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/McpServer"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
---
# McpServer source

Part of [MCP Server sample](README.md).

The source of the [McpServer](https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/McpServer) sample.

## Program.cs

````csharp
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using Orleans.Lattice;
using Orleans.Lattice.Api.Auth;
using Orleans.Lattice.Api.Data;
using Orleans.Lattice.Api.Mcp;
using Orleans.Lattice.Api.State;
using Orleans.Lattice.Auth;
using Orleans.Lattice.Membership;
using Orleans.Lattice.Samples.McpServer;

// ---------------------------------------------------------------------------
// Orleans.Lattice.Api.Mcp sample: a co-hosted single silo that exposes the
// Model Context Protocol (MCP) server over streamable HTTP, then drives it with
// a real MCP client end-to-end.
//
// One WebApplication process runs the whole stack: an Orleans silo with the core
// tree, Membership (identity), Auth (a default-deny enforcement gate), and the
// three transport-agnostic API facades (state, data, auth). On top of those the
// MCP server advertises the facades as MCP tools, scoped per caller by the
// caller's authorization grants.
//
// The sample proves the two headline properties of the MCP surface:
//
//   1. Permission-scoped discovery. An authenticated agent that has been granted
//      access sees the state / data / auth tool set and can call a tool
//      end-to-end over MCP.
//   2. Fail-closed by default. A caller the credential bridge cannot authenticate
//      is offered NOTHING - not even the lattice_capabilities meta-tool.
//
// Authorization on the endpoint is disabled purely to keep the sample
// one-command runnable with no identity provider; a demo credential bridge maps a
// request carrying a marker header onto a fixed "agent" credential, and a demo
// authenticator resolves that credential to the "agent" subject inside the
// cluster. A real deployment leaves RequireAuthorization at its secure default
// and lifts an authenticated ASP.NET Core principal onto the credential.
// ---------------------------------------------------------------------------

const string DemoTree = "catalog";
const string Agent = DemoCredentialBridge.AgentSubject;
const string Scheme = DemoAuthenticator.Scheme;
const int Port = 5290;

// Every data-plane + admin capability the registered tool groups require, in one
// mask, so a single Allow rule unlocks the whole granted tool set for the agent.
const LatticeOperation AllOperations =
    LatticeOperation.Read | LatticeOperation.Write | LatticeOperation.Delete |
    LatticeOperation.RangeRead | LatticeOperation.RangeDelete | LatticeOperation.CrdtApply |
    LatticeOperation.AtomicWrite | LatticeOperation.BulkLoad | LatticeOperation.Admin |
    LatticeOperation.Backup | LatticeOperation.Restore;

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.UseUrls($"http://localhost:{Port}");

builder.Host.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();
    silo.AddMemoryGrainStorageAsDefault();
    silo.UseInMemoryReminderService();
    silo.AddLattice((services, name) => services.AddMemoryGrainStorage(name));

    // Membership resolves the ambient caller credential into a subject.
    silo.AddLatticeMembership();

    // Auth installs the default-deny enforcement gate. "root-admin" is a
    // bootstrap administrator so the sample can seed the user + rule before any
    // rule exists.
    silo.AddLatticeAuth(options =>
    {
        options.DefaultEffect = LatticeEffect.Deny;
        options.BootstrapAdministrators.Add("root-admin");
    });

    // The trusted-token authenticator that maps a credential's token to the
    // caller subject id (a real deployment uses JWT / Entra).
    silo.Services.AddSingleton<ILatticeCredentialAuthenticator, DemoAuthenticator>();

    // The three transport-agnostic facades the MCP tools adapt.
    silo.AddLatticeStateApi();
    silo.AddLatticeDataApi();
    silo.AddLatticeAuthApi();
});

// The demo credential bridge is registered BEFORE AddLatticeMcp so its
// TryAdd-registered HttpContext bridge is skipped and ours wins.
builder.Services.AddSingleton<ILatticeApiMcpCredentialBridge, DemoCredentialBridge>();

// The MCP server front door. RequireAuthorization is disabled purely to keep the
// sample one-command runnable; discovery is still fail-closed and
// permission-scoped underneath.
builder.Services.AddLatticeMcp(options => options.RequireAuthorization = false);

// Opt in to the three tool modules, with writes and auth administration enabled
// so the agent's full granted surface is exercised.
builder.Services.AddStateTools();
builder.Services.AddDataTools(enableWrites: true);
builder.Services.AddAuthTools(enableAdministration: true);

var app = builder.Build();
app.MapLatticeMcp();
await app.StartAsync();

Console.WriteLine($"Silo + MCP server started on http://localhost:{Port}\n");

var store = app.Services.GetRequiredService<ILatticeAuthorizationPolicyStore>();
var grainFactory = app.Services.GetRequiredService<IGrainFactory>();
var tree = grainFactory.GetGrain<ILattice>(DemoTree);

// -- Seed the agent, its grant, and some data ------------------------------
// Seeding writes the reserved policy tree, which requires Admin, so it runs as
// the bootstrap administrator (which bypasses the gate).
Console.WriteLine("Seeding an 'agent' subject with a full-access grant on the demo tree...");
using (LatticeCredentialContext.Use("root-admin", scheme: Scheme))
{
    await store.PutRuleAsync(new LatticeAuthorizationRule(
        "agent-all",
        LatticeSubjectSelector.User(Agent),
        LatticeScope.Tree(DemoTree),
        AllOperations,
        LatticeEffect.Allow));

    for (var i = 0; i < 5; i++)
    {
        await tree.SetAsync($"item/{i:D3}", Encoding.UTF8.GetBytes($"value-{i}"));
    }
}

// -- Act 1: the authenticated, granted agent --------------------------------
Console.WriteLine("\n== Act 1: an authenticated, granted agent discovers and calls tools ==");
await using var agentClient = await ConnectAsync(withAgentHeader: true);

// The compiled policy snapshot rebuilds off the policy-tree change feed, so poll
// the advertised tool list until the grant is reflected.
var agentTools = await WaitForToolsAsync(agentClient, TimeSpan.FromSeconds(15));
var toolNames = agentTools.Select(t => t.Name).OrderBy(n => n, StringComparer.Ordinal).ToArray();

Console.WriteLine($"  agent sees {toolNames.Length} tools, including:");
foreach (var name in toolNames.Where(n => n is "lattice_capabilities"
             or "lattice_state_list_trees" or "lattice_data_get" or "lattice_auth_explain"))
{
    Console.WriteLine($"    - {name}");
}

var getEntry = await agentClient.CallToolAsync(
    "lattice_data_get",
    new Dictionary<string, object?> { ["treeId"] = DemoTree, ["key"] = "item/000" });
Console.WriteLine($"  called lattice_data_get(item/000) -> isError={getEntry.IsError == true}, "
    + $"structured={getEntry.StructuredContent?.ToString()}");

// -- Act 2: the anonymous caller (fail-closed) ------------------------------
Console.WriteLine("\n== Act 2: an unauthenticated caller is offered nothing ==");
await using var anonClient = await ConnectAsync(withAgentHeader: false);
var anonTools = await anonClient.ListToolsAsync();
Console.WriteLine($"  anonymous caller sees {anonTools.Count} tools (fail-closed).");

var ok = toolNames.Contains("lattice_state_list_trees") && anonTools.Count == 0;
Console.WriteLine();
Console.WriteLine(ok
    ? "[OK] permission-scoped discovery granted the agent its tools; the anonymous caller got none."
    : "[FAIL] the sample did not reach the expected end state.");

await app.StopAsync();
return ok ? 0 : 1;

// --- helpers ---------------------------------------------------------------

// Connects a real MCP client over streamable HTTP. When withAgentHeader is set,
// the request carries the marker header the demo bridge maps to the agent.
async Task<McpClient> ConnectAsync(bool withAgentHeader)
{
    var options = new HttpClientTransportOptions
    {
        Endpoint = new Uri($"http://localhost:{Port}"),
        Name = withAgentHeader ? "agent" : "anonymous",
    };

    if (withAgentHeader)
    {
        options.AdditionalHeaders = new Dictionary<string, string>
        {
            [DemoCredentialBridge.AgentHeader] = "true",
        };
    }

    return await McpClient.CreateAsync(new HttpClientTransport(options));
}

// Polls the advertised tool list until it is non-empty (the grant has been
// compiled into the policy snapshot) or the budget elapses.
async Task<IList<McpClientTool>> WaitForToolsAsync(McpClient client, TimeSpan budget)
{
    var deadline = DateTime.UtcNow + budget;
    while (DateTime.UtcNow < deadline)
    {
        var tools = await client.ListToolsAsync();
        if (tools.Count > 0)
        {
            return tools;
        }

        await Task.Delay(TimeSpan.FromMilliseconds(500));
    }

    return await client.ListToolsAsync();
}
````

## DemoAuthenticator.cs

````csharp
using Orleans.Lattice.Membership;

namespace Orleans.Lattice.Samples.McpServer;

/// <summary>
/// A minimal demo <see cref="ILatticeCredentialAuthenticator"/> that trusts the
/// ambient credential's token as the caller subject id. It handles only
/// credentials stamped with <see cref="Scheme"/>, so it never shadows the
/// built-in anonymous authenticator for an unstamped (system-origin) turn.
///
/// A real deployment resolves the subject from a validated JWT or Entra token
/// (see the authenticators shipped with the Membership package); this sample uses
/// a trivial trusted-token authenticator so the whole flow runs on one silo with
/// no identity provider.
/// </summary>
internal sealed class DemoAuthenticator : ILatticeCredentialAuthenticator
{
    /// <summary>The scheme hint this authenticator claims.</summary>
    public const string Scheme = "demo-scheme";

    /// <summary>The issuer stamped on the resolved principal.</summary>
    public const string Issuer = "https://issuer.mcp.sample/";

    /// <inheritdoc />
    public bool CanHandle(in LatticeCredential credential) =>
        string.Equals(credential.Scheme, Scheme, StringComparison.Ordinal);

    /// <inheritdoc />
    public ValueTask<LatticePrincipal?> AuthenticateAsync(
        LatticeCredential credential,
        CancellationToken cancellationToken = default) =>
        new(new LatticePrincipal(credential.Token, Issuer));
}
````

## DemoCredentialBridge.cs

````csharp
using Microsoft.AspNetCore.Http;
using Orleans.Lattice.Api.Mcp;
using Orleans.Lattice.Auth;

namespace Orleans.Lattice.Samples.McpServer;

/// <summary>
/// A minimal demo <see cref="ILatticeApiMcpCredentialBridge"/> that stands in for
/// a real authentication integration. In a production host the built-in
/// HttpContext bridge lifts an authenticated ASP.NET Core principal onto the
/// ambient Lattice credential; here, to keep the sample runnable with no identity
/// provider, this bridge simply maps a request that carries the
/// <see cref="AgentHeader"/> marker header onto a fixed <c>agent</c> credential,
/// and treats every other request as anonymous.
///
/// The credential's scheme is <see cref="DemoAuthenticator.Scheme"/> so the
/// cluster's authenticator resolves it to the <c>agent</c> subject when the agent
/// actually invokes a tool, and its <c>PrincipalId</c> is <c>agent</c> so the MCP
/// discovery core scopes the advertised tool list to that subject's grants.
/// </summary>
internal sealed class DemoCredentialBridge : ILatticeApiMcpCredentialBridge
{
    /// <summary>The marker header a request sends to be treated as the agent.</summary>
    public const string AgentHeader = "x-demo-agent";

    /// <summary>The subject id the agent request resolves to.</summary>
    public const string AgentSubject = "agent";

    /// <inheritdoc />
    public LatticeCredential? Resolve(HttpContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        // Fail closed: only a request carrying the marker header is the agent;
        // everything else is anonymous and is offered no tools.
        if (!context.Request.Headers.ContainsKey(AgentHeader))
        {
            return null;
        }

        return new LatticeCredential(
            token: AgentSubject,
            scheme: DemoAuthenticator.Scheme,
            principalId: AgentSubject);
    }
}
````

## McpServer.csproj

````xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>Orleans.Lattice.Samples.McpServer</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.McpServer</AssemblyName>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Orleans.Server" Version="10.2.2" />
    <PackageReference Include="ModelContextProtocol" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\lattice\Orleans.Lattice.csproj" />
    <ProjectReference Include="..\..\src\lattice.membership\Orleans.Lattice.Membership.csproj" />
    <ProjectReference Include="..\..\src\lattice.auth\Orleans.Lattice.Auth.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.state\Orleans.Lattice.Api.State.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.data\Orleans.Lattice.Api.Data.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.auth\Orleans.Lattice.Api.Auth.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.mcp\Orleans.Lattice.Api.Mcp.csproj" />
  </ItemGroup>

</Project>
````
