# MCP server

> Connect an MCP host to Artefaktum's hosted server and use its tools to find, upload and trust artifacts.

## Connecting

Artefaktum runs one hosted MCP server over Streamable HTTP:

```
https://api.artefaktum.dev/mcp
```

No stdio entry point; a stdio-only host needs a bridge such as `mcp-remote`.

Claude Code:

```sh
claude mcp add --transport http artefaktum https://api.artefaktum.dev/mcp
```

Cursor, Windsurf, Cline, or any host that reads an `mcp.json`:

```json
{ "mcpServers": { "artefaktum": { "url": "https://api.artefaktum.dev/mcp" } } }
```

### What happens on first use

The host opens a browser tab for an OAuth 2.1 sign-in handled by Clerk: sign in with Google or GitHub, approve the connection once, and the host gets an access token it attaches to every call after that. There is no key to paste. Sign-in provisions a tenant the first time it happens, keyed to your personal account or Clerk organisation, with a default project already in place.

## Authentication

The endpoint accepts two bearer credentials, checked by the same resolver chain as REST:

- **OAuth 2.1 access token**: issued by Clerk after the browser sign-in above. The default for hosts with an OAuth-capable MCP client.
- **API key**: the same key used against the REST API, created in the [console](https://artefaktum.dev/console/), sent as `Authorization: Bearer <key>`. Use it for hosts that can't complete a browser flow, or for unattended runs: pass `--header` on `claude mcp add`, or add a header next to `url` in `mcp.json` if the host allows one.

Two OAuth caveats: Clerk's tokens carry no audience (`aud`) claim, so audience checking is relaxed rather than strict; and Clerk doesn't know Artefaktum's scope names, so every OAuth caller gets a fixed grant of read, write and search, never delete or admin. An API key carries exactly the scopes it was minted with. Neither matters for normal use, only for auditing what an OAuth agent can reach.

## Tools

Every tool is scoped to a project. Call `project_list` first if you don't have a `project_id` yet. This matters most after OAuth sign-in, which binds you to a tenant, not a project. List-shaped tools cap at 25 items and truncate long text; file bytes never enter a tool response.

| Tool | Purpose |
|---|---|
| `artifact_search` | Find artifacts by meaning, text, or structured filters. |
| `artifact_get` | Read one artifact's metadata, lifecycle state, trust signals and relations. |
| `artifact_get_download_url` | Get a short-lived signed URL for an artifact's bytes. |
| `project_list` | List the projects this credential can use. |
| `artifact_list` | List artifacts in a project, with filters and a cursor. |
| `artifact_create_upload` | Reserve a new artifact and get a signed upload URL. |
| `artifact_complete_upload` | Confirm the bytes were uploaded so the server can verify and ready it. |
| `artifact_resolve` | Reuse a fresh artifact by external key, or claim the right to create it. |
| `artifact_update_metadata` | Change an artifact's title, description, tags, metadata or TTL. |
| `artifact_create_version` | Start a new immutable version of an existing artifact. |
| `artifact_add_relation` | State a lineage relation between two artifacts. |
| `artifact_delete` | Request permanent deletion of an artifact. |

### Finding artifacts

**`artifact_search`** matches descriptors (title, description, tags, metadata), never file contents. `mode` is `exact` (filters only), `text` (full-text), `semantic` (pgvector) or `hybrid` (fuses both; the default). Scores aren't comparable across modes. Superseded artifacts are hidden unless asked for.

| Parameter | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `query` | string | `""` |
| `mode` | string | `"hybrid"` |
| `tags_all` | string[] | none |
| `content_types` | string[] | none |
| `exclude_superseded` | boolean | `true` |
| `limit` | integer | `20` (capped at 25) |

```json
{ "project_id": "3f5c...", "query": "q3 churn analysis", "mode": "hybrid", "tags_all": ["churn"], "limit": 10 }
```

**`artifact_get`** returns one artifact plus its relations (up to 25). Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`.

**`artifact_list`** filters a project's artifacts with a cursor, not a free-text query.

| Parameter | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `status` | string[] | none |
| `tag` | string | none |
| `content_type` | string | none |
| `external_key` | string | none |
| `limit` | integer | `20` (capped at 25) |
| `cursor` | string | none |

```json
{ "project_id": "3f5c...", "tag": "churn", "limit": 20 }
```

**`project_list`** takes no arguments; it returns every project the credential can use.

### Caching by name

**`artifact_resolve`** turns "fetch only if nobody already has" into one call: `hit` (a fresh artifact exists, use it), `create` (you hold the reservation: `PUT` to `upload.url`, then `artifact_complete_upload`), or `pending` (retry after `retry_after_seconds`).

| Parameter | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `external_key` | string | required |
| `filename` | string | required |
| `content_type` | string | required |
| `size_bytes` | integer | required |
| `title` | string | required |
| `max_age_seconds` | integer | none (any age counts as fresh) |
| `run_id` | string | none |

```json
{
  "project_id": "3f5c...",
  "external_key": "company-api/acme/2026-09-17",
  "max_age_seconds": 86400,
  "filename": "acme.json",
  "content_type": "application/json",
  "size_bytes": 5120,
  "title": "Acme enrichment"
}
```

### Uploading

**`artifact_create_upload`** reserves a new artifact and returns a signed, short-lived upload URL. `PUT` the bytes to `upload.url` with the given headers, then call `artifact_complete_upload`.

| Parameter | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `filename` | string | required |
| `content_type` | string | required |
| `size_bytes` | integer | required |
| `title` | string | required |
| `description` | string | `""` |
| `tags` | string[] | none |
| `metadata` | object | none |
| `external_key` | string | none |
| `run_id` | string | none |
| `infer_lineage` | boolean | `true` |

Field sizes are limited; see [Limits](/docs/rest/#limits).

```json
{
  "project_id": "3f5c...",
  "filename": "report.pdf",
  "content_type": "application/pdf",
  "size_bytes": 88213,
  "title": "Q3 churn analysis",
  "tags": ["churn", "q3"]
}
```

**`artifact_complete_upload`** confirms the upload. The server verifies the object and marks it ready; the response says `processing` until that finishes, usually within a second or two; re-read with `artifact_get`. Safe to call more than once.

```json
{ "artifact_id": "0199198a-...", "version_id": "0199198b-...", "sha256": "e3b0c4..." }
```

**`artifact_create_version`** starts a new immutable version and returns another signed upload URL. On completion the server writes a `supersedes` relation to the previous version.

| Parameter | Type | Default |
|---|---|---|
| `artifact_id` | UUID | required |
| `filename` | string | required |
| `content_type` | string | required |
| `size_bytes` | integer | required |
| `run_id` | string | none |
| `infer_lineage` | boolean | `true` |

### Downloading

**`artifact_get_download_url`** returns a short-lived signed URL; fetch it yourself. Records a usage event.

| Parameter | Type | Default |
|---|---|---|
| `artifact_id` | UUID | required |
| `version_id` | UUID | none (latest version) |
| `run_id` | string | none |

Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`.

### Lineage and lifecycle

**`artifact_add_relation`** states that one artifact relates to another: `derived_from`, `supersedes`, `attachment_of`, `generated_by` or `related_to`. Explicit relations replace inferred ones.

| Parameter | Type | Default |
|---|---|---|
| `artifact_id` | UUID | required |
| `to_artifact_id` | UUID | required |
| `relation_type` | string | required |
| `to_version_id` | UUID | none |
| `metadata` | object | none |

```json
{ "artifact_id": "out-id", "to_artifact_id": "in-id", "relation_type": "derived_from" }
```

**`artifact_update_metadata`** changes title, description, tags, metadata or TTL. Bytes are immutable; use `artifact_create_version` for new content. Re-embeds when a searchable field changes.

| Parameter | Type | Default |
|---|---|---|
| `artifact_id` | UUID | required |
| `title` | string | none |
| `description` | string | none |
| `tags` | string[] | none |
| `metadata` | object | none |
| `expires_at` | string (date-time) | none |
| `clear_expires_at` | boolean | `false` |

**`artifact_delete`** moves the artifact to `deleting`; a worker removes the bytes. Not reversible. Argument: `{ "artifact_id": "0199198a-8f21-7c9e-9b1a-4e2f6d1a0c33" }`.

### The artifact resource

`artefactai://artifacts/{artifact_id}` is a pointer, not a data endpoint. A resource read carries no principal to authorize against, so it returns only the id you gave it plus a nudge to call `artifact_get` or `artifact_get_download_url` instead.

## Typical agent flow

A research agent hands a report to a writing agent, neither knowing where the other runs.

1. Research agent calls `artifact_create_upload`, then `PUT`s the bytes to `upload.url`.
2. Research agent calls `artifact_complete_upload`, then polls `artifact_get` until `status` is `ready`.
3. Writing agent, on a separate host, calls `artifact_search` (`hybrid`, same `project_id`) and picks a hit with `superseded: false` and `stale_upstream: false`.
4. Writing agent calls `artifact_get_download_url` and fetches the bytes itself.
5. Writing agent uploads its own output tagged with the same `run_id`; since it downloaded the first artifact in that run before uploading, the server infers a `derived_from` edge on its own, or either agent can call `artifact_add_relation` to state it explicitly.
