Apito MCP Server
Officialby apito-io
README.md
# Apito MCP Server
**Version 1.7.0** — see [CHANGELOG.md](./CHANGELOG.md).
A Model Context Protocol (MCP) server for [Apito](https://apito.io) - an API builder and headless CMS. This server enables LLMs like Claude to interact with Apito's system GraphQL API to create models, manage fields, and build schemas.
**System vs public GraphQL:** Most MCP tools call the **system** GraphQL surface (console-style operations such as `getModelData`, model and field mutations). Use **`get_public_graphql_model_map`** + **`probe_public_document`** for the **public** per-project `/secured/graphql` surface (root camel ops, nested snake selection keys). When debugging “works in MCP / system but not on public,” compare endpoints and credentials; do not assume identical behavior.
## Features
- **Model Management**: Create, list, query, and delete models in your Apito project
- **Field Management**: Add, update, rename, and delete fields with explicit type specification
- **Relation Management**: Create relations between models (has_one, has_many)
- **Public GraphQL map + probe**: Nested snake selection/connect keys + live document hydrate on `/secured/graphql`
- **Access token inspect**: Self-introspect configured `apt_` caps/grants via `/system/access-tokens/me`
- **Full Field Type Support**: All Apito field types including text, multiline, number, date, boolean, media, object, repeated, list (with sub-types), and geo
- **Resources**: Expose model schemas as MCP resources for easy access
- **Error Handling**: Comprehensive error handling with detailed messages
- **Cloudflare Workers**: Deploy as a remote MCP server for use with any MCP client
- **Project-Dependent API Keys**: API keys are passed per-request, allowing different projects to use the same worker
- **Schema versioning (pro)**: Stage schema mutations into a draft; verify via `get_effective_schema`; user publishes in Console — MCP never publishes
- **Platform management**: Full project administration beyond schema — tenant catalog, app end-users, auth testing, roles/settings/API keys, webhooks/plugins/functions/media, and extended data operations
- **Edition split**: `APITO_MCP_EDITION=open` hides pro-only tools (tenant catalog, some schema versioning extras); default is `pro`
- **Explicit project scope**: exact allowlist, canonical `X-Apito-Project-Id`, sticky write leases, and longer default TTL
## Project scope and write leases
Project access is fail-closed. Configure:
- `APITO_ALLOWED_PROJECT_IDS` — required comma-separated exact project IDs
- `APITO_DEFAULT_PROJECT_ID` — optional read default; must be in the allowlist
- `APITO_ALLOWED_TENANTS_BY_PROJECT` — optional JSON map of project ID to exact tenant ID arrays
- `APITO_PROJECT_SCOPE_TTL_SECONDS` — preparation/lease TTL (default **`1800`**)
Reads require `project_id` unless a sticky session default or env default is configured
(precedence: explicit arg → sticky default from prepare/confirm/scoped read → env).
Writes require explicit `project_id`; `scope_lease` may be omitted when an in-process
**sticky lease** from `confirm_project_scope` is still valid for the same project/tenant.
Destructive tools additionally require `confirm_destructive: true`. Obtain a lease with:
1. `prepare_project_scope({ project_id, tenant_id? })`
2. Explicitly verify the returned project/tenant, then call
`confirm_project_scope({ project_id, tenant_id?, preparation_id })`
3. Pass the returned lease to writes, or rely on the sticky lease for the same scope.
`get_project_scope` reports the allowlist/default, sticky lease present/expiry (never the
lease token), and active lease summaries. Leases are random, short-lived, and project-bound.
Every scoped GraphQL call sends only the canonical project header
`X-Apito-Project-Id` plus optional `X-Apito-Tenant-ID`. No alternate project header
spellings are recognized. `apt_` requests never send the legacy temporary tenant
cookie.
## Debugging list UI / public GraphQL gaps
MCP proves the **engine public surface**. App wiring (`useTable` dropping
`meta.connectionFields`, generated `hooks.ts`) stays **outside MCP** — grep codegen locally after:
1. `get_public_graphql_model_map({ model_name })` — root camel ops + nested snake
`selection_key` / `connect_key` / `filter_key` (e.g. `ledger_list`, `customer_id`).
2. `probe_public_document({ model_name, id, relations? })` — one-shot `/secured/graphql`
hydrate + selected-vs-null relation report.
3. If fields exist in the map/probe but the UI is empty → inspect app codegen and
`connectionFields` / `useTable` (not an MCP bug).
**Non-goals:** MCP does not parse app `hooks.ts`, audit Refine `meta.connectionFields`,
or publish/approve schema.
Optional: `APITO_PUBLIC_GRAPHQL_ENDPOINT` overrides the derived `/secured/graphql` URL.
## Debugging access token / CAPABILITY_DENIED
1. `inspect_access_token` — self-introspect the configured MCP `apt_` (preset, caps,
project/tenant grants, expiry). Never accepts or echoes a raw secret.
2. Optional `operation` (e.g. `queryDocuments`) → required-capability hint.
3. Optional `project_id` / `tenant_id` → whether current grants would authorize those headers.
4. If a cap/grant is missing → regenerate in Console → Access Token.
Requires engine `GET /system/access-tokens/me` (apt_ Bearer only).
## Platform management tools (v1.3)
Prior to v1.3, MCP covered **schema design**, **draft versioning (read)**, and **model data CRUD** only. SaaS operators still had to open Console for tenants, app users, roles, webhooks, and similar admin tasks. v1.3 adds **~50 platform tools** that mirror Console system GraphQL — so an LLM agent can provision tenants, manage end-users, configure project settings, and run integration setup in one MCP session alongside schema work.
All platform tools use the same transport as existing MCP tools: **system GraphQL** (`/system/graphql` + `Authorization: Bearer <apt_...>`). They are **not** public `/secured/graphql` operations. Tool descriptions are tagged **`[pro]`**, **`[core]`**, or **`[core/pro]`** for a future open-mcp vs pro-mcp package split.
If the access token's `project_ids`/capabilities grant doesn't cover an operation, the engine returns a `CAPABILITY_DENIED: missing capability <name>` GraphQL error, which surfaces verbatim in the tool call result — regenerate the token in Console → Access Token with the missing capability instead of retrying.
### Tool catalog by domain
| Domain | Tools | Engine GraphQL (representative) |
|--------|-------|--------------------------------|
| **SaaS tenants** `[pro]` | `list_tenants`, **`search_tenants`**, `create_tenant`, `update_tenant`, `delete_tenant`, `generate_tenant_token`, `search_tenant_by_domain` | `getTenants`, **`searchTenants`**, `createTenant`, … |
| **App end-users** | `search_app_users`, `create_app_user`, `update_app_user`, `delete_app_user`, `reset_app_user_password`, `login_app_user`, `google_oauth_state`, `login_app_user_google` | `searchUsers`, `createUser`, `updateUser`, `deleteUser`, `resetUserPassword`, `loginUser`, `googleOAuthState` |
| **Schema versioning extras** `[pro]` | `get_schema_diff`, `list_schema_versions`, `list_schema_change_events`, `discard_schema_draft` | `schemaDiff`, `schemaVersions`, `schemaChangeEvents`, `discardSchemaDraft` |
| **Project admin** | `list_roles`, `get_permissions_catalog`, `upsert_role`, `duplicate_role`, `delete_role`, `get_project_settings`, `update_project_settings`, `list_api_keys`, `create_api_key`, `delete_api_key`, `get_auth_settings`, `update_auth_settings`, `get_storage_settings`, `update_storage_settings`, `list_team_members`, `update_team_members` | `currentProject`, `upsertRoleToProject`, `generateProjectToken`, `updateProjectAuthentication`, etc. |
| **Integrations** | `list_webhooks`, `create_webhook`, `delete_webhook`, `list_plugins`, `configure_plugin`, `remove_plugin`, `list_media`, `upload_media_from_url`, `delete_media` | `createWebHook`, `upsertPlugin`, `uploadImageFromURL`, etc. |
| **Logic functions** | `list_functions`, `upsert_function`, `delete_function`, `test_function_draft`, `deploy_function`, `execute_function`, `list_function_revisions`, `list_function_deployments`, `rollback_function` | `projectFunctionsInfo`, `upsertFunctionToProject`, `testFunctionDraft`, `deployFunctionToProject`, `listFunctionRevisions`, `listFunctionDeployments`, `rollbackFunctionDeployment`, REST `POST /function/:project/:name` |
| **Data plane** | `list_data`, `connect_relation`, `disconnect_relation`, `get_model_document_counts`, `list_document_revisions`, `reorder_fields` | `getModelData`, `upsertModelData` (connect/disconnect), `modelDocumentCounts`, `listDocumentRevisions`, `rearrangeSerialOfFieldType` |
### Typical workflows
**SaaS tenant onboarding** `[pro]`
1. `search_tenants` — paginated catalog with optional `q` (prefer over `list_tenants` for large projects).
2. `list_tenants` — full unbounded catalog (small projects only).
3. `create_tenant` — provision a tenant (`name`, optional `domain`, optional `data` JSON string). On per-tenant separate DB projects, engine provisions the tenant database.
4. `generate_tenant_token` — mint a tenant-scoped API token (`tenant_id`, `duration` as `YYYY-MM-DD`, optional `role`). **Sensitive** — treat like a secret.
5. `search_tenant_by_domain` — resolve tenant by hostname for login routing (`project_id`, `domain`).
Pass **`tenant_id`** on subsequent data and app-user tools, or set `TENANT_ID` / `X-Apito-Tenant-ID` globally for the session.
**App end-user administration**
1. `search_app_users` with `project_id` (optional `tenant_id`, optional `q`) — paginated user list; matches Console → Users.
2. `create_app_user` — requires `password` plus `email` and/or `phone`; optional `role`, `username`, `tenant_id`.
3. `update_app_user` / `reset_app_user_password` / `delete_app_user` — lifecycle management by `user_id`.
**Auth testing (local + Google)**
Read **`apito://saas-auth-guide`** or call **`get_saas_auth_guide`** first.
- **Local:** `login_app_user` with `project_id`, `password`, and email or phone → returns `{ token, user }`. Use `token` as Bearer on **public** `/secured/graphql` (not the MCP system API key).
- **Google:** `google_oauth_state` → open OAuth URL in a browser → `login_app_user_google` with `code` + `state` (or `id_token`). MCP cannot complete OAuth without a human pasting the callback code.
Returned JWTs and generated API tokens are **sensitive**. Do not log or commit them.
**Project settings & access control**
- **Roles:** `list_roles` → `get_permissions_catalog` → `upsert_role` (name, `api_permissions`, `logic_executions`, `is_admin`). Use `duplicate_role` / `delete_role` as needed.
- **Settings:** `get_project_settings` / `update_project_settings` for name, description, and settings payload.
- **API keys:** `list_api_keys` → `create_api_key` (returns token once) → `delete_api_key` to revoke.
- **Auth & storage:** `get_auth_settings` / `update_auth_settings`, `get_storage_settings` / `update_storage_settings`.
- **Console team:** `list_team_members` / `update_team_members` (add/remove operators on the project).
**Integrations**
- **Webhooks:** `list_webhooks` → `create_webhook` (events, model, url, name) → `delete_webhook`.
- **Plugins:** `list_plugins` (by `type` enum, e.g. `STORAGE`, `FUNCTION`) → `configure_plugin` / `remove_plugin`.
- **Media:** `list_media` → `upload_media_from_url` → `delete_media` by ids.
**Logic functions (Deno/TS lifecycle)**
The full author → test → deploy → invoke loop mirrors the Console Logic workspace and reuses the engine's shipped GraphQL + REST callable.
1. `list_functions` (optional `name` filter) — shows `source`, `capabilities`, `active_revision_id`, `trigger_type`, `runtime_config`, and `rest_api_secret_url_key`.
2. `upsert_function` — saves the **draft** source (`source`, `capabilities`, `language`, `trigger_type`, `runtime`). Set `update: true` when editing an existing function.
3. `test_function_draft` — runs the draft synchronously with a mock `payload` (admin-only, no `X-Fn-Hash`). Pass `tenant_id` for SaaS data reads. Returns `ok`, `response`, `error`, `duration_ms`, and `logs`.
4. `deploy_function` — publishes the draft as an immutable revision and marks it active; response includes the new `active_revision_id`.
5. `execute_function` — invokes the **deployed** (active-revision) function over REST (`POST /function/:project/:name`). Resolves the `X-Fn-Hash` from `rest_api_secret_url_key` automatically (pass `fn_hash` to override); the secret is masked in output unless `reveal_secret: true`. Errors clearly if the function was never deployed. Pass `tenant_id` for SaaS.
6. `list_function_revisions` / `list_function_deployments` — history; `rollback_function` (`name`, `revision_id`) re-activates a prior revision.
The REST base is derived from `APITO_GRAPHQL_ENDPOINT` (stripping `/system/graphql`); set `APITO_REST_ENDPOINT` to override for non-standard path layouts.
**Extended data operations**
- `list_data` — same engine op as `get_data` but documented for filtering/pagination; supports `where`, `status`, `search`, `tenant_id`.
- `connect_relation` / `disconnect_relation` — relation updates via `upsertModelData` connect/disconnect maps (use `get_relation_graph` for field names).
- `get_model_document_counts` — row counts per model.
- `list_document_revisions` — revision history for a document.
- `reorder_fields` — change field serial order within a model.
Existing data tools (`get_data`, `upsert_data`, `delete_data`, `duplicate_data`) now accept optional **`tenant_id`** per call in addition to env/header routing.
### SaaS routing & edition
**Tenant scope** — three equivalent ways to route a request to a tenant:
| Method | Where |
|--------|-------|
| Tool argument `tenant_id` | Per-call override (preferred when switching tenants in one session) |
| `TENANT_ID` / `APITO_TENANT_ID` | Stdio MCP client env |
| `X-Apito-Tenant-ID` | Remote Cloudflare worker header |
**Edition** — set `APITO_MCP_EDITION=open` to hide pro-only platform tools (`list_tenants`, `get_schema_diff`, `list_schema_versions`, `list_schema_change_events`, `discard_schema_draft`, `get_saas_auth_guide`). Default is **`pro`** (full surface). This prepares a future split into open-mcp vs pro-mcp packages without changing tool names today.
### Resources & guides
| Resource / tool | Purpose |
|-----------------|---------|
| `apito://saas-auth-guide` / `get_saas_auth_guide` | App user login flows, tenant rules, token handling |
| `apito://saas-model-guide` / `get_saas_model_guide` | Common vs tenant-scoped models |
| `apito://schema-versioning-guide` | Draft/publish workflow (MCP never publishes) |
### Intentionally excluded
MCP does **not** expose destructive or irreversible Console workflows that belong in human-reviewed UI:
- Schema **publish** (`approveSchemaChanges`), **rollback** (`rollbackSchemaVersion`), **flush sync** (`flushSchemaSync`), execution repair mutations
- `deleteProject`, billing/subscription ops, plugin **build** trigger
MCP **stages** schema changes and **reads** drafts; operators **publish manually** in Console. `discard_schema_draft` is included as a safe undo for unstaged drafts only.
### Smoke tests
```bash
pnpm test:tenant-users # edition filtering + optional live tenant/user queries
pnpm test:versioning # schema versioning unit + optional live checks
```
Live tenant/user tests need `APITO_API_KEY`, `APITO_PROJECT_ID`, and optionally `TENANT_ID`.
## Schema versioning workflow (pro projects)
When `PRO_SCHEMA_VERSIONING_ENABLED=true` on the engine, schema mutations **stage a draft** instead of applying immediately.
1. Call **`get_schema_migration_guide`** (or read **`apito://schema-migration-guide`**) before any migration or bulk schema work.
2. Call **`get_schema_versioning_status`** at the start of schema work.
3. Use **`create_model`**, **`add_field`**, **`add_relation`**, etc. — changes go to the draft changeset.
4. Verify with **`get_effective_schema`** or **`get_schema_preview`** (`source: "draft"`).
5. Review **`get_schema_change_plan`** for the publish timeline.
6. End with **`summarize_schema_draft_for_review`** — tells the user to open **Console → Project Settings → Schema Changes → Publish manually**.
7. **`upsert_data`** / **`get_data`** only work on **published (live)** models.
Read tools accept optional **`source`**: `live` | `draft` | `effective` (default: effective when a draft exists). Resources: **`apito://schema-migration-guide`**, **`apito://schema-versioning-guide`**.
**MCP never calls publish mutations** (`approveSchemaChanges`, etc.).
See **[SCHEMA_MIGRATION_GUIDE.md](./SCHEMA_MIGRATION_GUIDE.md)** for full migration dos/donts (nested fields, live vs draft, delete/add pitfalls).
### SaaS tenant headers
For per-tenant database scope, set `TENANT_ID` or `APITO_TENANT_ID` in stdio env, or send **`X-Apito-Tenant-ID`** on remote worker requests (included in CORS allowlist). Most platform and data tools also accept per-call **`tenant_id`** in tool arguments.
Set **`APITO_MCP_EDITION=open`** to hide pro-only platform tools (tenant catalog, `get_schema_diff`, `list_schema_versions`, etc.). Default is **`pro`** (full surface).
### SaaS model scope (common vs tenant-scoped)
On **SaaS projects**, models can be:
| Scope | MCP flag | Meaning |
|-------|----------|---------|
| **Tenant-scoped** (default) | omit `is_common_model` | Each tenant sees only their own rows |
| **Common (project-wide)** | `is_common_model: true` | All tenants share the same rows — no tenant isolation |
**Examples:** `app_release_policy` and a shared `medicine` catalog in hospital SaaS should be **common**. `patient`, `appointment`, and `order` should stay **tenant-scoped**.
- Tool: **`get_saas_model_guide`** — full guide with examples and decision checklist
- Resource: **`apito://saas-model-guide`**
- **`create_model`** accepts `is_common_model`
- **`update_model`** toggles `is_common_model` on existing models (metadata-only, immediate on pro)
- **`rename_model`** renames a model identifier via `updateModel(type: rename)` (stages on pro; publish in Console)
- **`list_models`** shows scope per model (`common`, `tenant-scoped`, `tenant catalogue`)
Call **`get_project_context`** first on SaaS projects, then **`get_saas_model_guide`** before creating models.
### MCP Client Configuration (Cursor / local stdio)
**Monorepo path (this workspace):**
```json
{
"mcpServers": {
"my-project-mcp": {
"command": "npx",
"args": ["-y", "tsx", "/Users/diablo/Projects/monorepo/apito/apito-mcp/src/index.ts"],
"env": {
"APITO_GRAPHQL_ENDPOINT": "http://localhost:5050/system/graphql",
"APITO_API_KEY": "apt_...",
"APITO_ALLOWED_PROJECT_IDS": "your-project-id",
"APITO_DEFAULT_PROJECT_ID": "your-project-id",
"TENANT_ID": "optional-for-saas"
}
}
}
}
```
`APITO_API_KEY` (or `APITO_AUTH_TOKEN`) must be a unified `apt_` access token generated in Console → Access Token, sent as `Authorization: Bearer`. Legacy `mcp-`/`cli-`/`sdk-` prefixed keys are retired — the server refuses to start with one (`TOKEN_FORMAT_RETIRED`). Use one MCP server entry per Apito project access token. **Restart MCP** in Cursor after path or env changes.
### MCP Client Configuration (Cursor / mcp-remote)
Add the apito-mcp server to your MCP client config (e.g. Cursor `~/.cursor/mcp.json` or project `.cursor/mcp.json`):
```json
{
"mcpServers": {
"apito-mcp": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://apito-mcp.apito.workers.dev/sse",
"--header",
"Authorization:Bearer ${APITO_API_KEY}",
"--header",
"X-Apito-Tenant-ID:${TENANT_ID}"
],
"env": {
"APITO_API_KEY": "apt_your-access-token-here",
"TENANT_ID": "optional-tenant-id-for-saas"
}
}
}
}
```
Replace `apt_your-access-token-here` with your Apito access token (Console → Access Token). The `Authorization: Bearer` header is sent with each request to the remote worker. (The worker also still accepts `X-Apito-Key` or `?api_key=` for backwards-compatible transport into the MCP server itself, but the outbound call from the MCP server to the engine always uses `Authorization: Bearer`.)
## MCP Tools
### `create_model`
Create a new model in Apito.
**Arguments:**
- `model_name` (required): Name of the model
- `single_record` (optional): Whether this is a single-record model
- `is_common_model` (optional, SaaS): When `true`, creates a **project-wide common model** — all tenants share rows, no tenant scoping. See `get_saas_model_guide`.
### `update_model`
Update model metadata (not fields).
**Arguments:**
- `model_name` (required): Model to update
- `is_common_model` (optional): Mark model as common (`true`) or tenant-scoped (`false`)
- `single_page_model` (optional): Single-record settings model
At least one of `is_common_model` or `single_page_model` is required.
### `rename_model`
Rename a model's canonical identifier (e.g. `product` → `category`). Uses system `updateModel(type: rename, model_name, new_name)`.
**Arguments:**
- `model_name` (required): Current model name
- `new_name` (required): New model name (prefer snake_case singular: `category`, `variant`)
On pro engines this **stages** a draft `rename_model` op — MCP never publishes. After the user Publishes in Console → Schema Changes, regenerate app codegen and update resource strings. Does not rewrite application code.
### `get_schema_migration_guide`
Returns the full schema migration guide: dos/donts for any source (JSON export, live preview, gap diff). Covers sequential mutations, live vs draft, nested `parent_field`, delete/add pitfalls, verification with `get_schema_preview`, SaaS patterns, and publish handoff. **Call this before bulk schema work.** Same content as resource `apito://schema-migration-guide`. See also [SCHEMA_MIGRATION_GUIDE.md](./SCHEMA_MIGRATION_GUIDE.md).
### `get_saas_model_guide`
Returns the SaaS model classification guide: when to use common vs tenant-scoped models, with examples (`app_release_policy`, hospital `medicine` catalog). No arguments.
### `add_field`
Add a field to an existing model. You must specify `field_type` and `input_type` explicitly.
**Arguments:**
- `model_name` (required): Name of the model
- `field_label` (required): Label/name of the field
- `field_type` (required): Field type (see valid combinations below)
- `input_type` (required): Input type (see valid combinations below)
- `field_sub_type` (optional): Required for `list` fields. Valid values: `dynamicList`, `dropdown`, `multiSelect`
- `parent_field` (optional): Parent field name for nested fields
- `is_object_field` (optional): Whether this field can contain nested fields (auto-set for `object` and `repeated` types)
- `field_description` (optional): Field description
- `validation` (optional): Validation rules (see below for requirements)
- `serial` (optional): Serial number for field ordering
**Valid Field Type Combinations:**
- **Text Field**: `field_type="text"`, `input_type="string"` - Single line text input
- **Rich Text Field**: `field_type="multiline"`, `input_type="string"` - Multiline editor with formatting
- **DateTime Field**: `field_type="date"`, `input_type="string"` - Date & Time input
- **Dynamic Array**: `field_type="list"`, `field_sub_type="dynamicList"`, `input_type="string"` - Flexible list allowing multiple items
- **Dropdown Menu**: `field_type="list"`, `field_sub_type="dropdown"`, `input_type="string"` - Predefined list for single selection
- **REQUIRES**: `validation.fixed_list_elements` (array of strings) and `validation.fixed_list_element_type="string"`
- **Multi-Checkbox Selector**: `field_type="list"`, `field_sub_type="multiSelect"`, `input_type="string"` - Allows selecting multiple options
- **REQUIRES**: `validation.fixed_list_elements` (array of strings) and `validation.fixed_list_element_type="string"`
- **Boolean Field**: `field_type="boolean"`, `input_type="bool"` - True or False toggle
- **File Upload**: `field_type="media"`, `input_type="string"` - Upload images or files
- **Integer Field**: `field_type="number"`, `input_type="int"` - Whole numbers only
- **Decimal Field**: `field_type="number"`, `input_type="double"` - Decimal numbers
- **GeoPoint Field**: `field_type="geo"`, `input_type="geo"` - Latitude & Longitude
- **Object Schema**: `field_type="object"`, `input_type="object"`, `is_object_field=true` - Single object with multiple fields
- **Array Schema**: `field_type="repeated"`, `input_type="repeated"`, `is_object_field=true` - List of objects with multiple fields
**Example - Simple Field:**
```json
{
"model_name": "dentalAssessment",
"field_label": "Date",
"field_type": "date",
"input_type": "string"
}
```
**Example - Dropdown Field:**
```json
{
"model_name": "dentalAssessment",
"field_label": "Status",
"field_type": "list",
"field_sub_type": "dropdown",
"input_type": "string",
"validation": {
"fixed_list_elements": ["active", "inactive", "pending"],
"fixed_list_element_type": "string"
}
}
```
**Example - Nested Object Field:**
```json
{
"model_name": "dentalAssessment",
"field_label": "Chief Complaint",
"field_type": "object",
"input_type": "object",
"is_object_field": true
}
```
Then add nested fields with `parent_field="chief_complaint"`:
```json
{
"model_name": "dentalAssessment",
"field_label": "Complaint",
"field_type": "text",
"input_type": "string",
"parent_field": "chief_complaint"
}
```
### `update_field`
Update an existing field in a model.
**Arguments:**
- `model_name` (required): Name of the model
- `field_name` (required): Identifier of the field to update
- `field_label` (required): New label for the field
- `field_type` (optional): New field type
- `input_type` (optional): New input type
- `field_description` (optional): New description
- `validation` (optional): Updated validation rules
### `rename_field`
Rename a field in a model.
**Arguments:**
- `model_name` (required): Name of the model
- `field_name` (required): Current field identifier
- `new_name` (required): New field identifier
- `parent_field` (optional): Parent field name if this is a nested field
### `delete_field`
Delete a field from a model.
**Arguments:**
- `model_name` (required): Name of the model
- `field_name` (required): Field identifier to delete
- `parent_field` (optional): Parent field name if this is a nested field
### `delete_model`
**Irreversible** — removes the model from the project schema and deletes **all** rows in that model. Uses system `updateModel(type: delete, model_name)`. The engine blocks deletion until **all** schema relations involving this model are removed: `get_model_schema` for this model must show no `connections`, and no other model may list this model in `connections[].model`; use `delete_relation` for each edge first.
**Safety:** You must pass `acknowledge_permanent_deletion: true` (literal boolean) or the tool refuses without calling the API. Get explicit human confirmation before using.
**Arguments:**
- `model_name` (required): Name of the model to delete
- `acknowledge_permanent_deletion` (required): Must be `true`
### `list_models`
List models in the project. Optional **`source`**: `live`, `draft`, or `effective` (default: effective when a draft exists).
**Arguments:**
- `source` (optional): Schema source (see Schema versioning workflow)
### `get_schema_versioning_status`
Returns whether schema versioning is enabled, active version, draft changeset id, and pending operation count.
### `get_schema_preview`
Read `schemaPreview` JSON. **`source`** required: `live`, `draft`, or `version` (with optional `version` int).
### `get_effective_schema`
Merged live+draft schema (console overlay parity). Primary verification tool after staging mutations.
### `get_schema_change_plan`
Publish plan from `schemaChangeExecutionRecords`. Optional `changeset_id`.
### `summarize_schema_draft_for_review`
Markdown summary of effective schema + change plan + user publish instructions.
### `get_model_schema`
Get the complete schema for a model including all fields and their types. Optional **`source`** (default: effective when draft exists).
**Arguments:**
- `model_name` (required): Name of the model to get schema for
- `source` (optional): `live`, `draft`, or `effective`
### `get_model_physical_health`
Read-only: compare published logical fields to physical table columns on the **base project DB**. Use when `list_data` / `upsert_data` fail with `no such column` / `no such table`, or after publish. Returns verdict `ok` | `missing_table` | `column_drift`.
**MCP never applies DDL.** If drift is found, tell the user to repair via Console Schema publish or Studio ops.
**Arguments:**
- `model_name` (required)
### `get_project_physical_health`
Same check for many models (omit `model_names` to scan all, capped). Summarizes ok vs drifting models.
**Arguments:**
- `model_names` (optional): array of model names
### `get_project_query_structure`
Get the Apito project GraphQL query structure: which operations exist for each model. Apito uses a consistent naming convention: for model `Task` you get `task(_id)`, `taskList`, `taskListCount`, `createTask`, `updateTask`, `deleteTask`, `upsertTaskList`. **CamelCase matters** — model names are converted to camelCase for operation names.
**Note:** Public project GraphQL reflects **published (live)** schema only. Draft-only models appear after Console publish.
Use this tool when you need to know what GraphQL operations to call for querying or mutating project data. The schema is dynamic per project, so call this early to discover available operations.
**Arguments:** None
**Returns:** A mapping of each model to its operations:
- **Queries:** `{singular}(_id)` (single by ID), `{singular}List` (paginated list), `{singular}ListCount` (count)
- **Mutations:** `create{Model}`, `update{Model}`, `delete{Model}`, `upsert{Model}List`
### `add_relation`
Create a relation between two models. Relations define how models are connected (e.g., a Patient has many DentalAssessments, or a DentalAssessment belongs to one Patient).
**Arguments:**
- `from_model` (required): Source model name (the model that will have the relation field)
- `to_model` (required): Target model name (the model being related to)
- `forward_connection_type` (required): Forward relation type from source to target. Valid values: `"has_many"` (one-to-many) or `"has_one"` (one-to-one)
- `reverse_connection_type` (required): Reverse relation type from target back to source. Valid values: `"has_many"` (one-to-many) or `"has_one"` (one-to-one)
- `known_as` (optional): Optional alternate identifier for this relation (custom name for the relation field)
**Example:**
```json
{
"tool": "add_relation",
"arguments": {
"from_model": "dentalAssessment",
"to_model": "patient",
"forward_connection_type": "has_many",
"reverse_connection_type": "has_one",
"known_as": "assessments"
}
}
```
This creates:
- Forward: `dentalAssessment` has many `patient`
- Reverse: `patient` has one `dentalAssessment`
### `upsert_data`
Create or update a record in a model. Maps to Apito's `upsertModelData` system mutation.
**Arguments:**
- `model_name` (required): Name of the model
- `payload` (required): JSON object of field values to create/update
- `_id` (optional): Document ID for updates (omit for create)
- `status` (optional): Document status: `"draft"` or `"published"` (default: `"published"`)
- `local` (optional): Locale for localized content (default: `"en"`)
- `connect` (optional): JSON for connecting relations — `{"author_id": "uuid"}` for has_one, `{"tag_ids": ["uuid1","uuid2"]}` for has_many
- `disconnect` (optional): JSON for disconnecting relations
**Relation Connect Pattern:** Keys use `{modelName}_id` for has_one and `{modelName}_ids` for has_many (where `modelName` is the related model name or `known_as`).
### `get_data`
Query or list records from a model with optional filters, pagination, and search. Maps to Apito's `getModelData` system query.
**Arguments:**
- `model_name` (required): Name of the model
- `page` (optional): Page number (default: 1)
- `limit` (optional): Records per page (default: 10)
- `where` (optional): JSON filter object
- `status` (optional): Filter by status: `"all"`, `"draft"`, or `"published"`
- `search` (optional): Text search query
### `delete_data`
Delete a record by ID. Maps to Apito's `deleteModelData` system mutation.
**Arguments:**
- `model_name` (required): Name of the model
- `_id` (required): Document ID to delete
### `duplicate_data`
Duplicate a record by ID. Returns the new record ID. Maps to Apito's `duplicateModelData` system mutation.
**Arguments:**
- `model_name` (required): Name of the model
- `_id` (required): Document ID to duplicate
**Example: Create a category and connect an article to it**
```json
{
"tool": "upsert_data",
"arguments": {
"model_name": "Category",
"payload": {
"name": "Technology",
"slug": "technology"
}
}
}
```
Returns the new category with `id`. Then create an article connected to that category:
```json
{
"tool": "upsert_data",
"arguments": {
"model_name": "Article",
"payload": {
"title": "My First Post",
"slug": "my-first-post"
},
"connect": {
"category_id": "<category-id-from-above>"
}
}
}
```
## MCP Resources
Model schemas and the query structure guide are exposed as resources with URIs:
- `apito://schema-versioning-guide` - Draft vs live schema, verification tools, MCP never publishes, data after publish
- `apito://project-query-guide` - Apito query structure: **response shape** (id, data, meta), **multiline/geo/media sub-selection**, naming, `where` filters, **`relation` list filters** (default for has_one / has_many / M:N — e.g. `studentList(relation: { class: { _id: { eq: "…" } } })`), **`connection` filter** (advanced anchor traversal only — not for everyday related-model filtering), pagination, mutations
- `apito://field-design-guide` - Object vs repeated vs relation field selection rules
- `apito://model/{modelName}` - Model schema as JSON (effective schema when draft exists)
## Field Type Reference
### Available Field Types
- `text` - Single line text input
- `multiline` - Multiline editor with formatting
- `number` - Number field (use `int` or `double` input_type)
- `date` - Date & Time input
- `boolean` - True or False toggle
- `media` - File upload
- `object` - Single object with multiple fields
- `repeated` - Array of objects with multiple fields
- `list` - List field (requires `field_sub_type`)
- `geo` - GeoPoint (latitude & longitude)
### Available Input Types
- `string` - String value
- `int` - Integer number
- `double` - Decimal number
- `bool` - Boolean value
- `geo` - Geographic coordinates
- `object` - Object structure
- `repeated` - Array structure
### Query Sub-Selection (CRITICAL for multiline, geo, media, object, repeated)
When querying these fields, you **must** use a sub-selection. Bare selection causes GraphQL error "must have a sub selection":
- **multiline** → `content { html }` or `bio { text }` (pick `html`, `text`, or `markdown`)
- **geo** → `location { lat lon }`
- **media** → `thumbnail { url }` or `image { url }` (or other subfields)
- **object** / **repeated** → select nested fields
### List Field Sub Types
When using `field_type="list"`, you must specify `field_sub_type`:
- `dynamicList` - Dynamic Array (flexible list allowing multiple items)
- `dropdown` - Dropdown Menu (predefined list for single selection)
- Requires: `validation.fixed_list_elements` and `validation.fixed_list_element_type="string"`
- `multiSelect` - Multi-Checkbox Selector (allows selecting multiple options)
- Requires: `validation.fixed_list_elements` and `validation.fixed_list_element_type="string"`
## Example: Creating a Dental Assessment Model
The MCP server provides basic CRUD operations. The LLM should parse schema definitions and call the appropriate tools. Here's how an LLM would create a dental assessment model:
1. **Create the model:**
```json
{
"tool": "create_model",
"arguments": {
"model_name": "dentalAssessment"
}
}
```
2. **Add fields one by one:**
```json
{
"tool": "add_field",
"arguments": {
"model_name": "dentalAssessment",
"field_label": "Date",
"field_type": "date",
"input_type": "string"
}
}
```
3. **Add object field:**
```json
{
"tool": "add_field",
"arguments": {
"model_name": "dentalAssessment",
"field_label": "Chief Complaint",
"field_type": "object",
"input_type": "object",
"is_object_field": true
}
}
```
4. **Add nested fields:**
```json
{
"tool": "add_field",
"arguments": {
"model_name": "dentalAssessment",
"field_label": "Complaint",
"field_type": "text",
"input_type": "string",
"parent_field": "chief_complaint"
}
}
```
## Development
```bash
# Type check
pnpm run typecheck
# Schema versioning unit (+ optional live) tests
npx tsx test-schema-versioning.ts
# Build
npm run build
# Development mode (Cloudflare)
npm run dev
```
## Error Handling
The server provides detailed error messages including:
- GraphQL error codes and paths
- Validation errors
- Network errors
- Field type ambiguity warnings
All errors are logged to stderr (important for STDIO servers).
## Best Practices
1. **Model Names**: Avoid reserved names (`list`, `user`, `system`, `function`)
2. **Explicit Types**: Always specify `field_type` and `input_type` explicitly - don't rely on automatic detection
3. **List Fields**: For dropdown and multiSelect, always provide `validation.fixed_list_elements` and `validation.fixed_list_element_type`
4. **Nested Fields**: Set `is_object_field=true` for `object` and `repeated` field types
5. **Parent Fields**: Use `parent_field` parameter when adding nested fields to object or repeated fields
6. **API Keys**: For remote deployments, API keys are project-dependent and must be provided per-request, not stored as Cloudflare secrets
7. **Relations**: Use `add_relation` to create bidirectional relationships between models
8. **List filters by related model**: On public `*List` queries, use **`relation`** — not **`connection`** — to filter by linked models (has_one, has_many, M:N). Example: `studentList(relation: { class: { _id: { eq: "…" } } })` when student has one class. Read `apito://project-query-guide` for full examples; `connection` is only for advanced anchor-document traversal.
## Public GraphQL: `relation` vs `connection` list filters
When calling the **public** project GraphQL API (not MCP’s system `getModelData`):
| Goal | Use | Example |
|------|-----|---------|
| Students in a given class | **`relation`** | `studentList(relation: { class: { _id: { eq: "01KW4M8K7WR57HB3G0DWN48CTZ" } } })` |
| Students where class code is C100 | **`relation`** | `studentList(relation: { class: { code: { eq: "C100" } } })` |
| Articles with a tag | **`relation`** | `articleList(relation: { tag: { _id: { eq: "…" } } })` |
| Traverse from a known parent doc with direction metadata | **`connection`** (advanced) | See `apito://project-query-guide` |
**`connection`** is for a specific graph-navigation case (anchor `_id` + `connection_type` + `to_model` + `relation_type`). For everyday “filter this list by my relation to model X”, always use **`relation`**.
```graphql
query MyQuery {
studentList(relation: { class: { _id: { eq: "01KW4M8K7WR57HB3G0DWN48CTZ" } } }) {
id
data { name }
class {
id
data { name code }
}
}
}
```
### Cursor IDE
Add to `~/.cursor/mcp.json`:
**Local (STDIO):**
```json
{
"mcpServers": {
"apito": {
"command": "npx",
"args": [
"tsx",
"/Users/your-username/Projects/apito/apito-mcp/src/index.ts"
],
"env": {
"APITO_API_KEY": "apt_your-access-token-here",
"APITO_GRAPHQL_ENDPOINT": "https://api.apito.io/system/graphql"
}
}
}
}
```
**Remote (Cloudflare Workers):**
```json
{
"mcpServers": {
"apito-production": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://apito-mcp.apito.workers.dev/sse",
"--header",
"Authorization:Bearer ${APITO_API_KEY}"
],
"env": {
"APITO_API_KEY": "apt_your-access-token-here"
}
}
}
}
```
**Note**: The access token is passed via the `Authorization: Bearer` header using the `--header` flag. The `env.APITO_API_KEY` environment variable is automatically substituted by `mcp-remote`. Tokens must be unified `apt_` access tokens (Console → Access Token) — legacy `mcp-`/`cli-`/`sdk-` prefixed keys are retired.
Restart Cursor after configuration.
### VS Code
Install the [MCP extension](https://marketplace.visualstudio.com/items?itemName=modelcontextprotocol) and configure in settings:
```json
{
"mcp.servers": {
"apito": {
"command": "npx",
"args": ["tsx", "/path/to/apito-mcp/src/index.ts"],
"env": {
"APITO_API_KEY": "your-api-key-here"
}
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%/Claude/claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"apito": {
"command": "npx",
"args": ["tsx", "/path/to/apito-mcp/src/index.ts"],
"env": {
"APITO_API_KEY": "your-api-key-here",
"APITO_GRAPHQL_ENDPOINT": "https://api.apito.io/system/graphql"
}
}
}
}
```
### ChatGPT / OpenAI
ChatGPT doesn't directly support MCP, but you can use the remote Cloudflare Workers endpoint via HTTP/SSE. You'll need to use an MCP proxy or client library.
### Other MCP Clients
Any MCP-compatible client can connect to:
- **Local**: STDIO transport via `npx tsx src/index.ts`
- **Remote**: SSE transport via `https://apito-mcp.apito.workers.dev/sse` (requires `mcp-remote` proxy)
### Environment Variables
**For Remote Deployments (Cloudflare Workers):**
The API key is **not** stored as a Cloudflare Worker secret. It must be provided by the MCP client in each request. This allows the same worker to serve multiple projects.
Optional: Set GraphQL endpoint secret if you need to override the default:
```bash
# Set GraphQL endpoint (optional, defaults to https://api.apito.io/system/graphql)
npx wrangler secret put APITO_GRAPHQL_ENDPOINT --env production
```
**For Local Deployments (STDIO):**
Set environment variables in your MCP client configuration (see examples above).
### Testing the Connection
You can test the MCP server connection using the MCP Inspector or by checking if tools are available in your client. The server should expose schema tools (`create_model`, `add_field`, …), data tools (`get_data`, `upsert_data`, …), schema versioning read tools, and **platform tools** (`list_tenants`, `search_app_users`, `list_roles`, …). Run `pnpm test:tenant-users` for tenant/user smoke tests and `pnpm test:versioning` for schema versioning tests.
## License
MIT
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues