MCP OAuth
OAuth 2.0 endpoints used by MCP clients (Claude Desktop, Cursor, OpenAI Apps, and any other MCP-compatible host) to authenticate against your Pharlo workspace.
These endpoints implement the standard OAuth 2.0 Authorization Code flow plus RFC 7591 Dynamic Client Registration (opens in a new tab), so MCP clients can register and connect automatically without manual setup.
If you are integrating an MCP client, you usually do not call these endpoints directly — the client handles the flow on your behalf. The documentation here is provided so MCP host developers and security reviewers can audit the protocol surface.
Base URL: https://pharlo.io
These endpoints are not prefixed with /api/v1 — they live under the platform root so MCP clients can discover them via standard /.well-known/ paths.
Endpoints
| Method | Path | Description | Status |
|---|---|---|---|
| GET | /.well-known/oauth-authorization-server | OAuth Authorization Server metadata (RFC 8414) | 200 |
| GET | /.well-known/oauth-protected-resource | Protected Resource metadata | 200 |
| POST | /oauth/mcp/register | Dynamic Client Registration (RFC 7591) | 201 |
| GET | /oauth/mcp/authorize | User consent screen | 302 |
| POST | /oauth/mcp/token | Exchange code or refresh token for access tokens | 200 |
Discovery
MCP clients begin by fetching the discovery documents under /.well-known/. These return JSON describing the supported grant types, scopes, and the URLs of the registration, authorization, and token endpoints.
curl https://pharlo.io/.well-known/oauth-authorization-server
curl https://pharlo.io/.well-known/oauth-protected-resourceThe response follows RFC 8414 (opens in a new tab):
{
"issuer": "https://pharlo.io",
"registration_endpoint": "https://pharlo.io/oauth/mcp/register",
"authorization_endpoint": "https://pharlo.io/oauth/mcp/authorize",
"token_endpoint": "https://pharlo.io/oauth/mcp/token",
"grant_types_supported": ["authorization_code", "refresh_token"],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["mcp.read", "mcp.write"]
}POST /oauth/mcp/register
Register a new MCP client and receive a client_id. Implements RFC 7591 Dynamic Client Registration (opens in a new tab). No prior credentials required.
Request body
{
"client_name": "My MCP App",
"redirect_uris": ["https://my-mcp-app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}Response 201 Created
{
"client_id": "mcp_3f9e2a1b4c5d6e7f",
"client_id_issued_at": 1747576800,
"redirect_uris": ["https://my-mcp-app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}The client_id is permanent. Public MCP clients (token_endpoint_auth_method: none) must use PKCE on the authorization request.
GET /oauth/mcp/authorize
Browser-facing consent screen. The user reviews the requested scopes and approves or denies access. On approval, Pharlo redirects to the registered redirect_uri with an authorization code.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | From /oauth/mcp/register |
redirect_uri | string | Yes | Must exactly match one of the registered redirect_uris |
response_type | string | Yes | Must be code |
scope | string | Yes | Space-separated: mcp.read mcp.write |
code_challenge | string | Yes | PKCE challenge — base64url(SHA-256(verifier)) |
code_challenge_method | string | Yes | Must be S256 |
state | string | Recommended | Opaque value echoed back for CSRF protection |
Successful redirect
HTTP/1.1 302 Found
Location: https://my-mcp-app.example.com/oauth/callback?code=...&state=...Error redirect
If the user denies access or the request is malformed, the redirect contains error and error_description query params per RFC 6749 §4.1.2.1 (opens in a new tab).
POST /oauth/mcp/token
Exchange an authorization code for an access token, or refresh an existing access token.
Authorization Code grant
curl -X POST https://pharlo.io/oauth/mcp/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_FROM_AUTHORIZE" \
-d "redirect_uri=https://my-mcp-app.example.com/oauth/callback" \
-d "client_id=mcp_3f9e2a1b4c5d6e7f" \
-d "code_verifier=ORIGINAL_PKCE_VERIFIER"Refresh Token grant
curl -X POST https://pharlo.io/oauth/mcp/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN" \
-d "client_id=mcp_3f9e2a1b4c5d6e7f"Response 200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiI...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_8a9b0c1d2e3f4a5b",
"scope": "mcp.read mcp.write"
}| Field | Type | Description |
|---|---|---|
access_token | string | Bearer token — pass as Authorization: Bearer <access_token> to Pharlo API calls |
expires_in | int | Lifetime in seconds (3600 = 1 hour) |
refresh_token | string | Use to obtain a new access_token without user interaction |
scope | string | Granted scopes — may be narrower than requested |
Error responses
| Status | error value | Reason |
|---|---|---|
400 | invalid_request | Missing or malformed parameter |
400 | invalid_grant | Authorization code expired, already used, or PKCE verifier mismatch |
400 | invalid_client | Unknown client_id |
401 | invalid_token | Refresh token revoked or expired |
For details on the MCP integration story end-to-end, see MCP Integration: Setup.
See also
- MCP Integration overview — what MCP is and how Pharlo exposes its tools
- MCP Setup — connecting a specific MCP host (Claude Desktop, Cursor, …)
- OAuth / Channel Connect — separate flow for connecting user-side social channels (not MCP clients)