# MCP tool server (app.McpServer)

> `app.McpServer` exposes your app OUTWARD, to somebody else's agent, as a set of tools. Tools are grouped into named `Catalogs`, each gated by a `VisibleTo` policy (who may see it). A tool is one of: `new Tool(Function)` (call one of your functions), `new CrudTool<Entity>() { Operations = [...] }` (create/read/update/delete an entity), or `new Tool(Knowledge.Search)` (semantic search). A singleton. For the INWARD direction — a tool your OWN declared agent may call — see `agent-skills`; that is a `skill`, not a `Tool`.

<!-- id: config-mcp-server · area: config · stability: stable · html: https://osysharp.com/reference/config/mcp-server/ -->

## Summary        {#summary}

⚠ **This page is the OUTWARD direction.** If you are looking for what your own declared `agent` can call, you want
[A skill — a typed job an agent can be given, its check and its tools (Agents kit)](https://osysharp.com/reference/agent/skills/) — a tool an agent of yours may reach for is a `skill`, and it is granted with `Skills = [ … ]`.
There is no `Tools =` member on an `agent`; `Tools` belongs to a `ToolCatalog`, below.

`app.McpServer` exposes your application to an MCP (Model Context Protocol) client — typically an AI agent — as a set of
**tools** the agent may call. As with the REST surface, you **expose what you already have** rather than write a tool
handler: a tool is one of your functions, an entity's CRUD operations, or a semantic search over your knowledge. Tools
are grouped into named **catalogs**, and each catalog is gated by a `VisibleTo` policy that decides which principals may
see and call it. `app.McpServer` is a **singleton** — an app has at most one MCP server.

```osy syntax
app.McpServer = new McpServer {
  Catalogs = [
    new ToolCatalog("admin-tools") {
      Description = "Admin-only tools",
      VisibleTo   = IsAdmin,
      Tools = [ new Tool(OrderTotal) ],
    },
  ],
};
```

## Signature      {#signature}
```osy syntax
app.McpServer = new McpServer {
  Catalogs = [                        // one entry per named group of tools
    new ToolCatalog("Name") {         // the catalog's name, shown to the client
      Description = "…",              // optional; describes the catalog to the agent
      VisibleTo   = IsAdmin,          // a declared `policy` — who may see this catalog
      Tools = [                       // the tools this catalog exposes
        new Tool(OrderTotal),                                    // call a function
        new CrudTool<Order>() { Operations = [CrudOp.Read] },    // CRUD over an entity
        new Tool(Knowledge.Search),                              // semantic search
      ],
    },
  ],
};
```

`app.McpServer` is a singleton; its `Catalogs` is a list — an app may group its tools into several independently-gated
catalogs.

## Description    {#description}
A `ToolCatalog` has:

- **`"Name"`** — the catalog's name, passed to the constructor, shown to the MCP client.
- **`Description`** *(optional)* — prose describing the catalog to the agent.
- **`VisibleTo`** — the name of a declared `policy`. The catalog (and every tool in it) is only visible and callable to
  principals for whom the policy holds. `VisibleTo` must name a **declared policy** — an undeclared name is a compile
  error that lists your policies. Any declared policy works: one over the caller's role
  (`policy IsAdmin => user.Role == AppRole.Admin;`), over a role grant, or over your own data
  (`policy IsMember => Membership.Any(m => m.User == user && m.Active);`). A caller the policy does not admit — and a
  caller who is not signed in, unless the policy admits visitors ([[#no-principal]]) — does not see the catalog at all.
- **`Tools`** — a list of tools. A tool is one of three kinds:
  - **`new Tool(Function)`** — exposes one of your functions as a callable tool. The function must be declared — an
    unknown name is a compile error that lists your functions.
  - **`new CrudTool<Entity>() { Operations = [CrudOp.…] }`** — exposes create/read/update/delete over one entity;
    `Operations` chooses which of `Create` / `Read` / `Update` / `Delete` are available. The entity must be defined in
    your app.
  - **`new Tool(Knowledge.Search)`** — exposes semantic (vector) search over your app's searchable knowledge.

**To see what your app exposes without connecting a client**, run `osy model --json`: its `mcpCatalogs` lists each
catalog with its `visibleTo` policy and its tools (`kind` is `function`, `crud` or `builtin`; `name` is the function or
the entity; `operations` is a CRUD tool's mask, e.g. `"CR"`).

### What an agent is told about a function tool    {#described}

An agent picks a tool by its description and fills its arguments from their schema. Both come from the function
itself:

```osy title="the comment is the description; the signature is the schema" syntax
/// Put a question to the person you are working for, and get their decision.
/// <param name="question">The question, as one sentence.</param>
/// <param name="options">Two to four answers to choose from.</param>
string Ask(string question, List<Choice> options) { … }

class Choice {
  /// The answer, in a few words.
  public string Label = "";
  /// What choosing it costs.
  public string Tradeoff = "";
}
```

- **The description is the function's `///` comment** — its sentences, or its `<summary>` when it uses C#'s tags.
  `new Tool(Ask) { Description = "…" }` replaces it for that catalog; a function with no comment is described as
  "Execute the Ask function", which tells an agent nothing, so write the comment. The comment is kept up to its
  first 1,000 characters.
- **Each argument's description is its `<param name="…">` line.**
- **A list, array, set or dictionary argument is described as one** — `List<string>` is an array of strings,
  `List<Choice>` an array of objects with `Choice`'s members (their `///` comments as descriptions, the members that
  cannot be null as required), a nested class as an object. It is the document [Json.SchemaOf<T>() — the JSON Schema of a type](https://osysharp.com/reference/json/schema-of/) answers for the
  same type, so what an agent sends is what the function receives.
- **A parameter with a default is optional** — not in `required`, with its value as `default` (`int times = 1` →
  `"default": 1`, an enum member by its name). An agent that leaves it out gets that value, as a call in Osy# does.

### Who may connect    {#who-may-connect}

A deployed app's tool server answers only a caller who presents a key the app issued (`X-API-Key`) or a signed-in
session. On your **local** platform a caller with no key is let in as a signed-out visitor — it sees `list_types`,
`whoami` and whatever a catalog leaves open to everyone — so that an app can be tried from an MCP client before it
has accounts.

**Two tools every caller has.** `whoami` answers the caller's own account id (`userId`, empty for a signed-out
visitor), their roles and whether they are signed in — nothing about anybody else. `list_types` answers the data types **this caller's own tools** take or return, and the types those
refer to through members the caller may read, each with its member count (members hidden from the caller on every row
are not counted — see [below](#fields-told)). A caller who can see no tool is told no types, and a type no tool of
theirs touches is never named — so a key handed to an agent for six tools about one record does not learn the rest of
the app's shape. The summary a session is sent when it connects lists the same types, with the fields that caller may
read. People using the app's own pages are not affected; an administrator's key is told more only because more
catalogs are visible to it.

**One exception: an app that uses the computer it runs on (`use Osysharp.Host`) answers no caller without a key,
locally too.** Such an app can start programs and open files, and a tool client is any program on the machine, not a
page of the app. A connection with no key gets `401` and a sentence saying which header to send.

### A catalog for anyone, in an app nobody signs in to    {#no-principal}

`VisibleTo` needs a declared `[Principal]` only when its policy asks about the caller — names `user`. A policy that
never does compiles in an app with no `[Principal]` at all. "Anyone" is said the way it is everywhere else,
`IsAnonymous || IsAuthenticated` ([principal predicates (IsAuthenticated / IsAnonymous / IsTheApp) and open reads](https://osysharp.com/reference/security/principal-predicates/)); with no principal every caller is anonymous, so
the catalog is open to every caller that reaches the tool server ([[#who-may-connect]]):

```osy title="no accounts: one catalog, open to every caller" test app=config-mcp-no-principal
entity Recipe {
  [Required, MaxLength(120)] string Title;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

policy Anyone => IsAnonymous || IsAuthenticated;

/// How many recipes there are.
int CountRecipes() { return Recipe.Count(); }

app.McpServer = new McpServer {
  Catalogs = [
    new ToolCatalog("recipes") {
      Description = "Read the recipe book",
      VisibleTo = Anyone,
      Tools = [ new Tool(CountRecipes), new CrudTool<Recipe>() { Operations = [CrudOp.Read] } ],
    },
  ],
};
```

A policy that names `user` — `policy Gate => user != null;` — still needs someone to be: in an app with no
`[Principal]` it is refused, and the refusal says to declare one.

### A connected client follows your deploys    {#follows-deploys}

An MCP client keeps its connection open for as long as it runs — an editor all day, an agent host for days. When you
deploy a new version of the app, the client's **next call runs on the new version**: the tool it calls is the new one,
over the data as it is now, and the client is sent `notifications/tools/list_changed` so it fetches the new tool list.
A tool the new version no longer has answers "Tool '…' not found". The same happens when the caller's roles change: the
next call acts with the roles they have now. Nobody has to reconnect.

The summary a session was sent when it connected is not re-sent; a client that wants the new one reconnects.

### What a caller is told about a record's fields    {#fields-told}

Every place the tool server describes a record to a caller — `describe_Order`, `get_entity_schema`, the arguments of
`create_Order` and `update_Order`, the argument of a function tool that takes an `Order`, `list_types`' member counts,
and the summary a session is sent when it connects — follows the record's own `security {}` block, **as that caller**:

```osy title="one field, three callers" syntax
entity Order {
  [MaxLength(200)] string CustomerRef;
  decimal Margin;
  [MaxLength(500)] string? Notes;
  bool Shared;
  security {
    allow read when IsAuthenticated;
    allow create when IsAuthenticated;
    deny read Margin when !IsAdmin;     // hidden on EVERY row from a non-admin
    deny read Notes;                    // hidden…
    allow read Notes where Shared;      // …except on a row that says so
  }
}
```

- **A field the caller may read on no row is not named, and not counted.** A non-admin's `describe_Order` lists
  `CustomerRef`, `Notes` and `Shared` — not `Margin`. An admin's lists all four.
- **A field hidden only on some rows (a rule with `where`) is still described.** Whether `Notes` may be read depends on
  the row, and a description has no row to ask; its value is still withheld on every row that hides it.
- **A field the caller may write but not read is named only where it is written, marked `"writeOnly": true`.** Here a
  non-admin may create an `Order`, so `create_Order`'s arguments — and `get_entity_schema`, when the caller's catalog
  offers `Create` — include `Margin`, write-only; `describe_Order` does not. A field is offered for writing when the
  caller may create the record (`create_…`, and a function argument, which builds the record from its fields) or may
  update that field (`update_…`). Where neither holds, it is not named anywhere.

The data itself was never at risk — a read returns nothing the block withholds. What this decides is the *description*:
a key that may never see a field is not told its name, type or length.

A catalog may not mix `CrudTool` and `PolymorphicTool` — the compiler refuses it; give each its own catalog. Each
`Tools` entry is exactly one tool. You write no tool schema, no argument parsing, and no dispatch: the signature of
the function and the shape of the entity are the contract.

## Examples       {#examples}
A complete app that exposes one function and one entity's full CRUD as an admin-only catalog. An admin is somebody
signed in, so this catalog needs a declared `[Principal]`, its `[Role]` enum, and the `policy` that `VisibleTo`
references (a catalog for anyone needs none of them — [[#no-principal]]):

```osy title="basic" test app=config-mcp-example
entity Order { [Required, MaxLength(200)] string CustomerRef; decimal Total; }

[Role] enum AppRole { Staff, Admin }
[Principal] entity User {
  [MaxLength(200)] string Email;
}
[RoleGrant] entity RoleGrant { [Required] User Member; [Required] AppRole Role; }

policy IsAdmin => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Admin);

decimal OrderTotal(decimal subtotal, decimal tax) { return subtotal + tax; }

app.McpServer = new McpServer {
  Catalogs = [
    new ToolCatalog("admin-tools") {
      Description = "Admin-only tools",
      VisibleTo = IsAdmin,
      Tools = [
        new Tool(OrderTotal),
        new CrudTool<Order>() { Operations = [CrudOp.Create, CrudOp.Read, CrudOp.Update, CrudOp.Delete] },
      ],
    },
  ],
};
```

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous / IsTheApp) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `[Principal]`, `[Role]`, and `policy` that `VisibleTo` gates a catalog with
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`, the HTTP surface that exposes the same functions and entities to non-agent callers
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the searchable knowledge that `new Tool(Knowledge.Search)` exposes
- [Calling your own MCP tool catalog from a test](https://osysharp.com/reference/testing/mcp-calls/) — `Mcp.List`/`Mcp.Call`: driving THIS catalog from your own `.test.osy`, over a real MCP
  session — what an agent with a given credential actually sees and can call
