# Authenticating to the Norvet MSP MCP server

This document tells an AI agent how to get authenticated access to `https://norvetmsp.com/mcp`,
Norvet MSP's Model Context Protocol (MCP) server. Written for the `auth.md` convention
(<https://github.com/workos/auth.md>) and linked from this server's OAuth 2.0 Authorization Server
Metadata (`/.well-known/oauth-authorization-server`, the `agent_auth.skill` field).

## Two tiers, one server

Most of what `/mcp` offers needs no authentication at all. Thirteen tools (search, service info,
published pricing, fiber availability, seven free self-assessment checks, and lead/quote intake
gated by explicit consent instead of a login) work with a plain, unauthenticated `POST /mcp`. See
[`/agents`](https://norvetmsp.com/agents) for the full public tool list.

Three additional tools are **admin-only** and require an OAuth 2.1 access token:

| Tool                  | Scope            | What it does                                                            |
| --------------------- | ---------------- | ----------------------------------------------------------------------- |
| `list_recent_leads`   | `leads:read`     | Recent QuoteDesk leads (id, date, name, company, source, status).       |
| `get_lead`            | `leads:read`     | One lead's full record, by id.                                          |
| `quotedesk_dashboard` | `dashboard:read` | The same pipeline/attention summary the Command Center dashboard shows. |

`submit_lead` and `request_cabling_quote` also accept the `leads:write` scope: presenting a valid
token with that scope raises your rate limit from 5/hour per IP to 30/hour per signed-in admin,
because a known identity is trusted with a higher budget than an anonymous connection. Neither
tool REQUIRES a token; both still work exactly as before with no `Authorization` header at all.

## Important: this is human-delegated authorization, not an autonomous agent identity

Nothing on `norvetmsp.com` today lets an agent register its own standing identity and act on its
own authority. Every admin-tier token traces back to one specific human: a Norvet MSP administrator
who is already signed in (email + password or Google, plus mandatory TOTP MFA) and who explicitly
clicked "Approve" on a consent screen naming your application and the exact access it is requesting.
If you are an agent reading this file to figure out how to get access on your own, the honest answer
is: you cannot. Find the human who runs Norvet MSP's Command Center and have them complete the
authorize step below.

## How to get a token

This server implements standard **OAuth 2.1 with PKCE**, Dynamic Client Registration (RFC 7591),
and Protected Resource Metadata (RFC 9728) -- the same flow the
[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
describes. If your MCP client library already speaks that flow, point it at
`https://norvetmsp.com/mcp` and it should discover everything below on its own via
`GET /.well-known/oauth-protected-resource/mcp` and `GET /.well-known/oauth-authorization-server`.

1. **Register** (once per application/install): `POST /oauth/register` (RFC 7591) with a JSON body
   `{ "client_name": "...", "redirect_uris": ["https://your-app.example/callback"] }`. Redirect URIs
   must be `https://` (or `http://localhost` / `http://127.0.0.1` for a local CLI or desktop agent
   that cannot hold a TLS certificate). Public clients only -- the response's
   `token_endpoint_auth_method` is always `"none"`, there is no client secret to protect. Returns a
   `client_id`. This server is **stateless**: nothing is stored in a database, the `client_id` you
   get back is itself a signed, verifiable credential. There is no way to list, look up, or recover
   a lost `client_id`; register again if you lose it.

2. **Generate a PKCE pair**: a `code_verifier` (43-128 random characters) and its S256
   `code_challenge` (`BASE64URL(SHA256(code_verifier))`).

3. **Send the human to `/oauth/authorize`** with `client_id`, `redirect_uri` (must exactly match one
   you registered), `response_type=code`, `code_challenge`, `code_challenge_method=S256`, a `state`
   value you will verify on return, `scope` (space-separated; default is `leads:read dashboard:read`
   if omitted, add `leads:write` explicitly if you need it), and `resource=https://norvetmsp.com/mcp`
   (RFC 8707). If they are not already signed in as a Norvet MSP administrator, they will be sent to
   sign in and complete MFA first, then land back on the same consent screen automatically. If they
   approve, they are redirected to your `redirect_uri` with `code` and `state`. If they deny, or are
   not an authorized administrator, you get `error=access_denied` instead.

4. **Exchange the code**: `POST /oauth/token` (form-encoded) with `grant_type=authorization_code`,
   `code`, `redirect_uri` (same value again), `client_id`, and `code_verifier`. Returns
   `access_token` (Bearer, 1 hour), `refresh_token` (30 days), `token_type`, `expires_in`, `scope`.
   Codes are single-use and expire in 60 seconds.

5. **Call the tool**: `POST /mcp` with `Authorization: Bearer <access_token>`, same JSON-RPC body
   shape as every other tool call.

6. **Refresh** before the access token expires: `POST /oauth/token` with
   `grant_type=refresh_token`, `refresh_token`, `client_id`. Refresh tokens rotate on every use (the
   old one stops working the moment a new one is issued) and you may only request the same scopes
   you already had, or a subset -- never more.

7. **Revoke** when you are done: `POST /oauth/revoke` (RFC 7009) with `token=<access_or_refresh_token>`.
   Always answers `200`, whether or not the token was real.

If a `tools/call` for one of the three admin tools arrives with no `Authorization` header at all,
`/mcp` answers `401` with a `WWW-Authenticate: Bearer ...resource_metadata=...` header pointing back
at the discovery documents above, per the MCP authorization spec.

## What this server does NOT do

- **No ID tokens / OpenID Connect.** This is OAuth 2.1, not OIDC -- there is no
  `/.well-known/openid-configuration` and no `id_token`. If you need to know who approved a grant,
  the answer is out of scope for a third-party agent by design; Norvet's own logs know, you do not
  need to.
- **No client secrets.** Every registered client is public (`token_endpoint_auth_method: "none"`).
  Do not attempt a confidential-client flow against this server; it will fail.
- **No `identity_endpoint`/`claim_endpoint`/ID-JAG machinery from the `auth.md` reference
  implementation.** The reference implementation at <https://github.com/workos/auth.md> describes a
  richer agentic-registration protocol (signed identity assertions from a separate "agent
  provider," a claim ceremony, webhook revocation events). Norvet has none of that infrastructure
  and does not need it: the flow above (ordinary RFC 7591 DCR + OAuth 2.1 authorization code + PKCE,
  gated by a human's explicit consent) covers the one real use case this server has today. The
  `agent_auth` block in this server's AS metadata reflects that honestly (`identity_types_supported:
["anonymous"]`, `identity_endpoint` pointing at the same `/oauth/register` described above) rather
  than claiming conformance to fields this server does not implement.
- **No revocation webhook.** If Gregory needs to cut off a specific grant early, he rotates
  `MCP_OAUTH_SIGNING_KEY` (denylisting every token everywhere at once) or waits out the 1-hour access
  token / 30-day refresh token lifetimes. There is no per-client "your access has been revoked"
  push notification.

## Questions

Email [support@norvetmsp.com](mailto:support@norvetmsp.com), or see
[https://norvetmsp.com/agents](https://norvetmsp.com/agents) for the rest of the MCP server's public
surface.
