08l-hub

API Reference

18 tools exposed by 08l-hub. Callable over both MCP stdio and HTTP. Machine-readable catalog at /api/tools. Business-term glossary at /docs/glossary.

Explore (3)

describe_schema

Explore

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.

Parameters

NameTypeReqDescription
tablestringSpecific 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.

curl

curl -X POST http://localhost:3030/api/tools/describe_schema \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"table":"brand"}'

fetch

await fetch('http://localhost:3030/api/tools/describe_schema', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "table": "brand"
}),
});

get_ddl

Explore

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.

Parameters

NameTypeReqDescription
tablestringTable name to inspect. Omit to list every base table in the current schema with row counts.
includeIndexesbooleanIf true (default), include index definitions alongside columns.

curl

curl -X POST http://localhost:3030/api/tools/get_ddl \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

fetch

await fetch('http://localhost:3030/api/tools/get_ddl', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});

execute_read_sql

Explore

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.

Parameters

NameTypeReqDescription
sqlstringβœ“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.
limitintegerRow cap applied by wrapping the query in a subselect. Defaults to 1000. Clamped at runtime to MCP_MAX_ROW_LIMIT (currently 10000 in production).

curl

curl -X POST http://localhost:3030/api/tools/execute_read_sql \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

fetch

await fetch('http://localhost:3030/api/tools/execute_read_sql', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});

Query (read 08L) (9)

list_categories

Query (read 08L)

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).

Parameters

NameTypeReqDescription
parentIdintegerReturn only children of this category id. Omit for top-level (parent_id=0).
languageenum(ko|en|ja|cn|vi|in)Language for category_name

curl

curl -X POST http://localhost:3030/api/tools/list_categories \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"parentId":12,"language":"ko"}'

fetch

await fetch('http://localhost:3030/api/tools/list_categories', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "parentId": 12,
  "language": "ko"
}),
});

search_brands

Query (read 08L)

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.

Parameters

NameTypeReqDescription
categoryIdinteger
countryCodestring
lastCampaignBeforestringISO date β€” brands whose most recent campaign.start_at is before this
lastCampaignAfterstringISO date β€” brands whose most recent campaign.start_at is after this
hasVerifiedContactbooleanRequire at least one partner_admin with verified email
keywordstringSubstring match against brand.president or partner.name
limitinteger
offsetinteger

curl

curl -X POST http://localhost:3030/api/tools/search_brands \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"categoryId":12,"hasVerifiedContact":true,"lastCampaignBefore":"2025-01-01","limit":10}'

fetch

await fetch('http://localhost:3030/api/tools/search_brands', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "categoryId": 12,
  "hasVerifiedContact": true,
  "lastCampaignBefore": "2025-01-01",
  "limit": 10
}),
});

get_brand

Query (read 08L)

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.

Parameters

NameTypeReqDescription
brandIdintegerβœ“
recentCampaignsLimitintegerHow many recent campaigns to include (0 to skip)

curl

curl -X POST http://localhost:3030/api/tools/get_brand \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"brandId":12345,"recentCampaignsLimit":5}'

fetch

await fetch('http://localhost:3030/api/tools/get_brand', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "brandId": 12345,
  "recentCampaignsLimit": 5
}),
});

search_campaigns

Query (read 08L)

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.

Parameters

NameTypeReqDescription
partnerIdintegerFilter by partner_id
statusCodesarrayCampaign status_code values. Common: 1206=accepting, 1208=review, 1209=closed, 1210=cancelled, 1211=rejected
startedAfterstringISO date β€” campaign.start_at after this
startedBeforestringISO date β€” campaign.start_at before this
keywordstringSubstring match against campaign.name
minApplyCountintegerMinimum apply_count β€” useful for finding high-traction campaigns
limitinteger
offsetinteger

curl

curl -X POST http://localhost:3030/api/tools/search_campaigns \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"statusCodes":["1209"],"minApplyCount":100,"limit":10}'

fetch

await fetch('http://localhost:3030/api/tools/search_campaigns', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "statusCodes": [
    "1209"
  ],
  "minApplyCount": 100,
  "limit": 10
}),
});

search_partner_contacts

Query (read 08L)

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.

Parameters

NameTypeReqDescription
partnerIdintegerβœ“
includeInactivebooleanInclude admins whose status is not READY (e.g. LEAVE, PENDING). Default false.

curl

curl -X POST http://localhost:3030/api/tools/search_partner_contacts \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"partnerId":12345,"includeInactive":false}'

fetch

await fetch('http://localhost:3030/api/tools/search_partner_contacts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "partnerId": 12345,
  "includeInactive": false
}),
});

get_campaign_performance

Query (read 08L)

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.

Parameters

NameTypeReqDescription
campaignIdintegerβœ“

curl

curl -X POST http://localhost:3030/api/tools/get_campaign_performance \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"campaignId":98765}'

fetch

await fetch('http://localhost:3030/api/tools/get_campaign_performance', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "campaignId": 98765
}),
});

list_segments

Query (read 08L)

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.

Parameters

NameTypeReqDescription
limitinteger
kindenum(partner|brand|campaign)

curl

curl -X POST http://localhost:3030/api/tools/list_segments \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit":10}'

fetch

await fetch('http://localhost:3030/api/tools/list_segments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "limit": 10
}),
});

list_drafts

Query (read 08L)

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.

Parameters

NameTypeReqDescription
limitinteger
statusenum(draft|in_review|approved|rejected|sent|cancelled)Filter by draft status

curl

curl -X POST http://localhost:3030/api/tools/list_drafts \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit":10,"status":"in_review"}'

fetch

await fetch('http://localhost:3030/api/tools/list_drafts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "limit": 10,
  "status": "in_review"
}),
});

list_menu_groups

Query (read 08L)

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.

Parameters

NameTypeReqDescription
includeFixedbooleanInclude built-in fixed menu groups (System, Admin)

curl

curl -X POST http://localhost:3030/api/tools/list_menu_groups \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"includeFixed":false}'

fetch

await fetch('http://localhost:3030/api/tools/list_menu_groups', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "includeFixed": false
}),
});

Author (write CRM) (2)

create_segment

Author (write CRM)writes crm-pg

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 캠페인 λ‹¨μœ„.

Parameters

NameTypeReqDescription
namestringβœ“Human-readable segment name
kindenum(partner|brand|campaign)βœ“
descriptionstring
createdBystringActor identifier (manager or agent name)
targetsarrayβœ“

curl

curl -X POST http://localhost:3030/api/tools/create_segment \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"dormant beauty 2026Q2","kind":"partner","targets":[{"externalId":12345,"snapshot":{"partner_name":"μ˜ˆμ‹œ"}}]}'

fetch

await fetch('http://localhost:3030/api/tools/create_segment', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "name": "dormant beauty 2026Q2",
  "kind": "partner",
  "targets": [
    {
      "externalId": 12345,
      "snapshot": {
        "partner_name": "μ˜ˆμ‹œ"
      }
    }
  ]
}),
});

save_message_draft

Author (write CRM)writes crm-pg

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.

Parameters

NameTypeReqDescription
segmentIdstringβœ“Segment id returned from create_segment (string for BigInt safety)
namestringβœ“Campaign name β€” e.g. "2026Q2 dormant beauty reactivation"
subjectTemplatestringβœ“Default subject; overridden per target if target.subject is provided
bodyTemplatestringβœ“Default body; overridden per target if target.body is provided
senderNamestring
senderEmailstring
createdBystring
targetsarrayβœ“

curl

curl -X POST http://localhost:3030/api/tools/save_message_draft \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -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('http://localhost:3030/api/tools/save_message_draft', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "segmentId": "1",
  "name": "2026Q2 dormant reactivation",
  "subjectTemplate": "[08Liter] μž¬ν™œμ„±ν™” μ œμ•ˆ",
  "bodyTemplate": "(λ³Έλ¬Έ)",
  "targets": [
    {
      "externalId": 12345,
      "toEmail": "admin@example.com",
      "toName": "홍길동",
      "subject": "[08Liter] {brand_name} μž¬ν™œμ„±ν™” μ œμ•ˆ",
      "body": "..."
    }
  ]
}),
});

Control (1)

queue_for_review

Controlwrites crm-pg

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.

Parameters

NameTypeReqDescription
draftIdstringβœ“Draft id returned from save_message_draft
actorstringWho queued it
notestring

curl

curl -X POST http://localhost:3030/api/tools/queue_for_review \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"draftId":"1","note":"please review by Fri"}'

fetch

await fetch('http://localhost:3030/api/tools/queue_for_review', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "draftId": "1",
  "note": "please review by Fri"
}),
});

Admin (menu/page) (3)

create_menu_group

Admin (menu/page)writes crm-pg

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 메뉴λ₯Ό λ§Œλ“€μ–΄μ€˜".

Parameters

NameTypeReqDescription
namestringβœ“Display name for the menu group (e.g. "CRM", "Analytics")
slugstringβœ“URL-safe slug (e.g. "crm", "analytics")
iconstringEmoji icon (e.g. "πŸ“Š")
positionintegerSort order (lower = more left)

curl

curl -X POST http://localhost:3030/api/tools/create_menu_group \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"CRM","slug":"crm","icon":"πŸ“Š","position":10}'

fetch

await fetch('http://localhost:3030/api/tools/create_menu_group', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "name": "CRM",
  "slug": "crm",
  "icon": "πŸ“Š",
  "position": 10
}),
});

create_menu_item

Admin (menu/page)writes crm-pg

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.

Parameters

NameTypeReqDescription
groupSlugstringβœ“Parent menu group slug (from create_menu_group)
namestringβœ“Display name (e.g. "Dormant Brands")
slugstringβœ“URL-safe slug
iconstringEmoji icon
hrefstringCustom href (leave empty for dynamic page at /p/{groupSlug}/{slug})
positioninteger

curl

curl -X POST http://localhost:3030/api/tools/create_menu_item \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"groupSlug":"crm","name":"Dormant Brands","slug":"dormant"}'

fetch

await fetch('http://localhost:3030/api/tools/create_menu_item', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "groupSlug": "crm",
  "name": "Dormant Brands",
  "slug": "dormant"
}),
});

upsert_dynamic_page

Admin (menu/page)writes crm-pg

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.

Parameters

NameTypeReqDescription
groupSlugstringβœ“
itemSlugstringβœ“
titlestringβœ“
descriptionstring
sectionsarrayβœ“
createdBystring

curl

curl -X POST http://localhost:3030/api/tools/upsert_dynamic_page \
  -H "Authorization: Bearer crm_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"groupSlug":"crm","itemSlug":"dormant","title":"Dormant Brands","sections":[{"type":"text","body":"<h1>Dormant Brands</h1>"}]}'

fetch

await fetch('http://localhost:3030/api/tools/upsert_dynamic_page', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer crm_YOUR_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
  "groupSlug": "crm",
  "itemSlug": "dormant",
  "title": "Dormant Brands",
  "sections": [
    {
      "type": "text",
      "body": "<h1>Dormant Brands</h1>"
    }
  ]
}),
});

Safety constraints

  • No send tool β€” gateway never sends email. queue_for_review hands off to a human reviewer.
  • Read-only 08L β€” All zeliter queries use SET SESSION TRANSACTION READ ONLY.
  • B2C blocked β€” member.marketing_consent is 100% NULL. No 일반 νšŒμ› marketing.
  • Row cap β€” Every query clamped to MCP_MAX_ROW_LIMIT (default 1000).
  • Audit trail β€” Every call logged to AuditEntry. Viewable at /audit.
  • Suppression list β€” Bounced/unsubscribed addresses excluded at send time.