TrustDesk MCP Server
Allows connecting to a self-hosted Ollama server as an OpenAI-compatible provider for chat and embeddings, when the operator enables private LLM URLs.
Allows configuring OpenAI provider accounts for embeddings and chat, enabling semantic search and cited answer generation through OpenAI models. Supports testing chat/embeddings and re-embedding passages when switching embedding models.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
A compliance workspace built on Postgres + pgvector, with a web app, a REST API and an MCP server. Load your company rules (HR, travel, expenses, security), let your applications send employee requests (vacation, sick leave, trips, expense reimbursements and anything else), and have every request pre-validated by an AI model against your own documents before an approver decides. The advice comes with the reasons and the quoted passages it is based on. You can also search the documents and ask questions that cite their sources.
Documentation: docs/GUIDE.md is the complete guide (ecosystem fit, architecture, install, configuration, operations, limitations); docs/VIDEO-SCRIPT.md is a 6-minute tutorial video script; API.md and SECURITY.md cover the client API and the security model. The section EDB and AgentPG capabilities used by this app lists the database features it uses (insights, masked read-only access, unaccent, tcn live updates) and what degrades without them.
Hosted sandboxes can pause themselves and then answer 503.
If you deploy TrustDesk on a platform whose free tier auto-pauses idle workloads (AgentPG sandboxes do), the app stops after a period of inactivity and the platform returns an empty HTTP 503 for every URL until you resume it. Your data is safe (it lives in the database), but the app is unreachable.
Resume it:
agentpg sandbox resume <sandbox-id>.Then unlock it: a restart locks the stored provider keys until you sign in again (see How secrets are handled). Search with an external embedding provider and Ask need the keys.
Plan for it: resume a few minutes before any demo.
Details and fixes: see Troubleshooting and Deploy on AgentPG.
Features
Requests with AI pre-validation: 19 request types (leave, trips, expenses, remote work, overtime, equipment, anything else). Each new request gets a recommendation (approve, reject, needs information), the reasons, and the policy passages it rests on, with quotes checked word for word against the documents. A person always decides; going against the advice needs a written justification. See Compliance workflow.
Client API (
/api/v1, see API.md) for applications that create employees and send requests, with a token scope that cannot see the AI advice or decide anything.Reports: overview, all requests, compliance by type, out of compliance, decisions against AI advice, expenses, leave, ageing and upcoming absences, as tables and CSV.
Web app at
/: requests queue and review window, employees, reports, documents, search with explainable results, Ask (an answer with citations from the model you pick), drag-and-drop upload, document viewer, provider accounts, API tokens, and copy-paste MCP setup for ten clients. Light and dark mode; works on phones.Multiple provider accounts (OpenAI, OpenRouter, GLM, Kimi, Claude, any OpenAI-compatible server), configured in the browser. Choose one for embeddings and one per question.
Hybrid retrieval: vector and full-text results fused with reciprocal rank fusion (RRF), with provenance on every result.
Safe by default: no secret files, admin password set on first run, hashed API tokens, encrypted provider keys. See How secrets are handled.
MCP server at
/mcp(streamable HTTP):search_knowledge,get_policy_passage,list_documents.Plain-SQL migrations (no ORM), Markdown/text/PDF ingest with heading-aware chunking, idempotent and versioned.
Works offline with a built-in demo embedder, and ships a fictional "Acme Cloudworks" policy pack for demos.
Related MCP server: KSoR
Quick start
Requires Docker and uv (or any Python 3.11+ environment).
cp .env.example .env # then set POSTGRES_PASSWORD; .env is git-ignored
docker compose up -d db
uv venv && uv pip install -e ".[dev]"
set -a; . ./.env; set +a
.venv/bin/trustdesk migrate
.venv/bin/trustdesk serve # http://localhost:8000pyproject.toml takes the shared kit (agentpg-kit) from the same release tag the Docker image installs (v0.5.1), so no sibling checkout is needed. If you are also changing the kit, install your checkout over it: uv pip install -e ../agentpg-kit (or make kit-local).
The first time the server starts it prints a one-time setup code to its console:
=== TrustDesk first-run setup ===
Open the app in your browser and enter this one-time code to create the admin password:
XXXX-XXXX-XXXXOpen http://localhost:8000, enter the code and choose an admin password (12+ characters). Then go to Documents and click Load demo policies. Everything else is configured in the browser; there are no key files to create.
Everything in containers: docker compose up --build, then read the setup code from docker compose logs api.
Embeddings
With no provider configured, a built-in demo embedder is used so everything runs with no credentials. It matches on words, not meaning. For real semantic search, add a provider account and switch embeddings to it (see Using external LLMs).
How secrets are handled
Files full of credentials are the usual way these tools leak, so TrustDesk avoids them:
Secret | Where it lives | How it is protected |
Admin password | Entered once in the browser (first run needs the setup code from the server log) | Stored only as a salted scrypt hash. |
Browser session | An | Random value, only its SHA-256 stored server-side, 12-hour expiry, signing out or changing the password ends every session. No credential is kept in page storage. State-changing browser requests also need a custom header. |
API tokens (MCP, scripts) | Created in Settings, shown once | Only a SHA-256 hash is stored. Revocable. |
Provider API keys (OpenAI etc.) | Entered in Settings | Encrypted (AES via Fernet) with a random data key. The data key is never stored in the clear: it is wrapped under a key derived from your admin password (scrypt). The database alone reveals nothing. Keys are never returned by the API (only the last 4 characters) and never logged. |
What this means in practice
After a restart the stored provider keys are locked until you sign in or use the unlock box. The unwrapped key exists only in the server's memory. Until then, external embeddings and Ask return "locked" (searching with the demo embedder keeps working).
For unattended restarts, you may additionally provide a platform-managed secret as
TRUSTDESK_ENCRYPTION_KEY(or a file at/etc/trustdesk/encryption_key). The server then wraps the data key a second time and unlocks itself. This reintroduces one secret outside the app; keep it in your platform's secret store, never in git or an image.Sealed boot (optional hardening): anything in a container's environment, config files or image is readable by whoever breaches the platform. To deploy with none of it, set only
APGKIT_SEALED: one blob, encrypted with AES-256-GCM under an operator passphrase that is never stored on the platform. Create it withecoctl sealfrom agentpg-ecosystem, puttingDATABASE_URL,TRUSTDESK_OIDC_CLIENT_SECRET,LLM_API_KEYandTRUSTDESK_API_KEYin it. TrustDesk then starts sealed: every route answers 503 except/_seal/*and/healthz, and it does not touch the database. The operator runsecoctl unseal(the passphrase travels encrypted to a key that exists only in the running process); the app opens the blob, migrates, unlocks the vault with the same passphrase (TRUSTDESK_ENCRYPTION_KEYis ignored) and starts serving, including the background jobs and the MCP server. A restart seals it again. With noAPGKIT_SEALED, nothing changes.Row MACs (hardening): while the vault is unlocked (always, with sealed boot) every row that grants access or carries trust holds a MAC, an HMAC-SHA256 under a subkey of the vault data key. Someone who can only write to the database (stolen credentials) cannot forge one, so a planted or edited row is treated as missing and logged once per row (table and key, never values). Covered: web sessions and API tokens (signed by the kit), provider accounts (
llm_account: id, provider, base URL, chat and embedding model and dimension, which decide where requests are sent, and the encrypted provider key, so a key cannot be blanked, moved to another account or put back to an older value), and the settings that choose the validating and embedding account and the validation rates (app_setting, those four keys). After upgrading runtrustdesk migrate(it clears the provider-account MACs written before the key was covered), thentrustdesk adopt-rowmacsonce (it asks for the admin password) to sign the rows that exist; it trusts the database as it is at that moment. Rows written while the vault is locked have no MAC and are ignored once it opens until adopted. Rotating the data key re-signs the rows that verified. With the vault locked nothing is checked and behaviour is unchanged. Honest limits: an older authentic row can be put back from a backup (replay); deleting a row is still possible (a deleted session or account is a denial of service, a deleted setting reverts to its default); employees, requests, documents and every column outside the list above are not covered; an attacker who can also read the process memory or the data key is out of scope.Forgot the admin password? Run
trustdesk reset-adminon the server (it needs database access). It clears the password, the vault and every stored provider key, and the setup flow starts again. There is no back door: that is the point.Optionally, a static
TRUSTDESK_API_KEY(environment variable or file) still works as a full-access credential for automation. Prefer API tokens.Outbound requests: provider URLs you enter are checked before every call. Loopback, private, link-local (cloud metadata) and carrier-grade-NAT addresses are refused, along with embedded credentials and plain
httpto non-local hosts; redirects are never followed. To allow a local model server such as Ollama, the operator setsTRUSTDESK_ALLOW_PRIVATE_LLM_URLS=true.
See SECURITY.md for the full model and its limits.
Using external LLMs (OpenAI, OpenRouter, GLM, Kimi, Claude and others)
A language model does two different jobs here, configured in different places:
Job | Where | Notes |
Embeddings: turn documents and questions into vectors for search | Settings > Search embeddings: pick one account | Needs an OpenAI-compatible |
Answering: write the reply from retrieved passages | Ask: pick an account for each question | Needs an OpenAI-compatible |
Answering through your own tools | Your MCP client's model | Claude Code uses Claude, Codex uses OpenAI models, and so on; see Using the MCP server. |
Add accounts in the browser
Open Settings > Provider accounts > Add account, choose a provider (this pre-fills the URL and suggestions), paste the API key, set the chat and/or embedding model names, and use Test chat / Test embeddings to confirm it works. You can add as many accounts as you like. Model names change often, so use the ones from your provider's documentation.
Provider | Base URL | Notes |
OpenAI |
| Embedding model |
OpenRouter |
| One key for many models; has an OpenAI-style embeddings endpoint (for example |
Zhipu GLM |
| Documented as OpenAI-compatible; embedding model |
Moonshot Kimi |
| Documented for chat only; use another account for embeddings. |
Anthropic Claude |
| Anthropic's OpenAI-compatibility endpoint, which they describe as not for production use. Chat only: Anthropic offers no embedding model. |
Self-hosted (Ollama, vLLM, LiteLLM) | for example | Needs |
These providers were checked against their public documentation only. The request format is covered by automated tests against a mock server and was exercised end to end in the UI against a fake local provider, but it hasnot been run against each live provider. Test with a few documents first.
Choosing the embedding dimension. It must match what the model returns; if it doesn't, TrustDesk tells you the number the model actually returned. The index supports at most 2000 dimensions, so models whose default is larger (for example 3072) must be asked for a smaller size.
Privacy. With an external provider, the text of document passages, search queries and questions is sent to that provider. Don't use it for documents you aren't allowed to share with them, and check their data-retention terms.
Environment configuration (optional)
You can also configure a single provider from the environment instead of the browser. It appears read-only in Settings as "Environment".
Variable | Meaning |
| Provider base URL. Empty means the demo embedder. |
| Provider key. Prefer entering keys in the browser. |
| Embedding model and vector size (default |
| Chat model, to use this account in Ask. |
|
|
| How many waiting requests the background pass validates per minute (default |
| How many times in all a failed validation is attempted automatically (default |
The environment account is the default. With LLM_BASE_URL and LLM_CHAT_MODEL set, nothing has to be chosen in Settings: Request validation, Ask and search embeddings use it ("Using the environment account: default"). A choice saved in Settings always wins, including None (no AI validation); picking Default forgets the saved choice. The dropdown shows what is really saved, and Save confirms on the page. If a stored account has the same base URL and chat model as the environment one, Settings notes "Same endpoint as the environment account" (nothing is deleted for you).
Catching up. Requests that arrived while no account was usable are marked "validation skipped". As soon as an account is available, a background pass (once a minute, VALIDATE_BACKLOG_PER_MINUTE requests at most, oldest first, recorded in the history like any validation) validates them. It does nothing while there is no account, or while a stored account's key is locked, so it never records a failure for a problem a person can fix. Settings shows how many requests are waiting, lets you change the rate, and keeps Validate waiting requests for doing it right now.
Retrying failures. A validation that ended in "failed" (provider error, revoked key, upstream 402/502, an answer that was not a valid verdict) is retried by the same background pass after 5, then 15, then 60 minutes, at most VALIDATE_MAX_ATTEMPTS attempts in all (the count is kept per request in the database, and every attempt is a line in the request history saying whether another try is coming). A failure that is the provider refusing that very request (HTTP 400, 413 or 422) is not retried. Requests that were already stuck in "failed" before this existed count as one failed attempt, so they are picked up once the cause is fixed. Settings shows how many requests are waiting, failed and will be retried, and failed and gave up, and lets you change the number of attempts. Validate waiting requests and Run again include failed requests at once and start their attempt count over; editing a request after a question does too.
From a terminal, trustdesk reembed re-embeds everything after you change these.
If the database was created with another size than EMBEDDING_DIM now says, startup no longer fails: with up to 5000 chunks it re-embeds by itself (atomic; if the provider is down nothing changes and one warning is logged); with more, or for a size above 2000, it logs a warning and starts anyway. Until re-embedded, search uses full-text only, uploads answer 503, and GET /api/embeddings/status and /api/stats report needs_reembedding (the UI shows a banner). Run trustdesk reembed, or activate the embedding account again in Settings, to fix it.
Single sign-on and roles (the ecosystem identity provider)
TrustDesk can run standalone (the first-run setup code and an admin password, as above) or federated with the ecosystem's
identity provider. Register TrustDesk there as a web app with client id
trustdesk, redirect URI https://<trustdesk>/auth/callback, and these roles, then set:
Variable | Meaning |
| The identity provider's URL. Federation is off when unset. |
| Default |
| The app's secret from the provider. |
| The URL TrustDesk is reached at (used to build the callback). |
|
|
| With SSO only (no admin password) this lets TrustDesk create and unlock its vault for stored provider keys. |
Role | May |
| everything, including documents (upload, delete), provider accounts, tokens, validation settings and demo data |
| read and search, ask (the Ask tab, with a list of account names only), submit requests, decide requests, run validation again, open reports. Cannot change documents, accounts or settings |
| read and search, see requests, their AI advice and reports; cannot decide or ask |
Documents can be uploaded and deleted only with the kb:write permission, which only admin holds. Asking spends provider credit, so it is its own permission (ask); approvers have it and get a narrow, read-only list of the accounts that can chat (GET /api/ask/accounts: name and model, no URL or key details). The full accounts listing (/api/accounts) stays admin-only.
Services use client_credentials instead of a hand-copied token: scope trustdesk.requests.write makes a service a client
application (create employees and requests, no AI advice, no decisions), trustdesk.kb.read lets it search, trustdesk.metrics
lets it scrape. Local API tokens keep working beside them. Decisions are recorded under the approver's name.
Compliance workflow
Load the rules. Under Documents, upload your policies (Markdown, text or PDF) and tag them (HR, travel, expense, governance, security). Load demo policies adds a fictional "Acme" pack covering vacation, sick and parental leave, remote work, overtime, travel, expenses, customer entertainment and who approves what.
Connect an AI account. Add an account with a chat model under Settings > Provider accounts, then choose it under Request validation. Without one, requests still arrive but without advice.
Requests arrive from your applications through the client API or by hand (New request). TrustDesk checks the fields, stores the request and starts the pre-validation in the background.
Pre-validation (a few seconds). The system computes the facts a model gets wrong when it guesses: working days, notice given, overlapping leave, leave already taken this year, expenses already approved, possible duplicates, missing receipts. It retrieves the policy passages that relate to the request type and the request text, and asks the model for one structured verdict. TrustDesk then checks that verdict before anyone sees it: citations must point at real passages, each quote is matched word for word against the passage and marked verified or not, a "yes" or "no" with no citable basis becomes "needs information", and an approval is not suggested while an automatic check fails (for example an overlap with approved leave).
The review window shows the request, the employee, the recommendation, the reasons, the foundation (document, heading, version, quote, full passage), the automatic checks and the history. You approve, reject, or ask for information (which sends the request back to the employee). Rejecting or asking needs a note the employee will see. Approving against a "reject" recommendation, or rejecting against an "approve", needs a written justification and is listed under Reports > Decisions against AI advice.
Reports cover volumes and outcomes, compliance by request type, anything out of compliance (including requests approved anyway), expenses by department, approved leave, open requests by age and upcoming absences. Each can be downloaded as CSV.
What goes to the AI provider: the request text and details, the system-computed facts (department, job title, level, country, tenure, dates, amounts) and the retrieved policy passages. Not names, e-mail addresses or employee numbers. Free text written by an employee can still contain personal data, so use a provider you are allowed to send it to.
Limits worth knowing: the AI only advises and can be wrong, so read the quoted passages. Working days exclude weekends but not public holidays. There is one approver identity (the signed-in admin); there are no per-employee sign-ins, e-mail notifications or receipt uploads, and validation is only as good as the documents you load. Validation quality has been exercised with a scripted stand-in model in tests and for the UI, not benchmarked against real providers.
Using the web app
Requests: the queue (open by default) with counts, filters and the review window described above. New request adds one on behalf of an employee.
Employees: add, edit, deactivate; Requests jumps to that person's requests.
Reports: pick a report and dates, run it, print it or download the CSV.
Search: ask in plain language. Each result shows its document, heading, version and how it ranked in the vector, full-text and typo-tolerant (
pg_trgm) searches, so a misspelt query like "vaction polcy" still finds the vacation policy. "Copy citation" copies a ready-to-paste reference.Ask: choose an account and ask a question. You get an answer with
[1]-style citations that jump to the numbered source passages. Treat it as a draft: check the sources.Documents: drag files in (Markdown, text, PDF; 10 MB max each), choosing the type first (HR, travel, expense, governance, security or general). Re-uploading a changed file replaces the old version. Load demo policies adds a fictional policy pack. View shows how a document was split into chunks.
Settings: provider accounts, which account provides embeddings, your admin password and API tokens.
Database: (admin) slow statements, tuner recommendations and index hints from EDB's extensions, where the database has them.
Connect: pick your AI client and copy a ready-made configuration with this server's URL filled in.
Scripting uploads needs an API token with full access (Settings > API tokens):
for f in src/trustdesk/seed/acme/*.md; do
curl -H "Authorization: Bearer $TRUSTDESK_TOKEN" -F file=@"$f" https://<your-host>/api/documents
doneUsing the MCP server
TrustDesk serves MCP over streamable HTTP at https://<your-host>/mcp, authenticated with Authorization: Bearer <token>. Create a read-only token under Settings > API tokens. Browser sessions are not accepted on /mcp. Tools:
Tool | Purpose |
| Hybrid search; returns passages with document, version and heading to cite |
| Fetch one passage by id |
| List what is in the knowledge base |
Set the token in the environment where the client runs, then add the server to your client. The same snippets, with your URL filled in, are on the app's Connect tab. Replace <your-host> below; for a local server use http://localhost:8000/mcp.
export TRUSTDESK_TOKEN=<your token>Run in a terminal:
claude mcp add --transport http trustdesk https://<your-host>/mcp \
--header "Authorization: Bearer $TRUSTDESK_TOKEN"Save as .vscode/mcp.json in your workspace (VS Code asks for the key once and stores it securely):
{
"servers": {
"trustdesk": {
"type": "http",
"url": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer ${input:trustdesk-key}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "trustdesk-key",
"description": "TrustDesk API token",
"password": true
}
]
}Add to opencode.json (project) or ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"trustdesk": {
"type": "remote",
"url": "https://<your-host>/mcp",
"enabled": true,
"headers": {
"Authorization": "Bearer {env:TRUSTDESK_TOKEN}"
}
}
}
}Add to ~/.codex/config.toml:
[mcp_servers.trustdesk]
url = "https://<your-host>/mcp"
bearer_token_env_var = "TRUSTDESK_TOKEN"Add to ~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"trustdesk": {
"url": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer ${env:TRUSTDESK_TOKEN}"
}
}
}
}Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"trustdesk": {
"serverUrl": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer ${env:TRUSTDESK_TOKEN}"
}
}
}
}Add to ~/.gemini/settings.json:
{
"mcpServers": {
"trustdesk": {
"httpUrl": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer $TRUSTDESK_TOKEN"
}
}
}
}Add to cline_mcp_settings.json (Cline: MCP Servers, then Configure):
{
"mcpServers": {
"trustdesk": {
"type": "streamableHttp",
"url": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer <your token>"
}
}
}
}Add to Zed settings.json:
{
"context_servers": {
"trustdesk": {
"url": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer <your token>"
}
}
}
}Claude Desktop needs a local bridge for bearer-key servers. Add to claude_desktop_config.json (requires Node.js):
{
"mcpServers": {
"trustdesk": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://<your-host>/mcp",
"--header",
"Authorization:${AUTH}"
],
"env": {
"AUTH": "Bearer <your token>"
}
}
}
}Then ask your assistant something like: "Using trustdesk, how long do we retain customer data after termination?"
Notes:
The server authenticates with a bearer token, not OAuth, so web-based connector UIs that require OAuth (such as claude.ai custom connectors) are not supported. Use a client above.
Client configuration formats change between releases. If a snippet is rejected, check that client's MCP documentation for the current field names (
urlvsserverUrlvshttpUrl, how headers are declared, and whether it expands environment variables).If you use an external embedding provider and the server has restarted, searches return "locked" until you unlock the vault in the web app (or the operator configured a platform secret).
Treat search results as untrusted text: they come from your documents and may contain instructions. Don't give an assistant that reads them more authority than you would give the document authors.
AIDB mode (optional: EDB AI Accelerator inside the database)
With TRUSTDESK_RETRIEVAL_BACKEND=aidb the database does the AI plumbing that the app does itself otherwise. The default is app, which is plain Postgres and unchanged; every test of that path still runs and passes. aidb is opt-in and falls back to the app path, loudly, whenever it cannot be used. Section "AIDB mode" of SECURITY.md has the security model.
What was verified, and what was not. Everything below ran against AIDB 7.9.0 in a Docker image (aidb-postgres:17, built from the ecosystem repository) with the app connecting as an unprivileged role, and the tests are in tests/test_aidb_integration.py (marker aidb; make test-aidb). No real chat model was run: the agent's model is a scripted OpenAI-compatible server, so the tests prove the plumbing, the confinement and the checks, not how well a real model follows AIDB's simulated tool-calling format. It has not been run on an AgentPG-hosted instance (where the login role does not hold the AIDB grants today, which this mode reports as unavailable and serves from the app path). Agent memory (an AIDB preview feature) is not used. The agent framework and the governance tables are what AIDB 7.9 ships; treat them as moving.
Before and after
|
| |
Embedding the policy text |
| A Live AIDB pipeline ( |
Retrieval | The app's SQL: vector, full text and trigram, fused with RRF ( |
|
AI pre-validation | The app builds the prompt and calls the gateway or a provider account ( | An AIDB agent |
What the AI may read | Application checks ("the model gets no tools") | A purpose: the agent's tools run as the NOLOGIN Postgres role |
Audit of what it tried | Request history and the validation row | Plus AIDB's decision records ( |
Tamper evidence | Row MACs on accounts, settings, sessions, tokens | The same, extended to the AIDB definitions (pipeline, model, tools, purpose and its grants, agent): sealed with the vault, verified at start-up, every minute, and right before each agent call; a mismatch turns AIDB mode off and counts in |
The app code is not deleted: it is the fallback and the other half of every comparison, so both paths stay testable. In aidb mode the app still embeds at ingest (the kb_chunk.embedding column is required by the app path and by the fallback), so the saving is at run time and in what you have to operate once you stop using the app path: the vector index, the re-embed job, the provider account for embeddings, and the retrieval SQL. Measured on this repository: the AIDB layer is aidbmode.py (about 550 lines with its operator setup), aidbschema.py (70) and small hooks, against search.py, embeddings.py, embedcfg.py, reembed_job.py and the re-embed and reconcile code of migrate.py (about 450 lines) that the aidb path no longer needs.
Turn it on
The database needs the AIDB extension and a role for the app that is a member of
aidb_users(the setup command grants it). Creating roles, purposes and models needs more than the app's own role may have, so it is an operator command with a privileged connection that is asked for on a prompt or read fromTRUSTDESK_AIDB_ADMIN_DSN(or_FILE), used for that command only, never stored and never printed:export LLM_BASE_URL=https://gateway.example/v1 LLM_CHAT_MODEL=chat-default LLM_API_KEY=... # the gateway AIDB's agent model calls (https) trustdesk aidb-setup # asks for the admin password (opens the vault) and the privileged connection stringIt is idempotent (a repeat prints
unchangedfor everything). WithoutLLM_BASE_URLandLLM_CHAT_MODELit creates the pipeline, tools and purpose and says the agent was skipped.--runtime-rolenames the role the app runs as (default: the user ofDATABASE_URL);--resealaccepts AIDB objects that no longer match their seal, after you inspected them;--skip-model-checkregisters the model without AIDB calling the endpoint first.Set
TRUSTDESK_RETRIEVAL_BACKEND=aidband restart.trustdesk aidb-statusprints what is in use and why not, pipeline lag, the seals and the recent decisions;/readyzsays{"aidb": {"requested": "aidb", "retrieval": true, "agent": true}}(a one-wordproblemwhen not, never a failure: it staysready).
When it falls back to the app path (all of these were tested)
AIDB not installed or not granted to the app's role; the pipeline or agent not set up; the vault locked (the seals cannot be checked, so AIDB mode waits for it); any seal that does not match; an AIDB search that raises (that call only, then the next call looks again); an agent that errors, times out, is refused or answers something that is not a verdict (the gateway path answers, or the request is skipped as before when no account exists). Each is logged as AIDB MODE FALLBACK or the kit's RETRIEVAL BACKEND FALLBACK with the reason.
Honest limits
Not the same search. AIDB mode embeds with its own local model (384 dimensions) instead of the configured embedding account, has no trigram typo tolerance (
pg_trgmstays in theapppath) and uses the plainenglishtext-search configuration (no accent folding). Scores are comparable within one backend only. For obvious queries both paths return the same top passage (checked on the demo pack, with its offline demo embedder); for others they differ.departmentandas_ofare AIDB-mode filters. A document gets them from itsmeta(upload with?department=finance&effective_date=2027-01-01; without them a chunk is foralldepartments and effective from its upload day). The kit's filters are equality filters, soas_ofdrops passages not yet in force after retrieval. Theapppath ignores both. Nothing in the app sets them by default.Lag. A Live pipeline embeds after the row commits; a request validated in the first seconds after an upload may not see the new passage.
trustdesk reembedrewrites every chunk row; how the Live pipeline reacts to that was not measured.Detection, not prevention. Seals notice a database write that changes an AIDB definition or the purpose role's grants; a superuser can still change them, an older authentic definition can be replayed from a backup, and the check runs at start-up, every minute and before each agent call. Not covered: the contents of
kb_chunk,td_governance_decisions_v, who holdsUSAGEon the gateway credential, AIDB's own code and server settings. Any member ofaidb_userscan create AIDB agents; one namedtd_*that nobody sealed is reported, one with another name is not ours to judge. AIDB adds a built-insleeptool to every agent.The tools are as broad as their views.
td_employee_balancetakes a request id and returns aggregates (days, amounts, department, level, country) for it; the agent could ask for any request's. No personal data is in the view.Backlog. The background catch-up pass runs only while a provider account is usable; with only the agent configured, new requests are validated at once but a request that failed is not retried by the pass.
Needs https for a real gateway. AIDB refuses plain
httpfor model calls except to private addresses with the server optionaidb.allow_insecure_egress=on, which is test-only.
Monitoring TrustDesk (metrics)
TrustDesk exposes Prometheus metrics at /metrics with the same names PagerLens understands (http_requests_total, http_request_duration_seconds, db_up, app_info, trustdesk_documents, process_*) plus trustdesk_requests{status}, trustdesk_oldest_open_request_seconds, trustdesk_validations_total{state,recommendation} and trustdesk_validation_duration_seconds. Create a Metrics only token under Settings > API tokens: it can scrape /metrics and nothing else (it is refused on /mcp and on the REST API). Then point pagerlens-agent at it to get health probes, alerts and incident analysis; see that project's README.
Configuration
Variable | Default | Meaning |
|
| Postgres URI; first one set wins. If none is set, libpq uses |
|
| Bind address ( |
|
| Proxy addresses (comma-separated, CIDRs allowed) whose |
|
| How many reverse proxies sit in front of the app; the client address used for rate limiting is that entry of |
|
| With |
| none | Optional platform-managed secret that lets the vault unlock itself after a restart (file alternative: |
|
| Allow provider URLs on loopback/private networks, for a local model server. Link-local is always blocked. |
| none | Optional static full-access credential for automation (file alternative: |
|
|
|
| Optional environment provider; see Environment configuration. | |
|
|
|
| none | Read by |
Container image
.github/workflows/build-image.yml lints, tests, then publishes ghcr.io/<owner>/<repo> (latest, sha-*, version tags) from main, with a nightly rebuild for base-image updates.
The image runs trustdesk start: connect to the database (retrying while a scaled-to-zero database wakes), apply migrations, optionally seed, then serve. It runs as a non-root user and sets SSL_CERT_FILE so sslmode=verify-full works with the bundled libpq.
Deploy on AgentPG
Free-tier sandboxes auto-pause when idle and cannot be kept awake. agentpg sandbox status shows Auto-pause: true, and the AgentPG docs state it can't be disabled on the Free tier (plan changes were also unavailable during the private preview). A paused sandbox answers 503 and does not wake on a request. Before you rely on a deployment, read Troubleshooting.
AgentPG sandboxes run any public image and can be linked to a Postgres instance, which injects the connection details (PSQL_CONNECTION_STRING and the PG* variables). TrustDesk reads those automatically. No key files are needed.
agentpg login
agentpg create --name <instance-name> --size small # or reuse an instance; note its id
agentpg extensions create vector <instance-id> # may need elevated rights, so enable them here
agentpg extensions create pg_trgm <instance-id>
agentpg sandbox create trustdesk \
--image ghcr.io/<owner>/trustdesk:latest \
--instance <instance-id> --port 8000
agentpg sandbox status <sandbox-id> # prints the public HTTPS URL
agentpg sandbox logs <sandbox-id> # contains the one-time setup codeThen open the URL, enter the setup code from the logs, choose an admin password, and click Load demo policies on the Documents tab. Add provider accounts under Settings.
Notes:
--port 8000is required: without it the platform replaces the image's command with an idle process.The image must be public on GHCR (package settings > change visibility).
Setup is protected by the code in the logs, which only someone who can read the sandbox logs has. Complete setup promptly after creating the sandbox: until then, anyone who finds the URL can try to guess the code (attempts are rate limited).
agentpg sandbox createaccepts--env KEY=VALUE(repeatable), e.g.--env AUTO_SEED=trueto load the demo policies on first start. Keep secrets out of it: everything sensitive is configured in the browser, and values passed with--envare not a secret store.Free-tier sandboxes auto-pause when idle and do not wake on a request: a paused sandbox returns 503 until you run
agentpg sandbox resume. After a resume, unlock the provider keys with your admin password. See Troubleshooting.Optional: the documented way to give a sandbox a file is
--file. If you want unattended unlock after restarts, mount a platform-managed secret at/etc/trustdesk/encryption_key, created outside this repository, and never commit it. The trade-offs are described in How secrets are handled.
Troubleshooting
I get a 503 on every page
Cause: the sandbox is paused, not broken. Auto-pause is on by default and can't be turned off on the Free tier. While paused, the platform's proxy returns an empty 503 for every path, including /healthz and the static files, before any request reaches TrustDesk. Nothing expires on the app side.
How to confirm:
agentpg sandbox status <sandbox-id> # look for "Status: paused"
curl -i https://<your-host>/healthz # a 503 with no security headers (no content-security-policy) came from the platform, not the appFix: agentpg sandbox resume <sandbox-id>, reload the page and sign in. The web app explains a 502/503/504 itself and offers a Retry button.
The page says "Provider keys are locked"
The server restarted (a resume is a restart), and the stored provider keys can only be unwrapped with your admin password, which is not stored anywhere. Enter the password in the banner. Until then, external embeddings and Ask are paused; searching with the demo embedder still works. To avoid this on unattended restarts, see the platform secret under How secrets are handled.
I forgot the admin password
Run trustdesk reset-admin where the server can reach the database (on AgentPG: agentpg sandbox console <sandbox-id>). This clears the password, the vault and every stored provider key, then restarts the first-run setup (a new setup code is printed to the log on the next start). Re-add your provider accounts' keys afterwards.
What survives a pause
Your documents, search index, accounts (without their keys being usable until unlocked), API tokens and sessions live in the Postgres instance and are unaffected. Only the app process restarts. Pausing is not data loss.
Ways to avoid surprises
Resume the sandbox shortly before a demo or a test session and check
/readyzfirst.Ask your platform provider whether the idle timeout can be lengthened or auto-pause turned off for your account.
If you need an always-on service, run the container on infrastructure without idle auto-pause, such as a small VM or a platform tier that doesn't pause.
/readyz returns 503 with waking-database
This one comes from TrustDesk itself and is different from the platform 503 above: the app is running but cannot reach the database yet, for example while a scaled-to-zero database wakes. Wait a few seconds and retry; the app retries its connection with backoff.
Search says "re-embedding is in progress"
You switched embedding providers. Search and uploads pause while every passage is re-embedded (progress is shown in Settings). If the provider fails midway, nothing is changed.
Tests
make test # unit tests, no database needed
make test-db # integration tests against a throwaway database on the compose Postgres
# (needs POSTGRES_PASSWORD exported: set -a; . ./.env; set +a)
make test-aidb # the whole suite against a throwaway Docker Postgres with AIDB (image aidb-postgres:17), including the `aidb` testsThe tests marked aidb (tests/test_aidb_integration.py) skip unless AIDB_TEST_URL (a superuser DSN of a Postgres with AIDB) is set, which is why CI runs everything except them: there is no AIDB image or EDB token in CI. The switch, every fallback, the plain-Postgres objects and the CLI on a database without AIDB are in tests/test_aidb_switch.py and run everywhere.
Security
Read SECURITY.md before exposing this to a network. In short: no secret files, a salted-hash admin password, hashed revocable tokens, encrypted provider keys that stay locked after a restart, rate-limited sign-in, strict CSP, SSRF-guarded provider URLs, bounded uploads, non-root container. The knowledge base is single-tenant: anyone with full access can read and change every document.
Design notes
Just Postgres: SQL lives in
src/trustdesk/migrations/andsrc/trustdesk/search.py.Cold-start tolerant:
db.connectretries with backoff and/readyzreportswaking-databaserather than failing hard.Read-only MCP tools: the MCP server only searches and lists; retrieved text is data, never instructions.
Limits: 10 MB uploads, 500-page PDFs, text extraction only (no OCR).
License
Apache License 2.0. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only MCP server for the OrchestKit docs: full-text search + Markdown fetch. No auth.
Read-only MCP server over the developer docs for the portfolio's JSON API, MCP, A2A and NLWeb.
MCP server for querying Forkast documentation
Political Comms documentation MCP server: search docs, query the docs filesystem. No auth.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceA Streamable HTTP MCP server that packages QMD search and document retrieval with bounded index-maintenance operations, without exposing arbitrary shell execution.MIT
- FlicenseNot gradedqualityCmaintenanceServes a governed knowledge system of record over MCP, letting agents answer questions with citations drawn only from approved documents and explicitly decline when the record does not cover the question.73 npm-
- AlicenseNot gradedqualityCmaintenanceExposes a markdown knowledge base to MCP clients so they can search pages, fetch individual documents, and ask grounded questions that come back with numbered citations or a refusal. Retrieval is graded before answering, with a single query rewrite before giving up when nothing relevant is found.1MIT
- AlicenseNot gradedqualityCmaintenanceExposes clinical question answering, guideline search, and minimum-necessary FHIR patient context as tools to any MCP client.MIT