Cómo crear un servidor MCP de Elasticsearch con TypeScript
Aprende a crear un servidor MCP de Elasticsearch con TypeScript y Claude Desktop.
Cuando se trabaja con grandes bases de conocimiento en Elasticsearch, encontrar información es solo la mitad de la batalla. Los ingenieros suelen necesitar sintetizar resultados de varios documentos, generar resúmenes y rastrear las respuestas hasta sus fuentes. El protocolo de contexto de modelo (MCP) proporciona una manera estandarizada de conectar Elasticsearch con aplicaciones basadas en modelos de lenguaje grande (LLM) para lograr esto. Mientras que Elastic ofrece soluciones oficiales, como Elastic Agent Builder (que incluye un endpoint MCP entre sus características), construir un servidor MCP personalizado te brinda control total sobre la lógica de búsqueda, el formato de resultados y cómo se pasa el contenido recuperado a un LLM para síntesis, resúmenes y citas.
En este artículo, exploraremos las ventajas de construir un servidor MCP personalizado de Elasticsearch y mostraremos cómo crear uno en TypeScript que conecte Elasticsearch con aplicaciones impulsadas por LLM.
¿Por qué construir un servidor MCP personalizado de Elasticsearch?
Elastic ofrece algunas alternativas para los servidores MCP:
Servidor MCP de Elastic Agent Builder para Elasticsearch 9.2+
Servidor MCP de Elasticsearch para versiones anteriores (Python)
Si necesitas más control sobre cómo tu servidor MCP interactúa con Elasticsearch, construir tu propio servidor personalizado te da la flexibilidad de adaptarlo exactamente a tus necesidades. Por ejemplo, el endpoint MCP de Agent Builder está limitado a las consultas de lenguaje de búsqueda (ES|QL) de Elasticsearch, mientras que un servidor personalizado te permite usar el DSL de consulta completo. También obtienes control sobre cómo se formatean los resultados antes de pasarlos al LLM y puedes integrar pasos de procesamiento adicionales, como el resumen impulsado por OpenAI que implementaremos en este tutorial.
Al final de este artículo, tendrás un servidor MCP en TypeScript que busca información almacenada en un índice de Elasticsearch, la resume y proporciona citas. Usaremos Elasticsearch para la recuperación, el modelo gpt-4o-mini de OpenAI para resumir y generar citas, y Claude Desktop como cliente MCP y UI para recibir las búsquedas de los usuarios y dar respuestas. El resultado final es un asistente de conocimiento interno que ayuda a los ingenieros a descubrir y sintetizar las mejores prácticas en los documentos técnicos de su organización.

Requisitos previos:
Node.js 20 +
Elasticsearch
Clave de API de OpenAI
Claude Desktop
¿Qué es MCP?
MCP es un estándar abierto, creado por Anthropic, que ofrece conexiones seguras y bidireccionales entre los modelos de lenguaje grande (LLM) y sistemas externos, como Elasticsearch. Puedes leer más sobre el estado actual del MCP en este artículo.
El panorama de MCP evoluciona cada día, con servidores disponibles para una amplia gama de casos de uso. Además de eso, desarrollar tu propio servidor MCP personalizado es fácil, como te mostraremos en este artículo.
Clientes del MCP
Hay una larga lista de clientes del MCP disponibles, cada uno con sus propias características y limitaciones. Por su sencillez y popularidad, usaremos Claude Desktop como nuestro cliente MCP. Servirá como interfaz de chat en la que los usuarios podrán hacer preguntas en lenguaje natural e invocar automáticamente las herramientas expuestas por nuestro servidor MCP para buscar documentos y generar resúmenes.

Cómo crear un servidor MCP de Elasticsearch
Con el SDK de TypeScript, podemos crear fácilmente un servidor que entiende cómo hacer búsquedas en nuestros datos de Elasticsearch con base en la entrada de búsqueda del usuario.
Estos son los pasos en este artículo para integrar el servidor MCP de Elasticsearch con el cliente Claude Desktop:
Configurar el servidor MCP para Elasticsearch
Para comenzar, inicialicemos una aplicación de nodo:
npm init -yEsto creará un archivo package.json y, con él, podremos empezar a instalar las dependencias necesarias para esta aplicación.
npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript@elastic/elasticsearch nos dará acceso a la biblioteca de Elasticsearch para Node.js.
@modelcontextprotocol/sdk proporciona las herramientas básicas para crear y administrar un servidor MCP, registrar herramientas y manejar la comunicación con los clientes de MCP.
openAI permite la interacción con modelos OpenAI para generar resúmenes o respuestas en lenguaje natural.
zod ayuda a definir y validar esquemas estructurados para los datos de entrada y salida en cada herramienta.
ts-node, @types/node y typescript se usarán durante el desarrollo para escribir el código y compilar los scripts.
Configura los sets de datos
Para proporcionar los datos que Claude Desktop puede consultar con nuestro servidor de MCP, utilizaremos un conjunto de datos de base de conocimiento interna simulado. Así es como se verá un documento de este sets de datos:
{
"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 cargar los datos, preparamos un script que cree un índice en Elasticsearch y cargue el set de datos en él. Puedes encontrarlo aquí.
Servidor MCP
Crea un archivo llamado index.ts y agrega el siguiente código para importar las dependencias y gestionar las variables de entorno:
// 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";Además, preparemos a los clientes para que gestionen las llamadas a Elasticsearch y OpenAI:
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
const _client = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: {
apiKey: ELASTICSEARCH_API_KEY,
},
});Para hacer nuestra implementación más robusta y asegurar entradas y salidas estructuradas, definiremos esquemas usando zod. Esto nos permite validar los datos en tiempo de ejecución, detectar errores a tiempo y facilitar el procesamiento de las respuestas de la herramienta mediante código:
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>;Descubre más sobre las salidas estructuradas aquí.
Ahora vamos a inicializar el 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",
});Definición de las herramientas MCP
Ahora que ya tenemos todo configurado, podemos empezar a desarrollar las herramientas que ofrecerá nuestro servidor MCP. Este servidor ofrece dos herramientas:
search_docs: Búsquedas de documentos en Elasticsearch mediante la búsqueda de texto.summarize_and_cite: Resume y sintetiza información de documentos previamente recuperados para responder a una pregunta del usuario. Esta herramienta también agrega citas que hacen referencia a los documentos originales.
Juntas, estas herramientas forman un flujo de trabajo simple de “recuperación y resumen”, donde una herramienta busca documentos relevantes y la otra usa esos documentos para generar una respuesta resumida y citada.
Formato de respuesta de herramienta
Cada herramienta puede aceptar parámetros de entrada arbitrarios, pero debe responder con la siguiente estructura:
Contenido: esta es la respuesta de la herramienta en un formato no estructurado. Este campo se suele usar para mostrar texto, imágenes, audio, enlaces o contenido incrustado. Para esta aplicación, se utilizará para devolver texto formateado con la información generada por las herramientas.
structuredContent: este es un retorno opcional que se usa para proporcionar los resultados de cada herramienta en un formato estructurado. Esto es útil para fines programáticos. Aunque no se usa en este servidor de MCP, puede ser útil si quieres desarrollar otras herramientas o procesar los resultados mediante programación.
Con esa estructura en mente, comencemos con cada herramienta en detalle.
Herramienta Search_docs
Esta herramienta realiza una búsqueda de texto completo en el índice de Elasticsearch para recuperar los documentos más relevantes según la consulta del usuario. Destaca los resultados clave y ofrece una visión general rápida con puntuaciones de relevancia.
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 que tenga una tolerancia tipográfica variable basada en la longitud del token que se está analizando. También establecemos title^2 para aumentar la puntuación de los documentos donde se produce la coincidencia en el campo de título.
herramienta de resumen y cita: summarize_and_cite
Esta herramienta genera un resumen basado en los documentos recuperados en la búsqueda anterior. Usa el modelo gpt-4o-mini de OpenAI para sintetizar la información más relevante y responder a la pregunta del usuario para obtener respuestas derivadas directamente de los resultados de búsqueda. Además del resumen, también devuelve metadatos de citas para los documentos fuente utilizados.
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,
};
}
}
);Finalmente, necesitamos iniciar el servidor a través de stdio. Esto significa que el cliente MCP se comunicará con nuestro servidor leyendo y escribiendo en sus flujos estándar de entrada y salida. stdio es la opción de transporte más sencilla y funciona bien para servidores MCP locales lanzados como subprocesos por el cliente. Agrega el siguiente código al final del archivo:
const transport = new StdioServerTransport();
server.connect(transport);Ahora compila el proyecto usando el siguiente comando:
npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInteropEsto creará una carpeta dist y, dentro de ella, un archivo index.js.
Carga el servidor MCP en Claude Desktop
Sigue esta guía para configurar el servidor MCP con Claude Desktop. En el archivo de configuración de Claude, tienes que establecer los siguientes 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"
}
}
}
}El valor args debe apuntar al archivo compilado en la carpeta dist. También es necesario configurar las variables de entorno en el archivo de configuración con los mismos nombres exactos definidos en el código.
Pruébalo
Antes de ejecutar cada herramienta, haz clic en Búsqueda y herramientas para asegurarte de que las herramientas estén habilitadas. Aquí también puedes habilitar o deshabilitar cada una de ellas:

Finalmente, probemos el servidor MCP desde el chat de Claude Desktop y comencemos a hacer preguntas:

Para la consulta"Buscar documentos sobre métodos de autenticación y control de acceso basado en roles", se ejecuta la herramienta search_docs y arroja los siguientes 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.La respuesta es: “¡Genial! Encontré 5 documentos relevantes sobre métodos de autenticación y control de acceso basado en roles. Esto es lo que se encontró:”
La llamada a la herramienta devuelve los documentos de origen como parte de su carga útil de respuesta, que luego se utilizan para generar citas.

También es posible encadenar varias herramientas en una sola interacción. En este caso, Claude Desktop analiza la pregunta del usuario y determina que primero debe llamar a search_docs para recuperar documentos relevantes y luego pasar esos resultados a summarize_and_cite para generar la respuesta final, todo eso sin requerir indicaciones separadas del usuario:

En este caso, para la búsqueda: "¿Cuáles son las principales recomendaciones para mejorar la autenticación y el control de acceso en todos nuestros sistemas? Incluye referencias.", obtuvimos los siguientes 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.Al igual que en el paso anterior, podemos ver la respuesta de cada herramienta a esta pregunta:

Nota: si aparece un submenú que pregunta si apruebas el uso de cada herramienta, selecciona Permitir siempre o Permitir una vez.

Conclusión
Los servidores MCP representan un paso significativo hacia la estandarización de las herramientas LLM para aplicaciones tanto locales como remotas. Aunque la compatibilidad total todavía está en proceso, nos estamos moviendo rápido en esa dirección.
En este artículo, aprendimos cómo construir un servidor MCP personalizado en TypeScript que conecta Elasticsearch con aplicaciones impulsadas por modelos LLM. Nuestro servidor expone dos herramientas: search_docs para recuperar documentos relevantes con Query DSL y summarize_and_cite para generar resúmenes con citas a través de modelos de OpenAI y Claude Desktop como client UI.
El futuro de la compatibilidad entre los distintos proveedores de clientes y servidores parece prometedor. Los próximos pasos consisten en agregar más funcionalidades y flexibilidad a tu agente. Hay un artículo práctico sobre cómo puedes agregar parámetros a tus consultas a través de plantillas de búsqueda para ganar precisión y flexibilidad.




