Skip to main content

laserfiche-mcp

PyPI version Python versions CI License: MIT MCP

Community project — not affiliated with or endorsed by Laserfiche.

A Model Context Protocol server that lets Claude (Desktop, Code, or any MCP client) search, read, and — when you opt in — write documents in a self-hosted Laserfiche repository. The same binary is also a full command-line client (ls, get, cat, search, manifest, dedupe, ...) for the deterministic work that needs no model at all.

Current release v2.4.0 — read and write tools for self-hosted Repository API v1 and v2, BETA Laserfiche Cloud auth, a one-click Claude Desktop extension, an optional remote HTTP transport with per-user OAuth for web clients, and a full CLI (ls, get, cat, search, manifest, dedupe, ...) for the deterministic work that needs no model at all. Read-only by default; write tools register only with LF_READ_ONLY=false and are guarded by path fences and a two-step, parameter-bound confirmation flow. See the CHANGELOG for per-release notes and the roadmap for what's next.

Quick start

uv tool install laserfiche-mcp   # or run ad hoc: uvx laserfiche-mcp
laserfiche-mcp setup             # wizard: server URL + account, then verifies the connection
laserfiche-mcp ls 1              # you're in — list the repository root

Then wire it into your MCP client — see Claude Desktop and Claude Code below. Prefer environment variables over the wizard? See Configure. Want a no-terminal install instead? See the Claude Desktop extension. New to MCP, or want the slower walkthrough? See Connecting Claude to a Laserfiche repository (~7 min read).

What you can do with it

Once connected, Claude can:

Read (always available):

  • Search the repository with native Laserfiche search syntax, by name pattern, or via the LLM-friendly search_natural flow (asks the server for templates first, then runs with automatic 400 repair)
  • List the contents of any folder, look up an entry by ID or path, read all template field values, list field/tag/template/link definitions and audit reasons
  • Inspect document metadata, fetch the raw edoc as base64, or extract text locally (PDF, DOCX, PPTX, XLSX, EML, HTML, RTF, text/*) — all via get_document_edoc(..., mode=...)

Write (opt-in via LF_READ_ONLY=false):

  • Create folders, import documents, copy entries (async), rename and move entries
  • Set, merge, and clear fields, tags, and links on an entry
  • Assign and remove templates — with optional client-side validation of repository-required fields before the API call
  • Delete entries (folders cascade), edocs, and specific page ranges — all with a two-step preview→confirm-token flow, HMAC-signed and bound to operation + entry + the operation's own parameters, expiring after 5 minutes

Operate safely — every write checks the entry's path against LF_WRITE_PATHS_ALLOW / LF_WRITE_PATHS_DENY, folder deletes refuse unless force_large_delete=true when child count exceeds LF_DELETE_FOLDER_MAX_DESCENDANTS, and LF_WRITE_TOOLS_ALLOWED can scope a deployment to e.g. metadata-only writes.

Install

Two ways to run it, depending on who you are.

For everyone — the Claude Desktop extension

Chat with your Laserfiche repository from Claude Desktop — no terminal, no config files.

1. Download

Download the extension (always the newest version), or browse the latest release. You'll need Claude Desktop installed first.

2. Double-click & connect

Double-click the file, click Install, and fill in the short form that appears:

Field What to enter
Repository API URL Your Laserfiche server address, e.g. https://your-server/LFRepositoryAPI
Repository name The repository you pick when signing in to Laserfiche Web Access
Username A Laserfiche account that can read the repository
Password That account's password — stored safely in your computer's keychain

Not sure what goes where? Ask whoever runs Laserfiche at your organization — it takes them a minute.

3. Ask

Open a chat and try:

  • "Find every invoice from March in the Accounting folder."
  • "What's in the Onboarding folder? Summarize the newest document."
  • "Search for contracts mentioning Acme and list them with dates."

Full walkthrough for end users and team rollouts: docs/desktop-extension.md.

For developers — the Python package

uvx laserfiche-mcp            # run directly, no install
pip install laserfiche-mcp    # or add it to your environment

Requires Python 3.10+ and a reachable Laserfiche Repository API Server (self-hosted) with a service account that can read it, plus any MCP client (Claude Desktop, Claude Code, MCP Inspector). For local development:

git clone https://github.com/SamuelSHernandez/laserfiche-mcp
cd laserfiche-mcp
uv sync --extra dev

Configure

Copy the example file and fill in your repository details:

cp .env.example .env
$EDITOR .env

Minimum required variables for self-hosted password-grant auth:

Variable Example
LF_REPO_API_URL https://lf.example.com/LFRepositoryAPI
LF_REPOSITORY_ID my-repo
LF_API_VERSION v1 (default) or v2 — see below
LF_USERNAME service-account
LF_PASSWORD (your service account password)
LF_AUTH_MODE password
LF_READ_ONLY true (default — see Writes section below)

Optional — web-client links. Unset by default, so search/read tools never emit web_url. Cannot be derived from LF_REPO_API_URL — copy it by hand from your own Laserfiche web client (open a document, copy the browser URL, substitute the entry ID with {entry_id}). See .env.example for worked examples. A returned link grants no access by itself: opening it still requires the viewer's own Laserfiche web-client login and is subject to the repository's entry-level ACLs — useful for staff who have Laserfiche accounts, useless to an end user who doesn't.

Variable Default Purpose
LF_WEB_CLIENT_URL_TEMPLATE unset URL template for a Document viewer link, e.g. https://lf.example.com/Laserfiche/DocView.aspx?repo={repo_id}&id={entry_id}
LF_WEB_CLIENT_FOLDER_URL_TEMPLATE unset Same, for Folder/RecordSeries entries — most web clients browse on a different page than they view a document on

Optional write-mode variables (fences and allowlists default off; the delete batch cap and required-field validation default on — see the Safety model section for context):

Variable Default Purpose
LF_READ_ONLY true Set false to register the write tools
LF_WRITE_PATHS_ALLOW unset Comma-separated path prefixes where writes are permitted (case-insensitive)
LF_WRITE_PATHS_DENY unset Comma-separated path prefixes where writes are refused (deny wins over allow)
LF_WRITE_TOOLS_ALLOWED unset Comma-separated write-tool names to scope what registers; e.g. metadata-only
LF_DELETE_FOLDER_MAX_DESCENDANTS 50 Refuse folder deletes above this immediate-child count unless force_large_delete=true
LF_REQUIRE_AUDIT_REASON false When true, delete_entry refuses to execute without audit_reason_id
LF_VALIDATE_REQUIRED_FIELDS true Validate the target template's own required fields (fields with a defaultValue are skipped) client-side before assign_template PUTs
LF_VALIDATE_NAMES true Pre-flight field / tag / template / link-type names against cached schema definitions; returns invalid_*_name instead of an opaque 400
LF_SCHEMA_CACHE_TTL_SECONDS 300 Cache window for the schema-definition lookups that back LF_VALIDATE_NAMES and LF_VALIDATE_REQUIRED_FIELDS. Set to 0 to disable caching.
LF_IMPORT_MAX_BYTES 25 MB Client-side cap on import_document payload size
LF_IMPORT_SOURCE_DIRS unset Comma-separated local directories import_document may read file_path from (symlinks resolved before comparison). Unset: any path the MCP process can read is accepted.
LF_EDOC_MAX_BYTES 25 MB Cap on get_document_edoc downloads in bytes/text modes
LF_SEARCH_TIMEOUT_SECONDS 60 How long search_content waits for an async search before abandoning it
LF_SEARCH_POLL_INTERVAL_SECONDS 1 Delay between search_content status polls; backs off toward 2s on long searches
LF_SEARCH_CONTEXT_HITS_MAX 10 Hard cap on matched passages returned per entry by search_content
LF_LEGACY_TOOL_NAMES false Set true to also register the v1.x verb-first aliases alongside the v2 names. Default changed to false in v2.3.0 — registering both roughly doubles the per-request tool catalog; set true if you still call tools by their legacy names
LF_CONFIRMATION_SECRET unset Optional secret the destructive-op confirmation tokens are signed with. Unset: random per-process key, so a restart invalidates pending preview tokens (the safer single-instance default). Set it to keep tokens valid across restarts / across instances sharing the secret. Treat like a password.
LF_LOG_FORMAT text json emits one JSON object per log line (for jq / Datadog / Splunk)

See .env.example for the full list including OAuth config, pagination limits, request timeout, retry attempts, and SSL verification.

API version note: LFRepositoryAPI ships with different routing surfaces across builds. Older self-hosted installs expose /v1/... paths; newer ones expose /v2/.... Probe your server with:

curl {LF_REPO_API_URL}/v1/Repositories
curl {LF_REPO_API_URL}/v2/Repositories

Whichever returns a 200 with a JSON repo list is your version. If the wrong value is set, every call fails with 400 UnsupportedApiVersion. The default is v1 because that is what most current on-prem installations expose.

Auth note: Laserfiche self-hosted does not accept HTTP Basic auth. The server exchanges your username/password for a bearer token at POST /{api_version}/Repositories/{repository_id}/Token on first request and refreshes it automatically before expiry. The same flow works on both v1 and v2.

Connect to Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "laserfiche": {
      "command": "uvx",
      "args": ["laserfiche-mcp"],
      "env": {
        "LF_REPO_API_URL": "https://lf.example.com/LFRepositoryAPI",
        "LF_REPOSITORY_ID": "my-repo",
        "LF_API_VERSION": "v1",
        "LF_USERNAME": "service-account",
        "LF_PASSWORD": "replace-me",
        "LF_AUTH_MODE": "password",
        "LF_READ_ONLY": "true"
      }
    }
  }
}

Restart Claude Desktop. The Laserfiche tools will appear in the tool picker.

Connect to Claude Code

claude mcp add laserfiche -- uvx laserfiche-mcp

(Pass env vars via --env LF_REPO_API_URL=... flags or set them in your shell before running Claude Code.)

Test it locally with the MCP Inspector

npx @modelcontextprotocol/inspector uvx laserfiche-mcp

This opens a UI where you can call each tool directly and watch the JSON-RPC traffic — useful for verifying endpoint shapes against your specific Repository API Server version before wiring it into Claude.

Remote HTTP (web clients)

The default transport is stdio — for local clients that launch the server as a subprocess (Claude Desktop, Claude Code, Cursor, Gemini CLI). Web and cloud clients (claude.ai custom connectors, ChatGPT connectors) can't spawn a local process; they connect to a URL. The same server can serve those clients over Streamable HTTP:

laserfiche-mcp --http                 # binds 127.0.0.1:8000, path /mcp
laserfiche-mcp --http --port 9000     # override port for this run

Configuration (all optional, LF_* env like everything else):

Variable Default Purpose
LF_HTTP_HOST 127.0.0.1 Bind interface. Loopback by default — not reachable off the machine.
LF_HTTP_PORT 8000 Listen port.
LF_HTTP_PATH /mcp Endpoint path; clients connect to http(s)://host:port/mcp.
LF_HTTP_AUTH_TOKEN (unset) Static shared bearer token. Simplest auth; ignored when OAuth is on.
LF_HTTP_OAUTH_ISSUER (unset) Turns on per-user OAuth — verifies each caller's token against this authorization server.

The --http server chooses its auth by precedence: OAuth (if LF_HTTP_OAUTH_ISSUER is set) → static token (if LF_HTTP_AUTH_TOKEN) → none (loopback only). OAuth is the multi-user path for claude.ai / ChatGPT; see Per-user OAuth below.

Verify locally with the Inspector (point it at the URL, not the command):

laserfiche-mcp --http &
npx @modelcontextprotocol/inspector   # then connect to http://127.0.0.1:8000/mcp

Per-user OAuth

For a multi-user connector, run the server as an OAuth 2.1 Resource Server — each user signs in through your existing identity provider (LFDS, Microsoft Entra, Okta, Auth0, Google) and the server verifies their token:

pip install 'laserfiche-mcp[oauth]'

LF_HTTP_OAUTH_ISSUER="https://login.microsoftonline.com/<tenant>/v2.0" \
LF_HTTP_PUBLIC_URL="https://lf.example.com/mcp" \
LF_HTTP_OAUTH_AUDIENCE="api://laserfiche-mcp" \
LF_HTTP_OAUTH_REQUIRED_SCOPES="laserfiche.read" \
  laserfiche-mcp --http --host 0.0.0.0

The server then serves protected-resource metadata (RFC 9728), so claude.ai / ChatGPT discover your authorization server, run the authorization_code + PKCE flow, and present a bearer token that this server verifies (signature via JWKS, plus aud / iss / exp / scopes). This is authentication at the edge — verified requests still reach Laserfiche via the shared service account, so the Laserfiche audit trail shows that account, not the end user. Full details, including IdP registration and the security checklist, are in docs/remote-http.md.

Unattended agents

The --http transport doesn't distinguish a human's chat client from an autonomous agent's tool-calling loop — same OAuth flow, same tools. What should differ is what each is allowed to do unsupervised. Register a separate OAuth client for your agent (client_credentials grant, no human present to click through authorization_code), and withhold whichever scope you assign to LF_HTTP_OAUTH_DESTRUCTIVE_SCOPE from that client's grant. The agent can still call delete_entry/delete_edoc/delete_pages to get a preview — useful for it to reason about and surface to a human — but executing refuses with destructive_scope_required until a human's own token (which does carry the scope) makes the actual call. move_entry and rename_entry are unaffected by this gate: they're reversible, so an unattended agent can execute those on its own.

LF_HTTP_OAUTH_DESTRUCTIVE_SCOPE="laserfiche.destructive"   # only humans get this scope

This is on top of, not instead of, scoping the agent's own deployment with LF_WRITE_TOOLS_ALLOWED / LF_READ_ONLY — see Safety model.

Connecting a web client

claude.ai and ChatGPT connectors need a public HTTPS URL. In practice that means putting this server behind a reverse proxy (or a tunnel like cloudflared / ngrok for a spike) and adding the resulting https://…/mcp URL as a custom connector in the client's settings.

Command line (no model, no tokens)

Everything the MCP tools do against the repository, the same binary does from a shell — same code path, no LLM in the loop. Use it for the work that has one correct answer: inventories, downloads, dedupe, grepping a contract.

laserfiche-mcp setup                           # one-time: save connection details
laserfiche-mcp ls 1                            # list a folder (alias: list)
laserfiche-mcp ls '\HR\Leases'                 # ...or by path
laserfiche-mcp get 4821 --to ./lease.pdf       # stream a document to disk
laserfiche-mcp cat 4821                        # extracted text on stdout
laserfiche-mcp cat 4821 --pages 4-9            # just those pages
laserfiche-mcp find 4821 'unpaid balance'      # grep one document
laserfiche-mcp search 'unpaid balance'         # grep the whole repository
laserfiche-mcp manifest 1 --out inventory.csv  # walk a tree, write CSV
laserfiche-mcp dedupe 1                        # byte-identical documents
laserfiche-mcp diff 4821 4822                  # compare two entries (alias: compare)

Office-friendly synonyms work everywhere: list, read (cat), download (get), compare (diff), duplicates (dedupe). On Windows, quote paths with double quotes: laserfiche-mcp list "\HR\Leases".

ENTRY and FOLDER accept a numeric entry ID or a repository path. Every command takes --json, so the same invocation backs a shell pipeline:

laserfiche-mcp search 'termination' --json | jq -r '.results[].name'
laserfiche-mcp manifest 1 --json | jq '.by_extension'
laserfiche-mcp manifest 1 --out tree.jsonl --format jsonl && jq -r 'select(.extension=="pdf").name' tree.jsonl

Conventions that make these scriptable:

  • Results go to stdout; progress and warnings go to stderr.
  • Exit codes are meaningful: 0 success, 1 the operation failed or found nothing, 2 bad usage. laserfiche-mcp find 4821 'indemnify' >/dev/null && echo present does what you would expect.
  • Nothing here writes to the repository. The subcommands are read-only by construction and never register the write tools.

What each command saves you

Command Instead of Why it is cheaper
get --to a base64 blob in a tool result Streamed to disk; the bytes never enter a context window, and never sit in RAM either
cat / find reading a whole document into the model Extraction and matching happen locally; you get the passage, not the file
search opening each hit to see what it says Laserfiche's own OCR index returns the matched passages
manifest one list_folder call per subfolder One walk, one CSV, a summary you can read at a glance
dedupe downloading everything to compare Sizes are probed first; only size collisions are ever downloaded
diff eyeballing two records side by side A set comparison with one correct answer

Document formats

cat, find, and the MCP's mode="text" read these with no extra dependencies: PDF, DOCX, PPTX, EML, HTML, RTF, and anything text/* (including CSV, JSON, XML, Markdown).

Two more need the optional extra:

pip install 'laserfiche-mcp[office]'   # adds .xlsx and Outlook .msg

Legacy binary .doc / .xls / .ppt are not supported — correct extraction needs an external converter such as LibreOffice, and bundling that would break the "pip install and it works" promise. Re-save as OOXML, or read Laserfiche's own indexed text with search.

Scanned images have no text layer at all. cat says so explicitly rather than returning an empty string, and points you at search, which reads the OCR index where that text actually lives.

Tools

Tool names below are shown in their original verb-first form (get_entry, set_fields, ...) for readability. In v2.0 every tool is also registered under the laserfiche_{resource}_{verb} form (laserfiche_entry_get, laserfiche_field_set, ...). Both names resolve to the same function. The laserfiche_* names are the recommended path; the old names remain as deprecation aliases through v2.x and will be removed in v3.0. The authoritative mapping lives in _V2_RENAME_MAP in src/laserfiche_mcp/server.py.

Reads (always registered)

Tool v2 name Purpose
search_entries laserfiche_entry_search Run a raw Laserfiche search query, e.g. {LF:Name="*.pdf"}
search_by_name laserfiche_entry_search_by_name Convenience wrapper: name pattern + optional folder scope
search_natural laserfiche_entry_search_natural Two-mode guided search: ask for grammar+templates, then run with auto-repair on 400
search_content laserfiche_entry_search_content Full-text search that returns the matched passages — page number plus excerpt, straight from the OCR index
list_folder laserfiche_folder_list List children of a folder by ID
get_entry laserfiche_entry_get Fetch metadata for one entry by ID
get_entry_by_path laserfiche_entry_get_by_path Resolve a full path to an entry
get_field_values laserfiche_field_values_get Read all template fields assigned to an entry
get_document_text laserfiche_document_get_text Server-side extracted text (v2 only; v1 use get_document_edoc(mode="text"))
get_document_edoc laserfiche_document_get_edoc Inspect edoc (info), download bytes (bytes), or extract text (text)
compare_entries laserfiche_entry_compare Diff two entries' metadata and template fields — is this a re-scanned duplicate or a real difference?
find_duplicate_documents laserfiche_document_find_duplicates Scan a folder tree for byte-identical documents (size-then-hash, downloads almost nothing on a mostly-distinct tree)
list_repositories laserfiche_repository_list List repos for this account; falls back to the configured repo if endpoint disabled
list_field_definitions laserfiche_field_definition_list Enumerate all field definitions; pass summary_only=true for a {count, names} shape
list_tag_definitions laserfiche_tag_definition_list Enumerate tag definitions; supports summary_only
list_template_definitions laserfiche_template_definition_list Enumerate template definitions; supports summary_only
list_link_definitions laserfiche_link_definition_list Enumerate entry-link type definitions; supports summary_only
get_template_fields laserfiche_template_field_list Atomic "what fields does this template need" lookup; pass required_only=true to filter to mandatory fields. Replaces the three-call chain (list_template_definitions → list_field_definitions → manual filter).
get_audit_reasons laserfiche_audit_reason_list Audit reasons available to the authenticated user (for delete/export)
get_task_status laserfiche_task_get_status Poll the status of an async operation (delete, copy)
wait_for_task laserfiche_task_wait Block until an async operation reaches a terminal state
task_wait_or_poll laserfiche_task_update Combines the two above: timeout_seconds=0 polls once, >0 blocks. Read-only despite the _update v2 name — it never mutates anything.

Writes (registered only when LF_READ_ONLY=false)

Tool v2 name Purpose Two-step token?
set_fields laserfiche_field_set OVERWRITE all field values on an entry (fields not in the body are deleted) —
merge_fields laserfiche_field_merge GET-then-PUT helper: update specific fields, preserve the rest —
field_update laserfiche_field_update Collapses the two above: mode="merge" (default) or mode="replace" —
set_tags laserfiche_tag_set OVERWRITE all tags on an entry —
merge_tags laserfiche_tag_merge Add/remove specific tags without touching others —
tag_update laserfiche_tag_update Collapses the two above: pass add/remove (merge) or replace (overwrite) —
set_links laserfiche_link_set OVERWRITE all entry links —
link_update laserfiche_link_update Wraps set_links; mode="merge" does a GET-then-PUT union instead of overwrite —
assign_template laserfiche_template_assign Assign a template, optionally with initial field values (preflight-validated) —
remove_template laserfiche_template_remove Clear the template assignment —
template_assign_or_remove laserfiche_template_update Collapses the two above: template_name=<name> assigns, template_name=None clears —
create_folder laserfiche_folder_create Create a child folder under a parent —
import_document laserfiche_document_import Multipart upload from a local file path; capped by LF_IMPORT_MAX_BYTES —
copy_entry laserfiche_entry_copy Async copy via CopyAsync; returns an operation token to poll —
rename_entry laserfiche_entry_rename Rename an entry — preview shows old/new path, then re-call with the token yes
move_entry laserfiche_entry_move Move (optionally rename) — fence applies to both source AND destination paths yes
delete_entry laserfiche_entry_delete Delete an entry (folders cascade); preview shows child count + batch-cap status yes
delete_edoc laserfiche_document_edoc_delete Wipe the electronic-document content; entry + metadata remain yes
delete_pages laserfiche_document_pages_delete Delete specific page ranges; refuses empty page_range (would mean "delete all") yes

Tools with two-step token return a preview + HMAC-signed confirmation_token on first call. Surface the preview to the user; on go-ahead, re-call with the same arguments plus the token. Tokens are bound to (operation, entry_id, entry_name) and the operation's own parameters (page_range for delete_pages, new_name for rename_entry, destination + name for move_entry) — executing with different arguments than were previewed fails verification. They expire after 5 minutes, and are invalidated by server restart (unless LF_CONFIRMATION_SECRET is set — see Configure).

Each of the 5 two-step tools above is also registered as a ..._preview / ..._execute pair — same behavior, split into two single-purpose tool calls instead of one tool branching on whether confirmation_token is set. Use whichever style your client's model handles more reliably — both call the identical underlying logic.

Tool v2 name
rename_entry_preview / rename_entry_execute laserfiche_entry_rename_preview / laserfiche_entry_rename_execute
move_entry_preview / move_entry_execute laserfiche_entry_move_preview / laserfiche_entry_move_execute
delete_entry_preview / delete_entry_execute laserfiche_entry_delete_preview / laserfiche_entry_delete_execute
delete_edoc_preview / delete_edoc_execute laserfiche_document_edoc_delete_preview / laserfiche_document_edoc_delete_execute
delete_pages_preview / delete_pages_execute laserfiche_document_pages_delete_preview / laserfiche_document_pages_delete_execute

Using search_natural

search_entries requires hand-written Laserfiche query syntax. If the server rejects the query the only feedback the LLM gets is a generic HTTP 400 — there's nothing actionable to retry against. search_natural is the LLM-friendly path:

  1. First call — pass the user's question and (optionally) a folder_path to scope the answer; leave lf_query unset. The tool samples up to ten entries from that folder, returns the templates and field names it found, the Laserfiche search grammar reference, and 2–3 candidate query strings the LLM can choose from or refine.
  2. Second call — same question, plus the chosen lf_query. On HTTP 400, the tool tries up to two automatic repairs (escape unescaped quotes inside values, then wildcard-wrap bare Name= values if fuzzy=True) before returning a structured error with all attempts visible so the LLM can author a fresh query.

The page-size cap for search_natural is the dedicated LF_MAX_PAGE_SIZE env var (default 100) — some self-hosted SimpleSearches implementations reject $top values above an internal limit, so this defaults lower than the list/folder cap.

Using search_content

The other search tools answer which entries matched. search_content answers what they say, by returning the matched passages themselves — page number, surrounding excerpt, and the exact substring that matched — pulled from Laserfiche's full-text index, which is where OCR output for scanned documents lives.

search_content(query="unpaid balance", folder_path="\Leases")
→ { "results": [
      { "entry_id": 7, "name": "lease-4821.pdf", "hit_count": 6,
        "hits": [ { "page": 4,
                    "text": "…tenant owes an unpaid balance of $2,400 as of March…",
                    "match": "unpaid balance" } ] } ] }

That answers most "what does this document say about X" questions without downloading anything. Reach for get_document_edoc(mode="text") only when an excerpt points you at the right document and you need more of it.

A bare phrase is wrapped into {LF:Basic~="..."} for you. Pass a query starting with { to use raw syntax instead — e.g. option="D" to search only OCR'd document text, or option="DFANLT" to permit leading and trailing wildcards.

Two knobs control cost: hits_for_top (how many results get their passages fetched — one extra request each, default 5) and hits_per_entry (passages per result, default 3, capped by LF_SEARCH_CONTEXT_HITS_MAX).

This is the asynchronous /Searches flow rather than SimpleSearches, which is what makes context hits available at all — they're keyed by a search token that only the async flow produces. Laserfiche caps concurrent searches per session (two, on v1), so the tool runs the whole create→poll→read→close cycle inside one call and always releases the token, including on timeout and failure. Older builds without the /Searches endpoints get a structured async_search_unavailable error pointing at search_entries as the fallback.

get_document_edoc modes

On v1 servers the Laserfiche Text export endpoint doesn't exist, so get_document_text cannot return anything. get_document_edoc gained a mode parameter as the workaround:

Mode Use it when
info You only need metadata (size, content-type). Default.
bytes You want the raw file as base64 — capped at LF_EDOC_MAX_BYTES (25 MB by default; override per-call with max_bytes).
text You want extracted text. PDF, DOCX, PPTX, XLSX, EML, HTML, RTF and text/* are handled (format detected from content-type + entry name — octet-stream PDFs work). OCR is not attempted; for scans use search_content, which reads the OCR index.

All tool descriptions are written to read like prompts — they tell the model when to use the tool, valid input shapes, and what kind of follow-up is expected. See src/laserfiche_mcp/server.py.

Troubleshooting

Start with one command:

laserfiche-mcp diagnose

It authenticates, probes every endpoint your Laserfiche build exposes, and prints an OK/unavailable table plus your write-mode and logging config. Failures are classified — it will tell you whether the server was unreachable (URL/VPN/TLS, not your password), whether the other LF_API_VERSION would work (it probes both and names the right one), or whether the credentials themselves were rejected.

No config yet, or config in doubt? Run the wizard:

laserfiche-mcp setup

It asks for the server URL, repository, and service account, saves them to ~/.laserfiche-mcp/.env (%USERPROFILE%\.laserfiche-mcp\.env on Windows), and ends with a diagnose run. Every later CLI invocation finds that file automatically when nothing else is configured.

Windows notes:

  • In PowerShell/cmd, quote repository paths with double quotes: laserfiche-mcp list "\HR\Leases". Avoid a trailing backslash before the closing quote — it escapes the quote.
  • Claude Desktop logs live at %APPDATA%\Claude\logs\ (macOS: ~/Library/Logs/Claude/). Look for mcp-server-laserfiche.log.

Common misconfigurations:

Symptom Likely cause Fix
diagnose says UNREACHABLE Wrong LF_REPO_API_URL, VPN down, self-signed cert Fix the URL; for internal certs set LF_VERIFY_SSL=false (dev only)
diagnose suggests the other version LF_API_VERSION mismatch Set the version it names
HTTP 401, or LF error 9528 ("LFDS unreachable") Bad credentials — 9528's wording is misleading Re-run laserfiche-mcp setup or fix LF_USERNAME/LF_PASSWORD
A fence/setting seems to have no effect A typo'd LF_* variable name — Settings silently ignores unrecognized env vars Check laserfiche-mcp diagnose's Config sanity: section (or the startup log), which names any LF_* var that matches no known setting

Errors

Every tool returns a stable dict on failure instead of raising — so the LLM gets actionable, structured data instead of Error executing tool ....

{
  "mode": "error",
  "operation": "laserfiche_entry_delete",
  "kind": "not_found",
  "error": "not_found",
  "status_code": 404,
  "server_error_code": null,
  "server_message": null,
  "reason": "Server returned 404 — the entry, path, or endpoint does not exist.",
  "request_id": "9f2c…",
  "upstream_trace_id": null,
  "entry_id": 999
}

kind is one of five canonical ToolErrorKind values — LLMs branch on this for category-level decisions (retry vs ask user vs abort):

Kind Meaning
not_found The named entry, path, or endpoint doesn't exist. Verify with the user.
permission_denied Credentials, ACLs, or local fence config refused the operation.
rate_limited The server told the caller to slow down. Back off and retry.
invalid_input The request is malformed or fails a local pre-flight. Fix and re-call.
upstream_unavailable LF returned 5xx, 405, or an opaque failure. Retry once, then surface.

error is the more-specific subkind. Server-mapped subkinds:

Subkind Triggers
auth_failed HTTP 401/403, LF errorCode 9010, or LF 9528 ("LFDS unreachable" — usually creds too)
required_field_missing LF errorCode 9039/9066
not_found HTTP 404
method_not_allowed HTTP 405 — usually an MCP routing bug
unsupported_media_type HTTP 415 — usually a wire-format bug (missing Content-Type)
rate_limited HTTP 429
server_error HTTP 5xx or unrecognized failure

Tools also have pre-server mode: error shapes (path_not_allowed, path_traversal_blocked, exceeds_batch_cap, invalid_confirmation_token, missing_required_fields, page_range_required, invalid_page_range, invalid_name, invalid_field_name, invalid_tag_name, invalid_template_name, invalid_link_type, file_not_found, size_exceeds_cap, tool_not_allowed, destructive_scope_required). list_repositories returns mode: fallback instead of erroring when the server doesn't expose the endpoint — see the docstring for the response shape.

See docs/error-contract.md for the full taxonomy, per-tool triggers, and the kind ↔ subkind mapping.

Safety model

Wondering what Claude actually sees and where your document content goes? See Data handling & privacy — it covers the data flow, what leaves the machine, and how to scope a service account so sensitive folders are never exposed. The rest of this section is about the write-mode guards.

Writes are off by default. When you enable them (LF_READ_ONLY=false), the following guards are available — all independent, all opt-in except as noted:

  • Path-prefix fences (LF_WRITE_PATHS_ALLOW, LF_WRITE_PATHS_DENY) — every write checks the entry's fullPath (or the parent's for creates) against the configured prefixes. Case-insensitive, deny wins over allow, both \ and / accepted. move_entry fences on BOTH source and destination paths so a token from an allowed source can't be replayed to land in a denied folder. Strongest single fence — recommended for any non-trivial deployment.
  • Local import source fence (LF_IMPORT_SOURCE_DIRS, default unset) — import_document reads file_path off the MCP process's own filesystem; with this set, the resolved (symlinks included) path must fall inside one of the configured directories or the tool refuses with source_path_not_allowed. This is the source-side counterpart to the path-prefix fences above, which only cover the repository destination.
  • Tool-level allowlist (LF_WRITE_TOOLS_ALLOWED) — restrict which write tools register at all. Example: merge_fields,merge_tags,assign_template for a metadata-only deployment that can't create or delete anything.
  • Folder-delete batch cap (LF_DELETE_FOLDER_MAX_DESCENDANTS, default 50) — delete_entry on a folder with more immediate children refuses unless force_large_delete=true is passed alongside the confirmation token. The preview surfaces exceeds_batch_cap: true so the LLM can explain the size before re-calling. If the child-count probe itself fails (transient error), the delete fails CLOSED — child_count_probe_failed: true on the preview, and execute refuses with child_count_probe_failed regardless of force_large_delete — rather than assuming an unknown count is safe.
  • Audit-reason requirement (LF_REQUIRE_AUDIT_REASON, default false) — when true, delete_entry refuses without an audit_reason_id. Use get_audit_reasons to enumerate valid IDs.
  • Required-field validation (LF_VALIDATE_REQUIRED_FIELDS, default true) — assign_template lists FieldDefinitions, finds isRequired: true fields, checks them against what's on the entry and what's in the caller's fields=, and returns a structured missing_required_fields error before the PUT — instead of the server's opaque Multistatus response. [9039].
  • Two-step confirmation tokens (always on for destructive ops) — rename_entry, move_entry, delete_entry, delete_edoc, delete_pages return a preview + HMAC-signed token on first call; execute on second call. Tokens bind to (operation, entry_id, entry_name) plus the operation's execute-relevant parameters (page_range, new_name, destination), so the execute leg cannot silently swap in different arguments than the user confirmed; they expire after 5 minutes. By default the signing key is random per-process, so a restart invalidates pending tokens; set LF_CONFIRMATION_SECRET to derive a stable key instead (tokens survive restarts and verify across instances sharing the secret — for multi-instance deployments). This two-step dance is a prompt-level convention, not an enforced one — nothing stops an autonomous caller from confirming its own preview. LF_HTTP_OAUTH_DESTRUCTIVE_SCOPE (below) is the actual enforcement point for unattended callers.
  • Destructive-op OAuth scope (LF_HTTP_OAUTH_DESTRUCTIVE_SCOPE, OAuth mode only, opt-in) — executing delete_entry / delete_edoc / delete_pages requires this scope on the caller's token; previews don't. Lets one deployment serve both a human (whose token carries the scope) and an unattended agent (whose client_credentials token doesn't) — see Unattended agents. move_entry / rename_entry are reversible and not gated by this.
"env": {
  "LF_READ_ONLY": "false",
  "LF_WRITE_PATHS_ALLOW": "\\Sandbox\\mcp-test",        // scope to a sandbox first
  "LF_WRITE_TOOLS_ALLOWED": "create_folder,import_document,merge_fields,merge_tags,assign_template,delete_entry",
  "LF_DELETE_FOLDER_MAX_DESCENDANTS": "10",
  "LF_REQUIRE_AUDIT_REASON": "false"                    // turn on once you have a workflow
}

Pre-create the sandbox folder by hand in the Laserfiche web client; the fence needs an existing parent to read its fullPath. Once smoke-tested, broaden the tool list — path scope is still the strongest fence regardless of which tools are registered.

Roadmap

  • Server-side audit logging — sidecar file with rotation, capturing every write tool call with the authenticated user, target entry, and outcome.
  • Cloud verification — LF_DEPLOYMENT_MODE=cloud / LF_AUTH_MODE=api_key is implemented (service-app JWT-assertion flow against signin.laserfiche.com, routed through the existing api.laserfiche.com/repository/v2/... client) but has never been exercised against a live tenant — see the Configure section's Cloud warning. Needed: someone with Cloud access to confirm the token exchange and a handful of Repository API calls actually succeed end to end.
  • v3.0 — Remove the verb-first deprecation aliases (get_entry, set_fields, ...). Only the laserfiche_{resource}_{verb} names remain.
  • Stateless MCP (spec 2026-07-28) — the protocol core is now stateless: the initialize handshake and session IDs are gone, and cross-call state must live in server-minted handles passed as ordinary tool arguments. This server is already shaped for that world — the HMAC-signed confirmation_token is exactly such a handle (set LF_CONFIRMATION_SECRET so every instance can verify every instance's tokens), and the async-search token never crosses a call boundary. The remaining review item for a remote/multi-instance deployment is that the per-process lifespan client and schema caches become per-instance. The cacheable tools/list in the new spec also raises the value of a small catalog (LF_LEGACY_TOOL_NAMES=false).
  • Beyond — Workflow trigger tools, and per-viewer table summaries for spreadsheet entries.

Development

uv sync --extra dev
uv run pytest                  # mocked HTTP, enforces 85% coverage baseline
uv run ruff check src tests
uv run mypy src

Tests use pytest-httpx to mock the Repository API and committed fixture PDFs to exercise the text-extraction paths — they don't require a real Laserfiche server.

Opt-in integration tests

LF_INTEGRATION_TEST=1 uv run pytest tests/test_integration.py

Reads the same LF_* env vars the server uses at runtime. Optional overrides:

  • LF_INTEGRATION_FOLDER_PATH — folder used in the search_natural Mode A test (defaults to repository root)
  • LF_INTEGRATION_PDF_ENTRY_ID — known PDF entry; if unset, edoc tests skip
  • LF_INTEGRATION_SAFE_QUERY — a query expected to return results on your repo (defaults to {LF:Name="*"})

Use this before tagging a release if you have a reachable repository — it catches issues that mocked HTTP can't surface (server-side query syntax quirks, real PDF extraction, transport-level rejections).

Contributing

Issues and PRs welcome — particularly:

  • Endpoint corrections for Repository API Server builds the v1 / v2 wire format hasn't been validated against
  • Confirming Cloud auth (LF_AUTH_MODE=api_key) against a real Laserfiche Cloud tenant — implemented and unit-tested against the documented flow, but nobody has run it against a live account yet
  • Confirming true on-behalf-of (LF_AUTH_MODE=oauth_passthrough, see docs/remote-http.md) against a real LFDS tenant — reuses the calling user's own verified OAuth token instead of a shared service account; whether it works end-to-end depends on your LFDS's token audience, which nobody has confirmed against a live instance yet
  • Server-side audit logging for write-mode deployments (sidecar file + rotation)
  • Text extraction for more document formats (ops/extract.py)

This is a community project, not affiliated with or endorsed by Laserfiche.

License

Released under the MIT License. Copyright (c) 2026 Samuel S. Hernandez.

Metadata

Release files for laserfiche-mcp 2.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for laserfiche-mcp 2.5.0
File Size Uploaded
laserfiche_mcp-2.5.0.tar.gz 444.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for laserfiche-mcp 2.5.0
File Interpreter ABI Platform
laserfiche_mcp-2.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 651.9 kB

Release files / laserfiche_mcp-2.5.0.tar.gz

Download URL laserfiche_mcp-2.5.0.tar.gz
Size 444.4 kB
Tags Source
SHA-256 checksum
How to use checksums
8853c39fc22f5893fbcd89de33ce927c50466e6e60bd2b15bef22336141cbdaa
BLAKE2b-256 checksum
How to use checksums
8afa50b537d3abc5f123e5e89cbb9c3fbf71413bf2b9dff023a96af482d0ff59
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / laserfiche_mcp-2.5.0-py3-none-any.whl

Download URL laserfiche_mcp-2.5.0-py3-none-any.whl
Size 207.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c6d3bcbf1ce692ab9d01f5df04f87c8815164c6585e8e4e07f208ca449b3b02
BLAKE2b-256 checksum
How to use checksums
2a749c73127275a20524e247ee5aa80845305ee6a5bf02dda94ca2d31f3c7bd8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.5.0 This release

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.4.2

2 release files

1.4.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page