Waafir
Connect AI Tools

waafir-dataroom reference

The waafir-dataroom server is the stable API surface of this release. It exposes thirty tools for reading and managing your data rooms, grouped by what they do: six read tools, five folder tools, nine file tools (seven file-lifecycle tools plus two file-version tools), one document-text tool, seven permission tools, and two Q&A tools.

Connection: https://app.waafir.io/api/mcp/waafir-dataroom — see Setup.

How access is enforced

Every tool runs as the token's owner and is bounded by the same rules the product UI enforces — there is no looser path through MCP. Two gates apply on every call:

  • Scope. Each tool states a minimum scope: read tools require read, folder and file mutations require write, and the permission-granting tools require admin. A token below the required scope is rejected with a FORBIDDEN error (see How a failure is reported). Scope is cumulative — write includes read, admin includes both.
  • Data room scoping. If the token is restricted to specific data rooms, any call referencing a data room outside its allow-list is rejected, and a scoped token cannot even list the data rooms it is not allowed to open. The check is keyed on the data room a resource belongs to, so a token scoped to data room A can never reach a file or folder in data room B — even within the same organization.

In addition, folder and file mutations require a deal-team role for the data room (investors cannot mutate), and the permission tools additionally require the token owner to be an organization admin or manager. Deleting a folder or file is always a soft delete — rows are marked deleted and preserved for retention and audit, never hard-removed. Every mutation is recorded as the same audit event the product UI writes, so an MCP action is indistinguishable from a UI one in the audit log.

Identifiers (dataroom_id, folder_id, file_id, user_id) are UUIDs.

How a failure is reported

A tool failure is not an HTTP status. When a tool refuses a call, the HTTP response is still 200 and the JSON-RPC envelope is still a success envelope; the failure is carried inside the result, which sets isError: true and whose text content is:

{ "error": { "code": "CONFLICT", "message": "…" } }

Branch on error.code, never on the HTTP status — a client that switches on status will never match any refusal on this page. These are the codes a data room tool returns:

CodeMeaningWhat to do
INVALID_ARGUMENTAn argument is missing, malformed, or not a valid idFix the arguments and retry
FORBIDDENThe token is valid but not permitted — scope too low, outside its data-room allow-list, or the owner's role does not allow itAsk for access, or use a token with the right scope
NOT_FOUNDNo such record, or it is not yoursCheck the id
CONFLICTUnderstood and permitted, but the data room is not in a state where it can be doneRead the message, fix the blocker, retry
TOO_LARGEThe input exceeds a size limitSplit the input
QUOTA_EXCEEDEDA metered allowance is spent, or the organization's subscription has lapsedSomebody must buy or upgrade — retrying will not clear it
UPSTREAM_ERRORA downstream service failed — the stored document could not be fetched, or text extraction failed on itTransient. Retry later before treating it as fatal
UNAVAILABLEThe read could not be recorded to the audit log, so the content was withheld rather than disclosed unloggedTransient. Retry later
INTERNALAn unexpected failureDo not retry — escalate

Do not collapse the two retry-later codes into "failed". UPSTREAM_ERROR and UNAVAILABLE reach you today from get_file_text, and they mean ask again, not give up. A client that treats every non-OK result as terminal turns a passing blip into a permanent failure — the one mistake this table exists to prevent. Note also that UNAVAILABLE is a deliberate refusal: Waafir would rather withhold a document's text than hand it over without an audit record.

The message names the blocking record wherever there is one. Transport-level failures — an unknown server name, a missing or revoked token, an unpurchased paid add-on — are real HTTP statuses and are listed in Setup.

Pagination

No list tool returns an unbounded array. Every tool that returns a list accepts an optional limit and offset, and returns a pagination object alongside the rows:

FieldMeaning
limitthe page size actually applied
offsetthe offset actually applied
counthow many rows are on this page
has_moretrue when more rows exist beyond this page
next_offsetthe offset to pass for the next page, or null when the result is complete
totalthe full number of rows behind the window — present only where it is cheap to compute

limit and offset are clamped, not rejected: a value above the maximum comes back as the maximum, a value below the minimum as the minimum, and an absent or non-numeric value as the documented default. The pagination block always reports the values that were actually applied, so you can see what you got rather than what you asked for.

Use has_more to decide whether a result is complete — never infer it from count === limit, which is ambiguous on an exactly-full final page. To read a whole list, follow next_offset until has_more is false.


Read tools

Require read scope.

list_datarooms

List the data rooms the caller can access. For a token scoped to specific data rooms, the result is filtered to that allow-list. Archived data rooms are excluded.

  • Inputs: limit (optional, 1–200, default 50); offset (optional, default 0).
  • Output: { datarooms: [{ id, name, description, org_name, created_at }], pagination: { limit, offset, count, has_more, next_offset, total } }.
  • Ordered oldest-first by created_at, with an id tiebreak so pages are stable. total is the full number of data rooms this token can reach, counted after the allow-list filter is applied.

get_dataroom

Get a single data room by id. The caller must be authorized for it; a token scoped to other data rooms is rejected.

  • Inputs: dataroom_id (required).
  • Output: { dataroom: { id, name } | null }.

list_folders

List folders in a data room. Honors deal-team vs investor visibility — an investor only sees folders they may read. List one level under a parent, or pass all for the whole tree. Deleted folders are excluded.

  • Inputs: dataroom_id (required); parent_folder_id (optional — omit for top-level folders); all (optional boolean — true lists every folder in the data room); limit (optional, 1–500, default 100); offset (optional, default 0).
  • Output: { folders: [{ id, name, parent_folder_id, created_at }], pagination: { limit, offset, count, has_more, next_offset, total } }.
  • Sorted by name, with an id tiebreak so pages are stable. all: true is paginated too — on a large tree, page through with next_offset rather than expecting the whole tree in one response.

list_files

List active (non-deleted) files in a data room, optionally within a folder. Investor callers only see files they are permitted to read. Paginated.

  • Inputs: dataroom_id (required); folder_id (optional — omit for files at the data room root); limit (optional, 1–200, default 50); offset (optional, default 0); sort_dir (optional, asc | desc, default desc).
  • Output: { files: [{ id, name, folder_id, size_bytes, mime_type, created_at, updated_at }], pagination: { limit, offset, count, has_more, next_offset } }.
  • No total — counting the full match set is not cheap on this path. Use has_more / next_offset to page.

get_file_metadata

Get metadata for a single file: name, MIME type, size, folder path, version list, and AI summary/category if present. The file must belong to the named data room; a file in another data room (even the same org) returns null.

  • Inputs: dataroom_id (required); file_id (required).
  • Output: { file: { id, name, mime_type, size_bytes, folder_id, folder_path, created_at, updated_at } | null, versions: [{ id, version_number, is_current, created_at }], ai_metadata: { summary, key_points, category, is_redacted } | null }.

search_dataroom

Search files by name within a single authorized data room (case-insensitive substring match). Deleted files are excluded.

  • Inputs: dataroom_id (required); query (required, filename substring); limit (optional, 1–100, default 25); offset (optional, default 0).
  • Output: { files: [{ id, name, mime_type, size_bytes, created_at }], pagination: { limit, offset, count, has_more, next_offset } }.
  • Newest first, with an id tiebreak so pages are stable. When has_more is true the query matched more files than were returned — page with next_offset, or narrow the query. There is no total.

Folder tools

Require write scope and a deal-team role for the data room.

create_folder

Create a folder in a data room. Nest it under a parent, or omit the parent for the data room root. Rejects a duplicate name under the same parent with a CONFLICT.

  • Inputs: dataroom_id (required); name (required, 1–255 chars); parent_folder_id (optional — null/omitted for the data room root).
  • Output: { folder: { id, name, parent_folder_id } }. The CONFLICT message names the conflicting folder's id, so you do not need to re-list the parent to find it.

rename_folder

Rename a folder. The folder must belong to the caller's organization.

  • Inputs: folder_id (required); name (required, 1–255 chars).
  • Output: { folder: { id, name } }.

delete_folder

Soft-delete a folder and its entire subtree — every nested sub-folder and every file inside them. On a real data room that can be hundreds of documents from a single folder id, so the result reports exactly how many folders and files were removed. Pass dry_run: true to get those counts without deleting anything. Rows are marked deleted, never hard-removed, and the whole cascade can be undone with restore_folder.

  • Inputs: folder_id (required); dry_run (optional boolean — preview only).
  • Output: { deleted, dry_run, folder_id, folder_name, affected: { folders, files }, restore_with: "restore_folder" }.
  • A dry run changes nothing and records no deletion in the audit trail.

restore_folder

Undo a delete_folder. Restores the folder and everything that delete removed with it — anything deleted separately, earlier, stays deleted. Returns a conflict if the parent folder is still deleted (restore that first) or if a live folder has since taken the name; in both cases the message names the blocking folder's id.

  • Inputs: folder_id (required); dry_run (optional boolean — preview only).
  • Output: { restored, dry_run, folder_id, folder_name, affected: { folders, files } }.
  • A folder delete is lossless to undo, and deliberately unlike a file delete. The cascade does not move your organization's storage counter and does not drop the files' search-index chunks, so restore_folder credits nothing back — nothing was debited — and the restored files are still searchable. delete_file / restore_file below do both. Deleting a folder full of documents is therefore not the same accounting event as deleting each of those documents.

move_folder

Reparent a folder within the same data room. Nest it under another folder, or move it to the data room root. Rejects moving a folder into itself or a descendant (cycle), into a different data room, or where a sibling with the same name already exists.

  • Inputs: folder_id (required); new_parent_folder_id (optional — null/omitted for the data room root).
  • Output: { folder: { id, name, parent_folder_id, previous_parent_folder_id } }. Pass previous_parent_folder_id back as new_parent_folder_id to undo the move.

File tools

File mutations require write scope; get_file_download_url requires only read.

Uploading is brokered, and MCP never carries the bytes. Tool arguments are JSON, so the protocol has no binary channel: request_file_upload mints a presigned URL and your client PUTs the content to it directly. A client that cannot issue an HTTP PUT with a raw binary body cannot complete an upload at all — worth knowing before you start rather than after.

Which tool finalises the upload depends on whether a file of that name is already there, and you do not have to work it out: request_file_upload returns a next_step field naming the tool to call.

request_file_upload returnsMeaningFinalise with
is_new_version: false, next_step: "register_file"No same-name active file — a pending record was created at version 1register_file
is_new_version: true, next_step: "finalise_file_version"A same-name active file exists — this upload becomes a new version of itfinalise_file_version

Call the tool next_step names. register_file only ever finalises version 1 and rejects anything else, so reaching for it on the new-version branch fails the call.

request_file_upload

Request a presigned upload URL for a file in a data room. Returns the URL and headers to PUT the bytes to, the file id, and next_step — the tool that finalises this particular upload. If a same-name active file already exists in the target folder, is_new_version is true and the returned id and version_number target that existing file.

  • Scope: write.
  • Inputs: dataroom_id (required); file_name (required, with extension); file_size (required, exact bytes, positive integer); mime_type (required); folder_id (optional — omit for the data room root).
  • Output: { upload_url, headers, file_id, s3_key, is_new_version, version_number, next_step }next_step is "register_file" or "finalise_file_version".
  • This tool moves no bytes; it only brokers the URL. If object storage is not configured the call fails outright rather than handing back a URL that cannot work.

register_file

Finalise a pending first upload after PUTting the bytes from request_file_upload: marks the file active, increments org storage, and records version 1. Call it when request_file_upload returned is_new_version: false — its next_step names this tool.

  • Scope: write.
  • Inputs: file_id (required, the pending id from request_file_upload); s3_key (optional); size_bytes (optional — derived from the uploaded object when omitted).
  • Output: { file_id, status: "active", version_number: 1 }.
  • Version 1 only, and it enforces that. A file that is already active, or that already carries a version, is rejected — the refusal names finalise_file_version as the tool for versions 2 and up.
  • The storage key is derived server-side and the client's s3_key is never trusted. It is optional; if you do send it, it must equal the key request_file_upload returned or the call is rejected.

finalise_file_version

Finalise a new version of an existing file, after PUTting the bytes to the URL request_file_upload returned with is_new_version: true. This is the MCP equivalent of replacing a document in the UI, and the tool next_step names on that branch.

  • Scope: write.
  • Inputs: file_id (required); version_number (required — the version_number request_file_upload returned).
  • Output: { file_id, version_id, version_number, size_bytes, is_current: true, previous_version_number, previous_version_id, undo_with: "restore_file_version" }.
  • version_number must be exactly the next version for this file. It is graded, not guessed: any other value is rejected rather than quietly written, and the storage key is derived server-side from it.
  • The new version becomes current and the previous one is retained in the history. Pass previous_version_id to restore_file_version to undo.
  • A file that has not completed its first upload is rejected — finalise version 1 with register_file first. The call also fails if the bytes are not actually in storage yet, rather than recording a version with nothing behind it.

restore_file_version

Make an earlier version of a file current again — the undo of finalise_file_version. It moves the current marker and re-points the file at that version; it does not mint a new version row, so the version history stays a faithful list of the revisions that were actually uploaded.

  • Scope: write.
  • Inputs: file_id (required); version_id (required — must belong to this file; get_file_metadata lists the file's version ids).
  • Output: { file_id, version_id, version_number, already_current, new_version_created: false }.
  • already_current: true means the version was already the current one and nothing changed — a no-op, not a failure.
  • There is no separate list-versions tool: get_file_metadata already returns the file's versions with their ids, numbers, and current flag.

rename_file

Rename a file — metadata only. This does not create a new version; versioning is for content replacement, not relabelling. Names must be unique among live files in the same folder; a clash returns a CONFLICT naming the existing file.

  • Scope: write.
  • Inputs: file_id (required); name (required, 1–255 chars).
  • Output: { file_id, name, previous_name, changed }. The returned name is read back from storage, so it is what was actually persisted — changed is false if the stored name did not move.

move_file

Move a file into another folder in the same data room, or to the data room root. Cross-data-room moves are rejected: a data room is a permission boundary, and re-homing content across one would change who can read it. Rejects a move where a live file of the same name already sits in the destination.

  • Scope: write.
  • Inputs: file_id (required); folder_id (optional — null/omitted for the data room root).
  • Output: { file_id, name, folder_id, previous_folder_id, moved }. Pass previous_folder_id back as folder_id to undo the move.

delete_file

Soft-delete a file: sets deleted_at. The result echoes the name of the file that was actually deleted, so a mistaken id is visible in the response. The file row and its stored object/versions are preserved for retention and audit — never a hard delete — and restore_file brings it back.

  • Scope: write.
  • Inputs: file_id (required).
  • Output: { file_id, status: "deleted", name, folder_id, restore_with: "restore_file" }.
  • Storage is credited back only for an upload that actually completed. A file whose bytes landed returns its size to your organization's storage counter. A file still awaiting its upload was never charged for in the first place, so deleting it moves the counter by nothing — the delete is counter-neutral, not a refund of bytes you never stored.
  • Deleting also drops the file's search-index chunks, so the document leaves document search immediately. restore_file cannot bring those back — see below.
  • The tool only ever resolves a live file, so calling it a second time on the same id returns NOT_FOUND rather than deleting anything again. The storage counter cannot be debited twice for one file.

restore_file

Undo a delete_file: bring a soft-deleted file back to live, restoring the row and the storage accounting that delete reversed. It does not restore the file's search-index chunks — those are dropped on delete — so the file is live and downloadable but must be re-indexed before document search finds it again. search_index_restored is always false and says so rather than implying a lossless undo.

  • Scope: write.
  • Inputs: file_id (required).
  • Output: { restored, file_id, name, folder_id, storage_bytes_restored, search_index_restored: false }.

When a restore is refused. It fails rather than papering over a conflict, and names the blocking row where there is one:

#Conditionerror.codeWhat to do
1No such file, or not yoursNOT_FOUNDCheck the id.
2The file is not deleted — it is already liveCONFLICTNothing to restore.
3The file's upload never completedCONFLICTNothing to restore: its bytes never landed, so there is no document to bring back. Upload it again instead.
4The containing folder is still deletedCONFLICTrestore_folder first — that also brings back the files deleted with it.
5A live file has since taken the name in that folderCONFLICTRename the file that holds the name, then retry.

These are in-payload codes, not HTTP statuses — see How a failure is reported.

The checks run in the order numbered above, and you are told about the first one that fails. The upload-never-completed case (3) is therefore reported after the not-found and not-deleted checks but ahead of the folder and name checks: a file whose bytes never arrived cannot be restored whatever its folder or name situation, so there is no point sending you to fix either.

get_file_download_url

Get a short-lived presigned download URL for a file. The caller must be authorized for the file (read scope plus the per-file read/download permission). The URL expires after a few minutes and returns the raw file (no watermark burn-in).

  • Scope: read.
  • Inputs: file_id (required).
  • Output: { download_url, expires_in_seconds }.

Document text tools

One tool, for reading a document's extracted text rather than downloading the file itself.

get_file_text

Read a document's extracted text, one page at a time. Returns numbered segments — for a PDF, one segment per page unless the page is very long — plus a pagination block, so you can walk a long document with offset.

Two things make this stricter than a metadata read:

  • You must be permitted to download the file, not merely view it. View-only permission is not enough, and a token restricted away from the document is refused.
  • If the document has been redacted and you are not on the deal team, you receive the text of the redacted version, and source says which artifact you got. It never silently hands a perimeter reader the original.

Every successful call is written to the organization's audit log, on the same footing as a document read in the product.

  • Scope: read.
  • Inputs: file_id (required); dataroom_id (optional — validated against the file's own data room when supplied, and a mismatch reads as NOT_FOUND rather than confirming the file exists elsewhere); limit (optional, 1–5, default 3); offset (optional, default 0).
  • Output: { file_id, file_name, source, segment_max_chars, segments, pagination }. source is "original" or "redacted". pagination carries a total.
  • A file with no stored object — a note, whose body is held inline — returns NOT_FOUND with "no extractable content".
  • This is the tool that produces the two retry-later codes: UPSTREAM_ERROR when the stored document cannot be fetched or text extraction fails on it, and UNAVAILABLE when the audit write for the read fails — in which case the text is withheld deliberately rather than disclosed unrecorded. Both are worth one retry before you treat the document as unreadable.

Permission tools

These read and mutate who can see what. The three read tools require only read scope (the two directory tools additionally require a deal-team role); the four granting/revoking tools require admin scope and a token owner who is an organization admin or manager.

get_file_permissions

List the permission grants on a file (direct and folder-inherited). Mirrors the UI's file-permissions view.

  • Scope: read.
  • Inputs: dataroom_id (required); file_id (required); limit (optional, 1–200, default 50); offset (optional, default 0).
  • Output: { permissions: [{ user_id, can_read, can_download, access_revoked, expires_at, granted_by, created_at, source, source_folder_name?, source_folder_id?, user_email?, user_name?, granted_by_email? }], pagination: { limit, offset, count, has_more, next_offset, total }, identities_visible } (source is "direct" or "inherited").
  • Grants are sorted on created_at, then user_id, then source, then source_folder_id, so pages are stable across calls. total is the full grant count on the file.
  • The identity fields (user_email, user_name, granted_by_email) follow the same visibility rule as the directory tools below: they appear for deal-team callers, investor identities additionally require an organization admin or manager, and where you are not permitted the keys are absent rather than null. identities_visible tells you which case you are in; the grants themselves are always listed.

list_dataroom_users

List the users who hold an active access grant on a data room, with the user_id the permission tools require plus each grantee's email address and name. This is usually the first call when you need a user_id.

  • Scope: read, and a deal-team role in the data room's organization (the same gate as the team page).
  • Inputs: dataroom_id (required); limit (optional, 1–200, default 50); offset (optional, default 0).
  • Output: { users: [{ user_id, email, name, is_deal_team, org_role, permission, granted_at, expires_at }], pagination: { limit, offset, count, has_more, next_offset, total }, investor_rows_visible }.
  • Investor identities are omitted unless you are an organization admin or manager; investor_rows_visible reports which set you received.
  • Lists explicit grantees only. Organization owners and admins have implicit access without a grant row and will not appear — resolve those with find_user_by_email.

find_user_by_email

Resolve an email address to the user_id that the permission tools require, along with the user's organization role and current permission in this data room.

  • Scope: read, and a deal-team role in the data room's organization. Resolving an investor additionally requires an organization admin or manager.
  • Inputs: dataroom_id (required); email (required, matched case-insensitively).
  • Output: { found: true, user: { user_id, email, name, org_role, dataroom_permission } } or { found: false }. A dataroom_permission of null means the user is in the organization but has no access to this room yet — invite them first.
  • This is not a global directory lookup: only users inside the data room's own organization, or holding live access to it, resolve. An unknown address, a user outside your tenancy, and a user you are not permitted to see all return an identical { found: false }, so a negative answer never reveals whether an account exists.

set_file_permissions

Grant or update a user's read/download permission on a file. A read/write token, or an owner who is not an org admin or manager, is rejected.

  • Scope: admin.
  • Inputs: dataroom_id (required); file_id (required); user_id (required); can_read (required boolean); can_download (required boolean); expires_at (optional ISO timestamp, must be in the future).
  • Output: { success: true, effective: { can_read, can_download } } — the post-grant decision, which reflects file restrictions and folder inheritance and may therefore differ from what you requested.
  • This tool grants access; it cannot remove it. Passing can_read: false writes an active grant with read denied, which other grants (organization membership, a data-room grant, folder inheritance) may still override. To actually remove a user's access to a file, use revoke_file_permission.

revoke_file_permission

Remove a user's direct permission grant on a file (soft-revoke: the row is preserved for the audit trail). Returns revoked: false if the user had no direct grant on the file.

  • Scope: admin.
  • Inputs: dataroom_id (required); file_id (required); user_id (required).
  • Output: { success: true, revoked }.
  • Affects the direct grant only. Access inherited from a folder permission is unaffected and must be revoked on the folder. Reversible by re-granting with set_file_permissions.

invite_user_to_dataroom

Grant a known user data-room-level access at a given permission level — a direct access grant (not an email invite). Idempotent (upsert).

  • Scope: admin.
  • Inputs: dataroom_id (required); user_id (required); permission (required — one of viewer, downloader, contributor, manager); expires_at (optional ISO timestamp, must be in the future).
  • Output: { success: true, grant_id }.

remove_user_from_dataroom

Revoke a user's data-room-level access (soft-revoke: sets revoked_at, preserving the audit trail). Returns revoked: false if the user had no active grant.

  • Scope: admin.
  • Inputs: dataroom_id (required); user_id (required).
  • Output: { success: true, revoked }.

Q&A tools

Two read-only tools over the investor Q&A threads in a data room. There is deliberately no write tool here: an AI client can read and summarise a Q&A thread, but it cannot post a question or publish an answer in your name.

What a call returns depends on who owns the token:

  • a deal-team member sees every thread in the data room, including the asker's email address;
  • an investor sees only the questions they asked themselves, and no email addresses at all.

Attached documents come back as id and name only — never content, never a download link — and the name of a document the caller may not see is withheld. Use the document tools for content; they apply their own access checks. Org-wide Company Q&A questions, which are not attached to a data room, are never returned by either tool.

list_questions

List the Q&A threads in one authorized data room, newest first, each with its answers.

  • Scope: read.
  • Inputs: dataroom_id (required); status (optional — open, answered or closed); file_id (optional — only questions asked about that document); limit (optional, 1–100, default 25); offset (optional, default 0).
  • Output: { questions, pagination }. Page with offset while pagination.has_more is true.

get_question

Get one Q&A thread by id, with all of its answers.

  • Scope: read.
  • Inputs: dataroom_id (required); question_id (required).
  • Output: { question }.
  • A question in another data room or another organization — or, for an investor, one asked by somebody else — reports NOT_FOUND. The tool never confirms that such an id exists.

Each question carries { id, dataroom_id, title, body, question_type, status, asked_by, file_id, file_name, created_at, updated_at, responses }, plus asker_email only where the caller is entitled to it — for an investor the key is absent from the payload rather than present and null. Each entry in responses is { id, body, responded_by, is_ai_draft, created_at, updated_at, attachments }.