MCP em Produção em 2026
MCP deixou de esconder estado no transporte
A especificação MCP 2026-07-28 removeu as sessões de protocolo e o handshake de inicialização do novo core. O ganho não é apenas menos mensagens: qualquer request passa a carregar versão, identidade do cliente e capabilities e pode chegar a qualquer instância compatível.
Se você ainda precisa dos fundamentos, comece por MCP para Não-Programadores. Este guia não repete as analogias de USB-C ou garçom; ele parte de um servidor remoto já existente e trata da migração operacional.
O modelo anterior escondia uma dependência cara. Depois de initialize, o servidor podia vincular contexto ao Mcp-Session-Id e, por consequência, à conexão ou à instância que a atendia. Em produção isso criava quatro problemas:
- afinidade: o balanceador precisava reenviar chamadas da mesma sessão ao mesmo processo;
- escala horizontal: drenar, substituir ou adicionar réplicas exigia preservar estado de transporte;
- reconexão: a queda de uma conexão podia apagar contexto que nunca virou dado de aplicação;
- gateways cegos: para distinguir
tools/callderesources/read, um WAF precisava interpretar o corpo JSON-RPC.
O release oficial de 28 de julho muda essa base: o transporte é stateless e cada request é autodescritivo. Isso não elimina estado de negócio. Tasks, idempotência, progresso, aprovação e resultado continuam existindo, mas agora precisam de handles e storage explícitos.
Para não misturar níveis de maturidade, uso quatro rótulos ao longo do texto:
- Especificação: comportamento publicado em
2026-07-28; - Extensão: comportamento oficial, porém desabilitado por padrão e negociado por opt-in;
- Roadmap: direção anunciada, ainda não uma garantia universal de interoperabilidade;
- Exemplo local / recomendação: escolhas deste guia, não requisitos do protocolo.
O que mudou no MCP 2026
A tabela abaixo transforma as mudanças do release 2026-07-28 em ações concretas. A chamada a server/discover é opcional para o cliente; um servidor 2026-07-28 deve implementá-la. Fora dela, as informações necessárias seguem em _meta em cada request.
| Antes | Agora | Impacto operacional | Ação de migração |
|---|---|---|---|
initialize/initialized e contexto preso ao Mcp-Session-Id |
O novo core removeu handshake e sessões de protocolo; cada request é autodescritivo | Round-robin deixa de exigir afinidade por sessão de transporte | Remover dependências desses três elementos e transportar handles de aplicação explicitamente |
| Capabilities aprendidas no handshake | O cliente pode chamar server/discover; a chamada é opcional, e capabilities do cliente são declaradas por request |
Um processo pode validar cada chamada sem memória de uma negociação anterior | Implementar discovery, mas aceitar chamadas diretas bem formadas |
| Gateway precisava ler o corpo JSON-RPC para saber a operação | Streamable HTTP exige Mcp-Method; Mcp-Name identifica tool, resource, prompt e, na extensão Tasks, o taskId |
Roteamento, rate limit e políticas podem começar nos headers | Validar headers, comparar com o corpo e só então autorizar e despachar |
| Catálogos eram buscados novamente após reconexões | server/discover, tools/list, prompts/list, resources/list, resources/templates/list e resources/read carregam ttlMs e cacheScope |
Menos chamadas e prompts mais estáveis, sem misturar dados privados entre credenciais | Respeitar TTL; compartilhar cache apenas quando cacheScope for public |
| Elicitation, sampling e roots podiam iniciar requests do servidor em stream bidirecional | Multi Round-Trip Requests, ou MRTR, devolve resultType: "input_required"; o cliente repete a chamada com inputResponses |
Input intermediário funciona sem stream bidirecional permanente | Migrar fluxos intermediários para MRTR e persistir requestState quando fornecido |
| Tasks experimentais pareciam parte do core | Tasks virou a extensão oficial io.modelcontextprotocol/tasks |
Jobs longos ganham handle durável, polling e input no meio da execução, mas só entre pares compatíveis | Anunciar, verificar opt-in por request e oferecer fallback seguro |
| Registro dinâmico de cliente, ou DCR, era o caminho usual | CIMD é o caminho preferido; DCR está depreciado e mantido para compatibilidade | Credenciais e issuer passam a exigir associação mais rigorosa | Migrar registro para CIMD e manter DCR apenas durante o rollout compatível |
| HTTP+SSE legado e APIs como Roots, Sampling e Logging ainda eram recomendadas | Esses recursos foram depreciados no release | Código existente continua funcionando durante a janela, mas novos consumidores não devem ampliar a dependência | Inventariar consumidores, oferecer dual support temporário e medir a retirada |
Depreciação não é remoção imediata. A política formal introduziu uma janela mínima de doze meses; Roots, Sampling, Logging e HTTP+SSE continuam funcionando nesse período. Da mesma forma, Tasks, MCP Apps e outras extensões pertencem ao framework formal de extensões: ficam desabilitadas por padrão e exigem opt-in explícito.
Também não confunda server/discover com descoberta progressiva de tools. O primeiro já faz parte da especificação e anuncia versões e capabilities. A redução progressiva de catálogos muito grandes é uma iniciativa do roadmap de agosto de 2026; não presuma que todo host já consiga revelar tools sob demanda.
No MCP endpoint, header obrigatório ausente, malformado ou divergente do corpo — inclusive Mcp-Method e Mcp-Name — deve produzir HTTP 400 Bad Request com erro JSON-RPC -32020 (HeaderMismatch). Um gateway ou outro intermediário pode rejeitar a chamada no nível HTTP sem incluir um corpo JSON-RPC, mas não deve encaminhar headers inconsistentes como se fossem confiáveis.
Arquitetura alvo: transporte stateless, estado explícito
A taxonomia útil para a migração é: core stateless obrigatório, capabilities opcionais do core e extensões opt-in. Sessão implícita sai; handles de aplicação visíveis entram. Tasks e MCP Apps ficam fora do core e só alteram o fluxo quando cliente e servidor as declaram.
A arquitetura alvo é uma cadeia web convencional: cliente MCP → gateway/WAF → duas ou mais instâncias MCP stateless → store explícito de Tasks → worker → serviço externo. As duas instâncias não guardam a “sessão do usuário”; ambas leem e escrevem o mesmo estado durável quando a aplicação precisa continuar um trabalho.
Cada componente tem uma responsabilidade curta:
- cliente: envia
_metacompleto em cada request, declara extensões, persistetaskIde respeitapollIntervalMs; - gateway/WAF: termina TLS, valida
Origin, aplica limites e usaMcp-Method/Mcp-Namecomo sinal de roteamento, nunca como prova isolada de autorização; - instância MCP stateless: compara headers com o corpo, valida protocolo/capabilities/escopos e cria o handle durável antes de responder;
- task store: é a fonte de verdade para lifecycle, idempotência, progresso, input pendente, resultado, erro e expiração;
- worker: executa o job, observa cancelamento, grava checkpoints e nunca depende da conexão HTTP original;
- serviço externo: mantém sua própria autorização e idempotência para efeitos como publicar um relatório.
Recomendação: trate
taskIdcomo um localizador opaco, não como credencial. Autorizetasks/get,tasks/updateetasks/cancelde novo em cada request, vinculando a Task a tenant e subject. Para modelar checkpoints e transições, o padrão de Graph Engineering para Agentes complementa este desenho; para operação, instrumente o fluxo como mostra o guia de observabilidade de agentes.
Implementação: uma auditoria longa com MCP Tasks
O exemplo inteiro usa uma única tool, auditar_site. Ela recebe URLs, uma chave idempotente de negócio e a intenção de publicar ou não o relatório.
Escolha do exemplo local: até cinco URLs, sem publicação, usam uma resposta síncrona limitada. Mais de cinco URLs, ou qualquer chamada que peça publicação, exigem Tasks. O limite 5, o polling de dois segundos e o TTL de 24 horas são escolhas locais; a especificação não define esses números.
Negocie antes de escolher o resultado
O cliente pode descobrir a capability antecipadamente. Este é um exemplo de JSON-RPC bruto, não uma chamada inventada do SDK:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: server/discover
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "auditoria-console",
"version": "2.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
Uma resposta compatível anuncia a mesma extensão. ttlMs e cacheScope tornam o discovery cacheável; private impede reutilização entre contextos de autorização:
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
},
"ttlMs": 300000,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "auditoria-mcp",
"version": "2.0.0"
}
}
}
}
Discovery não cria uma sessão. O servidor não pode inferir que uma capability declarada ali vale para chamadas futuras: o cliente repete suas capabilities no _meta de cada request.
Chame auditar_site com headers verificáveis
A chamada longa abaixo contém seis URLs e declara Tasks. Em Streamable HTTP, Mcp-Method e Mcp-Name são obrigatórios e devem refletir method e params.name do corpo:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: auditar_site
{
"jsonrpc": "2.0",
"id": "call-101",
"method": "tools/call",
"params": {
"name": "auditar_site",
"arguments": {
"urls": [
"https://example.com/",
"https://example.com/docs/",
"https://example.com/blog/",
"https://example.com/status/",
"https://example.com/security/",
"https://example.com/contato/"
],
"idempotency_key": "audit:release-42",
"publicar": true
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "auditoria-console",
"version": "2.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {
"form": {}
},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
Antes de responder, o servidor abre uma transação, resolve a chave idempotente e persiste a Task. Só depois do commit devolve o CreateTaskResult, cujo resultType é task e cujos campos de Task são planos:
{
"jsonrpc": "2.0",
"id": "call-101",
"result": {
"resultType": "task",
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"status": "working",
"statusMessage": "Auditoria enfileirada",
"createdAt": "2026-08-27T09:15:00Z",
"lastUpdatedAt": "2026-08-27T09:15:00Z",
"ttlMs": 86400000,
"pollIntervalMs": 2000
}
}
O lote pequeno segue a forma normal de CallToolResult; não há Task escondida:
{
"jsonrpc": "2.0",
"id": "call-102",
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "2 URLs auditadas; 1 redirecionamento encontrado."
}
],
"structuredContent": {
"mode": "synchronous",
"checked": 2,
"findings": 1,
"truncated": false
},
"isError": false
}
}
Persista Task, job e idempotência separadamente
Uma Task MCP é o contrato com o cliente; o job_id é o identificador do trabalho no domínio; a idempotency_key representa a intenção repetível do chamador. Misturar os três torna retries perigosos.
Este schema SQLite é SQL executável para o exemplo local. Em produção com alto volume, escolha um banco e uma fila coerentes com seus requisitos de concorrência e durabilidade.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS mcp_tasks (
task_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL UNIQUE,
tenant_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
request_hash TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('working', 'input_required', 'completed', 'failed', 'cancelled')
),
progress INTEGER NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100),
result_json TEXT,
error_json TEXT,
input_requests_json TEXT,
cancel_requested_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
UNIQUE (tenant_id, subject_id, idempotency_key)
);
CREATE INDEX IF NOT EXISTS mcp_tasks_expires_at_idx
ON mcp_tasks (expires_at);
A criação precisa ser atômica. O algoritmo abaixo é pseudocódigo de referência, não Python executável:
BEGIN IMMEDIATE
existing = buscar(tenant_id, subject_id, idempotency_key)
se existing existe:
se existing.request_hash != hash_canonico(argumentos):
abortar com conflito de idempotência
COMMIT
devolver o mesmo taskId e o estado atual
inserir Task(status=working, progress=0, expires_at=agora+24h)
registrar job_id para o worker na mesma fronteira transacional
COMMIT
devolver CreateTaskResult
Proteção contra duplicação: se a conexão cair após o commit e antes da resposta, o cliente repete tools/call com um novo JSON-RPC id, mas mantém idempotency_key: "audit:release-42". O servidor compara o hash canônico dos argumentos e devolve task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D de novo; não cria outra auditoria. Uma mesma chave com payload diferente é conflito, não retry.
Recomendação: quando o worker estiver em uma fila separada, use uma outbox transacional ou outro mecanismo que feche a lacuna entre “Task gravada” e “job publicado”. O protocolo exige que
tasks/getresolva assim que o handle for devolvido; ele não resolve sozinho a atomicidade banco-fila.
Execute o worker fora da conexão HTTP
O worker explícito busca ou recebe job_id, faz lease, processa URLs e atualiza a linha. Este fluxo é pseudocódigo de referência:
obter lease do job_id
para cada URL ainda não auditada:
se cancel_requested_at existe: tentar interromper com segurança
auditar URL com timeout, limite de resposta e política de egress
gravar checkpoint e progress
se publicar = false:
gravar result_json e transicionar para completed
se publicar = true:
gravar input_requests_json com a confirmação
transicionar para input_required
encerrar este lease
O worker seguinte só publica após uma resposta afirmativa. Ele usa job_id como chave idempotente no serviço externo, grava o identificador retornado e então transiciona para completed. Se o usuário recusar, a auditoria pode terminar como completed com published: false; se ocorrer um erro JSON-RPC durante a execução, a Task vai para failed com error_json.
Faça polling e retome depois de uma queda
A extensão Tasks exige Mcp-Name igual ao params.taskId para tasks/get, tasks/update e tasks/cancel sobre Streamable HTTP. Isso permite roteamento sem transformar afinidade em fonte de verdade.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/get
Mcp-Name: task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D
{
"jsonrpc": "2.0",
"id": "task-get-1",
"method": "tasks/get",
"params": {
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {
"form": {}
},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
Enquanto trabalha, o servidor responde com resultType: "complete" porque essa é a forma normal do resultado de tasks/get; status descreve a Task consultada:
{
"jsonrpc": "2.0",
"id": "task-get-1",
"result": {
"resultType": "complete",
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"status": "working",
"statusMessage": "4 de 6 URLs auditadas",
"createdAt": "2026-08-27T09:15:00Z",
"lastUpdatedAt": "2026-08-27T09:15:08Z",
"ttlMs": 86400000,
"pollIntervalMs": 2000
}
}
O cliente espera pelo menos pollIntervalMs. Se o processo ou a rede cair, ele persiste o taskId, abre outra conexão e envia o mesmo tasks/get, com um novo id e o _meta completo. Não há Mcp-Session-Id para recuperar. O store, e não a conexão anterior, permite a retomada.
Os estados terminais são completed, failed e cancelled. No sucesso, o resultado final preserva a forma do resultado que tools/call teria retornado:
{
"jsonrpc": "2.0",
"id": "task-get-9",
"result": {
"resultType": "complete",
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"status": "completed",
"statusMessage": "Auditoria concluída e relatório publicado",
"createdAt": "2026-08-27T09:15:00Z",
"lastUpdatedAt": "2026-08-27T09:22:31Z",
"ttlMs": 86400000,
"pollIntervalMs": 2000,
"result": {
"content": [
{
"type": "text",
"text": "6 URLs auditadas; relatório rep_7F2A publicado."
}
],
"structuredContent": {
"checked": 6,
"findings": 3,
"published": true,
"reportId": "rep_7F2A"
},
"isError": false
}
}
}
Pause em input_required antes de publicar
Quando chega à fronteira de efeito externo, a Task expõe a confirmação como uma elicitation. Diferentemente do MRTR do core, que repete a chamada original, o input durante uma Task aparece em tasks/get e é respondido por tasks/update.
{
"jsonrpc": "2.0",
"id": "task-get-5",
"result": {
"resultType": "complete",
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"status": "input_required",
"statusMessage": "Aguardando aprovação para publicar",
"createdAt": "2026-08-27T09:15:00Z",
"lastUpdatedAt": "2026-08-27T09:19:40Z",
"ttlMs": 86400000,
"pollIntervalMs": 2000,
"inputRequests": {
"approve_publish_v1": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Publicar o relatório de auditoria no serviço externo?",
"requestedSchema": {
"type": "object",
"properties": {
"confirmar": {
"type": "boolean",
"title": "Confirmar publicação"
}
},
"required": ["confirmar"]
}
}
}
}
}
}
O cliente apresenta a decisão ao humano e preserva a chave approve_publish_v1 em inputResponses:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/update
Mcp-Name: task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D
{
"jsonrpc": "2.0",
"id": "task-update-1",
"method": "tasks/update",
"params": {
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"inputResponses": {
"approve_publish_v1": {
"action": "accept",
"content": {
"confirmar": true
}
}
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {
"form": {}
},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
O ack é vazio e eventualmente consistente. O cliente continua consultando até observar novo estado:
{
"jsonrpc": "2.0",
"id": "task-update-1",
"result": {
"resultType": "complete"
}
}
O servidor deve ignorar respostas para chaves desconhecidas ou já satisfeitas. O cliente deduplica chaves vistas em polls repetidos para não pedir a mesma aprovação duas vezes.
Cancele cooperativamente
tasks/cancel registra intenção de cancelar; não é um kill remoto garantido e não usa notifications/cancelled:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/cancel
Mcp-Name: task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D
{
"jsonrpc": "2.0",
"id": "task-cancel-1",
"method": "tasks/cancel",
"params": {
"taskId": "task_audit_01J6B8K4YQ7V2R9M3T5N6P8C0D",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
{
"jsonrpc": "2.0",
"id": "task-cancel-1",
"result": {
"resultType": "complete"
}
}
Após o ack, o worker pode observar cancel_requested_at e parar no próximo checkpoint seguro. A Task ainda pode terminar completed se o efeito concluiu antes do cancelamento; a extensão define cancelamento como cooperativo e eventualmente consistente.
Nunca devolva Task a quem não declarou Tasks
O servidor verifica a capability no request atual. Para um lote de até cinco URLs sem publicação, ele usa o caminho síncrono limitado. Para um lote maior ou com publicação, um cliente sem io.modelcontextprotocol/tasks recebe o erro documentado -32021, não um CreateTaskResult que não sabe interpretar:
{
"jsonrpc": "2.0",
"id": "call-legacy-1",
"error": {
"code": -32021,
"message": "Missing required client capability",
"data": {
"requiredCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
Esse fallback é uma decisão de produto que precisa estar na documentação da tool: quais lotes são seguros no modo síncrono, qual limite de saída é aplicado e quando a chamada falha em vez de bloquear indefinidamente.
Python e o SDK atual
O MCP Python SDK está na linha estável v2, suporta Python 3.10+ e tem como alvo a especificação 2026-07-28 e revisões anteriores. Use o SDK para o que sua API pública documenta; não derive nomes de métodos a partir do wire protocol.
O arquivo abaixo é um exemplo executável do caminho síncrono limitado. Ele usa apenas APIs documentadas do SDK v2: MCPServer, @mcp.tool() e mcp.run(transport="streamable-http").
from __future__ import annotations
import asyncio
from urllib.parse import urlsplit
import httpx
from mcp.server import MCPServer
MAX_SYNC_URLS = 5
mcp = MCPServer("auditoria-mcp")
async def inspect_url(client: httpx.AsyncClient, url: str) -> dict[str, object]:
parsed = urlsplit(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
return {"url": url, "ok": False, "error": "URL HTTP(S) inválida"}
try:
response = await client.head(url)
return {
"url": url,
"ok": response.is_success,
"status": response.status_code,
"content_type": response.headers.get("content-type", ""),
}
except httpx.HTTPError as exc:
return {"url": url, "ok": False, "error": type(exc).__name__}
@mcp.tool()
async def auditar_site(
urls: list[str],
idempotency_key: str,
publicar: bool = False,
) -> dict[str, object]:
"""Executa uma auditoria HTTP pequena e sem publicação externa."""
if not idempotency_key.strip():
raise ValueError("idempotency_key é obrigatória")
if not 1 <= len(urls) <= MAX_SYNC_URLS:
raise ValueError("o caminho síncrono aceita de 1 a 5 URLs")
if publicar:
raise ValueError("publicação exige o fluxo Tasks com aprovação")
timeout = httpx.Timeout(8.0)
async with httpx.AsyncClient(
timeout=timeout,
follow_redirects=False,
headers={"User-Agent": "auditoria-mcp/2.0"},
) as client:
sites = await asyncio.gather(*(inspect_url(client, url) for url in urls))
return {
"mode": "synchronous",
"bounded": True,
"checked": len(sites),
"sites": sites,
}
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Instale e rode com comandos coerentes com a linha v2:
uv add "mcp[cli]>=2,<3" httpx
uv run mcp run server.py --transport streamable-http
O snippet demonstra integração e limite de resposta, não hardening de rede. Um servidor remoto real ainda deve bloquear SSRF, resolver DNS contra uma allowlist, limitar egress, tempo, redirects e tamanho de resposta.
O SDK v2 documenta uma superfície genérica de extensões, mas o repositório oficial não documenta hoje um adaptador de alto nível específico para Tasks que possamos chamar como create_task ou get_task. Por isso, este guia mostrou CreateTaskResult, tasks/get, tasks/update e tasks/cancel como JSON-RPC/HTTP bruto e descreveu store e worker separadamente. Implemente essa extensão pela superfície pública documentada do SDK quando adotar um binding concreto; não invente métodos de conveniência.
Autorização e segurança
A especificação de autorização 2026-07-28 define o MCP server protegido como resource server OAuth. O MCP server publica Protected Resource Metadata e anuncia os authorization servers associados. O authorization server publica sua própria metadata e issuer, emite credenciais e conduz a interação com o usuário. A especificação não pede que cada equipe escreva um provedor OAuth.
Valide o issuer antes de trocar o code
O cliente registra o issuer obtido de metadata validada junto de state e do verificador PKCE antes do redirect. Quando a resposta traz iss, ele compara a string com o issuer registrado antes de enviar o authorization code ao token endpoint. Se a metadata anunciou suporte obrigatório ao parâmetro e iss não vier, a resposta é rejeitada. Isso fecha ataques de mix-up entre authorization servers.
Credenciais pré-registradas ou obtidas por DCR ficam vinculadas ao issuer que as emitiu. Se Protected Resource Metadata apontar para outro authorization server, o cliente não reutiliza client_id/secret antigos: re-registra ou falha de forma explícita.
Prefira CIMD sem quebrar o legado de uma vez
Client ID Metadata Documents, ou CIMD, são o caminho preferido. DCR está depreciado, mas permanece para autorização servers sem CIMD durante a compatibilidade. Recomendação de rollout: anuncie CIMD, meça clientes que ainda usam DCR, mantenha a rota legada durante a janela publicada e retire-a apenas depois de comunicar e observar a migração.
Separe escopo, TTL e aprovação
No exemplo, sites:audit autoriza leitura e cálculo; sites:publish autoriza a tentativa de publicação. O approval gate humano continua obrigatório para o efeito externo mesmo quando o token tem o segundo scope. OAuth responde “quem pode pedir”; a aplicação responde “esta execução pode publicar agora”.
O TTL do access token, o ttlMs da Task e a retenção do relatório são relógios diferentes. Expirar a Task não revoga automaticamente um token; expirar o token não deve tornar uma Task acessível sem reautorização. Em cada tasks/*, recupere tenant/subject do contexto autenticado e compare com o owner persistido.
Mantenha logs úteis sem registrar segredos
Registre Mcp-Method, nome da tool, task_id, job_id, status, duração, quantidade de URLs e um identificador de trace. Não registre Authorization, access/refresh tokens, secrets de cliente, respostas de approval completas ou URLs com query strings sensíveis. Redija headers e payloads antes de enviá-los ao sistema de logs.
A divisão de responsabilidade fica assim:
- gateway/WAF: TLS,
Origin, limites, filtros e validação inicial do token; - MCP server: correspondência header-corpo, versão, capability, owner, scope e transições válidas;
- task store/worker: TTL, lease, retry idempotente, cancelamento cooperativo e resultado durável;
- serviço externo: autorização e idempotência do efeito final;
- humano/host: decisão explícita no approval gate.
Para aprofundar o modelo de subject, scopes e menor privilégio, leia Identidade e Permissões para Agentes. Para ameaças como prompt injection, SSRF, vazamento e defesa em camadas, use também o guia de Segurança de Agentes de IA.
Síncrono, MRTR, Tasks ou fila?
“Stateless” não escolhe o mecanismo de execução. A decisão depende da duração, necessidade de retomar, interação e semântica operacional. MRTR resolve uma conversa intermediária dentro de uma chamada stateless; ele não vira um job durável por si só. Tasks expõe um job durável dentro do MCP; uma fila externa resolve requisitos operacionais além do protocolo.
| Mecanismo | Duração típica | Reconexão | Input humano | Throughput e durabilidade | Use quando | Limitação principal |
|---|---|---|---|---|---|---|
| Resposta síncrona | Curta e previsível | Cliente repete a chamada; idempotência continua necessária | Só antes da chamada | Limitados pela request e pelos timeouts HTTP | A operação cabe com folga no orçamento da conexão e tem saída limitada | A conexão fica presa ao trabalho |
| MRTR | Uma chamada com uma ou mais rodadas curtas | requestState pode permitir repetir a chamada com inputResponses |
Sim, durante a chamada lógica | Não oferece lifecycle durável de job | Falta um dado, elicitation ou sampling antes do resultado final | Não substitui persistência, polling ou fila |
| MCP Tasks | Segundos, minutos ou horas | Sim, por taskId persistido |
Sim, via input_required e tasks/update |
Durabilidade depende do store/worker implementado | Trabalho longo, progresso, retomada e resultado posterior precisam ser interoperáveis em MCP | Extensão exige opt-in e suporte do host |
| Fila externa | Alta variação e volume | Sim, pelo contrato da aplicação | Exige workflow próprio | DLQ, prioridades, backpressure, retries e isolamento configuráveis | Alto throughput, fan-out, SLA próprio ou integração assíncrona entre sistemas | Mais infraestrutura e adaptação entre fila e MCP |
Uma Task pode ser a fachada MCP de um job que internamente usa fila; não são concorrentes obrigatórios. Quando o requisito principal é DLQ, backpressure, retries e idempotência entre serviços, não replique isso dentro do handler: use o guia Quando NÃO Usar MCP ou A2A e deixe o MCP expor um handle seguro para o resultado.
Checklist de migração
Transporte e compatibilidade
- [ ] Procurar dependências de
initialize,initializedeMcp-Session-Idem clientes, servidores, gateways, testes e dashboards. - [ ] Atualizar cliente e servidor para
2026-07-28e confirmar que cada request carrega a mesma versão no header e em_meta. - [ ] Exigir
Mcp-Method; validarMcp-Namecontraparams.name,params.uriou, em Tasks,params.taskId; no MCP endpoint, responder a header ausente, malformado ou divergente com HTTP400e erro JSON-RPC-32020(HeaderMismatch), permitindo que intermediários rejeitem no nível HTTP sem corpo JSON-RPC. - [ ] Remover afinidade baseada em sessão; manter afinidade temporária apenas como otimização, nunca como fonte de verdade.
- [ ] Conservar suporte legado somente para consumidores medidos e documentar a data de saída dentro da janela mínima de doze meses.
Discovery, cache e extensões
- [ ] Implementar
server/discovere decidir quando o cliente realmente precisa chamá-lo antes da primeira operação. - [ ] Tratar descoberta progressiva de catálogos como roadmap, sem exigir comportamento ainda não negociado.
- [ ] Aplicar
ttlMs; separar cachespubliceprivatepor contexto de autorização. - [ ] Anunciar extensões do servidor e repetir capabilities do cliente em cada request.
- [ ] Testar as duas formas de
tools/call: resultado síncrono eCreateTaskResult. - [ ] Nunca devolver uma Task a um cliente sem
io.modelcontextprotocol/tasks; oferecer caminho síncrono limitado ou erro-32021.
Estado, Tasks e worker
- [ ] Mover estado antes implícito para handles e storage explícitos.
- [ ] Persistir
task_id,job_id, owner, idempotency key, hash do request, status, progresso, input pendente, resultado/erro e expiração. - [ ] Criar a Task duravelmente antes de responder e fechar a lacuna entre commit e despacho do worker.
- [ ] Fazer retry com idempotência no job e em cada efeito externo; rejeitar a mesma chave com payload diferente.
- [ ] Persistir
taskIdno cliente e retomartasks/getapós restart ou troca de conexão. - [ ] Respeitar
pollIntervalMse tratarcompleted,failedecancelledcomo terminais. - [ ] Definir TTL, limpeza, lease, retry, timeout e recuperação de jobs abandonados.
- [ ] Implementar cancelamento como pedido cooperativo observado em checkpoints seguros.
- [ ] Deduplicar
inputRequestse aceitartasks/updateapenas do owner autorizado.
Autorização, segurança e rollout
- [ ] Validar
Origin, header-corpo, token, audience, issuer, owner e scopes em cada request. - [ ] Registrar o issuer esperado e validar
issantes de trocar o authorization code. - [ ] Vincular credenciais pré-registradas/DCR ao issuer; migrar registro para CIMD com fallback medido.
- [ ] Separar scopes de auditoria e publicação e manter approval gate para efeitos externos.
- [ ] Redigir tokens, secrets e parâmetros sensíveis de logs, traces e mensagens de erro.
- [ ] Instrumentar latência, erros, polling, retomadas, expirações, conflitos idempotentes e pedidos de cancelamento.
- [ ] Fazer rollout por capability e protocolo, com métricas por versão, fallback testado e plano explícito de depreciação.
Conclusão: stateless não significa sem estado
A migração correta remove memória implícita do transporte e torna o estado importante explícito: handle, owner, lifecycle, idempotência, TTL e resultado. Comece pelo request autodescritivo e pelos headers, mova estado para storage durável, só então habilite Tasks por opt-in. Use MRTR para rodadas intermediárias e uma fila externa quando os requisitos operacionais ultrapassarem o contrato MCP.
O teste decisivo é simples: derrube a conexão e uma das instâncias depois de criar auditar_site. Se outro processo consegue autorizar o mesmo taskId, recuperar o estado, evitar duplicação e concluir ou cancelar cooperativamente, a arquitetura deixou de depender da sessão escondida.