MCP tools
Felspar's remote MCP server - connection, the four agent tools and the seven authoring tools, with inputs, results and errors.
Connection
| Address | https://mcp.felspar.ai/mcp |
| Transport | Streamable HTTP, stateless JSON-RPC: one JSON response per message |
| Protocol versions | 2025-11-25, 2025-06-18, 2025-03-26 |
| Agents authenticate with | Authorization: Bearer <agent key> |
| People authenticate with | OAuth sign-in with their Felspar login (the client opens it on first use) |
The credential is checked on every message, including initialize. A missing, unknown or revoked key answers 401, so a client fails to connect rather than failing each tool.
Agents and people see different tools, never each other’s. An agent key gets the four agent tools. A person’s sign-in gets the seven authoring tools. Each tool calls the same REST route as an HTTP client would, with the caller’s own credential, so access, release choice and receipts are identical.
Errors
A refused or failed call is a tool result with isError: true, never a decision. Refusals carry the same error, path, issues[] and fix as the REST API, so the model can read and correct them. Arguments that don’t match a tool’s schema return invalid_arguments with each issue’s path and problem.
Tool hints
Every tool declares destructiveHint: false and openWorldHint: false: nothing deletes, overwrites or reaches outside your workspace. Tools that record something new (add_current_version, propose_pack, get_job_decision, check_output) declare readOnlyHint: false; the rest are read-only.
Agent tools
list_jobs
List the jobs this agent key may run, so an agent that wasn’t told which job to run can pick one. No inputs. Each job comes from its released pack:
| Field | Meaning |
|---|---|
id |
The job ID |
purpose |
Why the job exists and what it resolves, quoted |
useWhen, notFor |
When the policy says to use the job, and when not, quoted |
subject |
What each case is about: type, definition (quoted), identifier and example; or null |
A job is listed only when get_job_requirements would serve it to this key: it is registered for the job, followed on Latest or pinned, with a live release. An agent picks the job whose purpose and use-when fit the task and whose not-for doesn’t; when none fits, it says so instead of forcing one. Over REST, GET /v0/jobs returns the job IDs.
get_job_requirements
Read what a company job needs before deciding: its purpose and subject, its questions, what each means (quoted from the policy), what evidence counts, the possible answers and outcomes, and request limits. Call it once per job; it changes only when a new version is released.
| Input | Type | Meaning |
|---|---|---|
job |
string, required | The job ID, such as triage-ticket |
detail |
"compact" or "full" |
compact (default) is enough to gather evidence; full adds the decision rules |
Returns the job’s brief from the release this agent uses:
purpose,useWhen,notForandowner: what the job is for, quoted, first;subject: what each case is about (type, quoteddefinition,identifier,example,match), its related records (related[], each withrelationship,cardinality,requiredand quoted definitions), andsend: in plain words, exactly which ID to send and how to send related ones;identity: how each case gets its ID (mode:provided,fields,contentorissued),sendin plain words, and thefieldsorquestionit uses;questions[]:key,type(enum,boolean,number,date),meaning(quoted, with its locator),answer(what to send and how it’s read:send,valueFormat,interpretation,threshold,applies,dependsOn,appliesWhen),answers[](each withmeaning,inclusions,exclusions,examples,edgeCases) andwhereToLook[]; a question about a related record carriesabout(relationship,subject);outcomes[]:id,meaning,nextStep(quoted), theoutputsthat outcome asks the agent to write, andescalatewhen it hands off;escalations: the hand-offs the policy names (routes[], eachidwith its quote), which one the unresolved path and the attempt limit use, and the rule in words;whenUnresolved: what to do when nothing is decided (quoted);attempts:max,withProgressand the rule in words;guidance.gather: how to gather evidence so one request is enough;input: for this tool,{ tool: "get_job_decision", jsonSchema, note }, the exact arguments to send;limits: the facts limits;release: the release ID and digests;rules(withdetail: "full"), each with its quoted text.
get_job_decision
Get the company’s decision for one case. Send, for each question, the exact words your sources say and where they came from. Use only the outcome Felspar returns.
| Input | Type | Meaning |
|---|---|---|
job |
string, required | The job ID |
entity_id |
string | The case’s ID, as the brief’s identity.send and subject.send say. Required when the job uses the record’s own ID (provided): a ticket number, a contract ID, a web domain; for an older job with no subject, the record’s own stable ID. Left out for fields and content jobs, and on the first decision of an issued job; after that, the case.id Felspar returned. Reuse it for every attempt on the case. |
identity |
object | Only for a job that identifies cases by fields: each field’s value by name, as the brief’s identity.fields lists them, such as { "email": "sam@example.com", "requisition": "REQ-114" } |
related |
object | Only when a question is about a related record: that record’s ID under its relationship name, such as { "counterparty": "northwind.com" }; a list of IDs for a many relationship |
facts |
object, required | One entry per question key: an excerpt { text, source: { class, id, locator, observedAt }, about? }, or a list of up to 3 excerpts from different places. For a many relationship, about names the related ID the excerpt comes from. |
Felspar works out the case ID by the job’s identity mode, normalizes it and each related ID by its subject’s match rule, picks the release (Latest or the agent’s pin) and assigns each call its own execution ID. The result is the same as the REST decision result: status, reason, and result with outcome, explanation, retry, attempts, nextStep, outputs, interpretation, case, subject, escalation, missing, receipt and executionObjectId.
-
case:{ id, mode }for a job released with identity modes, wheremodeisprovided,fields,contentorissued. Sendcase.idasentity_idfor every later decision on the same case. -
subject:{ type, id, related[] }, with the normalized IDs and, for each related record, the questions it supplied evidence for. -
escalation:{ id, trigger, why }when the release names a hand-off for this result.triggerisoutcome,unresolvedorattempt_limit. Report theidexactly and hand the case off; don’t ask again. See Subjects and escalations. -
missing: with statusgap, the required related records the request lacked, such as["missing: counterparty"].
A wrong or missing case ID is refused with one of the identity refusals, such as entity_id_required or unknown_case_id.
check_output
Check text the agent wrote for one of its own decisions (a ticket reply, an email) before it is sent or saved.
| Input | Type | Meaning |
|---|---|---|
job |
string, required | The same job ID sent to get_job_decision |
execution_id |
string, required | The executionId of the decision this text is for |
text |
string, required | The exact text, in full |
output |
string | Which output this is, when the outcome asks for more than one |
Returns pass, revise (each finding with the policy sentence it quotes and what to fix), review_required or unavailable, and on pass a receipt bound to exactly this text. Any edit needs a new check. A pass means the text follows the released rules; it is not approval to send. Refusals include output_not_required (the outcome asks for no such output; it lists the ones it does), decision_not_decided and no_output_checks. Each check counts as one decision toward rate limits. Over REST: POST /v0/jobs/{job}/output-checks.
Receipt verification is deliberately not an MCP tool. The code that acts calls POST /v0/executions/{id}/verify with the same key.
Authoring tools
Available to a signed-in member, approver or owner. None of them can approve or release.
get_pack_guide
How to write a pack draft: the format, the rules Felspar enforces and a small example. No inputs. Read it before the first check_pack. Same content as Pack draft format.
list_sources
The workspace’s connected policy documents. No inputs. Each entry:
| Field | Meaning |
|---|---|
sourceObjectId |
The source’s ID, used by the other tools |
provider |
google-drive or notion |
revision |
The document version that was added |
access |
Whether you can read it now: allowed, or why not |
title |
Only when you can read it |
watch |
Only when you can read it: state (current, changed, stopped, pending), checkedAt, changedAt, reason |
boundAt |
When it was added |
read_source
The exact text of one added source: the version that was added, which may be older than the document now.
| Input | Meaning |
|---|---|
source_object_id |
From list_sources |
Quote meanings from this text word for word; Felspar refuses any quote it can’t find.
add_current_version
When a document is changed, capture its current text as a new source, read with your own connection. Returns the new sourceObjectId; read, draft and propose from that one. Live decisions don’t change until a new proposal is released.
| Input | Meaning |
|---|---|
source_object_id |
The source whose document to re-read |
This is the only way an MCP sign-in can add a source. Text the assistant writes is never accepted as a source (source_text_not_allowed).
check_pack
Check a pack draft against its source without saving anything.
| Input | Meaning |
|---|---|
source_object_id |
The source the draft quotes |
draft |
A felspar.pack-draft object: v2, or v1 and v0 |
Returns status:
invalid, withproblems[](each apathandmessage) andwarnings;tests_failed, withtestsandwarnings;needs_definitions, withblocking(count),testsandwarnings;ready, withtests,warningsandartifactDigest.
When the draft revises a released job and removes an escalation ID, subject type or relationship name, or changes the job’s subject, the result also carries breaking: { changes[], agents }. Report each change to the person: code that acts on results may depend on it.
propose_pack
Submit a checked draft as a proposal for review. Same inputs as check_pack. Returns status: "proposed" with the proposal, tests, warnings, breaking when it applies, a review link to the web app and authority: "unapproved". Refuses with status: "needs_definitions" while any blocking finding remains. Agents keep using the current release until an approver approves and releases on the web app.
backtest
Re-decide the job’s saved cases (the last 30 days) under a proposal and under the current release, before anyone approves it.
| Input | Type | Meaning |
|---|---|---|
proposal_id |
string, required | From propose_pack |
reinterpret |
boolean | Re-classify only the quotes whose definitions changed. Run without it first to see the estimate. |
execution_ids |
string[] | Only these saved cases, up to 50 |
limit |
integer | How many saved cases, newest first: 1 to 50, default 25 |
Returns each case as unchanged, changed (current outcome to proposal outcome, with each side’s escalation), unresolved (with the questions still open and why), needs_reinterpretation (with an estimate) or unavailable; counts of each; escalationChanged, the number of cases whose escalation differs; breaking when the proposal removes something callers may depend on; a total estimate; and agents: those that ran the cases and those following the job on Latest or pinned. Calls no model unless reinterpret is true. Writes nothing. Counts as 10 decisions toward rate limits. See Review and backtest.