{"system":{"name":"08l-hub","version":"0.2.0","description":"08Liter AI-agent CRM platform. Query brand/campaign/partner data from the 08Liter marketing marketplace, create CRM segments and email drafts, manage a human-in-the-loop review gate, and dynamically build admin dashboard pages.","data_source":"08Liter zeliter MariaDB (B2B only — brand/partner/partner_admin/campaign). B2C member marketing is blocked at the data layer.","integration_guide":"https://hub.08liter.com/docs/integration","openapi":"https://hub.08liter.com/openapi.yaml","postman":"https://hub.08liter.com/postman.json","human_docs":"https://hub.08liter.com/docs"},"glossary":{"summary":"'법인 회원' == partner (DB). '브랜드' == brand (DB) == the marketing unit owned by a partner. '법인 담당자' == partner_admin (DB) == the email recipient. '일반 회원' == member (DB) == B2C reviewer, NOT marketable.","core_terms":[{"ko":"법인 회원","en":"partner","table":"partner","notes":"The B2B account."},{"ko":"브랜드","en":"brand","table":"brand","notes":"Marketing unit owned by a partner (N:1)."},{"ko":"법인 담당자","en":"partner_admin","table":"partner_admin","notes":"Email recipient. Must be verified."},{"ko":"캠페인","en":"campaign","table":"campaign","notes":"A single marketing run."},{"ko":"일반 회원","en":"member","table":"member","notes":"B2C reviewer — NOT marketable."}],"status_codes":{"1206":"참여자 모집중 (accepting)","1208":"심사 중 (under review)","1209":"완료 (closed)","1210":"취소 (cancelled)","1211":"반려 (rejected)"},"full_reference":"https://hub.08liter.com/docs/glossary"},"connection":{"http":{"base_url":"https://hub.08liter.com","tool_execute":"POST /api/tools/{name}","tool_detail":"GET /api/tools/{name}/detail","tool_catalog":"GET /api/tools","auth_header":"Authorization: Bearer crm_xxx","alt_auth_header":"X-API-Key: crm_xxx","idempotency_header":"Idempotency-Key: <uuid>  (required for write tools, optional otherwise)","request_id_header":"X-Request-Id: <uuid>      (optional; echoed in response envelope)"},"mcp_stdio":{"description":"Claude Cowork / Claude Code direct MCP stdio (alternative to HTTP).","config":{"mcpServers":{"08l-hub":{"command":"npx","args":["tsx","--env-file=/Users/joey/dev/code/work/08l/08l-hub/.env","/Users/joey/dev/code/work/08l/08l-hub/apps/server/src/index.ts"]}}}}},"response_envelope":{"success":{"ok":true,"tool":"<tool name>","data":"<tool-specific object>","count":"<primary row count>","request_id":"req_...","took_ms":0},"failure":{"ok":false,"error":{"code":"E_VALIDATION | E_AUTH_* | E_SCOPE | E_RATE_LIMIT | ...","message":"<human text>","details":"<optional>"},"request_id":"req_...","took_ms":0},"error_code_reference":"https://hub.08liter.com/docs/error-codes"},"constraints":{"read_only_source":"zeliter MariaDB — SET SESSION TRANSACTION READ ONLY on every query","write_target":"crm-pg PostgreSQL (segments, drafts, menus, audit)","max_row_limit":"MCP_MAX_ROW_LIMIT env (default 1000)","query_timeout":"MCP_QUERY_TIMEOUT_MS env (default 30s)","no_send":"HTTP gateway never sends email — use queue_for_review then approve via web UI","b2c_blocked":"member marketing consent is 100% NULL in DB — B2C outreach blocked at data layer","suppression":"crm-pg Suppression table checked before every email send"},"modules":[{"slug":"pay","name":"Payment","tagline":"NicePay 단일 가맹점으로 묶는 결제 허브","ai_docs":"https://hub.08liter.com/docs/modules/pay","demo":"https://hub.08liter.com/pay-demo"},{"slug":"crm","name":"CRM","tagline":"B2B 마케팅 메시지의 휴먼 게이트","ai_docs":"https://hub.08liter.com/docs/modules/crm","demo":"https://hub.08liter.com/modules/crm/demo"}],"modules_index":"https://hub.08liter.com/docs/modules","tools":[{"name":"describe_schema","category":"explore","description":"Curated quick-look at 08Liter zeliter tables. Without arguments, lists the canonical CRM tables (B2B partner/brand/campaign + B2C member/liter/apply/review) with approximate row counts. With `table`, returns the column list and comments. Tables outside the curated set are still queryable via get_ddl + execute_read_sql. Read-only — no schema reveals secret data and the marketing send path remains B2B-only regardless of what you read.","write":false,"input_schema":{"type":"object","properties":{"table":{"type":"string","description":"Specific table to describe. Omit to list the curated CRM tables (B2B + B2C analytics) with row counts. Any base table name is accepted; use get_ddl with no arguments to enumerate the full schema first."}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/describe_schema","curl":"curl -X POST https://hub.08liter.com/api/tools/describe_schema \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"table\":\"brand\"}'","fetch":"await fetch('https://hub.08liter.com/api/tools/describe_schema', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"table\": \"brand\"\n}),\n});"}},{"name":"get_ddl","category":"explore","description":"Full-schema introspection. Without arguments, lists every base table in the zeliter DB (primary CRM tables flagged + all others). With `table`, returns the CREATE TABLE DDL plus structured column/index metadata. Pair with execute_read_sql for ad-hoc analytics beyond the curated tools.","write":false,"input_schema":{"type":"object","properties":{"table":{"type":"string","description":"Table name to inspect. Omit to list every base table in the current schema with row counts."},"includeIndexes":{"type":"boolean","description":"If true (default), include index definitions alongside columns."}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/get_ddl","curl":"curl -X POST https://hub.08liter.com/api/tools/get_ddl \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","fetch":"await fetch('https://hub.08liter.com/api/tools/get_ddl', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});"}},{"name":"execute_read_sql","category":"explore","description":"Run an ad-hoc read-only SQL query against the 08Liter zeliter DB. Any base table is queryable — including B2C tables (member, member_liter_transaction, campaign_apply, campaign_review, etc.) for analytics and reporting. Use get_ddl to discover tables/columns first. Row cap is applied to the FINAL output rows only by wrapping your query as `SELECT * FROM (<your sql>) AS t LIMIT N` — aggregations (SUM/COUNT/GROUP BY), JOINs, and ORDER BY run over the full underlying dataset, so totals stay correct regardless of the cap. Enforces: SELECT-class statements only (SELECT/WITH/SHOW/EXPLAIN/DESCRIBE), no semicolons, output rows clamped to MCP_MAX_ROW_LIMIT (10000 in production), query timeout, READ ONLY session. Every call is audit-logged with the executed SQL, returned column list, row count, and truncation flag.","write":false,"input_schema":{"type":"object","properties":{"sql":{"type":"string","minLength":1,"maxLength":8000,"description":"Single read-only SQL statement. Allowed forms: SELECT, WITH ... SELECT, SHOW, EXPLAIN, DESCRIBE. Multi-statement (';' between statements) is rejected. The session is set to READ ONLY; writes fail at the DB level too."},"limit":{"type":"integer","minimum":1,"maximum":1000000,"description":"Row cap applied by wrapping the query in a subselect. Defaults to 1000. Clamped at runtime to MCP_MAX_ROW_LIMIT (currently 10000 in production)."}},"required":["sql"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/execute_read_sql","curl":"curl -X POST https://hub.08liter.com/api/tools/execute_read_sql \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","fetch":"await fetch('https://hub.08liter.com/api/tools/execute_read_sql', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});"}},{"name":"list_categories","category":"query","description":"08Liter canonical category tree. Use this to resolve numeric category_id values returned by search_brands into human-readable names, or to pick a category_id for scenario filtering. Top-level categories (parent_id=0) include 뷰티(12), 패션(13), 라이프(14), 식품(33), 유아(34), 취미(35), 반려동물(6666).","write":false,"input_schema":{"type":"object","properties":{"parentId":{"type":"integer","description":"Return only children of this category id. Omit for top-level (parent_id=0)."},"language":{"type":"string","enum":["ko","en","ja","cn","vi","in"],"description":"Language for category_name"}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_categories","curl":"curl -X POST https://hub.08liter.com/api/tools/list_categories \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"parentId\":12,\"language\":\"ko\"}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_categories', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"parentId\": 12,\n  \"language\": \"ko\"\n}),\n});"}},{"name":"search_brands","category":"query","description":"Search 08Liter brands (법인 회원의 마케팅 단위) with CRM-relevant filters. Returns brand identity, category, most-recent-campaign date, and a single contactable partner_admin (only when hasVerifiedContact=true). Always joins partner/partner_admin; never exposes unverified contacts. 'Brand' here means the 08L business entity — NOT a B2C member.","write":false,"input_schema":{"type":"object","properties":{"categoryId":{"type":"integer","minimum":0},"countryCode":{"type":"string","maxLength":5},"lastCampaignBefore":{"type":"string","description":"ISO date — brands whose most recent campaign.start_at is before this"},"lastCampaignAfter":{"type":"string","description":"ISO date — brands whose most recent campaign.start_at is after this"},"hasVerifiedContact":{"type":"boolean","description":"Require at least one partner_admin with verified email"},"keyword":{"type":"string","description":"Substring match against brand.president or partner.name"},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/search_brands","curl":"curl -X POST https://hub.08liter.com/api/tools/search_brands \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"categoryId\":12,\"hasVerifiedContact\":true,\"lastCampaignBefore\":\"2025-01-01\",\"limit\":10}'","fetch":"await fetch('https://hub.08liter.com/api/tools/search_brands', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"categoryId\": 12,\n  \"hasVerifiedContact\": true,\n  \"lastCampaignBefore\": \"2025-01-01\",\n  \"limit\": 10\n}),\n});"}},{"name":"get_brand","category":"query","description":"Drill into a single brand (법인 회원) with full CRM context: brand profile, parent partner, ALL verified contacts (not just one), N most-recent campaigns, and current liter balance from the latest deposit transaction. Use this after search_brands to build personalized outreach content.","write":false,"input_schema":{"type":"object","properties":{"brandId":{"type":"integer","minimum":0},"recentCampaignsLimit":{"type":"integer","minimum":0,"maximum":20,"description":"How many recent campaigns to include (0 to skip)"}},"required":["brandId"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/get_brand","curl":"curl -X POST https://hub.08liter.com/api/tools/get_brand \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"brandId\":12345,\"recentCampaignsLimit\":5}'","fetch":"await fetch('https://hub.08liter.com/api/tools/get_brand', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"brandId\": 12345,\n  \"recentCampaignsLimit\": 5\n}),\n});"}},{"name":"search_campaigns","category":"query","description":"Search 08Liter campaigns with CRM-relevant filters. Returns campaign metadata plus the owning partner (법인 회원). Common status_codes: 1206=accepting, 1208=under review, 1209=closed/completed, 1210=cancelled, 1211=rejected. Use partnerId (obtained via search_brands) to scope to one brand owner.","write":false,"input_schema":{"type":"object","properties":{"partnerId":{"type":"integer","minimum":0,"description":"Filter by partner_id"},"statusCodes":{"type":"array","items":{"type":"string"},"description":"Campaign status_code values. Common: 1206=accepting, 1208=review, 1209=closed, 1210=cancelled, 1211=rejected"},"startedAfter":{"type":"string","description":"ISO date — campaign.start_at after this"},"startedBefore":{"type":"string","description":"ISO date — campaign.start_at before this"},"keyword":{"type":"string","description":"Substring match against campaign.name"},"minApplyCount":{"type":"integer","minimum":0,"description":"Minimum apply_count — useful for finding high-traction campaigns"},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/search_campaigns","curl":"curl -X POST https://hub.08liter.com/api/tools/search_campaigns \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"statusCodes\":[\"1209\"],\"minApplyCount\":100,\"limit\":10}'","fetch":"await fetch('https://hub.08liter.com/api/tools/search_campaigns', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"statusCodes\": [\n    \"1209\"\n  ],\n  \"minApplyCount\": 100,\n  \"limit\": 10\n}),\n});"}},{"name":"search_partner_contacts","category":"query","description":"List ALL verified partner_admin contacts (법인 담당자) for a partner (법인 회원). Use this when you need to reach multiple contacts at one brand owner (e.g. CC the marketing team, reach out to multiple decision-makers). Every returned row is guaranteed to have a verified email and an active account.","write":false,"input_schema":{"type":"object","properties":{"partnerId":{"type":"integer","minimum":0},"includeInactive":{"type":"boolean","description":"Include admins whose status is not READY (e.g. LEAVE, PENDING). Default false."}},"required":["partnerId"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/search_partner_contacts","curl":"curl -X POST https://hub.08liter.com/api/tools/search_partner_contacts \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"partnerId\":12345,\"includeInactive\":false}'","fetch":"await fetch('https://hub.08liter.com/api/tools/search_partner_contacts', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"partnerId\": 12345,\n  \"includeInactive\": false\n}),\n});"}},{"name":"get_campaign_performance","category":"query","description":"Engagement snapshot for a single campaign: raw counts (view/apply/offer) plus derived ratios (fill rate, application density, oversubscription). Use this to pick high-performing campaigns for reference when drafting outreach, or to assess whether a partner (법인 회원) should be approached based on past traction.","write":false,"input_schema":{"type":"object","properties":{"campaignId":{"type":"integer","minimum":0}},"required":["campaignId"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/get_campaign_performance","curl":"curl -X POST https://hub.08liter.com/api/tools/get_campaign_performance \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"campaignId\":98765}'","fetch":"await fetch('https://hub.08liter.com/api/tools/get_campaign_performance', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"campaignId\": 98765\n}),\n});"}},{"name":"list_segments","category":"query","description":"List existing CRM segments stored in crm-pg. Use this to discover what target lists have already been created, before creating duplicates. Returns segment id, name, kind, target count, and creation info.","write":false,"input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100},"kind":{"type":"string","enum":["partner","brand","campaign"]}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_segments","curl":"curl -X POST https://hub.08liter.com/api/tools/list_segments \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"limit\":10}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_segments', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"limit\": 10\n}),\n});"}},{"name":"list_drafts","category":"query","description":"List existing email drafts in crm-pg with status, target count, and segment info. Use this to discover what campaigns are in progress before creating new ones.","write":false,"input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100},"status":{"type":"string","enum":["draft","in_review","approved","rejected","sent","cancelled"],"description":"Filter by draft status"}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_drafts","curl":"curl -X POST https://hub.08liter.com/api/tools/list_drafts \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"limit\":10,\"status\":\"in_review\"}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_drafts', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"limit\": 10,\n  \"status\": \"in_review\"\n}),\n});"}},{"name":"list_menu_groups","category":"query","description":"List all dynamic menu groups and their items in the crm-web admin UI. Use this to discover existing menus before creating new ones, or to find the groupSlug/itemSlug needed for upsert_dynamic_page.","write":false,"input_schema":{"type":"object","properties":{"includeFixed":{"type":"boolean","description":"Include built-in fixed menu groups (System, Admin)"}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_menu_groups","curl":"curl -X POST https://hub.08liter.com/api/tools/list_menu_groups \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"includeFixed\":false}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_menu_groups', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n  \"includeFixed\": false\n}),\n});"}},{"name":"create_segment","category":"author","description":"Persist a named target list in crm-pg so later tools can reference it. Use this after a search_brands / search_partner_contacts / search_campaigns call to freeze the result set, then pass the returned segment_id to save_message_draft. Each target row captures the full snapshot for audit. kind=brand/partner means 법인 회원 단위, campaign means 캠페인 단위.","write":true,"input_schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Human-readable segment name"},"kind":{"type":"string","enum":["partner","brand","campaign"]},"description":{"type":"string","maxLength":1000},"createdBy":{"type":"string","maxLength":200,"description":"Actor identifier (manager or agent name)"},"targets":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"integer","minimum":0,"description":"zeliter primary key — partner.id, brand.id, or campaign.id"},"snapshot":{"type":"object","additionalProperties":{},"description":"Full row captured from the search tool"}},"required":["externalId","snapshot"],"additionalProperties":false}}},"required":["name","kind","targets"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/create_segment","curl":"curl -X POST https://hub.08liter.com/api/tools/create_segment \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"name\":\"dormant beauty 2026Q2\",\"kind\":\"partner\",\"targets\":[{\"externalId\":12345,\"snapshot\":{\"partner_name\":\"예시\"}}]}'","fetch":"await fetch('https://hub.08liter.com/api/tools/create_segment', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"name\": \"dormant beauty 2026Q2\",\n  \"kind\": \"partner\",\n  \"targets\": [\n    {\n      \"externalId\": 12345,\n      \"snapshot\": {\n        \"partner_name\": \"예시\"\n      }\n    }\n  ]\n}),\n});"}},{"name":"save_message_draft","category":"author","description":"Persist an AI-generated email campaign targeted at partner_admin (법인 담당자) with per-target rendering. Takes a segment_id plus a rendered message per target (subject + body + toEmail). Creates an EmailDraft in \"draft\" status — NOT sent. Reviewers will see it after queue_for_review is called. Always pass the exact toEmail you want to send to; the suppression list is checked at queue time, not here. B2C (일반 member) recipients are blocked upstream and must not appear here.","write":true,"input_schema":{"type":"object","properties":{"segmentId":{"type":"string","description":"Segment id returned from create_segment (string for BigInt safety)"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Campaign name — e.g. \"2026Q2 dormant beauty reactivation\""},"subjectTemplate":{"type":"string","minLength":1,"description":"Default subject; overridden per target if target.subject is provided"},"bodyTemplate":{"type":"string","minLength":1,"description":"Default body; overridden per target if target.body is provided"},"senderName":{"type":"string"},"senderEmail":{"type":"string","format":"email"},"createdBy":{"type":"string","maxLength":200},"targets":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"integer","minimum":0,"description":"zeliter partner.id (B2B only)"},"toEmail":{"type":"string","format":"email"},"toName":{"type":"string"},"subject":{"type":"string","description":"Per-target override; falls back to subjectTemplate"},"body":{"type":"string","description":"Per-target override; falls back to bodyTemplate"},"context":{"type":"object","additionalProperties":{},"description":"Anything the reviewer should see"}},"required":["externalId","toEmail"],"additionalProperties":false}}},"required":["segmentId","name","subjectTemplate","bodyTemplate","targets"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/save_message_draft","curl":"curl -X POST https://hub.08liter.com/api/tools/save_message_draft \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"segmentId\":\"1\",\"name\":\"2026Q2 dormant reactivation\",\"subjectTemplate\":\"[08Liter] 재활성화 제안\",\"bodyTemplate\":\"(본문)\",\"targets\":[{\"externalId\":12345,\"toEmail\":\"admin@example.com\",\"toName\":\"홍길동\",\"subject\":\"[08Liter] {brand_name} 재활성화 제안\",\"body\":\"...\"}]}'","fetch":"await fetch('https://hub.08liter.com/api/tools/save_message_draft', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"segmentId\": \"1\",\n  \"name\": \"2026Q2 dormant reactivation\",\n  \"subjectTemplate\": \"[08Liter] 재활성화 제안\",\n  \"bodyTemplate\": \"(본문)\",\n  \"targets\": [\n    {\n      \"externalId\": 12345,\n      \"toEmail\": \"admin@example.com\",\n      \"toName\": \"홍길동\",\n      \"subject\": \"[08Liter] {brand_name} 재활성화 제안\",\n      \"body\": \"...\"\n    }\n  ]\n}),\n});"}},{"name":"queue_for_review","category":"control","description":"Hand a draft off to a human reviewer through the crm-web review gate. Transitions status draft -> in_review and sets queuedAt. The MCP server never sends email; after this call, the draft is visible at /drafts in crm-web and a reviewer approves or rejects per target. This is the hard gate between AI generation and outbound traffic.","write":true,"input_schema":{"type":"object","properties":{"draftId":{"type":"string","description":"Draft id returned from save_message_draft"},"actor":{"type":"string","maxLength":200,"description":"Who queued it"},"note":{"type":"string","maxLength":2000}},"required":["draftId"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/queue_for_review","curl":"curl -X POST https://hub.08liter.com/api/tools/queue_for_review \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"draftId\":\"1\",\"note\":\"please review by Fri\"}'","fetch":"await fetch('https://hub.08liter.com/api/tools/queue_for_review', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"draftId\": \"1\",\n  \"note\": \"please review by Fri\"\n}),\n});"}},{"name":"create_menu_group","category":"admin","description":"Create a new top-level menu group in the crm-web admin UI header. Groups appear as tabs in the top navigation bar. Each group contains sidebar menu items (added via create_menu_item). Use this when the user asks to create a new section in the admin, e.g. \"CRM 메뉴를 만들어줘\".","write":true,"input_schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50,"description":"Display name for the menu group (e.g. \"CRM\", \"Analytics\")"},"slug":{"type":"string","minLength":1,"maxLength":50,"description":"URL-safe slug (e.g. \"crm\", \"analytics\")"},"icon":{"type":"string","maxLength":10,"description":"Emoji icon (e.g. \"📊\")"},"position":{"type":"integer","minimum":0,"description":"Sort order (lower = more left)"}},"required":["name","slug"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/create_menu_group","curl":"curl -X POST https://hub.08liter.com/api/tools/create_menu_group \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"name\":\"CRM\",\"slug\":\"crm\",\"icon\":\"📊\",\"position\":10}'","fetch":"await fetch('https://hub.08liter.com/api/tools/create_menu_group', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"name\": \"CRM\",\n  \"slug\": \"crm\",\n  \"icon\": \"📊\",\n  \"position\": 10\n}),\n});"}},{"name":"create_menu_item","category":"admin","description":"Add a sidebar menu item under an existing menu group. The item will appear in the left sidebar when its parent group is selected. For dynamic pages, leave href empty — the system generates /p/{groupSlug}/{itemSlug} automatically. Then call upsert_dynamic_page to define the page content.","write":true,"input_schema":{"type":"object","properties":{"groupSlug":{"type":"string","description":"Parent menu group slug (from create_menu_group)"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Display name (e.g. \"Dormant Brands\")"},"slug":{"type":"string","minLength":1,"maxLength":100,"description":"URL-safe slug"},"icon":{"type":"string","maxLength":10,"description":"Emoji icon"},"href":{"type":"string","description":"Custom href (leave empty for dynamic page at /p/{groupSlug}/{slug})"},"position":{"type":"integer","minimum":0}},"required":["groupSlug","name","slug"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/create_menu_item","curl":"curl -X POST https://hub.08liter.com/api/tools/create_menu_item \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"groupSlug\":\"crm\",\"name\":\"Dormant Brands\",\"slug\":\"dormant\"}'","fetch":"await fetch('https://hub.08liter.com/api/tools/create_menu_item', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"groupSlug\": \"crm\",\n  \"name\": \"Dormant Brands\",\n  \"slug\": \"dormant\"\n}),\n});"}},{"name":"upsert_dynamic_page","category":"admin","description":"Create or update the content of a dynamic admin page. The page renders at /p/{groupSlug}/{itemSlug}. Sections define the page layout: \"text\" for HTML content, \"stats\" for metric cards, \"table\" for data tables. You can put search results from search_brands or search_campaigns directly into a table section's rows. Call this after create_menu_group + create_menu_item.","write":true,"input_schema":{"type":"object","properties":{"groupSlug":{"type":"string"},"itemSlug":{"type":"string"},"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000},"sections":{"type":"array","items":{"description":"(unsupported zod type: ZodDiscriminatedUnion)"}},"createdBy":{"type":"string","maxLength":200}},"required":["groupSlug","itemSlug","title","sections"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/upsert_dynamic_page","curl":"curl -X POST https://hub.08liter.com/api/tools/upsert_dynamic_page \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"groupSlug\":\"crm\",\"itemSlug\":\"dormant\",\"title\":\"Dormant Brands\",\"sections\":[{\"type\":\"text\",\"body\":\"<h1>Dormant Brands</h1>\"}]}'","fetch":"await fetch('https://hub.08liter.com/api/tools/upsert_dynamic_page', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n    'Idempotency-Key': crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n  \"groupSlug\": \"crm\",\n  \"itemSlug\": \"dormant\",\n  \"title\": \"Dormant Brands\",\n  \"sections\": [\n    {\n      \"type\": \"text\",\n      \"body\": \"<h1>Dormant Brands</h1>\"\n    }\n  ]\n}),\n});"}},{"name":"list_connected_apps","category":"admin","description":"List Connected Apps registered with 08L Hub (the services that call hub via ServiceJWT). Returns service_code, name, status, allowed redirect URLs, allowed origins, requested scopes, and whether a shared secret has been issued — never the secret itself. Use this to verify that a service is registered and what scopes it has before driving payment / id flows.","write":false,"input_schema":{"type":"object","properties":{"status":{"type":"string","enum":["ACTIVE","SUSPENDED","REVOKED"],"description":"Filter by status. Omit to return all."},"limit":{"type":"integer","minimum":1,"maximum":500}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_connected_apps","curl":"curl -X POST https://hub.08liter.com/api/tools/list_connected_apps \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_connected_apps', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});"}},{"name":"list_pg_credentials","category":"admin","description":"List PG credentials registered for the hub (currently NicePay only). Shows provider, env (sandbox/production), masked client_id, active flag, and timestamps — NEVER the encrypted secret. NOTE: 08Liter is already a NicePay merchant of record; you do NOT need to register anything in NicePay's own console. This list reflects which credential rows exist in the hub's pay_pg_credential table for use by /pay/* routes.","write":false,"input_schema":{"type":"object","properties":{"env":{"type":"string","enum":["sandbox","production"],"description":"Filter by environment. Omit to return both."},"active_only":{"type":"boolean","description":"When true, return only active=true rows."}},"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/list_pg_credentials","curl":"curl -X POST https://hub.08liter.com/api/tools/list_pg_credentials \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","fetch":"await fetch('https://hub.08liter.com/api/tools/list_pg_credentials', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});"}},{"name":"inspect_connected_app","category":"admin","description":"Inspect a single Connected App by service_code. Returns the same fields as list_connected_apps plus the count of identity links bound to this service. Use this when an integration is failing (e.g. ACCOUNT_LINK_REQUIRED) and you need to verify scopes / redirect URLs / origins / secret-issued state.","write":false,"input_schema":{"type":"object","properties":{"service_code":{"type":"string","minLength":1,"maxLength":40,"description":"The Connected App service_code (e.g. \"new-admin\", \"08liter-app\")."}},"required":["service_code"],"additionalProperties":false},"http_example":{"method":"POST","path":"/api/tools/inspect_connected_app","curl":"curl -X POST https://hub.08liter.com/api/tools/inspect_connected_app \\\n  -H \"Authorization: Bearer crm_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","fetch":"await fetch('https://hub.08liter.com/api/tools/inspect_connected_app', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer crm_YOUR_KEY',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});"}}],"tool_count":21,"getting_started":{"step_1":"Call GET /api/tools/{name}/detail for a detailed spec of a single tool","step_2":"Call describe_schema to learn available 08L tables and columns","step_3":"Call list_categories to resolve category IDs (뷰티=12, 패션=13, ...)","step_4":"Use search_brands to find target 법인 회원, then create_segment → save_message_draft → queue_for_review","step_5":"Human reviews at /drafts/{id} and approves per-target before send"}}