T-CREATOR

MCP サーバー で社内ナレッジ検索チャットを構築:権限制御・要約・根拠表示の実装パターン

MCP サーバー で社内ナレッジ検索チャットを構築:権限制御・要約・根拠表示の実装パターン

社内に散らばる膨大なナレッジを効率的に活用したいと考えたことはありませんか。Anthropic が提供する Model Context Protocol(MCP)を使えば、社内ドキュメントを横断検索し、要約結果と根拠を示すチャットボットを構築できます。本記事では、実務で必須となる権限制御、回答の要約、根拠表示の 3 つの実装パターンを詳しく解説していきます。

実際のコードとともに、セキュアで実用的なナレッジ検索システムの構築方法をご紹介しますので、ぜひ最後までお付き合いください。

背景

社内ナレッジ管理の現状と課題

企業では日々、議事録・技術ドキュメント・プロジェクト資料など、多様なナレッジが蓄積されます。しかし、これらの情報は複数のツール(Confluence、Notion、Google Drive など)に分散しており、必要な情報を探すのに時間がかかる状況です。

加えて、部門や役職ごとにアクセス権限が異なるため、誰もが全ての情報にアクセスできるわけではありません。こうした環境では、ナレッジの活用が進まず、「どこに情報があるか分からない」「同じ質問を何度もする」といった非効率が発生しています。

MCP(Model Context Protocol)とは

MCP は、Anthropic が開発した LLM とデータソースを接続するための標準プロトコル です。従来、各 LLM ベンダーや用途ごとに独自の統合方法が必要でしたが、MCP を使うことで統一的なインターフェースでデータアクセスが可能になります。

MCP サーバーは以下の要素で構成されます。

mermaidflowchart LR
  client["Claude などの<br/>LLM クライアント"] -->|リクエスト| mcp["MCP サーバー"]
  mcp -->|データ取得| ds1["Confluence"]
  mcp -->|データ取得| ds2["Google Drive"]
  mcp -->|データ取得| ds3["社内 DB"]
  mcp -->|レスポンス| client

図の要点:MCP サーバーが複数のデータソースを抽象化し、LLM クライアント側はプロトコルを意識せずに情報取得できます。

MCP を導入すると、以下のメリットが得られます。

#メリット説明
1統一インターフェース異なるデータソースを同じプロトコルでアクセス可能
2セキュリティ制御サーバー側で認証・認可を一元管理
3柔軟な拡張性新しいデータソースを追加しやすい
4メンテナンス性プロトコル変更時の影響範囲が限定的

課題

社内ナレッジ検索における 3 つの課題

社内ナレッジ検索チャットを構築する際、以下 3 つの課題に直面します。

課題 1:権限制御の実装

社内ドキュメントには機密情報や部門限定の資料が含まれます。チャットボットが誤って権限外の情報を返してしまうと、情報漏洩のリスクが生じるでしょう。

ユーザーごとに「閲覧可能なドキュメント」を判定し、権限外の情報は検索結果から除外する仕組みが必要です。

課題 2:要約機能の実装

複数のドキュメントから情報を収集した場合、そのまま全文を返すと冗長になります。ユーザーは「端的に知りたい」と考えているため、検索結果を適切に要約して提示することが求められます。

LLM の要約能力を活用しつつ、重要な情報を欠落させない工夫が必要でしょう。

課題 3:根拠の提示

要約された回答だけでは「本当に正しいのか」を判断できません。特にビジネス利用では、情報源(どのドキュメントのどの部分に記載されているか)を明示することで、ユーザーが元資料を確認できる状態にする必要があります。

以下の図は、これら 3 つの課題がどのように関連しているかを示しています。

mermaidflowchart TD
  user["ユーザー<br/>「○○について教えて」"] -->|検索| search["検索処理"]
  search --> auth["権限チェック"]
  auth -->|権限あり| filter["絞り込み済<br/>ドキュメント"]
  auth -->|権限なし| reject["アクセス拒否"]
  filter --> summarize["要約生成"]
  summarize --> cite["根拠情報付与"]
  cite --> result["回答+出典リンク"]
  result --> user

図の補足:検索から回答までの流れにおいて、権限チェック・要約・根拠提示が段階的に行われます。

解決策

MCP サーバーによる統合アーキテクチャ

上記の課題を解決するため、MCP サーバーを中心としたアーキテクチャを採用します。以下の構成により、権限制御・要約・根拠表示をシームレスに実現できるでしょう。

mermaidflowchart TB
  subgraph Frontend["フロントエンド"]
    chat["チャット UI"]
  end

  subgraph MCPServer["MCP サーバー"]
    auth_layer["認証・認可層"]
    search_tool["検索ツール"]
    summary_tool["要約ツール"]
    citation_tool["引用管理"]
  end

  subgraph DataSources["データソース"]
    confluence["Confluence"]
    notion["Notion"]
    gdrive["Google Drive"]
  end

  chat -->|ユーザークエリ| auth_layer
  auth_layer -->|トークン検証| search_tool
  search_tool --> confluence
  search_tool --> notion
  search_tool --> gdrive
  confluence --> summary_tool
  notion --> summary_tool
  gdrive --> summary_tool
  summary_tool --> citation_tool
  citation_tool -->|回答+出典| chat

図で理解できる要点

  • 認証・認可層でユーザー権限を管理
  • 検索ツールが複数データソースを並列検索
  • 要約ツールと引用管理で回答品質を担保

実装の全体方針

本記事では、以下の技術スタックを使用します。

#技術要素用途
1TypeScriptMCP サーバーの実装言語
2Node.jsサーバー実行環境
3@modelcontextprotocol/sdkMCP SDK(公式)
4Anthropic Claude API要約生成に使用
5JWT認証トークン管理

実装は以下の 3 ステップで進めます。

  1. 権限制御層の実装:JWT トークンを用いたユーザー認証とドキュメントアクセス権限の判定
  2. 検索・要約ツールの実装:MCP ツールとして検索と要約機能を定義
  3. 根拠表示の実装:引用情報(citation)を含むレスポンス形式の構築

それでは、具体的な実装コードを見ていきましょう。

具体例

ステップ 1:プロジェクトのセットアップ

まずは、プロジェクトの初期化と必要なパッケージのインストールから始めます。

bashmkdir knowledge-search-mcp
cd knowledge-search-mcp
yarn init -y

次に、必要な依存パッケージをインストールします。

bashyarn add @modelcontextprotocol/sdk
yarn add @anthropic-ai/sdk
yarn add jsonwebtoken
yarn add dotenv
yarn add -D @types/node @types/jsonwebtoken typescript

TypeScript の設定ファイルを作成します。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

この設定により、厳密な型チェックと最新の JavaScript 機能が利用可能になります。

ステップ 2:環境変数の設定

認証情報や API キーを環境変数で管理します。プロジェクトルートに .env ファイルを作成しましょう。

bash# Anthropic API キー
ANTHROPIC_API_KEY=your_api_key_here

# JWT シークレットキー(本番環境では強固なキーを使用)
JWT_SECRET=your_jwt_secret_here

# MCP サーバーのポート
MCP_SERVER_PORT=3000

これらの環境変数を読み込むための設定ファイルを作成します。

typescript// src/config/env.ts
import dotenv from 'dotenv';

// 環境変数を読み込み
dotenv.config();

export const config = {
  anthropicApiKey: process.env.ANTHROPIC_API_KEY || '',
  jwtSecret: process.env.JWT_SECRET || '',
  mcpServerPort: parseInt(
    process.env.MCP_SERVER_PORT || '3000',
    10
  ),
};

ステップ 3:型定義の作成

TypeScript の型安全性を活かすため、必要な型定義を作成します。

typescript// src/types/index.ts

// ユーザー情報の型定義
export interface User {
  userId: string;
  email: string;
  roles: string[]; // 例: ['engineer', 'manager']
  departments: string[]; // 例: ['development', 'product']
}

// ドキュメントの型定義
export interface Document {
  id: string;
  title: string;
  content: string;
  source: 'confluence' | 'notion' | 'gdrive';
  url: string;
  allowedRoles: string[];
  allowedDepartments: string[];
  createdAt: Date;
  updatedAt: Date;
}

検索結果と引用情報の型も定義します。

typescript// src/types/index.ts(続き)

// 検索結果の型定義
export interface SearchResult {
  documents: Document[];
  totalCount: number;
}

// 引用情報の型定義
export interface Citation {
  documentId: string;
  documentTitle: string;
  url: string;
  excerpt: string; // 引用部分の抜粋
  relevanceScore: number; // 関連度スコア
}

// 要約結果の型定義
export interface SummaryResponse {
  summary: string;
  citations: Citation[];
  totalDocuments: number;
}

ステップ 4:認証・認可層の実装

JWT トークンを用いた認証機能を実装します。この層が全てのリクエストで最初に実行されます。

typescript// src/auth/jwt.ts
import jwt from 'jsonwebtoken';
import { config } from '../config/env';
import { User } from '../types';

// JWT トークンを検証してユーザー情報を取得
export function verifyToken(token: string): User {
  try {
    const decoded = jwt.verify(
      token,
      config.jwtSecret
    ) as User;
    return decoded;
  } catch (error) {
    throw new Error(
      'Invalid token: 認証トークンが無効です'
    );
  }
}

トークン生成機能も用意します(テスト用途)。

typescript// src/auth/jwt.ts(続き)

// JWT トークンを生成(開発・テスト用)
export function generateToken(user: User): string {
  return jwt.sign(
    {
      userId: user.userId,
      email: user.email,
      roles: user.roles,
      departments: user.departments,
    },
    config.jwtSecret,
    { expiresIn: '24h' }
  );
}

次に、ドキュメントへのアクセス権限を判定する機能を実装します。

typescript// src/auth/permission.ts
import { User, Document } from '../types';

// ユーザーがドキュメントにアクセス可能かチェック
export function canAccessDocument(
  user: User,
  document: Document
): boolean {
  // 役職でのチェック
  const hasRole = document.allowedRoles.some((role) =>
    user.roles.includes(role)
  );

  // 部門でのチェック
  const hasDepartment = document.allowedDepartments.some(
    (dept) => user.departments.includes(dept)
  );

  return hasRole || hasDepartment;
}

複数のドキュメントをフィルタリングする関数も追加します。

typescript// src/auth/permission.ts(続き)

// ユーザーがアクセス可能なドキュメントのみをフィルタリング
export function filterAccessibleDocuments(
  user: User,
  documents: Document[]
): Document[] {
  return documents.filter((doc) =>
    canAccessDocument(user, doc)
  );
}

ステップ 5:データソース接続の実装

実際のデータソースへの接続を実装します。ここでは Confluence を例に示しますが、他のデータソースも同様のパターンで実装可能です。

typescript// src/datasources/confluence.ts
import { Document } from '../types';

// Confluence API クライアント(簡略版)
export class ConfluenceClient {
  private baseUrl: string;
  private apiToken: string;

  constructor(baseUrl: string, apiToken: string) {
    this.baseUrl = baseUrl;
    this.apiToken = apiToken;
  }

  // キーワードでドキュメントを検索
  async search(query: string): Promise<Document[]> {
    // 実際には Confluence REST API を呼び出し
    // この例では簡略化のためモックデータを返す
    return this.mockSearch(query);
  }
}

モックデータを返す実装を追加します。

typescript// src/datasources/confluence.ts(続き)

  // モックデータを返す(実装例)
  private mockSearch(query: string): Document[] {
    return [
      {
        id: 'conf-001',
        title: 'API 設計ガイドライン',
        content: 'RESTful API の設計原則について...',
        source: 'confluence',
        url: 'https://confluence.example.com/pages/001',
        allowedRoles: ['engineer', 'architect'],
        allowedDepartments: ['development'],
        createdAt: new Date('2024-01-15'),
        updatedAt: new Date('2024-03-20'),
      },
      // 他のドキュメント...
    ];
  }

同様に、Notion や Google Drive のクライアントも実装します。

typescript// src/datasources/index.ts
import { ConfluenceClient } from './confluence';
import { Document } from '../types';

// 全データソースを統合する検索クラス
export class DataSourceManager {
  private confluenceClient: ConfluenceClient;
  // private notionClient: NotionClient;
  // private gdriveClient: GDriveClient;

  constructor() {
    this.confluenceClient = new ConfluenceClient(
      'https://confluence.example.com',
      'your_api_token'
    );
  }

  // 全データソースから検索
  async searchAll(query: string): Promise<Document[]> {
    const results: Document[] = [];

    // Confluence から検索
    const confResults = await this.confluenceClient.search(
      query
    );
    results.push(...confResults);

    // 他のデータソースも同様に追加
    // results.push(...await this.notionClient.search(query));

    return results;
  }
}

ステップ 6:要約機能の実装

Claude API を使って検索結果を要約する機能を実装します。

typescript// src/services/summarizer.ts
import Anthropic from '@anthropic-ai/sdk';
import { config } from '../config/env';
import { Document, Citation } from '../types';

// Claude API クライアントの初期化
const anthropic = new Anthropic({
  apiKey: config.anthropicApiKey,
});

// 検索結果を要約
export async function summarizeDocuments(
  query: string,
  documents: Document[]
): Promise<{ summary: string; citations: Citation[] }> {
  // ドキュメント内容を整形
  const context = documents
    .map(
      (doc, index) =>
        `[${index + 1}] ${doc.title}\n${
          doc.content
        }\nURL: ${doc.url}`
    )
    .join('\n\n---\n\n');

  // プロンプトを構築
  const prompt = buildSummaryPrompt(query, context);

  return { summary: '', citations: [] };
}

プロンプト構築関数を実装します。

typescript// src/services/summarizer.ts(続き)

// 要約用のプロンプトを構築
function buildSummaryPrompt(
  query: string,
  context: string
): string {
  return `あなたは社内ナレッジ検索アシスタントです。
以下のドキュメントを参考に、ユーザーの質問に回答してください。

【質問】
${query}

【参考ドキュメント】
${context}

【回答の指示】
- 簡潔に要約してください(300 文字程度)
- 情報源となったドキュメント番号を明示してください
- 不明な点は「情報が見つかりませんでした」と答えてください`;
}

実際に Claude API を呼び出す処理を追加します。

typescript// src/services/summarizer.ts(続き)

export async function summarizeDocuments(
  query: string,
  documents: Document[]
): Promise<{ summary: string; citations: Citation[] }> {
  const context = documents
    .map(
      (doc, index) =>
        `[${index + 1}] ${doc.title}\n${
          doc.content
        }\nURL: ${doc.url}`
    )
    .join('\n\n---\n\n');

  const prompt = buildSummaryPrompt(query, context);

  // Claude API で要約を生成
  const message = await anthropic.messages.create({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 1024,
    messages: [
      {
        role: 'user',
        content: prompt,
      },
    ],
  });

  const summary = extractTextFromResponse(message);
  const citations = extractCitations(summary, documents);

  return { summary, citations };
}

レスポンスからテキストを抽出する関数を実装します。

typescript// src/services/summarizer.ts(続き)

// Claude のレスポンスからテキストを抽出
function extractTextFromResponse(message: any): string {
  const content = message.content[0];
  if (content.type === 'text') {
    return content.text;
  }
  return '';
}

ステップ 7:引用情報の抽出

要約結果から引用情報を抽出し、構造化する機能を実装します。

typescript// src/services/citation.ts
import { Document, Citation } from '../types';

// 要約テキストから引用情報を抽出
export function extractCitations(
  summary: string,
  documents: Document[]
): Citation[] {
  const citations: Citation[] = [];

  // [1]、[2] のようなパターンを検出
  const pattern = /\[(\d+)\]/g;
  const matches = summary.matchAll(pattern);

  for (const match of matches) {
    const index = parseInt(match[1], 10) - 1;
    if (index >= 0 && index < documents.length) {
      const doc = documents[index];
      citations.push(createCitation(doc, summary));
    }
  }

  return citations;
}

Citation オブジェクトを生成する関数を追加します。

typescript// src/services/citation.ts(続き)

// Citation オブジェクトを生成
function createCitation(
  document: Document,
  summary: string
): Citation {
  // 関連度スコアを簡易的に計算(実際にはより高度な手法を使用)
  const relevanceScore = calculateRelevance(
    document.content,
    summary
  );

  // 抜粋部分を取得(最初の 150 文字)
  const excerpt = document.content.slice(0, 150) + '...';

  return {
    documentId: document.id,
    documentTitle: document.title,
    url: document.url,
    excerpt,
    relevanceScore,
  };
}

// 関連度を計算(簡易版)
function calculateRelevance(
  content: string,
  summary: string
): number {
  // 実際には TF-IDF やベクトル類似度などを使用
  const commonWords = content
    .split(' ')
    .filter((word) => summary.includes(word)).length;
  return Math.min(commonWords / 10, 1.0);
}

ステップ 8:MCP ツールの定義

MCP サーバーで公開するツールを定義します。検索ツールから実装しましょう。

typescript// src/tools/search.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';

// 検索ツールの定義
export const searchTool: Tool = {
  name: 'search_knowledge',
  description:
    '社内ナレッジを検索します。権限に基づいてアクセス可能なドキュメントのみを返します。',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: '検索キーワードまたは質問文',
      },
      maxResults: {
        type: 'number',
        description: '最大取得件数(デフォルト: 10)',
        default: 10,
      },
    },
    required: ['query'],
  },
};

要約ツールの定義も追加します。

typescript// src/tools/summary.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';

// 要約ツールの定義
export const summaryTool: Tool = {
  name: 'summarize_results',
  description:
    '検索結果を要約し、引用情報とともに返します。',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: '元の質問文',
      },
      documentIds: {
        type: 'array',
        items: { type: 'string' },
        description: '要約対象のドキュメント ID リスト',
      },
    },
    required: ['query', 'documentIds'],
  },
};

ステップ 9:MCP サーバーのメイン実装

MCP サーバーのメイン処理を実装します。まずは初期化部分から始めましょう。

typescript// src/server.ts
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 { verifyToken } from './auth/jwt';
import { filterAccessibleDocuments } from './auth/permission';
import { DataSourceManager } from './datasources';
import { summarizeDocuments } from './services/summarizer';
import { searchTool, summaryTool } from './tools';

// MCP サーバーインスタンスを作成
const server = new Server(
  {
    name: 'knowledge-search-mcp',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

ツール一覧を返すハンドラを実装します。

typescript// src/server.ts(続き)

// ツール一覧を返す
server.setRequestHandler(
  ListToolsRequestSchema,
  async () => {
    return {
      tools: [searchTool, summaryTool],
    };
  }
);

データソースマネージャーのインスタンスを初期化します。

typescript// src/server.ts(続き)

// データソースマネージャーを初期化
const dataSourceManager = new DataSourceManager();

ツール実行ハンドラを実装します。まず、リクエストの受付と認証処理を行います。

typescript// src/server.ts(続き)

// ツール実行ハンドラ
server.setRequestHandler(
  CallToolRequestSchema,
  async (request) => {
    const { name, arguments: args } = request.params;

    // 認証トークンを取得(ヘッダーから)
    const token = request.params._meta?.token as string;
    if (!token) {
      throw new Error(
        'Error 401: 認証トークンが提供されていません'
      );
    }

    // トークンを検証してユーザー情報を取得
    let user;
    try {
      user = verifyToken(token);
    } catch (error) {
      throw new Error('Error 403: 認証に失敗しました');
    }

    // ツールごとの処理に分岐
    if (name === 'search_knowledge') {
      return await handleSearch(user, args);
    } else if (name === 'summarize_results') {
      return await handleSummary(user, args);
    }

    throw new Error(
      `Error 404: 不明なツール名です: ${name}`
    );
  }
);

検索ツールのハンドラを実装します。

typescript// src/server.ts(続き)

// 検索ツールのハンドラ
async function handleSearch(user: any, args: any) {
  const { query, maxResults = 10 } = args;

  // 全データソースから検索
  const allDocuments = await dataSourceManager.searchAll(
    query
  );

  // ユーザーがアクセス可能なドキュメントのみフィルタリング
  const accessibleDocs = filterAccessibleDocuments(
    user,
    allDocuments
  );

  // 件数制限
  const limitedDocs = accessibleDocs.slice(0, maxResults);

  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(
          {
            documents: limitedDocs.map((doc) => ({
              id: doc.id,
              title: doc.title,
              url: doc.url,
              excerpt: doc.content.slice(0, 200),
            })),
            totalCount: limitedDocs.length,
          },
          null,
          2
        ),
      },
    ],
  };
}

要約ツールのハンドラを実装します。

typescript// src/server.ts(続き)

// 要約ツールのハンドラ
async function handleSummary(user: any, args: any) {
  const { query, documentIds } = args;

  // 指定されたドキュメント ID を検索
  const allDocuments = await dataSourceManager.searchAll(
    ''
  );
  const targetDocs = allDocuments.filter((doc) =>
    documentIds.includes(doc.id)
  );

  // 権限チェック
  const accessibleDocs = filterAccessibleDocuments(
    user,
    targetDocs
  );

  if (accessibleDocs.length === 0) {
    throw new Error(
      'Error 403: アクセス可能なドキュメントがありません'
    );
  }

  // 要約を生成
  const { summary, citations } = await summarizeDocuments(
    query,
    accessibleDocs
  );

  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(
          {
            summary,
            citations,
            totalDocuments: accessibleDocs.length,
          },
          null,
          2
        ),
      },
    ],
  };
}

サーバーを起動する処理を追加します。

typescript// src/server.ts(続き)

// サーバーを起動
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(
    'Knowledge Search MCP Server が起動しました'
  );
}

main().catch((error) => {
  console.error('Error 500: サーバー起動エラー:', error);
  process.exit(1);
});

ステップ 10:エラーハンドリングの追加

実運用を見据えて、エラーハンドリングを強化します。

typescript// src/utils/errors.ts

// カスタムエラークラス
export class KnowledgeSearchError extends Error {
  constructor(
    public code: string,
    public statusCode: number,
    message: string
  ) {
    super(message);
    this.name = 'KnowledgeSearchError';
  }
}

// エラー種類ごとのファクトリ関数
export const Errors = {
  // Error 401: 認証エラー
  unauthorized: (message = '認証トークンが無効です') =>
    new KnowledgeSearchError('UNAUTHORIZED', 401, message),

  // Error 403: 権限エラー
  forbidden: (message = 'アクセス権限がありません') =>
    new KnowledgeSearchError('FORBIDDEN', 403, message),

  // Error 404: リソース未発見
  notFound: (
    message = '指定されたリソースが見つかりません'
  ) => new KnowledgeSearchError('NOT_FOUND', 404, message),

  // Error 500: サーバーエラー
  internal: (
    message = '内部サーバーエラーが発生しました'
  ) =>
    new KnowledgeSearchError(
      'INTERNAL_ERROR',
      500,
      message
    ),
};

エラーハンドリングミドルウェアを追加します。

typescript// src/utils/errorHandler.ts
import { KnowledgeSearchError } from './errors';

// エラーをログに記録し、適切なレスポンスを返す
export function handleError(error: unknown): {
  code: string;
  message: string;
  statusCode: number;
} {
  if (error instanceof KnowledgeSearchError) {
    console.error(`[${error.code}] ${error.message}`);
    return {
      code: error.code,
      message: error.message,
      statusCode: error.statusCode,
    };
  }

  // 予期しないエラー
  console.error('Unexpected error:', error);
  return {
    code: 'INTERNAL_ERROR',
    message: 'Error 500: 予期しないエラーが発生しました',
    statusCode: 500,
  };
}

ステップ 11:テストコードの作成

実装が正しく動作するか確認するため、簡単なテストを作成します。

typescript// src/test/auth.test.ts
import { generateToken, verifyToken } from '../auth/jwt';
import { User } from '../types';

// テスト用ユーザー
const testUser: User = {
  userId: 'test-001',
  email: 'test@example.com',
  roles: ['engineer'],
  departments: ['development'],
};

// JWT トークンの生成と検証をテスト
function testJwtAuth() {
  console.log('=== JWT 認証テスト ===');

  // トークン生成
  const token = generateToken(testUser);
  console.log('✓ トークン生成成功');

  // トークン検証
  try {
    const decoded = verifyToken(token);
    console.log('✓ トークン検証成功:', decoded.email);
  } catch (error) {
    console.error('✗ トークン検証失敗:', error);
  }
}

testJwtAuth();

権限チェックのテストも追加します。

typescript// src/test/permission.test.ts
import { canAccessDocument } from '../auth/permission';
import { User, Document } from '../types';

// テスト実行
function testPermission() {
  console.log('=== 権限チェックテスト ===');

  const user: User = {
    userId: 'test-001',
    email: 'test@example.com',
    roles: ['engineer'],
    departments: ['development'],
  };

  const doc: Document = {
    id: 'doc-001',
    title: 'テストドキュメント',
    content: 'テスト内容',
    source: 'confluence',
    url: 'https://example.com/doc-001',
    allowedRoles: ['engineer', 'manager'],
    allowedDepartments: ['development'],
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const hasAccess = canAccessDocument(user, doc);
  console.log('✓ アクセス可能:', hasAccess);
}

testPermission();

ステップ 12:ビルドと実行

実装したコードをビルドして実行します。package.json にスクリプトを追加しましょう。

json{
  "name": "knowledge-search-mcp",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js",
    "dev": "tsc && node dist/server.js",
    "test": "node dist/test/auth.test.js && node dist/test/permission.test.js"
  }
}

ビルドと実行のコマンドを順に実行します。

bash# ビルド
yarn build

# テスト実行
yarn test

# サーバー起動
yarn start

以下のようなログが表示されれば、サーバーが正常に起動しています。

bashKnowledge Search MCP Server が起動しました

ステップ 13:Claude Desktop での接続設定

構築した MCP サーバーを Claude Desktop から利用するための設定を行います。Claude Desktop の設定ファイル(claude_desktop_config.json)に以下を追加してください。

json{
  "mcpServers": {
    "knowledge-search": {
      "command": "node",
      "args": [
        "/path/to/knowledge-search-mcp/dist/server.js"
      ],
      "env": {
        "ANTHROPIC_API_KEY": "your_api_key",
        "JWT_SECRET": "your_jwt_secret"
      }
    }
  }
}

設定後、Claude Desktop を再起動すると、チャット画面から search_knowledgesummarize_results ツールが利用できるようになります。

実装結果の確認

以下の図は、実装したシステムの実行フローを示しています。

mermaidsequenceDiagram
  participant U as ユーザー
  participant C as Claude Desktop
  participant M as MCP サーバー
  participant D as データソース

  U->>C: "○○について教えて"
  C->>M: search_knowledge(query, token)
  M->>M: トークン検証
  M->>D: 全データソース検索
  D-->>M: 検索結果
  M->>M: 権限フィルタリング
  M-->>C: 絞り込み結果
  C->>M: summarize_results(query, docIds)
  M->>M: 要約生成(Claude API)
  M->>M: 引用情報抽出
  M-->>C: 要約+引用
  C-->>U: 回答表示

図の要点:ユーザーの質問から回答までの一連の流れが、権限チェックと要約を経て完結します。

実際に Claude Desktop で以下のように質問すると、権限に基づいた検索結果と要約が返ってくるでしょう。

質問例:「API 設計のベストプラクティスを教えてください」

回答例

less【要約】
API 設計では、RESTful 原則に従い、リソース指向の URL 設計を行うことが重要です[1]。また、適切なHTTP メソッドの使用、エラーハンドリングの統一、バージョニング戦略の策定が推奨されます[1][2]。

【参考ドキュメント】
[1] API 設計ガイドライン (https://confluence.example.com/pages/001)
"RESTful API の設計原則について..."

[2] API バージョン管理の方針 (https://notion.example.com/pages/042)
"API のバージョニング戦略と後方互換性..."

このように、要約と根拠が明確に提示され、ユーザーは信頼性の高い情報を得られます。

まとめ

本記事では、MCP サーバーを使った社内ナレッジ検索チャットの構築方法を、権限制御・要約・根拠表示の 3 つの実装パターンを中心に解説しました。

実装のポイントをまとめると、以下のようになります。

#ポイント説明
1JWT による認証トークンベースでユーザーを識別し、なりすましを防止
2役職・部門による権限制御アクセス可能なドキュメントのみをフィルタリング
3Claude API による要約複数ドキュメントを簡潔にまとめて提示
4引用情報の明示情報源を明確化し、信頼性を担保
5エラーハンドリングエラーコード付きで問題を特定しやすくする

MCP を活用することで、社内に散在するナレッジを統合し、セキュアで使いやすい検索システムを構築できるでしょう。今回の実装パターンは、Confluence や Notion 以外のデータソースにも応用可能です。

さらに、以下のような拡張も検討できます。

  • ベクトル検索の導入:セマンティック検索で精度を向上
  • キャッシュ機構:頻繁にアクセスされるドキュメントをキャッシュして高速化
  • 監査ログ:誰がいつどのドキュメントにアクセスしたかを記録
  • 多言語対応:翻訳機能を組み込み、グローバルチームで活用

ぜひ、この記事を参考に、あなたの組織に最適なナレッジ検索システムを構築してみてください。

関連リンク