Blueprint
한 줄 소개: 인간 운영자와 AI 에이전트가 함께 일하는 통합 워크스페이스. Next.js + Claude Agent SDK 로 13종 에이전트 실행, frame · stream · magnet MCP 서버를 도구로 호출.
기술 스택
| 항목 | 값 |
|---|---|
| 언어 | TypeScript |
| 프레임워크 | Next.js 16 (App Router) + React 19 |
| 스타일 | Tailwind CSS 4 + @axe/ui 디자인 토큰 (/Users/axe/axelabs/src/lib/, sync 됨 → src/app/_axe-ui/) |
| DB | PostgreSQL 16 (Prisma 6.19) — D-config-17 cutover 2026-05-15. SQLite 는 historical (cutover 이전 prisma/data/blueprint.db) |
| 인증 | NextAuth 4 + Microsoft Entra ID (Azure AD) |
| 에이전트 | @anthropic-ai/claude-agent-sdk 0.2.92 |
| 실시간 | Socket.IO 4.8 (port 3101 WebSocket) |
| 터미널 | node-pty 1.1 (PTY) |
| MCP 클라이언트 | @anthropic-ai/sdk 0.82 (HTTP/SSE transport) |
포트
| 포트 | 용도 |
|---|---|
| 3100 | Next.js dev / production HTTP |
| 3101 | Socket.IO WebSocket |
| 3180 | Caddy 외부 노출 (HTTPS) |
디렉토리 구조
/Users/axe/blueprint/
├── src/
│ ├── app/ Next.js App Router
│ │ ├── layout.tsx
│ │ ├── page.tsx landing
│ │ ├── onboard/ 가입 흐름
│ │ ├── axe/ 대시보드 (admin, agents, issues, projects, skills, team, trinity)
│ │ └── api/ 93+ route.ts
│ │ ├── auth/[...nextauth]
│ │ ├── agents/
│ │ ├── sessions/
│ │ ├── workspaces/
│ │ ├── graph/ Microsoft Graph API proxy
│ │ └── teams-graph-* Teams 통합
│ ├── lib/
│ │ ├── auth.ts NextAuth + Azure AD
│ │ ├── anthropic-client.ts
│ │ ├── customers.ts customers.yaml reader (mtime cache)
│ │ ├── mcp/
│ │ │ └── frame.ts frame HTTP MCP 등록, ~21개 tool wrapping
│ │ ├── agents/
│ │ │ └── execution-service.ts
│ │ └── teams/
│ ├── server/
│ │ └── websocket.ts Socket.IO + PTY (port 3101)
│ └── prisma/
│ └── schema.prisma Member / User / Agent / Session / Issue
├── docker/
│ └── docker-compose.yml
└── docs/
├── project-blueprint.md 47KB master doc (5 분과)
└── project-blueprint/
├── division1-org.md
├── division2-tech.md
├── division3-ops.md
├── division4-claude-code.md
└── division5-risk-governance.md데이터 모델 (요약)
Entity (axec / axev / sys — D-bp-entity-3 NOT NULL)
├─ slug UNIQUE (axec | axev | sys | …)
├─ name 한국어 정식 명칭 (hive shared.entity.legal_name mirror, D-bp-entity-4)
├─ kind "corporate" | "system" | "fund" | …
├─ bizNo 사업자등록번호 (D-bp-entity-5, hive mirror)
├─ fiscalYearStartMonth 1-12 (KR default 1)
├─ countryCode ISO 3166-1 alpha-2 (KR default)
└─ workspaces[] 1:N
Member (인간/에이전트 통합)
├─ User (email, aadObjectId, role: admin|member,
│ entityScopes: JSON ["axev","axec"] — D-bp-entity-8 raw 순서,
│ defaultEntity: 사용자별 명시적 default — D-bp-entity-7,
│ ownedBy: 본인 owner FK (AI agent ↔ human, D-bp-entity-9))
│ └─ EntityRole[] per-(user, entity) 권한 grant (D-bp-entity-17,
│ role: admin|owner|member, grantedById audit)
└─ Agent (agentId by SDK, memberId)
Workspace (PARA row — Project/Area/Resource/Archive)
├─ entityId → Entity (NOT NULL — D-bp-entity-3, General bucket 폐기)
├─ paraLayer ParaLayer enum {PROJECT, AREA, RESOURCE, ARCHIVE}
├─ sourceWorkspaceId / sourceArtifactPath / copiedAt (D-bp-entity-2 fork provenance)
└─ Session (mode: chat|terminal|hybrid|agent)
├─ Messages
└─ TerminalSessions
Issue
├─ assignee:Member
└─ reporter:Member
AppSetting (key-value, frame_mcp_token 회전 저장)
OperatorAlert (D-bp-alert-1, L2 silent-drop 영구 기록)
├─ source 처리 진입점 (e.g. processChatBundle)
├─ chatId / messageId
├─ primaryError / ackError
├─ createdAt
└─ acknowledgedAt / acknowledgedBy (NULL = unresolved)
MailSendLog (D-bp-mcp-mail-1, 2026-05-29 — send_mail append-only audit; migration 20260529120000_add_mail_send_log)
├─ callerEmail 인증된 caller (MCP token email claim)
├─ sentAs 실제 발신 mailbox (caller, 또는 send-as target)
├─ subject
├─ toCount / ccCount / bccCount / attachmentCount (주소 자체는 미저장)
├─ status "sent" | "dry_run" | "failed"
├─ messageId Graph sendMail 202 no-body → null
├─ httpStatus / error
└─ createdAt index: createdAt / (callerEmail, createdAt)
McpSchema (D-bp-artifact-1, 2026-05-23 — schema discovery cache)
├─ service ← 'frame' | 'hive' | 'magnet' | 'stream' | …
├─ schemaId ← '[email protected]' (@version suffix 포함)
├─ envelopeVersion ← MCP /schemas envelope.version
├─ body ← JSONB { description, produced_by, key_fields, … }
├─ fetchedAt ← @updatedAt — 매 fetch 마다 갱신
├─ firstSeenAt ← 첫 발견 시점
└─ @@unique([service, schemaId]) 새 version = 새 row, body 변경 = drift warn
OAuth* (D-axe-idp-1, 2026-06-03 — Blueprint = 플랫폼 OIDC Provider; src/lib/platform-oidc.ts)
├─ OAuthAuthCode 단발 authorization code (PKCE S256, 10분 TTL, atomic 소비)
├─ OAuthRefreshToken 회전 refresh token (sha256 hash 만 저장, revokedAt/rotatedTo)
└─ OAuthClient RFC 7591 DCR 동적 클라이언트 (loopback/downstream) — axe-cli 는 정적
Artifact (D-bp-artifact-1, 2026-05-23 — typed fact unit)
├─ id ← cuid PK
├─ schemaId ← '[email protected]' or 'freeform.llm@auto' — McpSchema lookup
├─ workspaceId ← Workspace FK (CASCADE delete)
├─ entityId ← cached from Workspace.entityId
├─ paraLayer ← cached from Workspace.paraLayer
├─ content ← JSONB — typed payload per schemaId
├─ citations ← JSONB array — 6-kind discriminated union
│ (onedrive / frame.* / hive.* / teams.message /
│ mail.thread / external.web)
├─ confidence ← Decimal(3,2) 0.00 ~ 1.00 (default 1.0)
├─ auditTrail ← JSONB array — append-only event log
│ (propose / confirm / reject / edit / dispatch /
│ archive / restore)
├─ parentArtifactId ← self-FK — sub-Project chain + dispatch fork
├─ createdAt / updatedAt / archivedAt
└─ index: workspaceId / (entityId, paraLayer) / schemaId /
parentArtifactId / archivedAt
ArtifactLink (D-bp-artifact-6, 2026-05-23 — multi-parent reference)
├─ artifactId ← Artifact FK (CASCADE delete)
├─ workspaceId ← Workspace FK (CASCADE delete)
├─ linkType ← 'reference' | 'sub_project' | 'cross_link'
├─ linkedAt ← @default(now())
├─ linkedById ← User FK (SetNull on user delete — preserve audit)
└─ PK (artifactId, workspaceId) 같은 artifact 동일 workspace 중복 link 차단entity scope flow
User.entityScopes 는 sign-in 콜백 (auth.ts → hydrateEntityScopesForUser) 가 customers.yaml user_entity_map + default_entities_by_domain 에서 hydrate. admin 이 customers.yaml 미명시 시 DB 의 모든 Entity slug fallback (lock-out 방지). 각 /api/workspaces/* 라우트가 requireWorkspaceEntityAccess 로 entity gate 검증 (D-bp-entity-3: nullable early-out 제거, 모든 workspace 명시적 entity).
User.defaultEntity (D-bp-entity-7): hydration 시 null + non-empty scopes → scopes[0] 자동 set. scope 외 default → scopes[0] fallback. TopNav 의 fallback chain (D-bp-entity-8) = URL ?entity= → localStorage axe:lastEntity → defaultEntity → entityScopes[0] → “all”. createWorkspace fallback = opts.entitySlug → drivePath segment → defaultEntity → entityScopes[0] → throw. 사용자 변경 endpoint = PATCH /api/user/default-entity { slug } (server-side entityScopes.includes(slug) 검증). D-bp-entity-8 정정: hydrate 가 raw customers.yaml 순서 보존 (이전 alphabetic sort 폐기) — yaml list 순서가 진정한 default SOT.
EntityRole (D-bp-entity-17, 2026-05-22)
entityScopes 가 entity 가시성만 표현하는 한계 해결. EntityRole { userId, entitySlug, role: admin|owner|member } 가 entity 별 3-tier 권한 SSOT. hierarchy admin > owner > member 를 src/lib/entity-roles.ts 의 hasEntityRole(userId, slug, min) / requireEntityRole(...) 로 평가. tenant-wide User.role='admin' (Soohun/ai@) 은 모든 entity 에 implicit admin (row 없어도 bypass — admin fallback 정신 계승).
Hydration 정책: signIn 콜백이 hydrateEntityRolesForUser 호출 — entityScopes 에 새로 들어온 slug 마다 member 행 자동 생성, scope 에서 제거된 slug 의 행은 revoke. owner/admin overlay 는 오직 /axe/admin/users 에서 tenant admin 이 grant (PATCH /api/users/[id]/entity-roles { entitySlug, role: admin|owner|member|none }).
MCP scope 매핑 (내부 API GET /api/internal/entity-roles?email=..., HMAC bearer BLUEPRINT_INTERNAL_API_KEY): admin/owner → hive [read,write,approve,admin] · frame [read,write,close,admin]; member → admin 제외 동일 set; row 없음 → 빈 set (entity 비노출). hive/frame OIDC RP 측 연동은 PR 2/3 에서 별도 결정.
owned account switching (D-bp-entity-9, 2026-05-22)
User.ownedBy — owner (human) → owned (AI agent identity, e.g. ai@/cfo@) 관계. 운영자 1 인이 본인 + agent 여러 identity 운영하는 패턴. switch flow:
- owner 가 TopNav 의 사용자 이름 (button) 을 클릭 → popover 메뉴 펼침 (D-bp-entity-12)
- owned account 선택 시
POST /api/auth/switch-account { targetUserId }→ server-sidetarget.ownedBy === caller.userId검증 - httpOnly cookie
axe:active-user-idset + reload - session callback (
src/lib/auth.ts): 매 read 마다 cookie 발견 시 ownership 재검증 →session.user.*(id/email/name/role/entityScopes/defaultEntity) 모두 target 로 override. JWT 의 authenticated identity 는 untouched (audit trail 보존) - revert = 메뉴 에서 self 선택 → 동일 endpoint (no-op, dropdown 즉시 close)
/api/auth/owned-accounts GET = dropdown options 채움 (caller + ownedUsers). isImpersonating=true 시 trigger button lime border + label color + tooltip. mid-session ownership revoke 시 자동 revert (매 session read 재검증). admin role 도 bypass 불가 — owned-only.
D-bp-entity-12 (TopNav UX): 사용자 이름 자체가 dropdown trigger (M365/Gmail 패턴). 별도 <select> 폐기. 메뉴 안에 + 다른 계정으로 로그인 옵션 → signIn("azure-ad", ..., { prompt: "select_account" }) 으로 Microsoft 계정 picker 호출. click-outside / Escape close + chevron rotate 120ms. data-testid nav-user-name 유지 (E2E backward compat).
Artifact + PARA 지식 레이어 (📐 설계, M6)
Blueprint 가 PARA OS 로 동작하기 위한 typed fact layer. ctx skill 의 markdown PKM 진화형. 상세 = 아키텍처 페이지.
요지:
- Artifact = per-field typed fact (Postgres + JSONB).
schema_id/content/citations/confidence/paraLayer/entityId/audit_trail/parent_artifact_id - Citation = OneDrive
driveItemId+version/ frame query (entity+account+period+queried_at) / hiveemployee_id/ TeamschatId+msgId/ mailthreadId+msgId/ externalURL+fetched_at. data 중복 0 - PARA dispatch = Project artifact 종료 시 Area/Resource/Archive 로 field-level routing. M3 dispatch (workspace-level) 의 진화
- ctx 진화 = curation interface (markdown 은 보조). 흐름: agent propose → ctx review → confirmed fact + audit
근거: D-bp-artifact-1~5. 마일스톤 M6.
Schema discovery — frame/hive /schemas mirror (2026-05-23, B-bp-artifact-schema-discovery)
M6 의 첫 implementation slice. Blueprint 가 각 MCP 서비스의 GET /{service}/schemas envelope 을 fetch + cache + version. SoT 는 MCP 측 (src/{frame,hive}/mcp/schemas.py); Blueprint 는 discovery / mirror 만.
- Prisma 모델:
McpSchema { service, schemaId, envelopeVersion, body, fetchedAt, firstSeenAt }.@@unique([service, schemaId]). body 변경 시fetchedAt갱신 + drift warning. 신규 schema_id (예:[email protected]) = 새 row - 모듈:
src/lib/artifact/schemas/(services / fetcher / registry / types). MCP_SERVICES 에 frame + hive 등재 (확장 시 entry 추가만) - Admin API:
GET /api/admin/mcp-schemas— 전체 list + count_by_service + registered_servicesPOST /api/admin/mcp-schemas/refresh— fetch trigger (per_serviceadded/changed/unchanged/total반환)
- CLI:
npx tsx scripts/fetch-mcp-schemas.ts [--service frame](docker exec / launchd cron 모두 사용 가능) - Token: 기존
frame_mcp_tokenAppSetting 재사용. hive 는hive_mcp_tokenAppSetting 또는HIVE_MCP_TOKENenv (현재 미배치, fetcher 가 auth_failed 로 cleanly skip) - 에러 모델: status ∈
ok/auth_failed/network_error/parse_error— drift 진단 surface 깨끗
다음 step: artifact 본체 Prisma migration (B-bp-artifact-prisma) + citation resolver (B-bp-artifact-citation-resolver).
hive ↔ Blueprint entity universe SOT
| Source | role |
|---|---|
~/.axe/customers.yaml entities | slug list (이 instance 에 활성화된 entity) |
hive.shared.entity | legal_name / biz_no / fiscal_year_start_month / country_code SOT (hive register-entity CLI) |
Blueprint Entity (이 페이지의 표) | hive metadata mirror + kind 분류. seed-entities.ts 가 매 boot upsert. sys 는 Blueprint 전용 (hive 미보유 — ad-hoc Teams 1:1 host) |
/api/entities GET = 클라이언트 entity 카탈로그 (entityScopes 권한 검증, TopNav fetch). hive 새 entity 등록 → Blueprint seed-entities.ts 갱신 → 매 boot upsert → 다음 page load 시 클라이언트 자동 인식 (재배포 0).
Blueprint MCP 서버 (D-bp-mcp-1, 2026-05-21)
Blueprint 자신의 read-only MCP 서버. Claude 웹 / 데스크탑 / Code 가 Custom Connector 로 등록 가능.
| 항목 | 값 |
|---|---|
| URL | https://axe.axelabs.ai/blueprint/mcp |
| Client ID | 482598f7-540c-462c-9dfd-b957651eb804 |
| Application ID URI | https://axe.axelabs.ai/blueprint/mcp |
| Scope | https://axe.axelabs.ai/blueprint/mcp/mcp.access |
| Tenant | 122fb574-7efa-476a-95b6-bee81bce2cce (axellc.com) |
| Stack | Python 3.12 + FastMCP + Starlette + uvicorn (frame/hive 패턴 미러) |
22 tools (13 read + 7 calendar + 1 mail write + 1 teams write)
| Tool | 분류 | 설명 |
|---|---|---|
whoami | read | 호출 사용자 (Caller) 정보 |
list_workspaces | read | 본인 멤버십 워크스페이스 |
get_workspace | read | 워크스페이스 상세 |
list_sessions | read | 본인 세션 목록 |
list_issues | read | 이슈 목록 (필터 가능) |
get_issue | read | 이슈 상세 |
list_agents | read | 등록된 에이전트 |
list_skills | read | 등록된 스킬 |
search_people | read | 멤버 검색 (이름/이메일) |
get_teams_message | read | Teams teams.microsoft.com/l/message/... deep link → 본문·발신자·타임스탬프 + 첨부·인용·인라인이미지 ID (D-bp-mcp-teams-1, D-bp-mcp-teams-2) |
get_teams_hosted_content | read | Teams 인라인 이미지 (hostedContents) → MCP image content block (Claude vision 직접 활용) (D-bp-mcp-teams-2) |
get_teams_attachment_file | read | Teams reference 첨부 (OneDrive/SharePoint 공유 파일) contentUrl → text 또는 base64. 50 MB 기본 / 100 MB hard cap (D-bp-mcp-teams-3) |
send_teams_message | write | Teams 채팅에 AXE 봇([email protected]) 신원으로 발송. chat_id/text/content_type/prefix/dry_run. admin 전용 (봇 신원 발신 = broadcast-class). 봇이 멤버인 chat 만 (Graph invariant). 발신 시 humanizer + audit footer 자동. 경로 = /api/teams/post (CRON_SECRET) → sendChatReply (D-bp-mcp-teams-send-1) |
list_teams_chats | read | 최근 Teams chat 목록 (TeamsInboundMessage 로그) — send_teams_message 용 chat_id 탐색. query(preview·sender ILIKE)/limit. chat당 최근 sender·preview·receivedAt. admin 전용 (D-bp-mcp-teams-send-1) |
create_event | write | 호출자 본인 M365 캘린더에 이벤트 생성. subject/start/end/location/body/recurrence/attendees (외부 도메인 OK) (D-bp-mcp-calendar-1) |
update_event | write | 기존 이벤트 부분 수정 (eventId + 변경 필드) |
delete_event | write | 이벤트 삭제 + attendees 에게 cancellation 자동 발송 |
get_event | read | 단일 이벤트 fetch by id |
list_events | read | 시간 윈도우 내 이벤트 목록 (UTC 정렬) |
add_attendees | write | 기존 attendees 보존 + 신규 추가 (case-insensitive dedup), Graph 가 초대 자동 발송 |
find_free_time | read | 다수 attendee 의 free/busy view (Graph /me/calendar/getSchedule) |
send_mail | write | caller 본인 mailbox 에서 임의 수신자(외부 도메인 OK)로 이메일 발송. to/subject/body(html|text)/cc/bcc/attachments(base64)/save_to_sent_items/dry_run + admin as_user_email send-as (D-bp-mcp-mail-1) |
Write tools (calendar) — 기본 동작: caller 본인 M365 캘린더에 적용.
getClientForUser(callerUserId)로 caller 의 delegated token (Calendars.ReadWrite) 사용. 외부 도메인 attendee (gmail/naver/personal) = Graph 가 SMTP 초대 메일 처리, 별도 provider 통합 불필요.Admin send-as (D-bp-mcp-calendar-2):
create_event/update_event/delete_event/get_event/list_events/add_attendees6개 tool 에as_user_emailoptional 파라미터. caller 의 Blueprintrole === "admin"인 경우만 작동 — target user 의 캘린더에 write, event organizer = target user 로 표시. 구현 = app-only Graph token (getAppOnlyClient()via client credentials) +/users/{targetUserId}/eventsendpoint. Azure App Application permissionCalendars.ReadWrite+ tenant admin consent 필요. 비-admin caller 가as_user_email사용 시 403send_as_forbidden.사용자 첫 호출 전 1회 re-consent 필요:
Calendars.ReadWritescope 가 2026-05-26 추가됨. 기존 사용자가 cached token 으로 calendar tool 호출 시 graph_token_unavailable / 403 응답 → Blueprint web 에 sign out + sign in 으로 incremental consent. admin 이 tenant-wide consent 시 skip.Read-only Blueprint domain tools (whoami, list_, get_, search_people) 은 D-bp-mcp-1 의 “MCP 는 조회 surface, Blueprint web UI 가 issue/session 작성 SoT” 원칙 유지. Calendar 는 별개 도메인 (Microsoft Graph) 이므로 write 채택 — D-bp-mcp-1 의 SoT 와 무관.
Mail (
send_mail, D-bp-mcp-mail-1): 기본 동작 = caller 본인 mailbox 발송 (getClientForUserdelegated token;Mail.Senddelegated 는 이미graph.tsSCOPES 에 존재 → calendar 와 달리 re-consent 불필요). GraphPOST /me/sendMail, body ={ message: { subject, body, toRecipients, ccRecipients?, bccRecipients?, attachments? }, saveToSentItems }. 외부 도메인 수신자 = Graph 가 SMTP 처리. Admin send-as:as_user_email지정 시 caller 의 Blueprintrole === "admin"인 경우만 작동 — app-only token (getAppOnlyClient()) +/users/{targetUpn}/sendMail, From = target user. Azure App Application permissionMail.Send+ tenant admin consent 필요 (Blueprint Next.js app2b222356-..., MCP connector app482598f7-...아님). 비-admin caller 가as_user_email사용 시 403send_as_forbidden.dry_run=true= 메시지 조립·검증·preview 만 반환 (미발송). GraphsendMail은 202 no-body →message_idnull (Sent 폴더 사본이 durable record). 모든 발송 (sent/dry_run/failed) = append-onlyMailSendLog1 row (caller · sentAs · to/cc/bcc count · subject · status · httpStatus) — injection-safety 정책 audit. 에러 코드:send_as_forbidden/recipient_invalid/mail_send_not_consented(Azure 권한 미동의 명시) /graph_error{status, detail}.Teams tools:
get_teams_message응답 schema:{ id, chatId, sender, senderId, timestamp, lastEditedDateTime, messageType, bodyText, bodyHtml, bodyContentType, mentions[], attachments[], inlineImageIds[], quotes[] }.attachments= OneDrive 파일 reference (contentUrl) / Adaptive Card 등.quotes=messageReference자동 resolve (1-level only — quote-of-quote opaque).inlineImageIds= body HTML 에서 추출한 hostedContent ID 배열 — 각각get_teams_hosted_content(chatId, messageId, hostedContentId)로 fetch.attachments[i].contentUrl(contentType=reference) 은get_teams_attachment_file(contentUrl)로 본문까지 fetch.
Teams write (
send_teams_message·list_teams_chats, D-bp-mcp-teams-send-1, 2026-06-04): Teams 발신을 1급 MCP 도구로 승격 (이전엔 운영자가 내부 API 역설계 + 수작업 DB 조회로만 가능).send_teams_message는 봇(ai@) 신원으로 발송하므로 calendar/mail send-as 와 동일하게 admin-gated (is_admin(callerUserId)DB 재확인). 봇은 자기가 멤버인 chat 에만 post 가능 (Graph invariant).list_teams_chats는TeamsInboundMessage로그에서 chat_id 를 찾아준다. 구현 = MCP-only — 새 app route 대신 기존/api/teams/post(CRON_SECRET,env_file ../.env로 MCP 컨테이너에 이미 주입됨) 재사용 → blueprint app 무배포 (P3005 마이그레이션 drift + main WIP 회피). 후속: 일관성 위해/api/internal/teams/send(BLUEPRINT_INTERNAL_API_KEY) +TeamsSendLogaudit 로 이전 (app 청정 배포 가능해질 때 — backlogB-bp-mcp-teams-send-internal-route).세 tool 모두 Blueprint Next.js 의
POST /api/internal/teams/{fetch-message,fetch-hosted-content,fetch-attachment-file}를 docker 네트워크로 호출 (BearerBLUEPRINT_INTERNAL_API_KEY공유 비밀). Graph 호출은 bot identity ([email protected], app permChatMessage.Read.All+ Sites/Files Read). bot 이 해당 chat 멤버여야 200. 미멤버 chat / share = 403. Channel 컨텍스트 URL 은 501 (follow-up). hostedContent 10 MB 초과 시 413. attachment file 50 MB 기본 (호출 시maxBytesoverride 가능, 100 MB hard cap).
데이터 흐름
cloudflared (axe.axelabs.ai/blueprint/mcp)
→ axe-blueprint-mcp-proxy (Caddy, host:3151)
→ blueprint-mcp:3000 (docker network alias)
→ blueprint-mcp-blue:3000 (active) OR blueprint-mcp-green:3000 (passive)
= Python+FastMCP+Starlette
→ JWTAuthMiddleware
- Authorization: Bearer <token>
- iss-dispatch (unverified peek → full RS256 verify):
· iss == BLUEPRINT_ISSUER → verify_blueprint_token (auth_blueprint.py)
플랫폼 토큰 (D-axe-idp-1, `axe login`). Blueprint JWKS,
aud=https://axe.axelabs.ai. ← 자기 OP 의 resource server
· else → verify_microsoft_access_token (auth_oidc.py)
Microsoft Entra access_token (claude.ai Custom Connector).
JWKS RS256 + multi-aud (client_id GUID OR app_id_uri)
- email claim → "User" 테이블 lookup (Prisma schema raw SQL) — 양 경로 공통
→ Caller ContextVar 주입
→ FastMCP streamable_http_app → tool handler운영
- 컨테이너:
blueprint-mcp-blue(host port 3152, compose servicemcp-blue, aliasblueprint-mcpactive) +blueprint-mcp-green(host port 3153, compose servicemcp-green, passive). frameframe-mcp-blue/green패턴 1:1 미러. - Proxy:
axe-blueprint-mcp-proxy(host port 3151) → docker aliasblueprint-mcp→ active color. - DB 직결:
blueprint-postgres의User/Workspace/Session/Issue/Agent/Skill테이블 read-only - 헬스체크:
curl http://127.0.0.1:3152/health(blue) ·curl http://127.0.0.1:3153/health(green) ·curl http://127.0.0.1:3151/health(Caddy → 현재 active) - Swap (zero-downtime):
docker network disconnect blueprint_default blueprint-mcp-blue && docker network connect --alias blueprint-mcp blueprint_default blueprint-mcp-green && docker exec axe-blueprint-mcp-proxy caddy reload --config /etc/caddy/Caddyfile. cloudflared 비건드림.
Custom Connector 등록 절차
운영자: axe secret send AZURE_BLUEPRINT_MCP_CLIENT_SECRET --service blueprint --to <local-part> → URL 발급 → /api/admin/broadcast-dm 으로 수신자 Teams DM 자동 발사. 전체 흐름은 /architecture/secrets § 사람에게 전달, 수신자 화면 절차 + 함정은 /architecture/mcp-server-checklist § 7 + /onboard/claude-frame-setup (동일 절차, MCP URL 만 다름).
개발 history
frame/hive 와 패턴 동일하지만 byte-by-byte 미러 실패로 5층 양파껍질 사고 (PR #331 ~ #335 누적). 회고는 /architecture/mcp-server-checklist § 10.
Teams bot integration
Microsoft Teams 1:1 / group chat 에서 @AXE AI (또는 @CFO) 멘션 시 봇이 Claude Code 세션을 띄워 응답하는 surface. Web UI 의 chat panel 은 2026-04-25 폐기됨 (architecture-chat-panel-removed) — Teams 가 사용자-facing primary, Web UI 는 admin/observe 전용.
Agent identity
| Agent | UPN | 용도 |
|---|---|---|
| ai | [email protected] | 일반 워크스페이스 dispatcher. 모든 chat 에 기본 멤버. getBotGraphClient() 가 가리키는 레거시 single-bot identity. |
| cfo | [email protected] | 회계 도메인 (frame MCP 보유). axe CFO 1:1 chat 의 전용 멤버. |
각 agent 는 Microsoft Graph user 계정 1 개 + Blueprint Agent row 1 개. getClientForUser(agentUserId) 가 agent 의 delegated token 으로 Graph 인증 — agent 가 chat 의 member 일 때만 인증 성공 (multi-bot 1:1 chat 의 자연 invariant). getBotGraphClient() 는 ai@ 의 shared client 로 레거시 경로 (ai@ 가 chat 멤버일 때만 작동).
Inbound 처리 흐름
- Microsoft Graph change-notification →
/api/teams/webhook→TeamsInboundMessage인큐 - processor:
messageType === "message"filter (systemEvent / typing 제외) → 봇 mention 검증 → trigger agent 해결 (mentioned identity) - agent ↔ chat 매핑 검증 (
resolveChatMemberAgentUserId) → workspace ACL → 첨부 처리 → claude-invoker - reply:
sendChatReply또는 정정 시editChatMessage(PATCH in-place edit) - resolve:
TeamsInboundMessage.resolvedAt+resolution마킹
첨부 처리 (src/lib/teams/attachments.ts)
processTriggerAttachments 가 trigger message 의 attachments[] 를 contentType 별 분기:
contentType | 처리 | 결과 위치 |
|---|---|---|
reference (OneDrive/SharePoint 파일) | downloadReferenceFile agent token 으로 download. sha256 dedup → 워크스페이스 기존 파일 일치 시 inbox 복사본 제거 후 기존 path 가리킴 | _teams_inbox/<YYYY-MM-DD>/<filename> |
inline image (<img src="hostedContents/.../$value">) | downloadHostedContent + content-type sniff → 확장자 추론 | _teams_inbox/<date>/image_<msgIdShort>_<idx>.<ext> |
messageReference (Teams quote/reply) | getChatMessage(chatId, refMsgId, agentId) 로 quoted body fetch → HTML → plain text → placeholder 상단에 [인용 메시지] block 으로 inline | LLM prompt only (disk 저장 X). fetch 실패 시 attachment.content.messagePreview 200자 fallback |
| 기타 (card / file 등) | [unsupported contentType: X] placeholder (LLM 이 “못 봤다” 답신 위험 — 향후 audit 대상, known-gaps) | — |
multi-bot chat 의 SharePoint URL 은 chat 참여자에게만 read 권한이 grant 되므로 — agent token propagation 이 필수. 레거시 getBotGraphClient 만 쓰면 비참여 chat 에서 403/404 발생 (feedback_multi_bot_attachment_download_gap 2026-05-13 fix).
Reply 안전 정책
sanitizeBotReply: markdown + HTML defense-in-depth. system-reminder ban, table → 바이너리 only, dangling cross-reference strip 등 4 종 룰 (architecture_bot_reply_pipeline PR #157 + PR #165).- silent-drop defense-in-depth: 사용자 @mention 에 무응답 금지 — 5 종 silent-drop class 별 layer 차단 (feedback_silent_drop_defense_in_depth PR #261/#263/#266).
- In-place edit: 잘못된 reply 정정 시 softDelete + repost X →
editChatMessagePATCH. 수정됨 표시 1개만 (feedback_in_place_edit_over_repost). - System prompt vs sanitizer: 시스템 프롬프트 텍스트 룰이 반복 위반될 경우 sanitizer 에 deterministic regex enforcer 추가 (layer 2 fail-safe).
알려진 제약 / 후속
- Context window messageReference: trigger message 만 quote 처리.
TeamsContextMessage(rolling context) 의 quote 는 미해석 — B-bp-teams-context-msgref 후속. - Unhandled contentType audit: card / file / 기타 — known-gaps.
get_session+list_messagesMCP tool 부재: agent transcript 외부 비공개 — Stage 1 전 추가 예정.
Admin Teams API — /api/admin/broadcast-dm
운영자 broadcast / 직원 onboarding 흐름에서 bot identity ([email protected]) 가 1:1 Teams DM 직접 발사. 봇이 @mention 받지 않는 proactive outreach 경로.
| 필드 | 값 |
|---|---|
| POST | /api/admin/broadcast-dm (:3100 내부, blueprint.axellc.com 외부) |
| Auth | Authorization: Bearer $CRON_SECRET (NextAuth 세션 불필요 — admin-only) |
| Body | \{ emails: string[], text: string, contentType?: "text" | "html" \} |
| 흐름 | each email → User.aadObjectId lookup → POST /chats (oneOnOne, Graph idempotent — 같은 pair 면 같은 chat id) → POST /chats/\{id\}/messages |
| 응답 | \{ summary: \{total, sent, skipped, error\}, results: [\{email, status, chatId?, messageId?, reason?\}] \} |
소스: src/app/api/admin/broadcast-dm/route.ts. 패턴 derivation: src/lib/agents/cron-failure-alert.ts:postAlertToAdmin. 운영자 onboarding 패턴은 /architecture/secrets § 자동 발사 참조.
Agent context reply (사용자 @mention 후 봇 응답) 은 본 엔드포인트 X — mcp__blueprint-graph__graph_chat_message_post MCP tool 사용.
MCP 클라이언트 (frame backend 호출용)
src/lib/mcp/frame.ts:
const frameServerUrl = process.env.FRAME_MCP_URL || 'http://frame-mcp:3710/mcp'
const token = await getAppSetting('frame_mcp_token') ?? process.env.FRAME_MCP_TOKEN
// HTTP MCP Streamable transport with Bearerframe-mcp 는 docker network artemis_default 의 alias (frame-mcp-blue 또는 green 으로 swap).
토큰 해결 순서:
- AppSetting
frame_mcp_token(DB 저장, cron job 으로 회전) - env
FRAME_MCP_TOKEN(백업)
customers.yaml 사용
src/lib/customers.ts:
export function getCustomerByPublicDomain(host: string): Customer | undefined {
// host: "axe.axelabs.ai" → axe customer
}
export function userBelongsToCustomer(email: string, customerId: string): boolean {
// email "[email protected]" → axellc.com → axe
}마운트: 호스트 /Users/axe/.axe/customers.yaml → 컨테이너 /etc/axe/customers.yaml:ro. mtime 변경 시 자동 재로드.
docker-compose
services:
app:
build: . (Dockerfile)
container_name: blueprint-app
ports:
- "3100:3000" # HTTP
- "3101:3001" # WebSocket
- "3102:7681" # ttyd (선택)
volumes:
- app-data:/app/data
- claude-data:/home/nextjs/.claude # skill sync
- /Users/axe/.axe/customers.yaml:/etc/axe/customers.yaml:ro
env_file: ../.env
networks: [artemis_default]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]환경 변수 (핵심)
# Auth
NEXTAUTH_URL=https://axe.axelabs.ai
NEXTAUTH_SECRET= # ≥32자
AZURE_AD_CLIENT_ID= # Microsoft Entra 'Blueprint' app
AZURE_AD_CLIENT_SECRET=
AZURE_AD_TENANT_ID= # customers.yaml 의 axe.sso.tenant_id
# Blueprint MCP — 플랫폼 토큰 (D-axe-idp-1) resource-server 신뢰.
# Blueprint MCP 가 자기 OP 가 발행한 `axe login` 토큰을 받게 한다 (미설정 시
# Microsoft Entra 경로만 → 자기 플랫폼 토큰을 unknown_kid 로 401, 2026-06-04 fix).
# JWKS 는 issuer 에서 파생({issuer}/.well-known/jwks.json). 주권 테넌트는 자기 URL 로.
BLUEPRINT_ISSUER=https://blueprint.axellc.com # = OP issuer (NEXTAUTH_URL)
BLUEPRINT_AUDIENCE=https://axe.axelabs.ai # 플랫폼 단일 audience
# Database — D-config-17 cutover 후 (2026-05-15~) Postgres.
# ⚠️ D-bp-mcp-3 (2026-05-22): `.env` 에 DATABASE_URL 명시 X 권고 — compose 의
# `${DATABASE_URL:-postgresql://...}` default 가 모든 service (app + mcp blue/green)
# 에 일관 적용. .env 에 SQLite legacy `file:./...` 잔존 시 blueprint-mcp 가
# SQLAlchemy parse fail 로 모든 OAuth 요청 silent 500 (8h 종일 broken incident).
# mcp/config.py 가 postgresql:// → postgresql+asyncpg:// 자동 변환.
DATABASE_URL=postgresql://blueprint:${BLUEPRINT_DB_PASSWORD}@postgres:5432/blueprint
BLUEPRINT_DB_USER=blueprint # docker-compose default
BLUEPRINT_DB_PASSWORD= # vault-pulled (D-ops-17)
BLUEPRINT_DB_NAME=blueprint
# Multi-tenant
CUSTOMERS_YAML_PATH=/etc/axe/customers.yaml
# MCP
FRAME_MCP_URL=http://frame-mcp:3710/mcp
FRAME_MCP_TOKEN= # AppSetting fallback
# Claude
CLAUDE_CODE_OAUTH_TOKEN= # 또는 ANTHROPIC_API_KEYAdmin vs Settings 경계
/axe/admin/* ← 조직 수준 (멤버 관리, 권한 부여, 감사 로그)
/axe/settings ← 개인 수준 (개인 토큰, 알림 환경설정)Admin 라우트는 User.role = 'admin' 만 접근.
디자인 시스템 (@axe/ui, D-bp-ui-1, 2026-05-22)
/Users/axe/axelabs/src/lib/{tokens,styles}/ SSOT 의 디자인 토큰 + 컴포넌트 스타일을 Blueprint 의 src/app/_axe-ui/ 로 sync. axelabs 메인 앱 · docs.axelabs.ai 와 동일한 토큰 (claret/cream/neon/noir 12-step) 사용.
| 동작 | 명령 |
|---|---|
| SSOT 가져오기 | npm run sync-axe-ui (axelabs HEAD + src/lib/ 마지막 변경 SHA 를 VERSION 에 기록) |
| 변경 감지 (dry-run) | npm run check-axe-ui — sync 후 stat 보여주고 revert |
| 자동 sync | predev / prebuild hook 이 매 npm run dev/build 마다 실행. Docker 빌드 안에서는 axelabs 경로 부재 → graceful skip (committed 산출물 사용). |
Blueprint 전용 패치 2종 (scripts/sync-axe-ui.mjs)
- 목적지 경로 — canonical (axelabs-docs) 는
app/_axe-ui/. Blueprint 는src/source root 라src/app/_axe-ui/. fonts.css의@import url(...)strip — Tailwind 4 + Turbopack 이 토큰 파일들을 globals.css 로 inline 한 뒤 fonts.css 의@import url(pretendard/d2coding)이 다른 rule 뒤로 밀려 CSS spec 위반으로 브라우저가 silent drop. sync 단계에서 strip → Pretendard/D2Coding 은layout.tsx의<link rel="stylesheet">로 로드 (SSOT 가 Clash Display 에 대해 이미 인정한 회피 패턴의 확장).
Legacy var alias bridge (src/app/globals.css)
35+ tsx 가 var(--navy) · var(--white) · var(--gray-ax) · var(--dark-border) 등 기존 토큰을 직접 사용. 비파괴 채택을 위해 globals.css 에서 alias:
--navy: var(--bg-base); /* light: #f5f1e8 / dark: #1a0610 */
--white: var(--text-primary); /* light: #1f1a13 / dark: #f2e8ee */
--gray: var(--text-muted);
--dark-border: var(--border-subtle);
/* ...등 */data-theme="dark" 토글 시 기존 tsx 무수정으로 자동 전환. 향후 별도 PR 로 inline ref 를 새 토큰명 (var(--bg-base) 등) 으로 점진 마이그레이션.
테마 토글
<html lang="en" data-theme="dark"> 기본. 라이트 모드 전환은 document.documentElement.setAttribute("data-theme", "light") — 토글 UI 는 별도 후속. SSOT colors.css 가 [data-theme="…"] + .light/.dark 양쪽 지원 (next-themes 호환 — [architecture/decisions D-axe-ui-1](/ops/decisions) 참조).
외부 의존성
| 시스템 | 통합 지점 |
|---|---|
| Microsoft Entra ID | NextAuth provider + Graph API |
| Microsoft Graph | Mail, Chat, Files, Teams 권한 |
| frame MCP | HTTP MCP (회계 도구) |
| Claude API | Anthropic SDK (에이전트 실행) |
| Cloudflare Tunnel | external 노출 |
| jsDelivr CDN | Pretendard + D2Coding 폰트 (CSP style-src + font-src 허용) |
| Fontshare CDN | Clash Display 폰트 (디스플레이 헤딩 옵션) |
운영 노트
- 세션 maxAge: 365일 (NextAuth)
- Cookie:
__Secure-next-auth.session-token(prod) - Skill sync:
claude-data:/home/nextjs/.claude볼륨, git-based SOT - PTY: max buffer 2000 lines per session
- CORS: WebSocket origin allowlist (
src/server/websocket.tsL180-183)
관련 문서
- Frame service — 회계 MCP backend
- Microsoft Entra ID app registration
- /Users/axe/blueprint/docs/project-blueprint.md — 마스터 문서 (47KB)