trackmcp
Back to directory
Layvenio

layven-mcp

View on GitHub

Claude, ChatGPT and Cursor read and write the same files. Every write is versioned, attributed to the agent that made it, and restorable to any moment.

0 stars JavaScriptOthers Updated Aug 6, 2026

Documentation

Layven MCP Server

Layven is a shared drive your agents mount over MCP. Every write is versioned, every action is attributed to the agent that made it, and anything can be rolled back.

One config block, no agent code changes. Hosted at `https://api.layven.io/mcp`.

Documents Layven MCP server 1.0.2.


Quick start

code
claude mcp add --transport http layven https://api.layven.io/mcp --header "Authorization: Bearer agd_your_agent_token"

1. Get a token at layven.io: open the console, go to Agents, create one. The secret is shown once.

2. Restart your client.

3. Ask your agent: "list my Layven workspaces".

The point of the product shows up on the second connection. Create a second token, connect a different tool with it, and have that tool read the file the first one just wrote. Both agents see the same drive, and the activity feed says which one did what. Walkthrough: examples/two-agent-handoff.md.


Connect your client

Claude Code

code
claude mcp add --transport http layven https://api.layven.io/mcp --header "Authorization: Bearer agd_your_agent_token"

Claude.ai custom connector

Paste `https://api.layven.io/mcp` and authenticate with OAuth. That UI has no custom-header field, which is why OAuth exists.

ChatGPT

Same as Claude.ai: paste `https://api.layven.io/mcp` and authenticate with OAuth.

Cursor (`~/.cursor/mcp.json`)

json
{
  "mcpServers": {
    "layven": {
      "url": "https://api.layven.io/mcp",
      "headers": {
        "Authorization": "Bearer agd_your_agent_token"
      }
    }
  }
}

Codex (`~/.codex/config.toml`)

toml
[mcp_servers.layven]
url = "https://api.layven.io/mcp"
http_headers = { "Authorization" = "Bearer agd_your_agent_token" }

Gemini (`~/.gemini/settings.json`)

Same JSON as Cursor, but the key is `httpUrl` instead of `url`, because Gemini reads `url` as an SSE endpoint.

json
{
  "mcpServers": {
    "layven": {
      "httpUrl": "https://api.layven.io/mcp",
      "headers": {
        "Authorization": "Bearer agd_your_agent_token"
      }
    }
  }
}

Generic MCP JSON

json
{
  "mcpServers": {
    "layven": {
      "url": "https://api.layven.io/mcp",
      "headers": {
        "Authorization": "Bearer agd_your_agent_token"
      }
    }
  }
}

Authentication

There are two ways in, and both resolve to the same token record, so permissions, revocation and the audit trail are identical either way.

Static bearer token. Send `Authorization: Bearer agd_...` on every request. Create it in the console; the secret is shown once.

OAuth 2.1, for clients whose UI has no custom-header field:

Authorization server`https://api.layven.io`
Protected resource metadata`https://api.layven.io/.well-known/oauth-protected-resource/mcp`
Resource`https://api.layven.io/mcp`
Dynamic client registrationSupported
Client secret expiryDoes not expire

Consent mints an ordinary token, which you can see and revoke in the console like any other.


Transport

  • `POST https://api.layven.io/mcp`, Streamable HTTP, stateless.
  • Every POST is fully independent. No session id, no session resumption, no separate SSE endpoint, no stdio.
  • GET and DELETE return HTTP 405 with exactly this body:
json
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed (stateless)"},"id":null}
  • Required request headers: `Content-Type: application/json` and `Accept: application/json, text/event-stream`. Both media types must be present in `Accept` or the server returns 406.
  • Responses come back as `Content-Type: text/event-stream`.
  • `MCP-Protocol-Version` is optional. If present it must be one of `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`, otherwise the server returns HTTP 400.
  • Capabilities: `tools` only. No resources, no prompts.
  • Server identity on `initialize`: name `Layven`, version `1.0.2`, website `https://layven.io`.

Tools

Every tool takes an optional `workspace`, the workspace slug. A token granted exactly one workspace may omit it. `list` with no `workspace` fans out over every workspace the token can reach.

ToolAccessDoesKey arguments
`list``ro`List a folder, or find files by path substring`path`, `recursive`, `q`, `limit`
`read``ro`Read a file at the current or an older version`path`, `version`
`grep``ro`Search file contents with an RE2 pattern`pattern`, `prefix`, `context_lines`, `max_matches`
`history``ro`List one file's versions, newest first`path`, `limit`, `cursor`
`activity``ro`List what changed and which agent changed it`since`, `path_prefix`, `limit`
`write``rw`Create or replace a file with inline content`path`, `content`, `encoding`, `expected_version`
`edit``rw`Apply string replacements to an existing text file`path`, `edits`, `expected_version`
`move``rw`Move or rename a file`from`, `to`, `overwrite`
`delete``rw`Tombstone a file, recoverable with `restore``path`, `expected_version`
`restore``rw`Restore a file to a version, or a folder to a timestamp`path` + `to_version`, or `prefix` + `at`
`lock``rw`Take a 5-minute advisory lock on a path`path`
`unlock``rw`Release a lock this token holds`path`
`request_upload``rw`Start a large-file upload, returns a presigned PUT URL`path`, `size`, `expected_version`
`finalize_upload``rw`Commit an uploaded file as a new version`upload_id`

Only `delete` is annotated destructive, and it writes a tombstone version that `restore` brings back. `write`, `delete`, `lock` and `unlock` are annotated idempotent; `edit`, `move`, `restore`, `request_upload` and `finalize_upload` are not. No tool reaches outside Layven.

`NOT_FOUND` (unresolvable workspace), `PERMISSION_DENIED`, `RATE_LIMITED` and `INTERNAL` can come back from any tool, so the `Errors` line under each tool below names what is specific to that tool.

`list`

List a folder, or search every granted workspace for paths matching a substring.

Arguments

code
workspace?        string    workspace slug; omit to fan out over every granted workspace
path              string    default "" (workspace root)
recursive         boolean   default false
include_deleted   boolean   default false
limit             integer   1..1000, default 500, applied per workspace
cursor?           string    opaque; requires an explicit workspace
q?                string    1..1024 chars, case-insensitive substring of the full path; implies recursive

Returns

json
{
  "results": [
    {
      "workspace": { "id": "...", "slug": "research" },
      "entries": [
        {
          "path": "notes/todo.md",
          "type": "file",
          "size": 1284,
          "version": 7,
          "modified_at": "2026-08-05T09:12:44Z",
          "modified_by_label": "research-agent",
          "by_actor": "agent",
          "deleted": true
        }
      ],
      "next_cursor": "..."
    }
  ]
}

`type` is `"file"` or `"folder"`. `size`, `version`, `modified_at`, `modified_by_label`, `by_actor` and `deleted` are optional per entry.

Errors: `INVALID_PATH`.

Notes

  • `results` is always an array of per-workspace blocks, even when the token is granted exactly one workspace. Do not write a client that expects a bare `entries` array.
  • Passing `cursor` while fanning out returns `INVALID_PATH` with `reason: "cursor_requires_workspace"`. Pin the workspace before you paginate.
  • Folders are synthesized from path prefixes. There are no folder records to create or delete.

`read`

Read a file, either the current version or an older one.

Arguments

code
workspace?   string
path         string
version?     integer   defaults to the current version

Returns, inline, when the raw size is 262144 bytes or less and the mime type is text:

json
{
  "path": "notes/todo.md",
  "version": 7,
  "size": 1284,
  "mime_type": "text/markdown",
  "encoding": "utf8",
  "content": "..."
}

Returns, for anything larger or binary:

json
{
  "path": "datasets/corpus.tar.gz",
  "version": 3,
  "size": 431912448,
  "mime_type": "application/gzip",
  "download_url": "https://...",
  "url_expires_at": "2026-08-05T09:27:00Z"
}

Errors: `NOT_FOUND` (with `deleted: true` and `last_version` when the path is tombstoned), `INVALID_PATH`.

Notes

  • The response shape switches on size and mime type, not on a flag you pass. Above 256 KiB, or for any non-text file, you get a `download_url` valid for 15 minutes instead of inline content. Handle both shapes.

`grep`

Search file contents with a regular expression.

Arguments

code
workspace?       string
path?            string    search a single file
prefix?          string    search one folder; omit both path and prefix to search the whole workspace
pattern          string    min 1 char, RE2 syntax
case_sensitive   boolean   default false
context_lines    integer   0..10, default 2
max_matches      integer   1..200, default 50

Returns

json
{
  "matches": [
    {
      "path": "notes/todo.md",
      "line": 42,
      "text": "TODO: rotate the staging token",
      "before": ["..."],
      "after": ["..."]
    }
  ],
  "files_scanned": 128,
  "files_skipped_binary": 3,
  "files_skipped_too_large": 0,
  "truncated": false
}

Errors: `INVALID_PATTERN`, `INVALID_PATH`.

Notes

  • Patterns are RE2, so there are no backreferences and no lookaround. An invalid pattern returns `INVALID_PATTERN`.
  • Matches are found within single lines only. A pattern spanning a newline will never match.
  • One entry per matching line, like `grep -n`, no matter how many occurrences that line holds.
  • `truncated: true` means the scan hit its budget and the results are partial. Narrow the `prefix` or tighten the pattern.
  • Line numbers are display-only. Never paste one into `edit`'s `old_str`.

`history`

List the versions of one file, newest first.

Arguments

code
workspace?   string
path         string
limit        integer   1..1000, default 50
cursor?      string    digits only

Returns

json
{
  "versions": [
    {
      "version": 7,
      "op": "write",
      "size": 1284,
      "content_hash": "...",
      "created_at": "2026-08-05T09:12:44Z",
      "by_label": "research-agent",
      "by_actor": "agent",
      "moved_from": "drafts/todo.md",
      "purged": false,
      "restorable_until": "2027-08-05T09:12:44Z"
    }
  ],
  "next_cursor": "182734"
}

`op` is one of `write`, `edit`, `move`, `delete`, `restore`. `moved_from`, `by_actor` and `restorable_until` are optional.

Errors: `NOT_FOUND`, `INVALID_PATH`.

Notes

  • Newest first, so `limit: 1` is the cheapest way to confirm a write landed.
  • `next_cursor` is an opaque string, never a number. It is absent on the last page, which is how you know to stop.
  • `purged: true` means retention released the content of that version. The row stays for the audit trail, but `restore` to it returns `NOT_FOUND` with `reason: "version_purged"`.

`activity`

List what changed in a workspace and which agent changed it.

Arguments

code
workspace?    string
since?        string    RFC 3339 timestamp
path_prefix?  string
limit         integer   1..1000, default 100
cursor?       string

Returns

json
{
  "events": [
    {
      "at": "2026-08-05T09:12:44Z",
      "actor": "agent",
      "by_label": "research-agent",
      "by_actor": "agent",
      "op": "write",
      "path": "notes/todo.md",
      "old_path": "drafts/todo.md",
      "version": 7
    }
  ],
  "next_cursor": "9014522"
}

`actor` is one of `agent`, `user`, `system`. Newest first.

`op` is `write`, `edit`, `move`, `delete`, `restore`, `purge`, `lock`, `unlock` or `grep`. System sweeps also record `purge_retention` and `purge_workspace_delete`, both with no path. Only `write`, `edit`, `move`, `delete` and a single-file `restore` carry a `version`; on the rest it is null. Note that `purge` carries a real path or folder prefix, so it matches a `path_prefix` filter: if you are auditing a folder from this feed, a purge shows up alongside the writes.

Errors: `INVALID_PATH`.

Notes

  • `next_cursor` is an opaque string, never a number. It is absent on the last page, which is how you know to stop.
  • This is a poll. There are no webhooks, no triggers and no push. Agents coordinate by calling `activity` on an interval, which fits comfortably inside the rate limit at seconds-to-minutes cadence.
  • `since` is a timestamp, not a cursor, and two events can share one. Expect occasional re-delivery and make the work idempotent. See examples/two-agent-handoff.md.

`write`

Create or replace a file with inline content.

Arguments

code
workspace?         string
path               string
content            string
encoding           "utf8" | "base64"   default "utf8"
mime_type?         string
expected_version?  integer   fail instead of overwriting a concurrent change
override_lock      boolean   default false

Inline cap: 1 MiB decoded. Anything larger goes through `request_upload`.

Returns

json
{
  "path": "notes/todo.md",
  "version": 8,
  "size": 1301,
  "content_hash": "...",
  "deduplicated": false,
  "unchanged": true
}

`unchanged` is present only when it applies.

Errors: `INVALID_PATH`, `VERSION_CONFLICT`, `LOCKED`, `QUOTA_EXCEEDED`, `TOO_LARGE`.

Notes

  • Writing content byte-identical to the current version is a no-op: it returns `unchanged: true` and creates no new version. For a scheduled job that is worth logging loudly, because it usually means the generator did not run.
  • Content identical to an older version does create a new version.
  • Writing to a tombstoned path resurrects it.

`edit`

Apply string replacements to an existing text file without resending the whole body.

Arguments

code
workspace?         string
path               string
edits              array of 1..20 objects:
                     old_str      string    min 1 char
                     new_str      string
                     replace_all  boolean   default false
expected_version?  integer
override_lock      boolean   default false

Text files up to 16 MiB.

Returns

json
{
  "path": "notes/todo.md",
  "version": 9,
  "edits_applied": 2,
  "replacements": [1, 3],
  "size_before": 1301,
  "size_after": 1288,
  "context": "..."
}

`unchanged` is present only when it applies.

Errors: `STRING_NOT_FOUND`, `STRING_NOT_UNIQUE`, `NOT_EDITABLE`, `TOO_LARGE_FOR_EDIT`, `VERSION_CONFLICT`, `LOCKED`, `QUOTA_EXCEEDED`, `NOT_FOUND`.

Notes

  • Edits apply in order and commit atomically as one new version, so a 20-edit call is one entry in `history`, not twenty. If any edit fails, none of them land.
  • Each `old_str` must match exactly once, otherwise you get `STRING_NOT_FOUND` or `STRING_NOT_UNIQUE`. Set `replace_all` when you mean every occurrence.
  • Because edits apply in order, a later `old_str` must match the text as the earlier edits left it.

`move`

Move or rename a file.

Arguments

code
workspace?         string
from               string
to                 string
expected_version?  integer
overwrite          boolean   default false
override_lock      boolean   default false

Returns

json
{ "from": "drafts/todo.md", "to": "notes/todo.md", "version": 1 }

`version` is the new version at the destination.

Errors: `NOT_FOUND`, `INVALID_PATH`, `VERSION_CONFLICT` (with `reason: "destination_exists"`), `LOCKED`.

Notes

  • The token needs write access to both paths. A prefix rule that covers the source but not the destination fails with `PERMISSION_DENIED`.
  • A live destination with `overwrite: false` returns `VERSION_CONFLICT` with `reason: "destination_exists"`. That failure is useful: two workers racing to claim the same item can `move` it, and the loser gets a clean error instead of duplicated work.

`delete`

Tombstone a file.

Arguments

code
workspace?         string
path               string
expected_version?  integer
override_lock      boolean   default false

Returns

json
{ "path": "notes/todo.md", "version": 10, "unchanged": true }

`unchanged` is present only when it applies.

Errors: `NOT_FOUND`, `INVALID_PATH`, `VERSION_CONFLICT`, `LOCKED`.

Notes

  • This writes a tombstone version, it does not erase content. The file stays recoverable with `restore` for as long as your plan's version history window allows.
  • Writing to the same path later resurrects it as a new version on the same history.
  • `list` hides tombstoned paths unless you pass `include_deleted: true`.

`restore`

Restore one file to an earlier version, or a whole folder to a point in time.

Arguments, file form:

code
workspace?      string
path            string
to_version      integer
override_lock   boolean   default false

Arguments, folder form:

code
workspace?      string
prefix          string
at              string    RFC 3339 timestamp in UTC, e.g. 2026-08-05T14:04:55Z
override_lock   boolean   default false

Returns, file form:

json
{ "path": "notes/todo.md", "version": 11 }

Returns, folder form:

json
{
  "restored": 42,
  "deleted": 3,
  "affected_paths": ["content/pricing.md", "content/index.md"],
  "total": 45
}

`affected_paths` is a capped sample; `total` is the true count.

Errors: `NOT_FOUND` (with `reason: "version_purged"` when retention already released the target version), `INVALID_PATH`, `LOCKED`.

Notes

  • `at` must be UTC, ending in `Z`, with optional milliseconds. A UTC offset such as `+02:00` is rejected even though it is valid RFC 3339, so convert before you call.
  • Restore appends new versions and never rewrites history. The bad versions stay in `history`, so restoring to the wrong timestamp is itself restorable, and restoring twice is safe.
  • In the folder form, files that did not exist at `at` receive delete tombstones, counted by `deleted`. They are not erased and a later write brings them back.
  • There is no dry run. Size the operation with `list` and confirm a single file with `history` first. Walkthrough: examples/folder-restore-after-bad-run.md.

`lock`

Take an advisory lock on a path.

Arguments

code
workspace?   string
path         string

Returns

json
{ "path": "notes/todo.md", "expires_at": "2026-08-05T09:17:44Z" }

Errors: `LOCKED`, `INVALID_PATH`.

Notes

  • Advisory and 5 minutes. Call `lock` again with the same token to renew.
  • Locks never block reads. A foreign valid lock makes writes return `LOCKED` with `locked_by_label` and `expires_at`, unless the caller passes `override_lock: true`, which is recorded in the activity feed.
  • The file need not exist, so you can lock a path you are about to create.
  • For work that can be made idempotent, prefer a claim with `move` over a lock: it survives a crash, and a 5-minute TTL does not.

`unlock`

Release a lock this token holds.

Arguments

code
workspace?   string
path         string

Returns

json
{ "path": "notes/todo.md", "released": true }

Errors: `PERMISSION_DENIED` (the lock belongs to another token), `INVALID_PATH`.

Notes

  • Unlocking a path that holds no lock returns `released: true`, not `NOT_FOUND`. The call says nothing about whether a lock existed, so it is not a way to ask who holds one.

`request_upload`

Start an upload for content too large for inline `write`. First of two steps; see Large file uploads.

Arguments

code
workspace?         string
path               string
size               integer   positive, the exact byte count of the file
mime_type?         string
expected_version?  integer
override_lock      boolean   default false

Returns

json
{
  "upload_id": "...",
  "put_url": "https://...",
  "url_expires_at": "2026-08-05T09:27:00Z",
  "max_size": 1073741824
}

Errors: `TOO_LARGE`, `QUOTA_EXCEEDED`, `VERSION_CONFLICT`, `LOCKED`, `INVALID_PATH`.

Notes

  • The presigned PUT URL is valid for 15 minutes and the upload record for 1 hour. There is no resumable or multipart upload: if the PUT fails, start again from `request_upload`.
  • `size` must be the exact byte count. It is re-checked at `finalize_upload`.

`finalize_upload`

Commit an uploaded file as a new version. Second of two steps.

Arguments

code
workspace?   string
upload_id    string

Returns

json
{
  "path": "datasets/corpus.tar.gz",
  "size": 431912448,
  "version": 3,
  "content_hash": "..."
}

Errors: `UPLOAD_EXPIRED`, `VERSION_CONFLICT`, `QUOTA_EXCEEDED`, `TOO_LARGE`.

Notes

  • Size and `expected_version` are both re-checked here, so a `VERSION_CONFLICT` can surface at finalize even though `request_upload` succeeded. Someone wrote the path while you were uploading; start again from `request_upload`.
  • Calling finalize a second time returns `UPLOAD_EXPIRED`.

Errors

Domain failures come back as a normal tool result carrying `isError: true`, whose text content is JSON:

json
{ "code": "VERSION_CONFLICT", "message": "...", "current_version": 9 }

They are never protocol-level JSON-RPC errors. Agents and clients should branch on `code`.

Argument validation fails the same way, but the text is not JSON. Passing an argument of the wrong type returns `isError: true` with a plain English string like `MCP error -32602: Input validation error: Invalid arguments for tool activity: ...`, produced by the MCP layer before the request reaches Layven. So parse defensively: an `isError` result whose text will not parse as JSON is a bug in your call, not a Layven domain error. `upload/layven-upload.mjs` does exactly this, falling back to `code: "UNKNOWN"` with the raw text as the message.

CodeMeansExtras
`NOT_FOUND`Path, version or workspace absent`workspaces` (the token's slugs) when a workspace slug does not resolve; `deleted: true` and `last_version` on a tombstoned path; `reason: "version_purged"` on a purged version
`PERMISSION_DENIED`Token lacks the access level, or a prefix rule blocks the path`required`
`INVALID_PATH`Path fails normalization`reason`
`VERSION_CONFLICT``expected_version` did not match, or the destination exists`current_version`, sometimes `reason` (for example `destination_exists`)
`LOCKED`Another token holds a valid lock`locked_by_label`, `expires_at`
`QUOTA_EXCEEDED`Organization is over its storage quota`limit_bytes`, `used_bytes`
`TOO_LARGE`Inline content over the cap, or file over 1 GiB`max_bytes`
`UPLOAD_EXPIRED`Finalize called on an expired or already-consumed upload
`RATE_LIMITED`Token or organization rate cap hit`retry_after_ms`
`INTERNAL`Unexpected server failure`request_id`
`STRING_NOT_FOUND``edit`: `old_str` is not present in the filehint
`STRING_NOT_UNIQUE``edit`: `old_str` matched more than once and `replace_all` was not set
`NOT_EDITABLE``edit`: the file is not text
`TOO_LARGE_FOR_EDIT``edit`: file over 16 MiB
`INVALID_PATTERN``grep`: the pattern is not valid RE2

`RATE_LIMITED` is the only code worth retrying automatically: sleep `retry_after_ms`, then try again.


Limits

LimitValue
Inline write1 MiB decoded; larger content goes through `request_upload`
Inline read256 KiB and a text mime type, otherwise a download URL
EditText files up to 16 MiB, up to 20 edits per call
Grep8 MiB per file scanned, 500 candidate files per call, partial results past the time budget
Max file size1 GiB
Presigned PUT URL15 minutes
Upload record1 hour
Download URL15 minutes
Path length1024 bytes
Lock TTL5 minutes, renewable
Rate limit600 requests per minute per token, 3000 per minute per organization

Exceeding a rate limit returns `RATE_LIMITED` with `retry_after_ms`.


Paths and versioning

Paths are UTF-8, `/`-separated and case-sensitive. They are normalized on input:

  • no leading slash: `/a/b` becomes `a/b`
  • no duplicate slashes
  • no `.` or `..` segments, and no empty segments
  • no trailing slash on a file
  • no control characters
  • 1024 bytes maximum

Anything else returns `INVALID_PATH` with a `reason`. Directories are implicit: there are no folder records, and `list` synthesizes folders from path prefixes.

Versioning. Every mutation creates a new version, numbered per file. `delete` writes a tombstone version. `move` creates a version at the destination and tombstones the source. `restore` appends a new version reproducing an earlier state. History is append-only and is never rewritten, which is why a bad restore is itself restorable.

Optimistic concurrency. The default is last-write-wins, which is safe because nothing is lost. When you want a write to fail rather than clobber a concurrent change, pass `expected_version` with the version you read. A mismatch returns `VERSION_CONFLICT` with `current_version` in the payload, so the caller can re-read and decide.

Locks. `lock` takes a 5-minute advisory lock, renewed by calling `lock` again with the same token. A foreign valid lock makes writes return `LOCKED` with `locked_by_label` and `expires_at`; `override_lock: true` proceeds anyway and is recorded in the activity feed. Locks never block reads.


Permissions

Each token is granted access to specific workspaces at one of three levels:

LevelTools
`ro``list`, `read`, `grep`, `history`, `activity`
`rw`everything in `ro`, plus `write`, `edit`, `move`, `delete`, `restore`, `lock`, `unlock`, `request_upload`, `finalize_upload`
`admin`reserved for future workspace management

A grant can carry optional prefix rules that restrict a token to particular folders, expressed as `allow` or `deny` lists. Deny beats allow. `move` requires permission on both the source and the destination path.

Give each agent its own token. That is what makes `by_label` in `list`, `history` and `activity` readable, and it is what lets you revoke one agent without touching the others.


Large file uploads

Inline `write` caps at 1 MiB. Above that, upload in three steps.

1. Ask for a URL. Call `request_upload` with the exact byte count:

json
{ "path": "datasets/corpus.tar.gz", "size": 431912448, "mime_type": "application/gzip" }
json
{
  "upload_id": "...",
  "put_url": "https://...",
  "url_expires_at": "2026-08-05T09:27:00Z",
  "max_size": 1073741824
}

2. PUT the bytes to `put_url`. This is a plain HTTP PUT of the raw file, not an MCP call. The URL is valid for 15 minutes.

3. Commit it. Call `finalize_upload`:

json
{ "upload_id": "..." }
json
{
  "path": "datasets/corpus.tar.gz",
  "size": 431912448,
  "version": 3,
  "content_hash": "..."
}

Why there is no SDK

There is no Layven client library and there will not be one. Layven is a hosted MCP server: you point your client at one URL and your agent gets 14 tools, with no code to write and nothing to keep up to date. This is the one exception. The upload flow has a middle step that is a raw HTTP PUT, and an MCP client cannot issue one, so we ship a single dependency-free script for it. Copy it, read it, change it. It is MIT.

`upload/layven-upload.mjs`

Node 18 or newer. No dependencies, no install.

code
LAYVEN_TOKEN=agd_your_agent_token \
node upload/layven-upload.mjs ./corpus.tar.gz datasets/corpus.tar.gz \
  [--workspace slug] [--mime type] [--expected-version N] [--override-lock] \
  [--url https://api.layven.io/mcp]

`LAYVEN_TOKEN` is required. `--url` defaults to `https://api.layven.io/mcp`.

Run its tests from the repository root with:

code
node --test

Examples


Plans

FreePro $19/moScale $49/moEnterprise
Storage5 GB100 GB500 GBCustom
Version history30 days1 yearUnlimitedUnlimited
Activity log30 days1 year1 yearUnlimited
Workspaces1525Unlimited
Tokens225100Unlimited
Seats1310Unlimited
Max file size1 GiB1 GiB1 GiB1 GiB
Rate limit600 req/min/token600 req/min/token600 req/min/token600 req/min/token

Flat price. No meters, no credits.


Data residency

Your files are stored on OVHcloud in Gravelines, France, and are encrypted at rest. No third-party AI or model API ever touches your content. You can export everything as plain files, and deletion is real deletion.


Versioning

The version in `server.json` and in the changelog matches the version the server reports on `initialize`, so you can always check what you are talking to. Documentation changes that do not track a server change do not bump the version.

See CHANGELOG.md.


Support

Bugs in these docs, the examples, or the upload helper: open an issue. Account, billing, or data questions: info@layven.io.


License

MIT covers this repository: the documentation, the examples, and the upload helper. The Layven service itself is closed source.

Frequently asked questions

What is layven-mcp?

layven-mcp is Claude, ChatGPT and Cursor read and write the same files. Every write is versioned, attributed to the agent that made it, and restorable to any moment.

How do I install layven-mcp?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is layven-mcp open source?

Yes — it is hosted on GitHub at https://github.com/Layvenio/layven-mcp.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP