# MCP Server — Exposing project capabilities as MCP

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** mcp server, expose mcp, build mcp server, mcp host, publish tools, McpServerTool, AddMcpServer, request filters, stateless, spec 2026-07-28
> **Read by Claude in:** implement (quando um projeto expõe capacidades como MCP server para consumo externo)

**Verified against:** ModelContextProtocol 2.2.0 + ModelContextProtocol.AspNetCore 2.2.0 (ai-pin 2026-09-08; o `.AspNetCore` não está no pin — versão apurada no nuget.org em 2026-09-08) + revisão de spec MCP **2026-07-28** (modelcontextprotocol.io/specification/versioning, lido 2026-09-08). **Verificação documental — o ai-kit não exercita MCP**, e por isso este standard NÃO carimba `provado por`. Os nomes de tipo, os defaults e os avisos de obsolescência abaixo foram **medidos por reflexão sobre as assemblies 2.2.0** em 2026-09-08. Last-verified: 2026-09-08.

---

## Quando criar um MCP server

Crie um MCP server quando você quer **expor capacidades do projeto** de forma padronizada para **agentes ou ferramentas externas** consumirem. O protocolo MCP garante interoperabilidade: qualquer agente compatível (Claude Code, outro agent MAF, VS Code Copilot, etc.) pode descobrir e usar as tools do servidor.

**Crie um MCP server quando:**

- Claude Code (ou outro agente externo) precisa executar queries/ações neste projeto
- Outro projeto MAF precisa consumir lógica encapsulada aqui via protocolo padrão
- Você quer expor capacidades de negócio de forma descobrível via `ListTools`
- O projeto é um serviço de infraestrutura que deve ser consumível cross-framework

**NÃO crie um MCP server quando:**

- As tools são usadas apenas pelos agentes do próprio projeto → use tools nativas `[Description]`
- É uma lógica de um único endpoint interno → `AIFunctionFactory.Create()` é suficiente
- A integração é ponto-a-ponto com um único consumidor → DI direta ou A2A é mais simples

---

## A API

### Não existe "MCP 2.0" como revisão de especificação

A confusão vale ser desfeita antes de qualquer código, porque ela muda o que se procura na documentação:

| Coisa | Como é versionada | Valor corrente (2026-09-08) |
|---|---|---|
| **A especificação MCP** | por **data** | revisão **`2026-07-28`** (sucede `2025-11-25`) |
| **O SDK C# `ModelContextProtocol`** | SemVer | **2.2.0**, publicado 2026-08-13 |

"MCP 2.0" é a **major do SDK**, nunca uma revisão de spec. Fonte: modelcontextprotocol.io/specification/versioning e o changelog da revisão `2026-07-28`, lidos em 2026-09-08.

### O que a revisão `2026-07-28` mudou para quem escreve servidor

- **Sessões removidas** — o cabeçalho `Mcp-Session-Id` deixou de existir (**SEP-2567**).
- **`initialize` / `notifications/initialized` removidos** — versão e capacidades viajam em `_meta` **por request** (**SEP-2575**).
- Novo RPC **`server/discover`**.
- **`ping`, `logging/setLevel` e `notifications/roots/list_changed` removidos.**
- **Roots, Sampling e Logging deprecados** (**SEP-2577**).
- **Tasks** (long-running) saíram do core para a extensão `io.modelcontextprotocol/tasks`.
- **Elicitation/sampling substituídos por MRTR** — `InputRequiredResult` + `InputRequest.ForElicitation(...)` (**SEP-2322**).
- **Resultado cacheável** — `ICacheableResult` com `TimeToLive` e `CacheScope` (**SEP-2549**).
- **HTTP+SSE formalmente Deprecated** (Streamable HTTP é o transporte).

> **Duas dessas afirmações não dependem do changelog: elas estão gravadas na assembly.** Medido em `ModelContextProtocol.AspNetCore` 2.2.0 e `ModelContextProtocol.Core` 2.2.0, no texto do `[Obsolete]` de cada membro:
> - `HttpServerTransportOptions.EventStreamStore`/`IdleTimeout`/`MaxIdleSessionCount`/`PerSessionExecutionContext`/`SessionMigrationHandler` — *"Stateful Streamable HTTP mode is a back-compat-only escape hatch for 2025-11-25 protocol revision clients and earlier. Set `HttpServerTransportOptions.SessionMode = HttpServerSessionMode.Stateless` (**the default as of the 2026-07-28 protocol revision**) for new code. See SEP-2567."*
> - `McpServerOptions.MaxSamplingOutputTokens` — *"The Sampling feature is deprecated as of specification version 2026-07-28 … See SEP-2577."*
> - `HttpServerTransportOptions.EnableLegacySse` — obsoleto: *"…Use Streamable HTTP instead."*

### A API C# de servidor NÃO mudou de nome

`AddMcpServer()`, `WithStdioServerTransport()`, `WithHttpTransport(...)`, `WithToolsFromAssembly()`, `WithTools([...])`, `[McpServerToolType]`, `[McpServerTool]`, `McpServerTool.Create(...)` e `app.MapMcp()` continuam iguais. **O que mudou é o default**, não a grafia.

**Packages:**

```xml
<!-- SDK cliente+servidor do protocolo MCP — no ai-pin.json -->
<PackageReference Include="ModelContextProtocol" Version="2.2.0" />

<!-- Pacote base MAF (para AsAIAgent, AsAIFunction) — no ai-pin.json -->
<PackageReference Include="Microsoft.Agents.AI" Version="1.20.0" />

<!-- Apenas se o transporte for HTTP (server-side).
     NÃO está no ai-pin.json; versão apurada no nuget.org em 2026-09-08. -->
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.2.0" />
```

### Stateless é o DEFAULT, não uma escolha

Medido em `ModelContextProtocol.AspNetCore` 2.2.0, instanciando `HttpServerTransportOptions` e lendo os valores iniciais: **`SessionMode == HttpServerSessionMode.Stateless`** e `Stateless == true`. Os valores de `HttpServerSessionMode` são `Stateless` (0), `Stateful` (1) e `StatefulForInitializeClients` (2).

Escrever `o.Stateless = true` hoje é redundante; escrever `o.SessionMode = HttpServerSessionMode.Stateful` é **optar por um escape hatch de compatibilidade** com clientes da revisão `2025-11-25` e anteriores — e é isso que a mensagem de obsolescência diz. A versão anterior deste standard apresentava stateless como uma decisão de escala do autor; o protocolo já a tomou.

**Dois jeitos de declarar tools:**

| Padrão | Quando usar |
|--------|-------------|
| Atributos `[McpServerToolType]` + `[McpServerTool]` | Tools são métodos do projeto — padrão canônico do SDK. Descoberta automática via `WithToolsFromAssembly()`. |
| `McpServerTool.Create(...)` | Embrulhar um agente MAF (`agent.AsAIFunction()`) ou um `AIFunction` construído em runtime como tool. |

**Padrão de servidor (stdio):**

```
Tools ([McpServerTool] ou McpServerTool.Create)
    → services.AddMcpServer()            // registra o servidor
    → .WithStdioServerTransport()        // transporte: stdio (Claude Code, subprocesso)
    → .WithToolsFromAssembly()           // descobre [McpServerTool] no assembly
       ou .WithTools([tool])             // registra tools criadas em runtime
    → Host.Build().RunAsync()            // sobe o servidor
```

**Transport stdio** é o padrão para servidores locais (subprocessos, Claude Code tools). **HTTP server-side** usa o pacote `ModelContextProtocol.AspNetCore`: troque `WithStdioServerTransport()` por `WithHttpTransport()` e exponha o endpoint com `app.MapMcp()` num `WebApplication`.

---

## Exemplo C# — tools por atributo (stdio)

Padrão canônico: tools são métodos estáticos decorados, descobertos por assembly.

```csharp
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()    // stdio: Claude Code / subprocesso
    .WithToolsFromAssembly();      // descobre [McpServerToolType] neste assembly

await builder.Build().RunAsync();

// ─── Tool exposta via atributo ──────────────────────────────────────────────
[McpServerToolType]
public static class OrderTools
{
    [McpServerTool, Description("Returns the current status of an order given its ID")]
    public static async Task<string> GetOrderStatus(
        [Description("The order identifier")] string orderId,
        CancellationToken ct)
    {
        // Validação de input antes de qualquer I/O — ver seção Segurança
        if (string.IsNullOrWhiteSpace(orderId) || orderId.Length > 36)
            return "Error: invalid orderId format.";

        // lógica real do projeto aqui
        return $"Order {orderId}: Confirmed";
    }
}
```

> Tools por atributo podem ser instância (não-estáticas) — nesse caso o tipo é resolvido via DI,
> permitindo injetar repositories/serviços no construtor. Métodos estáticos são o caso mais simples.

## Exemplo C# — agente MAF como tool (runtime)

Quando a tool é um agente MAF, use `McpServerTool.Create` com `agent.AsAIFunction()`:

```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;

// _modelRegistry vem de ModelRegistry injetado (ai-agents-providers-model-registry)
AIAgent queryAgent = _modelRegistry.GetChatClient("text-default")
    .AsAIAgent(
        instructions: "You answer questions about project data using the available tools.",
        name: "ProjectQueryAgent");

McpServerTool agentTool = McpServerTool.Create(queryAgent.AsAIFunction());

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithTools([agentTool]);       // tool criada em runtime → WithTools, não WithToolsFromAssembly

await builder.Build().RunAsync();
```

## Exemplo C# — transport HTTP

Para expor o MCP server via HTTP (consumível pela rede), use `ModelContextProtocol.AspNetCore`:

```csharp
using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport()          // SessionMode = Stateless já é o default (medido em 2.2.0)
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp();   // expõe o endpoint MCP (Streamable HTTP)
app.Run();
```

> Não escreva `options.Stateless = true`: é redundante desde a revisão `2026-07-28`. Se você
> precisar de `SessionMode = HttpServerSessionMode.Stateful`, saiba que está optando por um
> **escape hatch de compatibilidade** com clientes antigos — o próprio SDK diz isso no
> `[Obsolete]`. Para HTTP em produção, proteja `MapMcp()` com autenticação — ver seção Segurança.

---

## Filtros de request — listar não é poder chamar

Este é o mecanismo que permite **um servidor MCP servir vários tenants** sem um processo por tenant. A API foi medida em `ModelContextProtocol` 2.2.0 (`McpRequestFilterBuilderExtensions`) e `ModelContextProtocol.AspNetCore` 2.2.0 (`HttpMcpServerBuilderExtensions`).

```csharp
services.AddMcpServer()
    .WithListToolsHandler(async (context, ct) => new ListToolsResult { Tools = GetTools() })
    .WithRequestFilters(f =>
    {
        // (1) Poda a LISTAGEM — o cliente não vê a tool.
        f.AddListToolsFilter(next => async (context, ct) =>
        {
            var result = await next(context, ct);
            result.Tools = [.. result.Tools.Where(t => AllowedFor(context.User, t))];
            return result;
        });

        // (2) Bloqueia a CHAMADA — sem isto, quem souber o nome chama assim mesmo.
        f.AddCallToolFilter(next => async (context, ct) =>
        {
            if (!AllowedFor(context.User, context.Params!.Name))   // ClaimsPrincipal por request
                throw new McpException("tool not available for this principal");
            return await next(context, ct);
        });
    })
    .AddAuthorizationFilters();   // a ORDEM importa — ver abaixo
```

**Duas regras que este standard existe para gravar:**

1. **São DOIS filtros, não um.** `AddListToolsFilter` **esconde** a tool; só `AddCallToolFilter` **impede chamá-la**. Um cliente que já conhece o nome — porque leu a lista antes, porque outro tenant tem a mesma tool, ou porque adivinhou — chama direto. Poda de listagem é ergonomia, não autorização.
2. **Cliente já conectado não relê a lista até reconectar.** Revogar acesso podando a listagem não revoga nada para quem já está dentro. A revogação real é o filtro de `CallTool`.

**A ordem do encadeamento é semântica:** um filtro registrado **antes** de `AddAuthorizationFilters()` roda com a lista **completa** — vê todas as tools, inclusive as que a autorização removeria. Se a intenção é filtrar o que já passou pela autorização, registre depois.

Filtros disponíveis (medidos): `AddListToolsFilter`, `AddCallToolFilter`, `AddListPromptsFilter`, `AddGetPromptFilter`, `AddListResourcesFilter`, `AddReadResourceFilter`, `AddListResourceTemplatesFilter`, `AddCompleteFilter`, `AddSubscribeToResourcesFilter`, `AddUnsubscribeFromResourcesFilter`, `AddSetLoggingLevelFilter`.

### Identidade por request

- **`context.User`** é o `ClaimsPrincipal` do request, disponível dentro do filtro.
- Uma tool pode receber por **injeção de parâmetro**: `ClaimsPrincipal`, `RequestContext<T>`, `McpServer` e `IProgress<T>`. O escopo de DI é **por request** — `McpServerOptions.ScopeRequests` tem default **`true`** (medido). `[FromKeyedServices]` funciona.
- **`HttpServerTransportOptions.ConfigureSessionOptions`** permite montar o `ToolCollection` a partir do `HttpContext.User` — poda no nascimento da sessão, complementar (não substituta) ao filtro de `CallTool`.

### Argumento de tool é input NÃO confiável

O que chega nos argumentos foi escrito por um modelo, a partir de texto que um humano digitou. Trate como entrada hostil:

- **Allow-list** de valores onde houver conjunto finito; nunca `switch` com `default:` permissivo.
- **Limite de tamanho** em toda string antes de tocar I/O.
- **Query parametrizada**, sempre — nunca concatenação com o argumento.
- **Id de tenant vem da closure ou da credencial, NUNCA do modelo.** Uma tool que aceita `tenantId` como parâmetro é uma tool que qualquer prompt injection reaponta para outro cliente.

**Registrar no Claude Code (`settings.json` do projeto consumidor):**

```json
{
  "mcpServers": {
    "my-project-tools": {
      "command": "dotnet",
      "args": ["run", "--project", "src/MyProject.McpServer"]
    }
  }
}
```

---

## Tool nativa vs MCP server exposto

| Dimensão | Tool nativa (`[Description]`) | MCP server exposto |
|----------|------------------------------|--------------------|
| **Consumidor** | Agentes do próprio projeto | Agentes externos (Claude Code, outros projetos) |
| **Protocolo** | Chamada direta em processo | Protocolo MCP (stdio ou HTTP) |
| **Descoberta** | Registro manual no agente | `ListTools` — autodescoberta |
| **Quando usar** | Lógica interna do projeto | Capacidades que cruzam fronteiras de projeto/processo |
| **Overhead** | Zero | Processo separado, IPC via stdio/HTTP |
| **Testabilidade** | Unitária com xUnit/nSubstitute | Requer host em execução ou mock do transport |

**Regra**: se o único consumidor é um agente do próprio projeto → tool nativa. Se o consumidor é externo → MCP server.

---

## Segurança

Um MCP server exposto é uma **superfície de ataque real** — qualquer agente com acesso ao transporte pode invocar suas tools.

**Obrigatório antes de expor:**

- **Validação de input:** valide todos os parâmetros recebidos (IDs, datas, strings livres) antes de executar lógica de negócio. MCP não valida tipos por você.
- **Escopo mínimo:** exponha apenas as tools necessárias. Não exponha operações de escrita se o consumidor só lê.
- **Autenticação (HTTP transport):** se usar HTTP, exija bearer token ou API key via header. stdio é implicitamente restrito ao processo pai.
- **Nunca expor secrets via tools:** tools não devem retornar API keys, tokens, ou dados de configuração interna.
- **Rate limiting:** se o MCP server for exposto via HTTP em produção, adicione rate limiting na camada de transporte (reverse proxy / middleware ASP.NET).

A validação de input é mostrada inline no exemplo `GetOrderStatus` acima — valide IDs/datas/strings
**antes de qualquer I/O** e devolva uma mensagem de erro descritiva (o consumidor é um LLM, que
corrige o argumento na próxima iteração). Para tools por atributo que precisam de repositories,
prefira o tipo de instância resolvido por DI a métodos estáticos com estado global.

---

## Anti-patterns

| Anti-pattern | Problema | Solução |
|-------------|---------|---------|
| Criar MCP server para tools que só os agentes do próprio projeto usam | Overhead de processo + IPC sem benefício; mais difícil de testar | Tool nativa `[Description]` + `AIFunctionFactory.Create()` |
| Expor tools sem validação de input | Agentes externos (ou alucinações de LLM) podem enviar dados malformados | Validar todos os parâmetros antes de qualquer I/O |
| Tools sem escopo definido — expor tudo "por conveniência" | Superfície de ataque ampla; dificulta manutenção | Expor apenas o necessário; revisar a lista a cada release |
| HTTP transport sem autenticação | Qualquer processo na rede pode invocar as tools | Bearer token / API key obrigatório em HTTP; stdio é suficiente para uso local |
| **Podar só a listagem e chamar isso de autorização** | `ListTools` filtrado esconde a tool; quem sabe o nome chama assim mesmo, e cliente conectado não relê a lista | `AddListToolsFilter` **e** `AddCallToolFilter` |
| **`tenantId` como parâmetro de tool** | O argumento é escrito pelo modelo: prompt injection reaponta para outro cliente | Tenant vem da closure ou de `context.User` |
| Escrever `options.Stateless = true` "para escalar" | Redundante desde a revisão `2026-07-28`; sugere que há uma decisão a tomar onde não há | `WithHttpTransport()` sem argumento |
| Contar com `initialize`, `Mcp-Session-Id`, `ping` ou `logging/setLevel` | Removidos na revisão `2026-07-28` | Versão/capacidades em `_meta` por request; `server/discover` |
| Subir o MCP server no mesmo processo da aplicação principal | Acoplamento; falha no servidor MCP afeta a app | Projeto separado (`MyProject.McpServer`) com entrypoint próprio |

---

## Checklist (verifiable by morph-eval)

- [ ] `ModelContextProtocol` e `Microsoft.Agents.AI` no `.csproj`; `ModelContextProtocol.AspNetCore` apenas se HTTP
- [ ] Tools declaradas via `[McpServerToolType]` + `[McpServerTool]` (estáticas) ou `McpServerTool.Create(...)` (agente MAF / runtime)
- [ ] `AddMcpServer()` com `WithStdioServerTransport()` (stdio) ou `WithHttpTransport()` (HTTP) — sem reafirmar o default `Stateless`
- [ ] `WithToolsFromAssembly()` para tools por atributo, ou `WithTools([...])` para tools de runtime
- [ ] Entrypoint: `await builder.Build().RunAsync()` (stdio) ou `app.MapMcp(); app.Run()` (HTTP)
- [ ] Agente criado via `_modelRegistry.GetChatClient("alias").AsAIAgent(...)` — provider-agnostic
- [ ] Validação de input em todas as tools antes de executar lógica de negócio
- [ ] Escopo mínimo: apenas tools necessárias para o consumidor externo
- [ ] HTTP transport (se usado) protegido por autenticação
- [ ] Multi-tenant: existe `AddCallToolFilter`, e não apenas `AddListToolsFilter`
- [ ] Nenhuma tool recebe id de tenant como parâmetro
- [ ] Nenhum `PackageReference` com faixa flutuante; pacote fora do pin com data de apuração
- [ ] Nenhum `Guid.NewGuid()` — usar `Guid.CreateVersion7()` se IDs são necessários

---

## References

- `ai-agents-setup` — pacotes base MAF, DI, quick start
- `ai-agents-mcp-tools` — consumir MCP servers externos em um agente MAF
- `ai-agents-production` — middleware pipeline, `ResilientToolMiddleware`, segurança
- [ModelContextProtocol C# SDK](https://github.com/modelcontextprotocol/csharp-sdk) — `docs/concepts/filters.md`, `docs/concepts/identity/identity.md` (lidos 2026-09-08)
- [MCP — versionamento da especificação](https://modelcontextprotocol.io/specification/versioning) e o changelog da revisão `2026-07-28` (lidos 2026-09-08)
- [NuGet — ModelContextProtocol 2.2.0](https://www.nuget.org/packages/ModelContextProtocol/2.2.0) (publicado 2026-08-13)
- [Expose Agent as MCP Server — MAF docs](https://learn.microsoft.com/en-us/agent-framework/agents/tools/local-mcp-tools)

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/mcp-server.md v2.0 (2026-09-08)*
