Toolport is a local-first MCP gateway and manager. This document describes how it handles secrets, network boundaries, and trust across each way it runs, what it records locally, and how to report a vulnerability.
Please report security issues privately, not as a public issue:
- Preferred: GitHub's private vulnerability reporting (repo Security tab to Report a vulnerability).
- Or email the maintainer at tyler@southforgeai.com.
Please include reproduction steps and the affected version/commit. We aim to acknowledge within a few days. Coordinated disclosure is appreciated; we will credit reporters who want it.
Toolport ships fixes on the latest release only. Always run the newest version from the Releases page.
Toolport's core is a local gateway that AI clients reach; the desktop app is a manager around it. What network surface exists depends on the mode you run:
- Desktop app + stdio gateway (the default). Each AI client spawns a small
gateway adapter as a local child process and talks to it over stdio (standard
input/output). The adapters share one host daemon, which listens on a random
127.0.0.1port and requires a per-install token. Nothing listens off-host. The only other socket is a transient127.0.0.1loopback listener during an OAuth callback, which closes as soon as that flow completes. - Local HTTP / OpenAPI bridge (
TOOLPORT_HTTP, legacyCONDUIT_HTTP). For clients that speak HTTP or OpenAPI instead of stdio, the gateway can bind a listener. It defaults to127.0.0.1:8765(loopback only), configurable withTOOLPORT_HTTP_HOSTand the port. Every bind requires HTTP authentication: either a bearer token (TOOLPORT_HTTP_TOKEN; legacyCONDUIT_HTTP_TOKENstill accepted) or at least one registered HTTP client. With neither, the process prints a line to stderr and exits 1, including a hand-launched loopback listener. A registered HTTP client is a scoped bearer you create in the desktop app under Settings, "Open WebUI / HTTP endpoint", "Scoped clients": each entry gets its own token and its own set of servers, and the app stores only the token's SHA-256 in thehttp_clientslist inregistry.jsonin Toolport's local data directory (the plaintext token is shown once, at creation, and is not stored). One or more of those entries counts as authentication configured, exactly like an env token. When the desktop app starts this bridge it auto-generates a token. A request without a valid token gets401, and any cross-site browser request is rejected with403regardless of token. For isolated local development only,--insecure-loopbackpermits an unauthenticated loopback listener, but only when no token is set andhttp_clientsis empty. Registered clients left over from earlier use keep the bind authenticated, so the listener still answers401to every request without a matching bearer; revoke those entries first if you want the open listener. An emptyhttp_clientslist only counts when the registry was actually read: a registry recovered from a backup, or one the gateway could not read at all, is empty for a reason that says nothing about what is configured, so the flag does not open a listener over it. The flag never permits an open non-loopback bind. - Headless / Docker. The published image runs the HTTP bridge and sets
TOOLPORT_HTTP_HOST=0.0.0.0so it is reachable off-host. Every bind, including loopback, requiresTOOLPORT_HTTP_TOKEN(legacyCONDUIT_HTTP_TOKEN) or a registered HTTP client: without one the gateway refuses to start.--insecure-loopbackcannot open a non-loopback address. Put it behind your own TLS/ingress; the gateway serves plain HTTP and expects the operator to terminate TLS. - Sharing and Teams (opt-in, hosted). These make explicit, user-initiated requests to hosted services and are covered under Telemetry and hosted services below.
API keys and OAuth tokens are never written to plaintext config files and never written to logs. Where they live depends on the backend, selected by environment:
- OS keychain (desktop default). With no secret-related env var set, secrets go to the platform keychain: the macOS data-protection keychain (under a team-scoped access group shared with the signed gateway), Windows Credential Manager, or the Linux Secret Service.
- Encrypted file backend (
TOOLPORT_SECRET_KEY, legacyCONDUIT_SECRET_KEY). Setting this env var switches storage to an encryptedsecrets.encfile (XChaCha20-Poly1305) in Toolport's data directory, keyed from the passphrase. This is the backend for headless and containerized deployments where no OS keychain is available. The passphrase is the whole key: choose a high-entropy value and keep it out of shell history and image layers. Ifsecrets.encleaks together with a weak passphrase, the secrets are recoverable, so treat the file as sensitive. - Environment injection (headless). A secret can also be supplied directly as
TOOLPORT_SECRET_<KEY>(legacyCONDUIT_SECRET_<KEY>), or as a bare<KEY>only whenTOOLPORT_ALLOW_BARE_SECRET_ENV(legacyCONDUIT_ALLOW_BARE_SECRET_ENV) is set. These are read from the process environment in cleartext, so they are only as protected as the environment that holds them.
Downstream OAuth is done conservatively: dynamic client registration, PKCE
(S256), a CSRF state check, a loopback (127.0.0.1) redirect, RFC 8707 resource
binding, and offline_access refresh tokens. Discovered authorization, token, and
registration endpoints must be https:// (loopback http is allowed only for
local development).
Toolport is detection-first by default, but it is not detection-only: several controls actively gate or block a call before it reaches an upstream server.
- Destructive-tool policy. With the destructive-tool deny policy on, tools
judged destructive (by their
destructiveHint, or a write-verb name heuristic) are hidden from the catalog and blocked before a client can call them. - Quarantine on drift. When an already-approved tool changes in a high-risk way (poisoned description, or a destructive/annotation downgrade), it is quarantined and blocked until you re-approve it.
- Human-in-the-loop approval. When enabled, destructive tools and tools from untrusted-provenance servers (shared/registry sources) require an explicit human approval. This gate is fail-closed: a denied, timed-out, or unreachable decision blocks the call, which returns an error and never routes.
- Content screening. Tool results and resource reads are scanned for
injection patterns. Findings are recorded in Activity, and results are returned
exactly as the server sent them, with provenance only in hidden
_meta. In Strict mode a high-confidence finding blocks the result and returns an error.
Which of these are active depends on your settings and, for Teams, your org
policy. Debug logging is off by default, gated behind TOOLPORT_DEBUG (legacy CONDUIT_DEBUG), and never
records tokens or full authorization URLs.
In local use, Toolport sends no telemetry. Tool traffic is only between the gateway and the upstream MCP servers you configure. Data leaves your machine only through features you explicitly turn on:
- Teams usage reporting (opt-in, only when joined to a team). When connected
to a Toolport Teams organization, a periodic sync reports aggregate usage
for team-provided servers to the team server URL you joined: per-server row of
call count, estimated schema token equivalent (legacy wire field
tokens_saved), and a list-price equivalent estimate. It never sends tool names, arguments, results, or anything about your personal (non-team) servers. - Shared setup links. Creating a share link POSTs a secret-stripped server
setup to
https://toolport.app/api/share, and only when you take that action. Importing a shared link fetches it from the same endpoint. - Team server URL. You provide the Teams server address when you join; there
is no hardcoded default, and non-loopback team URLs must be
https://.
Everything below stays in Toolport's local data directory (%APPDATA%\Toolport on
Windows, the platform config dir elsewhere; override with TOOLPORT_DATA_DIR, legacy
CONDUIT_DATA_DIR) and
never leaves your device on its own. Each log is capped and trims oldest-first:
| Local record | File | Retained | Contains |
|---|---|---|---|
| Audit log | audit.jsonl |
last ~5,000 entries (or 4 MB) | tool calls and approval/policy decisions |
| Discovery search trace | search-trace.jsonl, search-trace-v2.jsonl |
legacy and recent v2 traces | lazy-discovery searches the agent ran |
| Live inspector | inspect.jsonl |
last ~50 (opt-in, off by default) | captured call arguments and results |
| Catalog telemetry | savings.jsonl, savings-v2.jsonl |
legacy estimates and bounded v2 detail with lifetime and daily carry records | local MCP byte measurements and estimated token equivalents |
Registry config, secrets, tool catalogs, and these logs never leave the device except through the opt-in hosted features above.
Clear controls: the Activity view has a confirmed Clear retained activity action that deletes all four logs; the live inspector also clears itself when you turn live inspection off. Both are local, irreversible deletes.
Toolport proxies to whatever MCP servers you add. It does not vet the behavior of third-party servers: an upstream server you configure runs as a local process (for stdio servers) and/or receives the credentials you give it. Only add servers you trust. Lazy discovery reduces how much surface is exposed to the agent, and the governance controls above can gate a call, but a tool you approve still executes upstream.
When you run Toolport outside the desktop app, some safety defaults become your
responsibility: configure HTTP authentication for every bind, including loopback,
by setting TOOLPORT_HTTP_TOKEN (legacy CONDUIT_HTTP_TOKEN) or by registering an
HTTP client in the desktop app (both are described under
How Toolport runs above), then
choose a high-entropy TOOLPORT_SECRET_KEY (legacy CONDUIT_SECRET_KEY) for the
encrypted file backend, and put the HTTP bridge behind your own TLS.
--insecure-loopback waives the requirement for isolated local development only,
and only while neither a token nor a registered client exists.
See Known issues in Troubleshooting for current
advisories, including the Linux-only glib RUSTSEC-2024-0429 soundness issue
inherited transitively from Tauri's Linux webview stack.