Sasha MCP reference

App table tools (app__<app>__<table>__<op>)

Tool names: app__<app>__<table>__{list,get,create,update,delete,aggregate}

Scopes: apps:invoke

When to use

Use the table tools to read or change the rows of a Sasha App without its view, for example to total a week's hours in a timesheet. If the app has an action for the job, prefer the action (the app-action family): it applies the app's own rules.

The tool name is app__<app>__<table>__<op>, with each - in the app id and the table name changed to _. <op> is list, get, aggregate, create, update or delete. For example, the table entries of the app timesheet has app__timesheet__entries__list, app__timesheet__entries__get and four more.

The tools are in the tool list only when all of these are true:

  • The connection holds apps:invoke, and the person has use or manage access on the app (the same rule as the open-app family).
  • The app declares that it stores data, and the package validates.
  • For create, update and delete: the app declares its data as read-write. An app with read data has only list, get and aggregate.

Each table is either shared or owner-scoped, and the tool description says which:

  • Shared: every user of the app sees the same rows, so a change reaches all of them.
  • Owner-scoped: you see and change only the rows of the person who connected. Sasha sets the owner column of a new row to that person, and an update cannot change it.

Arguments for each operation:

  • list: optional query, with select (up to 50 column names), where (up to 20 conditions of field, operator and value), orderBy (up to 3 entries of field and direction, asc or desc), limit (1 to 200, default 50) and cursor. The operators are eq, neq, lt, lte, gt, gte, in, contains and starts-with. The result is rows, and nextCursor when there are more rows. For the next page, put nextCursor in query as cursor, and keep the rest of query the same.
  • get: id, the row's integer id. The result is row.
  • aggregate: aggregation, with optional where and groupBy (up to 3 columns), and metrics: 1 to 10 named metrics, each one of { "count": true }, { "countDistinct": "<column>" }, or { "sum" | "avg" | "min" | "max": "<column>" }. The result is groups. Without groupBy, the metric values are also at the top level of the result.
  • create: values (1 to 50 columns) and idempotencyKey. The result is the new row.
  • update: id, changes (1 to 50 columns), expectedVersion and idempotencyKey. The result is the changed row.
  • delete: id, expectedVersion and idempotencyKey. The result is deleted: 1. A delete is permanent, with no undo and no history.

Each row has the table's columns and _sasha_version, an integer that goes up by one at each change. A list with select returns only the selected columns and _sasha_version. Pass it as expectedVersion to update and delete: the change is refused if the row changed after you read it.

idempotencyKey is 1 to 128 characters. Use a new key for each new change, and the same key only to retry the same change. The retry returns the first result and does not change data twice.

The result is in structuredContent, and the text is the same value as JSON.

Confirmation for a change

create, update and delete need a person to approve the change first, in the same way as an action that changes data (see the app-action family):

  • A client that offers MCP form elicitation asks the person. If they decline, the call is refused with APP_CONFIRMATION_DECLINED.
  • A client that does not offer form elicitation gets APP_CONFIRMATION_REQUIRED. A retry gets the same answer. The person can make the change in the Sasha web interface, or from a client that offers form elicitation.
  • In the Sasha web interface, the first call returns a result that is not an error and changes nothing, with structuredContent.confirmation holding required: true, a confirmationToken, expiresAt and effects. After the person approves, call again with the same arguments and the same idempotencyKey, plus the optional confirmationToken argument. A token works once, for those exact arguments, for 10 minutes.

list, get and aggregate need no confirmation.

Example

Total the hours per project for the week that starts on 28 September 2026, with app__timesheet__entries__aggregate:

{
  "aggregation": {
    "where": [
      { "field": "date", "operator": "gte", "value": "2026-09-28" },
      { "field": "date", "operator": "lte", "value": "2026-10-04" }
    ],
    "groupBy": ["project"],
    "metrics": { "totalHours": { "sum": "hours" } }
  }
}

The structured result (structuredContent):

{
  "groups": [
    { "project": "Client onboarding", "totalHours": 7.5 },
    { "project": "Regional health review", "totalHours": 22 }
  ]
}

Then correct one entry with app__timesheet__entries__update. Read the row first with app__timesheet__entries__get to learn its _sasha_version:

{ "id": 412, "changes": { "hours": 7 }, "expectedVersion": 1, "idempotencyKey": "fix-entry-412-hours" }

After the person confirms:

{ "row": { "id": 412, "created_by": 14, "date": "2026-10-01", "project": "Regional health review", "hours": 7, "note": null, "_sasha_version": 2 } }

Refusals and what to do

An app tool refuses with an error result. The text is the error message, and structuredContent.error holds code, message, retryable, a diagnosticId to quote, and sometimes details.field.

  • APP_CONFLICT (app_conflict): the row changed after you read it. Call get again, check the new values, and retry with the new _sasha_version and a new idempotencyKey.
  • APP_RECORD_NOT_FOUND (app_record_not_found): there is no row with that id that you can see. In an owner-scoped table, rows of other people are not visible.
  • APP_CONFIRMATION_REQUIRED and APP_CONFIRMATION_DECLINED: see the confirmation section above.
  • APP_INPUT_INVALID (app_input_invalid): the arguments do not match the schema, or a value or a query part is not valid, for example a limit above 200 or an orderBy entry without direction. details.field names the part that is wrong when it can.
  • APP_FIELD_UNDECLARED (app_field_undeclared): a column name is not declared for the table, or names a column that you cannot change, such as id, the owner column of an owner-scoped table, or a _sasha_ column. Read the table's columns from a get or list result.
  • APP_IDEMPOTENCY_MISMATCH (app_idempotency_mismatch): the idempotencyKey was used before with different arguments. Use a new key.
  • APP_BUSY (app_busy): a call with the same idempotencyKey is still running. Wait, then retry with the same arguments.
  • APP_ACCESS_DENIED (app_access_denied): the person no longer has access to the app.
  • If the connection does not hold apps:invoke, or the app does not store data, or its data is read only, the tool is not in your tool list, and a call returns MCP error -32602: Tool app__timesheet__entries__update not found (with the name you called). Read the permissions section of getDocs.

If an operation is missing, unclear or unsafe for the job, report one concrete issue with suggestImprovement. Do not include records, source or paths.

Related

  • The app-action family runs the app's own actions, and explains confirmation in full.
  • The open-app family opens the app's view.
  • listAppFiles shows the app's APP.md and schema.sql, for admin and staff users who manage the app.
  • suggestImprovement reports a problem with a table tool.
  • getDocs has the permissions section.
made with bernard

Cookie settings