Introdução
Um cliente de IA não deveria precisar de uma integração personalizada para cada aplicação que utiliza. O Model Context Protocol (MCP) oferece aos clientes uma forma padronizada de descobrir ferramentas, consultar seus contratos de entrada e executá-las. Neste laboratório, a ferramenta é deliberadamente pequena: ela consulta um caso de suporte sintético e não pode alterar nada.
Você criará o servidor usando o manipulador MCP stateless atual do Cloudflare:
- Um namespace dedicado do Cloudflare KV armazena o registro empresarial sintético. O KV é o armazenamento explícito dos dados da aplicação; ele não é uma memória de sessão MCP oculta.
- Um esquema rigoroso do Zod aceita apenas um identificador de ticket sintético e rejeita campos adicionais.
McpServer.registerTool()publica uma ferramenta com anotações somente leitura e não destrutivas.createMcpHandler()cria um servidor novo para cada requisição HTTP Streamable.- O cliente oficial MCP TypeScript descobre e executa a ferramenta por meio de conexões independentes.
- As verificações locais e implantadas comprovam a consulta válida, o comportamento seguro para registros ausentes, a rejeição de argumentos inválidos e a ausência de estado de sessão compartilhado implícito.
Este endpoint não exige autenticação de propósito, pois expõe apenas um registro sintético, descartável e somente leitura. Não use este padrão para publicar dados privados de clientes. Servidores de produção devem adicionar autenticação e autorização antes de acessar dados de tenants; provedores OAuth externos estão fora do escopo deste laboratório para iniciantes.
Anteriormente, o ecossistema MCP usava endpoints SSE e código padrão de servidor com estado. Este laboratório não ensina esse design legado. Ele usa Streamable HTTP e uma fábrica de servidores por requisição, que é a orientação atual do Cloudflare para um novo servidor remoto.
Antes de entrar diretamente neste curso, conclua Conectar o LabEx à sua conta do Cloudflare. Cada VM nova do LabEx precisa de sua própria autorização do Wrangler. Os laboratórios anteriores do curso são recomendados, mas suas VMs e recursos nunca são reutilizados aqui.
Autorizar a VM e criar um catálogo dedicado
Nesta etapa, você autorizará esta VM nova, escolherá a conta de aprendizado e criará um namespace KV descartável. Manter o catálogo separado deixa clara a propriedade do recurso e facilita a limpeza.
Entre no projeto preparado e verifique as versões fixadas das ferramentas:
cd /home/labex/project/read-only-mcp-tool
node --version
npx wrangler --version
Autorize esta VM:
npx wrangler login
Abra no navegador o link do dispositivo exibido, revise as permissões solicitadas e autorize sua conta de aprendizado dedicada. Volte ao terminal, aguarde a conclusão e consulte a identidade estruturada:

A lista de permissões é mais ampla do que a necessária para este laboratório, pois o Wrangler é a CLI de desenvolvimento geral do Cloudflare. Antes de aprovar, confirme que a página identifica o Wrangler, que você está usando a conta de aprendizado correta e que nenhuma senha ou token aparece no terminal.
npx wrangler whoami --json
Confirme loggedIn: true e o nome da conta pretendida, mesmo quando a saída listar apenas uma conta. Copie o id real dessa conta. Gere um prefixo exclusivo e salve a configuração inicial do Worker, substituindo primeiro o espaço reservado:
ACCOUNT_ID="paste-your-confirmed-account-id"
RUN="labex-c11-s07-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/server.ts",
"compatibility_date": "2026-09-19",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"preview_urls": false,
"observability": { "enabled": true }
}
JSON
Crie o namespace sem pedir ao Wrangler para editar o arquivo automaticamente:
npx wrangler kv namespace create "$RUN-cases" --update-config=false
Se o Wrangler perguntar se deve adicionar uma binding automaticamente, escolha No; a próxima edição fará essa conexão de forma explícita. Copie da saída o ID do namespace com 32 caracteres e adicione exatamente uma binding:
NAMESPACE_ID="paste-the-created-namespace-id"
python3 - "$NAMESPACE_ID" <<'PY'
import json, sys
from pathlib import Path
path = Path('wrangler.jsonc')
data = json.loads(path.read_text())
data['kv_namespaces'] = [{'binding': 'SUPPORT_CASES', 'id': sys.argv[1]}]
path.write_text(json.dumps(data, indent=2) + '\n')
PY
npx wrangler kv namespace list
python3 .labex/verify.py authorization
O nome da binding, SUPPORT_CASES, é o identificador que seu código usará. O ID do namespace aponta para o recurso real na conta confirmada. Nada foi implantado ainda.
Inserir dados empresariais sintéticos explícitos
Nesta etapa, você colocará o mesmo registro fornecido no KV local e no remoto. O armazenamento de dados é explícito: uma requisição MCP pode ser stateless enquanto a aplicação continua lendo dados empresariais persistentes por chave.
Consulte o fixture antes de fazer o upload:
cat fixtures/case.json
O prefixo T-SYNTH-101 e o marcador synthetic: true deixam visível o limite da demonstração. O registro não contém nome, e-mail, mensagem ou credencial de cliente real.
Insira os dados no armazenamento local usado por wrangler dev:
npx wrangler kv key put case:T-SYNTH-101 --path fixtures/case.json --binding SUPPORT_CASES --local
Insira os dados no namespace de nuvem dedicado:
npx wrangler kv key put case:T-SYNTH-101 --path fixtures/case.json --binding SUPPORT_CASES --remote
Leia as duas cópias por meio da binding:
npx wrangler kv key get case:T-SYNTH-101 --binding SUPPORT_CASES --local --text
npx wrangler kv key get case:T-SYNTH-101 --binding SUPPORT_CASES --remote --text
python3 .labex/verify.py catalog
O verificador confere o namespace pelo ID da conta, exige exatamente uma chave e compara o JSON remoto com o fixture sintético fornecido. O KV pode apresentar consistência eventual entre locais. Portanto, se a primeira leitura remota não encontrar por alguns instantes um valor recém-gravado, aguarde alguns segundos e tente novamente, em vez de gravar cópias repetidas.
Registrar uma ferramenta MCP rigorosa e somente leitura
Nesta etapa, você definirá uma fábrica de servidor MCP e uma ferramenta de consulta somente leitura.
McpServer descreve a superfície do protocolo. A fábrica cria uma instância nova para cada requisição HTTP, enquanto a binding SUPPORT_CASES continua sendo a fonte explícita dos dados empresariais. Crie src/server.ts:
cat > src/server.ts <<'TS'
import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
interface Env {
SUPPORT_CASES: KVNamespace;
}
const lookupInput = z.object({
ticketId: z.string().regex(/^T-SYNTH-[0-9]{3}$/, "use a synthetic ticket ID")
}).strict();
const storedCase = z.object({
ticketId: z.string(),
subject: z.string(),
status: z.string(),
priority: z.string(),
product: z.string(),
synthetic: z.literal(true)
}).strict();
function buildServer(env: Env): McpServer {
const requestInstance = crypto.randomUUID();
const server = new McpServer({
name: "synthetic-support-catalog",
version: "1.0.0"
});
server.registerTool("lookup_support_case", {
title: "Look up a synthetic support case",
description: "Read one synthetic demonstration case by its T-SYNTH identifier.",
inputSchema: lookupInput,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
}, async ({ ticketId }) => {
const raw = await env.SUPPORT_CASES.get(`case:${ticketId}`, "json");
if (raw === null) {
return {
isError: true,
content: [{ type: "text", text: `Synthetic case ${ticketId} was not found.` }]
};
}
const record = storedCase.parse(raw);
const result = { ...record, requestInstance };
return {
structuredContent: result,
content: [{ type: "text", text: JSON.stringify(result) }]
};
});
return server;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json({
service: "synthetic-support-mcp",
transport: "streamable-http",
state: "stateless"
});
}
if (url.pathname !== "/mcp") return new Response("Not found", { status: 404 });
const handler = createMcpHandler(
() => buildServer(env),
{ route: "/mcp", corsOptions: false, legacy: "stateless" }
);
return handler(request, env, ctx);
}
};
TS
npm run check
python3 .labex/verify.py server
Três limites são importantes aqui:
.strict()rejeita campos não declarados em vez de aceitá-los silenciosamente.- As anotações informam aos clientes que a ferramenta lê um catálogo sintético fechado e não tem efeito destrutivo. Uma anotação é metadado útil, mas não substitui a revisão de código que confirma a ausência de
put()oudelete(). requestInstanceé gerado quando a fábrica cria um servidor. Requisições de protocolo diferentes devem retornar marcadores diferentes, tornando observável o ciclo de vida stateless sem armazenar dados de sessão.
A configuração de compatibilidade legacy: "stateless" ainda usa Streamable HTTP. Ela permite clientes atuais que negociam a família de protocolos de 2025 e garante que cada requisição receba uma instância nova do servidor; nenhuma rota SSE ou sessão MCP durável é criada.
Criar uma verificação independente com um cliente MCP
Nesta etapa, você usará a biblioteca oficial do cliente em vez de escrever JSON-RPC manualmente. Um cliente real executa a inicialização do protocolo, a descoberta de ferramentas e a chamada por meio de StreamableHTTPClientTransport.
Crie scripts/test-client.mjs:
cat > scripts/test-client.mjs <<'JS'
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
const endpoint = process.argv[2];
if (!endpoint) throw new Error("usage: node scripts/test-client.mjs <mcp-url>");
async function withClient(label, action) {
const transport = new StreamableHTTPClientTransport(new URL(endpoint));
const client = new Client({ name: `labex-${label}`, version: "1.0.0" });
try {
await client.connect(transport);
return await action(client);
} finally {
await client.close();
}
}
const tools = await withClient("discovery", (client) => client.listTools());
const tool = tools.tools.find((item) => item.name === "lookup_support_case");
if (!tool || tool.annotations?.readOnlyHint !== true) {
throw new Error("the read-only lookup tool was not discoverable");
}
console.log("DISCOVERED lookup_support_case");
async function lookup(ticketId) {
return withClient(`lookup-${ticketId.toLowerCase()}`, (client) => client.callTool({
name: "lookup_support_case",
arguments: { ticketId }
}));
}
const first = await lookup("T-SYNTH-101");
const second = await lookup("T-SYNTH-101");
const a = first.structuredContent;
const b = second.structuredContent;
if (!a || !b || a.synthetic !== true || a.status !== "investigating") {
throw new Error("the valid synthetic record was not returned");
}
console.log(`VALID synthetic=${a.synthetic} status=${a.status}`);
const missing = await lookup("T-SYNTH-404");
console.log(`MISSING isError=${missing.isError === true}`);
let invalidRejected = false;
try {
const invalid = await withClient("invalid", (client) => client.callTool({
name: "lookup_support_case",
arguments: { ticketId: "REAL-101", unexpected: "must-not-pass" }
}));
invalidRejected = invalid.isError === true;
} catch {
invalidRejected = true;
}
console.log(`INVALID_REJECTED ${invalidRejected}`);
const stateless = typeof a.requestInstance === "string"
&& typeof b.requestInstance === "string"
&& a.requestInstance !== b.requestInstance;
console.log(`STATELESS ${stateless}`);
if (missing.isError !== true || !invalidRejected || !stateless) process.exitCode = 1;
JS
python3 .labex/verify.py client
Cada chamada auxiliar cria e fecha seu próprio transporte de cliente. A descoberta comprova que o servidor anuncia o contrato da ferramenta. Duas chamadas válidas devem ler o mesmo registro do KV, mas retornar marcadores requestInstance diferentes. O caso ausente é um erro normal no nível da ferramenta, enquanto um identificador inválido é rejeitado pelo esquema de entrada antes que o manipulador leia o KV.
Exercitar localmente o contrato MCP
Nesta etapa, você iniciará o Worker usando o KV local e executará a verificação completa do cliente antes de acessar o endpoint implantado.
Inicie o servidor de desenvolvimento:
npx wrangler dev --ip 127.0.0.1 --port 8787
Deixe esse terminal em execução. Abra um segundo terminal, entre no mesmo projeto e verifique a pequena rota de saúde:
cd /home/labex/project/read-only-mcp-tool
curl --fail --silent http://127.0.0.1:8787/health | python3 -m json.tool
A saída deve conter transport: "streamable-http" e state: "stateless". Agora execute o cliente do protocolo:
node scripts/test-client.mjs http://127.0.0.1:8787/mcp
As cinco linhas de verificação devem mostrar a descoberta, o resultado sintético válido, um erro seguro para o caso ausente, a rejeição da entrada inválida e STATELESS true. Volte ao primeiro terminal e pressione Ctrl+C depois da verificação.
Execute a verificação independente. Ela inicia outro Worker local limitado na porta 8791, exercita o mesmo código importado e o encerra automaticamente:
python3 .labex/verify.py local
Implantar e testar o endpoint MCP remoto
Nesta etapa, você implantará o Worker com sua binding KV explícita e executará o mesmo cliente no endpoint workers.dev real.
Faça a implantação usando a configuração do projeto:
npx wrangler deploy
Copie a URL de implantação exibida e salve-a sem a barra final:
WORKER_URL="https://your-generated-worker.your-subdomain.workers.dev"
Verifique a rota de saúde e conecte o cliente MCP a /mcp:
curl --fail --silent "$WORKER_URL/health" | python3 -m json.tool
node scripts/test-client.mjs "$WORKER_URL/mcp"
python3 .labex/verify.py deployed
O verificador independente obtém o endpoint a partir da conta selecionada, em vez de confiar na variável do shell. Ele também verifica a binding implantada SUPPORT_CASES, o registro remoto exato e os cinco comportamentos MCP. Uma rota de saúde acessível, sozinha, não é suficiente: a descoberta e a chamada precisam passar pelo cliente do protocolo.
Abra Workers & Pages e selecione o Worker gerado. A visão geral deve associar o domínio workers.dev ao Worker e mostrar uma binding KV SUPPORT_CASES. Os valores abaixo são exemplos da execução testada; os nomes exclusivos dos seus recursos e as quantidades serão diferentes.

Inspecionar e remover os recursos pertencentes à execução
Nesta etapa, você inspecionará o estado observável na nuvem e depois excluirá apenas o Worker e o namespace KV desta execução enquanto o Wrangler ainda estiver autorizado.
Abra o Cloudflare Dashboard e selecione a mesma conta de aprendizado. Em Workers & Pages, abra o Worker cujo nome começa com labex-c11-s07-. Confirme que a implantação mais recente está saudável, que a observabilidade está habilitada e que a binding SUPPORT_CASES aponta para o ID do namespace em wrangler.jsonc.
Abra Storage & databases > KV, selecione o namespace -cases correspondente e consulte case:T-SYNTH-101. O valor é o fixture sintético; não adicione informações pessoais. Essas telas do Dashboard são úteis para orientação, mas o cliente e o verificador continuam sendo as evidências funcionais autoritativas.
A visualização KV Pairs mostra primeiro a chave exata e uma prévia do valor JSON:

Expanda a linha para associar essa chave aos campos retornados pela ferramenta MCP. O fixture testado usa status: investigating, priority: medium e synthetic: true.

Volte ao Worker e abra Observability. Os eventos bem-sucedidos de POST /mcp e de transporte GET /mcp mostram que um cliente MCP remoto real alcançou o Worker implantado. Na execução testada, todos os 42 eventos capturados foram concluídos com sucesso e nenhum gerou erro do Worker; a quantidade de requisições pode ser diferente no seu caso.

Execute mais uma verificação de observação independente antes da exclusão:
python3 .labex/verify.py observed
cat wrangler.jsonc
Confirme o nome exclusivo exato do Worker e o ID do namespace. Em seguida, exclua o Worker:
npx wrangler delete
Se for solicitado, confirme o nome do Worker exibido e responda y. Exclua apenas o namespace selecionado pela binding SUPPORT_CASES:
npx wrangler kv namespace delete --binding SUPPORT_CASES
npx wrangler kv namespace list
python3 .labex/verify.py deleted
Atualize as listas de Workers e KV no Dashboard. Os dois recursos labex-c11-s07-... devem estar ausentes, enquanto os recursos não relacionados permanecem. Uma requisição malsucedida ao endpoint não comprova a exclusão; o verificador consulta diretamente os inventários da conta autorizada.
Pesquise o nome exato do Worker gerado. Um resultado vazio confirma que o Dashboard não o lista mais:

Pesquise no Workers KV o namespace -cases exato. O estado vazio e o armazenamento atual de 0 B confirmam que o catálogo descartável também foi removido desta conta de teste limpa:

Revogar a autorização desta VM
Nesta etapa, você revogará a autorização temporária da VM depois de comprovar a ausência dos recursos.
npx wrangler logout
npx wrangler whoami --json || true
O resultado estruturado deve informar loggedIn: false, ou o Wrangler pode retornar um resultado não autenticado com código diferente de zero. O logout é deixado propositalmente para o final: a verificação da exclusão precisa de acesso de leitura à conta selecionada, enquanto a VM descartável não precisa mais dele.
Resumo
Você publicou e removeu um serviço MCP limitado e somente leitura no Cloudflare. Você:
- manteve os dados empresariais sintéticos em um namespace KV dedicado, em vez de usar um estado de sessão MCP implícito;
- registrou uma ferramenta detectável com validação rigorosa de entrada e anotações somente leitura;
- disponibilizou a ferramenta por meio do manipulador Streamable HTTP stateless atual;
- usou um cliente MCP real para testar descoberta, consulta válida, registro ausente e entrada inválida;
- comprovou que requisições independentes recebem instâncias novas do servidor enquanto leem os mesmos dados explícitos;
- inspecionou o estado do Worker e do KV, excluiu os dois recursos pertencentes à execução e revogou a autorização da VM.
A principal lição de design é que transporte stateless não significa uma aplicação sem dados. Isso significa que as requisições do protocolo não dependem de memória de sessão oculta. Os dados empresariais persistentes continuam explícitos, delimitados e administrados de forma independente.



