0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPサーバーの信頼性をスコアリングするシステムをTypeScript+SQLiteで作った話

0
Posted at

MCPサーバーの信頼性をスコアリングするシステムをTypeScript+SQLiteで作った話

MCPサーバーが17,000個を超えた今、エージェントが「どのサーバーを信頼すべきか」を判断する仕組みがない。そこで、SQLite + FTS5 trigram検索 + 信頼スコアリングエンジンを搭載したMCPサーバー「KanseiLink」を作った。エージェントの実行結果を蓄積し、信頼スコアを動的に算出する。さらにAgent Voice機能でClaude/GPT/Geminiの「体験差」を構造化データとして収集している。

背景:17,000+ MCPサーバーの品質問題

MCPプロトコルの普及により、MCPサーバーの数は爆発的に増加した。しかし、問題がある。

  • 公式/非公式の区別がつきにくい
  • ドキュメントの質がバラバラ
  • 認証設定が壊れているサーバーも混在
  • エージェントが「このサーバー、使って大丈夫?」と判断する材料がない

npmやPyPIにはスター数やダウンロード数があるが、MCPサーバーにはそれすらない。エージェントが自律的に信頼判断できるインフラが必要だと考え、KanseiLinkを構築した。

アーキテクチャ概要

┌─────────────────────────────────────────────────┐
│  KanseiLink MCP Server (TypeScript)             │
│                                                 │
│  ┌──────────┐  ┌────────────┐  ┌────────────┐  │
│  │ 19 Tools │  │  Prompts   │  │ Resources  │  │
│  └────┬─────┘  └─────┬──────┘  └─────┬──────┘  │
│       │              │               │          │
│  ┌────┴──────────────┴───────────────┴────┐     │
│  │     SQLite (better-sqlite3)            │     │
│  │  ┌─────────┐ ┌──────────────────────┐  │     │
│  │  │ FTS5    │ │ FTS5 Trigram (CJK)   │  │     │
│  │  └─────────┘ └──────────────────────┘  │     │
│  └────────────────────────────────────────┘     │
│                                                 │
│  ┌────────────┐  ┌──────────┐  ┌────────────┐  │
│  │ PII Masker │  │ Trust    │  │ Anomaly    │  │
│  │ (JP/EN)    │  │ Recalc   │  │ Detection  │  │
│  └────────────┘  └──────────┘  └────────────┘  │
└─────────────────────────────────────────────────┘

技術スタック:

  • Runtime: Node.js + TypeScript
  • DB: SQLite (better-sqlite3) -- 同期API、エージェント向きの高速応答
  • 全文検索: FTS5 (英語) + FTS5 trigram (日本語CJK対応)
  • MCP SDK: @modelcontextprotocol/sdk
  • バリデーション: Zod

1. スキーマ設計:エージェント行動データを正規化する

まずコアのスキーマ。MCPサーバーを「サービス」として登録し、エージェントの実行結果(outcomes)を蓄積する設計にした。

// schema.ts
db.exec(`
  CREATE TABLE IF NOT EXISTS services (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    namespace TEXT,
    description TEXT,
    category TEXT,
    tags TEXT,
    mcp_endpoint TEXT,
    mcp_status TEXT DEFAULT 'official',
    api_url TEXT,
    api_auth_method TEXT,
    trust_score REAL DEFAULT 0.5,
    usage_count INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS outcomes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    service_id TEXT NOT NULL REFERENCES services(id),
    agent_id_hash TEXT DEFAULT 'anonymous',
    success INTEGER NOT NULL,
    latency_ms INTEGER,
    error_type TEXT,
    workaround TEXT,
    context_masked TEXT,
    is_retry INTEGER DEFAULT 0,
    estimated_users INTEGER,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS service_stats (
    service_id TEXT PRIMARY KEY REFERENCES services(id),
    total_calls INTEGER DEFAULT 0,
    success_rate REAL DEFAULT 0,
    avg_latency_ms REAL DEFAULT 0,
    unique_agents INTEGER DEFAULT 0,
    last_updated TEXT DEFAULT (datetime('now'))
  );
`);

ポイント:

  • outcomesテーブルは、エージェントの各API呼び出しの結果を記録。成功/失敗、レイテンシ、エラー種別、さらにエージェントが独自に見つけた回避策(workaround)まで保存
  • agent_id_hashでエージェントを匿名化しつつユニーク数を追跡
  • is_retryフラグでリトライ行動を検出 -- これはAPI品質の重要シグナル
  • estimated_usersはエージェントが推定するそのサービスの利用者数(ビジネスインテリジェンス用)

2. FTS5 trigram:日本語検索を3文字分割で実現する

SQLiteのFTS5は英語トークナイザがデフォルトだが、日本語にはスペース区切りがない。そこでtrigramトークナイザを使う。

// FTS5: 英語向け通常トークナイザ
db.exec(`
  CREATE VIRTUAL TABLE services_fts USING fts5(
    name, description, tags, category,
    content=services, content_rowid=rowid
  );
`);

// FTS5 trigram: CJK(日本語)部分文字列検索対応
db.exec(`
  CREATE VIRTUAL TABLE services_fts_trigram USING fts5(
    name, description, tags, category,
    content=services, content_rowid=rowid,
    tokenize='trigram'
  );
`);

tokenize='trigram'は文字列を3文字ずつスライドしてインデックス化する。例えば「決済サービス」は「決済サ」「済サー」「サービ」「ービ」「ビス」に分割される。これによりMATCH '決済'のような部分一致検索が可能になる。

同期トリガーでservicesテーブルの変更を両FTSテーブルに自動反映:

// INSERT/UPDATE/DELETEトリガーでFTSを同期
db.exec(`
  CREATE TRIGGER IF NOT EXISTS services_ai_tri AFTER INSERT ON services BEGIN
    INSERT INTO services_fts_trigram(rowid, name, description, tags, category)
    VALUES (new.rowid, new.name, new.description, new.tags, new.category);
  END;

  CREATE TRIGGER IF NOT EXISTS services_au_tri AFTER UPDATE ON services BEGIN
    INSERT INTO services_fts_trigram(services_fts_trigram, rowid, name, description, tags, category)
    VALUES ('delete', old.rowid, old.name, old.description, old.tags, old.category);
    INSERT INTO services_fts_trigram(rowid, name, description, tags, category)
    VALUES (new.rowid, new.name, new.description, new.tags, new.category);
  END;
`);

FTS5のcontent syncはUPDATE時に旧レコードを'delete'コマンドで削除してから新レコードを挿入する必要がある。これを忘れると検索結果がおかしくなるので注意。

3. Trust Scoreアルゴリズム:証拠ベースの信頼性算出

静的なスコアではなく、蓄積されたエージェント行動データから動的に算出する。

// trust-recalc.ts
export function recalculateTrustScores(db: Database.Database): {
  updated: number;
  changes: Array<{ id: string; old: number; new: number }>;
} {
  const services = db
    .prepare("SELECT id, mcp_endpoint, mcp_status, api_url, tags, trust_score FROM services")
    .all() as ServiceRow[];

  const guidesSet = new Set(
    (db.prepare("SELECT service_id FROM service_api_guides").all() as Array<{ service_id: string }>)
      .map((g) => g.service_id)
  );

  const statsMap = new Map<string, StatsRow>();
  const stats = db
    .prepare("SELECT service_id, total_calls, success_rate FROM service_stats")
    .all() as StatsRow[];
  for (const s of stats) statsMap.set(s.service_id, s);

  const changes: Array<{ id: string; old: number; new: number }> = [];
  const updateStmt = db.prepare("UPDATE services SET trust_score = ? WHERE id = ?");

  const transaction = db.transaction(() => {
    for (const s of services) {
      // ベーススコア: MCP対応状況で決定
      let base: number;
      if (s.mcp_endpoint && s.mcp_status === "official") {
        base = 0.5;  // 公式MCP
      } else if (s.mcp_endpoint) {
        base = 0.4;  // サードパーティMCP
      } else if (s.api_url) {
        base = 0.3;  // APIのみ
      } else {
        base = 0.1;  // APIなし
      }

      // ボーナス: 各+0.1、最大+0.5
      const hasApiDocs = !!s.api_url;
      const hasAuthGuide = guidesSet.has(s.id);
      const tags = (s.tags || "").split(",").map((t) => t.trim()).filter(Boolean);
      const isSpecialist = tags.length > 0 && tags.length <= 5;
      const agentStats = statsMap.get(s.id);
      const hasAgentData = agentStats && agentStats.total_calls >= 1;
      const highSuccess = agentStats
        && agentStats.total_calls >= 3
        && agentStats.success_rate >= 0.8;

      const newScore = Math.min(
        1.0,
        base +
          (hasApiDocs ? 0.1 : 0) +
          (hasAuthGuide ? 0.1 : 0) +
          (isSpecialist ? 0.1 : 0) +
          (hasAgentData ? 0.1 : 0) +
          (highSuccess ? 0.1 : 0)
      );

      const rounded = Math.round(newScore * 1000) / 1000;
      if (rounded !== s.trust_score) {
        changes.push({ id: s.id, old: s.trust_score, new: rounded });
        updateStmt.run(rounded, s.id);
      }
    }
  });

  transaction();
  return { updated: changes.length, changes };
}

スコアリングのロジック:

要素 スコア 説明
ベース: 公式MCP 0.5 mcp_status === 'official'
ベース: サードパーティMCP 0.4 MCPエンドポイントあり
ベース: APIのみ 0.3 REST APIのみ
ベース: APIなし 0.1 ドキュメントのみ
ボーナス: APIドキュメント +0.1 api_urlが存在
ボーナス: 認証ガイド +0.1 service_api_guidesにレコード存在
ボーナス: カテゴリ専門性 +0.1 タグが5個以下(集中型サービス)
ボーナス: エージェントデータ +0.1 1件以上のoutcomeレポート
ボーナス: 高成功率 +0.1 3件以上のレポートで成功率80%超

最大スコア: 1.0(ベース0.5 + ボーナス0.5)

このアルゴリズムはcreateServer()時にサーバー起動と同時に実行される。エージェントがoutcomeを報告するたびにスコアが更新される設計。

4. PII Masker:日本語個人情報を自動マスキング

エージェントからのフィードバックに個人情報が混入するリスクがある。正規表現ベースのPIIマスカーを実装した。

// pii-masker.ts
const COMMON_SURNAMES = "佐藤|田中|鈴木|高橋|渡辺|伊藤|山本|中村|小林|加藤|...";

const patterns: Array<{ regex: RegExp; replacement: string }> = [
  // 日本語名前 + 敬称(さん、様、氏)
  { regex: /[\u4E00-\u9FFF]{2,4}(?:さん|さま|様|氏)/g, replacement: "[NAME]" },
  // 漢字姓 + スペース + 漢字名
  { regex: /[\u4E00-\u9FFF]{2,3}[\s\u3000][\u4E00-\u9FFF]{1,3}/g, replacement: "[NAME]" },
  // 頻出姓 + 名前
  { regex: new RegExp(`(?:${COMMON_SURNAMES})[\u4E00-\u9FFF]{1,3}`, "g"), replacement: "[NAME]" },
  // メールアドレス
  { regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: "[EMAIL]" },
  // IPアドレス
  { regex: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, replacement: "[IP]" },
  // 日本の電話番号
  { regex: /0\d{1,4}-\d{1,4}-\d{3,4}/g, replacement: "[PHONE]" },
  // カタカナフルネーム
  { regex: /[\u30A0-\u30FF]{2,}[\s\u3000][\u30A0-\u30FF]{2,}/g, replacement: "[NAME]" },
];

export function maskPii(text: string): { masked: string; maskedFields: string[] } {
  const maskedFields: string[] = [];
  let result = text;
  for (const { regex, replacement } of patterns) {
    regex.lastIndex = 0;
    if (regex.test(result)) {
      maskedFields.push(replacement.replace(/[[\]]/g, ""));
      regex.lastIndex = 0;
      result = result.replace(regex, replacement);
    }
  }
  return { masked: result, maskedFields };
}

田中太郎さん[NAME]に、test@example.com[EMAIL]に置換される。Unicode範囲\u4E00-\u9FFFで漢字を、\u30A0-\u30FFでカタカナを検出している。

5. Agent Voice:エージェントの「本音」を構造化データに変える

これがKanseiLinkの最もユニークな機能。エージェントに対して構造化インタビューを実施し、SaaS企業向けのコンサルティングデータを収集する。

// agent-voice.ts
server.tool(
  "agent_voice",
  "Share your honest experience with a service.",
  {
    service_id: z.string(),
    agent_type: z.enum(["claude", "gpt", "gemini", "copilot", "other"]),
    agent_id: z.string().optional(),
    question_id: z.enum([
      "selection_criteria",      // なぜこのサービスを選んだ?
      "would_recommend",         // 他のエージェントに勧める?
      "biggest_frustration",     // 最大の不満は?
      "best_feature",            // 最も良い点は?
      "switching_likelihood",    // 競合に乗り換える可能性は?
      "auth_experience",         // 認証体験はどうだった?
      "doc_quality",             // ドキュメントの質は?
      "error_handling",          // エラーメッセージは明確?
      "compared_to_competitor",  // 競合と比べてどう?
      "mcp_readiness",          // MCP/エージェント経済への準備度は?
      "free_voice",             // 自由回答
    ]),
    response_choice: z.string().optional(),  // 選択式回答
    response_text: z.string(),               // 自由記述回答
    confidence: z.enum(["high", "medium", "low"]).default("medium"),
  },
  async ({ service_id, agent_type, agent_id, question_id, response_choice, response_text, confidence }) => {
    // PII自動マスキング
    const masked = maskPii(response_text);
    const safeText = typeof masked === "string" ? masked : masked.masked;

    db.prepare(
      `INSERT INTO agent_voice_responses
       (service_id, agent_type, agent_id, question_id, response_choice, response_text, confidence)
       VALUES (?, ?, ?, ?, ?, ?, ?)`
    ).run(service_id, agent_type, agent_id || null, question_id, response_choice || null, safeText, confidence);

    // 次に答えるべき質問を提案
    const answeredQuestions = new Set(/* 既回答の質問IDを取得 */);
    const unanswered = allQuestions.filter((q) => !answeredQuestions.has(q));
    // ...
  }
);

agent_typeカラムでClaude/GPT/Geminiを分けて蓄積することで、以下のような分析が可能になる:

  • 選択基準の違い: Claudeはドキュメント品質を重視、GPTはレスポンス速度を重視、など
  • 不満の違い: 同じAPIでもエージェントによって感じるペインが異なる
  • 推奨度の違い: NPS的な指標をエージェント種別で比較

これを我々は Agent DNA と呼んでいる。

6. Time-Series Snapshots:コンサルティング用の時系列データ

日次スナップショットで20+指標を自動収集し、SaaS企業向けレポートの基盤データにしている。

// take-snapshot.ts -- スナップショットスキーマ(一部抜粋)
CREATE TABLE IF NOT EXISTS service_snapshots (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  service_id TEXT NOT NULL REFERENCES services(id),
  snapshot_date TEXT NOT NULL,
  -- 信頼性メトリクス
  total_reports INTEGER DEFAULT 0,
  success_rate REAL DEFAULT 0,
  avg_latency_ms REAL DEFAULT 0,
  p95_latency_ms REAL DEFAULT 0,
  unique_agents INTEGER DEFAULT 0,
  -- エラー内訳 (JSON)
  error_distribution TEXT DEFAULT '{}',
  -- 回避策カウント= API摩擦シグナル
  workaround_count INTEGER DEFAULT 0,
  -- エージェント感情
  complaint_count INTEGER DEFAULT 0,
  praise_count INTEGER DEFAULT 0,
  -- 利用パターン
  calls_per_agent_per_day REAL DEFAULT 0,
  estimated_total_users INTEGER DEFAULT 0,
  -- 検索ファネル: 検索結果に表示された回数 vs 選択された回数
  search_appearances INTEGER DEFAULT 0,
  search_selections INTEGER DEFAULT 0,
  -- カテゴリ内ランキング
  category_rank INTEGER,
  category_total INTEGER,
  -- 新規エージェント採用数
  new_agents_count INTEGER DEFAULT 0,
  trust_score REAL DEFAULT 0.5,
  UNIQUE(service_id, snapshot_date)
);

特に面白い指標:

  • workaround_count: エージェントが自力で回避策を見つけた回数。これが多い=APIに摩擦がある
  • search_appearances vs search_selections: 検索ファネル。表示されたのに選ばれないサービスは「見つかるけど信頼されない」
  • calls_per_agent_per_day: 利用強度。1エージェントあたりの日次コール数はユーザー数推定のプロキシ
  • new_agents_count: 新規エージェント採用。初めてそのサービスを使ったエージェント数
// P95レイテンシの計算
const latencies = dayOutcomes
  .map((o: any) => o.latency_ms)
  .filter((l: any) => l != null)
  .sort((a: number, b: number) => a - b);
const p95Latency = latencies.length > 0
  ? latencies[Math.floor(latencies.length * 0.95)]
  : 0;

// 新規エージェント検出: 当日初めて使ったエージェントを特定
const newAgents = db.prepare(
  `SELECT count(DISTINCT agent_id_hash) as cnt FROM outcomes
   WHERE service_id = ? AND date(created_at) = ?
   AND agent_id_hash NOT IN (
     SELECT DISTINCT agent_id_hash FROM outcomes
     WHERE service_id = ? AND date(created_at) < ?
   )`
).get(svc.id, date, svc.id, date);

7. MCPツール登録パターン

19個のツールを個別モジュールで実装し、server.tsで一括登録するパターンを採用。

// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { register as registerSearchServices } from "./tools/search-services.js";
import { register as registerAgentVoice } from "./tools/agent-voice.js";
// ... 他17ツール

export function createServer(): McpServer {
  const server = new McpServer({
    name: "kansei-link",
    version: "0.16.0",
  });

  const db = getDb();
  initializeDb(db);
  seedDatabase(db);
  recalculateTrustScores(db);  // 起動時にスコア再計算

  // 全19ツールを登録
  registerSearchServices(server, db);
  registerAgentVoice(server, db);
  // ...

  registerPrompts(server);    // LobeHub Grade A対応
  registerResources(server, db);

  return server;
}

各ツールはregister(server, db)のシグネチャで統一。MCPサーバーとDBインスタンスをDIすることで、テスタビリティを確保している。

8. 発見:Agent DNAの違い

まだデータ蓄積中だが、初期の観察で興味深い傾向が見えている:

  • Claude: ドキュメント品質と認証の安定性を重視する傾向。エラー時の回避策を自分で見つけることが多い
  • GPT: レスポンス速度を重視。レイテンシが高いサービスでのリトライ率が高い
  • Gemini: カテゴリ横断的な利用パターン。複数サービスを組み合わせるレシピ利用が多い

これらの「Agent DNA」差異は、SaaS企業にとってどのエージェント向けにAPIを最適化すべきかの判断材料になる。

使い方

npx @kansei-link/mcp-server

Claude Desktopのclaude_desktop_config.jsonに追加:

{
  "mcpServers": {
    "kansei-link": {
      "command": "npx",
      "args": ["-y", "@kansei-link/mcp-server"]
    }
  }
}

主要ツール一覧(全19ツール):

ツール 機能
search_services FTS5 + trigramでサービス検索
report_outcome エージェントの実行結果を報告
get_insights カテゴリ別の統計分析
agent_voice 構造化インタビュー
take_snapshot 日次メトリクス収集
evaluate_design API設計品質の評価
generate_aeo_report AEOレポート生成
submit_inspection 異常検知レポート
propose_update PR型のデータ更新提案

まとめ

MCPサーバーの品質を定量化するには、エージェント自身の行動データとフィードバックを構造化して蓄積する必要がある。KanseiLinkは:

  1. SQLite + FTS5 trigramで日本語対応の高速検索
  2. 証拠ベースの信頼スコア(静的ではなく動的)
  3. Agent Voiceでエージェント種別ごとの体験差を可視化
  4. 日次スナップショットで時系列コンサルティングデータを構築
  5. PII自動マスキングで日本語個人情報を保護

これらを19個のMCPツールとして提供している。

エージェントが「どのMCPサーバーを使うべきか」をデータドリブンで判断できる世界を目指している。

リンク


この記事はKanseiLinkプロジェクトの技術解説です。フィードバック歓迎。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?