Capstone: build the complete de-identified support-triage MCP server — KB resources, ticket history, and three triage tools — then test it end-to-end with Claude.
Nos quatro módulos anteriores, construí cada peça do servidor MCP de triagem de suporte de forma isolada: o módulo 02 criou o esqueleto do servidor, o módulo 03 adicionou os recursos de base de conhecimento e histórico de tickets, e o módulo 04 conectou as ferramentas de classificar, rotear e escalonar. Cada peça funcionou por conta própria. O sistema de triagem tornou-se genuinamente útil apenas quando recursos e ferramentas operaram juntos em um único servidor.
Pense como uma biblioteca de referência e um conjunto de botões de ação dentro da mesma sala. Um agente de suporte que só pode ler o manual é um mecanismo de busca. Aquele que só pode pressionar botões escalará cada ticket cegamente. O servidor integrado dá ao Claude acesso a ambos ao mesmo tempo: ele pode consultar a política de reembolso, ler os tickets abertos recentes, classificar o problema e roteá-lo — tudo em um único turno de conversa.
Essa combinação também muda como a IA raciocina. Quando o Claude tem tanto a base de conhecimento quanto a fila de tickets disponíveis, ele pode comparar o assunto de um ticket com padrões de problemas conhecidos na base de conhecimento antes de decidir como classificá-lo. Os recursos e ferramentas não são independentes — os recursos informam como as ferramentas são usadas.
Antes de construir o servidor MCP, o sistema de automação de suporte usava integrações separadas para cada fonte de dados: um cliente HTTP para consultar a base de conhecimento, outro para puxar tickets da fila e um terceiro para postar decisões de roteamento. Cada integração tinha seu próprio auth, seu próprio tratamento de erros e seu próprio schema. Adicionar um novo modelo de IA significava recabear todos os três.
O MCP substituiu essa fiação personalizada por um único protocolo. A IA se conecta a um servidor e descobre cada recurso e ferramenta através de um endpoint de listagem padrão. Adicionar um novo modelo — ou trocar para um diferente — não requer nenhuma mudança no servidor.
O benefício mais profundo foi a manutenibilidade. O cliente da base de conhecimento, o cliente de tickets e o cliente de roteamento precisavam cada um de testes separados e monitoramento separado. O servidor MCP consolida tudo isso em um único lugar: um conjunto de testes, um fluxo de logs, um lugar para adicionar auth. O protocolo não simplifica apenas a integração de IA — simplifica toda a camada de acesso a dados.
Neste projeto final monto o servidor completo: ambos os recursos, todas as três ferramentas e um teste de ponta a ponta que conecta o Claude ao sistema finalizado. Ao final, você terá um servidor de triagem funcionando que pode estender — não um esboço pedagógico, mas um ponto de partida real.
Escopo: este é o menor servidor completo que demonstra o fluxo de trabalho completo de recurso mais ferramenta. Autenticação, limitação de taxa e observabilidade são abordadas brevemente na seção "Indo Além" — não como implementações completas. Um sistema real adiciona essas camadas em torno da mesma superfície MCP mostrada aqui.
O servidor expõe dois recursos — knowledge-base://articles/{id} (documentos de política da base de conhecimento) e tickets://recent (fila de tickets abertos ao vivo) — e três ferramentas: classify_ticket, route_ticket e escalate_ticket. Cada peça faz uma coisa. Juntas elas tratam um ciclo completo de triagem.
A fundação é inalterada do módulo 02: criar o servidor com capacidades tipadas, conectar um transport e conectar. A diferença principal aqui é que o objeto de capacidades deve declarar tanto resources quanto tools. O SDK usa essa declaração para determinar quais schemas de requisição tipados aceitar de clientes MCP.
Isso importa mais do que pode parecer. O objeto de capacidades é o contrato público do servidor. Se você declarar apenas recursos, um cliente verificando o contrato saberá para não chamar endpoints de ferramenta — ele nem tentará. Declarar tanto recursos quanto ferramentas antecipadamente é uma declaração honesta sobre o que seu servidor pode fazer.
O TypeScript reforça isso. Se você registrar um handler CallToolRequestSchema mas esquecer de declarar tools: {}, o servidor compila sem erro mas rejeita chamadas de ferramenta em tempo de execução. Declarar capacidades no código torna a intenção visível tanto para o SDK quanto para seus colegas.
Todas as quatro importações de schema aparecem aqui juntas pela primeira vez. Nos módulos isolados, apenas os schemas de recurso ou apenas os schemas de ferramenta eram necessários. No projeto final, todos os quatro são necessários porque este único servidor trata o ciclo completo: listar e ler recursos, listar e chamar ferramentas.
O caminho de importação @modelcontextprotocol/sdk/types.js é onde o SDK do MCP exporta os schemas tipados. Usar os schemas tipados em vez de strings simples é o que dá ao TypeScript as informações necessárias para verificar os argumentos do seu handler. Se você vir um handler cujo argumento é tipado como unknown, isso geralmente é sinal de que a forma errada de setRequestHandler foi chamada.
Modo estrito do TypeScript: se seu projeto usa strict: true no tsconfig.json, você pode precisar adicionar asserções de tipo ao acessar propriedades de args dentro de handlers de ferramenta, pois o SDK tipifica arguments como unknown. As asserções as { subject: string } no handler de ferramenta abaixo são o padrão padrão para isso.
Capacidades importam: omita resources: {} e clientes MCP não descobrirão seus recursos mesmo que os handlers estejam registrados. Declare tudo que você pretende expor antes de registrar qualquer handler.
Recursos são a prateleira somente leitura do servidor. O Claude pega artigos e dados de tickets da prateleira para informar suas decisões, mas não pode mudar o que está nela. Essa garantia somente leitura importa quando a equipe de suporte compartilha dados de tickets com a IA: ela pode ler e recomendar, mas não pode modificar registros diretamente. O limite entre leitura e ação é imposto pelo protocolo, não apenas por convenção.
Os dois recursos que o servidor de triagem expõe são fundamentalmente diferentes em caráter:
tickets://recent), buscado fresco a cada leitura. A IA obtém os últimos tickets abertos toda vez que pergunta.Essa diferença — itens individuais estáveis vs. agregado ao vivo — é um padrão de design útil. Dados estáveis mapeiam naturalmente para URIs de item individual; agregados ao vivo mapeiam naturalmente para um único endpoint.
Comece definindo a camada de dados. Em produção, isso seriam chamadas a banco de dados; aqui arrays em memória mantêm o foco na superfície MCP em vez de detalhes de persistência.
As definições de interface são mais do que dicas de tipo. Elas documentam o formato que a IA pode esperar receber. Quando o Claude lê tickets://recent, ele recebe um array JSON de objetos SupportTicket — os campos id, subject, status e priority são com o que ele trabalha.
Manter os tipos de dados explícitos também torna a refatoração futura mais segura. Se o schema de ticket mudar — digamos, você adicionar um campo customerId — o TypeScript evidenciará cada lugar no servidor que precisa ser atualizado. Esse feedback em tempo de compilação é especialmente valioso em um servidor MCP onde o código do handler é o contrato entre a IA e seus dados.
Agora registre o handler ListResourcesRequestSchema para que o Claude possa descobrir quais dados estão disponíveis. Ambos os recursos aparecem em uma única listagem. A IA solicita os específicos do mesmo menu, escolhendo com base na tarefa em mãos.
O campo mimeType diz ao Claude o que esperar. Uma tarefa de triagem de tickets o levaria a buscar tickets://recent (array JSON); uma pergunta sobre o prazo de reembolso o levaria a ler knowledge-base://articles/kb-001 (texto simples). O Claude usa o nome e o tipo MIME juntos para decidir quais recursos são relevantes.
Quando o Claude lista recursos e depois lê um, a conversa parece duas chamadas MCP sequenciais: resources/list para descobrir o que está disponível, depois resources/read com a URI específica. A IA conduz ambas as chamadas — o servidor apenas responde. Este modelo pull (IA solicita dados) é o oposto de um modelo push (servidor envia dados como eventos), e é por isso que recursos são descritos como "prateleira" em vez de "feed."
O handler ReadResourceRequestSchema roteia para a fonte de dados correta com base na URI. Um handler gerencia ambos os recursos — uma verificação startsWith para artigos da base de conhecimento, uma correspondência exata para a fila de tickets:
O throw final não é uma captura abrangente — é uma rejeição intencional de URIs que o servidor não reconhece. Se o Claude solicitar uma URI que nunca foi listada, o servidor diz claramente em vez de retornar conteúdo vazio. Falhar ruidosamente em uma URI inválida previne que a IA produza uma resposta fundamentada em nada.
Antes de continuar, preveja o que acontece quando o Claude solicita knowledge-base://articles/kb-999. Trace através do handler: a verificação startsWith passa, articleId se torna "kb-999", knowledgeBase.find retorna undefined e o handler lança "Article not found: kb-999". O cliente MCP apresenta isso como um erro — que é o comportamento correto. Retornos de conteúdo vazio silenciosos são mais difíceis de depurar do que erros explícitos.
Ferramentas são as tomadas de energia do servidor. Quando o Claude se conecta a uma ferramenta, ela desencadeia um efeito colateral no mundo real: atribuir um ticket a uma fila ou acionar um agente humano. Ao contrário dos recursos — que são seguros de chamar repetidamente sem consequências — chamadas de ferramenta alteram estado. Rotear um ticket notifica a fila; escalá-lo alerta um humano. Esses efeitos persistem após o fim da conversa.
Essa assimetria é por que a distinção recurso/ferramenta existe no protocolo, não apenas como uma convenção de nomenclatura. A IA pode navegar livremente pela base de conhecimento. Ela não pode disparar escalonamentos livremente. A estrutura do protocolo reflete a estrutura da responsabilidade.
Cada ferramenta trata uma transição no ciclo de vida do ticket:
classify_ticket: recebe um assunto de ticket, retorna uma categoria e nível de urgência. Este é um ponto de decisão, não uma etapa de efeito colateral — produz informação usada para tomar a decisão de roteamento.route_ticket: recebe um ID de ticket e um nome de fila, atribui o ticket. Isso altera estado no sistema de suporte.escalate_ticket: recebe um ID de ticket e uma razão, aciona um agente humano. Esta é a ação de maior consequência — interrompe uma pessoa.Registre todas as três em um único handler ListToolsRequestSchema para que o Claude veja o menu completo de uma vez. As descrições das ferramentas são a parte mais importante deste registro — o Claude as lê para decidir qual ferramenta chamar e quando:
A restrição enum em queue é deliberada. Ao listar nomes de fila válidos no schema, o Claude fica restrito a escolher das opções que você definiu — ele não pode inventar um nome de fila que não existe no seu sistema. Restrições de schema de entrada são sua primeira linha de validação antes de qualquer lógica de negócio ser executada.
O handler CallToolRequestSchema alterna pelo nome da ferramenta e executa a lógica apropriada. Cada ramo retorna um array content com um item text — o formato de resposta de ferramenta MCP padrão:
As respostas em texto de cada ferramenta tornam-se parte do contexto do Claude para o próximo passo. Quando classify_ticket retorna {"category":"account","urgency":"medium"}, o Claude lê esse resultado e o usa para decidir o que passar a route_ticket. As ferramentas são uma cadeia, e a resposta de cada etapa alimenta a próxima.
Lógica de classificação stub: a correspondência de palavras-chave em classify_ticket demonstra a superfície MCP, não um classificador de produção. Uma implementação real chamaria um modelo ou um serviço estruturado. O valor aqui é o formato do handler de ferramenta, não a lógica de negócio dentro dele.
A ferramenta escalate_ticket dispara sem salvaguardas — qualquer coisa que a IA decida passar como razão vai direto. O que você adicionaria para prevenir escalonamentos falsos? Considere dois níveis: validação imediata (este ticket está em um estado onde o escalonamento faz sentido?), e auditoria de longo prazo (como você rastrearia qual invocação de IA desencadeou cada escalonamento em produção?).
Esta não é uma preocupação hipotética. Uma ferramenta que aciona um humano às 3h tem custo real se disparar incorretamente. A responsabilidade por salvaguardas sensatas fica com o autor da ferramenta, não com a IA. A IA não sabe como é "muitos escalonamentos" para sua organização — você sabe. Projete ferramentas de acordo.
Com todos os handlers registrados, conecte o transport para tornar o servidor ativo. Qualquer cliente compatível com MCP pode agora descobrir e usar tanto os recursos quanto as ferramentas:
O servidor está agora em execução, aguardando no stdin por mensagens do protocolo MCP. Um cliente — Claude Desktop, seu próprio harness de teste ou qualquer ferramenta compatível com MCP — pode se conectar e enviar requisições.
Para verificar o fluxo completo, o seguinte script de teste usa o SDK da Anthropic para chamar o Claude diretamente com as definições de ferramenta. O Claude lê os schemas, decide quais ferramentas chamar e em que ordem, e retorna conteúdo de uso de ferramenta estruturado. Isso espelha o que qualquer cliente MCP faz por baixo dos panos.
Em um deploy MCP completo, o cliente de IA (Claude Desktop, por exemplo) se conecta ao processo do servidor via stdio e chama resources/list e tools/list na inicialização. Nosso script de teste pula a camada de transport MCP e chama o Claude diretamente com os schemas de ferramenta — uma forma útil de testar o design do schema e a qualidade das descrições antes de conectar a pilha completa do protocolo.
O teste fornece apenas as duas ferramentas mais relevantes — classify_ticket e route_ticket. Quando você dá ao Claude um conjunto de ferramentas mais estreito, ele pode raciocinar sobre a tarefa com mais eficiência. Exponha o que a tarefa precisa, não o inventário completo do servidor. Incluir escalate_ticket quando a tarefa não requer escalonamento introduz ambiguidade desnecessária.
Quando você executa isso, o Claude lê os schemas de ferramenta e chama classify_ticket com o texto do assunto primeiro. Ele recebe {"category":"account","urgency":"medium"}, depois chama route_ticket com t-001 e uma fila apropriada. O ciclo completo de triagem — classificar e depois rotear — acontece em um único turno de conversa.
Você não disse ao Claude para classificar antes de rotear. Ele inferiu essa ordem a partir da descrição da tarefa e dos schemas de ferramenta. Essa orquestração emergente de ferramentas é o que o MCP habilita: a inteligência está na IA; a estrutura está no protocolo.
O que isso demonstra: O Claude raciocina sobre quais ferramentas chamar e em que sequência com base apenas nas descrições das ferramentas. O servidor expõe capacidades; a IA decide como usá-las. Essa divisão é por que o MCP escala — adicionar uma nova ferramenta não requer mudar a IA ou o cliente.
O projeto final reuniu tudo dos quatro módulos anteriores. Antes de olhar adiante para o que vem a seguir, vale revisar a estrutura do que você montou — não apenas o código, mas as decisões de design embutidas nele.
Olhando para os cinco módulos deste percurso, você construiu um servidor MCP completo a partir dos primeiros princípios:
A escolha de design chave que percorre todos os cinco módulos é a distinção recurso/ferramenta. Não é uma convenção de nomenclatura — é uma decisão estrutural sobre o que a IA pode fazer e que consequências fluem de suas ações. Recursos são armazenamentos de dados somente leitura. Ferramentas são operações com efeitos colaterais. Manter essas categorias separadas torna servidores MCP mais fáceis de raciocinar, testar e auditar.
O servidor acima está completo mas não endurecido. Três áreas a abordar antes de colocá-lo diante de tráfego real:
Autenticação: MCP sobre stdio roda dentro de um limite de processo confiável, que frequentemente é suficiente para ferramentas internas. Para transports HTTP, adicione validação de token bearer em uma camada de middleware antes que a requisição alcance seus handlers. Os handlers em si não mudam — auth envolve a camada de protocolo.
Limitação de taxa: chamadas de ferramenta que desencadeiam ações externas — roteamento, escalonamento — devem ter taxa limitada por cliente para prevenir automação descontrolada. Um contador simples em memória com chave por identidade do cliente é um ponto de partida razoável para deploys de baixo tráfego. Rastreie contagens de chamadas por janela de tempo, retorne um erro quando o limite for excedido.
Observabilidade: registre cada chamada de ferramenta com sua entrada, saída e latência. Recursos que retornam payloads grandes valem ser cronometrados separadamente. Quando algo der errado em produção, você quer o rastro completo da chamada — qual ferramenta foi chamada, com quais argumentos e o que retornou — não apenas uma mensagem de exceção. Atribua um ID de requisição a cada sessão MCP para que você possa correlacionar rastros entre chamadas.
Nenhuma dessas mudanças altera a superfície MCP ou a lógica do handler. Elas envolvem ao redor dela. Essa separação limpa entre protocolo (handlers) e infraestrutura (auth, limites, logging) é um dos pontos fortes de design do SDK.
Mais uma área a considerar enquanto você estende este servidor: mensagens de erro. Os erros lançados nos handlers atuais ("Article not found: kb-001", "Unknown tool: xyz") são strings voltadas para desenvolvedores. Em um servidor de produção, você quereria códigos de erro ao lado de mensagens, para que clientes possam tratá-los programaticamente em vez de parsear conteúdo de string. A especificação MCP deixa a representação de erros para a implementação, então esta é uma escolha de design sua.
Por fim, teste suas ferramentas com entradas adversariais antes de implantar. O que acontece se o assunto passado para classify_ticket for uma string vazia? E se route_ticket receber um valor de queue que passa na validação do schema mas não corresponde a nenhuma fila real no seu sistema? As restrições do schema capturam muitas entradas inválidas, mas não todas. Adicionar verificações explícitas dentro do handler é defesa em profundidade.
Apply what you have built to reason about MCP resource and tool design.
1. Your team wants to add a 'get customer contact details by customer ID' feature. Should it be a resource or a tool?
Correct Answer: B
Resources are read-only data stores identified by URIs; they can be parameterized via URI (e.g., customers://contacts/cust-001). Tools are operations with side effects. Fetching read-only data from a store is a resource, not a tool.
2. In the capstone server, both KB articles and tickets://recent appear in the same ListResourcesRequestSchema response. Why does a single handler work for both?
Correct Answer: B
ListResourcesRequestSchema is registered once and its handler returns a single resources array. That array can describe any number of resources from any source — KB articles, ticket queues, agents, or anything else the server knows about.
3. Which of the following is the clearest reason route_ticket is implemented as a tool rather than a resource?
Correct Answer: C
The resource/tool distinction is about side effects, not parameter count or return type. Route_ticket changes the system by assigning a ticket — an irreversible action. That is the defining characteristic of a tool.
4. Why does the ReadResourceRequestSchema handler throw an Error when a KB article is not found, rather than returning an empty string?
Correct Answer: B
Fail-loud is a deliberate design choice. If the handler returned empty content, Claude might proceed with an answer grounded in nothing — and the user would get a confident but wrong response. A visible error is easier to diagnose and fix.
5. What happens if you omit `resources: {}` from the capabilities object when creating the Server?
Correct Answer: B
The capabilities object is the server's public declaration of what it supports. Clients check it before sending resource discovery requests. Omitting resources: {} means the client never sends ListResourcesRequestSchema, so your handlers never run.
6. The end-to-end test provides both classify_ticket and route_ticket as tools. What determines which tool Claude calls first?
Correct Answer: C
Claude reads the tool descriptions and the user message to decide which tools to call and in what sequence. It inferred that classification should precede routing from the task description — not from any ordering in the tools array.
7. A team member suggests a `tickets://summary/t-001` resource that returns a Claude-generated summary. What is the strongest objection?
Correct Answer: B
Resources are meant to return stored or efficiently computable data. A resource that calls a model on every read blurs the read/action boundary, makes caching unreliable, and hides a non-trivial cost behind a simple URI. A tool makes the intent explicit.
Estenda o servidor de triagem com um recurso tickets://detail/{id} que retorna um único ticket pelo ID. Este é o equivalente individual de tickets://recent — assim como artigos da KB são acessados individualmente via seus IDs, os tickets também devem ser.
Passos:
ListResourcesRequestSchema, espalhe recentTickets.map(...) para expor cada ticket como um URI individualtickets://detail/ ao ReadResourceRequestSchema que extrai o ID do ticketrecentTickets; lance um erro claro se não encontradomimeType: "application/json"Dica: Extraia o ID com uri.replace("tickets://detail/", ""). O spread da lista: ...recentTickets.map(t => ({uri: "tickets://detail/" + t.id, ...})).
Solução — expor URIs individuais de tickets na lista de recursos
<CodeExample title="Spread ListResourcesRequestSchema" language="typescript"> // Adicione ao lado de tickets://recent no seu array de recursos: ...recentTickets.map((t) => ({ uri: "tickets://detail/" + t.id, name: "Ticket " + t.id, description: t.subject, mimeType: "application/json", })), </CodeExample>Muito bem. Veja o que vem a seguir.
Voltar para esta trilhaOu agende a chamada de diagnóstico