TypeScriptを使用したElasticsearch MCPサーバーの作成
TypeScriptとClaude Desktopを使用してElasticsearch MCPサーバーを作成する方法を学びます。
Elasticsearchで大規模なナレッジベースを扱う場合、情報を見つけるだけでは片手落ちです。エンジニアは複数の文書から結果を統合し、要約を作成し、回答を情報源にたどる必要があることが多いです。モデルコンテキストプロトコル(MCP)は、Elasticsearchと大規模言語モデル(LLM)アプリケーションを接続するための標準化された方法を提供します。ElasticはElastic Agent Builder(MCPエンドポイントを機能の一つに含む)のような公式ソリューションを提供していますが、カスタムMCPサーバーを構築することで、検索ロジック、結果のフォーマット、取得したコンテンツをLLMに渡して合成、要約、引用を行う方法などを完全に制御できます。
この記事では、カスタムElasticsearch MCPサーバーを構築するメリットを探り、ElasticsearchをLLM対応アプリケーションに接続するサーバーをTypeScriptで作成する方法を紹介します。
カスタムのElasticsearch MCPサーバーを構築する理由
ElasticはMCPサーバーのいくつかの代替手段を提供しています。
MCPサーバーとElasticsearchの連携方法をより細かく制御したい場合は、独自のカスタムサーバーを構築することで、ニーズに合わせて柔軟にカスタマイズできます。例えば、Agent BuilderのMCPエンドポイントはElasticsearchクエリ言語(ES|QL)クエリに限定されていますが、カスタムサーバーでは完全なクエリDSLを使用できます。また、LLMに渡される前に結果をどのようにフォーマットするかを制御でき、このチュートリアルで実装するOpenAIを利用した要約など、追加の処理ステップを統合することもできます。
この記事を読み終える頃には、Elasticsearchインデックスに保存されている情報を検索し、要約し、引用を提供するTypeScriptで記述されたMCPサーバーが完成しているでしょう。Elasticsearchを使用して情報を検索し、OpenAIのgpt-4o-miniモデルを用いて要約と引用を生成し、Claude DesktopをMCPクライアントおよびUIとして活用してユーザーのクエリを受け取り、応答を提供します。最終的には、エンジニアが組織内の技術文書全体からベストプラクティスを発見し、統合するのに役立つ内部ナレッジアシスタントが完成します。

要件:
Node.js 20 +
Elasticsearch
OpenAI APIキー
Claude Desktop
MCPとは何ですか?
MCPはAnthropicによって作成されたオープンスタンダードで、LLMとElasticsearchのような外部システムとの間で安全かつ双方向の接続を提供します。MCP の現状についてはこの記事で詳しく読むことができます。
MCPの環境は日々進化しており、多様なユースケースに対応したサーバーが利用可能です。さらに、この記事でご紹介するように、独自のカスタムMCPサーバーを簡単に構築することもできます。
MCPクライアント
利用可能なMCPクライアントは多数あり、それぞれに特徴や制限があります。簡便性と普及度を考慮し、今回はMCPクライアントとしてClaude Desktopを使用します。これは、ユーザーが自然言語で質問できるチャットインターフェースとして機能し、MCPサーバーが公開しているツールを自動的に呼び出して、文書を検索し、要約を生成します。

Elasticsearch MCPサーバーの作成
TypeScript SDKを使えば、ユーザーのクエリ入力に基づいてElasticsearchデータのクエリ方法を理解するサーバーを簡単に作成できます。
この記事では、Elasticsearch MCPサーバーとClaude Desktopクライアントを統合するための手順を説明します。
Elasticsearch MCPサーバーを設定してください
まず、Nodeアプリケーションを初期化します。
npm init -yこれで package.jsonファイルが作成され、このアプリケーションに必要な依存関係のインストールを開始できます。
npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript@elastic/elasticsearch はElasticsearchのNode.jsライブラリにアクセスするためのものです。
@modelcontextprotocol/sdkは、MCPサーバーの作成と管理、ツールの登録、MCPクライアントとの通信処理を行うためのコアツールを提供します。
openaiは、OpenAIのモデルと対話し、要約や自然言語による対応を生成することができます。
zodは、各ツールの入出力データの構造化スキーマの定義と検証に役立ちます。
ts-node、@types/node、 typescriptは開発中にコードの入力やスクリプトのコンパイルに使用されます。
データセットを設定
Claude DesktopがMCPサーバーを使用してクエリできるデータを提供するために、内部の模擬ナレッジベースデータセットを使用します。このデータセットから作成される文書は以下のような形式になります。
{
"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にインデックスを作成し、そこにデータセットをロードするスクリプトを用意しました。こちらでご覧いただけます。
MCPサーバー
index.tsというファイルを作成し、依存関係をインポートして環境変数を処理するための以下のコードを追加します。
// 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";また、ElasticsearchとOpenAIの呼び出しを処理するようにクライアントを初期化します。
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
const _client = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: {
apiKey: ELASTICSEARCH_API_KEY,
},
});実装をより堅牢にし、構造化された入出力を保証するために、 zodを使用してスキーマを定義します。これにより、ランタイムでデータを検証し、エラーを早期に捕捉し、ツールの対応をプログラムで処理しやすくすることができます。
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>;構造化出力の詳細については、こちらをご覧ください。
それでは、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",
});MCPツールの定義
すべての設定が完了したので、MCPサーバーによって公開されるツールの作成を開始できます。このサーバーは2つのツールを公開します。
search_docs:Elasticsearchで文書を全文検索で検索します。summarize_and_cite:以前に取得した文書から情報を要約・統合し、ユーザーの質問に答えます。このツールは、出典となる文書を参照する引用も追加します。
これらのツールを組み合わせることで、シンプルな「検索してから要約」ワークフローが構築されます。一方のツールが関連文書を取得し、もう一方のツールがその文書を使用して要約と引用を含む回答を生成します。
ツールの応答形式
各ツールは任意の入力パラメータを受け入れることができますが、以下の構造で応答する必要があります。
Content: これは非構造化形式でのツールの応答です。このフィールドは通常、テキスト、画像、音声、リンク、または埋め込みを返すために使用されます。この用途では、ツールによって生成された情報を含む整形済みテキストを返すために使用されます。
structuredContent:これは、各ツールの結果を構造化された形式で提供するために使用されるオプションの戻り値です。これはプログラム上の目的に役立ちます。このMCPサーバーでは使用されていませんが、他のツールを開発したり、結果をプログラムで処理したりする場合に便利です。
その構造を念頭に置いて、各ツールについて詳しく見ていきましょう。
Search_docsツール
このツールは、Elasticsearchインデックスで全文検索を実行し、ユーザークエリに基づいて最も関連性の高いドキュメントを取得します。主要な一致をハイライトし、関連性スコアを素早くまとめてくれます。
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,
};
}
}
);fuzziness: “AUTO” は 分析対象のトークンの長さに応じて誤字許容度を調整するように設定しています。また 、 タイトルフィールドで一致が発生したドキュメントのスコアを上げるtitle^2も設定しました。。
summarize_and_citeツール
このツールは、前回の検索で取得したドキュメントに基づいて要約を生成します。OpenAIの gpt-4o-miniモデルを使用して、ユーザーの質問に答えるために最も関連性の高い情報を統合し、検索結果に直接基づく回答を提供します。要約に加えて、使用したソースドキュメントの引用情報(メタデータ)も返します。
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,
};
}
}
);最後に、stdioを使用してサーバーを起動する必要があります。つまり、MCPクライアントは、標準の入出力ストリームを読み書きすることでサーバーと通信します。stdioは最もシンプルな転送オプションで、クライアントによってサブプロセスとして立ち上げられるローカルMCPサーバーに適しています。ファイルの最後に以下のコードを追加します。
const transport = new StdioServerTransport();
server.connect(transport);次に、以下のコマンドを使用してプロジェクトをコンパイルします。
npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInteropこれによりdistフォルダが作成され、その中にindex.jsファイルが作成されます。
MCPサーバーをClaude Desktopにロード
Claude DesktopでMCPサーバーを設定するには、このガイドに従ってください。Claudeの設定ファイルでは、以下の値を設定する必要があります:
{
"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"
}
}
}
}args値は、 distフォルダ内のコンパイル済みファイルを指す必要があります。また、設定ファイル内の環境変数も、コード内で定義されているものと全く同じ名前で設定する必要があります。
試してみる
各ツールを実行する前に、「検索とツール」をクリックして、ツールが有効になっていることを確認します。ここでは、それぞれを有効または無効にすることもできます。

最後に、Claude DesktopのチャットからMCPサーバーをテストして、質問を始めましょう。

「認証方法とRBACに関する文書を検索する」という質問に対して、search_docs ツールが実行され、以下の結果が返されます。
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.回答は「素晴らしい!認証方法とロールベースのアクセス制御に関する関連文書を5件見つけました。見つかった内容は以下のとおりです。」
ツール呼び出しは、対応ペイロードの一部としてソース文書を返します。これらの文書は、後に引用を生成するために使用されます。

また、複数のツールを一度のインタラクションで連結することも可能です。この場合、Claude Desktopはユーザーの質問を分析し、まずsearch_docsを呼び出して関連文書を取得し、次にその結果をsummarize_and_citeに渡して最終的な回答を生成する必要があると判断します。これらすべては、ユーザーからの個別のプロンプトを必要とせずに実行されます。

この場合、「システム全体の認証とアクセス制御を改善するための主な推奨事項は何ですか?参考文献を含めてください。」というクエリに対して、以下の結果が得られました。
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.前のステップと同様に、この質問に対する各ツールの回答を確認できます。

注:各ツールの使用を承認するかを確認するサブメニューが表示された場合は、「常に許可」または「一度だけ許可」を選択します。

まとめ
MCPサーバーは、ローカルとリモートの両方のアプリケーションのLLMツールの標準化に向けた重要な一歩です。完全な互換性の実現にはまだ取り組んでいますが、その方向へ急速に進んでいます。
この記事では、ElasticsearchをLLM搭載アプリケーションに接続するカスタムMCPサーバーをTypeScriptで構築する方法を学びました。当サーバーは2つのツールを提供しています。1つはQuery DSLを使用して関連文書を取得するためのツールsearch_docs、もう1つはOpenAIモデルとクライアントUIとしてのClaude Desktopを使用して引用付きの要約を生成するためのツールsummarize_and_citeです。
異なるクライアントとサーバープロバイダー間の互換性の将来は有望に見えます。次のステップは、エージェントにより多くの機能と柔軟性を加えることです。実用的な記事で、検索テンプレートを使用してクエリをパラメーター化し、精度と柔軟性を得る方法を学ぶことができます。




