# MCP server

Connect Claude, ChatGPT, Cursor, or any MCP client to Openhandle with OAuth and call every endpoint as a tool.



Openhandle runs a remote MCP server over Streamable HTTP. Every public API
endpoint is one tool. The tool names, inputs, and responses match the REST API.

```text
https://api.openhandle.dev/mcp
```

## Connect a client [#connect-a-client]

Add the URL to your MCP client. The client discovers the OAuth settings, opens
Openhandle in your browser, and asks you to approve the connection. You pick the
workspace environment on that screen:

* **Test** answers every tool with synthetic data at $0.000 actual charge.
* **Live** uses real public data and normal billing. Live needs a verified
  workspace owner email.

Openhandle creates one API key for the connection. The key is named after the
client, for example `Claude (MCP)`. Usage, request history, and spend caps work
the same as for any other key.

Disconnect a client under **Settings → MCP server** in the dashboard. That
revokes the key and every token at once.

## OAuth details [#oauth-details]

The server follows the MCP authorization specification.

| Document                      | URL                                                                   |
| ----------------------------- | --------------------------------------------------------------------- |
| Protected resource metadata   | `https://api.openhandle.dev/.well-known/oauth-protected-resource/mcp` |
| Authorization server metadata | `https://api.openhandle.dev/.well-known/oauth-authorization-server`   |
| Dynamic client registration   | `https://api.openhandle.dev/oauth/register`                           |
| Authorization                 | `https://api.openhandle.dev/oauth/authorize`                          |
| Token                         | `https://api.openhandle.dev/oauth/token`                              |
| Revocation                    | `https://api.openhandle.dev/oauth/revoke`                             |

* Grant types: `authorization_code` with PKCE (`S256`), and `refresh_token`.
* Scopes: `read:instagram`, `read:tiktok`, and `read:twitter`.
* Legacy scope: `mcp` grants all MCP tools and remains the default when scope is omitted.
* Public clients register with `token_endpoint_auth_method: none`. Confidential
  clients receive a client secret.
* Access tokens live for one hour. Refresh tokens rotate on every use and live
  for 30 days.
* Send `resource=https://api.openhandle.dev/mcp` when your client supports
  resource indicators.

### Browser authorization flow [#browser-authorization-flow]

The authorization endpoint validates the OAuth parameters and stores a short-lived
authorization request. It then redirects the browser to the dashboard consent page
with an opaque `request` identifier. The dashboard loads the stored request from the
API and shows the client, requested access, environment, and spend-cap options.

Production sessions use a cookie shared across the `api.openhandle.dev` and
`app.openhandle.dev` subdomains. An unauthenticated user signs in through the
dashboard and then returns to the same consent request. Approval sends the browser
to the client's registered redirect URI with the authorization code and original
state.

## API key instead of OAuth [#api-key-instead-of-oauth]

Scripts and CI jobs can skip the browser flow. Send an API key as the bearer
token:

```text
Authorization: Bearer $OPENHANDLE_TEST_KEY
```

The key prefix selects the environment. There is no environment argument,
header, or setting.

## Tools [#tools]

* `find_test_data` and `get_test_data` find stable synthetic inputs. They are
  unmetered.
* Endpoint tools such as `instagram_get_profile` call the public API. They are
  metered exactly like REST calls and appear in request history with channel
  `mcp`.
* Every result carries metadata: `environment`, `requestId`, `actualCharge`,
  `liveEquivalentPrice`, and `datasetVersion`.

Read [Test with MCP](/docs/test-environment/mcp) for the agent sequence in the
test environment.

## Machine-readable discovery and permissions [#machine-readable-discovery-and-permissions]

Use `https://api.openhandle.dev/mcp` as the server endpoint. The [MCP setup guide](/setup-mcp) explains how to connect each client.

[Protected resource metadata](https://api.openhandle.dev/.well-known/oauth-protected-resource/mcp)
publishes `mcp`, `read:instagram`, `read:tiktok`, and `read:twitter` in `scopes_supported`.
Request only the platforms your agent needs, for example `scope=read:instagram`.
Combine scopes with spaces to allow multiple platforms. The consent page shows
these permissions before approval. Each endpoint tool also declares its
`requiredScope` in `_meta`.

| Scope            | Allowed data tools                 |
| ---------------- | ---------------------------------- |
| `read:instagram` | Instagram tools                    |
| `read:tiktok`    | TikTok tools                       |
| `read:twitter`   | X (Twitter) tools                  |
| `mcp`            | All platform tools; legacy default |

Discovery tools and synthetic catalog resources remain available to every
connection. No scope grants dashboard administration or social account writes.
API keys retain access to all tools. Use OAuth for platform restrictions.

A call outside the token's permissions returns HTTP `403` before metering.
Its `WWW-Authenticate` challenge declares `error="insufficient_scope"`, the
required `scope`, and the protected resource metadata URL.
Authorize the additional scope in the browser before retrying.

Refresh requests can narrow access-token permissions. Rotated refresh tokens
retain their original permissions, as required by OAuth. Reauthorizing a
connection never expands permissions on an existing token.

The [Openhandle MCP manifest](/mcp) uses the official MCP Registry
`server.json` format and declares the actual Streamable HTTP endpoint.
`GET /mcp` returns this JSON description without special request headers.
The same manifest remains available at `/mcp.json`.
JSON-RPC POST requests to the website URL redirect to the API endpoint;
configure the API URL directly to avoid redirects.

An unauthenticated request returns HTTP `401` and a `WWW-Authenticate` header
pointing to the protected resource metadata. This is expected. Verify a server
with an authenticated JSON-RPC `initialize` request using Streamable HTTP,
not by expecting a public JSON document from an unauthenticated website GET.
