> ## Documentation Index
> Fetch the complete documentation index at: https://docs.artifacta.io/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Use Artifacta from Claude Code, Codex, Cursor, Windsurf, Claude Desktop, and any MCP-compatible client.

Your coding agent just generated a build report, a test transcript, a
screenshot, or a 200 MB bundle. Where does it go? Pasted into chat (gets
lost), dumped to `/tmp` (gets cleaned), pushed into a random S3 bucket (no
session view, no metadata, no shareable link). **Artifacta is the artifact
store built for that** — purpose-built for AI agents from the ground up —
and `@artifacta-mcp/mcp` is how your agent talks to it.

Artifacta supports a hosted HTTP MCP endpoint and a local stdio package, so it
works in **Claude Code, Codex, Cursor, Windsurf, Cline, Zed, and Claude
Desktop**, plus any other MCP-compatible host. Once configured, your agent can
store files, list past artifacts, mint private download URLs for its own
follow-up calls, and—with consent—publish public share links, all without
leaving the editor.

This guide gets you from zero to first artifact in under five minutes.

To store a conversation with `store_artifact(..., transcript=true)` and filter it with
`list_artifacts(..., transcript=true)`, see the [session transcript guide](/guides/transcripts).

<Note>
  **Heads-up for Claude Desktop and Cursor.** Three tools are classified
  destructive — `create_download_link` (public share URLs), `delete_artifact`
  (soft-delete by id), and `seal_session` (irreversible, no unseal) — and are
  hidden from `tools/list` unless you add `--allow-destructive` to the launch
  `args`. See [Claude Desktop & Cursor: destructive
  tools](#claude-desktop--cursor-destructive-tools) for the recommended config
  and [Autonomy boundary](#autonomy-boundary) for the full compliant /
  non-compliant client behavior.
</Note>

## Install

<Note>
  **Recommended for most users: the hosted MCP endpoint.** Connect by URL —
  `https://mcp.artifacta.io/mcp` — and authenticate with a one-time browser login,
  no package install and no API key to copy. See [Connect Claude Code
  (Hosted)](/mcp/install/claude-code-hosted) or
  [install the Codex plugin](/mcp/install/codex-plugin). The local stdio install
  below remains fully supported for CI, restricted networks, and explicit
  credential control—same Artifacta tools, different connection method.
</Note>

The local stdio server is distributed via npm. Most clients launch it with
`npx`, so there is nothing to install globally on your machine.

You will need:

* **Node.js 20 or newer.** The package's `engines` field rejects earlier
  versions before any tool executes.
* An Artifacta API key. Create one in the [dashboard's API keys
  page](https://app.artifacta.io/dashboard/keys). The key shape is
  `ak_live_` followed by 32 alphanumeric characters.

**A note on version pinning.** The snippets below leave `@artifacta-mcp/mcp`
unpinned so `npx` resolves to the latest published release on host restart;
patch and minor fixes (including security patches) roll out without a config
edit. Pin to a specific version — e.g. `@artifacta-mcp/mcp@1.0.0` — only if
you need a frozen install, such as a managed deployment with gated config
rollouts. The current published line is **`1.0.4`** — the stable contract;
no further breaking changes to the existing 13 tools / 4 resources without
v2.

### Claude Desktop

Open Claude Desktop's settings → **Developer** → **Edit Config**, and add an
entry under `mcpServers`. The full file lives at
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and
`%APPDATA%\Claude\claude_desktop_config.json` on Windows.

```json claude_desktop_config.json theme={null}
{
  "mcpServers": {
    "artifacta": {
      "command": "npx",
      "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/uploads", "--allow-destructive"],
      "env": {
        "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Claude Desktop does not advertise MCP write confirmations, so the three
destructive tools — `create_download_link` (public share URLs),
`delete_artifact`, and `seal_session` — are hidden unless you add
`--allow-destructive`. See [Autonomy boundary](#autonomy-boundary) for the
matrix and [`create_download_link` consent](#create_download_link-consent)
for how to approve share links in chat.

Restart Claude Desktop. The Artifacta tools appear in the tool palette and
the agent can call them directly.

### Cursor

Add the same block to `~/.cursor/mcp.json` (create the file if it does not
exist):

```json ~/.cursor/mcp.json theme={null}
{
  "mcpServers": {
    "artifacta": {
      "command": "npx",
      "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/uploads", "--allow-destructive"],
      "env": {
        "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Cursor does not advertise MCP write confirmations either — add
`--allow-destructive` so `create_download_link`, `delete_artifact`, and
`seal_session` appear in the tool list. See [Autonomy
boundary](#autonomy-boundary) and [`create_download_link`
consent](#create_download_link-consent).

Restart Cursor and the Artifacta tools become available in agent mode.

### Claude Code

For a single project, drop a `.mcp.json` at the repo root (commit it so the
whole team picks up the server):

```json .mcp.json theme={null}
{
  "mcpServers": {
    "artifacta": {
      "command": "npx",
      "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project", "--allow-destructive"],
      "env": {
        "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}"
      }
    }
  }
}
```

Claude Code expands `${ARTIFACTA_API_KEY}` from your shell environment at
launch, so the literal key stays out of the committed file. For a one-line
equivalent without editing JSON, use the CLI:

```bash theme={null}
claude mcp add artifacta -- npx -y @artifacta-mcp/mcp \
  --allow-path /Users/you/project --allow-destructive
```

Then export `ARTIFACTA_API_KEY` in the shell that launches Claude Code. The
`--allow-destructive` rule from Claude Desktop / Cursor applies here too —
without it `create_download_link`, `delete_artifact`, and `seal_session`
are hidden from the tool list. See [Autonomy
boundary](#autonomy-boundary). For an end-to-end walkthrough of storing and
sharing a build artifact this way, see the [publish from Claude Code use
case](https://artifacta.io/use-cases/publish-from-claude-code).

### Codex plugin (hosted OAuth)

The [Artifacta Codex plugin](/mcp/install/codex-plugin) is the recommended
Codex setup. It bundles the hosted OAuth MCP connection, persistence skills,
and verified transcript capture:

```bash theme={null}
codex plugin marketplace add SagaPeak/artifacta-mcp
codex plugin add artifacta@artifacta
codex mcp login artifacta
```

No API key or local MCP process is required. Start a new thread after
installation so Codex loads the plugin's skills and hooks.

Hosted OAuth permissions are enforced by the Artifacta server. Tools may
remain visible when the current grant does not authorize them; an attempted
call returns `insufficient_scope` with the required scope. The local
`--allow-destructive` flag does not apply to the hosted plugin.

### Codex (local stdio)

Codex (OpenAI CLI) reads MCP configuration from `~/.codex/config.toml` —
**TOML, not JSON**. Add an `[mcp_servers.artifacta]` table:

```toml ~/.codex/config.toml theme={null}
[mcp_servers.artifacta]
command = "npx"
args = ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project", "--allow-destructive"]

[mcp_servers.artifacta.env]
ARTIFACTA_API_KEY = "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

The section name is `mcp_servers` with an **underscore** — `mcp-servers` or
`mcpservers` is silently ignored. For a project-scoped install, the same
table goes in `.codex/config.toml` at the project root (Codex must trust
the project first). Restart Codex; Artifacta tools appear in the next
session, with the same `--allow-destructive` rule.

### Other MCP clients

Windsurf, Cline, Zed, and Continue all accept the same `mcpServers` JSON
shape as Claude Desktop — drop the Claude Desktop block above into the
client's MCP config file (consult the client's docs for the path) and
restart. The `--allow-path` and `--allow-destructive` flags carry over
unchanged.

### Verifying the install

Run the version flag from any shell:

```bash theme={null}
npx -y @artifacta-mcp/mcp --version
# 1.0.4
```

If that prints `1.0.4` the binary resolved correctly through `npx`.

### Claude Desktop & Cursor: destructive tools

* Three tools are classified **destructive**: `create_download_link` mints
  a **public** `dl.artifacta.io/lnk_…` URL, `delete_artifact` soft-deletes
  an artifact by id, and `seal_session` marks a session as **irreversible**
  (no `unseal`).
* Hosts that advertise `experimental.confirmations` prompt before each call;
  **Claude Desktop and Cursor do not**, so all three tools are hidden from
  `tools/list` by default.
* Add `--allow-destructive` to the launch `args` to expose them; each call
  then emits a one-line stderr audit `[artifacta-mcp] destructive call: <tool>(<args>)` instead of a host confirmation UI. The flag is **never
  read from the environment or `mcp.toml`** — it must be in the launch
  command.
* Approve destructive calls explicitly in chat before the agent runs them.
  For share links, see [`create_download_link`
  consent](#create_download_link-consent); for delete and seal, the same
  principle applies — read the agent's plan, confirm, then proceed.
* If you use `store_artifact.path`, combine `--allow-destructive` with
  `--allow-path` in the same `args` array — see [Path confinement and
  `--allow-path`](#path-confinement-and-allow-path).

## First call: `whoami`

`whoami` is the right starting tool: it confirms authentication, surfaces the
plan tier, and reports current usage so the agent can size subsequent
operations against quota. It is free of side effects and quota-cheap.

A typical agent prompt that exercises it:

> "Confirm we're authenticated against my Artifacta account, then list the
> last 10 artifacts."

The agent will call `whoami`, receive a response shaped like the example
below, then chain `list_artifacts` with `limit: 10`:

```json whoami response theme={null}
{
  "tenant_name": "maya",
  "plan": "free",
  "api_key_last_4": "abcd",
  "usage_storage_bytes": 1048576,
  "plan_storage_limit_bytes": 1073741824,
  "usage_requests_month": 142,
  "plan_requests_limit_month": 10000,
  "active_links": 3,
  "max_active_links": 10,
  "rate_limit_sustained": 100,
  "rate_limit_burst": 200
}
```

The response is the canonical surface — the same payload is also available as
the resource `artifacta://whoami`. The MCP server caches the
`api_key_last_4` so that a later authentication failure can include the key
suffix in the remediation message ("Last-known key suffix: \*\*\*\*abcd.").

## Using Artifacta from your coding agent

Once `whoami` confirms auth, here are the four flows that cover most agent
work. Paste any of these prompts into your coding agent — it will pick the
right tool(s) on its own.

### 1. Save a generated file

> "Save `./out/build_report.html` to session `build_2118` and tag it
> `kind=report, status=green`."

The agent calls `store_artifact` with `path: "./out/build_report.html"`,
`session_id: "build_2118"`, and `metadata: { kind: "report", status: "green" }`,
then returns the new `art_…` id. Re-running the same prompt after a crash
returns the *same* artifact instead of creating a duplicate — the MCP server
auto-injects an `Idempotency-Key` and surfaces it on success as
`_meta.idempotency_key`. Local file paths must clear
[path confinement](#path-confinement-and-allow-path).

### 2. Find prior work

> "Show me the test outputs from session `ci_run_42` tagged `kind=failure`,
> newest first."

The agent calls `list_artifacts` with `session_id: "ci_run_42"` and
`metadata: { kind: "failure" }`. Results come back in
`created_at DESC, artifact_id DESC` order — the API's only sort contract —
with opaque cursor pagination if the list is long.

### 3. Hand a file to a human

> "Publish a 24-hour share link for artifact `art_a1b2c3d4e5f6g7h8`."

The agent calls `create_download_link` with `expires_in: 86400`, then prints
the resulting `https://dl.artifacta.io/lnk_…` URL. On hosted OAuth, this
requires the `artifacts:destroy` grant. With the local stdio package, clients
without MCP confirmations require `--allow-destructive` in the launch
`args`.
**`get_artifact_download_url` is not a stable-link substitute**: it returns a
private one-hour presigned URL for the authenticated caller, not a shareable
public link.

### 4. Resume a session across runs

> "Re-open session `nightly_eval_2026_05_25`, list its artifacts, and append
> today's run output from `./eval/out.jsonl`."

The agent calls `list_artifacts` with the session id, then `store_artifact`
with the same `session_id` to append. Sessions are **user-defined strings
supplied at upload** — there is no separate `create_session` tool, and the
session row appears the first time an artifact is stored against that id.
Subsequent runs just keep writing to the same string.

## Write tools

v0.2 added four write tools and one preview resource so an agent can
produce, persist, and share artifacts end-to-end. They are unchanged in
v1.0.

| Tool                   | What it does                                                                                                                                                                                                                                                                  | Idempotency / retry                                   |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `store_artifact`       | Create an artifact from inline `content` or a local `path`; carries attribution inputs (`session_id`, `agent_id`, `model`).                                                                                                                                                   | Auto `Idempotency-Key`; safe to replay.               |
| `request_upload_url`   | Mint a presigned R2 upload URL for files larger than `store_artifact`'s 500 MB ceiling. **Pro-only — Free-tier keys receive `quota_exceeded` with an upgrade URL.**                                                                                                           | **Non-idempotent** — no auto-retry on 5xx.            |
| `complete_upload`      | Finalize a presigned upload into a committed artifact.                                                                                                                                                                                                                        | Idempotent — auto-retries 5xx.                        |
| `create_download_link` | Mint a public `dl.artifacta.io/lnk_…` share link.                                                                                                                                                                                                                             | **Non-idempotent**; consent-gated.                    |
| `publish_artifact`     | Publish an existing artifact as a shareable page at `artifacta.io/a/{slug}`. Re-publishing the same `artifact_id` upserts the page and keeps the URL. Requires `artifact_id` plus optional `title`, `visibility` (`"unlisted"`/`"public"`), `access` (`"none"`/`"password"`). | Idempotent (`writeIdempotent`); safe to retry on 5xx. |
| `unpublish_artifact`   | Remove the public page for an artifact. The artifact itself is not deleted. Calling unpublish on an already-unpublished artifact is a no-op. Requires `artifact_id`.                                                                                                          | Idempotent (`writeIdempotent`); safe to retry on 5xx. |

The companion `artifacta://artifact/{id}/bytes` resource exposes an artifact's
bytes inline for preview. See [Resources](#resources) for the full resource
surface, including the 100 MB cap behavior.

### A note on sessions

`session_id` is a **user-defined string** supplied at upload time
(`store_artifact.session_id`, or carried through `request_upload_url` →
`complete_upload`). There is no separate `create_session` tool — a session row
appears the first time an artifact is stored against that id, and surfaces in
`list_sessions` and `artifacta://session/{session_id}` from that point on.

Sessions can be **sealed** with `seal_session` (v1.0 — irreversible, no
`unseal`). Once sealed, further uploads against that `session_id` are
refused at the API layer with `session_sealed`. `list_sessions` and
`artifacta://session/{session_id}` continue to report `is_sealed` for
read-only visibility. See [Destructive tools](#destructive-tools) for the
full description and the autonomy-boundary gating that protects the call.

### `store_artifact`: `content` vs `path`

`store_artifact` accepts **exactly one** of two inputs:

* **`content`** — the bytes inline, base64-encoded, up to **10 MB** decoded.
  Best for small, in-memory results (a generated report, a JSON blob).
* **`path`** — a path to a local file, streamed to the API as multipart, up to
  **500 MB**. Best for files already on disk. The path must pass
  [path confinement](#path-confinement-and-allow-path). For files larger than
  500 MB, switch to `request_upload_url` → `complete_upload` — note that path
  is **Pro-only**; Free-tier keys receive `quota_exceeded` with an upgrade URL,
  so on Free `store_artifact.path` is the upload ceiling.

See [Save a generated file](#1-save-a-generated-file) above for an end-to-end
prompt example using `path`.

### `store_artifact`: attribution inputs

Alongside the payload, `store_artifact` accepts the attribution fields that
feed listings and published-page receipts:

* `session_id` — groups the artifact with others from the same run.
* `agent_id` — defaults to the connected MCP client's name (or `"mcp"`) if
  omitted, so provenance is never blank.
* `model` — the model that produced the content, stored as `metadata.model`
  unless that key is already set in `metadata`. Always pass it, using the
  exact model ID from your own context; if a subagent produced the artifact
  end-to-end, pass that subagent's model, not yours. This is a declared
  producer claim — never describe it as a verified model. Published page
  receipts render `metadata.model` directly. For automatic capture from a
  Claude Code session transcript instead, see
  [model capture](/guides/transcripts#model-capture).
* `metadata`, `ttl`, `transcript` — as documented in the
  [transcript guide](/guides/transcripts) and REST reference.

### Path confinement and `--allow-path`

`store_artifact.path` reads files from the local filesystem, so it is guarded by
the path-confinement engine:

* **Default allow-list:** the directory the MCP server was launched in (its
  working directory). A path outside it is refused.
* **Extend it** with one or more `--allow-path=<dir>` launch flags:

```json claude_desktop_config.json theme={null}
{
  "mcpServers": {
    "artifacta": {
      "command": "npx",
      "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project/out", "--allow-destructive"],
      "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
    }
  }
}
```

Both `--allow-path` and `--allow-destructive` belong in the same `args`
array when you need local file uploads and public share links.

* **The deny-list always wins,** even inside an allow-listed directory:
  `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `~/.kube`, `~/.netrc`,
  `~/.artifacta`, `/etc`, `~/Library/Keychains`, and any `credentials.json` or
  `.env*` file are refused. Symlinks are resolved with `realpath()` before the
  check, so a symlink pointing outside the allow-list (or into the deny-list)
  is refused after resolution. Sockets, FIFOs, and device files are refused.

`--allow-path` only accepts **absolute** paths; a relative value exits at
startup with code 2. The CLI flag is not inferred from a config-file field,
but the server **also** appends roots from the
**`ARTIFACTA_MCP_ALLOW_PATH`** environment variable (colon-separated absolute
paths) when present in the launched server's `env` block — that env var
widens the allow-list at startup the same way `--allow-path` does, so audit
it whenever you review who can read local files through `store_artifact.path`.

### Ambiguous completion: when a write may or may not have happened

`request_upload_url` and `create_download_link` are **non-idempotent** — the API
does not honor an `Idempotency-Key` on them. So when one returns a `5xx` or the
network drops, the MCP server makes **exactly one** attempt and returns
guidance instead of silently retrying:

> The request may or may not have completed. Do not retry blindly — verify
> current state first (e.g. list links / list artifacts) or escalate to a human.

This is deliberate: a blind retry could mint a **second** public download link
or a duplicate upload slot. The correct agent behavior is to **verify or
escalate**, not retry. (`store_artifact` and `complete_upload`, which *are*
idempotent, do retry `5xx` automatically — a replay is safe there.)

### `create_download_link` consent

`create_download_link` produces a **public** URL, so it requires elevated
consent:

* On the **hosted OAuth connection**, the call requires
  `artifacts:destroy`. A lower-tier token receives `insufficient_scope` even
  if the tool remains visible.
* Clients that advertise `experimental.confirmations` receive
  `requiresConfirmation: true` from the **local stdio package** and prompt
  the human before the call runs.
* **Claude Desktop and Cursor** do not advertise confirmations. Without
  `--allow-destructive` in the local launch `args`,
  `create_download_link` is omitted from `tools/list` entirely—see
  [Claude Desktop & Cursor: public share
  links](#claude-desktop--cursor-public-share-links) for the recommended
  stdio config.
* Other clients that do **not** advertise confirmations behave the same:
  the local tool stays hidden unless the server was launched with
  `--allow-destructive` (and each such call then emits a one-line stderr
  audit). This prevents an agent from silently leaking a shareable URL.

## Destructive tools

v1.0 adds two destructive tools. On local stdio they share the same consent
surface as `create_download_link`: filtered from `tools/list` for
non-compliant clients (unless `--allow-destructive` is set), and carrying
`requiresConfirmation` for compliant ones. On hosted OAuth, all three
operations require `artifacts:destroy`. See
[Autonomy boundary](#autonomy-boundary) for both models.

| Tool              | What it does                                                                                                                                                                 | Replay behavior                                                                                                                                    |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `delete_artifact` | Soft-delete an artifact by id (`DELETE /v1/artifacts/{artifact_id}`). Requires MCP argument `confirm: true` after explicit user approval (API/CLI/SDK do not use this flag). | A second call on an already-deleted artifact returns `{ artifact_id, deleted: true, already_deleted: true }` rather than an error — safe to retry. |
| `seal_session`    | Mark a session **irreversible** (no `unseal`) so further uploads against that `session_id` are refused.                                                                      | A re-seal returns the existing `sealed_at` — passthrough at the API.                                                                               |

Both are classified `destructive` and follow the autonomy boundary below.
Neither injects `Idempotency-Key` (the API gates injection to
`POST /v1/artifacts` only); both use the `idempotentWrite` retry policy
(429 once, 5xx up to 3× with jitter) because each operation is naturally
idempotent.

### Example agent prompts

> "Delete artifact `art_a1b2c3d4e5f6g7h8` — it has the wrong session id and
> I am going to re-upload it under `build_2118`."

The agent asks the user to confirm, then calls `delete_artifact` with
`artifact_id: "art_a1b2c3d4e5f6g7h8"` and `confirm: true`, then re-runs
`store_artifact` with the right `session_id`. On a compliant client the host
also prompts before the delete fires; on Claude Desktop / Cursor the agent
prints its plan and waits for chat confirmation, then dispatches the call
(`--allow-destructive` must be in the launch `args`). Without `confirm: true`
the MCP tool rejects the call.

> "Seal session `nightly_eval_2026_05_25` — the run is finished and I don't
> want any further uploads against that id."

The agent calls `seal_session` with `session_id: "nightly_eval_2026_05_25"`.
After the call, `list_sessions` reports `is_sealed: true` and any further
`store_artifact` against that session id receives `session_sealed`.

## Autonomy boundary

For the **local stdio package**, the MCP server filters and prompts for tools
based on the client's declared capabilities in `initialize`. Three tools are
classified *destructive*: `create_download_link`, `delete_artifact`,
`seal_session`. The local-stdio matrix:

| Client                                                                                             | Default tool list                                                           | `requiresConfirmation`                                                              | With `--allow-destructive`                                                                                                                                          |
| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Compliant (advertises `experimental.confirmations` — e.g. Claude Code)                             | Destructive tools **present**.                                              | Set to `true` on each destructive tool; the host prompts the human before the call. | No change in surface; the flag is unused (the confirmation UI is already intact).                                                                                   |
| Non-compliant (no `experimental.confirmations` — Claude Desktop, Cursor, Codex, most others today) | Destructive tools **absent** from `tools/list`. The agent cannot call them. | Not applicable.                                                                     | Destructive tools **appear** in `tools/list`. Each call emits a one-line stderr audit `[artifacta-mcp] destructive call: <tool>(<args>)` — no host UI confirmation. |

The **hosted OAuth connection and plugins** do not use
`--allow-destructive`. All registered tools may remain visible, while the
server authorizes each call against the OAuth grant:
`artifacts:read` ⊆ `artifacts:write` ⊆ `artifacts:destroy`. The destroy tier
authorizes public share links, soft deletion, and irreversible session
sealing. A denied call returns `insufficient_scope`; logout and authenticate
again to change the grant.

Other rules that bind the flag:

* `--allow-destructive` is **never read from the environment or
  `mcp.toml`** — it must be in the per-launch CLI args. This prevents a
  leftover env export from silently unlocking destructive tools.
* The `ARTIFACTA_MCP_REQUIRE_WRITE_CONFIRM=1` env override promotes
  `requiresConfirmation` on the four **write** tools (`store_artifact`,
  `request_upload_url`, `complete_upload`, `create_download_link`) for
  compliant clients that want a stricter surface. It does **not** affect
  destructive tools — those always require confirmation on compliant
  clients.
* On compliant clients, `create_download_link` is destructive too — the
  host prompts before each share-link mint. The same one-line stderr
  audit still emits when `--allow-destructive` is the only reason a tool
  was exposed.

## Security model

A consolidated reference for the four security mechanisms the MCP server
relies on.

### 1. Path confinement

Applies to: `store_artifact.path` (the only tool that reads local files).

* Default allow-list = server CWD. Extend with `--allow-path=<absolute-dir>`
  (relative paths exit at startup with code 2; the flag is never read from
  the environment).
* Deny-list always wins: `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`,
  `~/.kube`, `~/.netrc`, `~/.artifacta`, `/etc`, `~/Library/Keychains`, any
  `credentials.json`, any `.env*`.
* Symlinks are resolved via `realpath()` before allow-list and deny-list
  checks. Special files (sockets, FIFOs, devices) are refused.
* 500 MB ceiling per file, regardless of plan tier. >500 MB → use
  `request_upload_url` → `complete_upload`.

See [Path confinement and `--allow-path`](#path-confinement-and-allow-path)
for configuration examples.

### 2. `Idempotency-Key` auto-injection

* `POST /v1/artifacts` (the `store_artifact` endpoint) — **the only
  endpoint that auto-injects an Idempotency-Key**. The MCP server attaches
  `mcp_<uuid4>` unless the caller passes their own `idempotency_key`. The
  effective key is surfaced on success as `_meta.idempotency_key`.
* Every other write endpoint — `request_upload_url`, `complete_upload`,
  `create_download_link`, `delete_artifact`, `seal_session` — does **not**
  inject. `complete_upload`, `delete_artifact`, and `seal_session` are
  naturally idempotent at the API layer (replays return the existing
  record). `request_upload_url` and `create_download_link` are
  non-idempotent; see ambiguous-completion below.

### 3. Retry policy

Per plan §6.1, retries are tied to backend idempotency:

| Tool                                                                                     | Retry policy         | 5xx auto-retry?                                      | Idempotency-Key injected? |
| ---------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------- | ------------------------- |
| `whoami`, `list_artifacts`, `get_artifact`, `get_artifact_download_url`, `list_sessions` | `safe`               | Yes (3× w/ jitter)                                   | n/a (GET)                 |
| `store_artifact`                                                                         | `idempotentWrite`    | Yes (3× w/ jitter)                                   | Yes                       |
| `request_upload_url`                                                                     | `nonIdempotentWrite` | **No** (1 call, then §6.1 ambiguous-completion text) | No                        |
| `complete_upload`                                                                        | `idempotentWrite`    | Yes (3× w/ jitter)                                   | No (natural idempotency)  |
| `create_download_link`                                                                   | `nonIdempotentWrite` | **No** (1 call, then §6.1 ambiguous-completion text) | No                        |
| `delete_artifact`, `seal_session`                                                        | `idempotentWrite`    | Yes (3× w/ jitter)                                   | No (natural idempotency)  |
| `publish_artifact`, `unpublish_artifact`                                                 | `idempotentWrite`    | Yes (3× w/ jitter)                                   | No (natural idempotency)  |

429 across the board: retried once with the `Retry-After` header value if
present, otherwise jittered backoff. The 3-consecutive-failure outage
notifier writes a single `Artifacta API unreachable…` line to stderr after
the third sequential transport-level failure and resets on the next
success.

### 4. Destructive-tool authorization

See [Autonomy boundary](#autonomy-boundary). On hosted OAuth,
`create_download_link`, `delete_artifact`, and `seal_session` require
`artifacts:destroy`; a lower-tier token receives `insufficient_scope`. With
local stdio, those tools are hidden from non-compliant clients unless the
per-launch `--allow-destructive` flag is set, and carry
`requiresConfirmation: true` for compliant clients. Each local call emits a
one-line stderr audit when the flag is the reason the tool was exposed
without a confirmation surface.

## Resources

In addition to tools, the MCP server exposes four resource URIs. Hosts that
prefer the resource model — or want to surface Artifacta data in a side panel
without firing a tool call — can read these directly.

| URI                                        | Returns                                               | Notes                                                                                         |
| ------------------------------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `artifacta://whoami`                       | Same payload as the `whoami` tool.                    | Cheap; safe to poll.                                                                          |
| `artifacta://artifact/{artifact_id}`       | Same metadata body as `get_artifact`.                 | Tenant-internal fields (`tenant_id`, `deleted_at`) are stripped at the MCP boundary.          |
| `artifacta://artifact/{artifact_id}/bytes` | Inline bytes (text or blob, routed by content type).  | Hard-capped at **100 MB** — see [size cap](#bytes-size-cap) below.                            |
| `artifacta://session/{session_id}`         | Aggregate view of a session's artifacts + seal state. | Read-only; sessions cannot be sealed in v0.2 (see [A note on sessions](#a-note-on-sessions)). |

### `…/bytes` size cap

The bytes resource is hard-capped at **100 MB**. Exactly 100 MB inline is
allowed; 100 MB + 1 byte is refused. Oversize requests fail at the MCP
`resources/read` layer with an **`InvalidRequest`** error — *not* a tool error
envelope — whose message steers the agent to `get_artifact_download_url`
(which mints a private 1-hour presigned URL for the authenticated caller; use
`create_download_link` only if a *public, shareable* URL is required).

Two refusal variants exist, both returning the same `InvalidRequest` class:

* **Metadata gate** — caught from the artifact's recorded size with a single
  `get_artifact` call, so an over-cap fetch never touches R2.
* **R2 oversize gate** — backstop in case the metadata size under-reports the
  actual blob size on disk.

Agents should branch on the `InvalidRequest` error class and reissue the read
as a `get_artifact_download_url` tool call; the resource cannot be widened with
a flag.

## Troubleshooting

### `unauthorized` on every call

The MCP server surfaces an `unauthorized` error when the API rejects the
configured key. Common causes:

1. **`ARTIFACTA_API_KEY` is not set in the MCP host's environment.** Editing
   your shell's `.zshrc` does not propagate to Claude Desktop or Cursor —
   they read the `env` block from your `mcpServers` config. Put the key
   directly in `claude_desktop_config.json` or `~/.cursor/mcp.json` and
   restart the host.
2. **The key was rotated.** The remediation message includes the
   last-known key suffix (`Last-known key suffix: ****abcd`). If that suffix
   does not match the key you currently have in the dashboard, regenerate or
   paste the new key into the host config.
3. **The key is malformed.** Keys must match `ak_live_` plus 32
   alphanumeric characters. The server exits at startup with code 2 if the
   shape check fails — check the host's stderr log for the rejection message.
4. **The tenant is in deletion grace period.** The server replaces the
   generic remediation with `"Account is scheduled for deletion — see
   https://app.artifacta.io/dashboard/account."` Restore the account from
   the dashboard before the grace period ends.

If none of those apply, capture the failing response — every error result
carries a `request_id` in `_meta.request_id` — and contact support with that
id.

### Destructive action unavailable

First identify the connection type:

* **Hosted OAuth or a plugin:** The tool may appear but return
  `insufficient_scope`. Reauthorize and grant `artifacts:destroy`. In Codex,
  run `codex mcp logout artifacta` followed by
  `codex mcp login artifacta`. Do not add `--allow-destructive`; the hosted
  server does not read local launch flags.
* **Local stdio:** If `create_download_link`, `delete_artifact`, or
  `seal_session` is missing while every other Artifacta tool appears, the
  host likely lacks MCP confirmation support and the server was launched
  without `--allow-destructive`. Add the flag to the launch `args`, restart
  the host, and confirm the tool appears. See
  [Autonomy boundary](#autonomy-boundary) for the local-stdio matrix.

For `create_download_link` specifically: `get_artifact_download_url` is
**not** a substitute — it returns a time-limited presigned URL for the
authenticated caller, not a stable public share link. On hosted OAuth,
`get_artifact_download_url` is read-scoped while `create_download_link`
requires `artifacts:destroy`.

### `tools/list` shows nothing

Make sure the host launched the server. Common causes:

* `npx` could not resolve `@artifacta-mcp/mcp` (no network, expired npm cache).
  Run `npx -y @artifacta-mcp/mcp --version` from a shell to reproduce.
* The host is running on Node 18 or older. The package's `engines.node` is
  `>=20.0.0`; the npm install step refuses to run on older Node.

### Path arguments are refused even though the file exists

The local path-confinement engine refuses any path outside the server's
allow-list (default: the host's working directory) and refuses paths inside
the built-in deny-list (`~/.ssh`, `~/.aws`, `~/.config/gh`, `/etc`, …) even
when the allow-list is widened. Pass `--allow-path=/your/dir` at launch to
extend the allow-list, but the deny-list always wins.

This guards the `store_artifact` `path` argument (v0.2). See [Path confinement
and `--allow-path`](#path-confinement-and-allow-path) for the full deny-list and
configuration examples.

## What's next

* **[Anthropic MCP introduction](https://modelcontextprotocol.io/introduction)**
  — protocol fundamentals if you are new to MCP.
* **[MCP Server Leaderboard](https://artifacta.io/mcp-leaderboard)** — see
  how Artifacta ranks among other MCP servers agents connect to.
