Sasha MCP reference

Read and change the data of a Sasha App

The job

Work with the records of a Sasha App, such as a timesheet, without its view: read and total rows, add a record, and correct a row.

Prerequisites

  • The connection holds apps:invoke. Admin, staff and member connections can all hold it.
  • The person has use or manage access on the app. apps:invoke never gives access to an app by itself.
  • The app stores data and its package validates. To add or change rows, the app declares its data as read-write.
  • A change needs a person to approve it. That works in a client that offers MCP form elicitation, or in the Sasha web interface. Another client can read, but cannot make a change. See the app action tools.

The example app is timesheet. Its table entries has the tools app__timesheet__entries__list, app__timesheet__entries__get, app__timesheet__entries__aggregate, app__timesheet__entries__create, app__timesheet__entries__update and app__timesheet__entries__delete. It also exposes the action log-hours as app__timesheet__log_hours.

Steps

  1. Find the app's tools in your tool list. Read each tool's description: it says whether a table is shared or owner-scoped. You can also call open_app__timesheet with {} to show the app's view in a client that shows MCP Apps. It changes nothing.

  2. Read recent rows with list.

    {
      "query": {
        "where": [{ "field": "date", "operator": "gte", "value": "2026-09-28" }],
        "orderBy": [{ "field": "date", "direction": "desc" }],
        "limit": 20
      }
    }
    

    The result is in structuredContent, with rows. When there are more rows, the result has nextCursor. For the next page, send the same query again, with nextCursor in it as cursor.

  3. Total the hours for each project with aggregate. Do not fetch every row to add them up yourself.

    {
      "aggregation": {
        "where": [
          { "field": "date", "operator": "gte", "value": "2026-09-28" },
          { "field": "date", "operator": "lte", "value": "2026-10-04" }
        ],
        "groupBy": ["project"],
        "metrics": { "totalHours": { "sum": "hours" } }
      }
    }
    
    {
      "groups": [
        { "project": "Client onboarding", "totalHours": 7.5 },
        { "project": "Regional health review", "totalHours": 22 }
      ]
    }
    
  4. Add a record. The app has an action for this job, so use the action instead of create: it applies the app's own rules. Make a new idempotencyKey for this change.

    {
      "input": { "date": "2026-10-01", "project": "Regional health review", "hours": 6.5 },
      "idempotencyKey": "log-hours-2026-10-01-1"
    }
    

    What happens next depends on the client:

    • With form elicitation, the client asks the person to confirm. If they accept, the action runs and the result is the action's declared result, for example { "id": 412, "date": "2026-10-01", "project": "Regional health review", "hours": 6.5 }.
    • In the Sasha web interface, the first call changes nothing. It returns structuredContent.confirmation with a confirmationToken. Ask the person to approve. If they say yes, call again with the same input, the same idempotencyKey and the confirmationToken. If they say no, do not call again.
    • In any other client, the call is refused with APP_CONFIRMATION_REQUIRED. Tell the person that they can make the change in the Sasha web interface, or from a client that offers form elicitation.
  5. Correct a row. First read it with get, to learn its _sasha_version.

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

    Then call update, with _sasha_version as expectedVersion and a new idempotencyKey. The person confirms it as in step 4.

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

    The result is the changed row, with _sasha_version 2.

A delete takes id, expectedVersion and idempotencyKey. It is permanent, with no undo and no history. See the app table tools for every operation and its arguments.

What can go wrong

The full lists are in the app table tools and the app action tools. An app tool refuses with an error result, and structuredContent.error holds code, message, retryable and a diagnosticId to quote.

  • APP_CONFIRMATION_REQUIRED: this client cannot confirm a change, and a retry gets the same answer. Tell the person.
  • APP_CONFIRMATION_DECLINED: the person said no. Ask them what they want instead.
  • 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_IDEMPOTENCY_MISMATCH: you used an idempotencyKey again with different arguments. Use a new key for a new change.
  • 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_FIELD_UNDECLARED: a column name is not declared for the table, or you cannot change that column.
  • MCP error -32602: Tool app__timesheet__entries__update not found: the connection does not hold apps:invoke, or the person has no access to the app, or the app's data is read only. Read the permissions section of getDocs.

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

Related notes

made with bernard

Cookie settings