# TimelyGPT MCP Hub — Full Context for AI Agents > 이 파일은 AI 코딩 에이전트(Cursor/Claude Code/Cody/Codex 등)가 TimelyGPT MCP Hub를 다른 레포에 이식할 때 필요한 모든 컨텍스트를 담은 단일 문서입니다. llms.txt가 네비, 이 파일이 encyclopedia입니다. 작성: 2026-05-15, 백상현 (마드라스체크). ## 0. AI 에이전트를 위한 메타 지시 당신은 이 레포(`source`)에서 타임리 측 레포(`target`)로 코드를 옮기는 작업을 합니다. 다음 원칙을 지키세요: 1. **백엔드는 통째 이식**: `src/` 전체를 target의 적절한 위치에 복사. NestJS가 아니면 `@Injectable()` / `@McpTool` 데코레이터만 어댑터로 교체. 2. **프론트는 부분 이식**: `apps/web/components/inputs/*` 와 `apps/web/components/errors/*` 만 가져가고, 디자인 토큰(Tailwind 클래스)을 target 디자인 시스템으로 search-replace. 페이지(`apps/web/app/*`)는 보고 다시 짜기. 3. **0-dep pure TS 모듈은 무조건 복붙**: `src/common/i18n/`, `src/common/input-hints/`, `src/common/errors.ts`는 NestJS 의존이 0이라 즉시 복사 가능. 4. **데이터 모델은 그대로**: Prisma schema의 9개 테이블은 변경 X. `migrate deploy`만 하면 됨. 5. **MCP 도구 25개는 한 단위**: atomic 19개 + composite 6개. 각 도구는 폴더 단위로 독립 — 필요한 것만 골라 옮겨도 동작. 6. **변경 라인은 모두 사용자 요청에 직결**: 인접 코드/주석/포매팅 "개선" 금지 (Karpathy 가이드라인). 7. **불확실하면 PRD.md를 source of truth로 삼고, 그래도 모르면 사용자에게 질문**. ## 1. 빠른 사실 (인용 가능) - 패키지 매니저: pnpm 10.32.1 (lockfile commit, npm/yarn 금지). - Node: ≥24.14 (24.15 권장). - TypeScript: 5.8, strict mode. - 백엔드 프레임워크: NestJS 11 (modules, guards, pipes, interceptors). - DB: Postgres 18 (Railway `ghcr.io/railwayapp-templates/postgres-ssl:18` 권장 — pgvector + SSL 내장). - Cache: Keyv + KeyvRedis + cacheable v2. Wrap 패턴: `cache.wrap(keyOf(name, input), fn, TTL)` 강제. - Queue: BullMQ + `@nestjs/bullmq` (분산), `@nestjs/schedule` (in-process). - LLM: Upstage Solar Pro (`solar-pro3` 기본). - 외부 OAuth: Composio v0.6 SDK (gmail/googlecalendar/slack/notion/github). - Test: Jest + ts-jest, `*.spec.ts` colocated. - Hooks: Husky + lint-staged + commitlint (conventional). `--no-verify` 금지. ## 2. 폴더 구조 한 눈에 ``` 04_timelygpt/ ├── src/ ← NestJS 백엔드 (Hub) │ ├── main.ts ← bootstrap + AllExceptionsFilter │ ├── app.module.ts ← 루트 모듈 │ ├── common/ ← 공용 (대부분 pure TS) │ │ ├── errors.ts ← AppError + ErrorCodes (17개) │ │ ├── i18n/ ← 친화 메시지, 0-dep │ │ │ └── error-messages.ts │ │ ├── input-hints/ ← MCP hints vendor extension │ │ │ ├── types.ts ← HintType 14개 │ │ │ └── registry.ts ← 19 atomic 도구 매핑 │ │ ├── reference/ ← /reference/lawd-codes, /reference/sido │ │ ├── filters/all-exceptions.filter.ts │ │ ├── solar/solar.client.ts ← Upstage Solar 래퍼 (Global) │ │ ├── cache/, prisma/, data/(lawd-codes.json 250 entries) │ ├── modules/ │ │ ├── mcp/ ← @McpTool 데코레이터, ToolDiscovery, JSON-RPC │ │ ├── conversations/ ← Conversation/Message CRUD │ │ ├── composio/ ← L1 OAuth bridge │ │ ├── scheduler/ ← BullMQ Repeatable + Worker + Delivery │ │ └── tools/ │ │ ├── weather/ (6 tools) │ │ ├── air-quality/ (4) │ │ ├── apartment-rent/, apartment-trade/, arxiv/, semantic-scholar/, │ │ ├── kosis/, library/, korean-dict/, exchange/, assembly/ │ │ └── composite/ │ │ ├── daily-briefing/ ← 날씨+대기+환율 │ │ ├── morning-briefing/ ← 🆕 Calendar+Gmail+Solar │ │ ├── unanswered-reminder/ ← 🆕 Gmail inbox vs sent+Solar │ │ ├── paper-research/ │ │ └── composite-stubs.service.ts ├── apps/web/ ← Next.js 15 시범 프론트 (이식 부분만) │ ├── app/ ← 페이지 (참고만) │ ├── components/ │ │ ├── inputs/ ← 🟡 부분 이식 대상 │ │ ├── errors/ ← 🟡 부분 이식 대상 │ │ └── AppShell.tsx ├── prisma/schema.prisma ← 9 테이블 ├── specs/ │ ├── PRD.md ← immutable to agents │ ├── HANDOFF.md ← 인간용 핸드오프 │ └── plans/ ← coordinator outputs ├── LEARNINGS.md ← append-only ├── CLAUDE.md ← 프로젝트 컨벤션 ├── llms.txt ← AI 네비 └── llms-full.txt ← 이 파일 ``` ## 3. 이식 단위 우선순위 (depth-first) ### 3.1 Tier 1: 즉시 복붙 가능 (NestJS 의존 X, 0-dep pure TS) | 경로 | 무엇 | 어디로 | |---|---|---| | `src/common/errors.ts` | `AppError`, `ErrorCodes` enum (17개) | target/common/errors | | `src/common/i18n/error-messages.ts` | `friendlyError(code, raw?)` → `{category, title, message, retry}` | target/common/i18n | | `src/common/input-hints/types.ts` | 14개 `HintType` 정의 | target/common/input-hints | | `src/common/input-hints/registry.ts` | 19 atomic 도구의 입력 hint 매핑 | target/common/input-hints | | `src/common/data/lawd-codes.json` | 250개 법정동 코드 시드 | target/common/data | | `apps/web/components/inputs/location-presets.ts` | 41개 도시 lat/lon/lawdCd 시드 | target/frontend/lib | ### 3.2 Tier 2: NestJS 어댑팅 필요 | 경로 | 의존 | 어댑팅 방법 | |---|---|---| | `src/modules/mcp/` (전체) | NestJS Reflector, DI | 데코레이터 → 수동 `registry.register({...})` 호출로 교체 (1:1) | | `src/modules/tools/*/` (25개) | `@Injectable()`, `@McpTool` | `@Injectable` 제거하고 클래스 인스턴스 한 번 만들어 사용. fetch + Zod 로직 그대로 | | `src/modules/scheduler/` | BullMQ, Prisma | Redis + Postgres 충족 시 그대로. Worker는 NestJS lifecycle 사용 | | `src/modules/composio/` | Composio SDK + ConfigService | ConfigService → `process.env` 직접 | | `src/common/solar/solar.client.ts` | ConfigService, fetch | NestJS DI 제거하고 함수 export | | `src/common/reference/reference.controller.ts` | `@Controller`, `@Get` | Express/Fastify 라우터로 4 줄 변환 | ### 3.3 Tier 3: 프론트 부분 이식 (디자인 토큰 교체) | 경로 | 의존성 | 손볼 곳 | |---|---|---| | `LocationPicker.tsx` | `location-presets.ts`, `lucide-react` | Tailwind 클래스 → 자체 토큰. `lucide-react` 아이콘 → 자체 아이콘 | | `LawdCodePicker.tsx` | `/api/reference/lawd-codes` fetch | 같음 | | `SidoSelector.tsx` | `/api/reference/sido` fetch | 같음 | | `DateSelector.tsx` | 없음 (HTML5 native) | Tailwind만 | | `EnumSelector.tsx` | 없음 | Tailwind만 | | `SmartArgField.tsx` | 위 5개 | Tailwind만 | | `ErrorChip.tsx` | `lucide-react` | 색칠 + 아이콘 교체 | | `FriendlyErrorBanner.tsx` | `ErrorChip` | Tailwind 카테고리별 색 | ### 3.4 Tier 4: 참고만 (target에서 다시 짜기) - `apps/web/app/api/*` (Next route handlers — BFF 패턴은 target 컨벤션에 맞춤) - `apps/web/app/schedules,inbox,chat,settings` 페이지 ## 4. MCP `tools/list` 응답 모양 (vendor extension) ```ts { jsonrpc: "2.0", id: 1, result: { tools: [ { name: "weather_short_forecast", description: "기상청 단기예보 (3일/12시간 단위) — 위경도 또는 nx/ny 입력.", inputSchema: { /* JSON Schema from Zod */ }, hints: { latitude: { type: "location-lat-lon-pair", pair: "longitude", sample: 37.5665 }, longitude: { type: "location-lat-lon-pair", pair: "latitude", sample: 126.978 }, hours: { type: "integer", presets: [6, 12, 24, 48, 72], sample: 24 } }, annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true } } ] } } ``` Hint 14종 (`/src/common/input-hints/types.ts`): - `location-lat-lon-pair` — 위경도 한 쌍 (LocationPicker로 처리) - `lawd-cd` — 5자리 법정동 코드 (LawdCodePicker) - `sido-name` — 17개 시도 (SidoSelector) - `date-yyyymm`, `date-yyyymmdd`, `date-iso`, `datetime-yyyymmddhhmm` (DateSelector) - `labeled-enum` (EnumSelector — labels prop) - `kma-mid-region`, `kma-stn-code`, `kosis-org-id`, `kosis-tbl-id` - `text`, `integer` (fallback) ## 5. 표준 에러 응답 모양 ```ts { statusCode: 502, message: "Composio GMAIL_FETCH_EMAILS error: HTTP 503", code: "UPSTREAM_HTTP_ERROR", friendly: { category: "external", // "external" | "user" | "system" title: "⚠️ 외부 API 일시 장애", message: "Composio 또는 외부 서비스가 일시적으로 응답하지 않습니다.", retry: "auto" // "auto" | "manual" | "none" }, data: { /* optional ctx */ } } ``` MCP JSON-RPC 응답에서는 `error.data.friendly` 위치에 동일 구조 부착. ## 6. 환경변수 전체 ```bash # === 인프라 === DATABASE_URL=postgres://...?sslmode=require REDIS_URL=redis://... # === Korean public APIs (필수) === PUBLIC_API_DATA_GO_KR_KEY=... # 단일 키, 19 atomic 도구 공통 # === LLM === UPSTAGE_SOLAR_API_KEY=... UPSTAGE_SOLAR_MODEL=solar-pro3 # optional, default UPSTAGE_SOLAR_BASE_URL=... # optional # === Composio OAuth === COMPOSIO_API_KEY=... COMPOSIO_AUTH_CONFIG_GMAIL=ac_xxx COMPOSIO_AUTH_CONFIG_GOOGLECALENDAR=ac_xxx COMPOSIO_AUTH_CONFIG_SLACK=ac_xxx COMPOSIO_AUTH_CONFIG_NOTION=ac_xxx COMPOSIO_AUTH_CONFIG_GITHUB=ac_xxx COMPOSIO_ENABLED=true # prod에서 false 권장 (다중 유저 OAuth 준비 전) DEMO_USER_ID=demo-user-1 COMPOSIO_TOOLKIT_VERSION= # 강제 override (옵션) # === 운영 === PORT=3000 NODE_ENV=production ``` ## 7. 자주 묻는 이식 패턴 ### Q1. NestJS가 아닌 환경에 `@McpTool` 데코레이터를 어떻게 옮기나? ```ts // 원본 (NestJS) @Injectable() class WeatherService { @McpTool({ name: "weather_short_forecast", inputSchema: WeatherSchema, ... }) async forecast(input: WeatherInput) { ... } } // Express/Fastify 어댑팅 import { z } from "zod"; const WeatherSchema = z.object({ ... }); async function forecast(input: z.infer) { ... } // 부팅 시 한 번 registry.register({ name: "weather_short_forecast", description: "...", inputSchema: WeatherSchema, inputSchemaJSON: z.toJSONSchema(WeatherSchema), handler: forecast, hints: HINTS.weather_short_forecast, }); ``` ### Q2. friendly error를 target의 응답에 어떻게 박나? ```ts // Express middleware import { friendlyError } from "./common/i18n/error-messages"; app.use((err, req, res, next) => { const code = err.code ?? "INTERNAL_ERROR"; const friendly = friendlyError(code, err.message); res.status(err.statusCode ?? 500).json({ code, friendly, message: err.message, data: err.data }); }); ``` ### Q3. 프론트가 `hints` 필드를 어떻게 활용하나? ```tsx const t = tools.find(x => x.name === "weather_short_forecast"); const hint = t?.hints?.latitude; setForm(prev => ({ ...prev, ...updates }))} /> // → LocationPicker가 렌더되고, 사용자가 "서울 강남" 검색 시 // onPatch({ latitude: 37.5172, longitude: 127.0473 })로 둘 다 한 번에 채움 ``` ### Q4. 새로운 atomic 도구를 추가하려면? ``` 1. src/modules/tools//dto/.schema.ts ← Zod 스키마 2. src/modules/tools//.service.ts ← @McpTool 데코레이터 + fetch 3. src/modules/tools//.controller.ts ← optional REST 4. src/modules/tools//.module.ts ← providers/exports 5. src/app.module.ts ← imports에 추가 6. src/common/input-hints/registry.ts ← hints 매핑 추가 (있으면 좋음) ``` ### Q5. 새 composite 도구는? `src/modules/tools/composite//` 아래에 `.service.ts` + `.module.ts` 2 파일. 다른 atomic 도구 호출이 필요하면 module imports에 해당 모듈 추가. Solar 요약 필요하면 `SolarModule` import + `SolarClient` inject. 예시: `morning-briefing` (Calendar + Gmail + Solar 조합) 또는 `unanswered-reminder` (Gmail inbox/sent diff + Solar) 가 reference. ## 8. 카톡 합의 (2026-05-15 이호근 PM ↔ 백상현) - 시연 시나리오 2개: 미팅 사전 브리핑(`morning_briefing`), 미회신 리마인드(`unanswered_reminder`). - Slack 통합은 target 유저 시스템 붙은 뒤 후속 (코드 자리만 있음). - 이메일 전수는 24시간 기준. ## 9. 알려진 한계 / 후속 - 다중 유저 OAuth 라우팅 미구현 — `DEMO_USER_ID` 단일값 사용 중. - `COMPOSIO_ENABLED=false`인 prod에서 morning_briefing/unanswered_reminder는 `warnings`만 반환하고 정상 종료. - Slack 통합 후 unanswered_reminder에 slack thread fetch 한 블록 추가 필요 (Solar 프롬프트는 그대로 OK). - KakaoTalk/Email delivery handler는 `not-implemented.handlers.ts`에 stub만 있음. ## 10. 검증 명령 (이식 후) ```bash pnpm install pnpm prisma migrate deploy pnpm build && pnpm test npx tsc --noEmit pnpm start:dev # MCP tools/list 확인 curl -X POST http://localhost:3000/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length' # 기대값: 25 (또는 COMPOSIO_ENABLED=true면 +N L1) # 신규 composite 호출 curl -X POST http://localhost:3000/mcp -H 'Content-Type: application/json' -d '{ "jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"morning_briefing","arguments":{}} }' # 기대값: events[], attendeeMailCounts, summary, preparationItems[], warnings[] ``` ## 11. 참고 외부 문서 - MCP 스펙: https://modelcontextprotocol.io - Composio 가이드: https://docs.composio.dev - Upstage Solar: https://console.upstage.ai - 공공데이터포털: https://data.go.kr - llmstxt.org (이 파일 포맷): https://llmstxt.org