# Halo API and MCP semantics

> **Shared platform corpus** — `halo-platform/` is vendored byte-identical across the plugins that carry it; a consistency check enforces identity. Do not hand-edit a vendored copy — edit the canonical copy (see `halo-platform/README.md`) and re-run the corpus sync.

How to talk to Halo: the REST API surface (auth, pagination, IDs, errors) and the semantics of the MCP tool catalog layered on top of it (SQL-backed reads, runbook-backed writes, response relays and their caps). Write payload shapes and null-handling live in `halo-platform/write-semantics.md`; SQL dialects in `halo-platform/sql-dialect.md`.

## 1. REST API — authentication

Halo uses OAuth2 Client Credentials flow; every integration starts with a token request.

1. **POST** `{base_url}/auth/token`, form-urlencoded: `grant_type=client_credentials`, `client_id`, `client_secret`, `scope=all` (or restricted scopes).
2. Response includes `access_token`, valid ~1 hour.
3. Subsequent requests: `Authorization: Bearer {access_token}`.

API applications are created in Halo admin (Configuration → Integrations → Halo API), auth method "Client ID and Secret", with module read/write permissions granted per need.

## 2. REST API — key resources

| Resource | Endpoint | Common operations |
|---|---|---|
| Tickets | `/api/tickets` | Create, list, update, search |
| Actions | `/api/actions` | Notes / field updates on tickets |
| Clients | `/api/client` | Customers |
| Users | `/api/users` | End-user contacts |
| Agents | `/api/agent` | Technicians |
| Assets | `/api/asset` | Configuration items (every asset type) |
| Statuses / Priorities / Ticket Types | `/api/status`, `/api/priority`, `/api/tickettype` | ID lookups |

**ID mapping.** Halo uses numeric IDs for statuses, priorities, ticket types, categories, and lookups. **IDs vary between tenants — always resolve dynamically** (lookup endpoints, the MCP catalog tools, or a pre-synced object table where one exists). Never carry an ID from one tenant to another.

**Tenant-specific configuration** (instance URL, OAuth client id/secret, ticket-type ids) must never be overwritten from repo-file defaults — repo files carry placeholders; production carries live tenant values.

## 3. REST API — pagination and errors

Pagination is page-based: `page_no` (1-based), `page_size` (default 50, max 100); responses carry `record_count`. Loop until a page returns fewer than `page_size` rows, or compute pages from `record_count`.

| Status | Meaning | Action |
|---|---|---|
| 401 | Token expired | Re-request token, retry |
| 403 | Insufficient permissions | Check API application scope |
| 404 | Resource not found | Verify the ID |
| 422 | Validation error | Check required fields / types |
| 429 | Rate limited | Exponential backoff |

## 4. The MCP tool catalog — two tool families

The grcitsm MCP server exposes two kinds of tools; their failure modes differ fundamentally.

| Family | Backing | Response behavior |
|---|---|---|
| **SQL-backed reads** (report tools: summaries, catalogs, lookups) | Halo report SQL | Synchronous data. Large responses save to a tool-results file (§ 6). |
| **Runbook-backed tools** (every write + some reads like `get_asset`) | Halo integration runbooks | Response relay is **tenant-config-dependent** — rich payload when relay is configured, else the bare `"Task queued."` / `"Completed successfully"` string (§ 5). |

`create_new_ticket`, `get_ticket`, `search_tickets` are built-ins (not runbook-backed) and return real payloads synchronously.

## 5. Runbook relay semantics — why "success" proves nothing

A runbook-backed write returns success when Halo **accepts the task**, not when it persists the data. A malformed body — wrong field name, wrong CF shape, mismatched lookup value — is silently dropped at the persistence step with no error. **Always read back the write** (full doctrine + verification tables: `write-semantics.md` § 11).

Worse than a silent no-op: the runbook engine treats an **empty Native-SQL resultset as step SUCCESS**, so a runbook-backed tool whose precondition query matches nothing can run its write step with null tokens and **destroy the target while returning success** (observed live 2026-08-07: an unknown-key invocation blanked a populated asset's identity fields and framework CFs). Read-back after every runbook-backed write is not optional hygiene — it is the only signal. The authoring-side control (`THROW` precondition guards) is dev-side doctrine and lives with the plugin that owns runbook creation.

Two independent tenant-config switches control whether tools relay rich payloads:

1. **Admin setting** — Configuration → Advanced Settings → "Pause all background services" must be UNCHECKED; when checked, every runbook-backed tool (including `get_asset`) short-circuits to the generic string.
2. **Runbook end-step config** — BOTH the Successful and Unsuccessful end-steps must relay the API response. If failures return rich errors but successes return `"Task queued."`, the Successful branch's relay field is unset (the asymmetric-relay trap).

## 6. Read-tool semantics

- **System-context blocks are IDs only.** MCP report reads surface `Client ID` / `Site ID` / `System ID` without names. Resolve via `client_details` (client) and `system_information` (site/system — also carries Impact Level, baselines, SSP narratives). One lookup-pair usually resolves a whole filtered batch. Never quote a name field straight from a bulk read — those columns are absent; you'll quote `undefined` or fabricate.
- **Large-response tools stage to files.** `categories`, `ticket_type_fields`, `ticket_summary`, `active_inventory`, `asset_summary`, `asset_type_fields` routinely exceed the MCP response budget (>32KB) and save to a tool-results file — plan to slice with `jq`/`python`. **Bulk reads stage to disk, then work from the file**: land the full pull in your run's data directory with a manifest checkpoint, then query the file; never accumulate raw payloads in context, and never re-pull per row inside parallel workers when one staged pull serves all.
- **Response-relay cap ~262 KB (~50-55K tokens).** Halo stores NVARCHAR(MAX); the binding constraint is what the server can relay back. Heavily-hydrated rows (e.g. KB articles with many linked devices) can exceed it even when the stored body is small.
- **`get_ticket` on an unknown id echoes your arguments instead of erroring** — treat an echo (no populated Faults columns) as not-found.
- **`search_tickets` is recency-biased,** not relevance-first; exact-subject matches may not surface. Confirm by ID when relevance matters. `search_knowledge_base` matches body + tags, NOT the title.
- **Tool descriptions and param lists can drift behind the live schema** — cross-check `ticket_type_fields` / `asset_type_fields` / `asset_types` at call time rather than trusting the advertised parameter docs.

## 7. SLA-scoped priority labels

The same integer priority level maps to different labels per SLA. Always resolve via `slas_and_priorities` on `(SLA ID, Priority Level)` before presenting or composing a priority change. Example schemes from the standard GRCITSM tenant: Remediation = Critical/High/Moderate/Low; Request and Incident = Critical/High/Medium/Low; validation-type SLAs may be 2-level; some SLAs are single-level or N-coded. A naive "set priority to 3" is meaningless on an SLA with no level 3 — validate the level against the ticket's SLA first.

## 8. MCP tool authoring caps (when extending the catalog)

- **`mcp.tool_description` ≤ ~974 chars** (`len(desc.rstrip())`). Two silent caps reject the save as `[object Object]`: ~1000 (UI) and ~1024 (wire).
- **`mcp.tool_name` must match `[A-Za-z0-9_]+`** — snake_case; no hyphens.
- Re-importing a runbook JSON does NOT refresh the live MCP tool's parameter descriptions — re-open and re-save the MCP tool config in Halo admin after any param change, or the tool advertises stale params.

Full authoring doctrine (runbook JSON editing, `new_*` field names, native-SQL and Array-Iteration steps, import mechanics) is dev-side tooling and lives with the plugin that owns runbook creation, not in this corpus.
