---
title: "Record an MCP tool invocation"
method: POST
path: "/audit/mcp-tool"
tags: ["Audit"]
---

# Record an MCP tool invocation

`POST /audit/mcp-tool`

## Request

- Base URL: `https://next.frase.io/api/v1`
- URL: `https://next.frase.io/api/v1/audit/mcp-tool`
- Auth: API key in header `X-API-KEY`

Append-only telemetry ingest for MCP tool calls. The Frase MCP server (npm/stdio build) posts one row here per tool invocation; the in-app HTTP MCP surface writes the same row directly without the round trip. Rows land in `mcp_tool_invocations`, scoped to the authenticated organization, and are swept by the daily retention job.

No scope is required beyond a valid API key - any key for the organization may write its own telemetry. Only a hash and the size of the tool input are stored, never the input itself, so no customer content reaches this table.

A dedicated per-key rate limit of 100 requests/minute sits on top of the shared tier bucket (keyed on organization plus user, falling back to the key id), so one buggy client cannot drive unbounded row growth between retention sweeps.

Clients should treat this call as fire-and-forget: telemetry must never break a tool call. Every field except `error_code` is required, and unknown fields are ignored.

## Request body

- object
  - `tool_name` string, required: Required. The MCP tool that ran. Trimmed before validation.
  - `input_hash` string, required: Required. SHA-256 of the JSON-serialized tool input, 64 hex characters. Case-insensitive on the way in; stored lowercased.
  - `input_size` integer, required: Required. Byte length of the serialized input (UTF-8).
  - `result_status` 'success' | 'error' | 'rate_limited' | 'validation_error' | 'not_found' | 'unauthorized' | 'timeout', required: Required. How the tool call ended.
  - `duration_ms` integer, required: Required. Wall-clock duration of the tool call, capped at 15 minutes.
  - `error_code` string: Optional. Short machine code for a non-success result. Trimmed; letters, digits, underscore, hyphen and dot only (case-insensitive). Stored as null when omitted.

## Response `201`

Invocation recorded

- object
  - `data` object
    - `recorded` boolean: Always true on 201.

## Other responses

- `400`: The body was not a JSON object, or a field failed validation. Structured Error with code `invalid_param`; `param` names the first failing field (or `body`) and `message` carries that field's validation message.
- `401`: Authentication failed
- `429`: The per-key ingest limit of 100 requests/minute was exceeded, or the shared API tier/budget limit denied the request. The ingest denial uses the plain `{ "data": { "error": "Rate limit exceeded. Retry after N seconds." } }` envelope and carries no Retry-After header - the retry delay is in the message.
- `500`: The insert failed (`Failed to record MCP audit event`). Plain `{ "data": { "error": "..." } }` envelope. Clients must not retry in a way that blocks the tool call this row describes.

---

[API](https://skmtc.dev/frase/apis/frase-api.md) · [All operations](https://skmtc.dev/frase/apis/frase-api/llms.txt) · [OpenAPI document](https://skmtc.dev/frase/apis/frase-api/revisions/7fb75d89f7a4?raw)
