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
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
| Name | Type | Req | Description |
|---|---|---|---|
| table | string | 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. |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| table | string | Table name to inspect. Omit to list every base table in the current schema with row counts. | |
| includeIndexes | boolean | If 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
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
| Name | Type | Req | Description |
|---|---|---|---|
| sql | string | β | 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 | integer | 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). |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| parentId | integer | Return only children of this category id. Omit for top-level (parent_id=0). | |
| language | enum(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
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
| Name | Type | Req | Description |
|---|---|---|---|
| categoryId | integer | ||
| countryCode | string | ||
| lastCampaignBefore | string | ISO date β brands whose most recent campaign.start_at is before this | |
| lastCampaignAfter | string | ISO date β brands whose most recent campaign.start_at is after this | |
| hasVerifiedContact | boolean | Require at least one partner_admin with verified email | |
| keyword | string | Substring match against brand.president or partner.name | |
| limit | integer | ||
| offset | integer |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| brandId | integer | β | |
| recentCampaignsLimit | integer | How 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
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
| Name | Type | Req | Description |
|---|---|---|---|
| partnerId | integer | Filter by partner_id | |
| statusCodes | array | Campaign status_code values. Common: 1206=accepting, 1208=review, 1209=closed, 1210=cancelled, 1211=rejected | |
| startedAfter | string | ISO date β campaign.start_at after this | |
| startedBefore | string | ISO date β campaign.start_at before this | |
| keyword | string | Substring match against campaign.name | |
| minApplyCount | integer | Minimum apply_count β useful for finding high-traction campaigns | |
| limit | integer | ||
| offset | integer |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| partnerId | integer | β | |
| includeInactive | boolean | Include 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
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
| Name | Type | Req | Description |
|---|---|---|---|
| campaignId | integer | β |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| limit | integer | ||
| kind | enum(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
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
| Name | Type | Req | Description |
|---|---|---|---|
| limit | integer | ||
| status | enum(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"
}),
});Author (write CRM) (2)
create_segment
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
| Name | Type | Req | Description |
|---|---|---|---|
| name | string | β | Human-readable segment name |
| kind | enum(partner|brand|campaign) | β | |
| description | string | ||
| createdBy | string | Actor identifier (manager or agent name) | |
| targets | array | β |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| segmentId | string | β | Segment id returned from create_segment (string for BigInt safety) |
| name | string | β | Campaign name β e.g. "2026Q2 dormant beauty reactivation" |
| subjectTemplate | string | β | Default subject; overridden per target if target.subject is provided |
| bodyTemplate | string | β | Default body; overridden per target if target.body is provided |
| senderName | string | ||
| senderEmail | string | ||
| createdBy | string | ||
| targets | array | β |
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
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
| Name | Type | Req | Description |
|---|---|---|---|
| draftId | string | β | Draft id returned from save_message_draft |
| actor | string | Who queued it | |
| note | string |
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)
upsert_dynamic_page
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
| Name | Type | Req | Description |
|---|---|---|---|
| groupSlug | string | β | |
| itemSlug | string | β | |
| title | string | β | |
| description | string | ||
| sections | array | β | |
| createdBy | string |
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_reviewhands off to a human reviewer. - Read-only 08L β All zeliter queries use
SET SESSION TRANSACTION READ ONLY. - B2C blocked β
member.marketing_consentis 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.