The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Layven listing page.
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.
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.
Paste https://api.layven.io/mcp and authenticate with OAuth. That UI has no custom-header field, which is why OAuth exists.
Same as Claude.ai: paste https://api.layven.io/mcp and authenticate with OAuth.
~/.cursor/mcp.json)~/.codex/config.toml)~/.gemini/settings.json)Same JSON as Cursor, but the key is httpUrl instead of url, because Gemini reads url as an SSE endpoint.
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 registration | Supported |
| Client secret expiry | Does not expire |
Consent mints an ordinary token, which you can see and revoke in the console like any other.
POST https://api.layven.io/mcp, Streamable HTTP, stateless.Content-Type: application/json and Accept: application/json, text/event-stream. Both media types must be present in Accept or the server returns 406.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.tools only. No resources, no prompts.initialize: name Layven, version 1.0.2, website https://layven.io.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.
| Tool | Access | Does | Key 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.
listList a folder, or search every granted workspace for paths matching a substring.
Arguments
Returns
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.cursor while fanning out returns INVALID_PATH with reason: "cursor_requires_workspace". Pin the workspace before you paginate.readRead a file, either the current version or an older one.
Arguments
Returns, inline, when the raw size is 262144 bytes or less and the mime type is text:
Returns, for anything larger or binary:
Errors: NOT_FOUND (with deleted: true and last_version when the path is tombstoned), INVALID_PATH.
Notes
download_url valid for 15 minutes instead of inline content. Handle both shapes.grepSearch file contents with a regular expression.
Arguments
Returns
Errors: INVALID_PATTERN, INVALID_PATH.
Notes
INVALID_PATTERN.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.edit's old_str.historyList the versions of one file, newest first.
Arguments
Returns
op is one of write, edit, move, delete, restore. moved_from, by_actor and restorable_until are optional.
Errors: NOT_FOUND, INVALID_PATH.
Notes
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".activityList what changed in a workspace and which agent changed it.
Arguments
Returns
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.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.writeCreate or replace a file with inline content.
Arguments
Inline cap: 1 MiB decoded. Anything larger goes through request_upload.
Returns
unchanged is present only when it applies.
Errors: INVALID_PATH, VERSION_CONFLICT, LOCKED, QUOTA_EXCEEDED, TOO_LARGE.
Notes
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.editApply string replacements to an existing text file without resending the whole body.
Arguments
Text files up to 16 MiB.
Returns
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
history, not twenty. If any edit fails, none of them land.old_str must match exactly once, otherwise you get STRING_NOT_FOUND or STRING_NOT_UNIQUE. Set replace_all when you mean every occurrence.old_str must match the text as the earlier edits left it.moveMove or rename a file.
Arguments
Returns
version is the new version at the destination.
Errors: NOT_FOUND, INVALID_PATH, VERSION_CONFLICT (with reason: "destination_exists"), LOCKED.
Notes
PERMISSION_DENIED.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.deleteTombstone a file.
Arguments
Returns
unchanged is present only when it applies.
Errors: NOT_FOUND, INVALID_PATH, VERSION_CONFLICT, LOCKED.
Notes
restore for as long as your plan's version history window allows.list hides tombstoned paths unless you pass include_deleted: true.restoreRestore one file to an earlier version, or a whole folder to a point in time.
Arguments, file form:
Arguments, folder form:
Returns, file form:
Returns, folder form:
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.history, so restoring to the wrong timestamp is itself restorable, and restoring twice is safe.at receive delete tombstones, counted by deleted. They are not erased and a later write brings them back.list and confirm a single file with history first. Walkthrough: examples/folder-restore-after-bad-run.md.lockTake an advisory lock on a path.
Arguments
Returns
Errors: LOCKED, INVALID_PATH.
Notes
lock again with the same token to renew.LOCKED with locked_by_label and expires_at, unless the caller passes override_lock: true, which is recorded in the activity feed.move over a lock: it survives a crash, and a 5-minute TTL does not.unlockRelease a lock this token holds.
Arguments
Returns
Errors: PERMISSION_DENIED (the lock belongs to another token), INVALID_PATH.
Notes
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_uploadStart an upload for content too large for inline write. First of two steps; see Large file uploads.
Arguments
Returns
Errors: TOO_LARGE, QUOTA_EXCEEDED, VERSION_CONFLICT, LOCKED, INVALID_PATH.
Notes
request_upload.size must be the exact byte count. It is re-checked at finalize_upload.finalize_uploadCommit an uploaded file as a new version. Second of two steps.
Arguments
Returns
Errors: UPLOAD_EXPIRED, VERSION_CONFLICT, QUOTA_EXCEEDED, TOO_LARGE.
Notes
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.UPLOAD_EXPIRED.Domain failures come back as a normal tool result carrying isError: true, whose text content is JSON:
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.
| Code | Means | Extras |
|---|---|---|
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 file | hint |
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.
| Limit | Value |
|---|---|
| Inline write | 1 MiB decoded; larger content goes through request_upload |
| Inline read | 256 KiB and a text mime type, otherwise a download URL |
| Edit | Text files up to 16 MiB, up to 20 edits per call |
| Grep | 8 MiB per file scanned, 500 candidate files per call, partial results past the time budget |
| Max file size | 1 GiB |
| Presigned PUT URL | 15 minutes |
| Upload record | 1 hour |
| Download URL | 15 minutes |
| Path length | 1024 bytes |
| Lock TTL | 5 minutes, renewable |
| Rate limit | 600 requests per minute per token, 3000 per minute per organization |
Exceeding a rate limit returns RATE_LIMITED with retry_after_ms.
Paths are UTF-8, /-separated and case-sensitive. They are normalized on input:
/a/b becomes a/b. or .. segments, and no empty segmentsAnything 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.
Each token is granted access to specific workspaces at one of three levels:
| Level | Tools |
|---|---|
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.
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:
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:
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.mjsNode 18 or newer. No dependencies, no install.
LAYVEN_TOKEN is required. --url defaults to https://api.layven.io/mcp.
Run its tests from the repository root with:
activity.| Free | Pro $19/mo | Scale $49/mo | Enterprise | |
|---|---|---|---|---|
| Storage | 5 GB | 100 GB | 500 GB | Custom |
| Version history | 30 days | 1 year | Unlimited | Unlimited |
| Activity log | 30 days | 1 year | 1 year | Unlimited |
| Workspaces | 1 | 5 | 25 | Unlimited |
| Tokens | 2 | 25 | 100 | Unlimited |
| Seats | 1 | 3 | 10 | Unlimited |
| Max file size | 1 GiB | 1 GiB | 1 GiB | 1 GiB |
| Rate limit | 600 req/min/token | 600 req/min/token | 600 req/min/token | 600 req/min/token |
Flat price. No meters, no credits.
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.
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.
Bugs in these docs, the examples, or the upload helper: open an issue. Account, billing, or data questions: info@layven.io.
MIT covers this repository: the documentation, the examples, and the upload helper. The Layven service itself is closed source.