Skip to main content
Glama
elchinoo

TrustDesk MCP Server

by elchinoo

A security-questionnaire knowledge base built on Postgres + pgvector, with a web app, a REST API and an MCP server. Load your security policies, then search them, or ask a question and get an answer that cites its sources. Every result carries its document, version and heading, so answers can show their evidence.

WARNING

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

  • Web app at /: 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: QMD MCP

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:8000

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-XXXX

Open 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 HttpOnly, SameSite=Strict cookie (Secure over HTTPS)

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. read tokens can search and list but cannot change anything or spend provider credit.

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.

  • Forgot the admin password? Run trustdesk reset-admin on 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 http to non-local hosts; redirects are never followed. To allow a local model server such as Ollama, the operator sets TRUSTDESK_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 /embeddings endpoint. Switching models re-embeds your passages in the background, with progress, and changes nothing if the provider call fails.

Answering: write the reply from retrieved passages

Ask: pick an account for each question

Needs an OpenAI-compatible /chat/completions endpoint. The answer cites the numbered passages; an answer that cites none is flagged.

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

https://api.openai.com/v1

Embedding model text-embedding-3-small supports a custom size, so 768 works.

OpenRouter

https://openrouter.ai/api/v1

One key for many models; has an OpenAI-style embeddings endpoint (for example openai/text-embedding-3-small).

Zhipu GLM

https://open.bigmodel.cn/api/paas/v4

Documented as OpenAI-compatible; embedding model embedding-3. Note /v4, not /v1.

Moonshot Kimi

https://api.moonshot.ai/v1

Documented for chat only; use another account for embeddings.

Anthropic Claude

https://api.anthropic.com/v1

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 http://localhost:11434/v1

Needs TRUSTDESK_ALLOW_PRIVATE_LLM_URLS=true on the server.

NOTE

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

LLM_BASE_URL

Provider base URL. Empty means the demo embedder.

LLM_API_KEY

Provider key. Prefer entering keys in the browser.

EMBEDDING_MODEL, EMBEDDING_DIM

Embedding model and vector size (default 768).

LLM_CHAT_MODEL

Chat model, to use this account in Ask.

EMBEDDING_SEND_DIMENSIONS

false if the provider rejects the dimensions field.

From a terminal, trustdesk reembed re-embeds everything after you change these.

Using the web app

  • Search: ask in plain language. Each result shows its document, heading, version and how it ranked in the vector and full-text searches. "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). 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.

  • 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
done

Using 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

search_knowledge(query, limit?, doc_type?)

Hybrid search; returns passages with document, version and heading to cite

get_policy_passage(chunk_id)

Fetch one passage by id

list_documents()

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 (url vs serverUrl vs httpUrl, 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.

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_*). 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

DATABASE_URL / DB_EXTERNAL_DSN / PSQL_CONNECTION_STRING

PG* variables

Postgres URI; first one set wins. If none is set, libpq uses PGHOST, PGUSER, PGPASSWORD, PGDATABASE. The vector and pg_trgm extensions must exist (or the role must be allowed to create them).

HOST / PORT

0.0.0.0 / 8000

Bind address.

AUTO_SEED

false

With trustdesk start, load the demo pack if the knowledge base is empty.

TRUSTDESK_ENCRYPTION_KEY

none

Optional platform-managed secret that lets the vault unlock itself after a restart (file alternative: /etc/trustdesk/encryption_key).

TRUSTDESK_ALLOW_PRIVATE_LLM_URLS

false

Allow provider URLs on loopback/private networks, for a local model server. Link-local is always blocked.

TRUSTDESK_API_KEY

none

Optional static full-access credential for automation (file alternative: /etc/trustdesk/api_key). Prefer API tokens.

TRUSTDESK_ALLOW_ANONYMOUS

false

true disables authentication entirely. Throwaway local demos only.

LLM_BASE_URL, LLM_API_KEY, EMBEDDING_MODEL, EMBEDDING_DIM, LLM_CHAT_MODEL, EMBEDDING_SEND_DIMENSIONS

Optional environment provider; see Environment configuration.

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

IMPORTANT

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 code

Then 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 8000 is 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 create accepts --env KEY=VALUE (repeatable), e.g. --env AUTO_SEED=true to load the demo policies on first start. Keep secrets out of it: everything sensitive is configured in the browser, and values passed with --env are 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 app

Fix: 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 /readyz first.

  • 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)

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/ and src/trustdesk/search.py.

  • Cold-start tolerant: db.connect retries with backoff and /readyz reports waking-database rather 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides semantic search over markdown documentation using RAG, allowing natural language queries and integration with MCP clients.
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A Streamable HTTP MCP server that packages QMD search and document retrieval with bounded index-maintenance operations, without exposing arbitrary shell execution.
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables chat clients to search, add, retrieve, list, and delete documents in a ragifix knowledge base via MCP, plus health checks.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Serves 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.
    1,304 npm
    -