Basalt docs
basaltapp.io

MCP server

Basalt ships a first-party MCP server, so an agent — Claude, an agent CLI, anything that speaks the protocol — can read and write a workspace without anyone writing glue code.

There is nothing to install. The server runs on your Basalt instance and answers at https://app.basaltapp.io/mcp; you point an agent at that address and give it a token.

It is a thin layer over the public API: the endpoint holds no permission logic of its own but makes ordinary API requests with your credential, so an agent sees and changes exactly what you can. Pages cross the wire as canonical Markdown.

Setting it up

Mint a token first: Settings → Tokens, in the workspace you want the agent to work in. Give it content:read if the agent should only read, content:write if it should also write. The secret is shown once.

A token minted here works here — it reads and writes what Basalt shows you, and nothing the other apps hold. That also means you do not have to tell the agent which workspace to use: the token names one, and the endpoint reads it from there.

In an MCP host's configuration file:

{
  "mcpServers": {
    "basalt": {
      "type": "http",
      "url": "https://app.basaltapp.io/mcp",
      "headers": {
        "Authorization": "Bearer basalt_pat_…"
      }
    }
  }
}

Hosts differ in how they spell this; what they all need is the address and the Authorization header. In Claude Code, for example:

claude mcp add --transport http basalt https://app.basaltapp.io/mcp \
  --header "Authorization: Bearer basalt_pat_…"

If your account is in several workspaces and you want to name one explicitly, address https://app.basaltapp.io/mcp/w/<workspace id> instead. A token may only ever name its own workspace, so the two have to agree.

Keeping an agent read-only is the token's job, not a setting: a content:read token is refused by the server on every write, which is a stronger guarantee than a switch on the client side.

Adding it as a connector

Hosts that offer no header field — claude.ai among them — add the endpoint as a connector and sign you in instead. Paste the same address, https://app.basaltapp.io/mcp, into the connector dialog; the rest is a sign-in and one screen asking whether you want to connect. Nothing to copy, no token to keep.

A connector is broader than a token, and the screen says so before you agree:

  • It covers every workspace you are a member of, not one.
  • It reads and writes what you can. There is no read-only connector; mint a content:read token for an agent that should only look.
  • It still sees Basalt only, and it stops working when your access does.

Because a connector covers several workspaces, the agent has to say which one it means: https://app.basaltapp.io/mcp/w/<workspace id>.

When something is refused

The endpoint answers the way the API does, so a refusal says which of three things happened:

  • 401 — no credential, or one that is unknown, revoked or expired.
  • 403 — a credential that may not do this: a token without content:read at all, a content:read token asked to write, or one belonging to a different workspace than the address names.
  • Everything else is the ordinary API's answer, passed through with the server's own wording.

What the agent gets

Twelve tools. Read-only unless marked.

Tool What it does
get_workspace The workspace, your role, and every collection you can see with your access level there. The first call in an unfamiliar workspace.
search_pages Full-text search over titles and bodies. How you find a page id.
list_pages Structural listing: the children of a page, or the top level of a collection.
get_page A page as canonical Markdown with field values as YAML frontmatter, plus a version string.
list_databases The databases in the workspace or in one collection.
get_database A database's columns: keys, types, whether required, and the allowed option labels.
query_database Rows with filters and sorts. Every row carries a pageId.
create_page write — a page in a collection, optionally under a parent, optionally with a body.
update_page write — replaces the whole body and/or renames. Takes the version from get_page.
update_page_fields write — sets field values without touching the body.
create_database_row write — a row, which in Basalt is a page.
trash_page write — moves a page and its subtree to the trash. Reversible; there is deliberately no permanent-delete tool.

Five fields you can rely on

Every Basalt workspace has these, under exactly these keys, and nobody can rename, retype or delete them. Read them in a page's frontmatter, write them with update_page_fields, filter on them with query_database.

Key Type What it is for
summary text One or two sentences saying what the page is about. search_pages and list_pages both print it under each hit, so you can pick what to open without opening anything.
status select Draft, In Review, Approved, Published, Outdated, Archived — tells you whether to trust a page. The list is the same in every workspace.
page_type select Core, Process, Guide, Reference, Decision, Meeting notes, Template, Archive. Also the same everywhere.
tags multi-select Free vocabulary. A value that does not exist yet is created when you write it.
related relation Curated links between pages, symmetric: relating A to B relates B to A.

Writing summary on a page you have just created or rewritten is the single most useful thing an agent can do for the next one.

Two resources are offered for attaching context directly: basalt://workspace (collections and databases with their ids) and basalt://page/{pageId} (a page as Markdown).

Three behaviours are worth knowing because they change how an agent should act:

  • update_page replaces the body. Read the page, change the text you got, send all of it back. A fragment truncates the page.
  • Versions are optimistic locks. get_page returns one; passing it to update_page makes the write fail rather than overwrite an edit somebody made in between. That refusal is the feature.
  • Ids are never guessed. They come from get_workspace, search_pages, list_pages or query_database.

What it can and cannot do

The token is the identity. The server acts as the person who created the token, with exactly their access — it cannot do more, because there is no code path below the API that would let it. A page the person cannot read is not returned, does not appear in listings, and is not found by search.

Consequently the agent sees a viewer-dependent workspace: row counts, search results and page trees are what that person can see, not what exists.

Some refusals are the operator's to fix and some are not, so the server distinguishes them in the message it hands the model:

  • A missing scope names the exact scope string and says retrying will not help — someone has to mint a different token.
  • Content the user may not read says so, and points at search_pages.
  • Not found deliberately does not claim to know whether the page is absent or merely unreadable — Basalt cannot tell those apart on purpose, and pretending otherwise would send an agent hunting for a typo that does not exist.

BASALT_READ_ONLY=true withholds the write tools entirely. It cannot widen anything — a content:read token already refuses every write — but "the model never saw a write tool" is a stronger operational statement than "the write always failed", and it saves the agent the turns it would spend finding out.

Token management is not reachable from here at all. A credential that can mint credentials escalates quietly and outlives the revocation of the first, so the token surface is session-only, by design.