---
title: MCP tools
description: Felspar's remote MCP server - connection, the four agent tools and the seven authoring tools, with inputs, results and errors.
sidebar:
  order: 1
---

## 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](/reference/refusal-codes), 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`, `notFor` and `owner`: what the job is for, quoted, first;
- `subject`: what each case is about (`type`, quoted `definition`, `identifier`, `example`, `match`), its related records (`related[]`, each with `relationship`, `cardinality`, `required` and quoted definitions), and `send`: in plain words, exactly which ID to send and how to send related ones;
- `identity`: how each case gets its ID (`mode`: `provided`, `fields`, `content` or `issued`), `send` in plain words, and the `fields` or `question` it 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 with `meaning`, `inclusions`, `exclusions`, `examples`, `edgeCases`) and `whereToLook[]`; a question about a related record carries `about` (`relationship`, `subject`);
- `outcomes[]`: `id`, `meaning`, `nextStep` (quoted), the `outputs` that outcome asks the agent to write, and `escalate` when it hands off;
- `escalations`: the hand-offs the policy names (`routes[]`, each `id` with 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`, `withProgress` and 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` (with `detail: "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](/concepts/subjects-and-escalations#identity-modes), 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](/reference/rest-api#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, where `mode` is `provided`, `fields`, `content` or `issued`. Send `case.id` as `entity_id` for 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. `trigger` is `outcome`, `unresolved` or `attempt_limit`. Report the `id` exactly and hand the case off; don't ask again. See [Subjects and escalations](/concepts/subjects-and-escalations#escalations).
- `missing`: with status `gap`, 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](/reference/refusal-codes#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](/reference/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`](/guides/verify-receipts) 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](/reference/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`, with `problems[]` (each a `path` and `message`) and `warnings`;
- `tests_failed`, with `tests` and `warnings`;
- `needs_definitions`, with `blocking` (count), `tests` and `warnings`;
- `ready`, with `tests`, `warnings` and `artifactDigest`.

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](/guides/review-and-backtest).
