MCP server
How MCP clients sign in to Pozy with OAuth 2.1: discovery, client registration, PKCE, scopes, workspace choice, read-only access, token lifetimes and revoking access.
When you add https://mcp.pozy.ai/mcp to a client that supports OAuth, the client signs you in to Pozy in the browser and receives a token for one workspace. You never copy a key, and you can cut the client off at any time.
This page explains what happens during that sign-in, for people who want to know what they are approving and for developers building their own MCP client.
#The sign-in, step by step
#The client is turned away
The client calls the server without a token. Pozy answers 401 with a WWW-Authenticate header whose resource_metadata points at the protected resource metadata document.
#The client discovers Pozy's authorization server
It reads the protected resource metadata, then the authorization server metadata, which lists the authorization, token, registration and revocation endpoints and the supported scopes.
#The client registers itself
Registration is open (RFC 7591 dynamic client registration). Any MCP client can register without asking Pozy first. Registrations are rate-limited per caller.
#You sign in and choose a workspace
The browser opens the Pozy sign-in page. If you belong to more than one workspace, you pick the one this client will work in.
#You approve the scopes
The consent screen names the client and lists what it asked for. Allow access sends the client back with an authorization code; Deny sends it back with nothing.
#The client gets its tokens
The client exchanges the code for an access token, proving it started the flow with PKCE (S256). From then on every MCP request carries Authorization: Bearer <access token>.
#Scopes
| Scope | Shown as | Allows |
|---|---|---|
read |
Read your workspace | Connected accounts, profiles, posts, analytics, inbox, reviews and ads |
write |
Act on your connected accounts | Publishing and scheduling, replying to messages, comments and reviews, changing ad campaigns |
offline_access |
Stay signed in | A refresh token, so you are not asked to sign in again every hour |
A client that asks for read without write gets a read-only grant: every read tool works and every write tool fails with read_only_key. Your workspace role caps this too: a viewer's grant is read-only even when the client asked for write.
#Workspaces
A grant belongs to one workspace, the one you picked during sign-in. The assistant only sees that workspace's profiles and accounts, and whoami tells it which one it is in.
To use a different workspace, revoke the client and connect it again, picking the other workspace. Some clients let you add the same server twice under different names, one per workspace.
If you leave the workspace or are removed from it, the grant stops working at once.
#Tokens
| Token | Prefix | Lifetime |
|---|---|---|
| Access token | pzat_ |
1 hour |
Refresh token (with offline_access) |
pzrt_ |
30 days |
Clients refresh access tokens on their own. If a client sits unused past the refresh token's lifetime, it asks you to sign in again.
OAuth tokens never start with pz_, which is reserved for API keys. The server accepts both on the same URL.
#Revoke a client
Open Settings → Connected apps in the dashboard. Each client shows its scopes, when you authorized it and when it was last used. Revoke ends every token it holds immediately.
You can do the same through the API:
# List the apps you have authorized
curl https://api.pozy.ai/v1/me/connected-apps \
-H "Authorization: Bearer $POZY_API_KEY"
# Revoke one
curl -X DELETE https://api.pozy.ai/v1/me/connected-apps/<clientId> \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/me/connected-apps", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const app of data.connectedApps ?? []) console.log(app.clientId, app.clientName, app.lastUsedAt);
const res2 = await fetch("https://api.pozy.ai/v1/me/connected-apps/<clientId>", {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
if (!res2.ok) throw new Error((await res2.json()).error);import os, requests
headers = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
apps = requests.get("https://api.pozy.ai/v1/me/connected-apps", headers=headers, timeout=30).json()
for app in apps["connectedApps"]:
print(app["clientId"], app["clientName"], app["lastUsedAt"])
requests.delete("https://api.pozy.ai/v1/me/connected-apps/<clientId>", headers=headers, timeout=30)The revoke response says how many tokens and pending authorization codes it ended.
#OAuth or an API key?
| OAuth | API key | |
|---|---|---|
| Setup | Paste the URL, sign in | Create a key, paste it into the client config |
| Acts as | You, with your workspace role | The key, with the key's scope and permission |
| Read-only | When the client asks only for read, or you are a viewer |
When the key's permission is read |
| Limit to some profiles | No, the whole workspace | Yes, with a profile-scoped key |
| Revoke | Settings → Connected apps | Delete the key |
| Best for | People using an assistant | Agents, CI, clients without OAuth |
#For client developers
| Item | Value |
|---|---|
| Resource | https://mcp.pozy.ai/mcp |
| Protected resource metadata | https://mcp.pozy.ai/.well-known/oauth-protected-resource |
| Authorization server metadata | https://mcp.pozy.ai/.well-known/oauth-authorization-server |
| Grant types | authorization_code, refresh_token |
| PKCE | Required, S256 |
| Client registration | Dynamic (RFC 7591), open |
| Resource indicators | Supported (RFC 8707); use https://mcp.pozy.ai/mcp as the resource |
Read every endpoint URL from the metadata documents rather than hard-coding it.