Pular para o conteúdo principal
André Costa
Blog
Claude Code Hermes Agent
Serviços Método Meu Sistema Contato
Falar comigo
26 de agosto de 2026

MCP em Produção em 2026: Como Migrar para Stateless, Tasks e Descoberta Progressiva

Guia prático para migrar MCP à especificação 2026-07-28: servidores stateless, Tasks, discovery, cache, autorização e compatibilidade em produção.

MCP IA Agentes Produção Stateless Tasks Arquitetura Segurança Python
Tempo de leitura 24 min
Nível Avançado
Código Python + JSON-RPC
MCP em Produção em 2026: Como Migrar para Stateless, Tasks e Descoberta Progressiva
Sumário
  • MCP deixou de esconder estado no transporte
  • O que mudou no MCP 2026
  • Arquitetura alvo: transporte stateless, estado explícito
  • Implementação: uma auditoria longa com MCP Tasks
  • Python e o SDK atual
  • Autorização e segurança
  • Síncrono, MRTR, Tasks ou fila?
  • Checklist de migração
  • Conclusão: stateless não significa sem estado
  • Fontes oficiais
Sumário do Artigo
  • MCP deixou de esconder estado no transporte
  • O que mudou no MCP 2026
  • Arquitetura alvo: transporte stateless, estado explícito
  • Implementação: uma auditoria longa com MCP Tasks
  • Python e o SDK atual
  • Autorização e segurança
  • Síncrono, MRTR, Tasks ou fila?
  • Checklist de migração
  • Conclusão: stateless não significa sem estado
  • Fontes oficiais

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/call de resources/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.

Comparação entre MCP legado com handshake e sessão presa a uma conexão, MCP 2026 stateless com requests autodescritivos e extensões opcionais como Tasks e MCP Apps

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 _meta completo em cada request, declara extensões, persiste taskId e respeita pollIntervalMs;
  • gateway/WAF: termina TLS, valida Origin, aplica limites e usa Mcp-Method/Mcp-Name como 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.
Fluxo de uma chamada MCP pelo gateway até instâncias stateless, store explícito de Tasks, worker, solicitação de aprovação humana e resultado final

Recomendação: trate taskId como um localizador opaco, não como credencial. Autorize tasks/get, tasks/update e tasks/cancel de 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/get resolva 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
Matriz de decisão que compara duração, reconexão, input humano, throughput e durabilidade entre execução síncrona, MRTR, MCP Tasks e fila externa

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, initialized e Mcp-Session-Id em clientes, servidores, gateways, testes e dashboards.
  • [ ] Atualizar cliente e servidor para 2026-07-28 e confirmar que cada request carrega a mesma versão no header e em _meta.
  • [ ] Exigir Mcp-Method; validar Mcp-Name contra params.name, params.uri ou, em Tasks, params.taskId; no MCP endpoint, responder a header ausente, malformado ou divergente com HTTP 400 e 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/discover e 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 caches public e private por 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 e CreateTaskResult.
  • [ ] 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 taskId no cliente e retomar tasks/get após restart ou troca de conexão.
  • [ ] Respeitar pollIntervalMs e tratar completed, failed e cancelled como terminais.
  • [ ] Definir TTL, limpeza, lease, retry, timeout e recuperação de jobs abandonados.
  • [ ] Implementar cancelamento como pedido cooperativo observado em checkpoints seguros.
  • [ ] Deduplicar inputRequests e aceitar tasks/update apenas 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 iss antes 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.

Fontes oficiais

  • The 2026-07-28 Specification — release oficial
  • The New MCP Roadmap
  • MCP Extensions Overview
  • MCP Tasks
  • Streamable HTTP — especificação 2026-07-28
  • Authorization — especificação 2026-07-28
  • MCP Python SDK

Leia também

  • MCP para Não-Programadores Comece pelos fundamentos e pelas primeiras conexões com ferramentas.
  • Quando NÃO Usar MCP ou A2A Filas, retries, DLQ e idempotência para jobs em produção.
  • Identidade e Permissões para Agentes Tokens scoped, menor privilégio e approval gates.
  • Observabilidade de Agentes Traces, logs e alertas para encontrar falhas reais.
  • Graph Engineering para Agentes Estado, checkpoints e transições determinísticas.

Quer aplicar isso no seu trabalho? Falar comigo no WhatsApp (confiança humana) ou comece seu próximo passo na trilha para construir autonomia real via fundamentos + método.

← Blog Início
André Costa

Fundamentos e método para você projetar e construir automações e agentes com autonomia real. Sem hype.

Páginas

Blog O que faço Método Conteúdos Contato

Legal

Privacidade Termos de Uso
© 2026 André Costa Autonomia com IA / fundamentos práticos / sistemas que você controla

Utilizamos cookies para entender como você interage com nosso site (GA4/GTM) e melhorar sua experiência técnica. Ao continuar, você concorda com nossa Política de Privacidade.