Implement the three support-triage tools — classify, route, and escalate — and understand how MCP tools trigger real-world state changes
No Módulo 3, expus a base de conhecimento e o histórico de tickets do sistema de automação de suporte como recursos MCP. A IA podia ler cada artigo da base de conhecimento e navegar pelos tickets recentes sem nenhuma intervenção minha. Essa foi a decisão certa, porque a leitura não carrega risco: se a IA ler o artigo errado, nada quebra. A base de conhecimento permanece intacta. A fila de tickets não é perturbada.
Mas classificar um ticket como de alta severidade, roteá-lo para a fila de pagamentos ou acionar um engenheiro de plantão? Essas são ações. Elas alteram estado, notificam pessoas e não podem ser desfeitas revertendo a chamada. Elas pertencem a ferramentas, não a recursos.
No sistema de automação de suporte, descobri essa distinção da maneira difícil. Um protótipo inicial expunha "rotear ticket" como uma URI de recurso (tickets://route/{id}) apenas porque parecia simétrico. O problema: clientes de IA leem recursos especulativamente para entender o que está disponível. Na primeira vez que uma IA listou os recursos disponíveis para se orientar, ela silenciosamente roteou três tickets ao vivo para a fila errada. Depois disso, o roteamento tornou-se uma ferramenta — algo que a IA deve deliberadamente invocar, não descobrir passivamente.
A linha é simples:
A distinção central: se a operação apareceria no log de auditoria do seu sistema, é uma ferramenta. Se a IA está apenas observando, é um recurso.
Pense em uma ferramenta como uma tomada de energia. Plugar algo desencadeia algo real — um ventilador gira, um aquecedor esquenta, uma máquina começa. Desconectar não reverte o que já aconteceu; apenas para a corrente futura. Ferramentas MCP funcionam da mesma forma: chamar escalate_ticket envia um pager. Não há endpoint de "des-escalonar" no sistema de pager.
Essa analogia importa para o design. Porque ferramentas têm efeitos colaterais, precisam de três coisas:
No servidor de triagem de suporte, classify_ticket é idempotente: classificar o mesmo ticket duas vezes produz a mesma categoria. route_ticket não é: rotear um ticket que já foi roteado poderia resultar em dupla atribuição.
Antes de construir cada ferramenta: decida se é idempotente e documente isso na descrição da ferramenta. A IA pode então raciocinar sobre se tentar novamente em caso de falha é seguro.
O SDK TypeScript do MCP fornece ListToolsRequestSchema para o handler de listagem de ferramentas. A IA chama isso primeiro, lê os schemas e decide qual ferramenta invocar. Observe a importação: esta é a forma de schema tipado, não a string "tools/list".
As outras duas ferramentas seguem o mesmo formato. route_ticket recebe { ticket_id, category } — onde category vem de uma chamada prévia a classify_ticket. escalate_ticket recebe { ticket_id, reason } — uma explicação em texto livre de por que o escalonamento é necessário.
Uma nota sobre descrições de ferramentas: o texto em description é o que a IA lê para decidir se chama a ferramenta. Escreva-o como uma cláusula verbal ("Classify a support ticket...") que diz à IA o que a ferramenta faz e quando usá-la. Descrições vagas produzem seleção de ferramenta imprevisível — a IA escolhe com base em semântica, não em suas intenções.
Antes de escrever os handlers, preveja as respostas para estas perguntas:
classify_ticket ou route_ticket? O que acontece se as chamar fora de ordem?escalate_ticket falhar porque a API de pager está fora do ar, o handler deve retornar isError: true ou lançar uma exceção não capturada? Qual é a diferença da perspectiva da IA?Mantenha essas respostas em mente enquanto lê cada implementação abaixo.
CallToolRequestSchema é o handler para invocações de ferramenta. A IA fornece o nome da ferramenta e os argumentos; o handler despacha pelo nome, valida as entradas e executa a lógica.
Primeiro, o helper classificador — uma função simples baseada em palavras-chave que mapeia o conteúdo do ticket para uma categoria:
A função classify é intencionalmente simples — ela usa correspondência de palavras-chave, que é rápida mas frágil. Em um sistema de suporte em produção, você a substituiria por um classificador baseado em embeddings ou uma chamada a um modelo que entende contexto ("my payment didn't go through" mapeia para faturamento mesmo sem a palavra "charge"). O handler MCP não se importa com qual abordagem você usa; ele chama classify(content) e continua.
Agora o handler CallToolRequestSchema. O ramo classify_ticket usa esse helper e retorna uma string JSON que a IA pode parsear para a próxima chamada de ferramenta:
Bom padrão: retorne strings JSON como conteúdo de texto. A IA parseia o JSON e pode encadear o valor category diretamente na próxima chamada a route_ticket — sem extração manual necessária.
O roteamento pega a categoria da classificação e atribui o ticket à fila de suporte correta. Adicione este ramo dentro do handler CallToolRequestSchema existente, antes do throw final:
O fallback ?? trata categorias desconhecidas de forma elegante: se classify_ticket de alguma forma retornar um valor que não está em queueMap, o ticket vai para tier-1-support em vez de causar um erro não tratado.
Quando a IA usa o servidor de triagem completo, a sequência típica de chamadas é assim: primeiro classify_ticket é executado no texto do ticket recebido, depois route_ticket pega a category retornada e atribui a fila. O escalonamento é opcional — acontece apenas quando a IA (ou uma regra posterior) determina que a situação é urgente o suficiente para acionar um humano.
O escalonamento é a ferramenta de maior risco no servidor — ele aciona um engenheiro de plantão, o que tem custo real e impacto de interrupção. O handler deve validar a razão antes de prosseguir e retornar um erro estruturado se o escalonamento não for justificado.
Três padrões a notar aqui. Primeiro, valide antes de agir: verifique o comprimento da razão antes de tocar qualquer sistema externo. Segundo, isError: true: definir este flag sinaliza falha para a IA para que ela possa decidir se tenta novamente, se pede desculpas ao usuário ou tenta uma abordagem diferente. Terceiro, capture erros externos: qualquer chamada real à API de pager pertence em try/catch, e o erro é apresentado como resposta estruturada — nunca como exceção não capturada que trava o handler.
Prompt reflexivo: Que salvaguardas você adicionaria a escalate_ticket para prevenir escalonamentos falsos — por exemplo, se a IA classificar erroneamente uma pergunta de faturamento de baixa severidade como uma interrupção crítica? Pense em limites de taxa, limiares de severidade ou um passo de confirmação antes de continuar.
As três ferramentas acima cobrem o ciclo central de triagem de suporte. Em um deploy de produção, você também adicionaria:
Esses padrões ficam fora do protocolo MCP em si — são preocupações do lado do servidor implementadas dentro de funções de handler. O MCP fornece os hooks; você fornece as salvaguardas.
Application-level questions about MCP tools, the power outlet analogy, and the support-triage server implementation.
1. What determines whether a support-system operation should be a tool rather than a resource?
Correct Answer: B
Resources are read-only — the AI reads without leaving a trace in the system. Tools trigger state changes: assigning a ticket, paging an engineer, updating a record. If the operation would appear in an audit log, it is a tool.
2. What does setRequestHandler(ListToolsRequestSchema, ...) give you that setRequestHandler("tools/list", ...) does not?
Correct Answer: B
The typed schema form lets TypeScript infer the exact shape of request.params and the expected return type. String-based registration is untyped — the compiler cannot catch shape mismatches between your handler and the MCP protocol.
3. Which of the following should be a tool, not a resource, in the support-triage server?
Correct Answer: C
Reading KB articles, browsing ticket history, and checking schedules are all read-only — perfect resources. Sending a pager notification changes state in an external system and cannot be undone, making it a tool.
4. The escalate_ticket handler checks reason.trim().length < 10 before escalating. A teammate suggests removing this check because the AI "always provides good reasons." What is the problem with this reasoning?
Correct Answer: B
Tool handlers are the last line of defense before touching external systems. The AI client might be misconfigured, upgraded, or replaced. Validating inputs in the handler ensures safe behavior regardless of who calls it.
5. route_ticket uses queueMap[category] ?? "tier-1-support". If the AI provides category: "uncategorized" (not in queueMap), what happens?
Correct Answer: B
The ?? operator returns the right-hand value when the left side is null or undefined. queueMap["uncategorized"] is undefined, so the fallback "tier-1-support" is used. This is intentional safe behavior — unknown categories land in the general queue rather than crashing.
6. You call escalate_ticket and the pager API times out. The handler is in a try/catch block. Which return value is correct?
Correct Answer: B
Returning isError: true signals the failure to the AI so it can inform the user or decide to retry. Bubbling the exception or re-throwing crashes the handler and leaves the AI without structured feedback. Always return a structured error response from tool handlers.
7. The classify helper returns "billing" if a ticket mentions "charge", even if it also mentions "error". A teammate wants to make this priority order configurable rather than hardcoded. Is this a good idea, and why?
Correct Answer: C
Hardcoding "billing beats technical" hides a business rule that will need to change as the product evolves. Externalizing priority order to a configuration object lets the team update classification behavior without touching the handler code or redeploying the server.
Estenda o servidor de triagem de suporte com uma ferramenta summarize_ticket que formata um ticket em um resumo estruturado para o painel de roteamento.
Passos:
summarize_ticket ao handler ListToolsRequestSchema. Entradas obrigatórias: ticket_id (string), content (string), category (string).summarize_ticket ao handler CallToolRequestSchema.summary deve conter a primeira frase de content (dividir em . ou nova linha).priority deve ser 'high' se content contém 'urgent' ou 'critical' (sem distinção de maiúsculas), caso contrário 'normal'.JSON.stringify de { ticket_id, category, summary, priority, generated_at }.Dica: Primeira frase: content.split(/[.\n]/)[0].trim(). Flag de prioridade: /urgent|critical/i.test(content). O campo generated_at é new Date().toISOString().
Solução
<CodeExample title="Branch CallToolRequestSchema" language="typescript"> if (name === "summarize_ticket") { const { ticket_id, content, category } = args as { ticket_id: string; content: string; category: string; }; const summary = content.split(/[.\n]/)[0].trim(); const priority = /urgent|critical/i.test(content) ? "high" : "normal"; return { content: [ { type: "text", text: JSON.stringify({ ticket_id, category, summary, priority, generated_at: new Date().toISOString(), }), }, ], }; } </CodeExample>