Blog

Criando um servidor MCP do Elasticsearch com TypeScript

Saiba como criar servidor MCP do Elasticsearch com TypeScript e Claude Desktop.

Ao trabalhar com grandes bases de conhecimento no Elasticsearch, encontrar informações é apenas metade da batalha. Engenheiros precisam sintetizar resultados de múltiplos documentos, gerar resumos e rastrear respostas até as fontes. Para isso, o Protocolo de Contexto do Modelo (MCP) oferece uma maneira padronizada de conectar o Elasticsearch a aplicativos baseados em grandes modelos de linguagem (LLM). Embora a Elastic ofereça soluções oficiais, como o Elastic Agent Builder (que inclui um endpoint MCP entre os recursos), a criação de um servidor MCP personalizado oferece controle total sobre a lógica de busca, a formatação dos resultados e como o conteúdo recuperado é passado para um LLM para síntese, resumos e citações.

Neste artigo, exploraremos os benefícios de criar um servidor MCP do Elasticsearch personalizado e mostraremos como criar um servidor em TypeScript que conecte o Elasticsearch a aplicativos com LLM.

Por que criar um servidor MCP do Elasticsearch personalizado?

A Elastic oferece algumas alternativas para servidores MCP:

Se você precisar de mais controle sobre como o servidor MCP interage com o Elasticsearch, a criação do seu próprio servidor personalizado oferece a flexibilidade de adaptá-lo exatamente às suas necessidades. Por exemplo, o endpoint MCP do Agent Builder é limitado a consultas em Elasticsearch Query Language (ES|QL), enquanto um servidor personalizado permite usar o Query DSL completo. Você também tem controle sobre como os resultados são formatados antes de serem passados para o LLM e pode integrar etapas adicionais de processamento, como o resumo com tecnologia OpenAI que implementaremos neste tutorial.

Ao final deste artigo, você terá um servidor MCP no TypeScript que busca informações armazenadas em um índice do Elasticsearch, resume essas informações e fornece citações. Usaremos o Elasticsearch para recuperação, o modelo gpt-4o-mini da OpenAI para resumir e gerar citações, e o Claude Desktop como cliente MCP e UI para receber consultas dos usuários e dar respostas. O resultado é um assistente de conhecimento interno que ajuda os engenheiros a entender e sintetizar as práticas recomendadas nos documentos técnicos de sua organização.

Criando um servidor MCP Elastic com TypeScript e Claude Desktop.

Pré-requisitos:

  • Node.js 20 +

  • Elasticsearch

  • Chave de API da OpenAI

  • Claude Desktop

O que é MCP?

O MCP é um padrão aberto, criado pela Anthropic, que oferece conexões seguras e bidirecionais entre LLMs e sistemas externos, como o Elasticsearch. Você pode ler mais sobre a situação atual do MCP neste artigo.

O cenário de MCP está em constante evolução, com servidores disponíveis para uma ampla gama de casos de uso. Além disso, é fácil criar seu próprio servidor MCP personalizado, como mostraremos neste artigo.

Clientes do MCP

Há uma longa lista de clientes MCP disponíveis, cada um com as próprias características e limitações. Por simplicidade e popularidade, usaremos o Claude Desktop como nosso cliente MCP. Ele servirá como interface de chat na qual os usuários poderão fazer perguntas em linguagem natural e que invocará automaticamente as ferramentas expostas pelo nosso servidor MCP para buscar documentos e gerar resumos.

Página do Claude 4.5 Sonnet, com a observação, "hora do café e do Claude? Como posso ajudar você hoje?"

Criando um servidor MCP do Elasticsearch

Usando o TypeScript SDK, podemos criar um servidor que entende como consultar nossos dados do Elasticsearch com base em uma entrada de consulta do usuário.

Aqui estão os passos deste artigo para integrar o servidor MCP do Elasticsearch com o cliente Claude Desktop:

  1. Configure o servidor MCP para o Elasticsearch.

  2. Carregue o servidor MCP no Claude Desktop.

  3. Faça o teste.

Configure o servidor MCP para o Elasticsearch

Para começar, vamos iniciar uma aplicação de nó:

npm init -y

Isso criará um arquivo package.json e, com ele, poderemos começar a instalar as dependências necessárias para essa aplicação.

npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript
  • @elastic/elasticsearch nos dará acesso à biblioteca Elasticsearch Node.js.

  • @modelcontextprotocol/sdk fornece as ferramentas de núcleo para criar e gerenciar um servidor MCP, registrar ferramentas e lidar com a comunicação com clientes MCP.

  • openai permite a interação com modelos OpenAI para gerar resumos ou respostas em linguagem natural.

  • zod ajuda a definir e validar esquemas estruturados para dados de entrada e saída em cada ferramenta.

ts-node, @types/node, e typescript serão usados durante o desenvolvimento para digitar o código e compilar os scripts.

Configurar o conjunto de dados

Para fornecer os dados que o Claude Desktop pode consultar usando nosso servidor MCP, usaremos um conjunto de dados fictício de base de conhecimento interna. Veja como será um documento desse conjunto de dados:

{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}

Para ingerir os dados, preparamos um script que cria um índice no Elasticsearch e carrega o conjunto de dados nele. Você pode encontrá-lo aqui.

Servidor MCP

Crie um arquivo chamado index.ts e adicione o seguinte código para importar as dependências e lidar com as variáveis de ambiente:

// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";

Além disso, vamos inicializar os clientes para lidar com as chamadas do Elasticsearch e do OpenAI:

const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});

Para tornar nossa implementação mais robusta e garantir entrada e saída estruturadas, definiremos esquemas usando zod. Isso nos permite validar dados em tempo de execução, detectar erros com antecedência e facilitar o processamento de forma programática das respostas da ferramenta:

const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

const SearchResultSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
  score: z.number(),
});

type Document = z.infer<typeof DocumentSchema>;
type SearchResult = z.infer<typeof SearchResultSchema>;

Saiba mais sobre saídas estruturadas aqui.

Agora vamos inicializar o servidor MCP:

const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});

Definição das ferramentas MCP

Com tudo configurado, podemos começar a escrever as ferramentas que serão expostas pelo nosso servidor MCP. Esse servidor expõe duas ferramentas:

  • search_docs: Busca por documentos no Elasticsearch usando busca de texto completo.

  • summarize_and_cite: Resume e sintetiza informações de documentos previamente recuperados para responder a uma pergunta do usuário. Essa ferramenta também adiciona citações que referenciam os documentos fonte.

Juntas, essas ferramentas formam um fluxo de trabalho simples de "recuperar e resumir", em que uma ferramenta busca documentos relevantes e a outra usa esses documentos para gerar uma resposta resumida e citada.

Formato de resposta da ferramenta

Cada ferramenta pode aceitar parâmetros de entrada arbitrários, mas deve responder com a seguinte estrutura:

  • Conteúdo: esta é a resposta da ferramenta em um formato não estruturado. Este campo geralmente é usado para retornar texto, imagens, áudio, links ou embeddings. Para esta aplicação, ele será usado para retornar texto formatado com as informações geradas pelas ferramentas.

  • structuredContent: Esse é um retorno opcional usado para fornecer os resultados de cada ferramenta em um formato estruturado. É útil para fins programáticos. Embora não seja usado neste servidor MCP, pode ser útil caso você queira desenvolver outras ferramentas ou processar os resultados programaticamente.

Com essa estrutura em mente, vamos nos aprofundar em cada ferramenta em detalhes.

Ferramenta de Busca de Documentos

Esta ferramenta realiza uma busca de texto completo no índice do Elasticsearch para recuperar os documentos mais relevantes com base na consulta do usuário. Ele destaca correspondências-chave e oferece uma visão geral rápida com pontuações de relevância.

server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) => {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

    try {
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"],
                  fuzziness: "AUTO",
                },
              },
            ],
            should: [
              {
                match_phrase: {
                  title: {
                    query: query,
                    boost: 2,
                  },
                },
              },
            ],
          },
        },
        highlight: {
          fields: {
            title: {},
            content: {},
          },
        },
      });

      const results: SearchResult[] = response.hits.hits.map((hit: any) => {
        const source = hit._source as Document;

        return {
          id: source.id,
          title: source.title,
          content: source.content,
          tags: source.tags,
          score: hit._score ?? 0,
        };
      });

      const contentText = results
        .map(
          (r, i) =>
            `[${i + 1}] ${r.title} (score: ${r.score.toFixed(
              2,
            )})\n${r.content.substring(0, 200)}...`,
        )
        .join("\n\n");

      const totalHits =
        typeof response.hits.total === "number"
          ? response.hits.total
          : (response.hits.total?.value ?? 0);

      return {
        content: [
          {
            type: "text",
            text: `Found ${results.length} relevant documents:\n\n${contentText}`,
          },
        ],
        structuredContent: {
          results: results,
          total: totalHits,
        },
      };
    } catch (error: any) {
      console.log("Error during search:", error);

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);

Configuramos fuzziness: “AUTO” para ter uma tolerância variável de erros de digitação com base no comprimento do token que está sendo analisado. Também definimos title^2 para aumentar a pontuação dos documentos onde a correspondência ocorre no campo de título.

Ferramenta summarize_and_cite

Esta ferramenta gera um resumo baseado em documentos recuperados na busca anterior. Usa o modelo gpt-4o-mini da OpenAI para sintetizar as informações mais relevantes e responder à pergunta do usuário, fornecendo respostas derivadas diretamente dos resultados da busca. Além do resumo, também retorna metadados de citação para os documentos de origem usados.

server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) => {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

    try {
      const used = results.slice(0, max_docs);

      const context = used
        .map(
          (r: SearchResult, i: number) =>
            `[Document ${i + 1}: ${r.title}]\\n${r.content}`
        )
        .join("\n\n---\n\n");

      // Generate summary with OpenAI
      const completion = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that answers questions based on provided documents. Synthesize information from the documents to answer the user's question accurately and concisely. If the documents don't contain relevant information, say so.",
          },
          {
            role: "user",
            content: `Question: ${question}\\n\\nRelevant Documents:\\n${context}`,
          },
        ],
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3,
      });

      const summaryText =
        completion.choices[0]?.message?.content ?? "No summary generated.";

      const citations = used.map((r: SearchResult) => ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =>
            `[${i + 1}] ID: ${c.id}, Title: "${c.title}", Tags: ${c.tags.join(
              ", ",
            )}, Score: ${c.relevance_score.toFixed(2)}`,
        )
        .join("\n");

      const combinedText = `Summary:\\n\\n${summaryText}\\n\\nSources used (${citations.length}):\\n\\n${citationText}`;

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);

Por fim, precisamos iniciar o servidor usando stdio. Isso significa que o cliente MCP se comunicará com nosso servidor lendo e escrevendo nos fluxos padrão de entrada e saída. Stdio é a opção de transporte mais simples e funciona bem para servidores MCP locais lançados como subprocessos pelo cliente. Adicione o seguinte código ao final do arquivo:

const transport = new StdioServerTransport();
server.connect(transport);

Agora compile o projeto usando o seguinte comando:

npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop

Isso criará uma pasta dist e, dentro dela, um arquivo index.js.

Carregue o servidor MCP no Claude Desktop

Siga este guia para configurar o servidor MCP com o Claude Desktop. No arquivo de configuração Claude, precisamos definir os seguintes valores:

{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}

O valor args deve apontar para o arquivo compilado na pasta dist. Você também precisa definir as variáveis de ambiente no arquivo de configuração com exatamente os mesmos nomes definidos no código.

Faça o teste

Antes de executar cada ferramenta, clique em Busca e Ferramentas para garantir que as ferramentas estejam ativadas. Aqui você também pode ativar ou desativar cada uma delas:

Página do Claude 4.5 Sonnet, com a observação, "Boa tarde, Jeff. Como posso ajudar você hoje?"

Por fim, vamos testar o servidor MCP no chat do Claude Desktop e começar a fazer perguntas:

Solicitação de pesquisa do usuário no chat do Claude Desktop para documentos sobre métodos de autenticação e controle de acesso por função (RBAC), junto com as respostas do Claude.

Para a pergunta “Buscar documentos sobre métodos de autenticação e controle de acesso por função”, a ferramenta search_docs é executada e retorna os seguintes resultados:

Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.

A resposta é: "Ótimo! Encontrei 5 documentos relevantes sobre métodos de autenticação e controle de acesso por função. Eis o que foi encontrado:"

A chamada de ferramenta retorna os documentos fonte como parte da carga útil de resposta, que são posteriormente usados para gerar citações.

Página do Claude 4.5 Sonnet, com respostas em rolagem que incluem os cinco documentos relevantes sobre métodos de autenticação e controle de acesso por função.

Também é possível encadear várias ferramentas em uma única interação. Neste caso, o Claude Desktop analisa a pergunta do usuário e determina que precisa primeiro chamar search_docs para recuperar documentos relevantes e depois passar esses resultados para summarize_and_cite para gerar a resposta final, tudo isso sem exigir prompts separados do usuário:

Chat no Claude Desktop, com a observação "Jeff está de volta", além de uma nova pergunta do usuário: "Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas?" Inclua referências."

Neste caso, para a consulta “Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas? Inclua referências.”, obtivemos os seguintes resultados:

Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

1. Implement Role-Based Access Control (RBAC)
Ensure users have only the permissions necessary for their job functions. This principle of least privilege minimizes security risks by limiting access to what's truly needed. [1]

2. Regular Access Audits
Conduct regular audits of user roles and promptly revoke access for inactive accounts. This helps prevent unauthorized access from dormant or compromised accounts. [1]

3. Just-in-Time (JIT) Access
Implement just-in-time access for sensitive operations, where permissions are granted only when needed and for a limited duration. This reduces the attack surface by minimizing standing privileges. [1]

4. OAuth 2.0 for Secure Authentication
Use OAuth 2.0 to enable secure delegated access without sharing user credentials. This protocol allows third-party applications to access resources on behalf of users without exposing passwords. [2]

5. Token Security and Management
Configure your identity provider to issue tokens with limited scope and lifetime

Store refresh tokens securely
Validate access tokens consistently to maintain security [2]
References

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.

Como na etapa anterior, podemos ver a resposta de cada ferramenta para esta pergunta:

Página de chat do Claude Desktop, com texto rolando que inclui a resposta de cada ferramenta para a pergunta: "Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas? Inclua referências."

Nota: Se aparecer um submenu perguntando se você aprova o uso de cada ferramenta, selecione Permitir sempre ou Permitir uma vez.

Opções "Sempre permitir" e "Permitir uma vez" do Claude Desktop para o usuário escolher.

Conclusão

Os servidores MCP representam um passo significativo rumo à padronização das ferramentas LLM para aplicações locais e remotas. Embora a compatibilidade total ainda esteja em andamento, estamos avançando nessa direção.

Neste artigo, aprendemos como criar um servidor MCP personalizado em TypeScript que conecta o Elasticsearch a aplicações baseadas em LLM. Nosso servidor expõe duas ferramentas: search_docs para recuperar documentos relevantes usando Query DSL; e summarize_and_cite para gerar resumos com citações via modelos OpenAI e Claude Desktop como UI.

O futuro da compatibilidade entre diferentes provedores de clientes e servidores parece promissor. As próximas etapas incluem adicionar mais funcionalidades e flexibilidade ao seu agente. Existe um artigo prático sobre como parametrizar suas consultas usando modelos de pesquisa para ter precisão e flexibilidade.

Conteúdo relacionado

Construtor de agentes além da caixa de bate-papo: apresentando a infraestrutura ampliada

Alexander Wert

Gerenciando a memória agentiva com o Elasticsearch

Someshwaran Mohankumar

Usando a API de Inferência Elasticsearch junto com modelos de Hugging Face

Jeffrey Rengifo

Criando um agente de IA para RH com Elastic Agent Builder e GPT-OSS

Tomás Murúa

A ferramenta shell não é uma solução milagrosa para engenharia de contexto

Leonie Monigatti

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)