Servidor MCP para SDD: como validar código contra a spec

Um servidor MCP pode ser o árbitro que faltava no Spec Driven Development, reprovando em tempo real o código que o agente escreve fora da especificação. Neste artigo, você vai entender como o Model Context Protocol conecta agentes a linters, validadores de OpenAPI e JSON Schema e checadores de conformidade.
Além disso, vamos construir em C# uma ferramenta que compara a implementação com o contrato ativo e devolve um veredito objetivo. Assim, o agente deixa de avaliar o próprio trabalho e passa a responder a uma regra que ele não controla.
Por que o SDD precisa de um árbitro
No SDD, a especificação vira a fonte da verdade e o código passa a ser uma implementação dela. Quem já usou o Spec Kit conhece o fluxo de specify, plan, tasks e implement.
O problema aparece na última etapa. Agentes geram código mais rápido do que qualquer pessoa consegue revisar, e pequenos desvios se acumulam sem que ninguém perceba.
Por exemplo, o agente cria um endpoint extra para facilitar, troca um 422 por um 400 ou esquece um campo obrigatório no retorno. Cada desvio parece inofensivo, mas juntos eles transformam a spec em documentação desatualizada.
A literatura sobre SDD chama esse fenômeno de drift. Sem algum mecanismo que detecte a divergência, o método volta a ser apenas desenvolvimento guiado por documentos.
Pedir ao próprio agente que confira o trabalho não resolve, porque o modelo que escreveu o código tende a concordar com ele. Por isso, a verificação precisa vir de fora, de forma determinística e com uma resposta que não dependa de interpretação.
Esse papel cabe muito bem a um servidor MCP. Ele fica entre o agente e as ferramentas de validação, e transforma a especificação ativa em um conjunto de checagens executáveis.
O que é um servidor MCP e como ele fala com o agente
O Model Context Protocol é um padrão aberto para conectar aplicações de IA a ferramentas, dados e prompts. Em vez de uma integração sob medida para cada par de agente e serviço, existe um único protocolo.
A arquitetura tem três papéis. O host é a aplicação onde o agente roda, como o VS Code com o GitHub Copilot ou o Claude Code.
Enquanto isso, o cliente MCP mantém a conexão com cada servidor.
Já o servidor MCP expõe as capacidades. Na prática, ele oferece três primitivas: tools, que o modelo decide chamar, resources, que a aplicação carrega como contexto, e prompts, que funcionam como modelos reutilizáveis.
Um detalhe importante merece atenção. O modelo nunca conversa direto com o servidor, porque ele pede a chamada, o cliente executa e o resultado volta para o contexto.
A revisão mais recente da especificação, publicada em 28 de julho de 2026, tornou o protocolo stateless. Com isso, cada requisição carrega a própria versão do protocolo e as capacidades do cliente, sem depender de uma sessão aberta.
Para quem programa em .NET, o caminho oficial é o SDK em C#. A versão 2.0 implementa a revisão 2026-07-28 e continua compatível com clientes que negociam versões anteriores.
O que um servidor MCP conecta ao agente
Um servidor MCP não precisa reinventar nenhuma validação. Na maior parte dos casos, ele apenas encapsula ferramentas que o time já usa e entrega o resultado em um formato que o agente entende.
Podemos agrupar essas ferramentas em três famílias. Cada uma responde a uma pergunta diferente sobre o código gerado.
Linters e analisadores estáticos
Linters respondem se o código segue as convenções do projeto. No ecossistema .NET, isso inclui os analyzers do Roslyn, o dotnet format e as regras do .editorconfig.
Quando essas checagens rodam por meio de uma tool, o agente recebe a lista de avisos logo depois de escrever o arquivo. Dessa forma, ele corrige o problema na mesma iteração, antes que o erro chegue ao pull request.
Validadores de OpenAPI e JSON Schema
Validadores de contrato respondem se a API faz o que a spec promete. Aqui entram ferramentas como o Spectral, que faz lint de documentos OpenAPI, e bibliotecas que verificam se um payload respeita o JSON Schema declarado.
O ASP.NET Core facilita muito esse trabalho. Desde o .NET 9 o framework gera documentos OpenAPI nativamente, e o pacote Microsoft.Extensions.ApiDescription.Server produz esse documento durante o build.
Além disso, no .NET 10 o documento segue o OpenAPI 3.1 e usa o JSON Schema draft 2020-12. Ou seja, o mesmo validador serve tanto para o contrato quanto para a implementação.
Checadores de conformidade
Checadores de conformidade respondem se o código respeita regras que não cabem em um schema. No Spec Kit, essas regras vivem na constitution, o documento com princípios que nenhuma feature pode quebrar.
Por exemplo, uma regra pode exigir que todo endpoint de escrita retorne ProblemDetails em caso de erro. Outra pode proibir dependências entre dois módulos específicos.
Essas checagens costumam ser scripts ou testes de arquitetura. Quando o servidor MCP as expõe como tools, o agente consegue executá-las sozinho, sem que alguém precise lembrar de rodar cada uma.
Como o servidor MCP atua como árbitro
Um árbitro não joga a partida, apenas aplica a regra e comunica a decisão. O servidor MCP árbitro segue a mesma lógica, porque não escreve código e apenas compara a implementação com a especificação ativa.
Esse desenho tem três partes. Primeiro, o servidor MCP expõe a spec ativa como um resource, para que o agente consulte o contrato antes de implementar.
Em seguida, tools executam as validações sob demanda e retornam um resultado estruturado. Por fim, uma regra na constitution obriga o agente a chamar essas tools antes de marcar qualquer tarefa como concluída.
O veredito precisa ser objetivo para funcionar. Em vez de um texto livre, a tool devolve um objeto com o campo approved e a lista de violações.
{
"approved": false,
"totalViolations": 2,
"violations": [
{
"rule": "missing-status-code",
"location": "POST /orders",
"message": "Status code 422 is required by the spec."
},
{
"rule": "undocumented-operation",
"location": "DELETE /orders/{id}",
"message": "Endpoint is not part of the spec. Remove it or update the spec first."
}
]
}
Com esse retorno, o agente sabe exatamente o que corrigir. Além disso, o host pode exibir o mesmo resultado para a pessoa que acompanha a sessão.
Reprovação não é erro de execução
A especificação do MCP separa dois tipos de falha. Erros de protocolo, como um método inexistente, voltam como erros JSON-RPC, enquanto erros de execução da tool voltam como um resultado comum com isError: true.
Essa distinção existe para que o cliente devolva a falha ao modelo e ele tente se corrigir. Contudo, uma implementação reprovada não representa uma falha da tool, porque a validação rodou e chegou a uma conclusão.
Por isso, um servidor MCP bem desenhado reserva o isError para quando a verificação não consegue rodar, como um contrato ausente ou um build quebrado. Já a reprovação volta como resultado normal, com approved: false e o conteúdo estruturado.
Assim, o agente distingue "não consegui verificar" de "verifiquei e você violou a spec". São situações diferentes, e cada uma pede uma reação diferente.
Criando o servidor MCP árbitro em C#
Agora vamos construir o servidor MCP na prática. O exemplo usa o .NET 10, o SDK oficial do MCP para C# e a biblioteca JsonSchema.Net para validar payloads.
O cenário é uma API de pedidos feita com Minimal APIs a partir de uma spec do Spec Kit. A pasta da feature contém o arquivo contracts/openapi.json, que funciona como contrato.
Configurando o servidor MCP no projeto
O servidor MCP roda como uma aplicação de console com transporte stdio. Esse formato é o mais simples para uso local, porque o host inicia o processo e conversa com ele pela entrada e saída padrão.
dotnet new console -n SpecArbiter
cd SpecArbiter
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting
dotnet add package JsonSchema.Net
O Program.cs registra o servidor e os serviços de apoio. Em seguida, ele descobre automaticamente as tools e os resources do assembly.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SpecArbiter;
var builder = Host.CreateApplicationBuilder(args);
// No transporte stdio, todo log precisa ir para o stderr
builder.Logging.AddConsole(options =>
options.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Services.AddSingleton<ActiveSpec>();
builder.Services.AddSingleton<OpenApiSnapshot>();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly()
.WithResourcesFromAssembly();
await builder.Build().RunAsync();
Note que todo log vai para o stderr. Como o stdout carrega as mensagens JSON-RPC, qualquer texto extra nesse canal quebraria a comunicação com o cliente.
Expondo a especificação ativa
A classe ActiveSpec localiza o contrato da feature em andamento. Para manter o exemplo direto, a pasta chega por variável de ambiente.
using System.Text.Json.Nodes;
using ModelContextProtocol;
namespace SpecArbiter;
public sealed class ActiveSpec
{
// Pasta da feature ativa do Spec Kit, por exemplo specs/003-orders
private readonly string _featureDirectory =
Environment.GetEnvironmentVariable("ACTIVE_SPEC_DIR")
?? throw new InvalidOperationException("ACTIVE_SPEC_DIR is not set.");
public string ContractPath =>
Path.Combine(_featureDirectory, "contracts", "openapi.json");
public JsonNode LoadContract()
{
if (!File.Exists(ContractPath))
throw new McpException($"Contract not found at {ContractPath}. Run /speckit.plan first.");
return JsonNode.Parse(File.ReadAllText(ContractPath))!;
}
}
Quando o contrato não existe, a classe lança uma McpException. Nesse caso, o SDK converte a exceção em um resultado com isError: true, e a mensagem orienta o agente a rodar o /speckit.plan primeiro.
Depois disso, um resource disponibiliza o contrato como contexto. Dessa forma, o agente lê a fonte da verdade antes de escrever a primeira linha.
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace SpecArbiter;
[McpServerResourceType]
public sealed class SpecResources(ActiveSpec spec)
{
[McpServerResource(
UriTemplate = "spec://active/contract",
Name = "active_contract",
MimeType = "application/json")]
[Description("OpenAPI contract of the feature currently being implemented.")]
public string GetContract() => spec.LoadContract().ToJsonString();
}
Comparando o contrato com a implementação
Para saber o que o código realmente expõe, o servidor MCP não lê os arquivos C#. Em vez disso, ele usa o próprio ASP.NET Core para gerar o documento OpenAPI da implementação durante o build.
Para isso, o projeto da API recebe o pacote Microsoft.Extensions.ApiDescription.Server e estas propriedades no .csproj.
<PropertyGroup>
<!-- Gera o OpenAPI da implementação a cada build -->
<OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
<OpenApiDocumentsDirectory>openapi</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name implementation</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
A classe OpenApiSnapshot dispara o build e lê o documento gerado. Se a compilação falhar, ela lança uma exceção, porque não faz sentido validar o contrato de um código que nem compila.
using System.Diagnostics;
using System.Text.Json.Nodes;
using ModelContextProtocol;
namespace SpecArbiter;
public sealed class OpenApiSnapshot
{
private readonly string _apiProject =
Environment.GetEnvironmentVariable("API_PROJECT_PATH")
?? throw new InvalidOperationException("API_PROJECT_PATH is not set.");
public async Task<JsonNode> GenerateAsync(CancellationToken cancellationToken)
{
// O build dispara a geração do OpenAPI pelo ApiDescription.Server
using var process = Process.Start(new ProcessStartInfo(
"dotnet", $"build \"{_apiProject}\" --nologo -v q")
{
RedirectStandardOutput = true
})!;
var output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
throw new McpException($"Build failed. Fix compilation errors first.{Environment.NewLine}{output}");
var file = Path.Combine(
Path.GetDirectoryName(_apiProject)!, "openapi", "implementation.json");
return JsonNode.Parse(await File.ReadAllTextAsync(file, cancellationToken))!;
}
}
Com os dois documentos em mãos, a tool check_api_conformance compara as operações e os status codes. Ela reprova endpoints ausentes, status codes faltando e qualquer rota que não esteja na spec.
using System.ComponentModel;
using System.Text.Json.Nodes;
using ModelContextProtocol.Server;
namespace SpecArbiter;
public sealed record Violation(string Rule, string Location, string Message);
public sealed record Verdict(bool Approved, int TotalViolations, IReadOnlyList<Violation> Violations);
[McpServerToolType]
public sealed partial class ConformanceTools(ActiveSpec spec, OpenApiSnapshot snapshot)
{
private static readonly string[] HttpMethods = ["get", "post", "put", "patch", "delete"];
[McpServerTool(Name = "check_api_conformance", ReadOnly = true, UseStructuredContent = true)]
[Description("Builds the API and compares every endpoint with the active OpenAPI contract. " +
"Call it before marking any task as done. " +
"Approved=false means the implementation violates the spec and must be fixed.")]
public async Task<Verdict> CheckApiConformanceAsync(CancellationToken cancellationToken)
{
var expected = ListOperations(spec.LoadContract());
var actual = ListOperations(await snapshot.GenerateAsync(cancellationToken));
var violations = new List<Violation>();
foreach (var (operation, statusCodes) in expected)
{
if (!actual.TryGetValue(operation, out var implementedCodes))
{
violations.Add(new("missing-operation", operation,
"Endpoint defined in the spec was not implemented."));
continue;
}
foreach (var code in statusCodes.Except(implementedCodes))
violations.Add(new("missing-status-code", operation,
$"Status code {code} is required by the spec."));
}
// Rotas que existem no código mas não na spec também reprovam
foreach (var operation in actual.Keys.Except(expected.Keys))
violations.Add(new("undocumented-operation", operation,
"Endpoint is not part of the spec. Remove it or update the spec first."));
return new Verdict(violations.Count == 0, violations.Count, violations);
}
private static Dictionary<string, HashSet<string>> ListOperations(JsonNode document)
{
var operations = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
if (document["paths"] is not JsonObject paths)
return operations;
foreach (var (path, item) in paths)
foreach (var method in HttpMethods)
{
if (item?[method] is not JsonObject operation)
continue;
HashSet<string> codes = operation["responses"] is JsonObject responses
? responses.Select(response => response.Key).ToHashSet()
: [];
operations[$"{method.ToUpperInvariant()} {path}"] = codes;
}
return operations;
}
}
Repare na descrição da tool. Ela diz ao modelo quando chamar a ferramenta e o que significa um Approved falso, e essa instrução pesa tanto quanto o código.
Também vale notar o ReadOnly = true. Esse atributo vira uma anotação que informa ao host que a tool não altera nada no ambiente.
Validando respostas contra o JSON Schema
Bater rotas e status codes ainda não basta. Um endpoint pode existir, retornar 201 e mesmo assim devolver um corpo que não respeita o schema da spec.
A segunda tool do servidor MCP recebe um payload real e valida o corpo contra o schema declarado no contrato. O agente pode obter esse payload em um teste de integração ou chamando a API local.
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;
using ModelContextProtocol.Server;
namespace SpecArbiter;
public sealed partial class ConformanceTools
{
[McpServerTool(Name = "validate_response_payload", ReadOnly = true, UseStructuredContent = true)]
[Description("Validates a JSON payload returned by an endpoint against the response schema of the active contract.")]
public Verdict ValidateResponsePayload(
[Description("HTTP method, for example POST")] string method,
[Description("Path exactly as in the contract, for example /orders/{id}")] string path,
[Description("Response status code, for example 201")] string statusCode,
[Description("Raw JSON returned by the endpoint")] string payload)
{
var contract = spec.LoadContract();
var schemaNode = contract["paths"]?[path]?[method.ToLowerInvariant()]
?["responses"]?[statusCode]?["content"]?["application/json"]?["schema"];
if (schemaNode is null)
return new Verdict(false, 1,
[
new("missing-schema", $"{method} {path} {statusCode}",
"The spec declares no JSON schema for this response.")
]);
// Copia os componentes do contrato para $defs e resolve os $ref
var root = new JsonObject
{
["$schema"] = "https://json-schema.org/draft/2020-12/schema",
["$defs"] = contract["components"]?["schemas"]?.DeepClone() ?? new JsonObject(),
["allOf"] = new JsonArray(schemaNode.DeepClone())
};
var schema = JsonSchema.FromText(
root.ToJsonString().Replace("#/components/schemas/", "#/$defs/"));
using var instance = JsonDocument.Parse(payload);
var result = schema.Evaluate(instance.RootElement,
new EvaluationOptions { OutputFormat = OutputFormat.List });
var violations = (result.Details ?? [])
.Where(detail => detail.Errors is { Count: > 0 })
.SelectMany(detail => detail.Errors!.Select(error => new Violation(
$"schema:{error.Key}", detail.InstanceLocation.ToString(), error.Value)))
.ToList();
return new Verdict(result.IsValid, violations.Count, violations);
}
}
O truque está na montagem do schema, já que o OpenAPI guarda os tipos em components/schemas. Por isso, a tool copia esses tipos para $defs e reescreve as referências, resolvendo cada $ref sem carregar o documento inteiro.
Com o formato de saída em lista, cada erro traz o caminho exato dentro do JSON. Então o agente recebe algo como /total com a mensagem de tipo incorreto, e não apenas um aviso genérico de payload inválido.
Integrando o servidor MCP ao Spec Kit
Com o servidor MCP pronto, falta registrá-lo no host. No VS Code, o arquivo .vscode/mcp.json aponta para o projeto e define as variáveis da feature ativa.
{
"servers": {
"spec-arbiter": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "--project", "tools/SpecArbiter/SpecArbiter.csproj"],
"env": {
"ACTIVE_SPEC_DIR": "specs/003-orders",
"API_PROJECT_PATH": "src/Orders.Api/Orders.Api.csproj"
}
}
}
}
Registrar a tool, porém, não garante que o agente vá usá-la. Por isso, a regra precisa entrar na constitution do projeto, que o Spec Kit carrega em todas as fases.
### Principle VII: Contract conformance is mandatory
- Every task that touches an endpoint MUST end with a call to `check_api_conformance`.
- A task is only done when the verdict returns `approved: true`.
- The agent MUST NOT edit `contracts/openapi.json` to make a verdict pass.
A última linha é a mais importante. Sem ela, um agente pressionado a concluir a tarefa pode simplesmente editar o contrato para que o veredito passe.
Com essa configuração, o ciclo do /speckit.implement muda. O agente implementa uma tarefa, chama o árbitro, lê as violações, corrige e chama de novo, até receber a aprovação.
Se você quer padronizar essa regra em vários repositórios, as extensões do Spec Kit permitem distribuí-la em presets e bundles. Assim, todo projeto novo já nasce com o árbitro configurado.
Boas práticas para um servidor MCP confiável
Um árbitro só tem valor quando todos confiam nele. Algumas práticas ajudam a manter essa confiança ao longo do tempo.
Primeiro, mantenha a validação determinística. O servidor MCP não deve usar um modelo de linguagem para julgar o código, porque isso traria de volta a subjetividade.
Além disso, escreva mensagens acionáveis. Uma mensagem que cita o status code esperado ajuda muito mais do que um simples "contract mismatch", porque diz ao agente exatamente onde agir.
Em terceiro lugar, proteja o contrato. O agente pode ler a pasta contracts, mas qualquer alteração nela deve passar por revisão humana.
Também vale rodar as mesmas checagens no CI. O servidor MCP dá feedback em tempo real durante a sessão, enquanto o pipeline garante que nada escape quando alguém trabalha sem o agente.
Por fim, mantenha o escopo pequeno. Cada tool deve verificar uma coisa e responder rápido, já que o agente vai chamá-la dezenas de vezes em uma mesma feature.
Se quiser ver outro uso prático do protocolo, o artigo sobre o WinDbg MCP mostra como um servidor dá ao agente acesso a crash dumps com segurança. A lógica é a mesma, porque o protocolo amplia o que o agente enxerga sem abrir mão do controle.
Conclusão
O SDD resolve a ambiguidade do vibe coding ao colocar a especificação no centro, mas só funciona se alguém fiscalizar a distância entre a spec e o código. Um servidor MCP ocupa esse lugar com naturalidade, porque fala a mesma língua do agente e executa validações que não dependem de opinião.
Na prática, o árbitro combina três peças simples: um resource com o contrato, tools que validam e uma regra na constitution. Com isso, o agente passa a ter um critério objetivo de pronto, e o time ganha uma spec que continua verdadeira depois da implementação.
O próximo passo é começar pequeno. Exponha uma única tool de conformidade no seu servidor MCP, observe como o agente reage às reprovações e amplie as regras conforme o time ganhar confiança.
FAQ
Preciso de um servidor MCP para usar Spec Driven Development?
Não. O SDD funciona sem MCP, mas o árbitro reduz o drift porque dá ao agente uma forma objetiva de verificar o próprio trabalho.
O árbitro substitui os testes automatizados?
Não. Ele verifica o contrato da API, enquanto os testes continuam responsáveis pelo comportamento e pelas regras de negócio.
Por que não usar isError para reprovar a implementação?
Porque o isError indica que a tool não conseguiu executar. Uma reprovação representa uma conclusão da verificação, então ela volta como resultado normal com approved igual a false.
Funciona com outros agentes além do GitHub Copilot?
Sim. Qualquer host compatível com MCP, como Claude Code, Cursor ou VS Code, consegue registrar e chamar o mesmo servidor.
Posso escrever o contrato em YAML em vez de JSON?
Sim, mas o servidor precisa converter o YAML para JSON antes da validação, por exemplo com a biblioteca Yaml2JsonNode.