MCP server security
v1.0 · effective 2026-08-06
Scope. This page describes Mill's Model Context Protocol (MCP) server at /api/mcp, which lets an AI assistant read from and create drafts in Mill on your behalf. It does not cover Mill's REST API, the Hall or Inn products, or the assistant vendor's own handling of data you send them.
The server is off by default. It exists only where an operator has explicitly enabled it, and only responds to a key an organization owner deliberately created and scoped.
The authorization boundary
One rule governs what this surface may ever do:
Static organization API keys carry read and draft-create. Anything that spends credits, publishes, or attributes a human action requires OAuth.
The boundary is drawn on blast radius. A leaked key today yields draft rows an owner deletes; drafts cost nothing and generate nothing. That is a tolerable exposure for a long-lived pasted secret. It stops being tolerable the moment a tool spends money or writes an attributed record, which is why those capabilities wait for short-lived, per-user OAuth tokens rather than shipping on a static key.
What is enforced
Each row names the mechanism. Every one of them runs on each request or fails the build.
| Control | Mechanism |
|---|---|
| Off unless enabled | A server flag checked before parsing and before authentication. Unset, the endpoint returns 404 and reveals nothing. |
| Assistants cannot spend credits | No credit-spending or job-queuing tool exists on the surface, and a build gate scans the MCP source tree and fails the build if one appears. Course creation produces a draft only; a human starts generation in Mill, where the cost is shown. |
| Explicit scopes | A key grants mcp:read and/or mcp:create only if those exact strings were selected when it was created. Wildcards and empty scope lists grant nothing here, so an existing integration key cannot acquire assistant access by accident. |
| Organization isolation | Every course read compares the record's organization against the key's and refuses otherwise, with no fallback branch. Pagination cursors are validated against the same boundary. |
| Revocation and expiry | Revoked and expired keys are rejected on the next request. Keys may be issued with an expiry date. |
| Plan entitlement | Available on Mill Pro and Team plans. Resolved from the higher of the organization and user records, so a paid subscriber is never refused because of which record holds the plan. |
| Rate and volume limits | A per-organization request window, a per-key hourly cap on course creation, and the same shared daily creation budget the Mill interface uses. An assistant gets no separate allowance. |
| Request-size cap | Oversized request bodies are rejected before parsing and before authentication. |
| Untrusted-content framing | Course content returned to an assistant is wrapped in an explicit data boundary telling the model to treat it as data and never as instruction, and the boundary cannot be closed early by the content itself. This raises the cost of a prompt-injection attempt hidden in course text; see Limitations for what it does not do. |
| Audit record | Course creation through MCP writes a tamper-evident audit entry marked with its origin and the key used. Read calls are recorded in the API request log. Failed authentication — a bad, revoked or expired key, a missing scope, an unentitled plan, or a throttled address — is recorded separately in the security log with its reason, the calling address and the client's user agent. The token is never written to either. |
What crosses to the AI vendor
Anything a tool returns enters your assistant provider's context and, subject to their terms, their logs. Mill limits what that can be:
- Course metadata and status for your organization only.
- Approval-gate and reviewer-signature state. Reviewer email addresses are masked on this surface, and reviewers' free-text notes are withheld entirely, even though both appear in the evidence pack Mill shows an authenticated user in the browser.
- Learner data: none. Mill does hold learner-side records (progress, notes, certificate activity), but no MCP tool reads any of them — the surface is limited to courses, their pipeline status, their approval state and their evidence pack. No learner name, address or progress record can be retrieved through an assistant.
Tools return structured data rather than instructions, and course content is summarised structurally rather than dumped, to limit what a prompt-injection attempt in authored content could reach.
Limitations
Stated because a security page that lists only strengths is not useful to the people reading it.
- Keys are long-lived and static. They are bearer credentials: anyone holding one has the access it carries until it is revoked or expires. Prefer a short expiry, prefer read-only, and treat an MCP client's configuration file as a place secrets can leak from.
- Rate limits are per server process. Where Mill runs more than one replica, the effective caps multiply by the number of replicas, and they reset on restart. They bound runaway automation; they are not a hard quota.
- Actions are attributed to the key's creator, not to a person at a keyboard. A draft created through MCP is owned by whoever created the key. Per-user attribution needs OAuth.
- Failure records are sampled under load. Failed authentication is throttled per network address and written to Mill's security log with its reason — never the token. But when an address trips the limiter, that is recorded at a reduced rate rather than once per attempt, so the log shows that an address was throttled, not how many times it tried.
- Content framing is mitigation, not a guarantee. No delimiter compels a language model to obey it. The boundary gives your assistant an explicit rule and prevents content from escaping the wrapper, but a sufficiently crafted payload may still influence a model that reads it. Treat course content authored by people you do not trust the way you would treat any untrusted document.
- No one-click connector yet. Connection is by API key in an MCP client's configuration. The browser-based “add a connector” flow requires OAuth discovery, which Mill does not yet implement.
Revoking access
- One client: revoke its key in Mill under Team → API keys. Effective on that key's next request; no restart needed.
- Everything at once: an operator unsets the server flag and restarts. The endpoint returns to 404. No data is altered — MCP adds no tables and stores nothing of its own.
Reporting a vulnerability
Email security@colport.io. Please include enough detail to reproduce. We will acknowledge receipt and keep you updated; please give us a reasonable window to remediate before public disclosure.