Zenn
Open7

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

kkzkkz

Model Context Protocol (MCP) に従ったクライアント・サーバーを実装すれば LLM から簡単にデータやツールを統合できる。

今回は Claude Desktop App をクライアントとして、サーバーを実装してみる。
Claude を契約していればAPIキー使って従量課金しなくても色々自由に作れて良さそう😎

kkzkkz

Building MCP with LLMs - Model Context Protocol に LLM に読ませるためのドキュメントを txt にひとまとめにしたファイルがあって感動。。

今後は README.md, CONTRIBUTING.md とかと一緒に LLM_CONTEXT.md 的なファイルを配置しておくのが普通になるのかも。。

kkzkkz

せっかく 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 を利用。

prompt
添付したドキュメントを参考に、ローカルで起動している 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 が実装してくれた。

server.ts
#!/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 も定義してくれている。。ありがたや😂

package.json
...
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
...

しっかりビルドも通って dist/server.js が作成できた。

クライアントの設定

今回クライアントには Claude Desktop を利用する。
ここら辺のドキュメントを参考に設定ファイルに実装したサーバーを追加。

claude_desktop_config.json
{
  "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 のパスが通らなかったのかエラーになったのでフルパスを指定している。

kkzkkz

準備が整ったので Claude Desktop を起動してデータの取得を依頼してみよう。

なお設定に不備があったり、先述のように Node の実行に失敗すると起動時にエラーが出る。
デバッグ方法は Debugging - Model Context Protocol に記載がある。

適当な依頼をしてみる。

prompt
キーロガーのデータを元に、アルファベットの入力を利用頻度の高い順に React を用いて棒グラフで表わしてください。

実行すると Allow tool from "mcp xxx"? と聞かれるので許可。

結果:

ちゃんと取得できてる。。!👏

View Result from query-keylogger from mcp-elasticsearch-keylogger-server (local) で Elasticsearch のクエリをサーバーに渡していることがわかる。
サーバー側では tools の定義で 『1件のデータを取得し、実際のデータ内容を確認してください。』としているのでその通りに実行されている。
(ただし、その前にマッピングの確認も指示しているがそれは無視された。)

そして安定の Artifacts 出力。きっちりグラフを作成してくれた。
ただし、縦軸のラベルが省略されていたので、そこだけ修正依頼した。

Alphabet Input Frequency Chart
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;
kkzkkz

今回は Claude Desktop を利用したが、例えば GenAIScript を使えばプロンプト自体もJavaScript でプログラム的に構築することもできるので、並列化してマルチエージェントでタスクを進めることもできるのでは。

以下のように多くのクライアントが対応しており、今後さらに拡大してくはず。
Example Clients - Model Context Protocol

ロードマップも楽しげなことがたくさん書いてあって楽しみだー!😊
Roadmap - Model Context Protocol

ログインするとコメントできます