# Get Gate Info Source: https://docs.artifacta.io/api/gate-info Check whether a public page is password-protected before rendering it. ## `GET /v1/public/pages/{slug}/gate-info` Returns a minimal gate signal for a password-protected page. This endpoint is **unauthenticated** and is called by the Artifacta viewer (`artifacta.io/a/{slug}`) to decide whether to show the password prompt. Returns `200` only for pages that are: * password-protected (`access="password"`) * live (not unpublished, not expired, not from a suspended account) Returns `404` for everything else — including unlisted and public pages with no password, missing slugs, expired pages, and suspended tenants. This is intentional: the endpoint reveals nothing about page existence beyond "you need a password here." ## Authentication None. This is a public endpoint. ## Path parameters | Parameter | Type | Description | | --------- | ------ | ----------------------------------------- | | `slug` | string | Page slug (`pg_...`) from the public URL. | ## Response ```json theme={null} { "exists": true, "access": "password" } ``` | Field | Type | Description | | -------- | ------- | -------------------------------------- | | `exists` | boolean | Always `true` on a 200 response. | | `access` | string | Always `"password"` on a 200 response. | ## Error codes | Code | Status | When | | -------------------- | ------ | ----------------------------------------------------------------------------------------------------- | | `artifact_not_found` | 404 | Page not found, not password-protected, unpublished, expired, or from a suspended account. No oracle. | ## Notes * **No oracle**: The 404 error code does not distinguish between a missing slug and an unlisted page that exists but has no password. This prevents enumeration. * **Rate limiting**: This endpoint shares the standard per-tenant rate limit. Abuse patterns (e.g. high-volume slug scanning) are also subject to the Vercel edge rate limit. ## Example ```bash theme={null} curl https://api.artifacta.io/v1/public/pages/pg_aB3xK9mP1qR5sT2u/gate-info ``` # API Overview Source: https://docs.artifacta.io/api/overview Base URL, authentication, pagination, rate limiting, and API guarantees. ## Base URL ``` https://api.artifacta.io ``` All endpoints (except `GET /health`) are prefixed with `/v1/`. API-direct transcript storage uses the existing artifact metadata and content-type fields, not a new REST field. See the multipart, JSON-body, and list recipes in the [session transcript guide](/guides/transcripts). Model provenance likewise uses existing metadata keys (`model`, `model_source`, `models_used`) — there is no new REST field. See [model capture](/guides/transcripts#model-capture). ## Authentication Every request requires a Bearer token: ```bash theme={null} Authorization: Bearer ak_live_ ``` Get your key from [app.artifacta.io/dashboard/keys](https://app.artifacta.io/dashboard/keys). Use `GET /v1/whoami` to verify your key. ## Endpoints ### Artifacts | Method | Endpoint | Description | | -------- | ------------------------------------------------------ | --------------------------- | | `POST` | [`/v1/artifacts`](/api/upload-artifact) | Upload an artifact | | `GET` | [`/v1/artifacts`](/api/list-artifacts) | List artifacts with filters | | `GET` | [`/v1/artifacts/{id}`](/api/get-artifact) | Get artifact metadata | | `GET` | [`/v1/artifacts/{id}/download-url`](/api/download-url) | Get presigned download URL | | `DELETE` | [`/v1/artifacts/{id}`](/api/delete-artifact) | Soft-delete an artifact | | `POST` | [`/v1/artifacts/{id}/links`](/api/create-link) | Create a download link | ### Presigned uploads | Method | Endpoint | Description | | ------ | ----------------------------------------------------- | -------------------------------------- | | `POST` | [`/v1/artifacts/upload-url`](/api/presigned-upload) | Get presigned upload URL (large files) | | `POST` | [`/v1/artifacts/{id}/complete`](/api/complete-upload) | Finalize presigned upload | ### Artifact Pages (authenticated) | Method | Endpoint | Description | | -------- | ------------------------------------------------------- | --------------------------------------------------- | | `POST` | [`/v1/artifacts/{id}/publish`](/api/publish-artifact) | Publish an artifact as a public page | | `DELETE` | [`/v1/artifacts/{id}/publish`](/api/unpublish-artifact) | Unpublish a page (accepts artifact ID or page slug) | ### Artifact Pages (public — no auth) | Method | Endpoint | Description | | ------ | ----------------------------------------------------- | ---------------------------------------- | | `GET` | [`/v1/public/pages/{slug}/gate-info`](/api/gate-info) | Check if a page is password-protected | | `POST` | [`/v1/public/pages/{slug}/unlock`](/api/unlock-page) | Verify passcode, receive a content token | | `POST` | [`/v1/public/pages/{slug}/report`](/api/report-page) | Submit an abuse report (always 202) | The three `/v1/public/pages/` endpoints require **no Bearer token**. They are called by the public viewer and by end users' browsers. The "Every request requires a Bearer token" rule applies only to authenticated endpoints. ### Sessions | Method | Endpoint | Description | | ------ | --------------------------------------------- | -------------- | | `POST` | [`/v1/sessions/{id}/seal`](/api/seal-session) | Seal a session | ### Account | Method | Endpoint | Description | | ------ | --------------------------- | ---------------------------- | | `GET` | [`/v1/whoami`](/api/whoami) | Verify auth, get tenant info | | `GET` | [`/health`](/api/health) | Health check (no auth) | ## Content upload paths | Method | Best for | Size limit | | ----------------------------------------------- | ----------------------------------- | ------------- | | Multipart form data (`POST /v1/artifacts`) | CLI, file uploads | 500 MB | | JSON body with base64 (`POST /v1/artifacts`) | Programmatic callers, small content | 10 MB decoded | | Presigned URL (`upload-url` → PUT → `complete`) | Large files | 5 GB | ## Pagination All list endpoints use **cursor-based pagination**. * Response includes `next_cursor` (`null` if no more results) * Pass `cursor=` for the next page * Max `limit`: 200, default: 50 **Sort order guarantee:** Results are always ordered by `created_at DESC, artifact_id DESC`. This is a V1 API contract and will not change without a new API version. ## Rate limiting Per-tenant: **100 requests/second sustained, 200 requests/second burst** (sliding window). **Headers on every response:** | Header | Description | | ----------------------- | --------------------------------- | | `X-RateLimit-Limit` | Requests per second allowed | | `X-RateLimit-Remaining` | Remaining in current window | | `X-RateLimit-Reset` | Unix timestamp when window resets | When exceeded: `429 Too Many Requests` with `Retry-After` header and `retry_after_seconds` in the body. ## Error response shape All errors return: ```json theme={null} { "error": { "code": "artifact_not_found", "message": "Artifact art_xxx was not found.", "status": 404 } } ``` Match on `error.code` (stable). Never parse `error.message`. See the [full error reference](/errors). ## The `page` field on artifact responses `GET /v1/artifacts/{id}` includes a `page` sub-object when the artifact has a live published page: ```json theme={null} { "artifact_id": "art_abc123", "filename": "report.pdf", "page": { "public_url": "https://artifacta.io/a/pg_aB3xK9mP1qR5sT2u", "visibility": "unlisted" } } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------ | | `page.public_url` | string | Full viewer URL. Anyone with this URL can view the artifact. | | `page.visibility` | string | `"unlisted"` or `"public"`. | `page` is `null` when no live page exists (never published, or unpublished). It is present only on the single-artifact `GET /v1/artifacts/{id}` response — list results always return `page: null`. ## API guarantees * **Read-after-write consistency:** An artifact written via `POST /v1/artifacts` is immediately visible to all read endpoints. * **Backward compatibility:** Additive changes (new fields, new optional parameters, new endpoints) are non-breaking. Removals and type changes require a new API version. V1 remains available for 12 months after V2 launches. **Coming in V1.1:** `POST /v1/artifacts/batch/download-urls` — batch download URLs for up to 20 artifacts in a single request. # Publish Artifact Source: https://docs.artifacta.io/api/publish-artifact Publish an artifact as a public shareable page. ## `POST /v1/artifacts/{id}/publish` Publishes an artifact as a page at `https://artifacta.io/a/{slug}`. The slug is minted on the first publish and stays stable across re-publishes. Calling this endpoint again on the same artifact upserts the existing page and keeps the same URL — it is safe to call repeatedly. Publishing is a lifecycle transition and does not increment the monthly request counter. ## Authentication Requires a Bearer token. See [Authentication](/authentication). ## Path parameters | Parameter | Type | Description | | --------- | ------ | ----------------------- | | `id` | string | Artifact ID (`art_...`) | ## Request body ```json theme={null} { "visibility": "unlisted", "access": "none", "title": "Q2 Report" } ``` | Field | Type | Required | Description | | ------------ | ------ | ----------- | -------------------------------------------------------------------------------------------------------------------- | | `visibility` | string | Yes | `"unlisted"` — accessible by URL only. `"public"` — discoverable. | | `access` | string | No | `"none"` (default) — open. `"password"` — password-protected (Pro plan). | | `title` | string | No | Page title shown in the viewer header. | | `password` | string | Conditional | Required when `access="password"`. Argon2id-hashed server-side. Pro plan only. Not available for HTML content types. | ## Response ```json theme={null} { "page_id": "pg_aB3xK9mP1qR5sT2u", "public_url": "https://artifacta.io/a/pg_aB3xK9mP1qR5sT2u", "visibility": "unlisted", "access": "none" } ``` | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------ | | `page_id` | string | Page slug (`pg_...`). Stable across re-publishes. | | `public_url` | string | Full viewer URL. Anyone with this URL can view the artifact. | | `visibility` | string | Echo of the requested visibility. | | `access` | string | Echo of the requested access mode. | ## Error codes | Code | Status | When | | -------------------------- | ------ | ---------------------------------------------------------------------------------------- | | `artifact_not_found` | 404 | Artifact ID does not exist or belongs to another tenant. | | `artifact_already_deleted` | 410 | Artifact has been soft-deleted. | | `artifact_expired` | 410 | Artifact has passed its TTL. | | `quota_exceeded` | 403 | `access="password"` on a Free-tier account, or monthly request quota reached. | | `invalid_request` | 400 | Missing `visibility`, invalid field value, or `access="password"` for HTML content type. | ## Notes * **HTML and passwords**: `access="password"` is rejected for `text/html` or `application/xhtml+xml` artifacts — there is no safe gated render path for HTML in the current viewer. * **Re-publish**: Calling publish again on the same artifact updates the page title, visibility, and access, keeping the existing `page_id` and URL. * **`page` field on artifacts**: After a successful publish, `GET /v1/artifacts/{id}` returns a `page` sub-object on the artifact — see [the artifact response page field](/api/overview#the-page-field-on-artifact-responses). ## Example ```bash theme={null} curl -X POST https://api.artifacta.io/v1/artifacts/art_abc123/publish \ -H "Authorization: Bearer ak_live_..." \ -H "Content-Type: application/json" \ -d '{"visibility": "unlisted", "title": "Q2 Analysis"}' ``` # Report Page Source: https://docs.artifacta.io/api/report-page Submit an abuse report for a public artifact page. ## `POST /v1/public/pages/{slug}/report` Submits an abuse report for a public page. This endpoint is **unauthenticated**. The response is always `202 Accepted` regardless of whether the slug exists — this is intentional. The endpoint never reveals page existence. Reports are forwarded to PostHog (and optionally Slack) for operator review. No database write occurs; takedown is handled by operator action through the standard unpublish mechanism. ## Authentication None. This is a public endpoint. ## Path parameters | Parameter | Type | Description | | --------- | ------ | ----------------------------------------- | | `slug` | string | Page slug (`pg_...`) from the public URL. | ## Request body ```json theme={null} { "reason": "spam", "detail": "This page is impersonating a financial institution." } ``` | Field | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `reason` | string | No | Report category. One of: `"spam"`, `"phishing"`, `"malware"`, `"abuse"`, `"copyright"`, `"other"`. Unrecognized values are stored as `"other"`. | | `detail` | string | No | Free-text description. Truncated to 2,000 characters server-side. | ## Response ```json theme={null} { "received": true } ``` HTTP status: `202 Accepted`. | Field | Type | Description | | ---------- | ------- | ---------------------------------------------------------------------------------------------- | | `received` | boolean | Always `true`. Indicates the report was accepted for review, not that the page was taken down. | ## Error codes | Code | Status | When | | -------------- | ------ | -------------------------------------------- | | `rate_limited` | 429 | Too many reports from this IP for this slug. | All other responses are `202`. A `202` for a missing slug is indistinguishable from a `202` for a valid one — this is intentional to prevent slug enumeration. ## Notes * **No oracle**: `202` is returned for any slug, including ones that do not exist. * **Async takedown**: Submitting a report does not immediately take down the page. Operator review is required. Confirmed violations are handled via `DELETE /v1/artifacts/{id}/publish`. * **Reason coercion**: If `reason` is missing, not a string, or not one of the recognized values, it is stored as `"other"`. ## Example ```bash theme={null} curl -X POST https://api.artifacta.io/v1/public/pages/pg_aB3xK9mP1qR5sT2u/report \ -H "Content-Type: application/json" \ -d '{"reason": "phishing", "detail": "Impersonating a bank login page."}' ``` # Unlock Page Source: https://docs.artifacta.io/api/unlock-page Verify a page passcode and receive a short-lived content token. ## `POST /v1/public/pages/{slug}/unlock` Verifies the password for a gated page and returns a short-lived content token. The token is passed to the content-origin Worker (`artifactausercontent.com/c/{slug}?token=...`) to fetch the gated bytes. This endpoint is **unauthenticated** and is called by the Artifacta viewer after the visitor submits the password prompt. The token expires in **5 minutes** (`expires_in: 300`). After expiry the viewer must call this endpoint again. ## Authentication None. This is a public endpoint. ## Path parameters | Parameter | Type | Description | | --------- | ------ | ----------------------------------------- | | `slug` | string | Page slug (`pg_...`) from the public URL. | ## Request body ```json theme={null} { "password": "hunter2" } ``` | Field | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------- | | `password` | string | Yes | The passcode set when the page was published. | ## Response ```json theme={null} { "content_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 300 } ``` | Field | Type | Description | | --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `content_token` | string | Signed JWT (HS256). Carries `slug`, `purpose: "page_content"`, and `exp`. Pass this as a query parameter to the content-origin Worker. | | `expires_in` | integer | Token lifetime in seconds. Always `300`. | ## Error codes | Code | Status | When | | -------------------- | ------ | ----------------------------------------------------------------------------------------- | | `artifact_not_found` | 404 | Page is missing, unpublished, expired, or access is not `"password"`. No oracle. | | `unauthorized` | 401 | Password is incorrect. Same error code regardless of whether the slug exists (no oracle). | | `rate_limited` | 429 | Too many unlock attempts for this slug. Try again after the `Retry-After` header value. | | `invalid_request` | 400 | Missing or empty `password` field, or malformed JSON body. | ## Notes * **Single failure mode**: An incorrect password returns `unauthorized`, not `artifact_not_found`. However, a missing or unpublished slug also returns a non-200 status code — callers cannot distinguish wrong-password from missing-slug. This is intentional. * **Brute-force protection**: Attempts are rate-limited per slug plus a separate per-IP edge rule. The per-slug cap is the primary control. * **Token scope**: The token is single-purpose (`purpose: "page_content"`) and accepted only by the content-origin Worker. It cannot be used to call any API endpoint. ## Example ```bash theme={null} curl -X POST https://api.artifacta.io/v1/public/pages/pg_aB3xK9mP1qR5sT2u/unlock \ -H "Content-Type: application/json" \ -d '{"password": "hunter2"}' ``` # Unpublish Artifact Source: https://docs.artifacta.io/api/unpublish-artifact Take down an artifact's public page without deleting the artifact. ## `DELETE /v1/artifacts/{id}/publish` Soft-unpublishes the public page for an artifact. The page URL stops resolving immediately. The underlying artifact is not deleted and remains accessible through the API. This endpoint is idempotent: calling it on an already-unpublished artifact is a no-op and returns the same `page_id`. ## Authentication Requires a Bearer token. See [Authentication](/authentication). ## Path parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Artifact ID (`art_...`) **or** page slug (`pg_...`). Both resolve within the caller's tenant — the public slug never authorizes cross-tenant mutations. | ## Request body None. ## Response ```json theme={null} { "page_id": "pg_aB3xK9mP1qR5sT2u", "unpublished": true } ``` | Field | Type | Description | | ------------- | ------- | ------------------------------------------ | | `page_id` | string | The slug of the page that was unpublished. | | `unpublished` | boolean | Always `true`. | ## Error codes | Code | Status | When | | -------------------- | ------ | ----------------------------------------------------------------------------- | | `artifact_not_found` | 404 | No published page found for the given artifact ID or slug within this tenant. | ## Notes * **Artifact is preserved**: Only the public page is taken down. The artifact bytes and metadata remain accessible to authenticated callers via `GET /v1/artifacts/{id}`. * **Artifact deletion auto-unpublishes**: Calling `DELETE /v1/artifacts/{id}` unpublishes any live page before tombstoning the artifact, so a separate unpublish call is not required before deleting. * **Re-publish**: Calling `POST /v1/artifacts/{id}/publish` after unpublish creates a new page with the same `page_id` and URL. ## Example ```bash theme={null} # By artifact ID curl -X DELETE https://api.artifacta.io/v1/artifacts/art_abc123/publish \ -H "Authorization: Bearer ak_live_..." # By page slug curl -X DELETE https://api.artifacta.io/v1/artifacts/pg_aB3xK9mP1qR5sT2u/publish \ -H "Authorization: Bearer ak_live_..." ``` # Artifact Pages Source: https://docs.artifacta.io/artifact-pages Publish artifacts as shareable public web pages — rendered in the browser, no account required. Artifact Pages let you publish any stored artifact as a web page at `artifacta.io/a/{slug}`. The page is viewable by anyone with the URL — no Artifacta account required. Agents can publish a build report, a rendered chart, or a Markdown summary and hand the URL to a human in the same step — see [share an agent's report with a client](https://artifacta.io/use-cases/share-agent-reports) for a worked example. ## How it works 1. **Upload** an artifact with `POST /v1/artifacts` (or `artifacta push`, or `store_artifact` via MCP). 2. **Publish** it with `POST /v1/artifacts/{id}/publish`. The API mints a stable `page_id` (`pg_...`) and returns a `public_url`. 3. The viewer at `artifacta.io/a/{slug}` fetches page metadata via the `get_public_page` RPC (anon key, no tenant data exposed) and renders the content. 4. **Unpublish** at any time with `DELETE /v1/artifacts/{id}/publish`. The URL stops resolving immediately. The artifact itself is unaffected. The `page_id` (and therefore the URL) is stable across re-publishes. Calling publish again on the same artifact updates the title, visibility, and access mode without changing the URL. ## Content types and rendering The viewer selects a renderer based on the artifact's MIME type: | Content type | Renderer | Notes | | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `text/html` | Sandboxed iframe | `sandbox="allow-scripts"` — no `allow-same-origin`. Scripts run in a separate origin (`artifactausercontent.com`). | | `text/markdown`, `text/x-markdown` | Markdown | Rendered with `react-markdown` + `rehype-sanitize`. Inline images allowed (HTTPS and `data:` only). Links restricted to `https:` and `mailto:`. | | `image/*` | `` | SVG served as bytes, never inlined into the page. | | Anything else | Download card | A styled card with a download button. No inline rendering. | `application/xhtml+xml` is treated as HTML **for gating** — password protection is rejected for it at publish time, same as `text/html` — but it currently **renders as a download card**, not in the iframe (only `text/html` routes to the iframe). Markdown is also matched by the `.md`/`.markdown` filename extension, so an artifact stored as `text/plain` or `application/octet-stream` with one of those extensions still uses the Markdown renderer. Common image formats render inline via ``, including PNG, JPEG (and `.jfif`), GIF, WebP, SVG, and APNG. `.webp`, `.jfif`, and `.apng` are stored with the correct `image/*` type by a server-side content-type override even on hosts whose MIME database omits them. ## Content origin isolation Artifact bytes are never served from `artifacta.io` directly. They are served from a separate registrable domain: ``` https://artifactausercontent.com/c/{slug} ``` This is a Cloudflare Worker route. Serving bytes from a separate origin means: * Untrusted HTML or scripts in an artifact cannot access `artifacta.io` cookies or session data. * The iframe `sandbox` attribute provides a second layer: no `allow-same-origin` means the framed document cannot escalate past its sandbox even if the content-origin were compromised. * The Worker sets strict `Content-Security-Policy` and `X-Content-Type-Options: nosniff` headers. ## Provenance receipt Every published page renders a receipt — a small provenance panel beside the content that answers *who made this, with what, when*, built entirely from data already on the artifact: | Receipt field | Where the value comes from | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `agent_id` | `--agent` on `push`/`publish`, the `ARTIFACTA_AGENT_ID` env var, or MCP `store_artifact.agent_id` (which defaults to the connected MCP client's name). | | `model` | `metadata.model` — declared by the caller (`--model`, `--meta model=`, `ARTIFACTA_MODEL`, or MCP `model`), or captured automatically from a Claude Code session transcript. | | `content_hash` | SHA-256 of the artifact bytes, computed server-side at upload. | | `generated_at` | The artifact's creation timestamp. | Fields without a value are simply absent — publishing never invents provenance, and no schema or API change is involved: the receipt reads `metadata.model` directly. ### Declared vs. captured model values `metadata.model` carries one of two claim strengths: * **Declared** — the caller supplied the model ID (`--model`, `--meta model=`, `ARTIFACTA_MODEL`, or MCP `model`). Absence of `metadata.model_source` means declared. This is a declared producer claim. * **Captured** — the model ID was machine-read from the agent runtime's own session log by the Claude Code SessionEnd hook or the `capture-transcript` skill, and marked `metadata.model_source=transcript`. Artifacta records the model automatically from the agent runtime's own session log and freezes it at store time — a captured producer claim, not a cryptographic attestation. Artifacta does not offer attestation of model identity (cryptographic or provider-signed proof). Neither tier proves which model authored a specific non-transcript artifact's bytes — the receipt reports what the producer declared or what the runtime's log recorded. Auto-capture exists for Claude Code transcript paths only; agents on other runtimes declare the model explicitly. See [model capture](/guides/transcripts#model-capture) for the extraction rules and their limits. ## Visibility | Value | Behavior | | ---------------------- | ----------------------------------------------------------------------------------------------- | | `"unlisted"` (default) | The page is accessible via URL but is not indexed or listed in any gallery. | | `"public"` | Reserved for future gallery / search features. Functionally the same as `"unlisted"` at launch. | ## Password protection Pages can be protected with a passcode (Pro plan). The access flow: 1. The viewer calls `GET /v1/public/pages/{slug}/gate-info` to check whether a password prompt is needed. 2. The visitor submits the password. 3. The viewer calls `POST /v1/public/pages/{slug}/unlock` with `{"password": "..."}`. 4. The API verifies the passcode (argon2id) and returns a `content_token` — a short-lived JWT (5 minutes). 5. The viewer passes the token to the content-origin Worker, which serves the gated bytes. Password protection is not available for HTML artifacts (`text/html`, `application/xhtml+xml`). The restriction is enforced at publish time. Brute-force is mitigated by a per-slug attempt cap on the API plus a per-IP edge rate-limit rule. An incorrect password returns the same `unauthorized` error as a missing or unpublished slug — the endpoint never reveals whether a slug exists. After unlock, gated (password-protected) content selects its renderer from the artifact's **stored MIME type only** — there is no filename-extension fallback on this path. So, for example, Markdown renders after unlock only when it is stored as `text/markdown`. The platform ensures this for `.md` and `.markdown` uploads via a server-side content-type override, so artifacts uploaded with those extensions render correctly whether or not the page is password-protected. ## Abuse reporting Any visitor can report a page via `POST /v1/public/pages/{slug}/report`. The endpoint: * Accepts an optional `reason` (`"spam"`, `"phishing"`, `"malware"`, `"abuse"`, `"copyright"`, `"other"`) and a free-text `detail`. * Always returns `202 Accepted`, regardless of whether the slug exists. * Forwards the report to PostHog (and optional Slack webhook) for operator review. * Does not automatically take down the page. Confirmed violations are unpublished by an operator. ## Indexing policy All Artifact Pages are served with `noindex` at launch. They do not appear in search engine results. ## API reference | Action | Endpoint | | ------------------ | -------------------------------------------------------------- | | Publish | [`POST /v1/artifacts/{id}/publish`](/api/publish-artifact) | | Unpublish | [`DELETE /v1/artifacts/{id}/publish`](/api/unpublish-artifact) | | Gate info (public) | [`GET /v1/public/pages/{slug}/gate-info`](/api/gate-info) | | Unlock (public) | [`POST /v1/public/pages/{slug}/unlock`](/api/unlock-page) | | Report (public) | [`POST /v1/public/pages/{slug}/report`](/api/report-page) | ## CLI ```bash theme={null} # Publish a file artifacta publish report.pdf --title "Q2 Report" --public # Unpublish by artifact ID or page slug artifacta unpublish art_abc123 artifacta unpublish pg_aB3xK9mP1qR5sT2u ``` ## Python SDK ```python theme={null} from artifacta import Client client = Client() # Upload and publish in two steps artifact = client.push("report.pdf") page = client.publish_artifact( artifact.id, title="Q2 Report", visibility="public", ) print(page["public_url"]) # Unpublish client.unpublish(artifact.id) ``` ## MCP (agent usage) ``` "Upload ./out/report.html and publish it as an unlisted page. Return the URL." ``` The agent calls `store_artifact` then `publish_artifact`. The `public_url` is available in the tool result immediately. ``` "Unpublish the page for artifact art_abc123." ``` The agent calls `unpublish_artifact` with `artifact_id: "art_abc123"`. # Authentication Source: https://docs.artifacta.io/authentication Authenticate the CLI, SDK, and API with your API key. Artifacta authenticates via API key. Every request — CLI, SDK, and REST API — requires a valid key. ## Get your API key 1. Sign up at [app.artifacta.io/signup](https://app.artifacta.io/signup) 2. Complete onboarding — a key is auto-generated for you 3. Or create additional keys at [app.artifacta.io/dashboard/keys](https://app.artifacta.io/dashboard/keys) Your full API key is shown **once** at creation time. Copy it immediately. You cannot retrieve it later — only the last 4 characters are visible after creation. ## Key format ``` ak_live_<32 alphanumeric characters> ``` Example: `ak_live_x9f7v3m1p0q2r4s6t8u0w1y3z5a7b9c2` ## Authenticate the CLI ```bash theme={null} export ARTIFACTA_API_KEY="ak_live_abc123..." ``` Best for agents and CI. Every sub-process inherits the key automatically. ```bash theme={null} artifacta auth login # Enter your API key: ak_live_abc123... # ✓ Authenticated as tenant "acme-corp" ``` Stores in `~/.config/artifacta/config.toml`. ```bash theme={null} echo "$ARTIFACTA_API_KEY" | artifacta auth login ``` Reads the key from stdin when stdout isn't a TTY. Won't hang on empty stdin and keeps the secret out of process args (where `--key` would expose it to `ps`). ```bash theme={null} artifacta auth login --key ak_live_abc123 ``` Convenient for one-off setups. Note: the key appears in shell history and `ps` output — prefer the stdin pipe for CI. **Priority order:** `ARTIFACTA_API_KEY` env var > config file. ## Authenticate the Python SDK ```python theme={null} from artifacta import Client # Option 1: Auto-detect from environment or config file client = Client() # Reads ARTIFACTA_API_KEY from env, then ~/.config/artifacta/config.toml # Option 2: Explicit key client = Client(api_key="ak_live_abc123") ``` ## Authenticate the REST API ```bash theme={null} curl https://api.artifacta.io/v1/artifacts \ -H "Authorization: Bearer ak_live_abc123" ``` Every API request requires the `Authorization: Bearer ` header. ## Verify authentication ```bash theme={null} artifacta whoami ``` ```text Output theme={null} Tenant: acme-corp Plan: free Key: ...k9f7 (last 4) Usage: 0 / 10,000 requests this month Storage: 0 B / 1 GB ``` Or in Python: ```python theme={null} info = client.whoami() print(info.tenant_name) # "acme-corp" print(info.plan) # "free" ``` ## Key limits | Plan | Max active keys | | ---- | --------------- | | Free | 10 | | Pro | 50 | Revoked keys don't count toward the limit. Revoke unused keys at [app.artifacta.io/dashboard/keys](https://app.artifacta.io/dashboard/keys). ## Security notes * Keys are hashed (SHA-256) before storage — Artifacta never stores your full key * A revoked key is immediately unusable * Keys do not expire — revoke manually when no longer needed * All keys have full tenant access (no per-key permission scopes in V1) # CLI Overview Source: https://docs.artifacta.io/cli/overview Global behavior, environment variables, and output conventions for the Artifacta CLI. ## Commands **Discoverable help:** `artifacta --help` shows flags, exit codes, and a worked Examples block on every command. The root `artifacta --help` also lists every `ARTIFACTA_*` environment variable, the four exit codes, and a Common workflow guide. | Command | Description | | -------------- | ----------------------------------------------------------- | | `push` | Upload a file as an artifact | | `pull` | Download an artifact | | `ls` | List artifacts with filters | | `inspect` | Show detailed artifact metadata | | `link` | Create a temporary download URL | | `rm` | Delete artifacts | | `session ls` | List sessions with artifact counts and seal status | | `session new` | Generate a new session ID (local operation, no server call) | | `session seal` | Seal a session (prevent new uploads) | | `whoami` | Show tenant info and usage | | `publish` | Upload a file and publish it as a shareable public page | | `unpublish` | Take down an artifact's public page | | `auth login` | Authenticate with an API key | | `config` | Read/write CLI configuration | Use `push --transcript` and `ls --transcript` to store and filter a session's conversation; see the [session transcript guide](/guides/transcripts) for precedence and automation. ## Artifact Pages commands ### `publish` Uploads a local file as an artifact and immediately publishes it as a shareable page. Visibility defaults to `unlisted` (accessible via URL, not indexed). The page URL is printed to stdout. | Flag | Default | Description | | ------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `--title TEXT` | — | Page title shown in the viewer header. | | `--public` | — | Make the page discoverable (mutually exclusive with `--unlisted`). | | `--unlisted` | default | Make the page unlisted / URL-only (default when neither flag is set). | | `--password TEXT` | — | Password-protect the page. Pro plan required. | | `--session TEXT` | env `ARTIFACTA_SESSION_ID` | Attach to a session (same fallback as `push`). | | `--agent TEXT` | env `ARTIFACTA_AGENT_ID` | Agent ID — shown in the page's provenance receipt. | | `--meta KEY=VALUE` | — | Metadata key=value (repeatable). | | `--model TEXT` | env `ARTIFACTA_MODEL` | Shorthand for `--meta model=`; mutually exclusive with it. Shown in the page's provenance receipt as a declared producer claim. | | `--ttl TEXT` | env `ARTIFACTA_TTL` | Override default TTL. | | `--json` | — | Output full response as JSON. | | `--human` | — | Force human-readable output even when stdout is piped. | ```bash theme={null} # Publish a file (unlisted by default) artifacta publish report.pdf # Publish with a title and make it publicly discoverable artifacta publish report.pdf --title "Q2 Report" --public # Publish with password protection (Pro plan) artifacta publish data.json --password hunter2 # Populate the page's provenance receipt artifacta publish report.pdf --agent earnings-bot --model claude-fable-5 # Capture the page URL in a script PAGE_URL=$(artifacta publish report.pdf --json | jq -r '.public_url') ``` ### `unpublish` Takes down an artifact's public page. The underlying artifact is not deleted. Accepts an artifact ID (`art_...`) or a page slug (`pg_...`). The `page_id` is printed to stdout on success. | Flag | Default | Description | | --------- | ------- | ------------------------------------------------------ | | `--json` | — | Output full response as JSON. | | `--human` | — | Force human-readable output even when stdout is piped. | ```bash theme={null} artifacta unpublish art_abc123def456 artifacta unpublish pg_slug123 artifacta unpublish art_abc123def456 --json ``` ## Global behavior | Behavior | Detail | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Output format** | Human-readable by default. `--json` for JSON. Auto-JSON when stdout is piped. `--human` to force human output in pipes. | | **Exit codes** | `0` success, `1` client error, `2` server error, `3` network error | | **Config file** | `~/.config/artifacta/config.toml` | | **Auth priority** | `ARTIFACTA_API_KEY` env var > config file | | **Destructive actions** | `artifacta rm` prompts for confirmation; pass `--yes` (or `--force`) to skip the prompt in scripts, or `--dry-run` to preview what would be deleted without committing. `artifacta session seal` is irreversible — sealed sessions cannot accept new artifacts. | **Auto-JSON for agents:** When stdout is not a TTY (piped or redirected), the CLI automatically outputs JSON. Human-readable output goes to stderr, data to stdout. This means `artifacta ls | jq` just works. ## Destructive actions The CLI carries verbatim warnings on every destructive command. Read them via `--help` once before a script run — the wording below is the exact text the CLI prints, and is part of the user-facing contract. **`artifacta rm --help`** — Warning: `--session` deletes every artifact in that session. Use `--dry-run` to preview. **`artifacta session seal --help`** — Warning: Sealing is irreversible. Sealed sessions cannot accept new artifacts. ## Environment variables | Variable | Default | Description | | ---------------------- | -------------------------- | -------------------------------------------------------------------------- | | `ARTIFACTA_API_KEY` | — | API key (overrides config file) | | `ARTIFACTA_API_URL` | `https://api.artifacta.io` | API base URL | | `ARTIFACTA_OUTPUT` | `human` | Output format: `human` or `json` | | `ARTIFACTA_TTL` | `30d` | Default TTL for uploads | | `ARTIFACTA_SESSION_ID` | — | Default session\_id for `push` (inherited by sub-processes) | | `ARTIFACTA_AGENT_ID` | — | Default agent\_id for `push` (inherited by sub-processes) | | `ARTIFACTA_MODEL` | — | Default `metadata.model` for `push`/`publish` (inherited by sub-processes) | | `ARTIFACTA_TELEMETRY` | — | Set to `1` to enable Sentry error reporting (opt-in) | **Agent integration pattern:** An orchestrator sets `ARTIFACTA_SESSION_ID` (and optionally `ARTIFACTA_MODEL`) once in the environment. Every sub-agent inherits them automatically — zero flag passing required. Explicit `--model` or `--meta model=` always beats the environment value. ```bash theme={null} # Orchestrator sets the session once export ARTIFACTA_SESSION_ID=$(artifacta session new) export ARTIFACTA_MODEL=claude-fable-5 # declared model for every push # Every agent inherits it python agent_a.py # push calls tagged with session automatically python agent_b.py # same session, zero config ``` ## Shell scripting patterns ```bash theme={null} # Upload all CSVs from a directory, capture artifact IDs for f in ./output/*.csv; do artifacta push "$f" --session batch_20260313 | jq -r '.artifact_id' done > artifact_ids.txt # Download all artifacts from a session artifacta ls --session batch_20260313 | \ jq -r '.artifacts[].artifact_id' | \ xargs -I{} artifacta pull {} -o ./downloads/ # Generate share links for all artifacts in a session artifacta ls --session batch_20260313 | \ jq -r '.artifacts[].artifact_id' | \ xargs -I{} artifacta link {} --json | jq -r '.url' # Network-resilient push — auto-idempotency dedupes (same content + # filename + session + agent), so retries never create duplicate artifacts. for i in 1 2 3; do artifacta push report.pdf --session batch_20260313 && break sleep 2 done ``` # Core Concepts Source: https://docs.artifacta.io/concepts The mental model behind Artifacta: tenants, artifacts, sessions, and metadata. ## Mental model ``` Tenant ├── API Key(s) └── Artifacts ├── art_abc ── filename, metadata, session_id, agent_id │ └── blob (content-addressed, deduplicated per tenant) ├── art_def ── same session, different agent │ └── blob └── art_ghi ── different session └── blob (may share storage with art_abc if identical content) Sessions are optional labels. They can be sealed to prevent late uploads. Agents are optional labels. No server-side management for either. ``` ## Core nouns An isolated account. All artifacts, API keys, and usage belong to exactly one tenant. Created automatically when you sign up. Think of it as your project or team. The core object. A single uploaded file with metadata. Has a stable ID (`art_` + 16 alphanumeric chars), a filename, content type, metadata, and belongs to a tenant. Every upload creates a new artifact — no implicit versioning. An optional, user-defined string grouping artifacts from the same run or workflow. Example: `"pipeline_run_42"`. Not managed by Artifacta — just a label. Sessions can be **sealed** to prevent late uploads. Store the run's conversation beside its outputs with the [session transcript guide](/guides/transcripts). An optional, user-defined string identifying which agent produced an artifact. Example: `"earnings_analyst"`. Just a label — not managed server-side. A JSON object of key-value pairs attached at upload time. Max 8 KB. Keys must match `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$` — **no dots allowed**. Filterable via the API (one key-value exact match per query). By convention a few keys carry ecosystem meaning: `type=transcript` marks session transcripts, `model` / `model_source` / `models_used` carry model provenance (see [model capture](/guides/transcripts#model-capture)), and `capture` distinguishes mid-session snapshots from end-of-session captures. SHA-256 hash of file contents. Used for blob-level deduplication within a tenant. Two artifacts with the same content share one blob in storage — invisible to you, saves space. Time-to-live. Default: 30 days. Can be overridden per artifact (`--ttl 7d`, `--ttl 90d`, `--ttl never`). A temporary, unauthenticated URL (`https://dl.artifacta.io/lnk_xxx`) for sharing artifacts with humans. Default: 7 days. Max: 30 days. ## Key design decisions **Every upload creates a new artifact.** Uploading `report.pdf` twice produces two distinct artifacts with two IDs. No implicit versioning. **Artifacts are immutable.** Once uploaded, you cannot change content, filename, metadata, or TTL. To correct something, upload a new artifact. **Sessions are labels, not managed objects.** There is no `POST /v1/sessions`. A session exists when at least one artifact references that `session_id`. Sealing is the only explicit session operation. ## Artifact lifecycle ``` push → active → expires → garbage collected → [delete] → soft-deleted → hard-deleted (30 days) ``` * **Soft delete:** `DELETE` sets `deleted_at`. Artifact disappears from listings and downloads immediately. * **Hard delete:** Background job removes the Postgres row and R2 blob 30 days after soft delete. * **Expiration:** Artifacts past their `expires_at` return `410 Gone`. Garbage collected automatically. ## Deduplication Two separate mechanisms: | Layer | Mechanism | What it does | | ---------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Blob storage** | Content-hash (SHA-256) per tenant | Two artifacts with identical content share one blob in R2. Saves storage. Invisible to you. | | **API** | Idempotency key (24h TTL) | Same `Idempotency-Key` header within 24h returns the original response. No content comparison. Prevents duplicate artifacts on retry. | | **CLI** | Auto-generated `Idempotency-Key` on `push` | The CLI auto-derives a key from `(content + filename + session + agent)` so accidental retries of the same `artifacta push` dedupe by default — no `--idempotency-key` flag needed. Pass `--idempotency-key` explicitly to override. | # Error Reference Source: https://docs.artifacta.io/errors Every error code the Artifacta API returns, with recommended agent actions. ## Error response shape Every error returns this structure: ```json theme={null} { "error": { "code": "artifact_not_found", "message": "Artifact art_xxx was not found.", "status": 404 } } ``` * **`code`** — stable, machine-readable string. Match on this. Part of the V1 API contract. * **`message`** — human-readable. May change without notice. Do not parse. * **`status`** — HTTP status code (duplicated in the body for convenience). For rate limit errors, an additional field is included: ```json theme={null} { "error": { "code": "rate_limited", "message": "Too many requests. Try again in 5 seconds.", "status": 429, "retry_after_seconds": 5 } } ``` ## Complete error code reference | Code | HTTP | Meaning | Agent action | | -------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `invalid_request` | 400 | Malformed body, missing fields, invalid metadata key | **Abort.** Fix the request. Same input always fails. | | `ttl_exceeds_plan_limit` | 400 | TTL exceeds plan max (Free: 90d, Pro: 365d) | **Retry with shorter TTL** or escalate for upgrade. | | `upload_not_found` | 400 | `complete` called but blob missing in R2 | **Retry the PUT upload**, then call `complete` again. | | `unauthorized` | 401 | Invalid, missing, or revoked API key | **Abort immediately.** Do not retry. Check or rotate key. | | `quota_exceeded` | 403 | Storage or request quota reached. Also returned when a Free-tier tenant attempts to publish a password-protected Artifact Page (`access: "password"` requires Pro). | **Abort.** Delete old artifacts or upgrade plan. | | `artifact_not_found` | 404 | No artifact with this ID for this tenant | **Abort.** Stale reference. Re-query to find correct artifact. | | `session_not_found` | 404 | No artifacts exist for this session (seal endpoint only) | **Abort.** Verify the session ID. | | `session_sealed` | 409 | Upload to a sealed session | **Use a different session.** No unseal operation exists. | | `artifact_expired` | 410 | Artifact's TTL has passed | **Abort.** Re-generate from source. Cannot recover. | | `artifact_already_deleted` | 410 | Artifact was soft-deleted | **Treat as success** if intent was to delete. Otherwise, artifact is gone. | | `file_too_large` | 413 | Exceeds size limit | **Switch method.** Direct: 500 MB max. Presigned: 5 GB max. | | `rate_limited` | 429 | Burst limit exceeded (100 req/s sustained). Also returned by the Artifact Pages `/unlock` and `/report` endpoints after the per-slug/per-IP attempt cap is reached. | **Retry after `retry_after_seconds`.** Implement exponential backoff. | These error codes are part of the V1 API contract. Existing codes will not be removed or renamed without a new API version. New codes may be added (non-breaking). ## SDK exception mapping The Python SDK maps each error code to a typed exception: | Exception | API error code | | -------------------------- | ------------------------------------------------ | | `ArtifactaError` | Base class for all errors | | `InvalidRequestError` | `invalid_request` | | `UnauthorizedError` | `unauthorized` | | `QuotaExceededError` | `quota_exceeded` | | `ArtifactNotFoundError` | `artifact_not_found` | | `ArtifactExpiredError` | `artifact_expired` | | `SessionSealedError` | `session_sealed` | | `TTLExceedsPlanLimitError` | `ttl_exceeds_plan_limit` | | `RateLimitedError` | `rate_limited` (includes `.retry_after_seconds`) | ```python theme={null} from artifacta import Client, ArtifactNotFoundError, ArtifactExpiredError client = Client() try: artifact = client.get("art_2xk9f7v3m1p0") except ArtifactNotFoundError: print("Artifact was never created or ID is wrong") except ArtifactExpiredError: print("Artifact existed but TTL passed — re-generate from source") ``` ## CLI exit codes | Exit code | Meaning | | --------- | ------------------------------------------------- | | `0` | Success | | `1` | Client error (bad input, auth failure, not found) | | `2` | Server error | | `3` | Network error | # Store session transcripts Source: https://docs.artifacta.io/guides/transcripts Store, filter, and automatically capture session transcripts with Artifacta. Artifacta stores transcripts as ordinary, opaque artifacts. Transcript sugar fills the conventional `metadata.type="transcript"` tag and the `application/x-ndjson` content type without adding a transcript resource, endpoint, or server-side schema. Start with the checked-in [recommended NDJSON example](https://github.com/SagaPeak/artifacta/blob/main/docs/examples/transcript_example.ndjson), or store your agent's existing transcript format. ## Use one session for the transcript and its outputs Give the transcript and every artifact produced during a run the same session ID. You can then list the whole run or filter it to the transcript alone. ```bash theme={null} artifacta push report.pdf --session run_42 artifacta push transcript.ndjson --session run_42 --transcript # Everything produced in the session artifacta ls --session run_42 # Only artifacts tagged as transcripts artifacta ls --session run_42 --transcript ``` `--transcript` defaults the upload to `content_type=application/x-ndjson` and adds `metadata.type=transcript`. It does not change output, errors, TTL, sealing, deduplication, idempotency, or upload limits. ## Explicit values take precedence Write precedence is independent for the two defaults: 1. An explicit content type always wins; otherwise a true transcript flag supplies `application/x-ndjson` before filename MIME guessing. 2. The presence of an explicit metadata `type` key always wins, including `type=""`; otherwise a true transcript flag supplies `type="transcript"`. 3. When the flag is omitted or false, existing MIME guessing and metadata behavior remain unchanged. On list calls, an explicit metadata `type` filter wins, including an empty value. Otherwise a true transcript flag adds exactly one `metadata.type=transcript` filter. Other metadata is preserved in every case. ```bash theme={null} # Explicit content type wins independently. artifacta push transcript.jsonl --session run_42 --transcript \ --content-type application/jsonl # Explicit metadata type wins independently. artifacta push transcript.ndjson --session run_42 --transcript \ --meta type=custom_transcript # The explicit list filter wins; this filters custom_transcript artifacts. artifacta ls --session run_42 --transcript --meta type=custom_transcript ``` ## Python SDK `transcript` is a keyword-only boolean on the existing `push()` and `list()` methods. Their public signatures are: ```python theme={null} def push( self, path: str | Path | None = None, *, content: bytes | None = None, filename: str | None = None, content_type: str | None = None, session_id: str | None = None, agent_id: str | None = None, metadata: dict[str, str] | None = None, ttl: str | None = None, idempotency_key: str | None = None, presigned: bool = False, transcript: bool = False, ) -> Artifact: ... ``` ```python theme={null} def list( self, *, session_id: str | None = None, agent_id: str | None = None, metadata: dict[str, str] | None = None, limit: int = 50, auto_paginate: bool = True, cursor: str | None = None, transcript: bool = False, ) -> ListResult: ... ``` Use either a path or the existing `content` plus `filename` form: ```python theme={null} from artifacta import Client client = Client() artifact = client.push( path="transcript.ndjson", session_id="run_42", transcript=True, ) transcripts = client.list(session_id="run_42", transcript=True) ``` ## MCP tools Both the TypeScript and Python MCP servers expose the same optional boolean on the existing tools. `store_artifact` remains a `writeIdempotent` tool, and `list_artifacts` remains a `safe` tool. ```json store_artifact arguments theme={null} { "filename": "transcript.ndjson", "path": "/workspace/transcript.ndjson", "session_id": "run_42", "transcript": true } ``` ```json list_artifacts arguments theme={null} { "session_id": "run_42", "transcript": true } ``` The write flag works with both MCP content and path uploads. The same explicit-value precedence above applies to `content_type` and `metadata.type`; list calls preserve an explicit `metadata.type` filter. ## Call the REST API directly The REST API is unchanged. API-direct callers express the convention with existing `metadata`, `content_type`, and metadata-filter parameters. The API does not infer NDJSON from `metadata.type` alone, so send the desired content type explicitly. ### Multipart upload Multipart metadata is a JSON string. `file` and `content_type` are separate existing form fields. ```bash theme={null} curl -X POST https://api.artifacta.io/v1/artifacts \ -H "Authorization: Bearer $ARTIFACTA_API_KEY" \ -F 'file=@transcript.ndjson' \ -F 'session_id=run_42' \ -F 'metadata={"type":"transcript"}' \ -F 'content_type=application/x-ndjson' ``` ### JSON-body upload JSON uploads carry base64 content, `content_encoding="base64"`, a filename, object metadata, and an explicit content type. ```bash theme={null} CONTENT_B64="$(base64 < transcript.ndjson | tr -d '\n')" jq -n --arg content "$CONTENT_B64" '{ content: $content, content_encoding: "base64", filename: "transcript.ndjson", session_id: "run_42", metadata: {type: "transcript"}, content_type: "application/x-ndjson" }' | curl -X POST https://api.artifacta.io/v1/artifacts \ -H "Authorization: Bearer $ARTIFACTA_API_KEY" \ -H 'Content-Type: application/json' \ --data-binary @- ``` ### List transcripts Use the existing metadata query and URL-encode the query string: ```bash theme={null} curl --get https://api.artifacta.io/v1/artifacts \ -H "Authorization: Bearer $ARTIFACTA_API_KEY" \ --data-urlencode 'session_id=run_42' \ --data-urlencode 'metadata.type=transcript' ``` There is no REST transcript field or query parameter on create or list requests. ## Capture Claude Code sessions automatically This recipe uses Claude Code's client-side `SessionEnd` hook to invoke the same Artifacta CLI push shown above. Artifacta does not run a listener or provide a separate capture service. **In Claude Code with the Artifacta plugin:** install with `/plugin marketplace add SagaPeak/artifacta-mcp` then `/plugin install artifacta@artifacta`, and ask in natural language — for example, *"use artifacta to upload this session's transcript"*. The plugin's `capture-transcript` skill locates and verifies the live session transcript, pushes a snapshot, and can offer to set up this hook for you. ### Prerequisites * Install and authenticate the `artifacta` CLI, and ensure `artifacta` is on `PATH`. * Install `jq` and ensure it is on `PATH`. * The documented hook expects JSON on stdin with `session_id`, `transcript_path`, and `reason`. It reads the first two fields; `transcript_path` must name an existing file. Add this hook to `~/.claude/settings.json`: ```json theme={null} { "hooks": { "SessionEnd": [ { "hooks": [ { "type": "command", "command": "~/.claude/hooks/push-transcript.sh" } ] } ] } } ``` Create `~/.claude/hooks/push-transcript.sh` with these exact contents: ```bash theme={null} #!/usr/bin/env bash set -euo pipefail INPUT="$(cat)" SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')" TRANSCRIPT_PATH="$(echo "$INPUT" | jq -r '.transcript_path')" if [[ -z "$SESSION_ID" || "$SESSION_ID" == "null" ]]; then echo "artifacta SessionEnd hook: missing session_id" >&2 exit 1 fi if [[ -z "$TRANSCRIPT_PATH" || "$TRANSCRIPT_PATH" == "null" || ! -f "$TRANSCRIPT_PATH" ]]; then echo "artifacta SessionEnd hook: missing transcript file: ${TRANSCRIPT_PATH:-}" >&2 exit 1 fi # Model capture (best-effort): metadata.model is the LAST MAIN-LOOP assistant # model — sidechain (subagent) entries and "" placeholders are # excluded. models_used lists every distinct model observed, sidechains # included. Extraction failure degrades to a plain push, never a lost one. MODEL="$(jq -r 'select(.type == "assistant" and .isSidechain != true) | .message.model // empty | select(. != "")' "$TRANSCRIPT_PATH" 2>/dev/null | tail -n 1 || true)" MODELS_USED="$(jq -r 'select(.type == "assistant") | .message.model // empty | select(. != "")' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u | paste -sd, - || true)" if [[ -n "$MODEL" ]]; then artifacta push "$TRANSCRIPT_PATH" --session "$SESSION_ID" --transcript --meta "model=$MODEL" --meta "model_source=transcript" --meta "models_used=$MODELS_USED" else artifacta push "$TRANSCRIPT_PATH" --session "$SESSION_ID" --transcript fi ``` Make the script executable: ```bash theme={null} mkdir -p ~/.claude/hooks chmod 700 ~/.claude/hooks/push-transcript.sh ``` Malformed JSON causes `jq` to fail. Missing, null, or empty required fields and a nonexistent transcript file also fail validation. Any non-zero Artifacta CLI exit propagates as a non-zero Claude Code hook failure. Input validation and the Artifacta push still fail hard: there are no retries and no swallowed push errors. Model extraction alone is best-effort; if it fails or finds no main-loop model, the script pushes the transcript without model metadata rather than losing the upload. `SessionEnd`, its lifecycle, and the `session_id`, `transcript_path`, and `reason` stdin fields are an external contract owned by Anthropic. Artifacta does not pin or abstract that contract. Re-check Anthropic's hook documentation after upgrading Claude Code. ## Model capture The hook script above also records which model produced the session, straight from the transcript itself: * `metadata.model` — the last **main-loop** assistant model in the transcript. Sidechain (subagent) entries and synthetic placeholders are excluded via the transcript's `isSidechain` marker, so a session that dispatched subagents on a different model still attributes to the orchestrating model — the model that directed the work and decided what got stored. * `metadata.model_source=transcript` — marks the value as machine-captured. Absence of `model_source` means the model was declared by the caller. * `metadata.models_used` — every distinct model observed in the transcript, comma-separated (e.g. `claude-fable-5,claude-sonnet-5`), including subagent sidechains. Artifacta records the model automatically from the agent runtime's own session log and freezes it at store time — a captured producer claim, not a cryptographic attestation. Two honest limits: session-level capture approximates per-artifact authorship — the transcript proves which models participated in the session, not which one emitted a specific artifact's bytes; and subagents spawned as separate sessions don't appear in the main transcript, so `models_used` is "models observed", never exhaustive. For pushes outside the hook, declare the model yourself: `artifacta push report.pdf --model claude-fable-5` (shorthand for `--meta model=`), or export `ARTIFACTA_MODEL` once in a wrapper script — explicit flags always beat the environment. If a wholly-delegated subagent authored the artifact, pass that subagent's model explicitly. Published pages render `metadata.model` in the page receipt automatically. ## Verify a captured transcript ```bash theme={null} artifacta ls --session run_42 --transcript artifacta pull -o ./captured-transcript.ndjson cmp transcript.ndjson ./captured-transcript.ndjson ``` An unfiltered session list should include both the transcript and the artifacts produced by the run. The transcript-filtered list should include only artifacts whose explicit metadata type is `transcript`. For a later native Claude Code lifecycle check, use the repository's [reproducible `SessionEnd` operator validation](https://github.com/SagaPeak/artifacta/blob/main/docs/qa/mcp/transcript-v1-claude-code-validation.md). That optional follow-up is not a PR blocker: automated exact-hook tests and live execution with canonical synthetic stdin already passed. ## Security: audit before you push Artifacta does **not** automatically scan, redact, or block secrets in transcript uploads. Callers are responsible for reviewing and redacting transcript content before storage. For an optional source-checkout audit, run the repository's existing scanner against a directory before pushing it: ```bash theme={null} cd /path/to/artifacta/mcp/typescript npx tsx scripts/secret-audit.ts /path/to/transcript-directory ``` The scanner exits `0` when clean, `1` when it finds credential-shaped content, and `2` for a usage error. It reports locations and pattern names without echoing matched values. The repeatable `--allow ` option can suppress a known benign match. This user-run audit is optional, is available only from a source checkout, and is not an Artifacta push gate; you may wire it into your own pre-push hook if desired. ## Codex plugin Artifacta plugin `1.1.0` supports verified transcript capture in Codex. Install and authenticate it with the [Codex plugin guide](/mcp/install/codex-plugin), then start a new thread so Codex loads the plugin's skills and hooks. For an immediate snapshot, ask: > "Use Artifacta to capture this Codex session's transcript." The `capture-transcript` skill searches Codex's rollout files for a distinctive phrase from the current conversation and refuses to continue unless it identifies exactly one regular file. It copies that live rollout to a private snapshot, uploads the snapshot through Artifacta MCP `store_artifact`, and removes the private copy after a successful upload. Codex transcript capture never invokes the local Artifacta CLI or sends a local filesystem path to the hosted MCP server. For one capture at the current thread's next `Stop`, include the explicit flag: > "Use Artifacta to capture this Codex session's transcript --automatic." `--automatic` arms one unredacted snapshot for this thread only. It does not enable every-turn capture, future-thread capture, or a background uploader. Open `/hooks`, review the bundled Artifacta hook, and trust its current definition; Codex skips untrusted non-managed hooks. An immediate mid-turn snapshot may not include the request that triggered capture or the assistant response reporting its result. Retrieve captured records with `list_artifacts` using the returned session ID and `transcript=true`. Codex snapshots are uploaded without redaction and can include prompts, tool arguments, tool results, credentials, and other sensitive text. Review the session before requesting capture. ## Limitations and hard exclusions * Transcript payloads remain opaque bytes. NDJSON is recommended, not a mandatory schema. * There is no incremental, streaming, chunked, or per-turn upload/reassembly behavior. * There are no dedicated `push_transcript`, `get_transcript`, `pull-transcript`, or MCP `get_transcript` APIs or tools. * There is no server-side agent-session detection or continuous capture mechanism. The Claude Code and Codex plugin skills perform verified client-side capture. * There is no server-side enforcement of the `metadata.type` convention and no transcript web viewer. * Existing artifact limits, retention/TTL choices, session seals, and errors apply unchanged. # Installation Source: https://docs.artifacta.io/installation Install the Artifacta CLI and Python SDK. ## Install ```bash pip (recommended) theme={null} pip install artifacta-cli ``` ```bash Homebrew theme={null} brew install artifacta/tap/artifacta ``` ```bash curl theme={null} curl -fsSL https://get.artifacta.io | sh ``` `pip install artifacta-cli` installs **both** the CLI binary and the Python SDK. You get `artifacta` on your PATH and `from artifacta import Client` in Python. ## Requirements | Method | Requires | | -------- | ----------------------------- | | pip | Python 3.10+ | | Homebrew | macOS or Linux with Homebrew | | curl | macOS or Linux (amd64, arm64) | ## Verify installation ```bash theme={null} artifacta --version ``` ```text Output theme={null} artifacta, version 0.2.0 ``` ## Upgrade ```bash pip theme={null} pip install --upgrade artifacta-cli ``` ```bash Homebrew theme={null} brew upgrade artifacta ``` ```bash curl theme={null} curl -fsSL https://get.artifacta.io | sh ``` ## What's included | Component | What you get | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | **CLI** | `artifacta push`, `pull`, `ls`, `inspect`, `link`, `rm`, `session`, `whoami`, `auth`, `config` | | **Python SDK** | `from artifacta import Client` — `push()`, `pull()`, `pull_bytes()`, `push_dict()`, `list()`, `get()`, `delete()`, `create_link()`, `seal_session()` | Both share a codebase and are always installed together. There is no separate CLI-only or SDK-only package. # Introduction Source: https://docs.artifacta.io/introduction Artifacta is the artifact store purpose-built for AI agents. ## What Artifacta is Artifacta is the artifact store purpose-built for AI agents. One CLI command or API call to push, pull, list, and share the files your agents produce — with session grouping, content dedup, metadata filtering, and automatic expiration. **Who it's for:** Developers building AI agent pipelines. Solo builders, startup engineers, and platform teams who need durable artifact storage without writing S3 glue code. For concrete scenarios — sharing agent reports with clients, multi-agent file handoff, publishing from Claude Code — see the [use cases](https://artifacta.io/use-cases). **What it replaces:** Custom S3 wrappers, `/tmp` directories, shared NFS mounts, and the 80 lines of Python you wrote to manage file output paths. See how Artifacta compares to [Claude Artifacts and a DIY S3 wrapper](https://artifacta.io/compare/claude-artifacts). ## What Artifacta is not Artifacta is **not** a database, a filesystem, a workflow orchestrator, or a collaboration tool. It stores artifacts between pipeline steps. It does not: * Orchestrate workflow steps (use Temporal, Inngest, etc.) * Search inside file contents (use a search service downstream) * Manage real-time collaborative editing * Scan files for malware or extract text from PDFs ## How it works ``` Your Agent → artifacta push report.pdf → Stored in Artifacta ├── Deduplicated (content-hash) ├── Tagged (session, agent, metadata) ├── Expiring (default 30 days) └── Shareable (download links) Another Agent → artifacta pull art_xxx → Gets the file back A Human → dl.artifacta.io/lnk_xx → Downloads via browser ``` ## Three ways to use it `artifacta push`, `pull`, `ls`, `rm` — Unix-style commands for terminal and scripts. `client.push()`, `pull_bytes()`, `push_dict()` — native Python for agents. `POST /v1/artifacts` — integrate from any language. ## Plans | | Free | Pro (\$20/mo) | | ---------- | -------------- | --------------- | | Storage | 1 GB | 50 GB | | Requests | 10,000 / month | 100,000 / month | | Max TTL | 90 days | 365 days | | API keys | 10 | 50 | | Rate limit | 100 req/s | 100 req/s | No credit card required for Free. Full details on the [pricing page](https://artifacta.io/pricing). Upgrade from the [dashboard](https://app.artifacta.io/dashboard/usage). # Install in Claude Code Source: https://docs.artifacta.io/mcp/install/claude-code Wire the Artifacta MCP server into Claude Code with a committed .mcp.json or a vendored .mcp/servers/artifacta config. **Most users should use the [hosted MCP endpoint](/mcp/install/claude-code-hosted) instead.** Connect Claude Code with a URL and a one-time browser login — no package install and no API key to copy. This page is the **advanced / CI** path: the local stdio package with an `ak_live_` API key, for CI, restricted networks, and explicit credential control. Claude Code is Artifacta's primary distribution surface — the "Maya, Solo Agent Builder" persona is building agents *with* Claude Code today (plan §8.2 rank 1). This page covers the project-scoped install most teams want plus the vendored, version-pinned layout for stricter environments. ## Prerequisites * **Node.js 20 or newer** (`node --version`). The package's `engines` field rejects older versions before any tool runs. * **An Artifacta API key** from the [API keys page](https://app.artifacta.io/dashboard/keys) — shape `ak_live_` plus 32 alphanumeric characters. * **Claude Code** installed (`claude --version`). `npx` fetches and runs the published `@artifacta-mcp/mcp` package on demand — nothing to install globally. ## Canonical config — project `.mcp.json` For a single project, drop a `.mcp.json` at the repo root and commit it so the whole team picks up the server automatically: ```json .mcp.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp"], "env": { "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}" } } } } ``` Claude Code expands `${ARTIFACTA_API_KEY}` from the shell environment at launch, so the literal key stays out of the committed file. Export it in the shell that starts Claude Code: ```bash theme={null} export ARTIFACTA_API_KEY="ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` This block works **as-is** — no edits beyond exporting your key. ### One-line equivalent To register the server without hand-editing JSON, use the CLI: ```bash theme={null} claude mcp add artifacta -- npx -y @artifacta-mcp/mcp ``` Then export `ARTIFACTA_API_KEY` in the same shell. Re-run with the flags below appended after `@artifacta-mcp/mcp` to add `--allow-path` / `--allow-destructive`. The snippets leave `@artifacta-mcp/mcp` unpinned so `npx` resolves the latest published release — patch and security fixes roll out without a config edit. Pin a version (e.g. `@artifacta-mcp/mcp@1.0.0`) for a frozen, managed install; the [version-pinned and vendored layouts](#version-pinned-and-vendored-layouts) below are the patterns for that. ## Version-pinned and vendored layouts Claude Code only loads MCP servers declared in the root [`.mcp.json`](#canonical-config-project-mcpjson) (or registered with `claude mcp add`). Dropping an `mcpServers` block under `.mcp/servers/artifacta/config.json` does **not** register a server — Claude Code never reads that file. Use one of the two patterns below instead. ### Version-pinned via root `.mcp.json` For most teams that just want a frozen release, pin the version in the canonical `.mcp.json` and commit it: ```json .mcp.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp@1.0.0"], "env": { "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}" } } } } ``` Bumping the pin is a single edit to `.mcp.json`; rollback is `git revert`. ### Vendored binary under `.mcp/servers/artifacta/` If your environment forbids resolving the package over the network at launch (air-gapped CI, strict supply-chain policy, security-reviewed binaries only), **vendor** the server's package or executable under `.mcp/servers/artifacta/` and point the root `.mcp.json` at it. `.mcp/servers/artifacta/` is a code location for the vendored release — not an MCP host config — so it never contains an `mcpServers` block. For example, after `npm install --prefix .mcp/servers/artifacta @artifacta-mcp/mcp@1.0.0` the executable lives at `.mcp/servers/artifacta/node_modules/.bin/artifacta-mcp`. Reference it from the loadable root `.mcp.json`: ```json .mcp.json theme={null} { "mcpServers": { "artifacta": { "command": "./.mcp/servers/artifacta/node_modules/.bin/artifacta-mcp", "args": [], "env": { "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}" } } } } ``` Commit the resolved files under `.mcp/servers/artifacta/` (or restore them in CI from a lockfile) so every contributor runs the same audited binary. The host-loaded config still lives in the root `.mcp.json`; `.mcp/servers/artifacta/` is purely the vendoring location. For most projects the unpinned [`.mcp.json`](#canonical-config-project-mcpjson) above is simpler; reach for these layouts only when you need a frozen pin or an offline-vendored binary. ## Optional flags Both flags go in the `args` array (or after `--` in `claude mcp add`). ### `--allow-path` — upload local files `store_artifact` can stream a file from disk (`path` argument). The [path-confinement engine](/mcp/troubleshooting#path-arguments-are-refused-even-though-the-file-exists) defaults its allow-list to the server's working directory; extend it to the directory your build outputs land in: ```json .mcp.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project/out"], "env": { "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}" } } } } ``` **Scope `--allow-path` as narrowly as possible.** It grants read access to everything under that directory (the built-in deny-list — `~/.ssh`, `~/.aws`, `/etc`, any `.env*` or `credentials.json` — always wins regardless). Point it at a dedicated build-output directory, not your whole project tree or home directory. The flag accepts **absolute paths only** (a relative value exits at startup with code 2). The CLI `--allow-path` flag is not inferred from a config-file field, but the server **also** widens its allow-list from the **`ARTIFACTA_MCP_ALLOW_PATH`** environment variable (colon-separated absolute paths) when present in the launched server's `env` block — audit it alongside `args` whenever you review who can read local files through `store_artifact.path`. ### `--allow-destructive` — expose destructive tools Whether destructive tools are gated depends on the host's declared capabilities. **Claude Code advertises `experimental.confirmations`**, so `create_download_link`, `delete_artifact`, and `seal_session` are **present by default** and the host prompts you before each call — you do *not* need `--allow-destructive` for them to appear. Non-compliant hosts (Claude Desktop, Cursor, Codex) hide them unless the flag is set. The flag is included here for parity and for non-compliant hosts that share this config: ```json .mcp.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project/out", "--allow-destructive"], "env": { "ARTIFACTA_API_KEY": "${ARTIFACTA_API_KEY}" } } } } ``` **On non-compliant hosts, `--allow-destructive` removes the confirmation barrier entirely** — destructive calls run with only a one-line stderr audit, no UI prompt. On Claude Code the host confirmation already protects you, so the flag changes nothing there. Never set it for an unattended agent on a non-compliant host. The flag is **never read from the environment or any config file** — it must be in the launch `args`. See the [autonomy boundary](/mcp/overview#autonomy-boundary) for the full matrix. ## First call: the `whoami` smoke test 1. Add the [canonical `.mcp.json`](#canonical-config-project-mcpjson) and export `ARTIFACTA_API_KEY`. 2. Start Claude Code in the project (`claude`). Approve the project's MCP server if prompted. 3. Ask: > "What's my Artifacta plan?" 4. **Expected:** Claude Code invokes the `whoami` tool and reports your tenant info — plan tier, storage usage, request quota — in the session: ```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 } ``` If no Artifacta tools appear, or you get `unauthorized`, see [Troubleshooting](/mcp/troubleshooting). You can also list registered servers with `claude mcp list` to confirm `artifacta` is wired up. ## Troubleshooting The dedicated [Troubleshooting](/mcp/troubleshooting) page covers the three most common failures: `unauthorized` on every call, `npx` not found (Node not installed), and an empty tool list (the server failed to start — check the host logs). # Connect Claude Code (Hosted) Source: https://docs.artifacta.io/mcp/install/claude-code-hosted Connect Claude Code to Artifacta over the hosted MCP endpoint with OAuth — one command, a browser login, no package install and no API key to copy. The **hosted MCP endpoint** is the recommended way to connect Claude Code to Artifacta. You add one URL, log in through your browser once, and pick the permissions on a consent screen — no npm/PyPI package, no local process, and no `ak_live_` API key pasted into a config file. ```text theme={null} https://mcp.artifacta.io/mcp ``` Hosted and local MCP expose the **same Artifacta tools** — this is a different connection method, not a different product. Prefer the local stdio package for CI, restricted networks, or when you want explicit control over credentials and process execution: see [Install in Claude Code (stdio)](/mcp/install/claude-code). ## Prerequisites * **Claude Code** installed (`claude --version`). * **A free Artifacta account.** Sign up at [app.artifacta.io/signup](https://app.artifacta.io/signup) — you log in with this account during the OAuth step below. Nothing else. There is no package to install and no API key to create for the hosted path. ## Primary install — one command Add the hosted server by URL. Claude Code registers itself with Artifacta automatically (OAuth Dynamic Client Registration) and uses PKCE — **no client id, no callback port, and no client secret to configure**: ```bash theme={null} claude mcp add --transport http artifacta https://mcp.artifacta.io/mcp ``` That writes: ```json theme={null} { "type": "http", "url": "https://mcp.artifacta.io/mcp" } ``` Artifacta supports **OAuth 2.1 Dynamic Client Registration (RFC 7591)**, so Claude Code self-registers a one-time **public** client (PKCE, no secret) the first time it connects. You do not paste a `clientId` or pick a callback port — Claude Code chooses an ephemeral loopback port for the login redirect itself. ### Alternative — fixed client (manual / pre-DCR fallback) If your client does not support Dynamic Client Registration, or you want to pin Artifacta's registered **public** OAuth client explicitly, use the `add-json` form with the fixed `clientId` and `callbackPort`: ```bash theme={null} claude mcp add-json artifacta '{ "type": "http", "url": "https://mcp.artifacta.io/mcp", "oauth": { "clientId": "d685c6a7-5cb9-4612-ba2b-cab12e38d9e0", "callbackPort": 8080, "scopes": "openid" } }' --scope local ``` Or the equivalent flags: ```bash theme={null} claude mcp add --transport http \ --client-id d685c6a7-5cb9-4612-ba2b-cab12e38d9e0 \ --callback-port 8080 \ artifacta https://mcp.artifacta.io/mcp ``` The `clientId` above is Artifacta's registered **public** OAuth client — a public identifier, safe to commit, with **no client secret**. Do **not** pass `--client-secret`; Artifacta uses PKCE. `scopes` is `openid` only — Artifacta tool permissions are chosen on the consent screen, not through the OAuth `scope` parameter (see [Permissions](#permissions)). Both the one-liner and this fixed form connect to the same endpoint and work in parallel. ## Authenticate Adding the server does not log you in — `claude mcp list` shows `artifacta` as needing authentication until you complete the browser flow once. Start a session in your project: ```bash theme={null} claude ``` Type `/mcp` in the session, select **artifacta** from the list, and choose **Authenticate**. Claude Code opens your browser to Artifacta. Sign in with your account, then on the **consent screen** tick the permissions you want to grant this connection (see [Permissions](#permissions)) and click **Authorize**. After **Authorize**, the browser redirects to a local `http://localhost:/callback` (or `127.0.0.1`) URL — this is the loopback listener Claude Code opened to receive the OAuth code, and a brief "you may close this tab" page or a momentary blank page is **expected**. Return to Claude Code; `/mcp` shows **artifacta** as connected and the granted tools appear in the session. During **Authenticate**, Claude Code opens a one-shot loopback listener (`http://localhost:/callback`) itself to receive the OAuth redirect — **you do not run a server**. With the one-liner, the port is chosen automatically; with the fixed-client `add-json` form, `callbackPort: 8080` pins it to match Artifacta's registered redirect URI. To re-authenticate (for example to change which permissions you granted), run `claude mcp logout artifacta`, then `/mcp` → **artifacta** → **Authenticate** again. ## Permissions You choose Artifacta's permissions (read / write / destroy) on the **consent screen** during authentication — not through the OAuth `scope` parameter. Each tier is nested: write includes read, and destroy includes write. | Grant | Authorized tools | Adds | | ------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | **Read** | **5** — `whoami`, `list_artifacts`, `get_artifact`, `get_artifact_download_url`, `list_sessions` (plus all resources) | Browse and download your own artifacts. | | **+ Write** | **8** | `store_artifact`, `request_upload_url`, `complete_upload` | | **+ Destroy** | **11** | `create_download_link` (public share URLs), `delete_artifact`, `seal_session` | The client may continue to list tools outside the granted tier. Artifacta authorizes each call server-side; a denied call returns `insufficient_scope` and names the required scope. Reauthenticate to broaden the grant. **Destroy** is elevated risk — it includes minting **public** share links, soft-deleting artifacts, and **irreversibly** sealing sessions. The consent screen marks it accordingly. Grant it only when your workflow needs those actions. After connecting, confirm the surface with a smoke prompt: > "What's my Artifacta plan?" Claude Code calls the `whoami` tool (available at every permission tier) and reports your tenant, plan, and usage. ## Troubleshooting Expected. Artifacta does not pre-verify the application names that self-registering clients submit, so every OAuth consent screen shows an **Unverified** label and the redirect URI for you to check. Confirm the redirect URI is a local loopback address (`http://localhost:…` or `http://127.0.0.1:…`) before you **Authorize**. Your client could not self-register. Artifacta **does** support Dynamic Client Registration, but if your client or network blocks it, fall back to the fixed **public** client: use the [`add-json` / `--client-id` form](#alternative--fixed-client-manual--pre-dcr-fallback) with `clientId` `d685c6a7-5cb9-4612-ba2b-cab12e38d9e0`. The OAuth authorization request must use `"scopes": "openid"` only (the one-liner does this for you). Artifacta's tool permissions (read / write / destroy) come from the consent screen, **not** the OAuth `scope` parameter — requesting `artifacts:*` at authorize time is rejected by the authorization server. If you set `scopes` explicitly, set it to `openid` and re-run. Artifacta is a **public** OAuth client and has no client secret. Remove any `--client-secret` flag or stored secret — authenticate with PKCE only. The tool is registered, but the current OAuth grant does not authorize the call. Run `claude mcp logout artifacta`, then `/mcp` → **artifacta** → **Authenticate** again and select the required tier. **Destroy** is required for public share links, soft deletion, and irreversible session sealing. ## Headless / CI (advanced) For unattended agents and CI that cannot run an interactive browser login, the hosted endpoint also accepts a raw `ak_live_` API key as a bearer token. This is the **advanced / CI-only** path — interactive users should use OAuth above. See the [stdio + API key install](/mcp/install/claude-code) and the hosted `ak_live_` curl smoke in the [`@artifacta-mcp/mcp` README](https://github.com/SagaPeak/artifacta-mcp). ## What's next * **[Install in Claude Code (stdio)](/mcp/install/claude-code)** — the local package with an `ak_live_` API key, for CI, restricted networks, and explicit credential control. * **[MCP overview](/mcp/overview)** — the full tool and resource surface, shared by hosted and local. # Install as a Claude Code Plugin Source: https://docs.artifacta.io/mcp/install/claude-code-plugin Install Artifacta as a Claude Code plugin — the hosted MCP connection plus the persisting-outputs and capture-transcript skills, wired up with two commands. The **Claude Code plugin** is the recommended path for Claude Code users. It adds Artifacta's hosted MCP server the same way as the [hosted endpoint](/mcp/install/claude-code-hosted) — one URL, OAuth, no API key — and bundles two skills that teach Claude Code when and how to persist run outputs to Artifacta and capture a session transcript, so you get the behavior without writing the prompt yourself. The plugin and the plain [hosted MCP connection](/mcp/install/claude-code-hosted) expose the **same Artifacta tools** over the same OAuth flow. The plugin adds two things on top: the `persisting-outputs` and `capture-transcript` skills. If you already connected via `claude mcp add --transport http artifacta …` you do not need the plugin too — install it only if you want the skills. For CI, restricted networks, or explicit credential control, use the local stdio package instead: see [Install in Claude Code (stdio)](/mcp/install/claude-code). Codex users should follow the separate [Codex plugin guide](/mcp/install/codex-plugin). ## Prerequisites * **Claude Code** installed (`claude --version`). * **A free Artifacta account.** Sign up at [app.artifacta.io/signup](https://app.artifacta.io/signup) — you log in with this account during the OAuth step below. Nothing else. There is no package to install and no API key to create. ## Install — two commands Inside a Claude Code session (these are Claude Code slash commands, not shell commands), add the marketplace, then install the plugin from it: ```text theme={null} /plugin marketplace add SagaPeak/artifacta-mcp /plugin install artifacta@artifacta ``` The first command registers `SagaPeak/artifacta-mcp` as a plugin marketplace (marketplace ID `artifacta`). The second installs the `artifacta` plugin from that marketplace, which wires up the hosted MCP server (`https://mcp.artifacta.io/mcp`) and both skills in one step — no manual `.mcp.json` edits. ## What you get * **The full hosted Artifacta MCP tool surface** — `whoami`, `store_artifact`, `list_artifacts`, `create_download_link`, and the rest, gated by the same read / write / destroy consent tiers as the plain hosted connection (see [Permissions](/mcp/install/claude-code-hosted#permissions)). * **The `persisting-outputs` skill** (`plugin/skills/persisting-outputs/SKILL.md` in the plugin). Invoke it explicitly with `/artifacta:persisting-outputs`, or let it auto-trigger — Claude Code reaches for it whenever a session produces outputs (files, reports, datasets, build results) worth saving, and it drives the `store_artifact` / `request_upload_url` + `complete_upload` tools for you. * **The `capture-transcript` skill** (`plugin/skills/capture-transcript/SKILL.md` in the plugin). Ask in natural language — e.g. *"use artifacta to upload this session's transcript"* — and it locates and verifies the live Claude Code session transcript, pushes a snapshot, and can offer to set up automatic SessionEnd capture. See the [session transcript guide](/guides/transcripts) for the underlying recipe. Both skills stamp model provenance: `persisting-outputs` instructs the agent to always pass `model` (a declared producer claim), and `capture-transcript` stamps `metadata.model` from the session transcript itself with `metadata.model_source=transcript` (a captured producer claim) — see [model capture](/guides/transcripts#model-capture). ## Authenticate Installing the plugin does not log you in — like the plain hosted connection, Artifacta needs a one-time browser OAuth login before its tools are usable. In a Claude Code session, type `/mcp`, select **artifacta** from the list, and choose **Authenticate**. Claude Code opens your browser to Artifacta. Sign in with your account, then on the **consent screen** tick the permissions you want to grant (read / write / destroy) and click **Authorize**. The browser redirects to a local loopback callback — a brief "you may close this tab" page is expected. Return to Claude Code; `/mcp` shows **artifacta** as connected, and the granted tools plus the `persisting-outputs` and `capture-transcript` skills are available in the session. Confirm the connection with a smoke prompt: > "What's my Artifacta plan?" Claude Code calls the `whoami` tool (available at every permission tier) and reports your tenant, plan, and usage. ## Updating Marketplace metadata is cached locally. Pull the latest plugin version with: ```text theme={null} /plugin marketplace update artifacta ``` The plugin manifest uses semantic versions. The current release is `1.1.0`; updating the marketplace pulls the latest published manifest and package. ## Uninstalling ```text theme={null} /plugin uninstall artifacta@artifacta ``` This removes the plugin, its MCP server registration, and the `persisting-outputs` and `capture-transcript` skills from Claude Code. It does not revoke the OAuth grant on Artifacta's side — do that from [app.artifacta.io](https://app.artifacta.io) if you're removing access entirely, or run `claude mcp logout artifacta` first if you plan to reconnect without the plugin. ## What's next * **[Install in Claude Code (Hosted)](/mcp/install/claude-code-hosted)** — the same hosted OAuth connection without the plugin wrapper or skills, if you want to manage the MCP server entry yourself. * **[Install in Claude Code (stdio)](/mcp/install/claude-code)** — the local package with an `ak_live_` API key, for CI, restricted networks, and explicit credential control. * **[Install as a Codex plugin](/mcp/install/codex-plugin)** — the same hosted OAuth MCP connection and skills, with verified Codex transcript capture. * **[MCP overview](/mcp/overview)** — the full tool and resource surface, shared by hosted, stdio, and the plugin. # Install in Claude Desktop Source: https://docs.artifacta.io/mcp/install/claude-desktop Copy one JSON block into claude_desktop_config.json and store artifacts from a Claude Desktop conversation. This page gets you from a fresh Artifacta API key to your first tool call in Claude Desktop in under five minutes. If you also use Claude Code, Cursor, Codex, or another MCP host, see the [Overview](/mcp/overview) for every client. ## Prerequisites * **Node.js 20 or newer.** The `@artifacta-mcp/mcp` package's `engines` field rejects earlier versions before any tool runs. Confirm with `node --version`. * **An Artifacta API key.** Create one on the [API keys page](https://app.artifacta.io/dashboard/keys). The key shape is `ak_live_` followed by 32 alphanumeric characters. * **Claude Desktop** installed (macOS or Windows). There is nothing to install globally for the default path — `npx` fetches and runs the published server on demand. If `npx` works in your terminal but Claude Desktop still cannot start the server, see [When `npx` works in Terminal but not in Claude Desktop](#when-npx-works-in-terminal-but-not-in-claude-desktop) below. ## Canonical config Open Claude Desktop → **Settings** → **Developer** → **Edit Config**. The file lives at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Add an `artifacta` entry under `mcpServers`: ```json claude_desktop_config.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` This block works for most installs — the only edit you need is pasting your real `ak_live_…` key in place of the placeholder. Save the file and **fully restart Claude Desktop** (quit, don't just close the window) so it relaunches the server. **macOS: Claude Desktop does not inherit your shell `PATH`.** If Node was installed via **nvm**, **fnm**, or **asdf**, bare `"command": "npx"` may show **Server disconnected** even when `npx` works in Terminal. Use the [absolute `npx` path](#when-npx-works-in-terminal-but-not-in-claude-desktop) or a [global install](#alternative-global-install-recommended-for-nvm-users) instead. Official Node from [nodejs.org](https://nodejs.org) or `brew install node` usually works as-is. The snippets here leave `@artifacta-mcp/mcp` unpinned so `npx` resolves the latest published release on each restart — patch and security fixes roll out without a config edit. Pin to a specific version (e.g. `@artifacta-mcp/mcp@1.0.0`) only for a frozen, managed deployment. ## When `npx` works in Terminal but not in Claude Desktop Symptoms in **Settings → Developer** or `~/Library/Logs/Claude/mcp-server-artifacta.log`: * `Failed to spawn process: No such file or directory` — often a **wrong** absolute path in `"command"` (e.g. Homebrew `npx` when Node is from nvm). * `Server disconnected` immediately after reload — same root cause, or `npx` not on the GUI app's `PATH`. **Diagnose in Terminal** (must succeed before editing Claude config): ```bash theme={null} npx -y @artifacta-mcp/mcp --version # expect: 1.0.0 which npx # copy this path exactly ``` Substitute the absolute `npx` path from `which npx` into `"command"` (keeps the on-demand `npx` flow): ```json claude_desktop_config.json theme={null} { "mcpServers": { "artifacta": { "command": "/Users/you/.nvm/versions/node/v22.0.0/bin/npx", "args": ["-y", "@artifacta-mcp/mcp"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` For nvm setups, a [global install](#alternative-global-install-recommended-for-nvm-users) is usually more reliable than chasing nvm's per-version `npx` path on every Node upgrade. ## Alternative: global install (recommended for nvm users) Install once, then point `"command"` at the global binary — no `npx` fetch on every Claude restart, and no dependency on GUI `PATH` resolution: ```bash theme={null} npm install -g @artifacta-mcp/mcp@1.0.0 which artifacta-mcp # copy this path into "command" artifacta-mcp --version ``` ```json claude_desktop_config.json theme={null} { "mcpServers": { "artifacta": { "command": "/Users/you/.nvm/versions/node/v22.0.0/bin/artifacta-mcp", "args": [], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` Append `--allow-path` / `--allow-destructive` to `"args"` exactly as in [Optional flags](#optional-flags). ## Optional flags Two launch flags extend what the server can do. Both belong in the same `args` array, after the package name. ### `--allow-path` — upload local files `store_artifact` can stream a file from disk (`path` argument) instead of inline `content`. The [path-confinement engine](/mcp/troubleshooting#path-arguments-are-refused-even-though-the-file-exists) restricts which directories it may read. By default the allow-list is **only the server's working directory**, which for a Claude Desktop launch is not a location you control — so add the directory your generated files land in: ```json claude_desktop_config.json theme={null} { "mcpServers": { "artifacta": { "command": "npx", "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/uploads"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` **Scope `--allow-path` as narrowly as the work allows.** It grants the agent read access to every file under that directory (subject to the built-in deny-list, which always wins — `~/.ssh`, `~/.aws`, `/etc`, any `.env*` or `credentials.json`, etc.). Point it at a dedicated `uploads/` or build-output directory, **not** at your home directory or a source tree full of secrets. The flag accepts **absolute paths only**; a relative value exits at startup with code 2. The CLI `--allow-path` flag itself is not inferred from a config-file field, but the server **also** widens its allow-list from the **`ARTIFACTA_MCP_ALLOW_PATH`** environment variable (colon-separated absolute paths) when present in the launched server's `env` block — audit it alongside `args` whenever you review who can read local files through `store_artifact.path`. ### `--allow-destructive` — expose destructive tools Claude Desktop does not advertise MCP write confirmations, so three tools — `create_download_link` (mints a **public** `dl.artifacta.io/lnk_…` URL), `delete_artifact` (soft-deletes by id), and `seal_session` (marks a session **irreversible** — no `unseal`) — are **hidden from `tools/list` by default**. Add `--allow-destructive` to expose them: ```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" } } } } ``` **`--allow-destructive` removes the only barrier between the agent and irreversible actions on Claude Desktop.** Because the host shows no confirmation UI, each destructive call instead emits a one-line stderr audit (`[artifacta-mcp] destructive call: ()`) and runs. With it set, an agent can publish a public share link, delete an artifact, or permanently seal a session without prompting you first. Only enable it if you intend to approve these actions in chat before the agent runs them, and never set it for an unattended agent. The flag is **never read from the environment or any config file** — it must be in the launch `args`. See the [autonomy boundary](/mcp/overview#autonomy-boundary) for the full matrix. ## First call: the `whoami` smoke test After restarting, confirm the install end-to-end: 1. Configure the [canonical config](#canonical-config) above with your real key. 2. **Fully restart Claude Desktop.** 3. In a new conversation, ask: > "What's my Artifacta plan?" 4. **Expected:** Claude invokes the `whoami` tool and reports your tenant info — plan tier (e.g. `free`), storage usage, and request quota — back in the conversation. A response looks like: ```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 } ``` If Claude says it has no Artifacta tools, returns an `unauthorized` error, or shows **Server disconnected** for the `artifacta` MCP entry, work through [Troubleshooting](/mcp/troubleshooting) — especially [`npx` not found](/mcp/troubleshooting#npx-not-found--node-is-not-installed) and [Server disconnected](/mcp/troubleshooting#server-disconnected--failed-to-spawn). `whoami` has no side effects and is quota-cheap, so it is the right tool to verify auth before the agent does anything else. From here, ask it to `list_artifacts`, `store_artifact`, and more — see [Using Artifacta from your coding agent](/mcp/overview#using-artifacta-from-your-coding-agent). ## Discoverability — Anthropic MCP directory Artifacta is being submitted to Anthropic's MCP directory so Claude Desktop users can discover and install it without reading docs first. | Field | Status | | ------------------- | --------------------------------------------------------- | | Submission prepared | 2026-05-29 | | Current status | Pending — awaiting Anthropic directory eligibility review | Eligibility criteria and listing decisions are controlled by Anthropic and may change; this row is updated when the listing goes live. Until then, the manual [canonical config](#canonical-config) above is the install path. ## Troubleshooting See the dedicated [Troubleshooting](/mcp/troubleshooting) page, which covers the three most common failures: `unauthorized` on every call, `npx` not found (Node not installed), and an empty tool list (the server failed to start). # Install as a Codex Plugin Source: https://docs.artifacta.io/mcp/install/codex-plugin Install Artifacta in Codex with hosted OAuth MCP, persistent-output skills, and verified transcript capture. The **Artifacta Codex plugin** is the recommended setup for Codex users. It bundles the hosted Artifacta MCP connection, reusable persistence skills, and verified Codex transcript capture. Authentication happens through browser OAuth—there is no API key to copy and no local Artifacta process to run. The plugin connects to `https://mcp.artifacta.io/mcp`. For CI, restricted networks, or explicit API-key control, use the [local stdio setup](/mcp/overview#codex-local-stdio) instead. ## Prerequisites * Codex installed and able to run `codex plugin`. * A free Artifacta account. Sign up at [app.artifacta.io/signup](https://app.artifacta.io/signup). ## Install Add the Artifacta marketplace, then install the plugin: ```bash theme={null} codex plugin marketplace add SagaPeak/artifacta-mcp codex plugin add artifacta@artifacta ``` Authenticate the bundled MCP server: ```bash theme={null} codex mcp login artifacta ``` The login command opens Artifacta in your browser. Sign in, choose the permissions you want to grant, and authorize the connection. Start a **new Codex thread** after installation so Codex loads the plugin's skills and hooks. Open `/hooks`, review the bundled Artifacta hook, and trust its current definition if you want to use one-shot automatic transcript capture. Codex skips untrusted non-managed hooks. ## Verify the install Check the installed version: ```bash theme={null} codex plugin list ``` The listing should show `artifacta@artifacta` as installed and enabled. The current plugin version is `1.1.0`. In the new thread, ask: > "Use Artifacta MCP `whoami` and report my tenant and plan." Codex should call `whoami` without asking for an API key. ## What the plugin adds * **Hosted Artifacta MCP** — read, upload, retrieve, share, delete, and session tools, authorized by the OAuth tier you choose. * **`persisting-outputs` skill** — stores reports, datasets, generated files, and other run outputs through Artifacta MCP. * **`capture-transcript` skill** — locates and verifies the current Codex rollout, creates a private snapshot, and uploads it through `store_artifact`. The hosted MCP server cannot read a path on your computer. The skills send small local files as base64 `content`; they do not pass a local path to the remote server and do not invoke the local Artifacta CLI. ## Capture a Codex transcript For an immediate snapshot, ask: > "Use Artifacta to capture this Codex session's transcript." The skill verifies the live rollout before copying or uploading it. The snapshot is tagged with `metadata.type=transcript` and `metadata.capture=snapshot`. For one capture at the current thread's next `Stop`, include the explicit flag: > "Use Artifacta to capture this Codex session's transcript --automatic." `--automatic` is one-shot consent for this thread. It does **not** enable every-turn capture, future-thread capture, or an ongoing background uploader. The trusted Stop hook requests one authenticated continuation, which uploads the snapshot through Artifacta MCP. Artifacta does not redact transcript snapshots. They can contain prompts, tool arguments, tool results, credentials, and other sensitive text. Review what your session contains before requesting capture. See [Store session transcripts](/guides/transcripts#codex-plugin) for capture metadata, retrieval, and limitations. ## OAuth permissions Artifacta permissions are nested: `artifacts:read` ⊆ `artifacts:write` ⊆ `artifacts:destroy`. | Grant | Authorized actions | | ------------- | ----------------------------------------------------------------------------------------------------- | | **Read** | Call `whoami`; list, inspect, and download artifacts; list sessions. | | **+ Write** | Upload with `store_artifact` or the large-file flow. | | **+ Destroy** | Mint public links with `create_download_link`, soft-delete artifacts, and irreversibly seal sessions. | The plugin may list all registered tools regardless of your grant. If a call exceeds the granted tier, Artifacta returns `insufficient_scope` and names the required scope. To broaden access, reauthorize: ```bash theme={null} codex mcp logout artifacta codex mcp login artifacta ``` Choose **Destroy** only when the workflow needs public share links, deletion, or irreversible session sealing. `get_artifact_download_url` is read-scoped and returns a one-hour presigned URL for direct retrieval. It is not a stable public share link. `create_download_link` creates the stable `dl.artifacta.io/lnk_…` URL and requires `artifacts:destroy`. The OAuth grant survives plugin removal. Revoke it separately from [app.artifacta.io](https://app.artifacta.io) if you no longer want the connection authorized. ## Update Refresh the marketplace, then start a new thread so Codex loads the updated cached plugin: ```bash theme={null} codex plugin marketplace upgrade artifacta ``` For a project-local marketplace, reinstall the plugin after refreshing its marketplace definition. ## Uninstall ```bash theme={null} codex plugin remove artifacta@artifacta ``` This removes the plugin, its bundled MCP registration, skills, and hooks from Codex. It does not delete any Artifacta artifacts or revoke the OAuth grant. ## What's next * [Store session transcripts](/guides/transcripts#codex-plugin) * [MCP overview](/mcp/overview) * [MCP troubleshooting](/mcp/troubleshooting) * [Claude Code plugin](/mcp/install/claude-code-plugin) # Install in Cursor Source: https://docs.artifacta.io/mcp/install/cursor Paste one MCP block into Cursor's mcp.json and use Artifacta tools from the inline AI chat. Cursor's MCP support matches Claude Desktop's, so wiring in Artifacta is a single config block — paste it, restart, and the tools appear in Cursor's inline AI chat. This page is for the "Daniel, Startup Engineer" persona who lives in Cursor and wants artifact storage without leaving the editor. ## Prerequisites * **Node.js 20 or newer** (`node --version`). The `@artifacta-mcp/mcp` package's `engines` field rejects older versions before any tool runs. * **An Artifacta API key** from the [API keys page](https://app.artifacta.io/dashboard/keys) — shape `ak_live_` plus 32 alphanumeric characters. * **Cursor** with MCP support (Settings → **MCP** / **Tools & Integrations**). `npx` fetches and runs the published server on demand — nothing to install globally. ## Canonical config Cursor reads MCP servers from `mcp.json`. Use the **global** file for every project or a **project-scoped** file for one repo: * **Global:** `~/.cursor/mcp.json` (create it if it does not exist). * **Project:** `.cursor/mcp.json` at the repo root. Cursor's `mcpServers` block is close to Claude Desktop's, with one mandatory difference: each STDIO server entry must declare `"type": "stdio"`. Omitting it leaves the server unregistered (or stuck in a red state) in current Cursor builds even when the rest of the block is valid: ```json ~/.cursor/mcp.json theme={null} { "mcpServers": { "artifacta": { "type": "stdio", "command": "npx", "args": ["-y", "@artifacta-mcp/mcp"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` This block works for most installs — the only edit is pasting your real `ak_live_…` key. Save the file, then in Cursor open **Settings → MCP** and confirm the `artifacta` server shows a green / connected status (toggle it off and on, or restart Cursor, if it does not pick up the new file). **macOS GUI `PATH`:** If Cursor shows **Server disconnected** but `npx -y @artifacta-mcp/mcp --version` works in Terminal (typical when Node is installed via nvm), paste the output of `which npx` into `"command"` or use a [global install](/mcp/install/claude-desktop#alternative-global-install-recommended-for-nvm-users). Full diagnosis: [Server disconnected](/mcp/troubleshooting#server-disconnected--failed-to-spawn). The snippet leaves `@artifacta-mcp/mcp` unpinned so `npx` resolves the latest published release on restart — patch and security fixes roll out without a config edit. Pin a version (e.g. `@artifacta-mcp/mcp@1.0.0`) only for a frozen, managed install. ## Optional flags Both flags go in the same `args` array, after the package name. ### `--allow-path` — upload local files `store_artifact` can stream a file from disk (`path` argument). The [path-confinement engine](/mcp/troubleshooting#path-arguments-are-refused-even-though-the-file-exists) defaults its allow-list to the server's working directory; add the directory your generated files land in: ```json ~/.cursor/mcp.json theme={null} { "mcpServers": { "artifacta": { "type": "stdio", "command": "npx", "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project/out"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` **Scope `--allow-path` as narrowly as the work allows.** It grants read access to everything under that directory (the built-in deny-list — `~/.ssh`, `~/.aws`, `/etc`, any `.env*` or `credentials.json` — always wins regardless). Point it at a dedicated build-output directory, not your whole project tree or home directory. The flag accepts **absolute paths only** (a relative value exits at startup with code 2). The CLI `--allow-path` flag itself is not inferred from a config-file field, but the server also widens its allow-list from the **`ARTIFACTA_MCP_ALLOW_PATH`** environment variable (colon-separated absolute paths) when present in the launched server's `env` block — audit it alongside `args` whenever you review who can read local files through `store_artifact.path`. ### `--allow-destructive` — expose destructive tools Cursor does **not** advertise MCP write confirmations, so three tools — `create_download_link` (mints a **public** `dl.artifacta.io/lnk_…` URL), `delete_artifact` (soft-deletes by id), and `seal_session` (marks a session **irreversible** — no `unseal`) — are **hidden from `tools/list` by default**. Add `--allow-destructive` to expose them: ```json ~/.cursor/mcp.json theme={null} { "mcpServers": { "artifacta": { "type": "stdio", "command": "npx", "args": ["-y", "@artifacta-mcp/mcp", "--allow-path", "/Users/you/project/out", "--allow-destructive"], "env": { "ARTIFACTA_API_KEY": "ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` **`--allow-destructive` removes the only barrier between the agent and irreversible actions on Cursor.** Because the host shows no confirmation UI, each destructive call instead emits a one-line stderr audit (`[artifacta-mcp] destructive call: ()`) and runs — the agent can publish a public share link, delete an artifact, or permanently seal a session without prompting you. Only enable it if you intend to approve these actions in chat first, and never for an unattended agent. The flag is **never read from the environment or any config file** — it must be in the launch `args`. See the [autonomy boundary](/mcp/overview#autonomy-boundary) for the full matrix. ## First call: the `whoami` smoke test After Cursor shows the `artifacta` server connected: 1. Configure the [canonical config](#canonical-config) with your real key. 2. Restart Cursor (or toggle the server off/on in **Settings → MCP**). 3. In the inline AI chat (agent mode), ask: > "What's my Artifacta plan?" 4. **Expected:** the agent invokes the `whoami` tool and reports your tenant info — plan tier, storage usage, request quota — in the chat: ```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 } ``` If no Artifacta tools appear, or you get `unauthorized`, see [Troubleshooting](/mcp/troubleshooting). ## Smoke-test record Cursor's MCP config format has shifted between releases (notably the addition of `"type": "stdio"` as a required field on STDIO server entries), so the snippets above must be re-checked against the live Cursor build at each docs update and signed off by the operator. | Field | Value | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Config format last reviewed against Cursor MCP docs | 2026-05-29 (file locations + `mcpServers` shape + required `"type": "stdio"` on STDIO entries) | | Live smoke-test Cursor version | *\[operator: fill version at HITL sign-off]* | | Live smoke-test date | *\[operator: fill date at HITL sign-off]* | | Result | *\[operator: PASS / FAIL recorded at sign-off]* | Until the operator fills the three rows above with a real Cursor build, treat the canonical config as **doc-reviewed but not live-validated** for that specific release. If a future Cursor release changes the MCP config shape (e.g. a new key name or file location), update the [canonical config](#canonical-config) and this table. If the divergence from the Claude Desktop format is large enough to need a separate package configuration mode, that is a follow-up task — not a docs-only change. ## Troubleshooting See the dedicated [Troubleshooting](/mcp/troubleshooting) page, which covers `unauthorized` on every call, `npx` not found (Node not installed), an empty tool list (the server failed to start), and destructive tools missing from `tools/list` (add `--allow-destructive`). # Use Artifacta with CrewAI Source: https://docs.artifacta.io/mcp/integrations/crewai Register the Artifacta MCP server as a CrewAI tool source so agents in a crew can persist and hand off artifacts. CrewAI multi-agent workflows often need to **hand off work between agents** — one agent produces an output, another consumes it. Artifacta is the durable store for that handoff: a producer agent stores an artifact, and a downstream agent retrieves it by id. This recipe wires the Artifacta MCP server into a crew as a tool source via CrewAI's MCP support (`crewai-tools`' `MCPServerAdapter`). This is a **recipe**, not a packaged adapter. CrewAI's MCP integration is newer than the others Artifacta supports, so the API may shift — if your `crewai-tools` version differs, check its MCP docs for the current `MCPServerAdapter` signature. We'll ship a packaged adapter (like the [LangChain one](/mcp/overview)) if CrewAI demand grows. ## Prerequisites * **Python 3.10+** and **Node.js is not required** — the Python `artifacta-mcp` server is launched directly. * An Artifacta API key (`ak_live_…`) from the [API keys page](https://app.artifacta.io/dashboard/keys). * An LLM provider configured for CrewAI (e.g. `OPENAI_API_KEY`). ## 1. Install Install the Artifacta MCP server, CrewAI, and the CrewAI tools package (which provides the MCP adapter): ```bash theme={null} pip install artifacta-mcp 'crewai-tools[mcp]' crewai export ARTIFACTA_API_KEY=ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` `artifacta-mcp` installs the `artifacta-mcp` console script that the adapter launches as a stdio subprocess. ## 2. Register Artifacta as a CrewAI tool source `MCPServerAdapter` takes `StdioServerParameters` and yields a list of CrewAI tools — one per Artifacta MCP tool. Use the `artifacta-mcp` helper `build_stdio_params` to assemble the launch command, args, and env (it injects `ARTIFACTA_API_KEY` and translates the `--allow-path` / `--allow-destructive` flags): ```python theme={null} from crewai_tools import MCPServerAdapter from mcp import StdioServerParameters from artifacta_mcp import build_stdio_params # allow_path: let store_artifact read local files from this dir. # allow_destructive: expose create_download_link / delete_artifact / seal_session. server_params = StdioServerParameters( **build_stdio_params(allow_path="/abs/path/to/work", allow_destructive=False) ) with MCPServerAdapter(server_params) as artifacta_tools: print("Artifacta tools:", [t.name for t in artifacta_tools]) # ... build agents with tools=artifacta_tools (see below) ... ``` The `with` block keeps the MCP server process alive while the crew runs; the tools are valid only inside it. If you prefer not to use the helper, build `StdioServerParameters(command="artifacta-mcp", args=[...], env={"ARTIFACTA_API_KEY": "..."})` directly. `--allow-destructive` (via `allow_destructive=True`) exposes the tools that mint **public** share links, delete artifacts, and irreversibly seal sessions. In an autonomous crew there is no human-in-the-loop confirmation, so enable it only when the crew is explicitly designed to perform those actions. Keep `--allow-path` scoped to a dedicated work directory — the built-in deny-list (`~/.ssh`, `/etc`, `.env*`, …) always wins, but a broad allow root still widens what agents can read. See [path confinement](/mcp/troubleshooting#path-arguments-are-refused-even-though-the-file-exists). ## 3. Example crew — producer hands an artifact to a consumer A two-agent crew: a **researcher** writes a summary and stores it in Artifacta; an **editor** retrieves that stored artifact by id and polishes it. The artifact is the handoff medium between the two agents. ```python theme={null} import os from crewai import Agent, Task, Crew, Process from crewai_tools import MCPServerAdapter from mcp import StdioServerParameters from artifacta_mcp import build_stdio_params server_params = StdioServerParameters(**build_stdio_params()) SESSION_ID = "crewai_handoff_demo" with MCPServerAdapter(server_params) as artifacta_tools: researcher = Agent( role="Researcher", goal="Write a short brief and store it in Artifacta for the editor.", backstory="You produce first drafts and persist them as artifacts.", tools=artifacta_tools, verbose=True, ) editor = Agent( role="Editor", goal="Retrieve the researcher's stored artifact and improve it.", backstory="You fetch artifacts by id and refine their content.", tools=artifacta_tools, verbose=True, ) write_task = Task( description=( "Write a 5-bullet brief on why agents need an artifact store. " f"Store it in Artifacta with store_artifact under session_id='{SESSION_ID}' " "and metadata kind=brief. Report the resulting artifact id." ), expected_output="The art_… id of the stored brief.", agent=researcher, ) edit_task = Task( description=( "Using the artifact id from the researcher, call get_artifact (and " "get_artifact_download_url if you need the bytes) to retrieve the brief, " "then produce an improved version." ), expected_output="The improved brief.", agent=editor, context=[write_task], # the researcher's output (the artifact id) feeds the editor ) crew = Crew( agents=[researcher, editor], tasks=[write_task, edit_task], process=Process.sequential, verbose=True, ) result = crew.kickoff() print(result) ``` The `context=[write_task]` wiring passes the researcher's output (the `art_…` id) to the editor's task, and both agents share the same Artifacta tool set — so the editor can fetch exactly what the researcher stored. This is the multi-agent handoff pattern from [`ARTIFACTA_MVP_SPEC_v5.md` use case #5](https://docs.artifacta.io). ## Recipe validation CrewAI's MCP integration is evolving, so this recipe is re-validated at each docs update and the live run is signed off by the operator. | Field | Value | | ----------------------------------------- | ----------------------------------------------------------------------- | | Integration API targeted | `crewai-tools` `MCPServerAdapter` + `mcp.StdioServerParameters` (stdio) | | Recipe authored / reviewed | 2026-05-29 | | Live-tested CrewAI / crewai-tools version | *recorded at operator HITL sign-off* | | Live-test date | *recorded at operator HITL sign-off* | | Result | *PASS recorded at sign-off* | If a CrewAI release changes the `MCPServerAdapter` signature or tool-binding shape, update the code samples and this table. If CrewAI traction warrants it, this recipe is promoted to a packaged `artifacta_mcp.crewai` adapter. ## Troubleshooting The Artifacta-side failures (auth, server not starting, path refusals) are the same as every other client — see [Troubleshooting](/mcp/troubleshooting). For CrewAI-specific issues (the adapter not yielding tools, the `with` block closing before the crew runs), confirm your `crewai-tools` version supports `MCPServerAdapter` and that the `with MCPServerAdapter(...)` block wraps the entire `crew.kickoff()` call. # MCP Server Source: https://docs.artifacta.io/mcp/overview 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). **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. ## Install **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. 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: ()` 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=` 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: ()` — 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=` (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_` 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. # Troubleshooting Source: https://docs.artifacta.io/mcp/troubleshooting Fix the common Artifacta MCP install failures: unauthorized errors, npx/PATH issues, server disconnected, and an empty tool list. The three failures below cover nearly every install problem in Claude Desktop, Claude Code, Cursor, and Codex. Each maps to a concrete fix. ## `unauthorized` on every call The server returns `unauthorized` when the Artifacta API rejects the configured key. Common causes, in order of likelihood: 1. **`ARTIFACTA_API_KEY` is not exported to the host.** Editing your shell's `.zshrc` or `.bashrc` does **not** propagate to Claude Desktop or Cursor — they read the `env` block from your `mcpServers` config, not your interactive shell. Put the key directly in the host's config file (`claude_desktop_config.json` or `~/.cursor/mcp.json`) and restart the host. For Claude Code, `${ARTIFACTA_API_KEY}` expands from the shell that launches `claude` — make sure you `export` it in that shell. 2. **Wrong scope or rotated key.** The remediation message includes the last-known key suffix (`Last-known key suffix: ****abcd`). If that suffix does not match the key in your [dashboard](https://app.artifacta.io/dashboard/keys), the key was rotated or you pasted the wrong one — regenerate or paste the current key and restart. 3. **Malformed key.** Keys must match `ak_live_` plus exactly 32 alphanumeric characters. If the shape check fails, the server exits at startup with code 2 — check the host's stderr log for the rejection message. 4. **Tenant in deletion grace period.** The remediation becomes `"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 apply, capture the failing response — every error result carries a `request_id` in `_meta.request_id` — and contact support with that id. ## `npx not found` — Node is not installed (or not on the host PATH) If the host log shows `npx: command not found`, **`Failed to spawn process: No such file or directory`** (when `"command"` is a bare or absolute path to `npx` that does not exist on disk), or the server simply never starts and no Artifacta tools appear, Node.js is not installed, not on the host's `PATH`, or the config points at the **wrong** `npx` binary. * **Install Node.js 20 or newer** from [nodejs.org](https://nodejs.org) or via a version manager (`nvm install 20`, `brew install node`). `npx` ships with Node, so installing Node fixes this. * **Verify** from a shell: `node --version` should print `v20.x` or higher, and `npx --version` should print a number. * **Confirm the package resolves:** `npx -y @artifacta-mcp/mcp --version` should print `1.0.0`. * **Shell works, Claude Desktop / Cursor does not:** macOS GUI apps don't inherit your shell `PATH`, so nvm/fnm/asdf-installed `npx` is invisible to them. See [Server disconnected](#server-disconnected--failed-to-spawn) for the fix. The official Node installer and Homebrew Node put `npx` on the default GUI `PATH`, so bare `"command": "npx"` usually works with them. The package's `engines.node` is `>=20.0.0`; on Node 18 or older the install step **refuses to run** before any tool executes. ## Server disconnected / failed to spawn Claude Desktop or Cursor shows **Server disconnected**, **Could not attach to MCP server**, or the log (`~/Library/Logs/Claude/mcp-server-artifacta.log` on macOS) contains **`Failed to spawn process: No such file or directory`** right after launch. | Log / UI signal | Likely cause | Fix | | ---------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Failed to spawn process: No such file or directory` | `"command"` path wrong (common: `/opt/homebrew/bin/npx` when Node is nvm-only) | Run `which npx` or `which artifacta-mcp`; paste exact path into `"command"` | | Server disconnects instantly; no Artifacta tools | Same as above, or bare `npx` not on GUI `PATH` | [Absolute `npx`](/mcp/install/claude-desktop#when-npx-works-in-terminal-but-not-in-claude-desktop) or [global install](/mcp/install/claude-desktop#alternative-global-install-recommended-for-nvm-users) | | `node: bad option: -y` | `"command"` is `node` but `args` start with `-y` (npx flags passed to node) | Use `"command": "npx"` (or full path to npx), not `node`, for `-y @artifacta-mcp/mcp` | | Tools empty; stderr mentions `engines` | Node \< 20 | Upgrade to Node 20+ | | `unauthorized` on first tool call | Key missing/wrong in host `env` block | Put `ARTIFACTA_API_KEY` in `mcpServers.artifacta.env`, not only in shell | After any config change, **fully quit and restart** the host (Cmd+Q on macOS for Claude Desktop — closing the window is not enough). ## `tools/list` is empty — the server failed to start If Claude reports no Artifacta tools at all, the host launched but the server process did not come up. Check the host's MCP / developer logs: * **Claude Desktop:** **Settings → Developer** shows server status; the log files live at `~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\` (Windows). Look for the `artifacta` server's stderr. * **Claude Code:** run `claude mcp list` to confirm `artifacta` is registered, and check the session output for the server's startup errors. * **Cursor / Codex:** check the client's MCP log panel. Common root causes: * **`npx` could not resolve `@artifacta-mcp/mcp`** — no network, or a stale npm cache. Reproduce with `npx -y @artifacta-mcp/mcp --version` from a shell; if that hangs or errors, fix connectivity / clear the cache (`npm cache clean --force`). * **Node 18 or older** — the `engines` gate refuses the install. Upgrade to Node 20+ (see above). * **A startup flag rejected the launch.** A relative `--allow-path` value, or a malformed API key, makes the server exit with code 2 at startup before registering any tools. The stderr log names the exact reason — fix the flag or key and restart. * **JSON syntax error in the config file.** A trailing comma or missing brace in `claude_desktop_config.json` / `.mcp.json` stops the host from launching the server. Validate the file. ## Destructive action is missing or denied The remedy depends on how Artifacta is connected: * **Hosted OAuth or the Claude Code/Codex plugin:** Tools may remain visible, but calls outside the granted tier return `insufficient_scope`. Reauthorize and grant `artifacts:destroy` to use `create_download_link`, `delete_artifact`, or `seal_session`. In Codex, run `codex mcp logout artifacta` and then `codex mcp login artifacta`. `--allow-destructive` does not apply to hosted connections. * **Local stdio:** A non-compliant host can hide all three destructive tools when the server starts without `--allow-destructive`. Add the flag to the launch `args`, restart the host, and confirm the tools appear. See the [autonomy boundary](/mcp/overview#autonomy-boundary) for the hosted OAuth and local-stdio models. For `create_download_link` specifically: `get_artifact_download_url` is **not** a stable public-link substitute. It is read-scoped and returns a one-hour presigned URL for the authenticated caller; the stable public `dl.artifacta.io/lnk_…` link requires `artifacts:destroy`. ## Path arguments are refused even though the file exists `store_artifact.path` is guarded by the local path-confinement engine. It refuses any path **outside the allow-list** (default: the server's working directory) and any path **inside the built-in deny-list** (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `~/.kube`, `~/.netrc`, `~/.artifacta`, `/etc`, `~/Library/Keychains`, any `credentials.json`, any `.env*`) — even when the allow-list is widened, because **the deny-list always wins**. * Pass `--allow-path=/your/absolute/dir` at launch to extend the allow-list. Relative values exit at startup with code 2. The CLI flag is not inferred from a config-file field, but the server **also** widens the allow-list from the **`ARTIFACTA_MCP_ALLOW_PATH`** environment variable (colon-separated absolute paths) when present in the launched server's `env` block — audit it alongside `args` whenever you review who can read local files through `store_artifact.path`. * 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. * Files over the **500 MB** ceiling are refused — switch to `request_upload_url` → `complete_upload` for larger uploads. See [Path confinement and `--allow-path`](/mcp/overview#path-confinement-and-allow-path) for the full configuration reference. # Quickstart Source: https://docs.artifacta.io/quickstart Push, list, and pull your first artifact in 60 seconds. ## Get your API key Sign up at [app.artifacta.io](https://app.artifacta.io/signup) and copy your API key from the onboarding screen. Keys start with `ak_live_`. Your full API key is shown only once at creation time. Copy it immediately. ## Install ```bash pip theme={null} pip install artifacta-cli ``` ```bash Homebrew theme={null} brew install artifacta/tap/artifacta ``` ```bash curl theme={null} curl -fsSL https://get.artifacta.io | sh ``` `pip install artifacta-cli` gives you both the CLI and the Python SDK. ## Authenticate ```bash theme={null} export ARTIFACTA_API_KEY="ak_live_abc123..." ``` Best for agents and CI — every sub-process inherits the key automatically. ```bash theme={null} # Interactive prompt artifacta auth login # Or pipe the key in (CI / agents) — won't hang on empty stdin echo "$ARTIFACTA_API_KEY" | artifacta auth login ``` Stores the key in `~/.config/artifacta/config.toml`. Verify it works: ```bash theme={null} artifacta whoami ``` ```text Output theme={null} Tenant: acme-corp Plan: free Key: ...k9f7 (last 4) Usage: 0 / 10,000 requests this month Storage: 0 B / 1 GB ``` ## Push → List → Pull Upload an artifact tagged with a session ID. ```bash theme={null} artifacta push report.pdf --session demo-run --model claude-fable-5 ``` ```text Output theme={null} ✓ Uploaded art_2xk9f7v3m1p0 Filename: report.pdf Size: 245 KB Hash: sha256:e3b0c44298fc1c149afbf4c8996fb9... Expires: 2026-04-28T10:30:00+00:00 art_2xk9f7v3m1p0 ``` The human-readable block prints to stderr; the trailing artifact ID prints to stdout so you can pipe it: `artifacta push report.pdf | xargs artifacta pull`. ```bash theme={null} artifacta ls --session demo-run ``` ```text Output theme={null} ARTIFACT ID FILENAME SIZE CREATED EXPIRES art_2xk9f7v3m1p0 report.pdf 245 KB 2026-03-29 10:30 2026-04-28 1 artifact, 245 KB total ``` ```bash theme={null} artifacta pull art_2xk9f7v3m1p0 ``` ```text Output theme={null} ✓ Downloaded report.pdf (245 KB) Saved to: ./report.pdf ``` That's it. Your artifact is stored, deduplicated, and will auto-expire in 30 days. To keep a run's conversation beside the files it produced, follow the [session transcript guide](/guides/transcripts). ## Do the same thing in Python ```python theme={null} from artifacta import Client client = Client() # reads ARTIFACTA_API_KEY from environment # Push artifact = client.push("report.pdf", session_id="demo-run") print(artifact.id) # art_2xk9f7v3m1p0 # List for a in client.list(session_id="demo-run"): print(f"{a.id}: {a.filename}") # Pull client.pull(artifact.id, output="./downloads/") ``` ## What to try next Coordinate artifact handoffs across agents using sessions and metadata. Full reference for every CLI command, flag, and environment variable. Push dicts, pull bytes, and manage sessions from Python. Integrate from any language with the REST API. See end-to-end patterns: client-ready reports, scheduled publishing, agent-to-agent handoff. Deep dives on agent artifact workflows. **Agent integration tip:** Set `ARTIFACTA_SESSION_ID` in the environment before spawning sub-agents. Every agent inherits the session automatically — zero flag passing required. # Python SDK Overview Source: https://docs.artifacta.io/sdk/overview Install, initialize, and use the Artifacta Python client. ## Install ```bash theme={null} pip install artifacta-cli ``` This installs both the CLI and the Python client library. ## Initialize the client ```python theme={null} from artifacta import Client # Option 1: Auto-detect from environment or config file client = Client() # Reads ARTIFACTA_API_KEY from env, then ~/.config/artifacta/config.toml # Also reads ARTIFACTA_SESSION_ID and ARTIFACTA_AGENT_ID as defaults # Option 2: Explicit API key client = Client(api_key="ak_live_abc123") # Option 3: Custom base URL client = Client(base_url="https://api.artifacta.io") ``` **Zero-config for agents:** If `ARTIFACTA_API_KEY` and `ARTIFACTA_SESSION_ID` are set in the environment, `Client()` with no arguments is all you need. Every `push()` call inherits the session automatically. ## Methods at a glance ### Write | Method | Description | | ----------------------------------------- | ---------------------------------------------- | | [`push()`](/sdk/push) | Upload a file from disk or bytes | | [`push_dict()`](/sdk/push-dict) | Serialize a dict to JSON and upload | | [`delete()`](/sdk/delete) | Soft-delete an artifact | | [`create_link()`](/sdk/create-link) | Create a temporary download URL | | [`seal_session()`](/sdk/seal-session) | Seal a session | | [`publish_artifact()`](#publish_artifact) | Publish an artifact as a shareable public page | | [`unpublish()`](#unpublish) | Take down an artifact's public page | ### Read | Method | Description | | --------------------------------- | ------------------------------- | | [`pull()`](/sdk/pull) | Download to a file path | | [`pull_bytes()`](/sdk/pull-bytes) | Download as bytes (in memory) | | [`pull_dict()`](/sdk/pull-dict) | Download and parse as JSON dict | | [`list()`](/sdk/list) | List artifacts with filters | | [`get()`](/sdk/get) | Get artifact metadata | The existing `push(..., transcript=True)` and `list(..., transcript=True)` calls store and filter transcripts. See the [session transcript guide](/guides/transcripts) for their exact signatures and precedence. ### Utilities | Method | Description | | ------------------------------------------ | --------------------------------------------------- | | [`Client.session_new()`](/sdk/session-new) | Generate a session ID locally (static, no API call) | | [`whoami()`](/sdk/whoami) | Verify auth and get tenant info | ## Quick example ```python theme={null} from artifacta import Client client = Client() # Push a file artifact = client.push( "report.pdf", session_id="run_q4", agent_id="earnings_bot", metadata={"model": "claude-fable-5"} # stored as metadata.model — a declared producer claim ) print(artifact.id) # art_2xk9f7v3m1p0 print(artifact.size_bytes) # 250880 # Push structured data artifact = client.push_dict( {"results": [1, 2, 3], "score": 0.95}, filename="results.json", session_id="run_q4" ) # Pull to disk client.pull(artifact.id, output="./downloads/") # Pull as bytes (no disk I/O) content = client.pull_bytes(artifact.id) # Pull as dict data = client.pull_dict(artifact.id) # List with filters for a in client.list(session_id="run_q4", metadata={"model": "claude-fable-5"}): print(f"{a.id}: {a.filename}") # Create a shareable link link = client.create_link(artifact.id, expires_in=86400) print(link.url) # https://dl.artifacta.io/lnk_xxx # Seal a session client.seal_session("run_q4") ``` ## `publish_artifact()` ```python theme={null} publish_artifact( artifact_id: str, *, title: str | None = None, visibility: str = "unlisted", access: str = "none", password: str | None = None, ) -> dict ``` Publishes an artifact as a shareable page at `https://artifacta.io/a/{slug}`. Idempotent — re-publishing the same artifact updates the page and keeps the same URL. | Parameter | Type | Default | Description | | ------------- | ----------- | ------------ | ----------------------------------------------------- | | `artifact_id` | str | — | Artifact ID to publish. | | `title` | str \| None | `None` | Page title. | | `visibility` | str | `"unlisted"` | `"unlisted"` (URL-only) or `"public"` (discoverable). | | `access` | str | `"none"` | `"none"` (open) or `"password"` (Pro plan). | | `password` | str \| None | `None` | Required when `access="password"`. | Returns a `dict` with `page_id`, `public_url`, `visibility`, and `access`. ```python theme={null} from artifacta import Client client = Client() # Publish an artifact as an unlisted page page = client.publish_artifact("art_abc123") print(page["public_url"]) # https://artifacta.io/a/pg_... # Publish publicly with a title page = client.publish_artifact( "art_abc123", title="Q2 Report", visibility="public", ) # Publish with password protection (Pro plan) page = client.publish_artifact( "art_abc123", access="password", password="hunter2", ) ``` ## `unpublish()` ```python theme={null} unpublish(id_or_slug: str) -> dict ``` Soft-unpublishes an artifact page. The artifact itself is not deleted. Accepts an artifact ID (`art_...`) or a page slug (`pg_...`). Returns a `dict` with `page_id` and `unpublished: True`. ```python theme={null} # Unpublish by artifact ID result = client.unpublish("art_abc123") print(result["page_id"]) # pg_... # Unpublish by page slug result = client.unpublish("pg_aB3xK9mP1qR5sT2u") ``` ## Error handling All methods raise typed exceptions matching the [API error taxonomy](/errors): ```python theme={null} from artifacta import ( Client, ArtifactNotFoundError, ArtifactExpiredError, SessionSealedError, QuotaExceededError, RateLimitedError, ) client = Client() try: artifact = client.push("output.json", session_id="run_42") except SessionSealedError: print("Session is finalized — use a new session") except QuotaExceededError: print("Storage or request limit reached — delete artifacts or upgrade") except RateLimitedError as e: print(f"Throttled — retry in {e.retry_after_seconds}s") ``` See the [full exception reference](/sdk/errors) for the complete mapping. ## Environment variable defaults The SDK respects the same environment variables as the CLI: | Variable | SDK behavior | | ---------------------- | ------------------------------------------------- | | `ARTIFACTA_API_KEY` | Used by `Client()` if no explicit `api_key` param | | `ARTIFACTA_API_URL` | Used as `base_url` default | | `ARTIFACTA_SESSION_ID` | Default `session_id` for `push()` and `list()` | | `ARTIFACTA_AGENT_ID` | Default `agent_id` for `push()` |