Self-host patent & trademark server for AI agents - CLI + MCP + REST, official IP offices
4.2K
Agentic-AI-first toolkit for patent and trademark research: a patent CLI and a
server that give AI agents and applications one uniform interface to the
official IP offices, using your own API keys (BYOK, stored encrypted).
One image, two roles:
patent server): an MCP
endpoint for AI agents, a REST API for apps and the CLI, and a web dashboard to
manage your account and provider credentials.Data sources - official patent and trademark offices:
Homepage: https://patent.dev
Use the hosted service - sign in at https://patent.dev/patent-connector/, then
add the MCP endpoint https://patent.dev/mcp to your AI client, or run patent login for the CLI.
Run it fully on-prem (the server and in-process tools) - get in touch at [email protected]. Write the free license into a persistent volume so the server picks it up automatically:
# 1) register (an activation code is sent to your email):
docker run --rm patentdev/patent-connector patent register --email [email protected]
# 2) activate, storing the license in a persistent volume:
docker run --rm -v patent-data:/app/data \
patentdev/patent-connector patent activate <code>
The license lands at /app/data/patent.license (the image's default
PATENT_LICENSE_FILE); mount the same patent-data volume into the server below.
Or license from the browser. Start the server without a license and it does
not crash - it serves a setup page at your BASE_URL with live deployment
diagnostics (reachability, certificate, DNS, SMTP). Paste a lic_... key there,
confirming with the one-time setup code printed in docker logs, or set
PATENT_LICENSE_KEY in the environment. The server restarts into normal mode as
soon as a valid license is present - including when it is minted or renewed on
our side, with nothing to do on the box. An unlicensed instance only contacts
license.patent.dev to check its own license; nothing else is sent. Because it
serves this page rather than exiting, keep a restart policy on the container
(restart: unless-stopped, already in the examples below).
The dataset is tiny (accounts and settings), so the bundled SQLite backend is plenty - persist one volume and you are done.
services:
patent-connector:
image: patentdev/patent-connector:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
BASE_URL: http://localhost:8080 # externally reachable URL
ENCRYPTION_KEY: ${ENCRYPTION_KEY} # 32 hex chars: openssl rand -hex 16
# Sign-in is passwordless: a one-time code is emailed. Configure SMTP so
# users can self-serve login (no relay? mint a code with `issue-code`, below):
SMTP_HOST: smtp.example.com # your mail server / relay
SMTP_FROM: [email protected] # from address for sign-in codes
# SMTP_USER/SMTP_PASS: set both if the relay needs a login; omit both for
# an internal relay that authorizes by IP.
# DB (SQLite) and license default into /app/data, the mounted volume below.
# For MySQL, override DATABASE_URL with a DSN:
# DATABASE_URL: user:pass@tcp(host:3306)/patent_mcp?parseTime=true
volumes:
- patent-data:/app/data
volumes:
patent-data:
export ENCRYPTION_KEY=$(openssl rand -hex 16)
docker compose up -d
Open http://localhost:8080 and sign in with your email - login is passwordless:
you enter your email, receive a one-time 6-digit code by email (delivered via the
SMTP relay above), and enter it. Then add your provider API keys in the dashboard.
Set BASE_URL to the exact URL you open in the browser (on a LAN that is something
like http://192.168.1.55:8080; behind a proxy, your https:// URL) - the login
cookie follows BASE_URL, so over plain HTTP this keeps the dashboard login working.
No mail relay? Sign-in codes are normally emailed (configure SMTP above). Without SMTP, mint a code on the host and type it on the sign-in page - handy for the first admin login or whenever email is unavailable:
docker exec <container> patent server issue-code --email [email protected]
AI assistants (Claude, ChatGPT) only connect to HTTPS endpoints on the standard port, and the server can terminate TLS itself - no Caddy/nginx/Traefik needed:
services:
patent-connector:
image: patentdev/patent-connector:latest
restart: unless-stopped
ports:
- "443:443"
- "80:80" # Let's Encrypt HTTP-01 challenge + redirect to HTTPS
environment:
BASE_URL: https://patents.acme.com # DNS must point at this host
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
TLS_ACME: "true" # automatic Let's Encrypt certificate
TLS_ACME_EMAIL: [email protected] # optional: CA expiry notices
SMTP_HOST: smtp.acme.com
SMTP_FROM: [email protected]
# Optional: dashboard (and login) only from your offices/VPN; connected
# assistants keep working from anywhere (they authenticate with tokens):
# WEBUI_ALLOWED_CIDRS: 203.0.113.0/24,2001:db8:acme::/48
volumes:
- patent-data:/app/data
volumes:
patent-data:
With TLS_ACME=true the listen port defaults to 443 and certificates are
obtained and renewed automatically (cached in /app/data, so keep the volume).
Issuance retries in the background with backoff - but do not delete the volume
and restart repeatedly against the production CA, Let's Encrypt rate-limits
failed attempts per hour. Prefer your own certificates? Set
TLS_CERT_FILE/TLS_KEY_FILE instead; renewed files are picked up without a
restart. A server without public ingress (LAN/VPN) can still get Let's Encrypt
certificates via the DNS-01 challenge (TLS_ACME_DNS_PROVIDER +
TLS_ACME_DNS_API_TOKEN).
A classic reverse proxy in front remains fully supported: terminate TLS there,
set BASE_URL to the public HTTPS URL, and bind the port locally
(ports: ["127.0.0.1:8080:8080"]) so only the proxy reaches it. If you combine
a proxy with WEBUI_ALLOWED_CIDRS, also set TRUSTED_PROXIES to the proxy's
address - otherwise every request appears to come from the proxy and the
allowlist cannot see real client addresses.
| Variable | Required | Description |
|---|---|---|
BASE_URL | yes | Externally reachable URL (e.g. https://patents.acme.com) |
ENCRYPTION_KEY | yes | 32 hex chars; encrypts stored provider credentials (BYOK) |
DATABASE_URL | no | Defaults to sqlite:/app/data/patent.db; set a MySQL DSN to override |
PATENT_LICENSE_FILE | no | Defaults to /app/data/patent.license (put your activated license there) |
PORT | no | Listen port (default 8080; 443 when TLS is enabled) |
TLS_CERT_FILE, TLS_KEY_FILE | no | Serve HTTPS directly with your own PEM pair (mount both into the container). Renewed files are picked up without a restart. Mutually exclusive with TLS_ACME |
TLS_ACME | no | true = obtain and renew a Let's Encrypt certificate for the BASE_URL host automatically. The domain must resolve to this server with ports 80+443 published, or use the DNS-01 variables below |
TLS_ACME_EMAIL | no | Optional ACME account email (certificate expiry notices from the CA) |
TLS_ACME_DNS_PROVIDER, TLS_ACME_DNS_API_TOKEN | no | DNS-01 challenge for servers without public ingress (LAN/VPN): cloudflare or hetzner plus an API token. Scope the token as narrowly as possible: Cloudflare tokens can be restricted to one zone; Hetzner DNS tokens are account-wide - treat that token like a root credential for all your zones |
HTTP_PORT | no | Plain-HTTP listener while TLS is on: answers the Let's Encrypt HTTP-01 challenge and 301-redirects everything else to HTTPS (default 80; 0 disables it, e.g. with DNS-01 on a LAN) |
WEBUI_ALLOWED_CIDRS | no | Restrict the browser dashboard, login and admin console to these IP ranges (comma-separated CIDRs or single addresses, IPv4+IPv6). MCP/OAuth/REST endpoints for connected assistants stay reachable from anywhere (they carry their own token auth), as do /health, /api/version and temporary media links. Connecting a NEW assistant includes a browser sign-in step, so that must happen from an allowed range; already-connected assistants are unaffected. If the host has an AAAA record, include your IPv6 prefixes too |
TRUSTED_PROXIES | no | CIDRs of reverse proxies in front of this server. Required for WEBUI_ALLOWED_CIDRS behind a proxy: only when the direct peer is a trusted proxy is X-Forwarded-For used to determine the real client address |
WEBUI_SUBTITLE | no | Self-host only: text shown under the "Patent Connector" header in the dashboard, e.g. your firm's name, so users see at a glance that this is the internal instance. Served via the open /api/version endpoint, so do not put anything confidential here |
PROVIDER_ENV_SELECTOR | no | true reveals a per-provider Production/Sandbox dropdown in the dashboard (for offices with a test system, e.g. EUIPO). Hidden by default |
SMTP_HOST, SMTP_FROM | for login | Deliver passwordless sign-in codes (and feedback email). Required together to enable self-service email login (otherwise mint codes with patent server issue-code) |
SMTP_USER, SMTP_PASS | no | SMTP auth. Optional pair (set both or neither); omit both for an internal relay that authorizes by IP |
SMTP_PORT | no | SMTP port (default 587; 465 = implicit TLS, otherwise STARTTLS per SMTP_STARTTLS) |
SMTP_STARTTLS | no | STARTTLS policy on non-465 ports: auto (default - mandatory on submission ports, opportunistic on port 25), off (never; a deliberately plaintext relay), opportunistic (upgrade if offered, otherwise plaintext), required (must upgrade or fail). Use off/opportunistic for an internal relay that advertises a broken STARTTLS (454 TLS not available) |
SMTP_TLS_CA_FILE | no | PEM file with an internal CA root, trusted for the SMTP connection only (mount it into the container). For relays whose certificate the system store cannot verify |
SMTP_TLS_INSECURE_SKIP_VERIFY | no | true disables SMTP TLS certificate verification. Last resort - prefer SMTP_TLS_CA_FILE or fixing the relay certificate |
ALLOWED_EMAIL_DOMAINS | no | Restrict sign-in to these email domains (comma-separated). A user must control a mailbox at an approved domain to receive the code - handy access control alongside a reverse-proxy/IP restriction. Exact domain match; unset = any domain |
SSO_TENANT_ID | no | Entra ID single sign-on (licensed feature): your tenant GUID. Employees sign in with their Microsoft work account; accounts are created on first login. Mutually exclusive with SSO_ISSUER. Register the redirect URI <BASE_URL>/auth/sso/callback (verbatim, including any path prefix) as a single-tenant web app |
SSO_ISSUER | no | Generic OIDC issuer URL instead of the Entra preset (Keycloak, ADFS, Authentik, sovereign Entra clouds). Must be https |
SSO_CLIENT_ID | with SSO | App registration / OIDC client id |
SSO_CLIENT_SECRET | with SSO | Client secret (SSO_CLIENT_SECRET_FILE reads it from a mounted file, e.g. a docker secret). Entra caps secrets at 24 months - calendar the rotation |
SSO_SCOPES | no | Extra scopes requested at login (space/comma separated), for connectors that need delegated tokens |
SSO_ADMIN_GROUP | no | App role values or group object ids (comma-separated, any match) synced to the admin flag on each SSO login - grant AND revoke. Manual make-admin/console promotions stay sticky; console demotion of a still-in-group user is re-granted at next login (remove them from the group instead) |
SSO_ALLOWED_GROUP | no | Only users carrying one of these app roles / group ids may sign in via SSO. NOTE: gates only the SSO path - combine with SSO_DISABLE_OTP_LOGIN=true to make it an instance gate, otherwise email-code login remains a way in |
SSO_ACCESS_REQUEST_EMAIL | no | Contact shown to a user the SSO_ALLOWED_GROUP gate turns away, so they know whom to ask for access ("To request access, contact ..."). Defaults to FEEDBACK_EMAIL, then SMTP_FROM; unset everywhere = no contact line |
SSO_LINK_EXISTING | no | How an SSO login whose email matches an existing email-code account links up: otp (default; confirmation code to that mailbox, needs SMTP), auto (link directly - never for admin accounts; only sensible if you trust every tenant guest), off (reject) |
SSO_DISABLE_OTP_LOGIN | no | true disables self-service email-code login server-side while SSO is armed (the request-code endpoint answers 404). patent server issue-code + code entry keeps working as the operator break-glass |
PATENT_UPDATE_NOTIFY_TO | no | Email this address once per new release (needs SMTP), so you do not have to watch the dashboard's update chip or the server log. Unset = off |
PATENT_UPDATE_NOTIFY_INTERVAL | no | Check cadence for the update email (Go duration, e.g. 12h; default 24h) |
Provider API keys (EPO OPS, USPTO, EUIPO, ...) are not set via environment
variables - add them in the dashboard after first login; they are stored encrypted
with ENCRYPTION_KEY.
The first account to sign in becomes the instance admin automatically. To promote another existing account from the host:
docker exec <container> patent server make-admin --email [email protected]
Offboarding from the host (also works when the user cannot log in themselves):
docker exec <container> patent server delete-user --email [email protected]
Admins open the console at <BASE_URL>/#/admin: instance diagnostics, user
management (grant/revoke admin and delete accounts; the last admin and the
shared-credentials source are protected), and shared
credentials for firm-wide use - designate one account (a dedicated service
account like [email protected] works best) whose provider credentials every
user transacts under. Providers configured on that account are locked for other
users; rotate a key by updating it on the source account and it propagates
instantly. Providers the source lacks stay configurable per-user.
Persist /app/data - it holds the SQLite database, the license file and the
Let's Encrypt certificate cache. Keep your
ENCRYPTION_KEY stable and backed up; it decrypts stored provider credentials, so
losing it means re-entering all provider keys.
The container exposes a health endpoint and ships with curl and jq:
curl -s http://localhost:8080/health | jq
docker exec <container> curl -s http://localhost:8080/health | jq
Envisioned and crafted by Wolfgang Stark - patent.dev - Funktionslust GmbH
Content type
Image
Digest
sha256:584812782…
Size
44.8 MB
Last updated
about 6 hours ago
docker pull patentdev/patent-connector