使用 TypeScript 创建 Elasticsearch MCP 服务器

从业务背景、核心协议原理、代码实现、集成测试全维度讲解如何用 TypeScript 搭建 Elasticsearch MCP 服务器,实现 LLM 与 Elasticsearch 知识库的标准化联动。

一、业务背景:Elasticsearch+LLM 的痛点与 MCP 的价值

在处理超大型企业内部知识库 (如技术文档、运维规范、研发标准)时,Elasticsearch 凭借强大的全文检索能力,能快速定位相关文档,但这只是信息处理的第一步。工程师的核心需求远不止「找到文档」,而是让大语言模型(LLM)综合多篇文档内容、生成精简摘要、并把答案溯源到原始文档,避免 LLM 生成内容出现「幻觉」,同时保证答案可追溯。

但 LLM 与外部系统(Elasticsearch)的连接长期存在碎片化问题 :自定义 API 对接、插件开发无统一标准,不同客户端适配成本极高。而模型上下文协议(MCP,Model Context Protocol) 正是为解决这一问题诞生的开放标准,它为 LLM 与外部系统提供了标准化、安全的双向通信规范,让 Elasticsearch 这类检索系统能无缝接入 LLM 应用。

Elastic 官方虽提供了现成的 MCP 解决方案,但自定义 MCP 服务器能让开发者完全掌控搜索逻辑、结果格式、后处理流程(如摘要生成、引用溯源),这也是本文搭建自定义服务器的核心初衷。

二、核心概念:先搞懂「是什么」,再谈「怎么做」

1. 什么是 MCP(模型上下文协议)?

MCP 是Anthropic 公司开源的开放通信标准 ,核心作用是建立LLM 应用 ↔ 外部系统(Elasticsearch、数据库、文件系统等) 的安全双向连接,替代传统自定义接口、专属插件的碎片化对接方式。

  • 核心特性:标准化工具注册、结构化数据传输、多客户端兼容、轻量通信;

  • 通信原理:MCP 服务器对外暴露「工具」,MCP 客户端主动发现并调用这些工具,实现数据互通;

  • 传输方式:本文采用Stdio(标准输入输出流) 传输,客户端以子进程形式启动服务器,无需额外端口,是本地 MCP 服务的最优轻量方案。

2. 官方 MCP 方案 vs 自定义 TypeScript MCP 服务器

维度 官方方案 (Elastic Agent Builder / Python) 自定义 TypeScript MCP 服务器
查询能力 受限:仅支持 ES|QL 或适配低版本 ES,无法使用完整 DSL 进行复杂检索。 完整:支持 Elasticsearch 完整查询 DSL,实现精准、复杂的全文检索。
结果处理 单一:无自定义摘要或引用生成逻辑,仅返回原始数据。 智能:可集成 OpenAI 等大模型,自定义摘要生成、引用标注及格式化逻辑。
开发体验 一般:Python 生态对前端/Node 工程师不够友好,或受限于特定版本。 高效:基于 Node.js + TypeScript,类型安全,完美适配现代前端/服务端生态。
扩展性 封闭:逻辑固化,难以添加额外的业务处理流程。 完全可控:支持按需添加数据清洗、权限校验、多源融合等后处理逻辑。

3. 核心技术栈与作用原理

依赖包 核心作用 底层原理
@elastic/elasticsearch Elasticsearch Node.js 官方客户端 封装 ES RESTful API,实现 DSL 查询、索引操作、结果解析
@modelcontextprotocol/sdk MCP 服务器核心 SDK 封装 MCP 协议规范,提供服务器初始化、工具注册、传输通信能力
openai OpenAI 官方 SDK 调用 gpt-4o-mini 模型,实现检索后文档的摘要生成与答案综合
zod 运行时类型校验库 弥补 TypeScript 编译期类型校验的不足,对工具入参/出参做实时校验,保证数据合法性
ts-node/typescript TypeScript 运行/编译工具 将 TS 代码编译为 Node.js 可执行的 JS 代码,支持开发时热运行

4. MCP 客户端:Claude Desktop

MCP 客户端是调用 MCP 服务器工具的终端载体,市面上有多种客户端可选,本文选择 Claude Desktop 核心原因:

  • 开箱即用,无需额外开发客户端界面;

  • 内置 LLM 意图解析能力,可自动串联多个 MCP 工具(先检索再总结);

  • 支持自然语言交互,用户直接提问即可触发 MCP 工具调用;

  • 支持工具调用权限授权,保证本地服务安全。

三、先修条件

搭建前需准备好以下环境与密钥,缺一不可:

  1. Node.js 20 及以上版本(保证 MCP SDK 与 ES 客户端兼容);

  2. 可用的 Elasticsearch 服务(本地/Elastic Cloud 均可);

  3. OpenAI API 密钥(用于调用 gpt-4o-mini 生成摘要);

  4. Claude Desktop 客户端(MCP 交互界面)。

四、全流程实现:从项目初始化到服务上线

第一步:初始化 Node.js 项目并安装依赖

首先创建空项目,初始化 package.json 管理依赖:

Bash 复制代码
npm init -y

再安装核心依赖与开发依赖:

Bash 复制代码
npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript

依赖作用补充

  • @elastic/elasticsearch:提供 ES 连接、查询、数据读写能力;

  • @modelcontextprotocol/sdk:MCP 服务器的核心骨架,负责工具注册、客户端通信;

  • openai:对接 OpenAI 大模型,实现 RAG 中的「生成」环节;

  • zod:定义工具入参/出参的结构化模式,运行时校验数据,避免非法参数导致服务崩溃;

  • ts-node/@types/node/typescript:TS 代码的运行、类型定义、编译工具,保证 TypeScript 可在 Node 环境执行。

第二步:准备 Elasticsearch 知识库数据集

为了让 MCP 服务器有可检索的数据,需构建模拟企业内部技术知识库,文档格式统一如下:

JSON 复制代码
{
    "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"]
}

补充实现细节 :需编写脚本完成Elasticsearch 索引创建 + 批量数据导入,核心逻辑为:

  1. 连接 ES 服务,创建名为 documents 的索引(设置字段映射:title/text、content/text、tags/keyword);

  2. 读取本地知识库 JSON 数据,通过 ES bulk API 批量导入文档;

  3. 校验索引数据是否写入成功,保证检索可用。

第三步:编写 TypeScript MCP 服务器核心代码

创建项目入口文件 index.ts,逐模块实现逻辑,每段代码均附带原理解析

1. 导入依赖与环境变量配置
TypeScript 复制代码
// 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";

// 环境变量配置:支持从系统环境变量读取,本地开发默认本地 ES 地址
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"; // 固定检索的 ES 索引名

说明:通过环境变量解耦配置,避免密钥硬编码;默认本地 ES 地址适配开发环境,生产环境可通过环境变量注入云服务地址。

2. 初始化 ES 与 OpenAI 客户端
TypeScript 复制代码
// 初始化 OpenAI 客户端
const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

// 初始化 Elasticsearch 客户端
const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY, // 云 ES 用 API Key,本地可省略
  },
});

说明:ES 客户端基于 HTTP/HTTPS 与 ES 集群通信,支持 API Key 认证(Elastic Cloud 强制要求);OpenAI 客户端通过 API Key 鉴权,调用模型接口。

3. Zod 结构化模式定义(核心:运行时类型安全)
TypeScript 复制代码
// 原始文档结构校验
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(),
});

// TypeScript 类型推导:Zod Schema 自动生成 TS 类型
type Document = z.infer<typeof DocumentSchema>;
type SearchResult = z.infer<typeof SearchResultSchema>;

补充 :TypeScript 仅做编译期类型校验,运行时无法校验接口返回/用户输入数据;Zod 实现运行时强校验,同时自动推导 TS 类型,兼顾类型安全与运行时稳定性,是 MCP 工具结构化交互的核心。

4. 初始化 MCP 服务器
TypeScript 复制代码
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",
});

说明McpServer 是 MCP SDK 的核心实例,定义服务器的基础信息(名称、描述、版本),用于客户端发现与识别服务。

第四步:注册 MCP 核心工具(RAG 工作流实现)

MCP 服务器的核心是对外暴露工具 ,本文实现检索+总结的完整 RAG 工作流,共两个工具:

  1. search_docs:Elasticsearch 全文检索,拉取相关文档;

  2. summarize_and_cite:基于检索结果,调用 OpenAI 生成摘要+文档引用。

工具 1:search_docs(Elasticsearch 检索工具)
TypeScript 复制代码
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 {
      // Elasticsearch DSL 复杂查询
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            // 必须匹配:多字段模糊检索
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"], // title 权重提升 2 倍
                  fuzziness: "AUTO", // 自动容错拼写错误
                },
              },
            ],
            // 可选匹配:短语精确匹配,额外加分
            should: [
              {
                match_phrase: {
                  title: { query: query, boost: 2 },
                },
              },
            ],
          },
        },
        // 高亮关键词:标记检索命中的字段
        highlight: { fields: { title: {}, content: {} } },
      });

      // 解析 ES 检索结果
      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, // 相关性得分
        };
      });

      // 格式化非结构化文本响应(供 Claude 展示)
      const contentText = results
        .map((r, i) => `[${i + 1}] ${r.title} (score: ${r.score.toFixed(2)})\n${r.content.substring(0, 200)}...`)
        .join("\n\n");

      // 总命中数处理(兼容 ES 不同版本返回格式)
      const totalHits = typeof response.hits.total === "number" 
        ? response.hits.total 
        : (response.hits.total?.value ?? 0);

      // 返回 MCP 标准响应:content(文本)+ structuredContent(结构化数据)
      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,
      };
    }
  }
);

核心原理深度解析

  1. Elasticsearch 查询 DSL 设计

    • bool/must:强制多字段匹配,覆盖标题、内容、标签;

    • title^2权重提升,标题含关键词的文档相关性得分翻倍,排序更靠前;

    • fuzziness: "AUTO"自动拼写容错,短词允许1个字符错误,长词允许2个,提升检索容错性;

    • should/match_phrase:短语精确匹配额外加分,强化精准检索效果;

    • highlight:高亮命中关键词,便于快速定位核心内容。

  2. MCP 响应规范 :必须返回 content(非结构化文本,供客户端展示)+ 可选 structuredContent(结构化数据,供程序二次处理)。

工具 2:summarize_and_cite(摘要+引用生成工具)
TypeScript 复制代码
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 {
      // 截取前N篇文档作为上下文
      const used = results.slice(0, max_docs);
      // 拼接上下文文本:格式化文档内容供 LLM 阅读
      const context = used
        .map((r: SearchResult, i: number) => `[Document ${i + 1}: ${r.title}]\n${r.content}`)
        .join("\n\n---\n\n");

      // 调用 OpenAI gpt-4o-mini 生成摘要
      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}` },
        ],
        // Token 计算:中文字符≈1token=4字符,最大限制1000token防止溢出
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3, // 低温度,减少生成随机性,保证答案精准
      });

      // 解析 LLM 返回的摘要
      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}`;

      // 返回 MCP 标准响应
      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,
      };
    }
  }
);

核心原理深度解析

  1. RAG 上下文拼接:将检索到的文档格式化编号,让 LLM 清晰区分不同来源,避免内容混淆;

  2. OpenAI 调用参数设计

    • temperature: 0.3:降低生成随机性,保证摘要严格基于检索文档,减少幻觉;

    • max_tokens:按字符数换算 token,控制摘要长度,同时设置上限防止接口报错;

  3. 引用溯源:提取检索结果的元数据(ID、标题、标签、得分),实现答案可追溯,满足企业知识库的合规性要求。

5. 启动 MCP 服务器(Stdio 传输)
TypeScript 复制代码
// 创建 Stdio 传输实例:标准输入输出流通信
const transport = new StdioServerTransport();
// 服务器连接传输层,开始监听客户端调用
server.connect(transport);

说明StdioServerTransport 是 MCP 最轻量化的传输方式,Claude Desktop 作为父进程,通过标准输入(stdin)发送调用指令 ,服务器通过标准输出(stdout)返回结果,无需占用网络端口,本地部署安全高效。

第五步:编译 TypeScript 代码

将 TS 代码编译为 Node.js 可执行的 JS 代码,执行编译命令:

Bash 复制代码
npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop

编译参数解析

  • --target ES2022:编译为 ES2022 语法,兼容 Node.js 20+;

  • --module node16:适配 Node.js 模块化规范;

  • --outDir ./dist:输出编译后的 JS 文件到 dist 目录;

  • --strict:开启 TS 严格模式,保证类型安全;

  • --esModuleInterop:兼容 CommonJS 与 ES 模块。

编译完成后,dist/index.js 即为 MCP 服务器的可执行文件。

第六步:集成 MCP 服务器到 Claude Desktop

Claude Desktop 通过配置文件加载本地 MCP 服务器,需在配置文件中添加以下内容:

JSON 复制代码
{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": ["/Users/user-name/app-dir/dist/index.js"], // 编译后 JS 文件的绝对路径
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here", // ES 服务地址
        "ELASTICSEARCH_API_KEY": "your-api-key-here",    // ES 认证密钥
        "OPENAI_API_KEY": "your-openai-key-here"         // OpenAI 密钥
      }
    }
  }
}

补充细节

  • 配置文件路径:macOS 为 ~/.claude/config.json,Windows 为 %APPDATA%/Claude/config.json

  • 环境变量需与代码中定义的变量名完全一致,保证服务能读取密钥与地址;

  • args 必须为绝对路径,避免 Claude 找不到可执行文件。

五、功能测试:验证 MCP 服务器全流程

1. 启用 MCP 工具

打开 Claude Desktop,点击「搜索和工具」,确认 elasticsearch-rag-mcp 下的两个工具已启用,可按需开启/关闭。

2. 单工具测试:检索文档

直接提问:搜索关于认证方法和基于角色访问控制的文档

Claude 会自动调用 search_docs 工具,Elasticsearch 检索后返回 5 篇相关文档,展示标题、相关性得分、内容摘要,效果如下:

Plain 复制代码
Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles...
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication...
...

3. 工具串联测试:检索+总结+引用

提问:改进我们系统认证和访问控制的主要建议是什么?请附上参考文献。

核心逻辑 :Claude Desktop 内置 LLM 自动解析意图,先调用 search_docs 检索文档,再将结果自动传递给 summarize_and_cite 工具,最终返回精简摘要+带标签的引用来源,示例结果:

Plain 复制代码
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) [1]
2. Regular Access Audits [1]
3. Just-in-Time (JIT) Access [1]
4. OAuth 2.0 for Secure Authentication [2]
5. Token Security and Management [2]

References
Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)

4. 权限授权

首次调用工具时,Claude 会弹出授权提示,选择「始终允许」或「允许一次」,即可正常使用 MCP 服务。

六、结论与后续拓展

1. 核心总结

本文完整实现了基于 TypeScript 的 Elasticsearch MCP 服务器 ,打通了「Elasticsearch 检索 → OpenAI 摘要生成 → Claude Desktop 交互」的完整 RAG 链路,解决了企业知识库 LLM 应用的检索精准性、答案可追溯、工具标准化三大核心问题。

相较于 Elastic 官方 MCP 方案,自定义服务器具备完整 DSL 检索、自定义后处理、类型安全、生态适配等核心优势,完全适配企业内部知识库的个性化需求。

2. 后续拓展方向

  • 为 MCP 服务器添加搜索模板,参数化查询提升检索准确性与灵活性;

  • 扩展更多 MCP 工具:如文档新增、索引管理、权限过滤等;

  • 适配其他 MCP 客户端(如 VS Code、Web 客户端);

  • 优化 LLM 提示词工程,提升摘要质量与引用精准度;

  • 添加日志、监控、异常告警,完善生产级服务能力。

MCP 作为 LLM 外部工具标准化的核心协议,未来兼容性与生态会持续完善,自定义 MCP 服务器或将成为 LLM 对接企业内部系统的主流方案。

相关推荐
默|笙2 小时前
【Linux】进程信号(4)_信号捕捉_内核态与用户态
linux·运维·服务器
深圳市恒讯科技2 小时前
OpenClaw 2026安全指南
运维·服务器·安全
We་ct2 小时前
LeetCode 373. 查找和最小的 K 对数字:题解+代码详解
前端·算法·leetcode·typescript·二分·
学编程的小程2 小时前
我的极空间 NAS 进阶玩法:开启 SSH,解锁私有云服务器新体验
运维·服务器·ssh
深念Y2 小时前
飞牛OS部署MCSM搭建MC服务器完整教程
运维·服务器·jdk·端口·nas·mc·飞牛os
JACK的服务器笔记2 小时前
《服务器测试百日学习计划——Day14:BMC基础与健康状态,为什么服务器排障不能只看OS》
运维·服务器·学习
虎头金猫2 小时前
自建 GitLab 没公网?用内网穿透技术,远程开发协作超丝滑
运维·服务器·网络·开源·gitlab·开源软件·开源协议
wei_shuo2 小时前
无需服务器的本地文档编辑器 document 部署与远程访问教程
运维·服务器
春日见2 小时前
深度神经网络的底层数学原理
运维·服务器·windows·深度学习·自动驾驶