Featured

Deploy OpenClaw in 60 seconds — 20% off logoDeploy OpenClaw in 60 seconds — 20% off

Launch OpenClaw on Hostinger in about 60 seconds and keep your agent live 24/7. Our referral link gives you 20% off, no coupon code needed.

Launch on Hostinger
Run your Hermes agent on Hostinger, fully managed logoRun your Hermes agent on Hostinger, fully managed

Launch Hermes on Hostinger in one click, fully managed, no VPS knowledge needed. Use code ZACAARON10 for 10% off.

Launch on Hostinger
Crawl and scrape any site into clean data, 10% off logoCrawl and scrape any site into clean data, 10% off

Firecrawl crawls and scrapes any site into clean markdown for your agent. Get 1,000 free credits, and new users get 10% off their first purchase.

Try Firecrawl free
6,000+ web scrapers for your AI agent, start free logo6,000+ web scrapers for your AI agent, start free

Apify gives your agent live web data: 6,000+ prebuilt scrapers and actors, MCP-ready. Sign up free with $5 in usage credits.

Try Apify free
One API to scrape, enrich, and extract the internet. logoOne API to scrape, enrich, and extract the internet.

Context.dev gives your agents a single API to scrape, enrich, and extract live web data — no proxies, no parsers, no maintenance.

Start building free
SetupClaw: done-for-you OpenClaw for founders & exec teams logoSetupClaw: done-for-you OpenClaw for founders & exec teams

White-glove OpenClaw for founders and exec teams (4–50+ employees): we install, harden, integrate your tools, and maintain it — secured from day one.

Get it set up for you
SEO data APIs for your agent, $1 free credit logoSEO data APIs for your agent, $1 free credit

DataForSEO gives your agent live access to SERP results, keyword data, backlinks, and on-page SEO data through one API. New accounts get a $1 credit, good for up to 20,000 keyword or backlink lookups.

Try DataForSEO free
Reach 48,000+ AI builders

A flat monthly placement in front of developers actively installing AI tools. No lock-in, cancel anytime.

Advertise here

Works with

Claude CodeClaude DesktopCursorVS CodeClineCodex CLIOpenClaw+ any MCP client

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

MCP server for LaSuite Docs API, enabling operations like list, read, create, update, delete documents, versions, favorites, and current user via OIDC authentication.

README.md

lasuite-docs-mcp

MCP server for the LaSuite Docs external API.

It exposes Docs operations (list/read/create/update/delete documents, versions, favorites, current user) as MCP tools. Authentication uses the OIDC Authorization Code + PKCE flow against any OpenID Connect provider, with a refresh token cached in the OS keychain. Runs over stdio — no public hosting, no inbound ports. The OIDC redirect is captured by a one-shot loopback server on 127.0.0.1.

Claude ──stdio──> lasuite-docs-mcp ──Bearer token──> Docs /external_api/v1.0
                        │
                        └── Auth Code + PKCE ──> OIDC provider (issues + introspects tokens)

Works with any standards-compliant OIDC provider (Keycloak, Authentik, Zitadel, Auth0, Okta, Ory Hydra, …). Authentik is used as a worked example throughout, marked Example (Authentik).

---

1. Prerequisites

  • Python ≥ 3.10
  • A running LaSuite Docs instance with the resource server enabled (§2)
  • An OIDC provider you control, able to register clients and run token

introspection (§3)

2. Configure LaSuite Docs (server side)

Docs must run as an OIDC resource server. Set in Docs' environment:

OIDC_RESOURCE_SERVER_ENABLED=True
OIDC_OP_URL=<issuer URL of your OIDC provider>
OIDC_OP_INTROSPECTION_ENDPOINT=<provider token-introspection endpoint>
OIDC_RS_CLIENT_ID=<client id Docs uses to call introspection>
OIDC_RS_CLIENT_SECRET=<that client's secret>
OIDC_RS_AUDIENCE_CLAIM=<claim Docs checks for audience, e.g. aud>
OIDC_RS_ALLOWED_AUDIENCES=<value the MCP token must carry, see §3.4>

Introspection-response format — important. django-lasuite ships two resource server backends:

| OIDC_RS_BACKEND_CLASS | Expects the introspection response as | |--|--| | …JWTResourceServerBackend (default) | a signed+encrypted JWT (RFC 9701 application/token-introspection+jwt) | | …ResourceServerBackend | plain JSON (RFC 7662, standard) |

Most providers (Keycloak, Authentik, Zitadel, Auth0, …) return plain JSON. If yours does, set: ``env OIDC_RS_BACKEND_CLASS=lasuite.oidc_resource_server.backend.ResourceServerBackend ` Leave the default only if your provider actually issues JWT-secured introspection responses (e.g. ProConnect). Wrong choice → 400 Bad Request` / "improperly configured" (see §9).

2.1 EXTERNAL_API action allowlist

EXTERNAL_API controls which resources/actions the resource-server API exposes. Every tool maps to an action that must appear in this allowlist, or the call returns 403 (and unknown nested resources return 404). The default only allows documents: [list, retrieve, create, children] and users: [get_me], so most tools need it widened. Full config covering every tool in this server:

EXTERNAL_API={"documents":{"enabled":True,"actions":["list","retrieve","create","children","formatted_content","content_retrieve","content","partial_update","destroy","move","favorite","versions_list","trashbin","favorite_list","restore"]},"document_access":{"enabled":True,"actions":["list","retrieve","create","update","partial_update","destroy"]},"users":{"enabled":True,"actions":["get_me"]}}

Gotcha — Python literal, not JSON. Docs parses this env var with ast.literal_eval, so booleans must be True/False (capitalized), not JSON true/false. JSON booleans crash Docs at boot with ValueError: Cannot interpret dict value ... malformed node or string (§9).

Action → tool mapping (non-obvious ones): formatted_contentget_document_content (reads the body), contentupdate_document_content (PATCH raw Yjs), content_retrieve → raw base64 Yjs GET, children → parented create_document, document_access. → the _document_access tools.

2.2 Markdown upload (content on create)

The document resource has no plain content field. create_document with a markdown body uploads it as a .md file that Docs converts to Yjs server-side. That conversion needs:

CONVERSION_UPLOAD_ENABLED=True

Without it, create_document(markdown=...) returns 400 file upload is not allowed. Reading the body back uses the formatted_content action (above).

Docs validates every token by introspecting it and checking: the token is active, its issuer equals OIDC_OP_URL, and its audience claim is in OIDC_RS_ALLOWED_AUDIENCES. All three must line up (§3). Mismatch → 400/401.

3. Configure the OIDC provider

Register a client for the MCP server and make sure the tokens it issues pass Docs' three checks. The concepts are provider-agnostic; the example shows where each lives in Authentik.

3.1 Register the MCP client

Create an OAuth2 / OIDC client for the MCP server:

  • Client type: public (PKCE, no secret) — recommended for a desktop/CLI tool.

Confidential (with secret) also works.

  • Grant type: Authorization Code (+ refresh token).
  • Redirect URI: http://127.0.0.1:8765/callback (exact match; change both

here and OIDC_REDIRECT_URI together).

  • Scopes: openid profile email (+ an audience scope, §3.4).

Example (Authentik) — Admin → Applications → Providers → Create → OAuth2/OpenID Provider. Set redirect URI http://127.0.0.1:8765/callback, client type Public, attach an Application. Note its issuer URL https://<host>/application/o/<app-slug>/.

3.2 Issuer must match Docs (iss check)

The token's iss claim must equal Docs' OIDC_OP_URL exactly. Easiest way: let the MCP client and Docs trust the same issuer.

  • If your provider has one global issuer (Keycloak realm, Zitadel, Auth0

tenant), this is automatic — point both at it.

  • If your provider issues a per-client/per-application issuer, either reuse

the same provider/issuer Docs already trusts, or switch to a global/shared issuer mode so both emit the same iss.

Example (Authentik) — issuer is per-application by default (…/o/<slug>/), so a separate MCP application gets a different iss than the Docs login app → InvalidClaimError: iss. Fix: set the provider's Issuer mode = "Same identifier (global)" on both providers, or have the MCP use the same provider Docs trusts.

3.3 Introspection must accept the token (active check)

Docs introspects with OIDC_RS_CLIENT_ID/SECRET. Many providers only return active: true when the introspecting client is allowed to introspect that token (usually the token-issuing client, or one explicitly granted). If a different client introspects, you get active: false → "user is not active".

Fix: set Docs' OIDC_RS_CLIENT_ID/SECRET to a client permitted to introspect the MCP tokens — typically the same client that issues them, or configure your provider's introspection/audience permissions accordingly.

Example (Authentik) — cross-client introspection returns active:false. Set Docs OIDC_RS_CLIENT_ID/SECRET to the MCP application's client.

3.4 Audience (OIDC_RS_ALLOWED_AUDIENCES check)

The token must carry an audience value that Docs allows. Two approaches:

A — provider emits the audience claim (recommended). Add a claim/mapper so issued tokens include the configured audience under the claim Docs reads (OIDC_RS_AUDIENCE_CLAIM). Set OIDC_RS_ALLOWED_AUDIENCES to that value.

Example (Authentik) — Customisation → Property Mappings → Create → Scope Mapping: scope name docs, expression return {"docs-aud": "<your-audience>"}. Attach scope docs to the provider, add docs to OIDC_SCOPE, set Docs OIDC_RS_AUDIENCE_CLAIM=docs-aud and OIDC_RS_ALLOWED_AUDIENCES=<your-audience>. Simpler alternative: use the standard aud claim (= client id by default) — set OIDC_RS_AUDIENCE_CLAIM=aud, OIDC_RS_ALLOWED_AUDIENCES=<mcp-client-id>.

B — audience request parameter. If your provider honors an audience auth-request param, set OIDC_AUDIENCE (§4). Not all providers honor it (Authentik ignores it) — prefer A.

The claim name in OIDC_RS_AUDIENCE_CLAIM and the value in OIDC_RS_ALLOWED_AUDIENCES must point at the same thing, and that thing must be present in the token / introspection response.

4. Configure the MCP server

cp .env.example .env   # then edit, or pass these via the MCP env block (§6)

| Var | Meaning | |-----|---------| | DOCS_BASE_URL | Docs host, e.g. https://docs.example.org | | DOCS_API_PREFIX | API prefix, default /external_api/v1.0 | | OIDC_OP_URL | Your provider's issuer URL (server reads …/.well-known/openid-configuration) | | OIDC_CLIENT_ID | the MCP client's id | | OIDC_CLIENT_SECRET | empty for public/PKCE; set for a confidential client | | OIDC_SCOPE | openid profile email (+ audience scope if using §3.4 A) | | OIDC_REDIRECT_URI | http://127.0.0.1:8765/callback (must match the provider) | | OIDC_AUDIENCE | only for §3.4 B; else leave empty |

5. Install

Pick one. All expose the lasuite-docs-mcp command.

uv tool (recommended): ``bash uv tool install git+https://github.com/Bone2510/lasuite-docs-mcp # from GitHub uv tool install . # local checkout ``

pipx: ``bash pipx install git+https://github.com/Bone2510/lasuite-docs-mcp ``

Editable dev install: ``bash python -m venv .venv && source .venv/bin/activate pip install -e . ``

No install (uvx, run on demand): ``bash uvx --from git+https://github.com/Bone2510/lasuite-docs-mcp lasuite-docs-mcp ``

6. Register in Claude Code

Add to your MCP config (~/.claude.json / project .mcp.json). With uv tool/pipx the command is on your PATH. The installed command runs from an arbitrary working dir, so pass config via the env block (a .env is only picked up when the cwd is the project dir):

{
  "mcpServers": {
    "lasuite-docs": {
      "command": "lasuite-docs-mcp",
      "env": {
        "DOCS_BASE_URL": "https://docs.example.org",
        "OIDC_OP_URL": "https://auth.example.org/realms/main",
        "OIDC_CLIENT_ID": "lasuite-docs-mcp",
        "OIDC_CLIENT_SECRET": "...",
        "OIDC_SCOPE": "openid profile email docs",
        "OIDC_REDIRECT_URI": "http://127.0.0.1:8765/callback"
      }
    }
  }
}

No-install variant: ``json "command": "uvx", "args": ["--from", "git+https://github.com/Bone2510/lasuite-docs-mcp", "lasuite-docs-mcp"] ``

On the first tool call the server opens your browser to the provider. After login it captures the redirect on 127.0.0.1:8765, stores the refresh token in your OS keychain, and reuses/refreshes it silently afterward.

7. Test standalone

mcp dev lasuite_mcp/server.py     # MCP inspector (interactive)

Call get_current_user first — it exercises the whole chain (login → introspection → issuer → audience → user).

8. Available tools

Read: get_current_user, list_documents, get_document (metadata only), get_document_content (body as markdown/html/json), get_children, list_versions, list_trashbin, list_favorites.

Write: create_document (optional markdown body), update_document (title), update_document_content (raw base64 Yjs), delete_document, restore_document, move_document, add_favorite, remove_favorite.

Accesses (sharing): list_document_accesses, get_document_access, create_document_access, update_document_access, delete_document_access.

get_document never returns the body — Docs stores it as collaborative Yjs. Use get_document_content to read it (needs the formatted_content action, §2.1).

9. Troubleshooting

Server-side errors surface as a bare Django page; the real reason is in the Docs backend logs. Common cases (all server-side OIDC config):

| Symptom | Cause / fix | |--------|-------------| | 401 Resource Server is improperly configured | RS backend failed to init — OIDC_RS_CLIENT_ID/SECRET missing/empty or OIDC_RS_BACKEND_CLASS invalid (§2). | | 400 right after introspection, decode/decrypt error | Provider returns plain-JSON introspection but JWTResourceServerBackend is active → set OIDC_RS_BACKEND_CLASS=…ResourceServerBackend (§2). | | 400 Introspected user is not active | Introspecting client may not introspect this token → set Docs RS creds to the token-issuing client (§3.3). | | 400 InvalidClaimError: iss | Token iss ≠ Docs OIDC_OP_URL → align issuers / issuer mode (§3.2). | | 401/400 audience | Token audience ∉ OIDC_RS_ALLOWED_AUDIENCES, or claim name mismatch → §3.4. | | Browser never opens | stdio has no TTY; the auth URL is printed to stderr — open it manually. | | redirect_uri_mismatch | Provider redirect URI ≠ OIDC_REDIRECT_URI. Must match exactly. | | 404 on a tool | Your Docs version uses a different path — adjust in lasuite_mcp/server.py. | | accesses/invitations 404 | Those scopes are disabled in Docs EXTERNAL_API by default — enable them (§2.1). | | 403 on a tool | The action is not in the EXTERNAL_API allowlist — add it (§2.1). | | get_document returns no body | By design — body is Yjs, use get_document_content (§8). | | 400 file upload is not allowed on create | CONVERSION_UPLOAD_ENABLED not set in Docs (§2.2). | | Docs crashes at boot, ValueError: Cannot interpret dict value … malformed node | EXTERNAL_API uses JSON true/false; needs Python True/False (§2.1). | | Port 8765 in use | Change OIDC_REDIRECT_URI port (and the provider redirect URI). |

10. Security notes

  • PKCE (S256) is always used, even with a client secret.
  • Redirect is loopback-only (127.0.0.1), random state checked against CSRF.
  • Refresh token lives in the OS keychain (via keyring); if keyring is

unavailable it falls back to in-memory (re-login each start).

  • Tokens are never logged.

Notes / unknowns to verify against your instance

  • Content model (verified against current Docs): the body is a Yjs/BlockNote

CRDT, not a field on the document resource. Read it via get_document_content (formatted-content endpoint → markdown/html/json). Write paths: create_document uploads a markdown file that Docs converts to Yjs (needs CONVERSION_UPLOAD_ENABLED); update_document_content PATCHes a raw base64 Yjs blob (no markdown→Yjs path exists for updates). update_document only changes the title.

  • Exact sub-paths (favorite-list/, move/, restore/, favorite/,

accesses/) follow current conventions — confirm against your Docs EXTERNAL_API routes.

See related servers & alternatives →

Related MCP servers

Browse all →

Related guides

Hand-picked reading to help you choose and use Files & Docs servers.