複数AIの会話履歴をSQLiteで保存・共有する方法|Googleドライブ同期対応

日々の開発や調査において、Google Antigravity、Claude Code、Cursor、ChatGPT(Codex)など、複数のAIツールを用途に合わせて使い分ける機会が増えました。

非常に便利な一方で、あるツールで解決したエラーや設計方針を別のツールが知らず、同じ説明を何度も繰り返したり、セッション終了やPCの買い替え時に貴重なやり取りが消えてしまったりする悩みを抱えている方も多いのではないでしょうか。

この記事では、自作のMCP(Model Context Protocol)サーバーとSQLiteを活用し、複数AIツールの会話履歴や知見を一元管理するローカルナレッジベースの構築手順を解説します。

外部のベクトルDBや有料クラウドサービスを使わず無料で動作し、Googleドライブのミラーリングフォルダに配置することで、PCの故障や買い替えにも備えられる構成を実現します。

  1. この記事で行うこと
  2. 前提条件・対象読者
  3. 1. 複数AIツールの「知見分断問題」とSQLite×MCPによる解決策
      1. 複数AIの併用で生じる主な悩み
    1. ベクトルDBではなく「SQLite + FTS5」を選ぶべき理由
      1. 1. 無料でランニングコストがかからない
      2. 2. ローカル完結でレスポンスが高速
      3. 3. 単一ファイルでバックアップやPC移行が容易
    2. 日本語検索における「FTS5 + LIKE」ハイブリッド設計のポイント
  4. 2. Googleドライブ同期でSQLiteを使うときの必須設計(ファイルロック対策)
    1. 対策1:Googleドライブ側を「ミラーリング(ローカル常駐)」にする
    2. 対策2:WAL(Write-Ahead Logging)モードの有効化
    3. 対策3:busy_timeout(ロック待ちリトライ)の設定
    4. 対策4:ステートレス接続(都度オープン・即時クローズ)
  5. 手順1:フォルダ構成の準備とライブラリの導入
  6. 手順2:SQLiteマネージャー(db.py)を作成する
  7. 手順3:MCPサーバー本体(server.py)を作成する
  8. 手順4:各AIツールへMCPサーバーを登録する
    1. 1. Google Antigravity への登録手順
    2. 2. Claude Code / Claude Desktop への登録手順
      1. Claude Code(CLI版)の場合
      2. Claude Desktop(デスクトップアプリ版)の場合
    3. 3. Cursor への登録手順
    4. 4. ChatGPT デスクトップ版 / Codex への登録手順
  9. 手順5:AIとの会話でナレッジを保存・共有する実践フロー
    1. 手動での保存・検索の指示例
      1. 知見を保存させたいとき
      2. 過去の曖昧な記憶から検索・回答させたいとき
    2. 複数AIツール間での引き継ぎ例(Antigravity → Codex)
    3. 会話終了時に自動でナレッジ保存させるルール設定
  10. 手順6:人間用CLIツールとMarkdownエクスポートを用意する
    1. ターミナルから直接操作するCLI(cli.py)
    2. Markdown一括エクスポート(export_md.py)
  11. よくある質問・注意点
    1. クラウド同期ソフトのコンフリクト(競合コピー)が起きたら?
    2. 個人情報やAPIキーの混入を防ぐには?
  12. まとめ
      1. 今回のポイント
  13. 次に読むおすすめ記事
  14. 参考情報

この記事で行うこと

  • 複数AIツール(Antigravity、Claude Code、Cursor、Codex)で知見を共有する自作MCPサーバーの作成
  • SQLiteのFTS5(全文検索)と部分一致検索を組み合わせた日本語ナレッジ検索環境の構築
  • Googleドライブ同期下でもファイルロック(database is locked)を起こしにくいDB接続設計
  • 各AIツールへの登録手順と、チャット終了時に自動保存させるプロンプト・運用ルールの導入

前提条件・対象読者

本記事で紹介する手順は、以下の環境で動作を確認しています。

項目内容
OSWindows 11(macOSやLinuxでもパスを読み替えれば動作可能)
PythonPython 3.11以上(3.11または3.12推奨)
クラウド同期Googleドライブ パソコン版(ミラーリングまたはオフライン常駐設定)
必須ライブラリmcp(FastMCP対応の公式Python SDK)
連携確認済みAIGoogle Antigravity、Claude Code、Cursor、Codex

事前にPythonがインストールされ、ターミナルで python --version が実行できる状態にしてください。

1. 複数AIツールの「知見分断問題」とSQLite×MCPによる解決策

各AIツールはそれぞれ独自のチャット履歴やワークスペースを持っています。そのため、複数のAIを併用すると以下のような課題が生じがちです。

複数AIの併用で生じる主な悩み

  • Antigravityで調査・解決したNext.jsのエラー原因を、あとでCursorやCodexで作業しているときに参照できない
  • ChatGPTで決めたアーキテクチャの命名規則を、Claude Codeが知らずに別の命名でコードを書いてしまう
  • 新しいPCへの移行時やPC故障時に、ローカルに散在していたAIとのやり取りが失われる
  • 過去の曖昧な記憶からやり取りを探そうとしても、各ツールの履歴画面を個別に遡る手間がかかる

これらの課題を解決するのが、オープン標準規格である「MCP(Model Context Protocol)」を介した共通データベースの活用です。

ベクトルDBではなく「SQLite + FTS5」を選ぶべき理由

AIの外部記憶やRAG(検索拡張生成)というと、PineconeやQdrantなどのベクトルデータベースや、文章をベクトル化するEmbedding APIを連想するかもしれません。

しかし、個人の日々の開発知見や会話ログを管理する目的であれば、SQLiteの全文検索拡張機能「FTS5(Full-Text Search 5)」と標準SQLを組み合わせた構成に大きなメリットがあります。

1. 無料でランニングコストがかからない

外部DBサービスの有料プランやEmbedding APIの呼び出し課金が発生せず、手元のPCリソースだけで運用できます。

2. ローカル完結でレスポンスが高速

外部APIとのネットワーク通信やベクトル変換の待ち時間が発生しないため、AIがツールを呼び出した際の検索応答がミリ秒単位で完了します。

3. 単一ファイルでバックアップやPC移行が容易

データベース本体が単一のファイル(knowledge.db)に収まるため、フォルダごとGoogleドライブで同期するだけでバックアップと別PCへの移行が完結します。

日本語検索における「FTS5 + LIKE」ハイブリッド設計のポイント

SQLiteに標準搭載されているFTS5のデフォルトトークナイザー(unicode61)は、スペースや記号で区切られた英単語(Next.js、Supabase、Docker など)や、空白区切りのキーワードを高速にインデックス検索できます。

一方で、日本語の文章は単語間にスペースがないため、デフォルトのFTS5だけでは連続した日本語フレーズの部分一致を取りこぼす場合があります(SQLite 3.34.0以降であれば tokenize='trigram' オプションで3文字以上の部分一致インデックスを作ることも可能です)。

本記事の実装では、どのような環境・文字数でも確実に日本語の曖昧な記憶を引き出せるよう、「FTS5による高速ランク検索」と「LIKE 演算子による日本語部分一致検索」の両方を実行して結果を統合するハイブリッド方式を採用しています。

2. Googleドライブ同期でSQLiteを使うときの必須設計(ファイルロック対策)

SQLiteデータベースファイルをGoogleドライブやDropbox、OneDriveなどのクラウド同期フォルダ内に置く場合、初期設定のまま運用すると sqlite3.OperationalError: database is locked エラーが頻発します。

これは、クラウド同期クライアントがファイルの変更を検知してアップロード処理を行う際、一時的にファイルをつかんでロックしてしまうためです。

この競合を防ぎ、クラウド同期環境下でも安定して読み書きを行うため、以下の4つの対策を組み込みます。

対策1:Googleドライブ側を「ミラーリング(ローカル常駐)」にする

Googleドライブ パソコン版の同期方式には「ストリーミング」と「ミラーリング」があります。

ストリーミング(クラウド上にのみ実体を置き、アクセス時にダウンロードする仮想ドライブ方式)のままSQLiteを使うと、トランザクション用の一時ファイル(-wal や -shm)の同期ズレや遅延による破損リスクが高まります。

必ずGoogleドライブの設定で「ミラーリング」を選択するか、対象フォルダを右クリックして オフラインで使用可能 に設定し、ローカルディスク上に常時ファイルの実体が存在する状態で運用してください。

対策2:WAL(Write-Ahead Logging)モードの有効化

デフォルトのSQLiteはロールバックジャーナルモードで動作し、書き込み時だけでなく読み込み時にも排他制御が強く働きます。

これをWALモード(Write-Ahead Logging)に変更することで、読み込み処理と書き込み処理が互いをブロックしにくくなります。

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;

同期クライアントがファイルを読み取っている最中でも、AIツールからの読み書きが阻害されにくくなります。

対策3:busy_timeout(ロック待ちリトライ)の設定

同期ソフトが一瞬だけファイルをロックした場合に、即座にエラーで落とさず、指定時間だけ待機して自動リトライする設定を行います。

PRAGMA busy_timeout = 10000;

Pythonの sqlite3.connect の引数 timeout=10.0 とあわせて10秒間のロック待ちを設定することで、同期ソフトのアップロード処理が一瞬重なっても自然に待機して処理が完了します。

対策4:ステートレス接続(都度オープン・即時クローズ)

MCPサーバー起動中にデータベース接続(Connection)を常時開いたまま保持すると、同期ソフトとの競合リスクが高まります。

そのため、Pythonのコンテキストマネージャー(with 構文)を用い、ツールが呼び出された瞬間だけDBをオープンし、処理が終わったら即座に close() してファイルハンドルを解放するステートレス設計にします。

手順1:フォルダ構成の準備とライブラリの導入

ここからは、実際に動作するスクリプトを構築していきます。

Googleドライブのミラーリングフォルダ内に、任意の作業フォルダ(例として C:\GoogleDrive\AI_work\ai-knowledge-db)を作成します。

最終的なフォルダ構成は以下のようになります。

ai-knowledge-db/
├── knowledge.db         # SQLiteデータベース本体(初回起動時に自動生成)
├── db.py                # データベース操作モジュール(ロック対策・ハイブリッド検索)
├── server.py            # MCPサーバー本体(各AIツールから呼び出される窓口)
├── cli.py               # 人間用のコマンドライン操作ツール(検索・追加・閲覧・削除)
├── export_md.py         # Markdown一括エクスポートツール(Obsidian連携用)
└── markdown_exports/    # Markdownファイルの出力先ディレクトリ

PowerShellなどのターミナルを開き、公式のMCP Python SDKをインストールします。

pip install "mcp[cli]"

なお、PythonのFastMCPを使った自作MCPサーバーの基本構造やデバッグ方法については、以下の記事でも詳しく解説しています。

手順2:SQLiteマネージャー(db.py)を作成する

C:\GoogleDrive\AI_work\ai-knowledge-db\db.py を作成し、以下のコードを記述します。

WALモードやタイムアウト設定、FTS5仮想テーブルの自動更新トリガー、および日本語対応のハイブリッド検索ロジックが含まれています。

"""
SQLite Knowledge Database Manager
Googleドライブ同期下でも安定動作するSQLiteマネージャー
"""

import os
import sqlite3
from contextlib import contextmanager
from datetime import datetime
from typing import Any, Dict, List, Optional

DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(DB_DIR, "knowledge.db")


@contextmanager
def get_db_connection():
    """
    ステートレスかつタイムアウト耐性のあるSQLite接続コンテキストマネージャー。
    Googleドライブの同期競合を防ぐため、処理終了時に即座にクローズする。
    """
    conn = sqlite3.connect(
        DB_PATH,
        timeout=10.0,
        isolation_level=None,
    )
    conn.row_factory = sqlite3.Row
    try:
        conn.execute("PRAGMA journal_mode = WAL;")
        conn.execute("PRAGMA synchronous = NORMAL;")
        conn.execute("PRAGMA busy_timeout = 10000;")
        yield conn
    finally:
        conn.close()


def init_db():
    """データベースとテーブル・インデックスを初期化する"""
    with get_db_connection() as conn:
        with conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS knowledge (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL,
                    content TEXT NOT NULL,
                    category TEXT NOT NULL DEFAULT 'general',
                    tags TEXT DEFAULT '',
                    source_tool TEXT DEFAULT 'unknown',
                    created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
                    updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
                );
                """
            )

            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge(category);"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_knowledge_created ON knowledge(created_at);"
            )

            conn.execute(
                """
                CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
                    title,
                    content,
                    tags,
                    category,
                    content='knowledge',
                    content_rowid='id'
                );
                """
            )

            conn.execute(
                """
                CREATE TRIGGER IF NOT EXISTS knowledge_ai AFTER INSERT ON knowledge BEGIN
                    INSERT INTO knowledge_fts(rowid, title, content, tags, category)
                    VALUES (new.id, new.title, new.content, new.tags, new.category);
                END;
                """
            )

            conn.execute(
                """
                CREATE TRIGGER IF NOT EXISTS knowledge_ad AFTER DELETE ON knowledge BEGIN
                    INSERT INTO knowledge_fts(knowledge_fts, rowid, title, content, tags, category)
                    VALUES('delete', old.id, old.title, old.content, old.tags, old.category);
                END;
                """
            )

            conn.execute(
                """
                CREATE TRIGGER IF NOT EXISTS knowledge_au AFTER UPDATE ON knowledge BEGIN
                    INSERT INTO knowledge_fts(knowledge_fts, rowid, title, content, tags, category)
                    VALUES('delete', old.id, old.title, old.content, old.tags, old.category);
                    INSERT INTO knowledge_fts(rowid, title, content, tags, category)
                    VALUES (new.id, new.title, new.content, new.tags, new.category);
                END;
                """
            )


def save_knowledge(
    title: str,
    content: str,
    category: str = "general",
    tags: str = "",
    source_tool: str = "unknown",
) -> Dict[str, Any]:
    """知見を新規保存する"""
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with get_db_connection() as conn:
        with conn:
            cursor = conn.execute(
                """
                INSERT INTO knowledge (title, content, category, tags, source_tool, created_at, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    title.strip(),
                    content.strip(),
                    category.strip().lower(),
                    tags.strip(),
                    source_tool.strip(),
                    now,
                    now,
                ),
            )
            item_id = cursor.lastrowid
            row = conn.execute(
                "SELECT * FROM knowledge WHERE id = ?", (item_id,)
            ).fetchone()
            return dict(row)


def update_knowledge(
    item_id: int,
    title: Optional[str] = None,
    content: Optional[str] = None,
    category: Optional[str] = None,
    tags: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
    """既存の知見を更新する"""
    with get_db_connection() as conn:
        with conn:
            row = conn.execute(
                "SELECT * FROM knowledge WHERE id = ?", (item_id,)
            ).fetchone()
            if not row:
                return None

            new_title = title.strip() if title is not None else row["title"]
            new_content = content.strip() if content is not None else row["content"]
            new_category = (
                category.strip().lower() if category is not None else row["category"]
            )
            new_tags = tags.strip() if tags is not None else row["tags"]
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            conn.execute(
                """
                UPDATE knowledge
                SET title = ?, content = ?, category = ?, tags = ?, updated_at = ?
                WHERE id = ?
                """,
                (new_title, new_content, new_category, new_tags, now, item_id),
            )
            updated_row = conn.execute(
                "SELECT * FROM knowledge WHERE id = ?", (item_id,)
            ).fetchone()
            return dict(updated_row)


def get_knowledge(item_id: int) -> Optional[Dict[str, Any]]:
    """IDで指定した知見の詳細を取得する"""
    with get_db_connection() as conn:
        row = conn.execute(
            "SELECT * FROM knowledge WHERE id = ?", (item_id,)
        ).fetchone()
        return dict(row) if row else None


def search_knowledge(
    query: str,
    category: Optional[str] = None,
    limit: int = 5,
) -> List[Dict[str, Any]]:
    """
    FTS5全文検索とLIKE検索をハイブリッドで組み合わせ、
    英数字キーワードと日本語の部分一致検索の両方に対応する。
    """
    clean_query = query.strip()
    if not clean_query:
        return list_recent_knowledge(limit=limit, category=category)

    with get_db_connection() as conn:
        fts_ids = []
        try:
            fts_query = " ".join(
                [f'"{part}"' for part in clean_query.split() if part]
            )
            if fts_query:
                cursor = conn.execute(
                    "SELECT rowid FROM knowledge_fts WHERE knowledge_fts MATCH ? ORDER BY rank LIMIT ?",
                    (fts_query, limit * 2),
                )
                fts_ids = [r[0] for r in cursor.fetchall()]
        except Exception:
            fts_ids = []

        like_pattern = f"%{clean_query}%"
        like_sql = """
            SELECT id FROM knowledge
            WHERE (title LIKE ? OR content LIKE ? OR tags LIKE ? OR category LIKE ?)
        """
        params: List[Any] = [like_pattern, like_pattern, like_pattern, like_pattern]
        if category:
            like_sql += " AND category = ?"
            params.append(category.strip().lower())
        like_sql += " ORDER BY updated_at DESC LIMIT ?"
        params.append(limit * 2)

        cursor = conn.execute(like_sql, params)
        like_ids = [r[0] for r in cursor.fetchall()]

        seen = set()
        merged_ids = []
        for i in fts_ids + like_ids:
            if i not in seen:
                seen.add(i)
                merged_ids.append(i)

        if not merged_ids:
            return []

        target_ids = merged_ids[:limit]
        placeholders = ",".join(["?"] * len(target_ids))
        fetch_sql = f"SELECT * FROM knowledge WHERE id IN ({placeholders})"
        fetch_params: List[Any] = list(target_ids)

        if category:
            fetch_sql += " AND category = ?"
            fetch_params.append(category.strip().lower())

        cursor = conn.execute(fetch_sql, fetch_params)
        results = [dict(r) for r in cursor.fetchall()]

        id_order = {item_id: idx for idx, item_id in enumerate(target_ids)}
        results.sort(key=lambda x: id_order.get(x["id"], 999))
        return results[:limit]


def list_recent_knowledge(
    limit: int = 10, category: Optional[str] = None
) -> List[Dict[str, Any]]:
    """最近登録・更新された知見一覧を取得する"""
    with get_db_connection() as conn:
        sql = "SELECT id, title, category, tags, source_tool, updated_at FROM knowledge"
        params: List[Any] = []
        if category:
            sql += " WHERE category = ?"
            params.append(category.strip().lower())
        sql += " ORDER BY updated_at DESC LIMIT ?"
        params.append(limit)

        cursor = conn.execute(sql, params)
        return [dict(r) for r in cursor.fetchall()]


def list_categories() -> List[Dict[str, Any]]:
    """登録されているカテゴリと各件数の一覧を取得する"""
    with get_db_connection() as conn:
        cursor = conn.execute(
            """
            SELECT category, COUNT(*) as count, MAX(updated_at) as last_updated
            FROM knowledge
            GROUP BY category
            ORDER BY count DESC
            """
        )
        return [dict(r) for r in cursor.fetchall()]


def delete_knowledge(item_id: int) -> bool:
    """指定IDの知見を削除する"""
    with get_db_connection() as conn:
        with conn:
            cursor = conn.execute("DELETE FROM knowledge WHERE id = ?", (item_id,))
            return cursor.rowcount > 0

手順3:MCPサーバー本体(server.py)を作成する

続いて、各AIツールからのリクエストを受け付ける C:\GoogleDrive\AI_work\ai-knowledge-db\server.py を作成します。

ファイルの末尾で server.run(transport="stdio") を呼び出すことで、標準入出力(Stdio)を介したローカルMCPサーバーとして動作します。

"""
AI Knowledge MCP Server
Antigravity, Claude Code, Cursor, Codex等の各AIツールから共通利用できるナレッジMCPサーバー
"""

import os
import sys

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
if CURRENT_DIR not in sys.path:
    sys.path.insert(0, CURRENT_DIR)

import db
from mcp.server.mcpserver import MCPServer

server = MCPServer(name="ai-knowledge-db", version="1.0.0")

db.init_db()


@server.tool()
def save_knowledge(
    title: str,
    content: str,
    category: str = "general",
    tags: str = "",
    source_tool: str = "unknown",
) -> str:
    """
    開発の知見、エラー解決策、設計方針、会話の要約などをナレッジベースに保存します。

    Parameters:
    - title: 知見の簡潔なタイトル
    - content: 知見の詳細、原因、解決手順、会話内容などの本文
    - category: カテゴリ(例: coding, troubleshooting, infra, architecture, rules, general)
    - tags: 検索用キーワード(カンマ区切り)
    - source_tool: 記録元のAIツール名(例: antigravity, claude-code, cursor, codex)
    """
    try:
        saved = db.save_knowledge(
            title=title,
            content=content,
            category=category,
            tags=tags,
            source_tool=source_tool,
        )
        return (
            f"【保存成功】ID: {saved['id']}\n"
            f"タイトル: {saved['title']}\n"
            f"カテゴリ: {saved['category']}\n"
            f"タグ: {saved['tags']}"
        )
    except Exception as e:
        return f"【保存失敗】エラーが発生しました: {str(e)}"


@server.tool()
def search_knowledge(
    query: str,
    category: str | None = None,
    limit: int = 5,
) -> str:
    """
    過去に蓄積された知見や会話履歴を検索します。日本語のキーワードやタグに対応しています。

    Parameters:
    - query: 検索キーワード(例: Next.js 認証, Supabase RLS, デプロイ手順)
    - category: 特定カテゴリに絞り込む場合は指定(省略可)
    - limit: 取得する最大件数(デフォルト: 5件)
    """
    try:
        results = db.search_knowledge(query=query, category=category, limit=limit)
        if not results:
            return f"「{query}」に一致する知見は見つかりませんでした。"

        output = [f"【検索結果: {len(results)}件】(キーワード: {query})\n"]
        for item in results:
            content_preview = item["content"]
            if len(content_preview) > 300:
                content_preview = (
                    content_preview[:300]
                    + f"...\n(※全文は get_knowledge(id={item['id']}) で確認できます)"
                )

            output.append(
                f"--- [ID: {item['id']}] {item['title']} ---\n"
                f"カテゴリ: {item['category']} | タグ: {item['tags']} | ツール: {item['source_tool']} | 更新日: {item['updated_at']}\n"
                f"{content_preview}\n"
            )
        return "\n".join(output)
    except Exception as e:
        return f"【検索エラー】{str(e)}"


@server.tool()
def get_knowledge(id: int) -> str:
    """指定したIDの知見の全文と詳細情報を取得します。"""
    try:
        item = db.get_knowledge(item_id=id)
        if not item:
            return f"ID: {id} の知見は見つかりませんでした。"

        return (
            f"=== [ID: {item['id']}] {item['title']} ===\n"
            f"カテゴリ: {item['category']}\n"
            f"タグ: {item['tags']}\n"
            f"記録元ツール: {item['source_tool']}\n"
            f"作成日: {item['created_at']} | 最終更新日: {item['updated_at']}\n\n"
            f"--- 本文 ---\n"
            f"{item['content']}"
        )
    except Exception as e:
        return f"【取得エラー】{str(e)}"


@server.tool()
def update_knowledge(
    id: int,
    title: str | None = None,
    content: str | None = None,
    category: str | None = None,
    tags: str | None = None,
) -> str:
    """既存の知見を最新情報に更新します。"""
    try:
        updated = db.update_knowledge(
            item_id=id,
            title=title,
            content=content,
            category=category,
            tags=tags,
        )
        if not updated:
            return f"ID: {id} の知見は見つかりませんでした。"

        return f"【更新成功】ID: {updated['id']} ({updated['title']}) を更新しました。"
    except Exception as e:
        return f"【更新エラー】{str(e)}"


@server.tool()
def list_recent_knowledge(limit: int = 10, category: str | None = None) -> str:
    """最近蓄積・更新された知見の一覧を取得します。"""
    try:
        results = db.list_recent_knowledge(limit=limit, category=category)
        if not results:
            return "登録されている知見はまだありません。"

        output = [f"【最新知見一覧 ({len(results)}件)】\n"]
        for item in results:
            output.append(
                f"- [ID: {item['id']}] [{item['category']}] {item['title']} "
                f"(タグ: {item['tags']}, ツール: {item['source_tool']}, 更新: {item['updated_at']})"
            )
        return "\n".join(output)
    except Exception as e:
        return f"【一覧取得エラー】{str(e)}"


@server.tool()
def list_categories() -> str:
    """登録されているカテゴリ一覧とそれぞれの件数を取得します。"""
    try:
        cats = db.list_categories()
        if not cats:
            return "登録されているカテゴリはありません。"

        output = ["【登録カテゴリ一覧】\n"]
        for c in cats:
            output.append(
                f"- {c['category']}: {c['count']}件 (最終更新: {c['last_updated']})"
            )
        return "\n".join(output)
    except Exception as e:
        return f"【カテゴリ取得エラー】{str(e)}"


if __name__ == "__main__":
    server.run(transport="stdio")

手順4:各AIツールへMCPサーバーを登録する

作成した server.py を、普段使っている各AIツールに登録します。

※以下の設定例にある C:\GoogleDrive\AI_work\ai-knowledge-db\server.py は、ご自身の環境で配置した実際のフォルダパスに置き換えてください。また、仮想環境(venv)や特定のPython実行ファイルを使っている場合は、command に python.exe のフルパスを指定すると環境変数のズレを防げます。

1. Google Antigravity への登録手順

設定ファイル C:\Users\<username>\.gemini\settings.json を開きます。

mcpServers オブジェクトに以下の設定を追加します。

{
  "mcpServers": {
    "ai-knowledge-db": {
      "command": "python",
      "args": [
        "C:\\GoogleDrive\\AI_work\\ai-knowledge-db\\server.py"
      ]
    }
  }
}

設定を保存すると、次回セッション開始時から save_knowledge や search_knowledge などのツールが認識されます。

2. Claude Code / Claude Desktop への登録手順

Claude Code(CLI版)の場合

ターミナルで以下のコマンドを実行するだけで登録が完了します。

claude mcp add ai-knowledge-db python "C:\GoogleDrive\AI_work\ai-knowledge-db\server.py"

Claude Desktop(デスクトップアプリ版)の場合

設定ファイル %APPDATA%\Claude\claude_desktop_config.json を開き、以下を追記します。

{
  "mcpServers": {
    "ai-knowledge-db": {
      "command": "python",
      "args": [
        "C:\\GoogleDrive\\AI_work\\ai-knowledge-db\\server.py"
      ]
    }
  }
}

Claude DesktopにおけるMCP設定の詳しい手順やエラー対処法は、以下の記事もあわせて参照してください。

3. Cursor への登録手順

Cursorの設定画面(Settings > Features > MCP)から追加するか、プロジェクト直下の .cursor/mcp.json(またはグローバル設定の C:\Users\<username>\.cursor\mcp.json)に以下を記述します。

{
  "mcpServers": {
    "ai-knowledge-db": {
      "command": "python",
      "args": [
        "C:/GoogleDrive/AI_work/ai-knowledge-db/server.py"
      ]
    }
  }
}

4. ChatGPT デスクトップ版 / Codex への登録手順

Codexの設定ファイル C:\Users\<username>\.codex\config.toml を開き、末尾に以下を追記します。

[mcp_servers.ai_knowledge_db]
command = "python"
args = ["C:\\GoogleDrive\\AI_work\\ai-knowledge-db\\server.py"]

Codex CLIからコマンドで追加する場合は、以下を実行します。

codex mcp add ai_knowledge_db -- python "C:\GoogleDrive\AI_work\ai-knowledge-db\server.py"

手順5:AIとの会話でナレッジを保存・共有する実践フロー

登録が完了すると、チャット画面で普段通り話しかけるだけでAIが自動的にDBへアクセスしてくれます。

手動での保存・検索の指示例

知見を保存させたいとき

このNext.jsの認証エラーの原因と解決手順を、ナレッジDBに記録しておいて。

AIが自動的に save_knowledge ツールを呼び出し、タイトル、本文、カテゴリ、検索用タグを整えて knowledge.db に保存します。

過去の曖昧な記憶から検索・回答させたいとき

過去にNext.jsの認証周りで相談した内容がナレッジDBにあるか検索して、それに合わせて実装して。

AIが search_knowledge と get_knowledge を実行し、過去のやり取りを文脈に読み込んだ上で回答してくれます。

複数AIツール間での引き継ぎ例(Antigravity → Codex)

実際に使ってみると、ツールを跨いだ引き継ぎの快適さを実感できます。

  • Antigravityで調査・実装を行った際、決定した設計方針やコマンド手順をナレッジDBに保存する
  • あとからChatGPT(Codex)やClaude Codeを開き、「さっきAntigravityで話した設計方針をナレッジDBから確認して続きを進めて」と指示する
  • Codexが同じ knowledge.db を検索し、前提条件を把握した状態で回答・コーディングを開始する

毎回プロンプトをコピペして背景を説明し直す手間がなくなります。

会話終了時に自動でナレッジ保存させるルール設定

毎回「保存して」と指示するのが手間に感じる場合は、各AIツールの共通ルールファイル(GEMINI.md、CLAUDE.md、AGENTS.md など)に自動保存ルールを記載しておくのがおすすめです。

各ターンにおいて、ユーザーからの依頼への対応や回答を完了する際、やり取りの要約(質問内容と対応結果)を ai-knowledge-db の save_knowledge ツール(または cli.py add コマンド)を使って自動保存してください。

Antigravityの GEMINI.md を使ったルール設定方法や、複数AIでスキル・ルールファイルを一元管理する方法については、以下の記事で詳しく解説しています。

手順6:人間用CLIツールとMarkdownエクスポートを用意する

AI経由だけでなく、人間が直接ターミナルから過去の履歴を検索したり、ObsidianなどのMarkdownビューアで閲覧できるようにしておくとさらに利便性が高まります。

ターミナルから直接操作するCLI(cli.py)

C:\GoogleDrive\AI_work\ai-knowledge-db\cli.py を作成します。Windowsのコンソールで絵文字や特殊文字を含むログを出力しても UnicodeEncodeError が起きないよう、標準出力のUTF-8再構成処理を入れています。

"""
AI Knowledge Database CLI
コマンドラインから知見の登録・検索・閲覧・削除を直接操作できるユーティリティ
"""

import argparse
import os
import sys

if sys.platform == "win32":
    try:
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
if CURRENT_DIR not in sys.path:
    sys.path.insert(0, CURRENT_DIR)

import db


def main():
    db.init_db()
    parser = argparse.ArgumentParser(description="AI Knowledge Database CLI")
    subparsers = parser.add_subparsers(dest="command", help="利用可能なコマンド")

    p_search = subparsers.add_parser("search", help="知見をキーワード検索")
    p_search.add_argument("query", help="検索キーワード")
    p_search.add_argument("-c", "--category", help="カテゴリ絞り込み", default=None)
    p_search.add_argument("-l", "--limit", help="最大取得件数", type=int, default=5)

    p_list = subparsers.add_parser("list", help="最近の知見一覧を表示")
    p_list.add_argument("-c", "--category", help="カテゴリ絞り込み", default=None)
    p_list.add_argument("-l", "--limit", help="最大取得件数", type=int, default=10)

    p_get = subparsers.add_parser("get", help="知見の全文を表示")
    p_get.add_argument("id", help="知見ID", type=int)

    p_add = subparsers.add_parser("add", help="新しい知見を追加")
    p_add.add_argument("-t", "--title", required=True, help="タイトル")
    p_add.add_argument("-b", "--body", required=True, help="本文")
    p_add.add_argument("-c", "--category", default="general", help="カテゴリ")
    p_add.add_argument("--tags", default="", help="タグ(カンマ区切り)")
    p_add.add_argument("--source", default="manual", help="記録元ツール名")

    p_del = subparsers.add_parser("delete", help="知見を削除")
    p_del.add_argument("id", help="知見ID", type=int)

    subparsers.add_parser("categories", help="カテゴリ一覧と登録件数を表示")

    args = parser.parse_args()

    if args.command == "search":
        results = db.search_knowledge(
            query=args.query, category=args.category, limit=args.limit
        )
        print(f"\n=== 検索結果: {len(results)}件 (キーワード: '{args.query}') ===")
        for r in results:
            print(f"\n[ID: {r['id']}] {r['title']}")
            print(
                f"カテゴリ: {r['category']} | タグ: {r['tags']} | ツール: {r['source_tool']} | 更新日: {r['updated_at']}"
            )
            preview = r["content"][:150] + ("..." if len(r["content"]) > 150 else "")
            print(f"内容:\n  {preview}")

    elif args.command == "list":
        results = db.list_recent_knowledge(limit=args.limit, category=args.category)
        print(f"\n=== 最新知見一覧 ({len(results)}件) ===")
        for r in results:
            print(
                f"- [ID: {r['id']}] [{r['category']}] {r['title']} "
                f"(タグ: {r['tags']}, ツール: {r['source_tool']}, 更新: {r['updated_at']})"
            )

    elif args.command == "get":
        item = db.get_knowledge(item_id=args.id)
        if not item:
            print(f"ID: {args.id} の知見は見つかりませんでした。")
            return
        print(f"\n==========================================")
        print(f"[ID: {item['id']}] {item['title']}")
        print(f"カテゴリ: {item['category']} | タグ: {item['tags']}")
        print(
            f"記録元: {item['source_tool']} | 作成日: {item['created_at']} | 最終更新: {item['updated_at']}"
        )
        print(f"==========================================")
        print(f"\n{item['content']}\n")

    elif args.command == "add":
        item = db.save_knowledge(
            title=args.title,
            content=args.body,
            category=args.category,
            tags=args.tags,
            source_tool=args.source,
        )
        print(f"知見を保存しました。(ID: {item['id']})")

    elif args.command == "delete":
        ok = db.delete_knowledge(item_id=args.id)
        if ok:
            print(f"ID: {args.id} の知見を削除しました。")
        else:
            print(f"ID: {args.id} の削除に失敗しました。")

    elif args.command == "categories":
        cats = db.list_categories()
        print("\n=== 登録カテゴリ一覧 ===")
        for c in cats:
            print(f"- {c['category']}: {c['count']}件 (最終更新: {c['last_updated']})")

    else:
        parser.print_help()


if __name__ == "__main__":
    main()

ターミナルから以下のように手軽に検索や追加が行えます。

# キーワードで検索
python cli.py search "Next.js"

# 最新10件の一覧を表示
python cli.py list

# IDを指定して全文を表示
python cli.py get 1

Markdown一括エクスポート(export_md.py)

DB内の全知見をカテゴリ別のMarkdownファイルとして書き出す C:\GoogleDrive\AI_work\ai-knowledge-db\export_md.py です。

"""
SQLite Knowledge Database Export to Markdown
"""

import os
import db

OUTPUT_DIR = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "markdown_exports"
)


def export_all():
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    with db.get_db_connection() as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM knowledge ORDER BY id ASC;")
        rows = cursor.fetchall()

    for row in rows:
        cat_dir = os.path.join(OUTPUT_DIR, row["category"])
        os.makedirs(cat_dir, exist_ok=True)
        safe_title = "".join(
            c for c in row["title"] if c.isalnum() or c in (" ", "_", "-")
        ).strip()
        filename = f"{row['id']:04d}_{safe_title[:30]}.md"
        filepath = os.path.join(cat_dir, filename)

        with open(filepath, "w", encoding="utf-8") as f:
            f.write(f"# {row['title']}\n\n")
            f.write(f"- ID: {row['id']}\n")
            f.write(f"- カテゴリ: {row['category']}\n")
            f.write(f"- タグ: {row['tags']}\n")
            f.write(f"- ツール: {row['source_tool']}\n")
            f.write(f"- 更新日: {row['updated_at']}\n\n")
            f.write(row["content"])

    print(f"エクスポート完了: {len(rows)}件を出力しました。")


if __name__ == "__main__":
    export_all()

python export_md.py を実行すると、markdown_exports/ 配下にカテゴリごとのMarkdownファイルが出力されます。Obsidianの保管庫(Vault)として開いたり、テキストエディタで一覧したりする際に役立ちます。

よくある質問・注意点

クラウド同期ソフトのコンフリクト(競合コピー)が起きたら?

複数のPCで同時に書き込みが発生したり、オフライン状態から復帰したりした際、Googleドライブが knowledge (1).db のような競合ファイルを生成することがあります。

1台のPCをメインで使っている限り発生頻度は低いですが、もし競合ファイルができた場合は python cli.py list で最新データが入っている方のファイルを確認し、ファイル名を元の knowledge.db に揃えてください。また、定期的に export_md.py でテキスト出力しておくと、差分確認や復旧がさらに簡単になります。

個人情報やAPIキーの混入を防ぐには?

AIに会話を自動保存させる運用を行う場合、.env ファイルのシークレットやAPIキー、PC固有のフルパスなどがそのままナレッジDBに書き込まれないよう注意が必要です。

各AIのシステムプロンプトやルールファイルに「APIキー、パスワード、秘密鍵、個人情報はナレッジDBに記録せず、API_KEY や <username> などのプレースホルダーに置き換えて保存すること」と明記しておくことをおすすめします。

まとめ

Google Antigravity、Claude Code、Cursor、Codexなど、複数のAIツールを横断して会話履歴や知見を共有するローカルナレッジベースの構築手順を解説しました。

今回のポイント

  • SQLiteのFTS5全文検索と LIKE 検索のハイブリッド構成により、無料で高速な日本語ナレッジ検索を実現できる
  • Googleドライブのミラーリング設定に加え、WALモード、busy_timeout、ステートレス接続を組み合わせることで、クラウド同期下でもファイルロックを防ぎながら安全に運用できる
  • 各AIツールのMCP設定に server.py を登録するだけで、ツール間の「記憶の分断」を解消できる
  • PCの故障や買い替え時も、Googleドライブのフォルダを同期するだけで過去のやり取りをそのまま引き継げる

AIとの対話で得た知見を一度きりで流してしまうのではなく、検索可能な自分の資産として蓄積していくために、ぜひ試してみてください。

次に読むおすすめ記事

複数のAIエージェント(Codex・Claude・Antigravity)でスキルファイル(指示書)そのものを一元管理する方法や、生成AI・AIエージェント活用の全体像を体系的に学びたい方は、以下のガイドもあわせて参考にしてください。

【完全版】エンジニアのための生成AI・AIエージェント活用ガイド
2023年に始まった生成AIの爆発的な普及から数年が経過し、エンジニアやITビジネスパーソンにおけるAIの活用フェーズは「単なるチャットでの質問(Q&A)」から「自律型AIエージェントによる業務・開発の自動化」へと完全に移行しました。現在で…

参考情報