Connect MCP clients securely
Enable the project-bound Nibleaf MCP endpoint, issue least-privilege keys, configure a client, rotate access, and operate the audit and rate-limit boundaries.
- 7 min read
- Updated Aug 31, 2026
Nibleaf exposes an authenticated HTTP MCP endpoint at /api/mcp. It is off by
default. Every key is bound to one project, carries explicit scopes, expires,
and can be revoked or atomically rotated by a project administrator. The server
derives the project and organization from the key; tools never accept either as
caller authority.
The initial surface is read-only, except for preview_theme_import, which
validates and previews a theme without writing project state. Publishing,
deletion, provider credentials, payment actions, database primitives, arbitrary
files, and code execution are not available.
Enable the endpoint
Set the API service environment:
MCP_ENABLED=true
MCP_ALLOWED_HOSTS=mcp.docs.example.com
MCP_RATE_LIMIT_PER_MIN=120
API_URL and APP_URL hostnames are always allowed. Add only additional public
hostnames to MCP_ALLOWED_HOSTS; do not add schemes, paths, user information,
commas inside one host, or wildcard values. Recreate the API service after a
change.
The endpoint validates Host, Origin, X-Forwarded-Host, and
X-Forwarded-Proto before authentication. Configure the public proxy to replace
untrusted forwarded headers and pass one unambiguous host and protocol. Keep MCP
disabled until the public TLS and proxy path return the expected hostname.
Create a project-bound key
Sign in through Better Auth, open the project's Settings → API keys tab,
choose only the required read scopes and an expiration, then create the key.
API-key lifecycle routes require a project ADMIN or OWNER role. The secret
is displayed once; store it in the client's secret store, not source control,
shell history, support messages, logs, screenshots, or an MCP configuration
committed to a repository.
Every MCP key needs mcp:connect. A scope controls only its named surface and
never widens the key's project binding. Capability discovery reports the tools
and resources registered for the exact key. Do not assume a documented future
capability exists if it is absent from tools/list or nibleaf://capabilities.
Legacy wildcard keys are intentionally unable to connect. A null expiration,
missing mcp:connect, or migrated legacy state appears as rotation required in
the dashboard. Rotate it into an explicit, expiring scope set rather than
editing its database row.
Configure a client
Use the public API origin and pass the key as a bearer token. A client that supports remote HTTP MCP servers commonly accepts a configuration shaped like:
{
"mcpServers": {
"nibleaf": {
"url": "https://api.docs.example.com/api/mcp",
"headers": {
"Authorization": "Bearer ${NIBLEAF_MCP_API_KEY}"
}
}
}
}
Client configuration names differ. Use its HTTP or remote-server option, not a
local command or filesystem adapter. Supply NIBLEAF_MCP_API_KEY through the
client's supported secret mechanism. First call tools/list, then
get_capabilities. A successful response includes both MCP protocol content and
a structured envelope with the request ID, bound project ID, and required
capability.
Current capability boundary
The capability resource is authoritative. On this release the stable surface includes project metadata; page metadata and authored page content; languages and versions; aggregate analytics without search terms; search configuration and content-free index diagnostics; exact aggregate usage and advisory entitlement summaries; add-on catalog, state, and audit reads; safe theme template/catalog metadata and non-writing import preview; redacted export and deployment summaries; pending publish-change metadata; and truth-based integration catalog, connection, and Git status reads.
The currently issuable domain scopes are search:read, usage:read,
entitlements:read, addons:read, themes:read, and integrations:read, in
addition to the project/page/language/version/analytics/export/deployment reads.
No new-domain write scope, usage:export, search reindex, integration
verification/deletion, credential, or payment scope is issuable through this
release. exports:read remains the separate redacted export-job read surface.
Project resources use bound URIs such as:
nibleaf://projects/{boundProjectId}
nibleaf://projects/{boundProjectId}/pages
nibleaf://projects/{boundProjectId}/pages/{pageId}
nibleaf://projects/{boundProjectId}/languages
nibleaf://projects/{boundProjectId}/versions
nibleaf://projects/{boundProjectId}/search/configuration
nibleaf://projects/{boundProjectId}/usage
nibleaf://projects/{boundProjectId}/entitlements
nibleaf://projects/{boundProjectId}/addons
nibleaf://projects/{boundProjectId}/theme-catalog
nibleaf://projects/{boundProjectId}/theme-template
nibleaf://projects/{boundProjectId}/integrations
nibleaf://projects/{boundProjectId}/deployments/latest
A different project ID or a page outside the bound project returns the same
not-found protocol result as a missing resource. Read projections omit provider
payloads, preview bearer URLs, storage keys and checksums, deployment snapshots,
theme filesystem state, raw errors, credentials, and analytics search terms.
Search diagnostics expose logical schema/revision/model metadata but not physical
collections, content, vectors, hashes, or raw provider payloads. Usage quantities
and limits remain exact decimal strings or null; partial and unavailable data
is never rewritten as zero, and configured blocking behavior is advisory only.
The pages:read scope does include authored content inside the one bound project;
grant it only when the client needs document bodies.
Audit and rate-limit operations
Every successful or failed tool/resource call must append a privacy-minimized audit event before success is returned. The event records project, API-key provenance, request ID, operation, capability, outcome, stable error code, duration, and timestamp. It never stores arguments, results, content, bearer tokens, provider payloads, IP addresses, or raw client keys. If audit persistence fails, the operation fails closed with a sanitized storage error.
MCP_RATE_LIMIT_PER_MIN is a bounded, per-key, per-API-process defense. It is
not deployment-global across replicas. Enforce a deployment-wide limit at the
trusted ingress or shared edge and monitor HTTP 429, Retry-After, and
X-RateLimit-* headers. To bound audit write amplification, the transport
records one transport_rate_limit rejection per key/window instead of one row
for every repeated 429. Never describe an individual process's counter as the
global customer allowance.
Rotate, revoke, and roll back
Rotation atomically revokes the old key and creates a new expiring key. Update
the client with the once-visible secret, verify tools/list, then remove the old
secret from its store. Revocation is irreversible and requires the dashboard
confirmation dialog. It takes effect on the next request. A database backup is
not a reason to re-enable a leaked key.
Apply the MCP migration during a low-activity window before enabling the
endpoint. It adds nullable provenance columns and two indexes to the existing,
operator-managed api_key table, then creates the initially empty audit table.
PostgreSQL's standard index and foreign-key DDL can briefly block API-key
create, rotate, or revoke writes. Check the table size and active locks first,
pause those dashboard operations during the migration, and keep MCP disabled
until it completes. The migration deliberately uses the repository's normal
Prisma migration path instead of a multi-step concurrent-index procedure that
could leave a partially applied or invalid index requiring manual recovery.
Before application rollback, disable MCP and stop MCP clients. Do not execute
the MCP rollback.sql after a successful Prisma migration. Deploy an
application that does not read MCP audit data or MCP-owned API-key provenance
columns, then ship reviewed down statements as a later forward migration in
reverse dependency order. Use the repository rollback statements directly only
to reverse a failed, partially applied migration; after safely reversing the
partial statements, mark that failed migration rolled back and rerun the
corrected forward migration. Back up audit records first if your retention
policy requires them.
Because MCP is the final dependent migration, disable and reverse it before the integration schema. Follow the complete reverse order in the combined release migration guide.
Troubleshoot safely
503 mcp:disabled: enable MCP on the API service and recreate it.400 mcp:invalid_request: correct malformed or ambiguous host/origin forwarding.401 auth:invalid_api_key: replace a malformed, expired, revoked, or null-expiry key.403 auth:insufficient_scope: rotate the key withmcp:connectand the exact read scope.404resource error: confirm the URI uses the key's bound project and an in-project resource.429 http:rate_limited: honorRetry-After; inspect both ingress and instance-local limits.503 storage:error: restore audit persistence before retrying; a read is not reported as successful without its audit event.
Correlate failures with the safe request ID. Never paste an Authorization header, raw MCP arguments/results, document content, or provider response into logs or a support ticket.