MCP サーバーを実装して Claude Desktop から使ってみる

Model Context Protocol (MCP) に従ったクライアント・サーバーを実装すれば LLM から簡単にデータやツールを統合できる。
今回は Claude Desktop App をクライアントとして、サーバーを実装してみる。
Claude を契約していればAPIキー使って従量課金しなくても色々自由に作れて良さそう😎

Building MCP with LLMs - Model Context Protocol に LLM に読ませるためのドキュメントを txt にひとまとめにしたファイルがあって感動。。
今後は README.md, CONTRIBUTING.md とかと一緒に LLM_CONTEXT.md 的なファイルを配置しておくのが普通になるのかも。。

せっかく LLM 用のドキュメント llms-full.txt
があるので、Cursor の Agent に丸投げしてみよう。
偶然手元に Elasticsearch とキー入力を収集したデータがあるので、そこからデータを取得できるようにサーバーを実装してもらう。
実装
サーバーの実装
Building MCP with LLMs - Model Context Protocol から llms-full.txt をダウンロードしプロジェクトディレクトリに配置。
llms-full.txt を context に追加し、以下のプロンプトを Cursor の Composer で Agent として実行。モデルには Sonet 3.5 を利用。
添付したドキュメントを参考に、ローカルで起動している elasticsearch にアクセスし、インデックス keylogger-* に対してクエリを実行し、データを取得できる MCP サーバーを TypeScript で実装してください。
以下のようなファイル群を勝手に作成してくれた。
.
├── llms-full.md
├── mapping.json
├── package-lock.json
├── package.json
├── README.md
├── src
│ └── server.ts
└── tsconfig.json
サーバー実装の Agent オリジナルの出力を無くしてしまった。。
以下は一部自分が手を加えてもの。
といっても Elasticsearch への接続に basic 認証を追加したり、ツールの定義の文言を修正したのみで、大部分は Agent が実装してくれた。
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@elastic/elasticsearch";
import { z } from "zod";
// Elasticsearchクライアントの初期化
const client = new Client({
node: "https://localhost:9200",
auth: {
username: "elastic",
password: "password",
},
tls: {
rejectUnauthorized: false, // 自己署名証明書を許可
},
});
// バリデーションスキーマの定義
const QueryArgumentsSchema = z.object({
query: z.string().describe("Elasticsearch DSLクエリ(JSON文字列)"),
});
// サーバーインスタンスの作成
const server = new Server(
{
name: "elasticsearch-keylogger",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
// 利用可能なツールの一覧を定義
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query-keylogger",
description:
`キーロガーのデータに対してElasticsearchクエリを実行します。
重要: 最初に以下のクエリでインデックスのマッピングを確認してください:
\`\`\`json
{
"query": {
"match_all": {}
},
"size": 0,
"aggs": {
"indices": {
"terms": { "field": "_index" }
}
}
}
\`\`\`
次に、1件のデータを取得し、実際のデータ内容を確認してください。
\`\`\`json
{
"query": {
"match_all": {}
},
"size": 1
}
\`\`\`
確認後、以下のような操作が可能です:
- 特定期間内の入力キーの集計
- キーの使用頻度ランキング
- 時系列でのキー入力パターン分析
など
クエリは Elasticsearch DSL の JSON 文字列形式で指定してください。
インデックスパターンは 'keylogger-*' が自動的に使用されます。`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Elasticsearch DSLクエリ(JSON文字列)",
},
},
required: ["query"],
},
},
],
};
});
// ツール実行のハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "query-keylogger") {
try {
// クエリの検証
const { query } = QueryArgumentsSchema.parse(args);
// クエリをパースしてバリデーション
let parsedQuery;
try {
parsedQuery = JSON.parse(query);
} catch (e) {
throw new Error("Invalid JSON query");
}
// クエリの実行
const result = await client.search({
index: "keylogger-*",
...parsedQuery,
});
// 結果の整形
const formattedResult = JSON.stringify(result, null, 2);
// レスポンスサイズのチェック (1MB制限)
if (formattedResult.length > 1000000) {
throw new Error(
"レスポンスサイズが最大長を超えています。例えば以下のような方法でクエリの結果を減らしてください:\n" +
"1. 'size'パラメータを小さくする\n" +
"2. より具体的なフィルター条件を使用する\n" +
"3. 集計バケットの数を制限する"
);
}
return {
content: [
{
type: "text",
text: formattedResult,
},
],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${
error.errors.map((e) =>
`${e.path.join(".")}: ${e.message}`
).join(", ")
}`,
);
}
// エラーメッセージをそのまま返す
throw error;
}
} else {
throw new Error(`Unknown tool: ${name}`);
}
});
// サーバーの起動
async function main() {
// Elasticsearchの接続確認
try {
await client.ping();
console.error("Successfully connected to Elasticsearch");
} catch (error) {
console.error("Failed to connect to Elasticsearch:", error);
process.exit(1);
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Elasticsearch Keylogger MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
きっちりとビルド用に script も定義してくれている。。ありがたや😂
...
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
...
しっかりビルドも通って dist/server.js
が作成できた。
クライアントの設定
今回クライアントには Claude Desktop を利用する。
ここら辺のドキュメントを参考に設定ファイルに実装したサーバーを追加。
{
"allowDevTools": true,
"mcpServers": {
"mcp-elasticsearch-keylogger-server": {
"command": "/Users/xxx/.asdf/shims/node",
"args": [
"/Users/kawasaki/dev/github.com/momiom/keylogger-analytics/dist/server.js"
]
}
}
}
command
は実行可能であればなんでも良いよう。
例えば Golang でサーバーを実装したらビルドした実行ファイルを記載すれば良い。
自分の環境では asdf のパスが通らなかったのかエラーになったのでフルパスを指定している。

準備が整ったので Claude Desktop を起動してデータの取得を依頼してみよう。
なお設定に不備があったり、先述のように Node の実行に失敗すると起動時にエラーが出る。
デバッグ方法は Debugging - Model Context Protocol に記載がある。
適当な依頼をしてみる。
キーロガーのデータを元に、アルファベットの入力を利用頻度の高い順に React を用いて棒グラフで表わしてください。
実行すると Allow tool from "mcp xxx"?
と聞かれるので許可。
結果:
ちゃんと取得できてる。。!👏
View Result from query-keylogger from mcp-elasticsearch-keylogger-server (local)
で Elasticsearch のクエリをサーバーに渡していることがわかる。
サーバー側では tools の定義で 『1件のデータを取得し、実際のデータ内容を確認してください。』としているのでその通りに実行されている。
(ただし、その前にマッピングの確認も指示しているがそれは無視された。)
そして安定の Artifacts 出力。きっちりグラフを作成してくれた。
ただし、縦軸のラベルが省略されていたので、そこだけ修正依頼した。
import React from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
const AlphabetFrequencyChart = () => {
const data = [
{ letter: 'a', count: 4413 },
{ letter: 'i', count: 3607 },
{ letter: 'o', count: 3443 },
{ letter: 'n', count: 3013 },
{ letter: 'u', count: 2942 },
{ letter: 'e', count: 2772 },
{ letter: 's', count: 2435 },
{ letter: 't', count: 2430 },
{ letter: 'k', count: 2214 },
{ letter: 'r', count: 1524 },
{ letter: 'd', count: 1386 },
{ letter: 'c', count: 929 },
{ letter: 'm', count: 882 },
{ letter: 'y', count: 822 },
{ letter: 'v', count: 740 },
{ letter: 'g', count: 739 },
{ letter: 'j', count: 699 },
{ letter: 'w', count: 692 },
{ letter: 'h', count: 610 },
{ letter: 'l', count: 519 },
{ letter: 'z', count: 446 },
{ letter: 'p', count: 433 },
{ letter: 'b', count: 394 },
{ letter: 'f', count: 307 }
].sort((a, b) => b.count - a.count);
return (
<Card className="w-full">
<CardHeader>
<CardTitle>アルファベット入力頻度分布</CardTitle>
</CardHeader>
<CardContent>
<div className="h-96">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="letter" type="category" width={60} interval={0} />
<Tooltip
formatter={(value) => [`${value}回`, '入力回数']}
labelFormatter={(label) => `${label}キー`}
/>
<Bar
dataKey="count"
fill="#4f46e5"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
};
export default AlphabetFrequencyChart;

以下にはサンプルがたくさん載っていて、 Claude Desktop からの利用であれば設定ファイルの command に npx を指定して直接実行することもできる。

puppeteer の自動操作は熱いな。。🤩
🔥 Claude MCP + Puppeteer で始めるAI駆動のブラウザ自動化 #生成AI - Qiita 等にあるように、自然言語でブラウザ操作できる。
最近話題の Browser Use は API キーが必要だけど、上記なら Claude の契約があれば従量課金なし安心して使える。

今回は Claude Desktop を利用したが、例えば GenAIScript を使えばプロンプト自体もJavaScript でプログラム的に構築することもできるので、並列化してマルチエージェントでタスクを進めることもできるのでは。
以下のように多くのクライアントが対応しており、今後さらに拡大してくはず。
Example Clients - Model Context Protocol
ロードマップも楽しげなことがたくさん書いてあって楽しみだー!😊
Roadmap - Model Context Protocol