저는 최근 한 금융 핀테크 스타트업의 사내 AI 어시스턴트 프로젝트를 처음부터 설계하면서 가장 먼저 부딪힌 난관이 '권한 분리'였습니다. 엔지니어링 팀은 Jira에서만 티켓을 조회하고, 제품 마케팅 팀은 Notion에서만 사내 위키를 읽어야 하며, 일반 직원은 두 데이터 소스에 모두 접근할 수 있어야 했기 때문입니다. 동시에 사내 보안팀은 모든 도구 호출에 대한 감사 로그를 요구했습니다. 이 글에서는 Claude Code와 Model Context Protocol(MCP)을 활용하여 데이터 소스별로 권한을 명확히 분리한 멀티 테넌트 지식 허브를 구축한 전 과정을 공유합니다.

왜 MCP인가: 세 가지 접근 방식 비교

저는 처음에 Function Calling 기반의 커스텀 도구를 직접 구현했지만, 도구가 늘어날수록 토큰 컨텍스트가 폭증하고 권한 관리가 분산되는 문제가 있었습니다. MCP는 도구 호출을 표준화하고, 서버 단위에서 인증과 권한을 캡슐화할 수 있어 엔터프라이즈 환경에 훨씬 적합했습니다. 또한 MCP는 호출 측과 서버 측의 결합도를 낮춰, 향후 데이터 소스가 추가되더라도 게이트웨이 코드 변경 없이 신규 서버만 등록하면 됩니다.

비용 비교: 동일 시나리오 30일 운영 시 (월 230만 출력 토큰 가정)

저는 사내 봇 응답 품질이 중요한 핵심 케이스에는 Sonnet 4.5를, 단순 분류나 키워드 추출 같은 경량 작업에는 DeepSeek V3.2를 동적 라우팅하여 비용을 약 75% 절감했습니다. 모든 호출은

아키텍처 개요: 3계층 권한 분리

  • 1계층: MCP 서버 자체의 토큰 스코프 (엔지니어링용 Jira 토큰 / 마케팅용 Notion 토큰 분리)
  • 2계층: 사용자 역할 기반 게이트웨이 (엔지니어링, 마케팅, 어드민 매트릭스)
  • 3계층: 세션 누적 권한 그래프 (도구 체이닝 우회 공격 방어)

1계층: MCP 서버 설정 (claude_desktop_config.json)

저는 다음과 같이 두 개의 격리된 MCP 서버를 정의했습니다. Jira 서버는 엔지니어링 전용 토큰만, Notion 서버는 마케팅 전용 read-only 토큰만 사용하도록 스코프를 분리했습니다.

{
  "mcpServers": {
    "jira-engineering": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-jira"],
      "env": {
        "JIRA_HOST": "acme.atlassian.net",
        "JIRA_EMAIL": "[email protected]",
        "JIRA_API_TOKEN": "ENG_TEAM_SCOPED_TOKEN_xxx",
        "JIRA_ALLOWED_PROJECTS": "ENG,INFRA,DEVOPS",
        "JIRA_PERMISSION_MODE": "read_only"
      }
    },
    "notion-marketing": {
      "command": "npx",
      "args": ["-y", "@notionhq/mcp-server"],
      "env": {
        "NOTION_TOKEN": "secret_marketing_readonly_xxx",
        "NOTION_ALLOWED_DATABASE_IDS": "db_wiki,db_roadmap",
        "NOTION_DENY_PAGE_PATTERNS": "salary,hr-private,finance,confidential"
      }
    }
  }
}

2계층: 권한 분리 게이트 미들웨어 (Python)

MCP는 표준화된 호출 인터페이스를 제공하지만, 서버 자체에 권한 로직을 심어야 사내 컴플라이언스 요건을 충족할 수 있습니다. 저는 다음과 같은 게이트웨이를 작성해 모든 호출 직전에 사용자·팀·리소스 매트릭스를 검증했습니다. 이 코드는 그대로 복사하여 실행 가능한 완결된 형태입니다.

import asyncio, json
from typing import Dict, Any
from anthropic import AsyncAnthropic

HOLYSHEEP_BASE = "https://api.holysheep.ai/v1"
HOLYSHEEP_KEY = "YOUR_HOLYSHEEP_API_KEY"

PERMISSION_MATRIX = {
    "engineering_team": {
        "jira":   {"projects": ["ENG", "INFRA", "DEVOPS"], "ops": ["read"]},
        "notion": {"databases": [], "ops": []}
    },
    "marketing_team": {
        "jira":   {"projects": [], "ops": []},
        "notion": {"databases": ["db_wiki", "db_roadmap"], "ops": ["read"]}
    },
    "admin": {
        "jira":   {"projects": ["*"], "ops": ["read", "write"]},
        "notion": {"databases": ["*"], "ops": ["read", "write"]}
    }
}

DENY_PATTERNS = ["salary", "hr-private", "finance", "confidential"]


def authorize(user_role: str, server: str, payload: Dict[str, Any]) -> bool:
    rule = PERMISSION_MATRIX.get(user_role, {}).get(server)
    if not rule:
        return False
    text = json.dumps(payload, ensure_ascii=False).lower()
    if any(p in text for p in DENY_PATTERNS):
        return False
    if server == "jira":
        proj = payload.get("arguments", {}).get("projectKey", "")
        if "*" not in rule["projects"] and proj not in rule["projects"]:
            return False
        op = payload.get("arguments", {}).get("operation", "read")
        if op not in rule["ops"]:
            return False
    elif server == "notion":
        db = payload.get("arguments", {}).get("database_id", "")
        if "*" not in rule["databases"] and db not in rule["databases"]:
            return False
    return True


async def call_claude_with_mcp(prompt: str, user_role: str):
    client = AsyncAnthropic(api_key=HOLYSHEEP_KEY, base_url=HOLYSHEEP_BASE)
    tools = [
        {"name": "jira_search",   "description": "Jira 이슈 검색",
         "input_schema": {"type": "object",
                          "properties": {"projectKey": {"type": "string"},
                                         "jql": {"type": "string"}}}},
        {"name": "notion_query",  "description": "Notion DB 조회",
         "input_schema": {"type": "object",
                          "properties": {"database_id": {"type": "string"},
                                         "filter": {"type": "object"}}}}
    ]
    resp = await client.messages.create(
        model="claude-sonnet-4.5",
        max_tokens=1024,
        tools=tools,
        messages=[{"role": "user", "content": prompt}]
    )
    for blk in resp.content:
        if blk.type == "tool_use":
            payload = {"name": blk.name, "arguments": blk.input}
            if not authorize(user_role, blk.name.split("_")[0], payload):
                return {"error": "PERMISSION_DENIED",
                        "tool": blk.name, "role": user_role}
    return resp


if __name__ == "__main__":
    print(asyncio.run(call_claude_with_mcp("ENG 팀 진행 중 티켓 알려줘", "engineering_team")))

3계층: 세션 누적 권한 그래프와 MCP 라우터 (Node.js, 프로덕션용)

저는 운영 환경에 배포할 때 다음과 같이 호출을 라우팅하고 감사 로그를 남기는 Node.js 서버를 사용했습니다. 모든 호출은 HolySheep AI를 통해 표준화되어 비용 추적이 단일 키에서 가능했습니다. 동시에 세션별로 접근한 리소스를 누적 기록하여 도구 체이닝 우회 공격을 방어했습니다.

import express from "express";
import { spawn } from "child_process";

const app = express();
app.use(express.json());

const ALLOWED_USERS = new Set(["alice", "bob", "carol"]);
const SERVER_SCOPE = {
  alice: ["jira-engineering"],
  bob:   ["notion-marketing"],
  carol: ["jira-engineering", "notion-marketing"]
};
const MAX_RESOURCES_PER_SESSION = 5;
const sessionLog = new Map();

function callMCPServer(serverName, payload) {
  return new Promise((resolve, reject) => {
    const proc = spawn("npx", ["-y", "@modelcontextprotocol/cli",
                                "call", serverName, JSON.stringify(payload)]);
    let out = "", err = "";