MCP App Store
by KeenEthics
Productivity
Anchor icon

Anchor

by Anchor Technologies, Inc.

Overview

Anchor gives ChatGPT a shared drive where it can create files and organize them in folders. ChatGPT can read and write files in HTML, Markdown (MD), JSON, CSV, and other text formats, design typed tables, and query them with SQL. It can move files around, make copies, and keep everything organized. Every file, table, and folder it creates is shareable with your team, while Anchor handles storage, permissions, and versioning. Log in once, with no API keys and no setup, and ChatGPT only ever touches what you can access.

Tools

add_table_column

ChatGPT
Add a new column to an existing Anchor table. This is the column-level counterpart to create_table — same column schema, but for a single column added after the table already exists. Example input: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "column": { "identifier": "project_id", "display_name": "Project", "type": "INTEGER", "isUnique": false, "isRequired": false, "foreignKeyTableIdentifier": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60002", "foreignKeyOnDelete": "CASCADE" } } Example with required ENUM column and defaultValue: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "column": { "identifier": "priority", "display_name": "Priority", "type": "ENUM", "isUnique": false, "isRequired": true, "possibleValues": ["P0", "P1", "P2"], "defaultValue": "P2" } } Under the hood this runs an ALTER TABLE … ADD COLUMN on the underlying PostgreSQL table: ALTER TABLE anchor_tables."<table_uuid>" ADD COLUMN <identifier> <PG_TYPE> [NOT NULL] [UNIQUE] [DEFAULT <value>::<PG_TYPE>] [CHECK (...)] [REFERENCES <fk_table>(record_id) ON DELETE CASCADE|SET NULL]; Type mapping: STRING → TEXT, NUMBER → DOUBLE PRECISION, INTEGER → BIGINT, BOOLEAN → BOOLEAN, ENUM → TEXT (with CHECK constraint), DATE → DATE, DATETIME → TIMESTAMPTZ, EMAIL / CREATED_BY_EMAIL → TEXT. RULES: - The column uses exactly one of three forms: standard, ENUM (requires possibleValues), or foreign key (requires foreignKeyTableIdentifier + foreignKeyOnDelete). - Cannot add a column whose identifier already exists in the table. - If the column is required (isRequired: true), defaultValue MUST be set — existing rows are backfilled with it via DEFAULT. The column will be rejected without it. - If the column is not required, defaultValue is optional — existing rows get NULL when omitted. - Unique columns: when isUnique is true, isRequired must be false and defaultValue must not be set — existing rows are filled with NULL (which satisfies uniqueness). - CREATED_BY_EMAIL columns: defaultValue must NOT be set (the enforcement trigger overrides it on every INSERT/UPDATE with the caller's email). New rows inserted after the column is added are always populated by the trigger. Existing rows cannot be backfilled — the trigger only fires on INSERT/UPDATE — so isRequired may only be true if the table currently has zero rows. On a non-empty table, isRequired must be false; existing rows will be NULL while new rows are auto-filled by the trigger.

build_app

ChatGPT
Builds an Anchor app from the contents of a temporary folder. Two modes: NEW APP — pass temporary_folder_id + name + destination_folder_id. Creates a new APP file in destination_folder_id, which must be in the temporary folder's org, and builds+publishes v1. NEW VERSION — pass temporary_folder_id + app_id. Builds a new version of app_id from scratch using every file currently in the temporary folder — nothing carries over from the previous version except what you put there yourself (e.g. via unpack_file first). Auto-publishes iff the app isn't shared with anyone else; otherwise the owner must publish it from the app's management page. Required temporary-folder files: index.tsx, favicon.json. Optional: index.css, functions.json, plus any number of helper modules index.tsx imports. Reserved build-output names (bundle.html, fetch.ts) are rejected if present — those are emitted by the build pipeline, not authored by you. See write_file's description for exactly how to author index.tsx, functions.json, and favicon.json (including the JSON-escaping rules for functions.json's SQL strings). index.tsx is bundled, NOT typechecked. The ./fetch helpers take positional arguments in inputSchema order — addTask(title) not addTask({ title }), listTasks() not listTasks({}). An object argument compiles, publishes, and then fails on every call with INVALID_REQUEST, so re-read every ./fetch call in index.tsx before you build. Full contract: write_file's description. index.tsx is the bundle's entry module and is responsible for mounting. The served page contains an empty <div id="root"></div> and runs your compiled bundle as a module script; the platform adds no bootstrapping code. End index.tsx with createRoot(document.getElementById('root')!).render(<App />), importing createRoot from 'react-dom/client'. A file that only exports a component produces a blank page, and the build rejects it with NO_ROOT_MOUNT. functions.json declares this version's complete data-access footprint via dependencies (TABLE and OTHER entries) plus the SQL functions that actually touch them. The build verifies the declaration is exactly right, in both directions, before it will succeed: - Every table or OTHER file a SQL function touches must be declared in dependencies, at the level actually used (READ vs WRITE) — an undeclared or under-declared touch fails with UNDECLARED_TABLE_ACCESS. - Every declared TABLE entry must actually be used at that level by some function — an unused or over-declared entry fails with UNUSED_TABLE_DEPENDENCY. An app can never ask for more access than it uses. - Every declared entry requires YOUR OWN direct access at that level before the build can succeed (TABLE READ needs your direct CONSUMER access, TABLE WRITE needs your direct CONTRIBUTOR access, OTHER needs your direct CONSUMER access) — building never grants access on your behalf. - Every OTHER entry must exist, be an OTHER file (never APP or TABLE), and not sit inside any temporary folder. - An image the app displays is such an OTHER entry: declare the file (no functions entry needed — the unused-dependency check is TABLE-only) and render it with anchorOtherFileFetch(fileId) from ./fetch as the <img src>. Never base64-encode an image into index.tsx. Building never grants anyone access to anything — it only records the requirement. If the app declares dependencies, it is unusable by everyone right after the build, including you, until each person separately consents to what it does with their access (and, for anyone lacking the access itself, requests it from the owner first); direct the user to the app's management page (app_management_link) for that flow. An app that declares no dependencies has nothing to consent to, and is reachable as soon as someone has access to it. On a successful build, the temporary folder (and everything in it) is deleted — do not try to write_file/cp/mv into temporary_folder_id afterward, it no longer exists. A failed build leaves the temporary folder in place so you can fix the problem and retry without re-writing every file. There is no other cleanup …

commit_document

ChatGPT
Turn a temporary folder of files into an Anchor document, or into a new version of one that already exists. An Anchor document is one file made of several: one HTML file per page, a config.json listing them, and whatever CSS and images they use. There is no build step — the files are served as written. To make one: mkdir(temporary: true), write the files into that folder with write_file, copy in any images with cp, then call this — naming a normal folder in the staging folder's org as the destination, never the staging one. To change an existing one: unpack_file first, edit the files it writes out, then call this with that folder and the base_version unpack_file returned. To restore an older version, unpack_file it by number and call this with the folder unchanged, the same way: it becomes a new version with the old content. Everything validates before anything is stored, so a rejected commit creates no version and leaves the folder untouched for you to fix and retry. A successful one deletes the folder. Write ordinary HTML. One complete HTML document per page, no framework. Anchor supplies everything around the page: page navigation, comments, version history, printing. Do not build navigation, in-page tabs, or anything that hides content behind a click — <details> included. What you write is also what gets printed and thumbnailed. Let height follow content, and never hide anything behind scrolling. Put the content in the markup — search engines, text extraction and version diffs see only the DOM. JavaScript is for interaction, never for producing text. Every element inside <body> needs an id, unique across every file in the document. This is checked and it is literal — a layout wrapper needs one exactly as much as a paragraph does. Inside an <svg>, only the <svg> itself and the tags that carry words (<text>, <tspan>, <textPath>, <title>, <desc>); shapes and plumbing do not. Keep ids the same across edits wherever the content has not changed: comments anchor to them, and a changed id is a lost comment. Renaming an id while its element stays is the easy mistake — it feels like tidying, and to a comment it is a deletion. If you restructure, keep the id on any element whose words survive, even where it has moved or been re-tagged. Committing names every id that is no longer present, so check each one and put back any that should have survived. Wrap each sentence in a <span>, so a comment has a natural unit to attach to. No browser storage — no localStorage, no cookies. The page has no internet. Everything it loads is a file inside the document, by relative path — Google Fonts is the only exception, and data: URIs are refused. fetch, XHR and WebSockets fail too. Links are the one way out: they may point anywhere, and Anchor handles the click, so target and window.open do nothing. Tags, attributes and ARIA roles are allowlisted, and narrower than you would guess: no form controls, iframes, canvas or on handlers, and <label>, <progress> and interactive roles are out too — except the few below that a reader's state lives on, which are the only controls a document has, whether or not it keeps what the reader does. A rejection lists everything that would have been accepted. Ordinary CSS is fine, inline in a <style> block or in a .css file beside the page. Charts and diagrams are SVG, which stays readable to search engines, diffs and printers. config.json says what the pages are, in the order they appear: { "kind": "document", "tabs": [ { "id": "k3f9x2", "title": "Introduction", "file": "page-1.html" }, { "id": "m7p2qd", "title": "How it works", "file": "page-2.html" } ] } Each entry: `id` is a stable handle, `title` is what Anchor's chrome shows, `file` is a .html file at the root of the folder. An `id` is opaque and permanent.* Invent about six characters with no relation to the title — a letter, then letters or digits, like k3f9x2 — and never change it again. It is what a link to this page points at and what a reader's saved state is filed under, so changing one silently breaks both: a link that used to open here lands o…

commit_presentation

ChatGPT
Turn a temporary folder of files into an Anchor presentation, or into a new version of one that already exists. An Anchor presentation is one file made of several: one HTML file per slide, a config.json listing them, and whatever CSS and images they use. There is no build step — the files are served as written. To make one: mkdir(temporary: true), write the files into that folder with write_file, copy in any images with cp, then call this — naming a normal folder in the staging folder's org as the destination, never the staging one. To change an existing one: unpack_file first, edit the files it writes out, then call this with that folder and the base_version unpack_file returned. To restore an older version, unpack_file it by number and call this with the folder unchanged, the same way: it becomes a new version with the old content. Everything validates before anything is stored, so a rejected commit creates no version and leaves the folder untouched for you to fix and retry. A successful one deletes the folder. Write ordinary HTML. One complete HTML document per slide, no framework. Anchor supplies everything around the page: slide navigation, comments, version history, printing. Do not build navigation, in-page tabs, or anything that hides content behind a click — <details> included. What you write is also what gets printed and thumbnailed. Let height follow content, and never hide anything behind scrolling. Put the content in the markup — search engines, text extraction and version diffs see only the DOM. JavaScript is for interaction, never for producing text. Every element inside <body> needs an id, unique across every file in the presentation. This is checked and it is literal — a layout wrapper needs one exactly as much as a paragraph does. Inside an <svg>, only the <svg> itself and the tags that carry words (<text>, <tspan>, <textPath>, <title>, <desc>); shapes and plumbing do not. Keep ids the same across edits wherever the content has not changed: comments anchor to them, and a changed id is a lost comment. Renaming an id while its element stays is the easy mistake — it feels like tidying, and to a comment it is a deletion. If you restructure, keep the id on any element whose words survive, even where it has moved or been re-tagged. Committing names every id that is no longer present, so check each one and put back any that should have survived. Wrap each sentence in a <span>, so a comment has a natural unit to attach to. No browser storage — no localStorage, no cookies. The page has no internet. Everything it loads is a file inside the presentation, by relative path — Google Fonts is the only exception, and data: URIs are refused. fetch, XHR and WebSockets fail too. Links are the one way out: they may point anywhere, and Anchor handles the click, so target and window.open do nothing. Tags, attributes and ARIA roles are allowlisted, and narrower than you would guess: no form controls, iframes, canvas or on handlers, and <label>, <progress> and interactive roles are out too — except the few below that a reader's state lives on, which are the only controls a presentation has, whether or not it keeps what the reader does. A rejection lists everything that would have been accepted. Ordinary CSS is fine, inline in a <style> block or in a .css file beside the page. Charts and diagrams are SVG, which stays readable to search engines, diffs and printers. config.json says what the slides are, in the order they appear: { "kind": "presentation", "canvas": { "width": 1280, "height": 720 }, "tabs": [ { "id": "k3f9x2", "title": "Introduction", "file": "slide-1.html" }, { "id": "m7p2qd", "title": "How it works", "file": "slide-2.html" } ] } Each entry: `id` is a stable handle, `title` is what Anchor's chrome shows, `file` is a .html file at the root of the folder. An `id` is opaque and permanent.* Invent about six characters with no relation to the title — a letter, then letters or digits, like k3f9x2 — and never change it again. It is what a link to this slide points at and what a reader's saved state is filed under, …

cp

ChatGPT
Copy a file. Only files can be copied (not folders). The copy gets a new ID, the caller becomes the owner, and no existing access roles are carried over. For tables, the full PostgreSQL table (schema + data) is duplicated. For documents and presentations, only the latest version is copied and becomes version 1 of the copy, with the tables it reads declared again. For apps, the full version history (every version's source, dependencies, and built bundle) is duplicated and the copy gets a fresh slug; copying an app requires contributor access to the source, since the copy's owner can unpack its source code. Requires read access to the source file and write access to the destination folder. Only in-org copies are allowed. An OTHER file may be copied into, out of, or between temporary folders (the scratch folders mkdir creates with temporary: true); nothing else — a table, an app, a document or a presentation — can enter one.

create_table

ChatGPT
Create a new table in Anchor (File type: TABLE). Tables are structured data stores with typed columns. Under the hood, this creates a PostgreSQL table whose name is the returned UUID. The table is displayed to users without exposing database concepts, but all column configurations (types, constraints, foreign keys) map directly to PostgreSQL features — the Postgres type is noted next to each config option. Tables are designed to work together through normalization: - Master data entities (Customers, Products, Employees, Vendors, Locations, Categories, etc.) are the core reusable "nouns" of an organization. - Transactional tables (Orders, Invoices, Appointments, Transactions) reference master tables via foreign keys (foreignKeyTableIdentifier) rather than embedding fields like "customer_name" or "product_name" directly. - Existing tables in the organization can be inspected with ls and describe_table to identify foreign key targets and avoid duplicating master data. SCHEMA RULES: - Every table must include a "record_id" column (INTEGER, isUnique: true, isRequired: true). This column is auto-generated (GENERATED ALWAYS AS IDENTITY) — do NOT include record_id values in subsequent INSERT queries. - isRequired: true needs a defaultValue of the column's type (except record_id and CREATED_BY_EMAIL): "" for STRING, 0 for NUMBER/INTEGER, false for BOOLEAN, a date like "2024-01-01" for DATE. No sensible default? Use isRequired: false. - Each column has an identifier (snake_case, immutable, used in SQL) and a display_name (human-friendly, shown in UI). - Each column uses exactly one of three forms: standard, ENUM (requires possibleValues), or foreign key (requires foreignKeyTableIdentifier + foreignKeyOnDelete). - columns is an array of column definitions (preferred). A legacy map keyed by identifier is also accepted. - The full column schema is set at creation time. After creating, use describe_table to inspect the schema. Example input: { "folder_id": "0195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "name": "Tasks", "description": "Tracks work items and their status", "columns": [ { "identifier": "record_id", "display_name": "Record ID", "type": "INTEGER", "isUnique": true, "isRequired": true }, { "identifier": "name", "display_name": "Name", "type": "STRING", "isUnique": false, "isRequired": true, "defaultValue": "" }, { "identifier": "due_date", "display_name": "Due Date", "type": "DATE", "isUnique": false, "isRequired": false }, { "identifier": "priority", "display_name": "Priority", "type": "ENUM", "isUnique": false, "isRequired": true, "possibleValues": ["P0", "P1", "P2"], "defaultValue": "P2" } ] }

delete_table_column

ChatGPT
Permanently delete a column from an existing Anchor table. The deletion is immediate and irreversible — all data in the column across every row is destroyed and cannot be recovered. This is a destructive action — confirm with the user before deleting a column. Under the hood this runs an ALTER TABLE … DROP COLUMN on the underlying PostgreSQL table: ALTER TABLE anchor_tables."<table_uuid>" DROP COLUMN <column_identifier>; The "record_id" column cannot be deleted — it is the primary key.

describe_table

ChatGPT
Get a table's full schema, metadata, and row count. Use this to understand a table's structure before querying or modifying it with query_table, add_table_column, or delete_table_column. Does not return row data — use query_table for that. For tables with many rows, use query_table to peek at a sample or write aggregated SQL queries rather than fetching all rows. Example input: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" } Returns: - Row count: total number of rows in the table. For large tables, use query_table with LIMIT to peek at sample data, or write aggregated SQL queries (COUNT, SUM, AVG, GROUP BY, etc.) instead of selecting all rows. - Column definitions: each column has an identifier (snake_case, immutable — use this in SQL queries) and a display_name (human-friendly — use this when communicating with the user). Also includes types, constraints, allowed values, and foreign key references. - Timestamps: creation and last update times.

download_file_presigned_url

ChatGPT
Returns a presigned URL to download a file's raw bytes. GET it yourself (e.g. curl). Preferred over read_file for any file when you can reach the URL. If the GET fails, read_file is the fallback: it only works for textable files, and returns extracted text for PDF, DOCX, PPT, and XLS. Files that are not textable need to be downloaded from the Anchor UI. The URL expires in 15 minutes and allows GET only.

ls

ChatGPT
Map of your orgs, folders, and files. A good starting point when the layout isn't already known. If you're not sure where to begin, omit target (or pass {by:"overview"}) — it lists every org you belong to plus "Shared with Me", each expanded breadth-first under number_of_items (default 20). To scope in, set target to exactly one tagged variant: - {by:"overview"} → every org + "Shared with Me" (the default). - {by:"org", org_id} → that org's root folders, expanded breadth-first. - {by:"folder", folder_id} → that folder, then its children, expanded breadth-first. - {by:"file", file_id} → one file's details (leaf — no recursion). For an APP file, also lists its source file names — read-only here; call unpack_file to get an editable copy in a temporary folder, then read_file/write_file on the copies there. - {by:"shared_with_me"} → folders and files shared with you from outside your orgs. - {by:"slug", slug} → resolve an app from its serving URL (e.g. "a1b2" or "a1b2-v3"); behaves like {by:"file"} on the resolved app. A bare slug needs consumer access; a -vN suffix needs contributor access (it previews an unpublished version). A listing that shows an app as a row does not repeat its source-file list — call ls again with {by:"file", file_id} (or {by:"slug"}) on that app to see it. Reading the output (indented text, not JSON): - Lines starting with "- " are items. Lines without "- " are properties of the nearest item above them. - Indentation is 2 spaces per level; a child sits one level deeper than its parent. - Folders end with "/". Files do not. Orgs are prefixed with "[Org] ". "Shared with Me" has no id. - Every item shows its uuid after " -- " (except "Shared with Me"). - "# items: N" under a folder/org is its total visible child count. If fewer rows appear beneath it, the rest were trimmed by number_of_items — call ls with target {by:"folder",folder_id} to drill in. - A trailing "... N more orgs not shown" line means the overview hit number_of_items before listing every org. "Shared with Me" is always shown; raise number_of_items or scope in with {by:"org",org_id}. Example: - [Org] Neural Bridge -- 019385a0-0000-0000-0000-000000000001 items: 5 - Home/ -- 019385a0-0000-0000-0000-000000000002 items: 2 - file1 -- 019385a0-0000-0000-0000-000000000010 - file2 -- 019385a0-0000-0000-0000-000000000011 Tunables (all optional): - number_of_items (default 20): global budget across orgs + folders + files. - show_org_details / show_folder_details / show_file_details / show_shared_with_me_details: add property blocks (timestamps, type, sizes, versions, …) per item type. - hide_orgs / hide_folders / hide_files / hide_shared_with_me: drop a whole category from the output and from the budget. - show_temporary (default false): include temporary folders when browsing an org/overview. A known temporary folder id always resolves via {by:"folder"} regardless of this flag — it only gates unprompted discovery. IDs appear in Anchor URLs: - https://anchor.cc/org/$org_id - https://anchor.cc/folder/$folder_id - https://anchor.cc/file/$file_id — also /table/, /app/, /document/ and /presentation/$file_id - https://anchor.cc/shared-with-me — target {by:"shared_with_me"}

mkdir

ChatGPT
Create a new folder. Pass folder_id to create a subfolder inside an existing folder, or org_id to create a folder at the org root. Requires permission at the parent location. Set temporary: true to create a temporary folder instead — private scratch space, visible only to you, always at the org root (org_id required, folder_id forbidden). It is where files are staged before build_app, commit_document or commit_presentation. A temporary folder persists until one of those succeeds against it and deletes it; there is currently no other way to remove one, so an abandoned temporary folder sits there permanently — only create one when you are about to populate and commit it.

mv

ChatGPT
Move or rename a file or folder. Source can be a file or folder. Destination can be an org (root level) or a folder — but files cannot be moved directly under an org, only into folders. Requires editor (or owner) access on the source and on a destination folder; moving a folder to an org root only requires membership in that org. An OTHER file may be moved into, out of, or between temporary folders (the scratch folders mkdir creates with temporary: true); apps and tables can never enter a temporary folder, and a temporary folder itself can never be a move source or destination as a folder. Valid argument combinations (set exactly one source; unused fields must be omitted or null): • Move a file into a folder: { file_id, destination_folder_id, new_name? } • Rename a file in place: { file_id, new_name } • Move a folder into a folder: { folder_id, destination_folder_id, new_name? } • Move a folder to an org root: { folder_id, destination_org_id, new_name? } • Rename a folder in place: { folder_id, new_name } Constraints: set exactly one of file_id / folder_id. Provide at least one of destination_folder_id, destination_org_id, or new_name. destination_org_id is only valid for folder sources (files cannot live at the org root). destination_folder_id takes precedence over destination_org_id when both are set.

query_table

ChatGPT
Run a SQL query against Anchor tables. This executes real SQL against the underlying PostgreSQL database — every Anchor table is a Postgres table whose name is its UUID. ALLOWED STATEMENTS: SELECT, INSERT, UPDATE, DELETE (data operations only). BLOCKED STATEMENTS: CREATE, ALTER, DROP, TRUNCATE, triggers, indexes, and all other schema/DDL operations — these are rejected. Use create_table, add_table_column, or delete_table_column for schema changes. ONE STATEMENT PER CALL: - Exactly one SQL statement per call: no ";"-stacked statements, no BEGIN/COMMIT wrappers (each call already runs in its own transaction). - Bulk-insert with a single multi-row VALUES list. - For upserts use INSERT ... ON CONFLICT (MERGE is not allowed). - Inline literal values; bind parameters ($1) are not supported. ACCESS CONTROL: - Viewer access (read-only): SELECT queries only. - Editor access (read + write): SELECT, INSERT, UPDATE, and DELETE. If a user only has viewer access, write queries will be rejected with an access error. REFERENCING TABLES AND COLUMNS: - Tables are referenced by UUID as a double-quoted identifier: SELECT FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" - Columns are referenced by their snake_case identifier (no quoting needed). - Always call describe_table first to discover column identifiers, types, and constraints before writing queries. - Every query must reference at least one Anchor table (a bare SELECT 1 is rejected). - System catalogs (pg_catalog, information_schema) are not queryable; use describe_table for schema discovery. CROSS-TABLE JOINS: You can JOIN multiple tables in a single query as long as all tables belong to the same organization. Cross-org queries are not allowed. RECORD_ID: The record_id column is auto-generated (GENERATED ALWAYS AS IDENTITY). Never include record_id in INSERT statements — it is assigned automatically. You can use record_id in WHERE clauses, JOINs, and SELECT lists. Add RETURNING record_id to an INSERT to get the generated ids back. CREATED_BY_EMAIL COLUMNS: Columns of type CREATED_BY_EMAIL are enforced server-side by a trigger that overrides the column with the calling user's email on every INSERT and UPDATE. You usually don't need to include these columns in writes — INSERT without them and the trigger will populate them automatically. If you do include a CREATED_BY_EMAIL column in an INSERT or UPDATE, you MUST set it to your own email (the email of the user making the request); any other value will be silently overwritten with your email by the trigger, so inserting or updating a peer's email will not work. DESTRUCTIVE OPERATIONS (UPDATE, DELETE): Before running UPDATE or DELETE queries, ALWAYS confirm with the user first. Describe what rows will be affected (e.g. "This will delete 3 rows where status = 'archived'") and wait for explicit approval. Data modifications cannot be undone. EXAMPLES: Select all rows: SELECT FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" Select with filter: SELECT name, email FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" WHERE status = 'active' ORDER BY name Insert a single row (omit record_id): INSERT INTO "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" (name, email, status) VALUES ('Alice', 'alice@example.com', 'active') RETURNING record_id Insert multiple rows: INSERT INTO "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" (name, email, status) VALUES ('Alice', 'alice@example.com', 'active'), ('Bob', 'bob@example.com', 'pending') Join two tables (same org): SELECT o.order_date, c.name AS customer_name, o.total FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" o JOIN "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60002" c ON o.customer_id = c.record_id WHERE o.total > 100 Aggregate query: SELECT status, COUNT(*) AS count FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" GROUP BY status Update rows (confirm with user first): UPDATE "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" SET status = 'archived' WHERE last_login < '2024-01-01' Delete rows (confirm with user first): DELETE FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" WHERE status = 'archived'

read_file

ChatGPT
Reads the text content of an OTHER file where textable is true. Text formats (Markdown, HTML, JSON, CSV/TSV, XML/YAML, plain text, source code) come back verbatim, so oldString edits via write_file match exactly. PDF, DOC/DOCX, PPT/PPTX, XLS/XLSX, RTF come back as extracted plain text. Prefer download_file_presigned_url when you can reach its URL; use read_file when you want that extracted text rather than raw bytes, or need a file's current content for a surgical edit. TABLE files: describe_table or query_table. Apps, documents and presentations: unpack_file, then read_file the sources.

rm_file

ChatGPT
Permanently delete any Anchor file — a table, an app, a document, a presentation, or an other-file (HTML, Markdown, CSV, etc.). The deletion is immediate and irreversible — there is no trash or undo. For a TABLE, the underlying data is dropped along with the file; for everything else, the stored content goes with it, including every version of a document, presentation or app. Only an owner of the file (or of a parent folder) can delete a file. This is a destructive action — confirm with the user before deleting files you did not create.

unpack_file

ChatGPT
Writes an Anchor app, document or presentation back out as editable files in a new temporary folder, so it can be read, changed and saved as a new version. Returns the folder id, every file written with the id read_file and write_file need, and which tool saves it: build_app for an app, commit_document or commit_presentation otherwise. View access is enough, which makes this the way to read how an app works or what a document says; a viewer of an app gets its published version only. Saving the folder as a new version still needs edit access, and can_save_new_version says whether you have it. For tables use query_table; for anything else use read_file. Version: null unpacks an app's published version — what its users see — and a document's or presentation's current one. Pass a number for any other version; ls on the file shows the latest. To restore an older version of a document or presentation, unpack it by number and commit the folder unchanged with the base_version this returns. An app's version is unpacked as its own source files only: build artifacts (bundle.html, fetch.ts) are regenerated by build_app, and OTHER files the app declares in functions.json stay where they are, referenced by file_id. build_app can save the folder as a new version of the same app, or fork it into a new one. Edit the folder with write_file, cp, mv and rm_file. The folder is deleted when the save succeeds; an abandoned one stays, invisible to the user and with no anchor link, so do not direct them to it.

upload_file_presigned_url

ChatGPT
Returns a presigned URL to upload file bytes to a staging key in one folder. This is the preferred way to move any file content into Anchor (html, md, json, code, binaries), but it works only if you can make HTTP requests to the URL. Confirm that with step 2 before generating anything large. Flow: 1. Call this tool with the folder the file will live in. For a replacement that is the existing file's parent folder — ls the file to read its parent_folder_id. You get a URL valid for 2 hours, bound to that folder. 2. PUT a tiny test body to it (e.g. curl -X PUT --data-binary 'hello' "<url>"). If that fails, use write_file with text_content instead, typed straight into the tool call argument, not into a local file first (text formats only; binary content has no fallback). 3. PUT the real bytes to the same URL. 4. Call write_file with presigned_url to turn the bytes into a file: { folder_id, name, presigned_url } for a new file, { file_id, presigned_url } to replace an existing file's content. The folder_id must be the folder this URL was minted for, and a file_id's parent folder must be that same folder — otherwise the write is rejected. One URL per file: write_file consumes the URL's upload, so call this tool again for the next file.

write_file

ChatGPT
Each file becomes a live, shareable page (its view_link), so prefer writing to Anchor whenever the user will want to view, keep, or share what you produce. Create or edit one or more OTHER files in a single call. Pass writes: an array where each entry independently creates a new file (folder_id + name) or edits an existing one (file_id), with content sourced either inline (text_content) or from a prior presigned upload (presigned_url) — the two are mutually exclusive per entry. Malformed entries are rejected by schema validation before anything runs (the whole call fails — fix the entry and retry). Once the batch starts, entries are applied sequentially in array order; each gets its own per-item result so a single runtime failure (e.g. NO_ACCESS, EDIT_CONFLICT) does not halt the rest of the batch — the response array preserves the same length and order as the input. Two content sources, presigned_url is the default: • presigned_url: the preferred source for any file type, including text formats like html and md, whenever you can reach the URL it returns. Mime is detected from the uploaded bytes, extension as fallback. • text_content: the fallback when the network probe fails, or the source for a surgical oldString/newString edit, since presigned_url only ever replaces a file's entire content, never a diff. Type the content straight into the tool call argument, not into a local file first; the only reason to write a local file is to PUT it to a presigned URL. Text formats only: Markdown, plain text, JSON, HTML, CSV/TSV, XML, YAML, TOML, SQL, and source code (.tsx, .ts, .js, .css, .py, .go, .rs, .java, .sh, etc.). Mime is detected from content + extension on every create and full rewrite; anything else is rejected with NOT_TEXTABLE. Per-entry shape — exactly one of the following five forms (enforced by the input schema; each form accepts only its listed fields): • Create new, inline: { folder_id, name, text_content: { newString } }. • Edit existing — surgical: { file_id, text_content: { newString, oldString, replaceAll? } }. oldString must match exactly once unless replaceAll is true; include surrounding context to keep oldString unambiguous. • Edit existing — full rewrite: { file_id, text_content: { newString, rewrite: true } }. Replaces the entire file with newString. • Create new, from a presigned upload: { folder_id, name, presigned_url }. Uses the bytes you PUT to a URL from upload_file_presigned_url, minted for this same folder_id. • Replace existing, from a presigned upload: { file_id, presigned_url }. Always replaces the whole file, like rewrite: true. Mint the URL with the folder the file lives in. Presigned writes. Get a URL from upload_file_presigned_url, passing the folder the file lives in (for a replacement, the folder the existing file is already in). PUT a tiny test body to it first, then PUT the real bytes, then call write_file with that URL. If the test PUT fails you cannot reach the URL: use text_content instead. The URL is bound to the folder it was minted for: a folder_id that differs, or a file_id whose parent folder differs, is rejected with UPLOAD_FOLDER_MISMATCH. One URL per file — write_file consumes the upload, so mint a new URL for the next file. Temporary folders as a write target. A temporary folder (created by mkdir with temporary: true, or by unpack_file) is a normal write target — create and edit files inside it exactly as in any other folder, same create / surgical-edit / full-rewrite mechanics (text_content or presigned_url). Two rules apply only inside a temporary folder: sibling names are compared case-insensitively (Index.tsx collides with index.tsx, rejected with TEMPORARY_FOLDER_NAME_CONFLICT), and a temporary folder's total content is capped at 100 MB (rejected with TEMPORARY_FOLDER_FULL). This is where an app's source is authored before calling build_app: index.tsx, favicon.json, functions.json, optionally index.css, plus any helper modules. Authoring functions.json and favicon.json. Both are plain JSON text written the same way as any other file. Inside a tem…

add_table_column

Claude

cp

Claude

create_table

Claude

delete_table_column

Claude

describe_table

Claude

list_orgs

Claude

ls

Claude

mkdir

Claude

mv

Claude

query_table

Claude

read_file

Claude

rm_file

Claude

write_file

Claude

Capabilities

Writes

App Stats

33

Tools

ChatGPT, Claude

Platforms

Works with

ChatGPT
Claude

Data refreshed daily