Operate MailGuardHQ from an AI client
The Model Context Protocol is how an AI assistant calls a service on your behalf. Our MCP server exposes 20 tools over the same public REST API the web app uses - so an assistant can check a domain's email authentication, manage the domains you monitor and handle API tokens, with the same permissions you would have yourself.
Not released yet - there is nothing to connect to
This documents a server that exists and works, and that we do not host for you. There is no public endpoint, no published repository and no registry listing yet. Running it today means running it yourself, from source, against your own account.
Public release is tracked as its own piece of work and is not finished. This page is here so you can decide whether it is worth waiting for, and tell us what it is missing. Everything below is what the server does now, not what it is planned to do.
What it is
A remote MCP server, spoken over HTTP. There is nothing to install on your machine and nothing to keep running beside your editor - a client opens a connection, asks what tools exist, and calls them.
It is a thin wrapper, and that word is doing real work. It holds no business logic, talks to no database, and makes no authorization decision of its own: no scope check, no role check, no plan check, no tenancy check. All of that stays in the API, computed on every request from what your token carries, what your role permits, what the token's audience allows and what your plan sells. A second place to decide permissions is a second place to be wrong, and it is always the second one that drifts.
The tools are generated from the API's OpenAPI document, not written by hand, and a build in the server's own repository fails if the generated list has drifted from the contract. 20 tools from 71 operations, with 51 operations deliberately left out - listed below with the reason for each.
Connecting
Three things: a credential, a running server, and a client pointed at it.
1. Get a token
You need a MailGuardHQ API token minted for the mcp audience. A
free account is enough - sign up, verify your address, and mint one
from your organisation. Tokens look like mgh_live_... in the live environment
and mgh_test_... in test, and every one of them expires.
What free actually carries: one token at a time, a maximum lifetime of
30 days, and never dns:write - which an MCP token could not
use anyway. That is enough to connect a client and drive the product; it is not enough
to run a fleet on.
2. Run the server
One setting decides everything: which API it wraps. Nothing secret goes in a file - the credential travels on the request, from the client.
Run it against the live API
# the one setting that matters: which API this wraps
Api__BaseUrl=https://api.mailguardhq.com
dotnet run --project src/MailGuardHq.Mcp
Check it came up, and what it thinks it is. The readiness probe reports its own posture, including the honest state of audience verification, so an operator sees the gap in a probe rather than only in a document:
Readiness
curl -s localhost:5000/health/ready
{"status":"ok","tools":20,"excluded":51,
"specVersion":"v1","audienceVerification":"FormatOnly",
"audienceVerifierOperational":false,"credentialRedaction":true,
...}
3. Point a client at it
The transport is streamable HTTP and the endpoint is /mcp. Configuration
differs slightly between clients; the shape is always a URL and an
Authorization header.
A typical MCP client configuration
{
"mcpServers": {
"mailguardhq": {
"type": "http",
"url": "http://localhost:5000/mcp",
"headers": {
"Authorization": "Bearer mgh_live_..."
}
}
}
}
The server is stateless - every request carries its own credential and nothing is remembered between them - so it scales sideways and a restart costs nobody a session.
Authentication
One credential kind works today: an API token you minted, sent as a bearer token. What the server does before it forwards anything upstream, in order:
| What arrives | What happens |
|---|---|
| No credential at all | Answered 401 with an RFC 9728 WWW-Authenticate challenge naming
where to look, so a client that speaks OAuth discovery can find its own way rather than
simply failing. |
| A credential minted by somebody else | Refused, never forwarded. Passing a token minted for another service straight through is a named MCP anti-pattern: it makes this server a confused deputy, detaches the audit trail from the real actor, and turns a token stolen anywhere into a token usable here. |
| A malformed credential | Refused arithmetically, from the checksum inside the token, with no network call. Without that, every junk string would cost the API a database round trip and flooding this endpoint would be a cheap way to take the authentication path down. |
| An interactive session token | Refused by default. A session token is a browser credential; finding one in a client config file means somebody pasted their signed-in session somewhere it will outlive their attention. |
Forwarding your token upstream is not passthrough. The anti-pattern is accepting a token minted for a different service. Yours was minted by MailGuardHQ for MailGuardHQ, and forwarding it unchanged is what keeps the API the single place permissions are decided, with one audit trail and the real actor on every row.
Scopes, and the two things MCP can never do
Scopes are resource:action and never imply one another -
domains:write does not grant domains:read. What a call is
actually allowed to do is the intersection of four ceilings, recomputed on every
request: the token's scopes, your current role, the token's audience, and what your plan
sells. A demotion or a downgrade therefore applies immediately rather than at the next
token rotation.
dns:write and members:write are excluded from MCP, whatever your token says
An MCP token is driven by a model reading text it did not write, so prompt injection is routine input rather than an exotic attack. The two actions a model must never be talked into are breaking a customer's mail flow and granting somebody administrative access - the first stops legitimate email and is slow to notice, the second is how a single bad instruction becomes a permanent one.
The API applies that ceiling itself, so it holds even if the MCP server is wrong, misconfigured or replaced.
The 20 tools
Generated from the API contract, so this table cannot describe a call the API does not serve. Each tool wraps exactly one endpoint.
Checks
Run the same thirteen checks the free domain check runs, and read what a past run found.
| Tool | What it does | Calls |
|---|---|---|
mgh_list_check_history | List past check results | GET /v1/checks/history |
mgh_list_checks | List the checks that a run performs | GET /v1/checks/catalog |
mgh_run_domain_check | Run a domain check | POST /v1/checks |
Domains
The domains an organisation monitors. Adding one is a data change - no deploy, no code.
| Tool | What it does | Calls |
|---|---|---|
mgh_add_domain | Start monitoring a domain | POST /v1/domains |
mgh_get_domain | Fetch one monitored domain | GET /v1/domains/{id} |
mgh_list_domains | List monitored domains | GET /v1/domains |
Account
Who the credential belongs to.
| Tool | What it does | Calls |
|---|---|---|
mgh_get_account | The signed-in person | GET /v1/accounts/me |
API tokens
Mint, list, rotate and revoke credentials - including the breach-response path that revokes many at once.
| Tool | What it does | Calls |
|---|---|---|
mgh_get_token | Fetch one token's metadata | GET /v1/orgs/{organizationId}/tokens/{tokenId} |
mgh_list_tokens | List an organisation's tokens | GET /v1/orgs/{organizationId}/tokens |
mgh_revoke_token | Revoke one token | DELETE /v1/orgs/{organizationId}/tokens/{tokenId} |
Organisations and members
The organisation record, who is on it, and the invitations outstanding.
| Tool | What it does | Calls |
|---|---|---|
mgh_get_organization | Fetch one organisation | GET /v1/orgs/{organizationId} |
mgh_list_account_settings | List this organisation's settings | GET /v1/orgs/{organizationId}/settings |
mgh_list_invitations | List an organisation's invitations | GET /v1/orgs/{organizationId}/invitations |
mgh_list_members | List an organisation's members | GET /v1/orgs/{organizationId}/members |
mgh_list_organization_check_history | List one organisation's check history | GET /v1/orgs/{organizationId}/checks/history |
mgh_list_organization_domains | List one organisation's monitored domains | GET /v1/orgs/{organizationId}/domains |
Pricing
What a plan costs on a term, including the domains past its allowance. Anonymous, and it changes nothing - the pricing console that discloses our costs and the one that changes a price are both excluded by name.
| Tool | What it does | Calls |
|---|---|---|
mgh_get_pricing_quote | Price a plan | GET /v1/pricing/quote |
Health
Whether the server is up and what it thinks it is. Neither needs a credential.
| Tool | What it does | Calls |
|---|---|---|
mgh_health_dependencies | Registry dependency check - reads a real row and the schema. Not a probe. | GET /health/dependencies |
mgh_health_live | Liveness probe | GET /health/live |
mgh_health_ready | Readiness probe | GET /health/ready |
A worked example
Connect, check a domain, read the findings. This is the whole loop.
- Point your client at the server as above. On connect it asks for the tool list and gets all 20, each with its JSON schema, so the model knows what arguments are valid without being told.
- Ask for a check in ordinary words - "check whether anyone can send email as yourcompany.com". The client picks the tool.
- Read the findings. Each is a discrete result you can act on, not a paragraph of prose to interpret.
The call the client makes, and what comes back
mgh_run_domain_check { "domain": "yourcompany.com" }
-> 13 findings, one per check, each with an id, a verdict of
pass / warn / fail / not_applicable, and the evidence behind it.
mgh_run_domain_check accepts an Idempotency-Key, like every write
on the API: repeat a call with the same key and the original response is replayed rather
than the work being done twice.
51 operations are deliberately not exposed
Every one of them establishes a credential. A tool that takes a password puts that password in the model's context, in the client's transcript, and in every log along the way. None of the 51 is a product capability, so the whole product surface is still reachable.
| Operation | Why not |
|---|---|
| AcceptInvitation POST /v1/invitations/accept | Refuses API keys by design (session-only route) and consumes a single-use token from an invitation email. Accepting an invitation is a human act of joining an organisation. |
| AdminAnalysePricing GET /v1/admin/pricing/analysis | The platform admin surface, and the one route on it that discloses what the platform COSTS US: fully-loaded COGS, the margin model in force, and the multiple the published price achieves. MGH-264 kept every one of those off the public quote by construction - there is no property on the customer-facing type able to carry one - and publishing them as an MCP tool would undo that through the other door. MGH-119 additionally makes the route INVISIBLE, answering a non-administrator exactly as an unroutable path does, which advertising it as a tool defeats. |
| AdminChangeLimits PUT /v1/admin/organizations/{organizationId}/limits | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminChangeOrganizationStatus PUT /v1/admin/organizations/{organizationId}/status | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminChangePlan PUT /v1/admin/organizations/{organizationId}/plan | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminGetAccount GET /v1/admin/accounts/{userId} | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminGetOrganization GET /v1/admin/organizations/{organizationId} | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminListAudit GET /v1/admin/audit | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminListPlans GET /v1/admin/plans | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminRepricePlan POST /v1/admin/pricing/plans/{planCode}/price | It changes what every NEW customer is charged, and MGH-17 AC3 says outright that an AI operator must never reprice autonomously. An MCP token is a model reading untrusted content, so prompt injection is routine input here rather than an exotic attack - this is the clearest case in this file. Repricing belongs in the console, behind a human who has to type a reason that is recorded against the change. |
| AdminRevokeDomainOwnership POST /v1/admin/organizations/{organizationId}/domains/{domainId}/ownership/revoke | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminSearchAccounts GET /v1/admin/accounts | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| AdminSearchOrganizations GET /v1/admin/organizations | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| ChangeMemberRole PATCH /v1/orgs/{organizationId}/members/{seatId} | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| ConfirmCheckReportEmail POST /v1/checks/report-email/confirm | The report-by-email flow is a MARKETING-SITE mechanism for an anonymous visitor, not an API capability: it is the one place the product will send mail to an address that was typed into a form. Handing that to an MCP token means handing an outbound-mail primitive, aimed at an address chosen in the call, to a model reading untrusted content - which is how our sending reputation gets spent on somebody else's phishing. It also completes in an inbox: confirm and unsubscribe consume one-shot tokens that arrive by email, so a model could only ever start a flow it cannot finish. An authenticated MCP caller needs none of it - RunDomainCheck returns the findings in the response. |
| CreateOrganization POST /v1/orgs | The API serves this route to NO token audience - it accepts an interactive session only, which is how it refuses a machine credential acting unattended on a person's behalf. This is not an mcp narrowing and widening the audience would not help: there is no token to mint that would work. |
| CreateProvisionalSession POST /v1/provisional-sessions | Mints an anonymous pre-signup identity for the marketing site's free check. It is a website mechanism, not an API capability, and an authenticated MCP caller already has a real identity. |
| EndImpersonation DELETE /v1/impersonation | Impersonation is a property of a browser SESSION, and an MCP client authenticates with a token - so this can only ever answer nothing at all, or refuse. Publishing a tool whose entire purpose is to report on a capability the caller cannot have advertises the platform admin surface for no capability at all, which is precisely what StartImpersonation is excluded to avoid. |
| GetDomainOwnership GET /v1/domains/{id}/ownership | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| GetImpersonation GET /v1/impersonation | Impersonation is a property of a browser SESSION, and an MCP client authenticates with a token - so this can only ever answer nothing at all, or refuse. Publishing a tool whose entire purpose is to report on a capability the caller cannot have advertises the platform admin surface for no capability at all, which is precisely what StartImpersonation is excluded to avoid. |
| GetReportEmailNotice GET /v1/checks/report-email/notice | The report-by-email flow is a MARKETING-SITE mechanism for an anonymous visitor, not an API capability: it is the one place the product will send mail to an address that was typed into a form. Handing that to an MCP token means handing an outbound-mail primitive, aimed at an address chosen in the call, to a model reading untrusted content - which is how our sending reputation gets spent on somebody else's phishing. It also completes in an inbox: confirm and unsubscribe consume one-shot tokens that arrive by email, so a model could only ever start a flow it cannot finish. An authenticated MCP caller needs none of it - RunDomainCheck returns the findings in the response. |
| GetSystemSetting GET /v1/admin/settings/{key} | A SYSTEM-scope setting is vendor-controlled and platform-wide: one value changes behaviour for every tenant at once, which is why MGH-70 restricts the scope to admins. A model talked into moving one has a blast radius no single customer could have. Account-scope settings stay available - those are the customer's own, bounded by the system value above them, and audited. |
| GetSystemSettingAudit GET /v1/admin/settings/{key}/audit | A SYSTEM-scope setting is vendor-controlled and platform-wide: one value changes behaviour for every tenant at once, which is why MGH-70 restricts the scope to admins. A model talked into moving one has a blast radius no single customer could have. Account-scope settings stay available - those are the customer's own, bounded by the system value above them, and audited. |
| InviteMember POST /v1/orgs/{organizationId}/invitations | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| ListActivity GET /v1/orgs/{organizationId}/activity | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| ListDomainOwnershipEvidence GET /v1/domains/{id}/ownership/evidence | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| ListOrganizations GET /v1/orgs | The API serves this route to NO token audience - it accepts an interactive session only, which is how it refuses a machine credential acting unattended on a person's behalf. This is not an mcp narrowing and widening the audience would not help: there is no token to mint that would work. |
| ListSignalAttestations GET /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestations | REST-only, customer self-attestation is not an agent tool. The checklist exists precisely because MailGuardHQ cannot measure these signals itself; the answers are the organisation's own statements about things a model cannot verify on their behalf. MGH-249 declares this route to the "api" audience only, not "mcp", so the audience gate already excludes it - this entry is recorded anyway so the exclusion survives an audience change instead of silently republishing. |
| ListSystemSettings GET /v1/admin/settings | A SYSTEM-scope setting is vendor-controlled and platform-wide: one value changes behaviour for every tenant at once, which is why MGH-70 restricts the scope to admins. A model talked into moving one has a blast radius no single customer could have. Account-scope settings stay available - those are the customer's own, bounded by the system value above them, and audited. |
| Login POST /v1/accounts/login | Accepts a password and returns a session token - same reason as Signup. An MCP client authenticates with an already-minted API token. |
| Logout POST /v1/accounts/logout | Refuses API keys by design (session-only route). Ending a browser session is not something a machine credential does on a human's behalf. |
| MintToken POST /v1/orgs/{organizationId}/tokens | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| RecheckDomainOwnership POST /v1/admin/ownership/recheck | The platform admin surface. MGH-119 makes it INVISIBLE - an ordinary caller gets an identical not_found for an admin route and a route that does not exist - and advertising it as an MCP tool undoes exactly that. An MCP token is driven by a model reading untrusted content, so prompt injection is routine input rather than an exotic attack; administrative reach is the last thing that should be available that way. Support work belongs in the console, behind a human. |
| RecordSignalAttestation PUT /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestations/{signalCode} | REST-only, customer self-attestation is not an agent tool. Recording an answer here is a human asserting they personally checked something MailGuardHQ cannot measure; a model reading untrusted content must never be the one making that assertion on a customer's behalf. MGH-249 declares this route to the "api" audience only, not "mcp", so the audience gate already excludes it - this entry is recorded anyway so the exclusion survives an audience change instead of silently republishing. |
| RemoveMember DELETE /v1/orgs/{organizationId}/members/{seatId} | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| RequestCheckReportByEmail POST /v1/checks/report-email | The report-by-email flow is a MARKETING-SITE mechanism for an anonymous visitor, not an API capability: it is the one place the product will send mail to an address that was typed into a form. Handing that to an MCP token means handing an outbound-mail primitive, aimed at an address chosen in the call, to a model reading untrusted content - which is how our sending reputation gets spent on somebody else's phishing. It also completes in an inbox: confirm and unsubscribe consume one-shot tokens that arrive by email, so a model could only ever start a flow it cannot finish. An authenticated MCP caller needs none of it - RunDomainCheck returns the findings in the response. |
| ResendVerification POST /v1/accounts/resend-verification | Part of the email-verification loop, which completes in a browser. Exposing only half of a flow invites a model to start something it cannot finish. |
| ResetAccountSetting POST /v1/orgs/{organizationId}/settings/{key}/reset | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| ResetSystemSetting POST /v1/admin/settings/{key}/reset | A SYSTEM-scope setting is vendor-controlled and platform-wide: one value changes behaviour for every tenant at once, which is why MGH-70 restricts the scope to admins. A model talked into moving one has a blast radius no single customer could have. Account-scope settings stay available - those are the customer's own, bounded by the system value above them, and audited. |
| RevokeTokens POST /v1/orgs/{organizationId}/tokens/revoke | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| RotateToken POST /v1/orgs/{organizationId}/tokens/{tokenId}/rotate | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| Signup POST /v1/accounts/signup | Accepts a password and returns a session token. A tool that takes a password puts it in the model's context and in the client transcript; sign up on the web console instead, then mint an mcp-audience token. |
| StartDomainOwnershipVerification POST /v1/domains/{id}/ownership | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| StartImpersonation POST /v1/admin/impersonation | The platform admin surface, and the sharpest instrument on it: it mints a credential that sees the product as a named customer. MGH-120 gates it on consent and audit and a human being present. A model reading untrusted content is exactly who must never be able to start one, and an exclusion recorded here survives an audience change - the audience gate alone would silently republish it. |
| StepUp POST /v1/accounts/step-up | Accepts a password to re-confirm identity, and an API token can never satisfy step-up anyway - so exposing it would only ever leak a password for a guaranteed refusal. |
| StopCheckReportEmail POST /v1/checks/report-email/unsubscribe | The report-by-email flow is a MARKETING-SITE mechanism for an anonymous visitor, not an API capability: it is the one place the product will send mail to an address that was typed into a form. Handing that to an MCP token means handing an outbound-mail primitive, aimed at an address chosen in the call, to a model reading untrusted content - which is how our sending reputation gets spent on somebody else's phishing. It also completes in an inbox: confirm and unsubscribe consume one-shot tokens that arrive by email, so a model could only ever start a flow it cannot finish. An authenticated MCP caller needs none of it - RunDomainCheck returns the findings in the response. |
| UpdateAccountSetting PUT /v1/orgs/{organizationId}/settings/{key} | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| UpdateSystemSetting PUT /v1/admin/settings/{key} | A SYSTEM-scope setting is vendor-controlled and platform-wide: one value changes behaviour for every tenant at once, which is why MGH-70 restricts the scope to admins. A model talked into moving one has a blast radius no single customer could have. Account-scope settings stay available - those are the customer's own, bounded by the system value above them, and audited. |
| VerifyDomainOwnership POST /v1/domains/{id}/ownership/verify | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
| VerifyEmail POST /v1/accounts/verify-email | Consumes a single-use token from a verification email. The link belongs in a browser, not in a model's context. |
| WithdrawDomainOwnership DELETE /v1/domains/{id}/ownership | The API does not serve this route to the "mcp" audience, so every call would be refused 403 wrong_audience by the only kind of token this server accepts. An advertised tool that cannot be called is worse than an absent one: a model retries it and reports a product failure to the user. Publishing it becomes automatic the day the route declares mcp in x-mgh-audiences. |
What is not built yet
Stated here rather than discovered later. Each one is real, each is open, and none of them is worked around.
There is no hosted endpoint, and no public release
Nothing is running at a URL you can point a client at. The server is not on GitHub, not in any MCP registry, and not installable from a package manager. Today it runs from source, against your own MailGuardHQ credential.
Public release - the repository, the licence and the registry listings - is tracked separately and is not done. Until it is, everything below describes a server you would run yourself.
Interactive sign-in is not available - clients paste a token
MailGuardHQ does not operate an OAuth authorization server. There is no
/authorize, no /token and no client registration, so the
OAuth 2.1 and PKCE flow an interactive client would normally use has nothing to talk
to.
The server implements the half it can: it publishes RFC 9728 protected-resource
metadata and challenges correctly. The authorization_servers list in that
document is deliberately empty, because naming a server that does not
exist sends every compliant client into a discovery loop that cannot end. So an
interactive client asks you to paste a token, which works and is honest about what is
available.
The server cannot verify a token was minted for it
The API mints tokens with an audience and applies the MCP ceiling when it does, so the restriction exists on the issuing side. What is missing is any way for this server to read the audience of a token it has been handed - there is no introspection endpoint - so it checks the token's format and no more, and logs a warning saying so on every start.
The consequence, stated plainly: a token minted for the ordinary API audience is accepted here. The verifier that closes this is written and tested against a stub, and starts working the day the API can answer the question.
Domain and organisation calls refuse an MCP token today
The API defaults every route to the API audience and opts individual routes in. The
three check routes and five of the six token routes are opted in. The domain routes,
the account route, the organisation and member routes and the bulk token-revoke route
are not - so a correctly-minted MCP token is refused on them with
wrong_audience.
That means "operate the product entirely through MCP" is not true today: checks and credential management work, domains do not. It is not worked around, because both available workarounds are worse - shipping tools that fail, or telling people to mint a token for the wrong audience, which is exactly what the audience mechanism exists to prevent.
A token minted over MCP does not hand back its secret
Minting and rotating work, and the response carries the token's id, prefix, scopes and expiry - but the secret is stripped before it reaches the model. You collect it from the web console.
A real narrowing, deliberately chosen: the alternative was a brand-new credential, at its maximum useful lifetime, deposited into a model's context and from there into any transcript, log or screenshot that context reaches.
Nothing to connect to yet - so tell us what you would connect
The MCP server ships when the release work behind it is done. What it should do first is driven by what people actually ask for.