Developers · MCP server
Sign-in and tokens
How an app signs a person in to PorchQ with OAuth 2.1, gets and refreshes tokens, and what ends a connection.
PorchQ’s MCP server follows the MCP authorization specification: OAuth 2.1 with PKCE, discovered from the server itself. Most MCP clients do all of this for you. This page is for anyone building or debugging one.
The flow at a glance
- The app calls
/mcpwithout a token and gets401with aWWW-Authenticateheader. - It reads PorchQ’s protected resource metadata, then its authorization server metadata.
- It identifies itself: with a client ID metadata document (preferred) or by registering.
- It sends the person’s browser to
/oauth/authorizewith a PKCE challenge. - The person signs in, chooses communities and permissions, and allows.
- The app exchanges the code at
/oauth/tokenfor an access token and a refresh token. - It calls
/mcpwithAuthorization: Bearer <access token>, and refreshes when the token runs out.
1. Discovery
Every request to /mcp needs a token, including initialize, server/discover and tools/list. Without one, PorchQ answers HTTP 401:
HTTP/2 401
WWW-Authenticate: Bearer realm="porchq", resource_metadata="https://porchq.com/.well-known/oauth-protected-resource/mcp", scope="community:read community:write maintenance:read maintenance:write documents:read people:read meetings:read billing:read"
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Sign in to PorchQ to use this server."}}
The scope value is what today’s tools need (community:read, community:write, maintenance:read, maintenance:write, documents:read, people:read, meetings:read, billing:read). Ask for all of it, so one consent covers everything the person may want; they can still untick changes.
Protected resource metadata (GET https://porchq.com/.well-known/oauth-protected-resource/mcp):
{
"resource": "https://porchq.com/mcp",
"authorization_servers": ["https://porchq.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["community:read", "community:write", "maintenance:read", "maintenance:write", "documents:read", "people:read", "meetings:read", "billing:read"],
"resource_name": "PorchQ"
}
Authorization server metadata (GET https://porchq.com/.well-known/oauth-authorization-server):
{
"issuer": "https://porchq.com",
"authorization_endpoint": "https://porchq.com/oauth/authorize",
"token_endpoint": "https://porchq.com/oauth/token",
"revocation_endpoint": "https://porchq.com/oauth/revoke",
"introspection_endpoint": "https://porchq.com/oauth/introspect",
"registration_endpoint": "https://porchq.com/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true,
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["read", "write", "community:read", "…", "offline_access"]
}
scopes_supported lists every scope, the two older ones (read, write) and offline_access. Apps always get a refresh token, so offline_access is accepted and ignored.
2. Identify your app
Client ID metadata document (preferred)
Host a JSON document at an https URL you control and use that URL as your client_id. PorchQ fetches it when a person is sent to sign in, and shows the person where the details came from.
{
"client_id": "https://example.com/oauth/porchq-client.json",
"client_name": "Example Assistant",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png",
"redirect_uris": ["https://example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
PorchQ’s rules for the document:
- The URL is
https, has a path, and has no user name, password or fragment. It must resolve only to public addresses. client_idin the document repeats the URL exactly.client_nameis present.redirect_urislists 1 to 10 addresses, eachhttpsor a loopback address (see below).token_endpoint_auth_methodisnoneorprivate_key_jwt. Both are treated as public clients, protected by PKCE.- The document is at most 32 KB and answers within 3 seconds, without redirects.
- PorchQ caches it for the
Cache-Control: max-ageyou send, between 5 minutes and a day (an hour if you send none).
Dynamic client registration
If your client can’t host a document, register at POST https://porchq.com/oauth/register (RFC 7591):
POST /oauth/register
Content-Type: application/json
{
"client_name": "My PorchQ script",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"token_endpoint_auth_method": "none",
"scope": "community:read community:write maintenance:read maintenance:write"
}
HTTP/2 201
Cache-Control: no-store
Content-Type: application/json
{
"client_id": "Zx1…",
"client_id_issued_at": 1792188000,
"client_name": "My PorchQ script",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"application_type": "web",
"scope": "community:read community:write maintenance:read maintenance:write"
}
- Only public clients register: there is no client secret.
scopelists what the app may ever ask for. Without it, the app may ask for any scope; the person still decides at sign-in.grant_typesmay only beauthorization_codeandrefresh_token;response_typesonlycode;application_typewebornative.- A registered app is shown to the person as one that “registered itself with PorchQ”, since PorchQ can’t confirm who made it.
- Registrations are limited per address; see Errors and limits.
Redirect addresses
A redirect address is https, or http on the person’s own computer: 127.0.0.1, [::1] or localhost. For those, any port is accepted, so a command-line tool can listen on whatever port is free. PorchQ warns the person that such an app runs on their own computer.
3. Send the person to sign in
Make a PKCE verifier (43 to 128 random characters) and its S256 challenge. Then open:
https://porchq.com/oauth/authorize
?response_type=code
&client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback
&scope=community%3Aread+community%3Awrite+maintenance%3Aread+maintenance%3Awrite+documents%3Aread+people%3Aread+meetings%3Aread+billing%3Aread
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&resource=https://porchq.com/mcp
code_challenge_methodmust beS256.resource(RFC 8707) names the server the token is for. Sendhttps://porchq.com/mcp, or leave it out and PorchQ binds the token to it. Any other value is refused withinvalid_target.- Ask for scopes by area. The older
readandwritestill work and are read as the area scopes they cover.
What the person sees. If they aren’t signed in, PorchQ asks them to sign in, then brings them back to this page. They must have confirmed their email address. The consent page shows:
- the app’s name and where PorchQ learned it (its metadata document’s host, or that it registered itself), and where they’ll be sent back;
- what the app will be able to see, in plain words, limited to what their roles allow;
- Let the app make changes: ticked by default; unticked, the app gets only reading permissions;
- For these communities: a box per community they belong to, with their role. At least one must be ticked.
PorchQ never skips this page, even for an app the person allowed before.
The answer. PorchQ redirects to your redirect_uri with code, your state and iss=https://porchq.com (RFC 9207). Check that state is yours and that iss is https://porchq.com. If the person cancels, you get error=access_denied with the same state and iss.
What the app is granted is decided by PorchQ, never by the app or the page:
- no changing permissions unless the person allowed changes;
- no owner- or manager-only permissions when every ticked community is one where they are a resident;
- only the communities they ticked;
- never more than the app asked for.
4. Exchange the code
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback
&client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&resource=https://porchq.com/mcp
{
"access_token": "kV3n…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "Qm8t…",
"scope": "community:read community:write maintenance:read maintenance:write",
"created_at": 1792188060
}
scope is what was actually granted. Read it rather than assuming the app got everything it asked for.
5. Call the server
Send the access token on every request:
POST /mcp
Authorization: Bearer kV3n…
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
A token is accepted only if it is live, was issued for https://porchq.com/mcp, belongs to an active PorchQ account with a confirmed email address, and its app has not been turned off. Otherwise PorchQ answers 401 with error="invalid_token" in WWW-Authenticate; refresh, or sign the person in again.
6. Refresh
Access tokens last 60 minutes. Before or when one runs out, refresh:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=Qm8t…
&client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
You get a new access token and a new refresh token. Keep the new one; the old one is retired.
- Reuse ends the connection. If a retired refresh token is used again more than 60 seconds after it was replaced, PorchQ treats it as stolen and ends the whole connection for that app and person. Within those 60 seconds a reused token is only refused, so two refreshes racing each other don’t disconnect anyone.
- Unused connections end. If an app doesn’t refresh for 60 days, the connection ends and the person signs in again.
- A refresh keeps the communities and permissions the person chose.
7. More permissions later
If a tool needs a permission the connection doesn’t have, PorchQ answers 403 with the missing scopes:
HTTP/2 403
WWW-Authenticate: Bearer realm="porchq", resource_metadata="https://porchq.com/.well-known/oauth-protected-resource/mcp", scope="people:read", error="insufficient_scope"
Run step 3 again asking for those scopes. The consent page shows the new lines first, with what’s already allowed folded underneath.
8. Ending a connection
- Your app revokes its tokens at
POST https://porchq.com/oauth/revokewithtokenandclient_id. Do this when the person disconnects PorchQ in your app. - PorchQ can turn an app off. A turned-off app gets no consent page and no tokens, and its existing tokens are refused.
- Time: reusing a retired refresh token, or not refreshing for 60 days, ends the connection (see step 6).
Tokens and security
- PorchQ stores codes and tokens as SHA-256 hashes, and shows a token once, in the response that issues it. One issued before 13 October 2026 is rewritten as a hash the first time it is used.
- Keep tokens out of logs, URLs and error reports. Store refresh tokens encrypted.
- Calls from a web page are accepted only from PorchQ,
https://chatgpt.com,https://claude.aior the person’s own computer. Server-side calls send noOriginand are unaffected.
This page as Markdown: /developers/mcp/authorization.md