Blog

Crear un conector de ChatGPT con Elasticsearch para buscar incidencias de GitHub.

Aprende cómo crear un conector personalizado de ChatGPT y desplegar un servidor MCP de Elasticsearch que utiliza una búsqueda híbrida para buscar incidencias internas de GitHub.

Recientemente, OpenAI anunció la característica de conectores personalizados para ChatGPT en los planes Pro/Business/Enterprise y Edu. Además de los conectores listos para usar que permiten acceder a datos en Gmail, GitHub, Dropbox, etc. Es posible crear conectores personalizados utilizando servidores MCP.

Los conectores personalizados te permiten combinar tus conectores de ChatGPT existentes con otras fuentes de datos como Elasticsearch para obtener respuestas integrales.

En este artículo, crearemos un servidor MCP que conecta ChatGPT a un índice de Elasticsearch que contiene información sobre incidencias internas de GitHub y solicitudes de extracción. Esto permite responder a búsquedas en lenguaje natural mediante los datos de Elasticsearch.

Desplegaremos el servidor MCP utilizando FastMCP en Google Colab con ngrok para obtener una URL pública a la que ChatGPT pueda conectarse, lo que eliminará la necesidad de una infraestructura compleja.

Para una visión general del MCP y su ecosistema, consulta El estado actual de MCP.

Prerrequisitos

Antes de comenzar, necesitarás:

  • Clúster de Elasticsearch (8.X o superior).

  • Clave API de Elasticsearch con acceso de lectura a tu índice.

  • Cuenta de Google (para Google Colab)

  • Cuenta de Ngrok (el nivel gratuito funciona)

  • Cuenta de ChatGPT con plan Pro/Enterprise/Business o Edu.

Comprensión de los requisitos del conector MCP de ChatGPT.

Los conectores MCP de ChatGPT requieren la implementación de dos herramientas: search y fetch. Para más detalles, consulta OpenAI Docs.

Herramienta de búsqueda

Devuelve una lista de resultados relevantes de tu índice de Elasticsearch según la búsqueda del usuario.

Lo que recibe:

  • Un solo texto con la búsqueda de lenguaje natural del usuario.

  • Ejemplo: “Encuentra incidencias relacionadas con la migración de Elasticsearch”.

Lo que devuelve: 

  • Un objeto con una clave result que contiene un arreglo de objetos de resultado. Cada resultado incluye:

    • id - Identificador único de documentos.

    • title - Título de la incidencia o PR.

    • url - Enlace a la incidencia o PR.

En nuestra implementación:

return {
    "results": [
        {
            "id": "PR-612",
            "title": "Fix memory leak in WebSocket notification service",
            "url": "https://internal-git.techcorp.com/pulls/612"
        },
        # ... more results
    ]
}

Herramienta de extracción

Recupera el contenido completo de un documento específico.

Lo que recibe:

  • Una sola cadena de texto con el ID del documento de Elasticsearch del resultado de la búsqueda.

  • Ejemplo: “Consígueme los detalles de PR-578”.

Lo que devuelve:

  • Un objeto de documento completo con:

    • id - Identificador único de documentos.

    • title - Título de la incidencia o PR.

    • text - Descripción completa del problema/PR y sus detalles

    • url - Enlace a la incidencia o PR.

    • type - Tipo de documento (incidencia, pull_request).

    • status - Estado actual (abierto, en progreso, resuelto)

    • priority - Nivel de prioridad (bajo, medio, alto, crítico)

    • assignee - Persona asignada al problema/PR

    • created_date - Fecha de creación.

    • resolved_date - Cuando se resolvió (si procede)

    • labels - Etiquetas asociadas al documento

    • related_pr - ID de solicitud de extracción relacionado

return {
    "id": "PR-578",
    "title": "Security hotfix: Patch SQL injection vulnerabilities",
    "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
    "url": "https://internal-git.techcorp.com/pulls/578",
    "type": "pull_request",
    "status": "closed",
    "priority": "critical",
    "assignee": "sarah_dev",
    "created_date": "2025-09-19",
    "resolved_date": "2025-09-19",
    "labels": "security, hotfix, sql",
    "related_pr": null
}

Nota: Este ejemplo usa una estructura plana donde todos los campos están en el nivel raíz. Los requisitos de OpenAI son flexibles y también admiten objetos de metadatos anidados.

Sets de datos de incidencias y PR de GitHub

Para este tutorial, vamos a usar un set de datos interno de GitHub que contenga incidencias y solicitudes de extracción. Esto representa un escenario en el que deseas buscar datos internos privados a través de ChatGPT.

Los sets de datos se pueden encontrar aquí. Y actualizaremos el índice de los datos mediante la API de bulk.

Este sets de datos incluye:

  • Problemas con descripciones, estado, prioridad y responsables.

  • Solicitudes de extracción con cambios de código, revisiones e información de despliegue.

  • Relaciones entre incidencias y PR (p. ej., PR-578 soluciona ISSUE-1889).

  • Etiquetas, fechas y otros metadatos

Mappings de índices

El índice utiliza los siguientes mappings para brindar soporte a la búsqueda híbrida con ELSER. El campo text_semantic se utiliza para la búsqueda semántica, mientras que los demás campos permiten la búsqueda por palabras clave.

{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "text": {
        "type": "text"
      },
      "text_semantic": {
        "type": "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      },
      "url": {
        "type": "keyword"
      },
      "type": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "priority": {
        "type": "keyword"
      },
      "assignee": {
        "type": "keyword"
      },
      "created_date": {
        "type": "date",
        "format": "iso8601"
      },
      "resolved_date": {
        "type": "date",
        "format": "iso8601"
      },
      "labels": {
        "type": "keyword"
      },
      "related_pr": {
        "type": "keyword"
      }
    }
  }
}

Construye el servidor MCP

Nuestro servidor MCP implementa dos herramientas que siguen las especificaciones de OpenAI, y utilizan búsquedas híbridas para combinar coincidencia semántica y textual para obtener mejores resultados.

Herramienta de búsqueda

Usa la búsqueda híbrida con RRF (Fusión de Rango Recíproco), combinando la búsqueda semántica con la coincidencia de texto:

@mcp.tool()
    async def search(query: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Search for internal issues and PRs using hybrid search (semantic + text with RRF).
        Returns list with id, title, and url per OpenAI spec.
        """
        if not query or not query.strip():
            return {"results": []}

        logger.info(f"Searching for: '{query}'")

        try:
            # Hybrid search with RRF (Reciprocal Rank Fusion)
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                size=10,
                source=["id", "title", "url", "type", "priority"],
                retriever={
                    "rrf": {
                        "retrievers": [
                            {
                                # Semantic search with ELSER
                                "standard": {
                                    "query": {
                                        "semantic": {
                                            "field": "text_semantic",
                                            "query": query
                                        }
                                    }
                                }
                            },
                            {
                                # Text search (BM25) for keyword matching
                                "standard": {
                                    "query": {
                                        "multi_match": {
                                            "query": query,
                                            "fields": [
                                                "title^3",
                                                "text^2",
                                                "assignee^2",
                                                "type",
                                                "labels",
                                                "priority"
                                            ],
                                            "type": "best_fields",
                                            "fuzziness": "AUTO"
                                        }
                                    }
                                }
                            }
                        ],
                        "rank_window_size": 50,
                        "rank_constant": 60
                    }
                }
            )

            results = []
            if response and 'hits' in response:
                for hit in response['hits']['hits']:
                    source = hit['_source']
                    results.append({
                        "id": source.get('id', hit['_id']),
                        "title": source.get('title', 'Unknown'),
                        "url": source.get('url', '')
                    })

            logger.info(f"Found {len(results)} results")
            return {"results": results}

        except Exception as e:
            logger.error(f"Search error: {e}")
            raise ValueError(f"Search failed: {str(e)}")

Puntos clave:

  • Búsqueda híbrida con RRF: Combina búsqueda semántica (ELSER) y búsqueda por texto (BM25) para mejores resultados.

  • Búsqueda de múltiples coincidencias: Busca en múltiples campos con mejores ponderaciones (título^3, texto^2, responsable^2). El símbolo de intercalación (^) multiplica las puntuaciones de relevancia, y prioriza las coincidencias en los títulos sobre el contenido.

  • Correspondencia aproximada: fuzziness: AUTO maneja los errores tipográficos y ortográficos al permitir coincidencias aproximadas.

  • Ajuste de parámetros de RRF:

    • rank_window_size: 50 - Especifica cuántos resultados principales de cada recuperador (semántico y textual) se consideran antes de combinarlos.

    • rank_constant: 60 - Este valor determina cuánta influencia tienen los documentos en los conjuntos de resultados individuales sobre el resultado final clasificado.

  • Solo devuelve los campos obligatorios: id, title, url según la especificación de OpenAI, y evita exponer otros campos innecesariamente.

Herramienta de extracción

Recupera los detalles del documento por ID de documento, si existe:

@mcp.tool()
    async def fetch(id: str) -> Dict[str, Any]:
        """
        Retrieve complete issue/PR details by ID.
        Returns id, title, text, url.
        """
        if not id:
            raise ValueError("ID is required")

        logger.info(f"Fetching: {id}")

        try:
            # Search by the 'id' field (not _id) since IDs are stored as a field
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                body={
                    "query": {
                        "term": {
                            "id": id  # Search by your custom 'id' field
                        }
                    },
                    "size": 1
                }
            )

            if not response or not response['hits']['hits']:
                raise ValueError(f"Document with id '{id}' not found")

            hit = response['hits']['hits'][0]
            source = hit['_source']

            result = {
                "id": source.get('id', id),
                "title": source.get('title', 'Unknown'),
                "text": source.get('text', ''),
                "url": source.get('url', ''),
                "type": source.get('type', ''),
                "status": source.get('status', ''),
                "priority": source.get('priority', ''),
                "assignee": source.get('assignee', ''),
                "created_date": source.get('created_date', ''),
                "resolved_date": source.get('resolved_date', ''),
                "labels": source.get('labels', ''),
                "related_pr": source.get('related_pr', '')
            }

            logger.info(f"Fetched: {result['title']}")
            return result

        except Exception as e:
            logger.error(f"Fetch error: {e}")
            raise ValueError(f"Failed to fetch '{id}': {str(e)}")

Puntos clave:

  • Búsqueda por campo de ID de documento: usa la búsqueda de término en el campo personalizado id.

  • Devuelve el documento completo: incluye el campo completo text con todo el contenido.

  • Estructura plana: todos los campos en el nivel raíz, coincidiendo con la estructura de documentos de Elasticsearch.

Desplegar en Google Colab

Usaremos Google Colab para ejecutar nuestro servidor MCP y ngrok para exponerlo públicamente de forma tal que ChatGPT pueda conectarse.

Paso 1: Abre el cuaderno de Google Colab.

Accede a nuestro cuaderno preconfigurado Elasticsearch MCP para ChatGPT.

Paso 2: Configura tus credenciales

Necesitarás tres datos:

  • URL de Elasticsearch: Tu URL del cluster de Elasticsearch.

  • Clave API de Elasticsearch: Clave API con acceso de lectura a tu índice.

  • Token de autenticación ngrok: Token gratis de ngrok. Usaremos ngrok para exponer la URL de MCP a internet y así ChatGPT pueda conectarse.

Obtener tu token ngrok

  1. Regístrate para una cuenta gratis en ngrok

  2. Ve a tu dashboard de ngrok

  3. Copia tu token de autenticación

Agregar secretos a Google Colab

En el cuaderno de Google Colab:

  1. Haz clic en el icono de llave en la barra lateral izquierda para abrir Secretos.

  2. Añade estos tres secretos:

ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
ELASTICSEARCH_API_KEY=your-api-key
NGROK_TOKEN=your-ngrok-token

3. Habilitar el acceso al cuaderno para cada secreto

Agregar secretos a Google Collab

Paso 3: Ejecutar el cuaderno

  1. Haz clic en Tiempo de ejecución y, a continuación, en Ejecutar todo para ejecutar todas las celdas.

  2. Espera que el servidor se inicie (aproximadamente 30 segundos).

  3. Busque la salida que muestre su URL pública de ngrok

4. La salida mostrará algo como:

La salida de ejecutar un cuaderno en Google Collab

Conéctate a ChatGPT.

Ahora conectaremos el servidor MCP a tu cuenta de ChatGPT.

  1. Abre ChatGPT y ve a Configuración.

  2. Navega a Conectores.Si estás usando una cuenta Pro, debes activar el modo de desarrollador en los conectores.

Conectando el servidor MPC a una cuenta de ChatGPT

Si estás usando la versión Enterprise o Business de ChatGPT, debes publicar el conector en tu lugar de trabajo.

3. Haz clic en Crear.

Agregar un conector a ChatGPT

Nota: En los espacios de trabajo Business, Enterprise y Edu, solo los propietarios, administradores y usuarios que tengan habilitada la correspondiente configuración (para Enterprise/Edu) pueden agregar conectores personalizados. Los usuarios con un rol de miembro común no tienen la capacidad de agregar conectores personalizados por su cuenta.

Una vez que un propietario o usuario administrador agrega un conector y lo habilita, estará disponible para que lo usen todos los miembros del espacio de trabajo.

4. Introduce la información requerida y la URL de tu ngrok que termina en /sse/. Recuerda la “/” después de “sse”. No funcionará si no la agregas:

  • Nombre: Elasticsearch MCP

  • Descripción: MCP personalizado para buscar y extraer información interna de GitHub.

Crear un conector MCP de Elastic

5. Presione Crear para guardar el MCP personalizado.

Guardar el conector MCP personalizado haciendo clic en crear

La conexión es instantánea si tu servidor está en funcionamiento. No se necesita ninguna otra autenticación, ya que la clave API de Elasticsearch está configurada en tu servidor.

Prueba el servidor MCP

Antes de hacer preguntas, necesitas seleccionar qué conector ChatGPT debe usar.

Seleccionar qué conector debe usar ChatGPT

Indicación 1: Buscar incidencias

Pregunta: “Encuentre incidencias relacionadas con la migración de Elasticsearch” y confirma la llamada a la herramienta de acciones.

Pide a ChatGPT “Encuentra incidencias relacionadas con la migración de Elasticsearch” y confirma la llamada a la herramienta de acciones.

ChatGPT llamará a la herramienta search con tu búsqueda. Puedes ver que está buscando herramientas disponibles y preparándose para llamar a la herramienta Elasticsearch y confirma con el usuario antes de tomar cualquier acción en relación con la herramienta.

Solicitud de llamada a la herramienta:

{
  "query": "Elasticsearch migration issues"
}

Respuesta de la herramienta:

{
  "results": [
    {
      "id": "PR-598",
      "title": "Elasticsearch 8.x migration - Application code changes",
      "url": "https://internal-git.techcorp.com/pulls/598"
    },
    {
      "id": "ISSUE-1712",
      "title": "Migrate from Elasticsearch 7.x to 8.x",
      "url": "https://internal-git.techcorp.com/issues/1712"
    },
    {
      "id": "RFC-045",
      "title": "Design Proposal: Microservices Migration Architecture",
      "url": "https://internal-git.techcorp.com/rfcs/045"
    }
    // ... 7 more results
  ]
}

ChatGPT procesa los resultados y los presenta en un formato natural y conocido.

Cómo procesa ChatGPT los resultados de la solicitud de llamada a la herramienta y la respuesta de la herramienta

Entre bastidores

Indicación: “Busca incidencias relacionadas con la migración de Elasticsearch”

1. Llamadas de ChatGPT search(“Elasticsearch migration”)

2. Elasticsearch realiza una búsqueda híbrida

  • La búsqueda semántica entiende conceptos como “actualización” y “compatibilidad de versiones”.

  • La búsqueda de texto encuentra coincidencias exactas de "Elasticsearch" y "migración".

  • RRF combina y clasifica los resultados de ambos enfoques

3. Devuelve los 10 mejores eventos que coinciden con id, title, url

4. ChatGPT identifica “ISSUE-1712: migrar de Elasticsearch 7.x a 8.x” como el resultado más relevante.

Indicación 2: Obtén los detalles completos

Pregunta: “Obtén detalles de ISSUE-1889”

ChatGPT reconoce que quieres información detallada sobre un problema específico y llama a la herramienta de búsqueda y confirma con el usuario antes de hacer cualquier acción con la herramienta.

ChatGPT reconoce que deseas información detallada sobre una incidencia específica, llama a la herramienta fetch y confirma con el usuario antes de tomar cualquier acción con la herramienta.

Solicitud de llamada a la herramienta:

{
  "id": "ISSUE-1889"
}

Respuesta de la herramienta:

{
  "id": "ISSUE-1889",
  "title": "SQL injection vulnerability in search endpoint",
  "text": "Description: Security audit identified SQL injection vulnerability in /api/v1/search endpoint. User input from query parameter is not properly sanitized before being used in raw SQL query. Severity: HIGH - Immediate action required Affected Code: - File: services/search/query_builder.py - Line: 145-152 - Issue: String concatenation used instead of parameterized queries Investigation: - @security_team_alice: Confirmed exploitable with UNION-based injection - @sarah_dev: Checking all other endpoints for similar patterns - @john_backend: Found 3 more instances in legacy codebase Remediation: - Rewrite using SQLAlchemy ORM or parameterized queries - Add input validation and sanitization - Implement WAF rules as additional layer - Security regression tests Comments: - @tech_lead_mike: Stop all other work, this is P0 - @sarah_dev: PR-578 ready with fixes for all 4 vulnerable endpoints - @alex_devops: Deployed hotfix to production 2025-09-19 at 14:30 UTC - @security_team_alice: Verified fix, conducting full pentest next week Resolution: All vulnerable endpoints patched. Added pre-commit hooks to catch raw SQL queries. Security training scheduled for team.",
  "url": "https://internal-git.techcorp.com/issues/1889",
  "type": "issue",
  "status": "closed",
  "priority": "critical",
  "assignee": "sarah_dev",
  "created_date": "2025-09-18",
  "resolved_date": "2025-09-19",
  "labels": "security, vulnerability, bug, sql",
  "related_pr": "PR-578"
}

ChatGPT sintetiza la información y la presenta de manera clara.

Cómo ChatGPT sintetiza la información y la presenta
Cómo ChatGPT presenta la información

Entre bastidores

Indicación: «Obtén los detalles de ISSUE-1889»

  1. Llamadas de ChatGPT fetch(“ISSUE-1889”)

  2. Elasticsearch recupera el documento completo

  3. Devuelve un documento completo con todos los campos a nivel raíz.

  4. ChatGPT sintetiza la información y responde con citas adecuadas.

Conclusión

En este artículo, creamos un servidor MCP personalizado que conecta ChatGPT a Elasticsearch con herramientas MCP dedicadas de búsqueda y extracción, lo cual permite realizar búsquedas en lenguaje natural sobre datos privados.

Este patrón MCP funciona para cualquier índice, documentación, productos, logs o cualquier otro dato de Elasticsearch que quieras buscar mediante lenguaje natural.

Contenido relacionado

Agent Builder, más allá del chat: presentamos la infraestructura aumentada

Alexander Wert

Gestión de la memoria agentic con Elasticsearch

Someshwaran Mohankumar

Uso de la API de inferencia de Elasticsearch junto con modelos de Hugging Face

Jeffrey Rengifo

Construir un agente de IA para RRHH con Elastic Agent Builder y GPT-OSS

Tomás Murúa

Cómo crear un servidor MCP de Elasticsearch con TypeScript

Jeffrey Rengifo

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo