아키텍처 개요: 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 = "";