본문 바로가기
인공지능 (AI,GPT)

“토큰 절약”의 정체, MCP + SQLite + FTS5로 구현하는 Context Mode

by 날으는물고기 2026. 4. 24.

“토큰 절약”의 정체, MCP + SQLite + FTS5로 구현하는 Context Mode

728x90

요즘 보이는 Context Mode / MCP / Claude mem / caveman 스타일 전부 하나의 흐름입니다.

❌ 토큰을 줄인다
✅ LLM이 불필요한 데이터를 “안 보게 만든다”

왜 이게 유행인가?

AI 코딩 에이전트 쓰면 바로 겪습니다.

  • 세션 길어지면 느려짐
  • 맥락 깨짐
  • 비용 증가
  • 대용량 데이터 처리 불가
300x250

이유는 단순합니다.

LLM = 입력된 모든 텍스트를 다 읽는다

[Raw Data] → [Sandbox / DB 저장]
                 ↓
        [검색 / 코드 실행]
                 ↓
          [결과만 LLM 전달]

핵심

“LLM은 처리하지 말고, 결과만 받아라”

토큰 절약 4대 전략

① 압축 (Compaction)

Before

대화 전체 계속 누적

After

Session Summary:
- 로그 분석 수행
- timeout 에러 발견
- 사용자 재분석 요청

👉 효과

  • 세션 유지 (30분 → 수시간)
  • 토큰 감소

👉 위험

  • 과도한 요약 → 정보 손실

② 검색 기반 구조 (FTS5 + BM25)

“전체를 넣지 말고 필요한 것만 검색”

SQLite + FTS5 구현

CREATE VIRTUAL TABLE events USING fts5(content);

INSERT INTO events(content)
VALUES ('user edited file'), ('error timeout occurred');

SELECT content
FROM events
WHERE events MATCH 'error'
ORDER BY bm25(events)
LIMIT 5;

👉 핵심

“세션을 DB로 만들고 검색한다”

③ 실행 분리 (Execution Isolation)

잘못된 방식

코드 전체 붙여넣고 "함수 몇 개?"

올바른 방식

import re

with open("app.py") as f:
    code = f.read()

print(len(re.findall(r"def ", code)))

추가 예시

# 로그 분석
grep -i error app.log | tail -n 200

# JSON 분석
jq '.users | length' data.json

# Git diff
git diff HEAD~1 | head -n 200

👉 효과

  • 토큰 90% 절감
  • 정확도 상승
  • hallucination 감소

④ 출력 압축 (Output Compression)

장황한 출력

The issue appears to be related to a timeout error...

압축 출력

- error: timeout
- cause: db connection
- fix: increase pool

👉 효과

  • 출력 토큰 65~75% 감소

MCP 아키텍처 완전 이해

MCP 구조

Host (LLM)
   ↓
Client
   ↓
MCP Server
   ├─ Tools
   ├─ Resources
   └─ Prompts

핵심 역할

컨텍스트 외부화

  • SQLite 저장
  • 파일 시스템
  • 캐시

이벤트 기반 관리

CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  type TEXT,
  content TEXT,
  created_at DATETIME
);

검색 기반 복구

SELECT * FROM events
WHERE content MATCH 'error'
ORDER BY bm25(events)
LIMIT 10;

실행 분리

ctx_execute("grep error app.log")

👉 결론

MCP = “LLM을 DB + OS처럼 쓰는 구조”

ctx_* MCP 도구 전체 구조

도구 설명
ctx_execute 단일 명령 실행
ctx_batch_execute 다중 작업
ctx_execute_file 파일 기반 실행
ctx_index 데이터 저장
ctx_search 검색
ctx_fetch_and_index URL → 저장
ctx_stats 상태 확인
ctx_doctor 문제 진단
ctx_upgrade 업데이트
ctx_purge 데이터 삭제
ctx_insight 분석

👉 이건 사실상

“LLM 전용 운영체제”

Hook 기반 제어

Hook 종류

  • PreToolUse
  • PostToolUse
  • SessionStart
  • PreCompact

예시

def pre_tool_use(cmd):
    if "rm -rf" in cmd:
        raise Exception("blocked")
def pre_compact(context):
    return summarize(context)

👉 의미

“언제 실행하고, 언제 압축할지 제어”

세션 연속성 (Session Continuity)

구조

  1. 이벤트 저장
  2. 요약 저장
  3. 검색
  4. 재조합

기존 방식

대화 계속 유지

MCP 방식

필요한 부분만 재구성

👉 효과

  • 세션 30분 → 3시간 이상 가능
  • 메모리 문제 해결

플랫폼별 차이

플랫폼 특징
Claude Code Hook 강력
Cursor 제한적
Codex CLI 자유도 높음
Gemini CLI 통합형

👉 핵심

“같은 MCP라도 기능 차이 있음”

보안 관점

위험 요소

명령 실행

ctx_execute("rm -rf /")

외부 fetch

ctx_fetch_and_index("http://malicious.site")

자동 승인

보안 가이드

allow / deny 정책

deny:
  - "rm -rf *"
  - "curl * | bash"
allow:
  - "grep *"
  - "jq *"

샌드박스

  • container
  • read-only FS

로그 기록

audit.log

사용자 승인

👉 핵심

“토큰 절약보다 실행 통제가 더 중요”

운영 관점

✔ 로컬 실행

  • telemetry 없음
  • 데이터 외부 유출 없음

✔ SQLite 기반

  • 가볍고 빠름
  • FTS5 검색 가능

✔ License

  • Elastic License 2.0
  • SaaS 제공 제한

✔ 관리 도구

  • ctx_stats → 상태
  • ctx_doctor → 문제 진단
  • ctx_purge → 데이터 관리

바로 따라해보기 (미니 구현)

SQLite + FTS5

sqlite3 ctx.db
CREATE VIRTUAL TABLE events USING fts5(content);
INSERT INTO events VALUES ('error timeout'), ('file updated');

Python 검색

import sqlite3

conn = sqlite3.connect("ctx.db")
cursor = conn.cursor()

cursor.execute("""
SELECT content FROM events
WHERE events MATCH 'error'
ORDER BY bm25(events)
LIMIT 5;
""")

print(cursor.fetchall())

실행 분리 적용

grep error app.log > result.txt

→ 결과만 LLM에 전달

가장 중요한 3가지

1️⃣ LLM에 원본 데이터를 주지 말 것
2️⃣ 코드/명령으로 먼저 처리할 것
3️⃣ 필요한 결과만 전달할 것

“토큰 절약은 기술이 아니라 아키텍처다”

추천 구조

실제로는 이렇게 나누는 게 제일 편합니다.

ctx-mode/
├─ store.py        # SQLite + FTS5 저장/검색 로직
├─ mcp_server.py   # MCP tools
├─ api.py          # FastAPI 운영용 API
└─ requirements.txt

이렇게 분리하면 좋은 점은 세 가지입니다.

  1. MCP 호스트(Claude Code, Cursor 등)는 mcp_server.py만 붙이면 됩니다.
  2. 운영팀은 api.py로 상태 확인, 검색, purge를 할 수 있습니다.
  3. 둘 다 같은 SQLite 파일을 보므로 세션 이력이 한 곳에 모입니다.

requirements.txt

fastapi
uvicorn[standard]
mcp
pydantic

store.py — SQLite + FTS5 핵심 저장소

from __future__ import annotations

import json
import sqlite3
import threading
from pathlib import Path
from typing import Any

DEFAULT_DB_PATH = Path("~/.ctx_mode/ctx.db").expanduser()


class SQLiteStore:
    def __init__(self, db_path: Path | str = DEFAULT_DB_PATH):
        self.db_path = Path(db_path).expanduser()
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

        self.lock = threading.Lock()
        self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
        self.conn.row_factory = sqlite3.Row

        # 운영 편의용 기본 설정
        self.conn.execute("PRAGMA journal_mode=WAL;")
        self.conn.execute("PRAGMA synchronous=NORMAL;")
        self.conn.execute("PRAGMA foreign_keys=ON;")

        self.init_db()

    def init_db(self) -> None:
        with self.lock:
            self.conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS events (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    event_type TEXT NOT NULL,
                    content TEXT NOT NULL,
                    source TEXT NOT NULL DEFAULT 'manual',
                    meta_json TEXT NOT NULL DEFAULT '{}',
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                );

                CREATE INDEX IF NOT EXISTS idx_events_created_at
                ON events(created_at);

                CREATE TABLE IF NOT EXISTS session_summary (
                    id INTEGER PRIMARY KEY CHECK (id = 1),
                    summary TEXT NOT NULL DEFAULT '',
                    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
                );

                INSERT OR IGNORE INTO session_summary(id, summary)
                VALUES (1, '');

                CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
                    content,
                    event_type,
                    source,
                    content='events',
                    content_rowid='id',
                    tokenize='unicode61'
                );

                CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
                    INSERT INTO events_fts(rowid, content, event_type, source)
                    VALUES (new.id, new.content, new.event_type, new.source);
                END;

                CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
                    INSERT INTO events_fts(events_fts, rowid, content, event_type, source)
                    VALUES ('delete', old.id, old.content, old.event_type, old.source);
                END;

                CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
                    INSERT INTO events_fts(events_fts, rowid, content, event_type, source)
                    VALUES ('delete', old.id, old.content, old.event_type, old.source);
                    INSERT INTO events_fts(rowid, content, event_type, source)
                    VALUES (new.id, new.content, new.event_type, new.source);
                END;
                """
            )
            self.conn.commit()

    def add_event(
        self,
        content: str,
        event_type: str = "note",
        source: str = "manual",
        meta: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        meta_json = json.dumps(meta or {}, ensure_ascii=False)

        with self.lock:
            cur = self.conn.execute(
                """
                INSERT INTO events (event_type, content, source, meta_json)
                VALUES (?, ?, ?, ?)
                """,
                (event_type, content, source, meta_json),
            )
            self.conn.commit()

            row = self.conn.execute(
                """
                SELECT id, event_type, content, source, meta_json, created_at
                FROM events
                WHERE id = ?
                """,
                (cur.lastrowid,),
            ).fetchone()

        return self._row_to_dict(row)

    def search(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
        sql = """
            SELECT
                e.id,
                e.event_type,
                e.content,
                e.source,
                e.meta_json,
                e.created_at,
                bm25(events_fts) AS score,
                snippet(events_fts, 0, '[', ']', '…', 12) AS snippet
            FROM events_fts
            JOIN events e ON e.id = events_fts.rowid
            WHERE events_fts MATCH ?
            ORDER BY score
            LIMIT ?;
        """

        with self.lock:
            try:
                rows = self.conn.execute(sql, (query, limit)).fetchall()
            except sqlite3.OperationalError:
                # FTS 구문이 깨질 때를 대비한 단순 검색 fallback
                fallback_query = '"' + query.replace('"', '""') + '"'
                rows = self.conn.execute(sql, (fallback_query, limit)).fetchall()

        return [self._row_to_dict(row) for row in rows]

    def list_recent(self, limit: int = 20) -> list[dict[str, Any]]:
        with self.lock:
            rows = self.conn.execute(
                """
                SELECT id, event_type, content, source, meta_json, created_at
                FROM events
                ORDER BY id DESC
                LIMIT ?
                """,
                (limit,),
            ).fetchall()

        return [self._row_to_dict(row) for row in rows]

    def stats(self) -> dict[str, Any]:
        with self.lock:
            total = self.conn.execute("SELECT COUNT(*) FROM events").fetchone()[0]
            recent_24h = self.conn.execute(
                """
                SELECT COUNT(*)
                FROM events
                WHERE created_at >= datetime('now', '-1 day')
                """
            ).fetchone()[0]

            by_type_rows = self.conn.execute(
                """
                SELECT event_type, COUNT(*) AS cnt
                FROM events
                GROUP BY event_type
                ORDER BY cnt DESC, event_type ASC
                """
            ).fetchall()

        return {
            "db_path": str(self.db_path),
            "total_events": total,
            "events_last_24h": recent_24h,
            "by_type": [
                {"event_type": row["event_type"], "count": row["cnt"]}
                for row in by_type_rows
            ],
        }

    def get_summary(self) -> dict[str, Any]:
        with self.lock:
            row = self.conn.execute(
                "SELECT summary, updated_at FROM session_summary WHERE id = 1"
            ).fetchone()

        return {"summary": row["summary"], "updated_at": row["updated_at"]}

    def set_summary(self, summary: str) -> dict[str, Any]:
        with self.lock:
            self.conn.execute(
                """
                UPDATE session_summary
                SET summary = ?, updated_at = datetime('now')
                WHERE id = 1
                """,
                (summary,),
            )
            self.conn.commit()

        return self.get_summary()

    def purge_older_than_days(self, days: int) -> dict[str, Any]:
        with self.lock:
            before = self.conn.execute("SELECT COUNT(*) FROM events").fetchone()[0]
            self.conn.execute(
                "DELETE FROM events WHERE created_at < datetime('now', ?)",
                (f"-{days} days",),
            )
            self.conn.commit()
            after = self.conn.execute("SELECT COUNT(*) FROM events").fetchone()[0]

        return {"deleted": before - after, "remaining": after}

    def doctor(self) -> dict[str, Any]:
        with self.lock:
            version = self.conn.execute("SELECT sqlite_version()").fetchone()[0]
            journal_mode = self.conn.execute("PRAGMA journal_mode").fetchone()[0]

        return {
            "sqlite_version": version,
            "journal_mode": journal_mode,
            "db_path": str(self.db_path),
            "fts5_ready": True,
        }

    @staticmethod
    def _row_to_dict(row: sqlite3.Row | None) -> dict[str, Any]:
        if row is None:
            return {}

        data = dict(row)
        if "meta_json" in data:
            try:
                data["meta"] = json.loads(data["meta_json"] or "{}")
            except json.JSONDecodeError:
                data["meta"] = {}
        return data

mcp_server.py — 실제 MCP 서버

FastMCP는 tool 정의를 함수 시그니처와 docstring으로 자동 생성해 주기 때문에, 이 부분이 제일 깔끔합니다. 공식 예제도 같은 방식으로 tool/resource/prompt를 정의하고 있습니다.

from __future__ import annotations

import os
from typing import Any

from mcp.server.fastmcp import FastMCP

from store import SQLiteStore

store = SQLiteStore()
mcp = FastMCP("ctx-mode", json_response=True)


@mcp.tool()
def ctx_index(
    content: str,
    event_type: str = "note",
    source: str = "manual",
    meta: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """세션 이벤트나 지식 조각을 SQLite + FTS5에 저장한다."""
    return store.add_event(
        content=content,
        event_type=event_type,
        source=source,
        meta=meta,
    )


@mcp.tool()
def ctx_search(query: str, limit: int = 10) -> dict[str, Any]:
    """FTS5로 저장된 이벤트를 검색한다."""
    return {"query": query, "limit": limit, "results": store.search(query, limit)}


@mcp.tool()
def ctx_recent(limit: int = 20) -> dict[str, Any]:
    """가장 최근 이벤트를 가져온다."""
    return {"limit": limit, "results": store.list_recent(limit)}


@mcp.tool()
def ctx_stats() -> dict[str, Any]:
    """DB 상태와 이벤트 통계를 보여준다."""
    return store.stats()


@mcp.tool()
def ctx_get_summary() -> dict[str, Any]:
    """세션 요약을 읽는다."""
    return store.get_summary()


@mcp.tool()
def ctx_set_summary(summary: str) -> dict[str, Any]:
    """세션 요약을 저장한다."""
    return store.set_summary(summary)


@mcp.tool()
def ctx_purge(days: int = 30) -> dict[str, Any]:
    """오래된 이벤트를 삭제한다."""
    return store.purge_older_than_days(days)


@mcp.tool()
def ctx_doctor() -> dict[str, Any]:
    """SQLite/FTS5 상태를 점검한다."""
    return store.doctor()


def main() -> None:
    transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()

    if transport == "streamable-http":
        mcp.run(transport="streamable-http")
    else:
        mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

api.py — FastAPI 운영용 API

FastAPI는 API 서버를 빠르게 만들기에 적합합니다. 공식 문서도 고성능, 타입 힌트 기반 개발을 강조합니다.

from __future__ import annotations

from typing import Any

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

from store import SQLiteStore

store = SQLiteStore()
app = FastAPI(title="Context Mode API", version="1.0.0")


class IndexRequest(BaseModel):
    content: str = Field(min_length=1)
    event_type: str = "note"
    source: str = "manual"
    meta: dict[str, Any] = Field(default_factory=dict)


class SearchRequest(BaseModel):
    query: str = Field(min_length=1)
    limit: int = Field(default=10, ge=1, le=100)


class PurgeRequest(BaseModel):
    days: int = Field(default=30, ge=1, le=3650)


@app.get("/healthz")
def healthz() -> dict[str, Any]:
    return {"ok": True, "doctor": store.doctor()}


@app.post("/index")
def index(req: IndexRequest) -> dict[str, Any]:
    return store.add_event(
        content=req.content,
        event_type=req.event_type,
        source=req.source,
        meta=req.meta,
    )


@app.post("/search")
def search(req: SearchRequest) -> dict[str, Any]:
    return {"query": req.query, "limit": req.limit, "results": store.search(req.query, req.limit)}


@app.get("/recent")
def recent(limit: int = 20) -> dict[str, Any]:
    return {"limit": limit, "results": store.list_recent(limit)}


@app.get("/stats")
def stats() -> dict[str, Any]:
    return store.stats()


@app.get("/summary")
def get_summary() -> dict[str, Any]:
    return store.get_summary()


@app.post("/summary")
def set_summary(payload: dict[str, str]) -> dict[str, Any]:
    summary = payload.get("summary", "").strip()
    if not summary:
        raise HTTPException(status_code=400, detail="summary is required")
    return store.set_summary(summary)


@app.post("/purge")
def purge(req: PurgeRequest) -> dict[str, Any]:
    return store.purge_older_than_days(req.days)

실행 방법

uv venv
source .venv/bin/activate
uv pip install -r requirements.txt

FastAPI는 이렇게 띄우면 됩니다.

uvicorn api:app --reload --host 0.0.0.0 --port 8001

MCP 서버는 stdio로 띄우는 게 가장 기본입니다.

python mcp_server.py

streamable-http로 쓰고 싶으면

MCP_TRANSPORT=streamable-http python mcp_server.py

공식 MCP 문서도 stdiostreamable-http 같은 transport를 지원하는 예시를 제공합니다.

실제 사용 예시

이벤트 저장

{
  "content": "사용자가 prod 배포 후 timeout 에러를 보고함",
  "event_type": "error",
  "source": "slack",
  "meta": {
    "channel": "#incident",
    "severity": "high"
  }
}

검색

{
  "query": "timeout error",
  "limit": 5
}

결과는 bm25()로 정렬되고, snippet()으로 일부 문맥이 잘려 나옵니다. FTS5 공식 문서는 MATCH 검색과 bm25(), snippet() 같은 보조 함수를 명시합니다.

이 구조가 잘 맞는 경우

이 패턴은 아래에 특히 잘 맞습니다.

  • 코드 리뷰 기록
  • Slack/이슈 요약
  • 세션 이벤트 추적
  • 장기 작업의 중간 상태 저장
  • AI 코딩 에이전트의 compact/restore
  • 운영 이력 검색

반대로, 초고속 쓰기 경쟁이 심한 대규모 멀티유저 백엔드에는 SQLite 하나로 끝내기보다 별도 DB가 더 낫습니다. 여기서는 “로컬 세션 메모리 + 검색”이라는 목적에 맞춰 설계했습니다.

다음 단계로 바로 붙일 것

이제 이 기본형에 아래를 붙이면 꽤 쓸만해집니다.

  • ctx_fetch_and_index(url)
    → URL 내용을 긁어 와서 저장
  • ctx_batch_index(items[])
    → 여러 이벤트 한 번에 저장
  • ctx_compact()
    → 최근 이벤트를 요약해서 session_summary 갱신
  • ctx_insight()
    → 최근 에러/파일/태그 통계 리포트
  • deny/allow 정책
    → 나중에 ctx_execute를 붙일 때 필수

추천 운영 원칙

  1. 원본 데이터는 SQLite에만 저장하고, LLM에는 요약·검색 결과만 넘깁니다.
  2. 실행 도구는 최소화합니다. ctx_search, ctx_stats, ctx_recent, ctx_compact처럼 읽기/요약 위주가 좋습니다.
  3. 위험한 작업은 분리합니다. ctx_execute는 별도 허가가 있어야만 쓰도록 둡니다.
  4. 로컬이면 stdio, 원격이면 Streamable HTTP로 갑니다.

Claude Code 최적 세팅

Claude Code는 기본적으로 read-only는 허용, 편집/실행/네트워크는 승인 필요 구조입니다. 권한은 /permissions로 관리할 수 있고, settings.json과 훅으로 세밀하게 제어할 수 있습니다.

추천 파일 구조

project/
├─ CLAUDE.md
├─ .claude/
│  ├─ settings.json
│  ├─ hooks/
│  └─ rules/
└─ .mcp.json

Claude Code는 프로젝트와 ~/.claude에서 CLAUDE.md, settings.json, hooks, rules, memory를 읽고, 프로젝트 MCP 서버는 .mcp.json에 둡니다.

CLAUDE.md 는 이렇게

여기는 길게 쓰지 말고, 딱 프로젝트 컨텍스트 + 작업 원칙만 넣는 게 좋습니다. Claude Code는 CLAUDE.md를 세션마다 읽습니다.

# Project Instructions

- 이 프로젝트는 ctx-mode MCP 서버를 사용한다.
- 검색 우선, 원문 전체 출력 금지.
- ctx_search / ctx_stats / ctx_recent 우선 사용.
- 위험한 shell 명령은 실행 전 반드시 확인한다.
- 세션 요약은 compact 후에만 갱신한다.

.claude/settings.json 권장값

Claude Code는 ~/.claude/settings.json과 프로젝트 .claude/settings.json을 지원합니다. 권한 예시도 공식 문서에 있습니다.

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Read(*)",
      "Grep(*)",
      "Bash(npm run lint)",
      "Bash(npm run test *)"
    ],
    "deny": [
      "Bash(curl *)",
      "Bash(wget *)",
      "Bash(rm -rf *)",
      "Read(./.env)",
      "Read(./secrets/**)"
    ]
  },
  "env": {
    "MCP_TRANSPORT": "stdio"
  }
}

핵심은 이겁니다.

  • Read, Grep는 넓게 허용
  • Bash는 lint/test만 제한적으로 허용
  • 네트워크 다운로드, 파괴적 명령, 비밀 파일 읽기는 차단
  • MCP 서버는 stdio로 돌림

훅 설정은 Claude Code 쪽이 제일 강합니다

Claude Code 훅은 PreToolUse, PreCompact, SessionStart, PostToolUse 같은 이벤트에 걸 수 있고, PreToolUse는 tool 이름을 기준으로 allow/deny/ask/defer를 제어할 수 있습니다. PreCompact는 자동/수동 compact 직전에 요약이나 상태 갱신에 쓰기 좋습니다.

추천 훅 배치

  • SessionStart
    → 마지막 세션 요약 읽어서 복구
  • PreToolUse
    → 위험 명령 차단, ctx_execute는 별도 검사
  • PostToolUse
    → 이벤트 DB에 기록
  • PreCompact
    → 최근 이벤트를 요약하고 session_summary 갱신

이 조합이 Claude Code에서는 가장 실용적입니다. 훅이 여러 이벤트에 걸쳐 지원되고, prompt/agent 훅까지 확장 가능합니다.

Cursor 최적 세팅

Cursor는 rules + MCP + permissions 조합이 핵심입니다. 프로젝트 규칙은 .cursor/rules에 두고, 규칙 우선순위는 Team → Project → User 순입니다.

추천 파일 구조

project/
├─ .cursor/
│  ├─ rules/
│  └─ mcp.json
└─ AGENTS.md

Cursor는 프로젝트 규칙을 .cursor/rules에 버전관리 파일로 저장하고, AGENTS.md는 더 단순한 대안입니다.

.cursor/rules 추천

여기는 코드 스타일보다 에이전트 행동 규칙을 넣는 게 중요합니다.

---
description: Context Mode usage rule
alwaysApply: true
---

- 원문 로그를 통째로 붙이지 말고 ctx_search를 먼저 사용한다.
- 요약이 가능하면 요약본만 반환한다.
- 파일 변경 전에는 변경 범위를 먼저 설명한다.
- 위험한 shell 명령은 반드시 확인한다.

Cursor 문서상 alwaysApply, description, globs로 적용 범위를 조절할 수 있고, 규칙은 특정 파일에만 붙이거나 자동 적용할 수 있습니다.

.cursor/mcp.json

Cursor는 프로젝트별 .cursor/mcp.json, 전역 ~/.cursor/mcp.json을 지원합니다. stdio 서버는 command, args, env, envFile을 쓰고, 원격 서버는 url, headers를 씁니다.

{
  "mcpServers": {
    "ctx-mode": {
      "command": "python",
      "args": ["./mcp_server.py"],
      "env": {
        "MCP_TRANSPORT": "stdio"
      }
    }
  }
}

원격 서버

{
  "mcpServers": {
    "ctx-mode": {
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer ${env:CTX_MODE_TOKEN}"
      }
    }
  }
}

Cursor는 MCP를 통해 Tools, Prompts, Resources를 지원하고, stdio, SSE, Streamable HTTP를 사용합니다.

permissions.json

Cursor는 MCP 도구를 기본적으로 사용자 승인 후 사용하고, 자동 실행하려면 ~/.cursor/permissions.json에 미리 넣을 수 있습니다.

  • ctx_search, ctx_stats, ctx_recent → auto-run 허용
  • ctx_execute, ctx_purge → 승인 필요
  • rm, curl, wget 같은 터미널 명령은 자동 실행 금지

Claude Code와 Cursor를 같이 쓸 때 제일 좋은 조합

Claude Code

  • 권한 제어: 강하게
  • 훅: 적극적으로
  • compact: 자동/수동 모두 활용
  • 세션 복구: PreCompactSessionStart 중심

Cursor

  • 규칙: 프로젝트 단위로 명확하게
  • MCP: 프로젝트 .cursor/mcp.json로 고정
  • auto-run: 읽기 전용 도구만 제한 허용
  • 팀 배포: Team Rules / Team marketplace 활용

Cursor 팀 규칙은 프로젝트/유저보다 우선하고, 강제(enforce)하면 사용자가 끌 수 없습니다.

당신의 ctx-mode 서버에 맞춘 권장값

이 서버는 검색형 메모리 + 세션 복구형 MCP이므로, 아래처럼 나누는 게 가장 좋습니다.

반드시 켤 것

  • ctx_search
  • ctx_recent
  • ctx_stats
  • ctx_doctor
  • ctx_get_summary
  • ctx_set_summary

조건부로만 쓸 것

  • ctx_index
  • ctx_purge
  • ctx_execute
  • ctx_batch_execute
  • ctx_fetch_and_index

즉, 모델이 항상 쓸 수 있는 건 읽기/검색/요약이고, 쓰기/삭제/실행은 승인형으로 두는 게 안전합니다. Claude Code는 기본적으로 추가 동작에 승인이 필요하고, Cursor도 MCP 도구를 기본 승인형으로 다룹니다.

가장 실전적인 추천 세트

개인 개발자용

  • MCP transport: stdio
  • Claude Code: allow는 최소, deny는 강하게
  • Cursor: .cursor/rules + .cursor/mcp.json
  • auto-run: 검색/조회만

팀 공유용

  • MCP transport: Streamable HTTP
  • 인증: bearer token
  • CLAUDE.md / AGENTS.md / .cursor/rules에 공통 원칙 명시
  • 관리형 정책으로 MCP 서버와 권한을 제한

MCP 공식 문서는 Streamable HTTP에서 인증과 Origin 검증이 중요하다고 말하고, Cursor는 원격 서버 등록과 OAuth/헤더 인증을 지원합니다.

바로 적용용 최소 세팅

Claude Code

  1. .claude/settings.json 작성
  2. CLAUDE.md 작성
  3. mcp_server.pystdio로 실행
  4. PreToolUse + PreCompact 훅 추가
  5. Read/Grep 위주 허용, Bash는 제한 허용

Cursor

  1. .cursor/rules 작성
  2. .cursor/mcp.json 작성
  3. ~/.cursor/permissions.json에 검색 도구만 auto-run 등록
  4. 팀이면 Team Rules로 공통 정책 배포
  5. AGENTS.md는 필요할 때만 추가
728x90
그리드형(광고전용)

댓글