# Any MCP client

> What a client needs to speak to Graft: streamable HTTP and one of two ways to say which agent it is, a static bearer header or MCP OAuth; what its model is told in the handshake when no skill is installed; and what the Hermes skill adds on top.

Source: https://docs.getgraft.ai/graft/harnesses/any-mcp-client

Graft is an ordinary MCP server. Any client that speaks streamable HTTP works, and there are two ways for it to say which agent it is. Hermes and OpenClaw carry a static bearer header from a config file; Claude.ai and ChatGPT discover Graft's sign-in from the endpoint and connect over OAuth. Both end at exactly one agent per connection. A client that installs nothing else still gets the playbook, because the handshake carries it.

## Requirements

- **Streamable HTTP**, at your deployment's origin plus `/mcp`. The endpoint answers `POST`, `GET` and `DELETE` and keeps sessions by `Mcp-Session-Id`, as the transport specifies.
- **An `Authorization: Bearer <token>` header on every request**, checked before any MCP handshake. The token is either the agent's static token, as the console showed it once, or the access token the OAuth flow below issued.

## The static bearer path

Create an agent in the console, copy its token once, and send it as a fixed header. The console shows the generic block:

```json
{
  "mcpServers": {
    "graft": {
      "type": "http",
      "url": "https://app.getgraft.ai/mcp",
      "headers": { "Authorization": "Bearer ${GRAFT_TOKEN}" }
    }
  }
}
```

This is the path for a client on a server with a config file and no browser to send anyone to. Nothing about it changed when the OAuth path arrived.

## Two clients that take the OAuth path from a terminal

**Claude Code** adds Graft as a user-scoped HTTP server and authenticates from its interactive session:

```bash
claude mcp add --transport http --scope user graft https://app.getgraft.ai/mcp
```

then `/mcp` in a Claude Code session, select **graft** and authenticate; Graft's consent page opens in the browser. Leave client id and secret unset: Graft registers the client itself.

**Codex** (app, CLI or IDE) is the same shape:

```bash
codex mcp add graft --url https://app.getgraft.ai/mcp
codex mcp login graft
```

The login opens Graft's consent page; keep **A new agent** and choose **Connect**. Both then list Graft's tools in a fresh session.

## The OAuth path

A client that implements the MCP authorization specification (2025-06-18 or later) connects with the URL alone. In order:

1. It calls `/mcp` without a token and is refused with `401` and `WWW-Authenticate: Bearer resource_metadata="https://app.getgraft.ai/.well-known/oauth-protected-resource/mcp"`.
2. It reads that document (RFC 9728), which names `https://app.getgraft.ai/mcp` as the resource and `https://app.getgraft.ai` as its authorization server. The document is also served without the `/mcp` suffix.
3. It reads `https://app.getgraft.ai/.well-known/oauth-authorization-server` (RFC 8414), which names the four endpoints below and says `code` with PKCE `S256` only.
4. It registers itself at `POST /mcp/oauth/register` (RFC 7591), sending its `client_name` and `redirect_uris`. Registration is open; the answer is a `client_id` and, unless the client asked for `token_endpoint_auth_method: none`, a `client_secret` shown once.
5. It opens `GET /mcp/oauth/authorize` in the person's browser with `response_type=code`, the `client_id`, a registered `redirect_uri`, a `code_challenge` with `code_challenge_method=S256`, `resource=https://app.getgraft.ai/mcp`, and a `state`. Graft sends the browser to the console's consent page, through sign-in if there is no session.
6. The person consents and names the agent; the browser returns to the redirect URI with `code`, `state` and `iss`.
7. The client posts the code to `POST /mcp/oauth/token` as a form body with `grant_type=authorization_code`, `code_verifier` and `redirect_uri`, authenticating as it registered to: nothing beyond `client_id` for a public client, or `client_secret_basic` or `client_secret_post` for a confidential one. It receives an access token, `expires_in: 3600`, and a refresh token.
8. It sends the access token as the bearer on every MCP request. When it expires, `grant_type=refresh_token` at the same endpoint issues a new pair and retires the old refresh token. Presenting the old one again within thirty seconds answers the same new pair, so a client that lost the response to a successful refresh recovers by retrying; past thirty seconds the same presentation revokes the whole grant.
9. `POST /mcp/oauth/revoke` (RFC 7009) with a refresh token revokes the grant; with an access token, that token alone.

The four endpoints and the two documents are all Graft implements of OAuth. There is no OpenID Connect, no `client_credentials`, no introspection; a client that needs one of those cannot connect this way and uses a static token instead.

The token's subject is the agent the consent created or named, not the person. Revoking that agent in the console ends the grant: the next call and the next refresh are both refused, and a client built on the MCP TypeScript SDK starts a fresh authorization, which is how "connect again" reaches the person.

## What Graft answers when the token is wrong

A refused request is a `401` with a JSON body in the same shape the proxy uses, and the `WWW-Authenticate` header above:

```json
{ "error": "unauthorized", "reason": "token_missing", "message": "…" }
```

`reason` is `token_missing` when no bearer header arrived, `token_unknown` when the token does not resolve to an agent or the agent was revoked, `token_expired` when an OAuth access token has run out and the refresh token is the way back, and `session_mismatch` when a request carries a session id opened by another agent's token. A session id is not a credential. When a token was presented, the header also carries `error="invalid_token"`.

## What a client without a skill receives

Nothing about Graft has to be installed in the client for its model to use Graft well. The `initialize` result carries an `instructions` field, and every tool's description opens with when to call it and what to do with its answer. Between them they carry the playbook the Hermes skill spells out at length, so a client that shows its model the server's instructions, as Claude Code does, behaves like a Hermes agent with the skill installed: it searches before it authors, it hands you a link rather than asking for a key, and it polls `acquire_status` instead of calling `acquire` twice.

The instructions run to under 1,800 characters, because some clients truncate the field, and say five things:

- **The order of operations.** `find_tool` first when a task has no tool; `promote` a demoted match rather than authoring again; `request_connection` when the vendor has no connection in the agent's scope; `acquire` only when nothing fits, then `acquire_status`, relaying each new progress line to you in one sentence. Never a second `acquire` for the same goal, and never the authoring tools unless you asked the agent to author by hand.
- **Handoffs.** Any answer with a `url` and an `awaiting_` word is yours to finish in the console. The agent gives you the link exactly as returned, says what it is for and waits, then calls the same tool again with the same arguments once you say it is done.
- **Secrets.** The agent never asks you for an API key, a password or a token in chat, whatever the vendor calls it. A secret goes to Graft on its own form; the agent never sees one.
- **A snapshotted list.** Some clients read the tool list once per conversation, so a tool just promoted or acquired may be missing from it. `run_tool { vendor, name, input }` calls it by name until the list refreshes, and a client that handles `tools/list_changed` refreshes on its own.
- **Approvals.** A read-only tool never asks. Any other tool asks once and the answer holds, a destructive tool too; you can set a tool to ask every time. `acquire` asks once per agent per connection.

The descriptions carry the long form. `acquire`'s says what Graft's model will do and that the first call against a connection may answer `awaiting_approval`; `acquire_status`'s says how often to poll and what to do on `succeeded` and `failed`; `request_connection`'s carries the scheme table and the OAuth proposal; `request_credential`'s says when a vendor's `401` means a re-entry; and the eight authoring tools each open with the sentence that they are the loop `acquire` runs for you, to be called only when you asked the agent to author a tool by hand.

The instructions and the skill are held to the same sentences by a test in the open repository, `packages/mcp/src/session.test.ts`, so the two cannot disagree on the order of operations, the approval rule or where a secret goes.

## What the Hermes skill adds

`skills/hermes-graft/SKILL.md` says the same things at more length, in the format Hermes loads, and adds what a handshake cannot carry:

- **The OAuth guide.** `connecting-with-oauth.md` beside it walks the agent through a vendor that issues no API key: where to create the client in the vendor's console, what to name it, which redirect URI to paste, and Google's Testing-mode expiry. The `request_connection` description carries the proposal; the skill carries the worked example.
- **What the approval card means.** Hermes renders Graft's asks as its own Allow and Deny buttons, and the skill tells the agent what each records, so it can explain a card that came back.
- **The install.** The `mcp_servers` entry, the token in `~/.hermes/.env`, and the `mcp_graft_` prefix Hermes gives every Graft tool.

For any other client, install nothing: the handshake is enough. If your client can load a skill in the agentskills format, the skill is portable and only the install location is Hermes's. See [Hermes](https://docs.getgraft.ai/graft/harnesses/hermes.md) for the install.

## Optional: re-fetching the tool list

Graft advertises `tools.listChanged: true` and sends `notifications/tools/list_changed` whenever an agent's working set changes. Handling it lazily is fine: OpenClaw re-lists at its next turn and works. A client that never re-fetches still works too, through two meta-tools that are always in the list: `find_tool` says what exists and whether it is promoted, and `run_tool { vendor, name, input }` runs any tool in the toolbox by name.

## Optional: elicitation for approvals

If your client advertises form-mode elicitation in its `initialize` capabilities, Graft asks for approvals in place with a form of two booleans on a tool ask, `allow` and `askEveryCall` ("Ask every time for this tool"), and `allow` alone on a build ask. An `accept` is a yes unless its content says `allow: false`; `askEveryCall` sets the tool's per-call opt-in when present and leaves it alone when absent; a `decline` is a no; a `cancel` records nothing and falls back to the console link, as a failed elicitation does. That is what a client gets when it advertises forms it cannot show in the mode it is running in — Claude Code's non-interactive `-p` mode cancels every form — so the person still receives a link to answer from.

If your client advertises nothing, every approval arrives as a result with `error: "awaiting_approval"` and a `url` for the person to open. Secrets are never requested through elicitation on any client; a connection's credential always goes through the console.

## Result shapes

Every tool answers a JSON text block, with `structuredContent` beside it for objects. A refusal sets `isError` and carries `{ error, reason, message }`, where `reason` is one word your code can branch on: `tool_denied`, `approval_declined`, `awaiting_approval`, `connection_not_in_scope`, `tool_not_found` and so on. A vendor's own answer through an authored tool comes back verbatim.

Tool names fit MCP's recommended `^[a-zA-Z0-9_-]{1,128}$`. Authored tools are `<vendor>__<name>`, execute tools `execute__<connection id>`, and meta-tools carry a single underscore.

## Try it

```text
What is my public IP according to httpbin?
```

```text
Find a tool that lists my open pull requests, and if there is none, acquire one.
```

```text
Run the tool by name; my tool list has not refreshed yet.
```
