Expose read-only data to the AI using the TypeScript MCP SDK — knowledge-base articles and ticket history for the support-triage server
Quando construí o sistema de automação de suporte, a IA precisava de acesso constante a duas fontes de dados: artigos da base de conhecimento que explicavam como classificar tickets por prioridade e tipo, e um histórico de tickets recentes que ela podia usar como contexto. Meu primeiro instinto foi transformar ambas em chamadas de ferramenta — get_kb_article(id) e get_recent_tickets().
Mas chamadas de ferramenta são para ações que alteram estado. A IA nunca deveria conseguir reescrever um artigo da base de conhecimento ou modificar o histórico de tickets — esses são registros autoritativos. Dar a dados somente leitura a mesma interface de ações que alteram estado tornava o sistema mais difícil de raciocinar e mais difícil de auditar.
Recursos resolvem isso no nível de design. Pense em um recurso como uma prateleira somente leitura. A IA pode pegar qualquer artigo da prateleira e usar o que aprendeu — mas não pode colocar coisas de volta, mudar rótulos ou alterar o que está na prateleira. Ela pode navegar pela prateleira sem supervisão, com segurança.
A regra central: se a IA nunca deve conseguir modificar algo, exponha-o como recurso. Se precisar causar uma mudança de estado — rotear um ticket, escalonar um problema — isso pertence como ferramenta. Este módulo constrói o lado de leitura; o próximo módulo constrói o lado de ação.
Neste módulo implementamos dois recursos para o servidor de triagem de suporte:
knowledge-base://articles/{id} — artigos da base de conhecimento que a IA lê para entender regras de classificaçãotickets://recent — um feed ao vivo dos últimos 20 tickets que é atualizado conforme novos tickets chegamAntes que a IA possa ler um recurso, ela precisa descobrir quais recursos existem. O handler ListResourcesRequestSchema responde a essa pergunta. Observe a importação: estamos usando schemas tipados do SDK, não nomes de handler baseados em string como 'resources/list'.
A declaração capabilities: { resources: {} } diz a qualquer cliente conectado que este servidor fornece recursos. Sem ela, clientes conformes não tentarão a descoberta de recursos. Agora registre o handler de lista:
A uri é o endereço na prateleira — um endereço único que a IA usa para solicitar o conteúdo real. O mimeType diz à IA que tipo de conteúdo esperar antes de lê-lo. Retornamos um array plano; paginação é possível mas desnecessária para dois recursos.
Pause aqui antes de adicionar o handler de leitura. Você definiu quais recursos existem — agora pense em como a IA os usará.
tickets://recent retorna dados diferentes a cada leitura conforme novos tickets chegam. Isso o torna uma ferramenta?
A resposta é não. Os dados são dinâmicos, mas o acesso da IA ainda é somente leitura. A IA pode observar novos tickets chegando, mas não consegue criar, excluir ou modificar um ticket através deste recurso. Conteúdo dinâmico, acesso somente leitura — ainda é uma prateleira, mesmo que alguém continue adicionando novos livros a ela.
Confusão comum: "Dinâmico" descreve como o conteúdo muda, não quem pode mudá-lo. Um recurso pode retornar dados frescos a cada chamada e ainda ser somente leitura. O teste é sempre: a IA consegue modificar os dados subjacentes através desta interface? Se não, é um recurso.
O handler ReadResourceRequestSchema é acionado quando a IA solicita um recurso por URI. Aqui está a implementação para artigos da base de conhecimento — observe o mesmo padrão de schema tipado:
Duas coisas a notar: uri.startsWith() roteia requisições para o ramo de handler correto pelo prefixo URI, e throw new Error(...) é como você retorna um erro de recurso-não-encontrado — o SDK do MCP converte erros lançados na resposta de erro correta para o cliente. O throw final atua como captura abrangente para qualquer URI que o handler não reconheça.
Pense no Map como a prateleira — cada ID de artigo é seu endereço, e lançar um erro quando uma chave está faltando é equivalente a dizer "nada naquele endereço está nesta prateleira."
O armazenamento de dados aqui é um simples Map em memória — perfeitamente adequado para um pequeno conjunto de artigos estáticos. Em produção você o substituiria por uma consulta ao seu sistema de base de conhecimento, mas a estrutura do handler permanece idêntica. A camada MCP não se importa de onde os dados vêm; ela apenas se importa que você retorne o formato correto.
O histórico de tickets é dinâmico — fetchRecentTickets acessa um backend ao vivo. Estendemos o mesmo handler ReadResourceRequestSchema para cobrir ambos os recursos:
fetchRecentTickets(20) chama qualquer backend que mantém os dados de tickets — um banco de dados, uma API interna. O trabalho do handler é traduzir esses dados para o formato de resposta MCP. A IA vê um snapshot JSON limpo e nunca toca o armazenamento de tickets diretamente.
O que você construiu: A IA agora pode ler regras de classificação da base de conhecimento e escanear o histórico recente de tickets — as duas fontes de dados somente leitura que ela precisa para triar com precisão. Nenhum recurso permite que a IA altere um ticket ou edite a base de conhecimento. Esse poder pertence às ferramentas que adicionamos no próximo módulo.
Agora que você tem ambos os recursos funcionando, qual é sua regra de decisão para capacidades futuras? Se você precisasse adicionar "pesquisa de tickets" ao servidor, seria um recurso ou uma ferramenta?
Pesquisa é complicada: ela lê dados, o que soa como recurso. Mas uma URI como tickets://search?q=billing não se encaixa no modelo de recurso MCP — recursos têm URIs estáveis e endereçáveis, não parâmetros de consulta. Pesquisa também tipicamente envolve efeitos colaterais no backend (logging, atualizações de ranking). A resposta correta é uma ferramenta. A regra de decisão: se uma capacidade tem uma URI estável e retorna dados que a IA não deve modificar, é um recurso. Se requer parâmetros variáveis por chamada ou aciona qualquer efeito colateral no backend, é uma ferramenta.
Este módulo cobre os dois recursos que o servidor de triagem de suporte precisa. Servidores de produção reais às vezes adicionam assinaturas de recursos — o servidor envia uma notificação ao cliente quando o conteúdo de um recurso muda, em vez de esperar que o cliente faça polling. O SDK do MCP suporta este padrão via server.sendResourceUpdated(). Assinaturas valem considerar quando a IA precisa reagir a mudanças de dados em tempo quase real — por exemplo, re-triar imediatamente quando um ticket de alta prioridade chega e o feed tickets://recent é atualizado.
Para a maioria das cargas de trabalho de triagem de suporte, polling sob demanda é mais simples e funciona bem. A IA solicita tickets://recent quando precisa de contexto; não há necessidade de enviar atualizações a menos que a latência se torne um problema. Adicione assinaturas apenas quando você tiver medido que o polling é muito lento — push prematuro introduz complexidade real em torno de estado de assinatura, tratamento de reconexão e contrapressão que o polling evita completamente.
Application-level questions on MCP resources and the support-triage server
1. Your ReadResourceRequestSchema handler receives uri = 'knowledge-base://articles/99'. Article 99 does not exist in the Map. What is the correct handler behavior?
2. Which import is required to register a resource listing handler in the TypeScript MCP SDK?
3. tickets://recent returns different data on every read because the ticket database changes. This makes it a tool, not a resource.
True or False question
Correct Answer: FALSE
False. Dynamic content does not change the resource/tool classification. Resources are read-only regardless of how often their content changes. The AI cannot create, modify, or delete tickets through tickets://recent — it can only read the current snapshot.
4. You register ReadResourceRequestSchema but forget to register ListResourcesRequestSchema. What can the AI still do with resources?
5. What does capabilities: { resources: {} } in the Server constructor do?
6. Which field in a resource content object carries JSON data returned to the AI?
7. A resource URI like tickets://search?q=billing is a good way to add ticket search to the support-triage server.
True or False question
Correct Answer: FALSE
False. MCP resource URIs represent stable, addressable data — not parameterized queries. A search-by-query capability has a URI that changes with every search term and typically involves backend side effects (logging, ranking). Search belongs as a tool, not a resource.
8. You want to add configs://thresholds (a static JSON object of SLA thresholds) to the server. Which two changes are required?
O servidor de triagem de suporte roteia tickets para agentes disponíveis. Antes de rotear, a IA precisa saber quais agentes têm capacidade no momento. Adicione um terceiro recurso — agents://active — que lista agentes disponíveis para novos tickets.
Passos:
agents://active ao array resources no seu handler ListResourcesRequestSchemaagents://active ao seu handler ReadResourceRequestSchemaapplication/json com um array de objetos de agenteid (string), name (string) e openTickets (number)fetchActiveAgents()Dica: O mimeType do novo recurso é "application/json". O formato de retorno é {contents: [{uri, mimeType: "application/json", text: JSON.stringify(data)}]}.
// Adicione ao array de recursos no handler ListResourcesRequestSchema: // { // uri: "agents://active", // name: "Active Support Agents", // description: "Agents currently available for ticket assignment", // mimeType: "application/json", // } </CodeExample>
Solução
<CodeExample title="Solução: branch ReadResourceRequestSchema" language="typescript"> if (uri === "agents://active") { const agents = await fetchActiveAgents(); return { contents: [ { uri: "agents://active", mimeType: "application/json", text: JSON.stringify(agents), }, ], }; } </CodeExample>