Developers › MCP
Developer Reference MCP — hosted server LeadLogiX runs a Model Context Protocol server at /mcp. An AI application authorizes through OAuth, receives a token scoped to one workspace, and reads scored leads, customer profiles and routing through twenty-four tools — plus, where the workspace publishes them, its own automations as callable tools. Nothing to install, and no credential the customer has to copy anywhere.
There are two MCP servers. This is the one you probably want. Hosted server (this page) leadval-mcp (npm) Who runs it LeadLogiX. Nothing to install. You do — it runs beside your agent, over stdio. How it authenticates OAuth 2.1 — the customer approves scopes once, and can revoke them. Your own ingestion API key ( val_key__… ). Whose data Any workspace that authorizes your application Your own workspace What it does READS scored leads, customer profiles, routing and hours; with the matching scopes it also keeps notes and files, registers your tools for review, and runs the workspace's published automations. ASKS and TELLS — notify a session, poll it, and look up whether a caller is known. Tools The catalogue below, filtered to the scopes you were granted leadval_notify_session , leadval_get_session_status , leadval_process_and_wait , leadval_identity_lookup — leadval-mcp Reach for it when You are building an app other companies connect to LeadLogiX, or you want a customer's own data in a model. You are wiring your own agent to your own workspace — Claude Desktop, Cursor, a custom runtime. Most teams use both: leadval-mcp on the conversational side tells LeadLogiX a conversation finished and asks whether the caller is known; this server pulls the scored lead or the merged history back into a model. Recognise a returning customer uses one of each. Neither is the OpenAPI spec or llms.txt , which are for a coding assistant writing your integration — AI & tooling . A token for this endpoint comes from the OAuth journey: OAuth 2.1 / OIDC , Register a client , Scopes & limits .
Connecting Item Value Endpoint POST /mcp Transport Streamable HTTP, stateless — every request stands alone; no Mcp-Session-Id is issued or needed, so requests may run concurrently and in any order. Authorization An OAuth access token in Authorization: Bearer … . Header only. Resource Request the token with resource=https://<host>/mcp and without openid — two planes . Discovery /.well-known/oauth-protected-resource — an unauthenticated 401 also returns it in WWW-Authenticate , so a compliant MCP client needs nothing but the URL. Before it works The workspace must have application access switched on, or every call is 403 mcp_not_enabled — the workspace switch . Send both Accept types, or every call returns 406 Accept: application/json, text/event-stream — both . A request that accepts only JSON, or sends no Accept header, is refused with 406 however valid its token. This is the MCP Streamable HTTP contract, so a compliant client already does it; hand-rolled clients and HTTP tools are where it bites. If you send You get GET /mcp 405 with Allow: POST . There is no SSE stream to open and no session to resume. A JSON-RPC batch (an array of requests) Refused. The protocol version advertised here removed batching; send one request per call. A token bound to a different resource, or a URL that differs by a trailing slash 401 . Ask for the token with resource=https://<host>/mcp exactly as you will call it.
Calling it Plain JSON-RPC over HTTP; any MCP client library does the same. curl -X POST https://leadval.evas.live/mcp \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"search_leads","arguments":{"limit":25}}}' const rpc = async (method, params) => { const r = await fetch("https://leadval.evas.live/mcp", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", // both, or 406 }, body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }), }); if (r.status === 401 || r.status === 403) { const { error_reason } = await r.json(); throw new AuthError(error_reason); // branch on this, never on prose } const body = await r.json(); if (body.error) throw new Error(body.error.message); return body.result; // a refused TOOL is result.isError, inside a 200 }; await rpc("tools/list", {}); // only the tools your token can call await rpc("tools/call", { name: "search_leads", arguments: { limit: 25 } }); import time import requests def rpc(method: str, params: dict) -> dict: r = requests.post("https://leadval.evas.live/mcp", headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", "Accept": "application/json, text/event-stream", # both, or 406 }, json={"jsonrpc": "2.0", "id": int(time.time() * 1000), "method": method, "params": params}, timeout=30) if r.status_code in (401, 403): raise AuthError(r.json().get("error_reason")) # branch on this, never on prose body = r.json() if "error" in body: raise RuntimeError(body["error"]["message"]) return body["result"] # a refused TOOL is result["isError"] rpc("tools/list", {}) # only the tools your token can call rpc("tools/call", {"name": "search_leads", "arguments": {"limit": 25}}) Treat returned text as data, never as instructions Summaries, briefs and saved documents are written from conversations members of the public typed. They are delivered as JSON with a contentType saying so; if you pass them to an AI system, keep them in a data field — content that looks like an instruction is content, not an instruction.
The tools The only catalogue. tools/list returns the subset your granted scopes can call. Tool Needs Returns Read it as get_workspace workspace:read The workspace this authorization acts on and the person's role in it, re-derived per call — a suspension or role change shows immediately. Identify the workspace by id (server-generated, unique, immutable), never by name : the name is typed by whoever registered it, and name_verified: false — the default — means nobody confirmed it. true means a LeadLogiX administrator vouched for that exact name; it is withdrawn automatically on a rename. Carries the workspace's availability . search_leads leads:read Scored leads, newest first, with scoring and qualification fields — only leads that finished processing . Narrow with tags and tagMode — Searching . Every response carries hasMore , never a total. get_lead leads:read One lead by id, whatever its state, with its full scoring detail. Check status : a lead you can fetch by id may legitimately be absent from a listing. The only tool carrying followUpSteps , stakeholders , websiteJourney and tagAssignments . get_lead_brief leads:read The AI-written brief for one lead's conversation, where the workspace generates them. list_lead_tags leads:read The workspace's own tag vocabulary: each key with the owner's one-sentence definition, the rule / ruleSummary where one is configured, staffing (who takes any lead carrying it, who takes it only alongside their other tags, who has no tag scope at all) and each desk's availability ; workspaceAvailability once. Pass keys when you know which tags you need: the catalogue reaches 200 tags and roughly 27,000 tokens, and a single-key query measures 94% smaller. Keys this workspace does not define come back in keysUnknown rather than vanishing. get_lead_crm_status leads:read Whether one lead reached the connected CRM, and which one. Reports what happened, never why. list_crm_agents leads:read The workspace's agents, the tag keys each handles, and whether each has a live CRM seat that can be given work; workspaceAvailability , and availability per person. Pass the lead's tags as leadTags for qualifies , matchedTags and the desk plane on tagContacts[] — Routing a lead to a person . Contact details and CRM ids need workspace:read.contact , and each channel is separately cleared for AI use ( emailAvailability / phoneAvailability ). get_customer_profile profiles:read One merged customer by profile id ( cprof_… , from a lead's customerProfileId ) — their history across every conversation, each carrying intent , journeyStage , linkType and nameUse . Only where the workspace enabled the Customer Profiles API. There is no profile search. Read verticalRollups[] — Reading a profile . identity_lookup identity:lookup Whether this workspace has spoken to this person before, asked mid-conversation : send the identifying signals you have, receive pointers — a lead id, and a profile id where conversations were merged — never customer data. The one tool that takes personal data in . Read nameUse ( PERMITTED / WITHHOLD , with nameUseReason ) on the row you act on — Reading the response . Only where the Identity Resolution API is enabled. submit_tool tools:write · writes Registers one of your tools with this workspace, so LeadLogiX reads its calls as structured evidence. Returns a submission id and status: "PENDING" . A proposal : nothing changes until a LeadLogiX administrator approves it, and you cannot approve your own. Idempotent on the contract — Submitting your own tools . verify_tool tools:write Whether the tool you are shipping still matches what was approved, and which field names and types differ if not — never a value. Read compared_against : on a tool still awaiting review it compares against what you submitted, so matches: true alone does not mean live. list_tools tools:write Your registrations for this workspace and where each one stands, with the reviewer's note on anything declined. memory_get memory:read One note this application saved earlier, under a key it chose. A key never written is { found: false } — an answer, not an error; do not retry it. Notes are private to this application and the person who authorized it. memory_list memory:read What this application has saved in one group — keys and sizes by default, contents on request. complete says whether that is everything; a full page is not the end of the list. memory_usage memory:read How much this application is holding for this person , by group, and the limits it is measured against. Call it when a save is refused for space. Never a workspace total. knowledge_search memory:read The nearest passages to a question in words — from LeadLogiX's product documentation, or from a collection this application wrote — each with a distance and a citable link. It always returns its nearest match, so the usual failure is an answer merely close : read distance before relying on one. A fault is a coded refusal, never an empty list. memory_put memory:write · writes Save one JSON value under a key. The same key again replaces it, so a retry is safe. Everything expires — ttlSeconds chooses when. Only where Application memory is enabled. memory_delete memory:write · writes Remove one note. Not there is { removed: false } , an answer rather than an error. knowledge_write memory:write · writes Save a document into a collection of this application's own, to be found later by meaning rather than by key. Long documents are split, not shortened; the same id again replaces every part at once. ⚠ Does not expire — a quota bounds it. LeadLogiX's documentation is read-only. knowledge_delete memory:write · writes Remove one saved document, every part of it. Not there is { removed: false } . file_list files:read What files this application saved for you in one collection — names, sizes and types. complete says whether that is everything. file_get files:read One file back, whole. Text comes back as text and anything else as base64, decided by the stored type rather than how it was sent. Never written is found: false . file_put files:write · writes Save one file. The same name again replaces it. ⚠ Files do not expire: delete what is no longer needed, because waiting does not free space. file_delete files:write · writes Remove one file — every stored version of it. Already gone is removed: false . A tool you cannot call is not advertised — and the schema is the contract tools/list is filtered by granted scope: a token holding only workspace:read sees exactly one tool, because an advertised tool that always refuses teaches a model to keep retrying it. Each tool arrives with its own JSON schema and description carrying arguments and response fields no page repeats — build against what tools/list returns from your own connection, and enumerate at runtime rather than hard-coding this table. Whether lead results include names, phones and emails depends on leads:read.contact ; each tool's description says which you are getting.
Searching and filtering leads Tags are per workspace, and an empty page is not an answer. Argument or field Read it as tags Tag keys , never display names — a name is a label an admin can rename; the key cannot change. Each workspace invents its own: reroof_now means what that workspace decided, and may not exist in the next one. Read list_lead_tags once per workspace first. A key the workspace has not defined matches nothing rather than erroring, so a stale key looks like “no leads”. tagMode ANY (default) or ALL . ALL is applied after the window is read, so it can return an empty page while matching leads sit just past the edge — the response then says so with hasMore: true and a note ; widen limit or send fewer tags. hasMore true : the list is a page, not a census — do not total it, average it, or answer “none match” from it. Only hasMore: false is complete for the filter you sent. A filter that cannot be applied Refused, not ignored: tags that is not an array of non-empty strings (a bare "urgent" is the usual slip), a tagMode other than ANY / ALL , or more than 30 keys is an error — a silently dropped filter would look exactly like one that matched the whole workspace. A lead missing from the listing search_leads leaves out leads that failed to process (no score, no verdict, no summary); get_lead returns any lead by id with its status .
Routing a lead to a person Who covers it, whether they can be given it, and whether anyone is there. Offering a visitor a real person is a join of two tools and one field. Find the area's tag with list_lead_tags ; ask list_crm_agents the narrow question with { leadTags: ["<key>"] } and keep those who qualify and are assignable; then check availability before you put anyone through. The three fail differently: a person whose CRM seat is disabled cannot be given the lead, and one with a live seat may still be asleep — the first two checks say nothing about the hour. Field Read it as leadTags (argument) Prefer it over tag : it answers qualifies directly instead of leaving you to apply tagMatchMode , and it is the argument that returns tagContacts[] — the desk plane, with that desk's hours. With tag you get neither. qualifies Whether this person covers this lead, by the same rule that governs which leads they can see. It answers WHO, never whether anyone is there. matchedTags Which of the lead's tags this person covers. tagMatchMode ANY — the usual setting: a rep tagged miami and tampa covers both. ALL : only leads carrying every one of their tags, so the same rep covers neither city alone. Matching on the array without the mode silently inverts the rule. unrestricted: true The person has no tags and handles everything — never evidence of territory coverage. assignableInAnyCrm Governs assigning a lead to own , not whether you may reach someone: a workspace with no CRM connected reports false for everyone and may be perfectly reachable by phone. crmChecked true : compared against the CRM, and no live seat is a real finding. false : not looked up , which is not “no seat”; uncheckedPeople counts them. connectedCrms , complete An empty connectedCrms means no CRM user list is held — not connected, or not pulled yet. complete: false means the response was truncated, so absence proves nothing. In all three cases say the check is incomplete rather than that nobody can help. availability On tagContacts[] for the desk, falling back to workspaceAvailability . CLOSED : do not transfer — offer a callback and, if effective.opensAt is present, say when they reopen. A no-answer state: claim nothing either way. Hours never withhold a contact. All eight states: Availability & hours . autoAssignByLlm: true with no rule A tag applied by AI reading the conversation — a judgement, not a tested fact. Say so when a promise rests on it: “it sounded like the Tampa area” is honest, “you are in our Tampa territory” is not.
What a lead actually carries Every key on the wire, with what it does NOT mean. A tool description carries only what changes behaviour; this is the reference. Field What it is Read it as id The lead id Rows carry id , never leadId . conversationId The conversation it came from Correlates with a session your agent may still be inside. score , qualityLabel The calibrated Lead Quality Index, and its grade suppressed Failed a hard integrity gate true is not a live opportunity : LeadLogiX neither sells nor bills it. Do not route it, offer a callback on it, or count it as pipeline. role Their part in the transaction The vocabulary is per line of business — buyer/seller/renter is real estate; life insurance uses a different set. Never enum it across lines. journeyStage Where they are in the journey Lower-case on the wire ( awareness / consideration / decision ); leads written before the vocabulary settled carry UPPER-CASE values that were not migrated — fold case before comparing. intentSentiment How they feel about transacting budgetAmount Stated budget Means whatever the line of business means by money — a purchase price on real estate, a monthly premium on life insurance. Read it with the processor mode; never sum or compare across lines. Paired with budgetCurrency . city , stateCode Resolved location stateCode: "XX" is “not resolved”, not a state. statisticalGrounding The cohort grounding Always null on search_leads , whatever the lead's real grounding — never report a listing row as ungrounded; call get_lead when it matters. tags Tag keys Join them to list_lead_tags for what each means in this workspace. customerProfileId The merged customer, when there is one Present on search_leads and get_lead when the authorization also holds profiles:read . Absence is normal — a first conversation has no profile — and it is also withheld for a profile that was superseded or lost every member, because get_customer_profile would refuse it. financing.* Financing position status (e.g. PRE_APPROVED ), mortgagePreApproved , payingCash — each boolean or null. contactability.* How reachable, and on what basis phoneStatus , emailStatus , carrierIdentityMatch , recordingDisclosed , and the consent fields. contactability.consentGranted Recorded consent null means unknown, not denied — decide from your own policy rather than reading it as a refusal. businessSummary The written summary of the conversation Free text: names removed without leads:read.contact , regulated content removed always — see below. targetProperties[] What they are looking at status , city , stateCode ; address needs leads:read.contact . Real-estate shaped; another line may carry none. contact.* First name, last name, email, phone Only with leads:read.contact . A scope-gated key you were not granted is absent , never an empty array — empty would claim “nobody configured one”. followUpSteps[] get_lead only The next actions, written from the conversation. Absent from a listing row, not empty. stakeholders[] get_lead only Everyone else who took part, with role and relationship ; only with leads:read.contact . websiteJourney[] get_lead only What they searched for and did before speaking to anyone, rebuilt from your agent's tool calls: eventType , toolName , turnIndex , direction , summary . tagAssignments[] get_lead only key , assignedAt , a rationale quoting the conversation, and source : DETERMINISTIC (a workspace rule was evaluated and matched — a tested fact), LLM (the AI judged it; confidence 0–1, and anything below the workspace's floor was never applied), MANUAL / AUTOMATION . Where a rule and its description differ, the rule is what happened. Redacted text reads [withheld] — strip it before a person sees it Free-text fields pass two independent controls. Personal names are removed unless your token carries the matching .contact scope; regulated content declared by the line of business — health information on life insurance, for example — is removed unconditionally , whatever scopes you hold. Where something was removed the literal token [withheld] is left in its place: a marker, not content. Strip it, or drop the sentence containing it. A whole field may instead come back null with a note, or as "omitted" — both mean “not available to you”, never “the lead has none”.
How much you may store Published numbers, so you can size an integration before you build it rather than meet a ceiling in production. Every allowance below belongs to one application, for one person, in one workspace . That is the unit a customer can revoke, so it is the unit everything is counted against — and it means another application in the same workspace, or the same application acting for a colleague, has its own separate allowance that yours cannot spend. What you store How much How long Notes — memory_put , a JSON value under a key you choose 64,000 characters each · 10,000 notes and 40,000,000 characters in total 30 days by default, up to a year if you ask for it Documents you search by meaning — knowledge_write 60,000 characters per document · 5,000 sections and 10,000,000 characters in total, across at most 25 collections ⚠ nothing expires — knowledge_delete is the only thing that frees space Files — file_put 128 KiB each · 1,000 files and 125 MiB in total ⚠ nothing expires — file_delete is the only thing that frees space Reaching one of these is a refusal you can read and act on, never a quietly shortened answer. The two that never expire are the ones worth watching: waiting does not free space, so an application that writes and never deletes will eventually stop being able to write. Ask rather than assume — a workspace can be given more These are the figures a workspace starts with, and LeadLogiX can raise them for a particular customer. memory_usage returns the limits your connection is actually measured against, alongside what you are holding, broken down by collection for all three. Read them from there rather than hard-coding what you see here, and a workspace that was tuned will never surprise you.
How fast you may call Every tool family has its own budget, counted per application, per person, per workspace — so staying under the read limit does not keep you under the others. Going over is 429 with Retry-After ; honour it. The numbers are on OAuth & MCP errors → Rate limits .
Running the workspace's own automations The only part of the catalogue that differs between workspaces. Every tool above is the same for everyone. Automation tools are not: they are built from the workspace's own records at the moment you call tools/list , so two applications holding identical grants on different workspaces legitimately see different tools. Discover them, never hard-code them. Scope automations:run — and the workspace must have automation access enabled by LeadLogiX. Tool name automation__<id> . The namespace is reserved, so no built-in tool can occupy it. Description Written for a model, by the people who built the automation — it says what the automation does and when to call it. This is what your model should select on. Arguments Published as the tool's own inputSchema . Validated strictly : a number outside its bounds is rejected rather than clamped, an unknown key is rejected rather than ignored, and a required text field will not accept blanks. Result { ok: true, executionId, result? } . The result carries only the fields that automation declares — never its logs, and never anything it read along the way. An automation that declares no result returns none. Budget 60 automation calls per minute, separate from the 600 reads. Exhausting it is an HTTP 429 with Retry-After, not a tool error. Nothing is published by default, and a setting you configured always wins A workspace admin approving automations:run publishes nothing on its own — each automation is published to applications individually. And where an argument and a workspace setting name the same thing, the setting wins: an argument can never redirect an automation the workspace configured, which is why an address is not something you can pass. Every refusal carries a code and a retryable flag in structuredContent . Branch on the code; the sentence beside it is for a person reading a log. Code Retry? What happened INVALID_ARGUMENTS No Your call is wrong. The message names the field and the rule it broke. AUTOMATION_NOT_CALLABLE No Not published to applications. Nothing you send will change that — the workspace has to ask us to publish it. AUTOMATION_NOT_SHARED No Published, but switched off for this workspace. AUTOMATION_UNAVAILABLE Yes A gate could not be READ. A transient fault refusing safely — not a decision about you. WORKSPACE_SUSPENDED No The workspace is suspended or being deleted. CONCURRENCY_LIMIT Yes The workspace is already running its share. ⚠ The run never started — wait retryAfterSeconds and send it again. TIMEOUT Yes The run exceeded the platform ceiling. It may not next time. AUTOMATION_FAILED No It ran and threw. The same input will throw again. RESULT_TOO_LARGE No ⚠ The automation ran . Only its answer could not be returned, so retrying does the work twice. A budget refusal is not in that table Exhausting the 60-per-minute automation allowance is answered by the transport before any tool runs — HTTP 429 with Retry-After , the same answer as every other budget here. There is no tool-level duplicate of it. Some automations can act — post to a channel, call another system, send a message. That is switched off per automation and off by default: while it is off the run can read and answer but cannot reach anything outside itself. An automation that is allowed to act may also promise to do something later , and that is worth understanding before you call one. What an automation may do while it answers you What you see Search the product documentation by meaning, so the answer is grounded in the manual rather than recalled Whatever it declares as its result — typically the answer plus the section it came from. Keep a short transcript under an id it chooses, usually one you pass in Nothing directly. Later calls sharing that id can build on the earlier ones instead of starting cold. Queue work to be carried out later — a follow-up minutes or days from now The call returns as soon as the run finishes. The follow-up is not in the response and nothing calls you back; it appears on the workspace's own Automations history when it happens. You cannot cancel it. What the workspace ships for this surface today Seven, in two groups that behave very differently. Always discover the real list from tools/list — it is built per workspace and this is only what the platform publishes to everyone. Answers — they read nothing, store nothing and send nothing What it tells you automation__script_global_check_email Is this address well formed, and is its domain one that can never receive mail. ⚠ Nothing contacts a mail server, so a well-formed address is not a mailbox that exists. automation__script_global_check_phone Is this number dialable, its +country form, and what kind of line. ⚠ FIXED_LINE_OR_MOBILE means that country's plan does not separate them — not that a text will arrive. automation__script_global_check_name Do two names refer to one person — the same graded answer LeadLogiX's own identity resolution gives. ⚠ NEAR is evidence, never a decision. Acts — these send messages or spend money What it does …notify_slack , …notify_teams , …notify_webhook Sends the workspace's new high-scoring leads to the channel or endpoint it configured, now, instead of waiting for the next scheduled run. They take no arguments at all : which leads qualify and where they go are the workspace's settings, so you cannot aim one. Each lead is sent at most once ever, so a second call normally reports sent: 0 — that means nothing was new, not that it failed. …promote_for_valuation Sends one record that was captured without a valuation through the full engine, so it comes back with a score, a price and tags. You name the record with leadId ; you do not choose the line of business, because that is the billing decision and the workspace owns it. The second group costs the customer something A promotion is a full engine run and is billed on a pay-per-lead workspace; a notification is a message real people read. Both are ordinary, both are what the customer switched them on for — but do not call one to find out what it does, and do not retry one on a timeout without checking first. A record can be promoted once, and a refusal saying so is an answer, not a failure.
What an automation may keep, and for how long Not a database you can reach — but what an automation keeps is still the workspace's data. None of this is a tool you can call. There is no way to read or write a workspace's stored values over this API, and no tool exposes one — the only things you can call are the automations themselves. It matters for a different reason: an automation you invoke may keep something, and the customer whose workspace it is has a right to know what, and for how long. What it may keep How long Ceiling A stored value — a cursor, a cache, something remembered between runs 7 days by default, 30 at most 64,000 characters per value; 1,000 values and 4,000,000 characters per workspace A conversation's turns — kept under an id, usually one you passed in 24 hours by default, 7 days at most 16,000 characters per turn; 1,000 turns per conversation; 500 conversations per workspace A promise to do something later — a follow-up minutes or days out runs up to 90 days out; gives up after 30 days by default, 180 at most 8,000 characters of detail; 500 open promises per workspace Three things worth knowing before you pass anything in An id you choose is an id you share. If you pass a conversation id, what that automation keeps is filed under it, and a later call using the same id can build on it. Choose one that belongs to the conversation, not to a person. It expires on its own, and you cannot cancel it. Nothing here has a delete you can call. A stored value falls away on its own clock, and a promised follow-up will happen — it is not in your response, nothing calls you back, and it appears on the workspace's own Automations history when it does. It is the workspace's, not yours. Everything an automation keeps belongs to that workspace, is reachable only by that workspace's own automations, and goes with it when the workspace is deleted. These are the ceilings a workspace starts with, and a LeadLogiX administrator can change them per workspace. An automation that reaches one is refused rather than truncated, so what you see is an error and never a quietly shortened answer.