# Service request MCP tools

> Five tools cover the inbound queue end to end. list_service_requests reads the queue, get_service_request reads one and returns the actions available on it, triage_service_request marks it under review, decline_service_request closes it with a client-visible reason, and create_proposal_from_service_request turns it into a draft quote. All five share one operations module with the operator UI, so agent and human triage produce identical records.

URL: https://biloh.com.au/docs/reference/service-request-mcp-tools
Category: Reference | Audience: agent | Updated: 2026-08-16

## What are these tools for?

They expose a client's **inbound service request queue** — the work clients
submit from their portal — to an assistant connected over MCP. Everything the
operator can do to a request from the UI, an agent can do through these five
tools, and the resulting record is identical.

For the concept, read
[how client requests close the loop](/docs/concepts/client-requests-close-the-loop).
For the human path, read
[triage an inbound client request](/docs/how-to/triage-an-inbound-client-request).

## The five tools

| Tool | Class | What it does |
|---|---|---|
| `list_service_requests` | read | The queue, filterable by client, status, or `awaiting_triage` |
| `get_service_request` | read | One request in full, plus `next_actions` |
| `triage_service_request` | write | Marks it under review |
| `decline_service_request` | write | Closes it with a client-visible reason |
| `create_proposal_from_service_request` | write | Turns it into a draft proposal |

## Reading the queue

`list_service_requests` takes `client_id`, `status`, `awaiting_triage` and
`limit`. Pass `awaiting_triage: true` rather than enumerating statuses — it
covers `new`, `pending` and `under_review` in one call, and returns an
`awaiting_triage_count` alongside the rows.

Each row carries the fields you need to decide without a second call:

```json
{
  "reference": "REQ-XXXXXX",
  "status": "pending",
  "client_name": "…",
  "location": "12 Example Street, Suburb QLD 4000",
  "location_is_known_site": false,
  "site_id": null,
  "requested_address": "12 Example Street, Suburb QLD 4000",
  "description": "…",
  "urgency": "flexible",
  "age_days": 3
}
```

**Read `location_is_known_site` on every row.** When it is `false`, the client
has named somewhere the business has no site for. Those requests cannot be
quoted against an existing premises and usually need `create_site` first.

## Reading one request

`get_service_request(service_request_id)` returns the full record plus a
`next_actions` array — the actions legitimately available given the request's
state. When the location is not a known site, `create_site` appears in that
list **before** `create_proposal_from_service_request`, because quoting first
produces a proposal with no premises attached.

Treat `next_actions` as the ordering instruction, not a menu.

## Triaging

`triage_service_request(service_request_id)` sets the status to `under_review`,
stamps the calling identity as the reviewer, and emits
`service_request.status_changed` carrying that identity as the event actor. The
client sees the status move in their portal.

## Declining

`decline_service_request(service_request_id, reason)` requires a non-empty
`reason`, and rejects whitespace. That is not input hygiene — **the reason is
rendered to the client in their portal**, so write it in the words you would
say to them.

The call sets the status, stores the reason, stamps the reviewer, emits the
status-change event with the reason in its payload, and writes an audit row.

## Quoting

`create_proposal_from_service_request(service_request_id)` creates a **draft**
proposal, links it to the request, and moves the request to `quoted`.

The proposal inherits the client, the site (when the request named one), the
client's purchase-order reference, the description as notes, and — when the
location is an address rather than a site — that address written into the notes
so it cannot be lost.

The response tells you what you are holding:

```json
{
  "proposal_id": "…",
  "line_count": 0,
  "requested_address_needs_a_site": "12 Example Street, Suburb QLD 4000",
  "next_step": "add_proposal_line — this proposal has NO lines and would send as an empty quote"
}
```

**It prices nothing.** A request naming a catalogue service yields one line at
zero; a request describing something not in the catalogue yields no lines at
all. Read `line_count` before going anywhere near `propose_send_proposal`.

If the line insert fails, the proposal is deleted and the request is left
untouched — the call fails rather than leaving a half-made quote in the
pipeline.

## Why agent and human triage agree

The three write tools and the operator UI call **one shared operations module**.
Neither surface owns a private copy of the rules.

This matters more than it sounds. When the same logic is implemented twice, the
two copies drift in the details nobody asserts — which field gets stamped, which
identity is recorded, which column is carried onto the next record. A single
module makes "the agent did it" and "the operator did it" produce byte-identical
records, and makes a test of one a test of both.

## Related

- [How client requests close the loop](/docs/concepts/client-requests-close-the-loop)
- [Triage an inbound client request](/docs/how-to/triage-an-inbound-client-request)
