Tool reference

Contacts tools

All 23 contacts MCP tools with input shape, response shape, and example calls.

Twenty-three tools cover the contact-intelligence CRM — directory, memory, live context, proactive outreach, analytics, and AI-assisted drafting.

All endpoints follow the pattern POST https://mcp.vlozi.app/tools/contacts.<name> with Authorization: Bearer ls_xxx. All responses use this envelope:

{ "data": <payload>, "error": null }   // success
{ "data": null,      "error": "..." }  // failure (with HTTP 4xx/5xx)

NOTE

Every tool takes ids as flat fields (contact_id, id) in the query string or JSON body — never as URL path segments, since the MCP gateway can only fill query params or a JSON body.

Directory

contacts.list_contacts

List contacts with their live state (mood, energy, relationship stage, churn risk, message count, last-active). Use before get_contact_profile to find a contact id.

Scope: contacts:contacts.read

Input:

Field Type Notes
channel "whatsapp" | "telegram" | "widget" | "instagram" | "all" Default all
stage "new" | "building" | "established" | "deep" | "fading" | "dormant" | "all" Default all
sort "recent" | "churn_risk" | "messages" | "created" Default recent
q string Substring match over display name / external id
limit number 1–100, default 20
offset number Default 0

Response: { contacts: ContactRow[], total: number, limit: number, offset: number } — each row includes mood/energy/relationshipStage/totalMessages/churnRisk/lastActiveAt (null if the contact has no state row yet).

NOTE

Soft-deleted contacts are excluded automatically.

curl -X POST https://mcp.vlozi.app/tools/contacts.list_contacts \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"stage": "fading", "sort": "churn_risk", "limit": 10}'

contacts.get_contact_profile

Full profile for one contact: the contact row, its state, up to 50 top memories by importance, and resolved entities.

Scope: contacts:contacts.read

Input: { contact_id: string } (required)

Response: { contact, state: object | null, memories: Memory[], entities: Entity[] }

NOTE

Returns 404 if the contact doesn't exist or is soft-deleted.


contacts.get_contact_channels

List the channels a contact has been reached on, with the external id and match confidence per channel. Reflects multi-channel identity after merges.

Scope: contacts:contacts.read

Input: { contact_id: string } (required)

Response: { channels: { id, channelType, externalId, confidence, linkedAt }[] }, newest-linked first

NOTE

No existence check — an unknown contact_id returns channels: [], not a 404.


contacts.get_contact_timeline

Emotional + relationship trajectory over a lookback window: per-message mood/energy points, weekly emotional snapshots, and relationship snapshots (stage, score, trajectory, velocity).

Scope: contacts:contacts.read

Input: { contact_id: string, days?: number } (days 1–365, default 90)

Response: { days: number, mood_points: { mood, energy, role, at }[], emotional_snapshots: { periodType, periodStart, dominantMood, moodScore, variance, trendDirection, keyTriggers }[], relationship_snapshots: { periodStart, stage, relationshipScore, trajectory, velocity }[] }

NOTE

mood_points is capped at 500 rows even within the requested window.


contacts.create_contact

Create a contact for a channel identity, or return the existing one if that (channel, external_id) is already known.

Scope: contacts:contacts.create

Input:

Field Type Notes
external_id string (required) Channel-native id (phone, telegram id, widget id…), 1–200 chars
channel "whatsapp" | "telegram" | "widget" | "instagram" (required)
display_name string Optional, ≤200 chars

Response: { contact: { id, ... }, created: boolean }, HTTP 201 on a true create

NOTE

Idempotent — checked against the channel-identity table first (survives merges), then the legacy (external_id, channel) pair. On the already-exists branch (created: false), contact is only { id } — not the full row. On a true create, contact echoes the full input plus id.

curl -X POST https://mcp.vlozi.app/tools/contacts.create_contact \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"external_id": "+15551234567", "channel": "whatsapp", "display_name": "Ben"}'

contacts.merge_contacts

Merge a source contact into a target: re-parents memories, entities, channels, logs, and outreach onto the target (deduping collisions), folds engagement counters, then soft-deletes the source.

Scope: contacts:contacts.delete

Input: { target_contact_id: string, source_contact_id: string } (both required)

Response: { merged: true, target: string, channel_count: number }

CAUTION

Irreversible — no undo tool exists. Real conflict-resolution rules worth knowing: entities unique per (type, name) — a colliding source entity is dropped, not merged; a colliding source channel (channelType, externalId) is also dropped; memories, interaction logs, and outreach triggers always re-parent (no collision possible); the source's emotional/relationship snapshots are deleted outright (they regenerate on the next weekly consolidation); engagement counters (message/session counts, longest streak) are summed/maxed into the target. Returns 400 if target and source are the same id, 404 if either doesn't belong to the tenant.


contacts.delete_contact

GDPR deletion: soft-deletes and anonymises the contact, then hard-deletes its memories, entities, and interaction logs.

Scope: contacts:contacts.delete

Input: { contact_id: string } (required)

Response: { deleted: true }

CAUTION

Irreversible for the memories/entities/logs even though the contact row itself is only a tombstone — displayName is cleared and externalId rewritten to deleted_<id>. Confirm with the user before calling.


contacts.export_contact

GDPR data export: the contact row, its state, all memories and entities, and the interaction count — everything held about the contact.

Scope: contacts:contacts.read

Input: { contact_id: string } (required)

Response: { export_date: string, contact, state: object | null, memories: Memory[], entities: Entity[], interaction_count: number }

NOTE

Unlike get_contact_profile, memory/entity lists here are unbounded (no top-50 cap), and — deliberately, since this is a compliance export — it does not exclude a soft-deleted contact.

Memory

contacts.list_memories

List a contact's memories (facts, preferences, episodes, patterns), highest-importance first, with type, importance, access count, and linked entity ids.

Scope: contacts:memory.read

Input: { contact_id: string } (required)

Response: { memories: { id, memoryType, content, importance, accessCount, entityIds, createdAt, accessedAt }[] } (up to 100)

NOTE

Includes superseded memories (ones a correct_memory call has since replaced) — this list isn't filtered to "currently active" the way correct_memory's own lookup is.


contacts.delete_all_memories

Delete ALL memories for a contact — the contact and its state row are kept.

Scope: contacts:memory.delete

Input: { contact_id: string } (required)

Response: { deleted: true }

CAUTION

Returned unconditionally, even for an unknown contact or one with zero memories. Irreversible.


contacts.correct_memory

Correct a contact's memory. hint fuzzy-matches (case-insensitive substring) the highest-importance active memory; that memory is superseded and a new corrected memory is stored.

Scope: contacts:context.write

Input: { contact_id: string, hint: string, correction: string } (all required — hint 1–500 chars, correction 1–2000 chars)

Response: { corrected: true, memory_id: string } — id of the new corrected memory, not the one it replaced

NOTE

This is a supersede, not an in-place edit — both the old and new memory rows persist in the database; the old one is just marked superseded so it drops out of active-memory views. Returns MEMORY_NOT_FOUND if no active memory matches hint.

Context

contacts.get_context

Build the LLM context packet for a contact — the same hot-path context the chat engine itself uses.

Scope: contacts:context.read

Input:

Field Type Notes
contact_id string (required)
message string Current user message, ≤500 chars — enables semantic + entity-graph recall
session_count number Sessions seen so far, drives cold-start vs summary; default 0
bot_id string For a cold-start brief lookup

Response:

{
  "session_core": "string | null",
  "message_relevance": "string | null",
  "has_message_context": false,
  "relationship": { "stage": "...", "trajectory": null, "velocity": null, "mood_trend": null },
  "crisis_level": "...",
  "triggers": []
}

NOTE

If session_count is below the contact's cold-start threshold, session_core comes from a bot-specific cold-start brief instead of the usual rolling summary (falls back to a generic new-contact string if unavailable). message_relevance/has_message_context only populate when message is passed — a semantic + light entity-graph search over memories; a search failure is swallowed, never surfaced as an error. triggers lists any already-due pending outreach (up to 3) the caller should be aware of inline.

Outreach

contacts.list_outreach

List outreach triggers (scheduled proactive messages), newest scheduled first.

Scope: contacts:outreach.read

Input:

Field Type Notes
status "pending" | "fired" | "cancelled" | "failed" | "expired" | "all" Default all
contact_id string Restrict to one contact
limit number 1–100, default 20
offset number Default 0

Response: { triggers: OutreachTrigger[], total: number, limit: number, offset: number }


contacts.schedule_outreach

Schedule a proactive outreach message to a contact at a future time.

Scope: contacts:outreach.create

Input:

Field Type Notes
contact_id string (required)
trigger_type "scheduled" | "inactivity" | "milestone" | "recurring" (required) Why this outreach fires
scheduled_at string (required) Strict ISO-8601 UTC, e.g. 2026-07-10T09:00:00Z
message string Concrete body, ≤2000 chars — provide this OR message_template
message_template string Rendered at send time
context object Arbitrary context passed to the sender

Response: { trigger: { id, contact_id, trigger_type, scheduled_at, message, message_template, context, status: "pending" } }, HTTP 201

WARNING

Unlike blog.publish_post, a past scheduled_at is not rejected — it just makes the trigger immediately eligible for the next scan. Fails with 404 if the contact doesn't exist, or 403 if the contact has disabled outreach.

curl -X POST https://mcp.vlozi.app/tools/contacts.schedule_outreach \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{
    "contact_id": "ct_xxx",
    "trigger_type": "milestone",
    "scheduled_at": "2026-09-01T09:00:00Z",
    "message": "You have been with us a full month — how is it going?"
  }'

contacts.cancel_outreach

Cancel a pending outreach trigger by id.

Scope: contacts:outreach.create

Input: { id: string } (required — outreach trigger id)

Response: { cancelled: true }

NOTE

Only pending triggers can be cancelled — returns 404 ("Trigger not found or not in pending state") for anything already fired, cancelled, failed, expired, or unknown.


contacts.get_outreach_config

Get the workspace outreach config. Returns sensible defaults if none has been saved yet.

Scope: contacts:outreach.read

Input: none

Response: { config: { enabled, inactivityDays, maxInactivityAttempts, minGapDays, maxPerDay, quietHoursStart, quietHoursEnd, timezone } }

NOTE

Unsaved-workspace defaults: enabled: true, inactivityDays: 3, maxInactivityAttempts: 2, minGapDays: 3, maxPerDay: 3, quietHoursStart: 22, quietHoursEnd: 8, timezone: "Asia/Kolkata".


contacts.update_outreach_config

Update (upsert) the workspace outreach config. Pass only the fields to change.

Scope: contacts:outreach.create

Input:

Field Type Notes
enabled boolean Master on/off for proactive outreach
inactivity_days number 1–30
max_inactivity_attempts number 1–5
min_gap_days number 1–14
max_per_day number 1–10
quiet_hours_start number 0–23
quiet_hours_end number 0–23
timezone string IANA, e.g. Asia/Kolkata

Response: { config: object }

NOTE

The response echoes only the fields you passed (plus tenantId/updatedAt) — it's not a full re-read of the row. Fields you omitted are preserved in the database but won't appear in this response.


contacts.trigger_outreach_scan

Ops action: run the outreach scanner immediately (fires due triggers, creates inactivity/milestone triggers). Normally runs on a 6-hour cron — use this to force a scan.

Scope: contacts:outreach.create

Input: none

Response: { triggered: number, inactivityCreated: number, milestonesCreated: number, errors: string[] }

IMPORTANT

This runs synchronously in-request, not as a background job — expect it to take noticeably longer than a typical read call, since it processes up to 50 due triggers, generating an AI message for any that don't have one preset and delivering each via the chat engine. It also scans for inactive contacts (respecting the configured caps) and milestone contacts (7-day streak, 100 total messages) and creates new triggers for them. Each of the three stages fails independently into errors[] rather than failing the whole call.

Analytics

Engagement overview: total contacts, contacts active in the last 7 days, and the distribution of contacts across relationship stages.

Scope: contacts:analytics.read

Input: none

Response: { total_contacts: number, active_last_7_days: number, stage_distribution: Record<string, number> }

WARNING

total_contacts counts every contact row for the tenant with no soft-delete filter — it can run higher than what list_contacts returns, since merged-away/deleted contacts are still counted here.


contacts.analytics_churn

Churn-risk report: a high/medium-risk count summary plus the top at-risk contacts, ordered by risk.

Scope: contacts:analytics.read

Input: { limit?: number } (1–100, default 20)

Response: { summary: { high_risk: number, medium_risk: number }, at_risk_contacts: { contactId, churnRisk, relationshipStage, lastActiveAt, totalMessages, displayName, channel }[] }

NOTE

"At risk" means churnRisk >= 0.3. high_risk counts churnRisk >= 0.7; medium_risk counts 0.3–0.7.


contacts.analytics_moods

Workspace mood + energy distribution — a count of contacts per current mood and per current energy level.

Scope: contacts:analytics.read

Input: none

Response: { moods: Record<string, number>, energy: Record<string, number> }

AI

contacts.draft_outreach_message

AI-write a personalized proactive message for a contact, using their memories, mood, and relationship context. Returns the text — does not send it. Pair with schedule_outreach.

Scope: contacts:outreach.create

Input: { contact_id: string, goal?: string, tone?: string } (goal e.g. "win-back", "check-in"; tone e.g. "warm", "professional")

Response: { message: string, creditsCharged: number } — message capped at 300 chars

IMPORTANT

Charges a flat 5-credit fee up front, refunded automatically if the contact isn't found or generation fails.


contacts.summarize_contact

AI-summarize what you know about a contact — key memories, preferences, mood, and relationship state — into a short 2–4 sentence brief.

Scope: contacts:contacts.read

Input: { contact_id: string } (required)

Response: { summary: string, creditsCharged: number }

IMPORTANT

Charges a flat 5-credit fee, refunded automatically if generation fails.

MCP · Tool referenceEdit on GitHub