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:
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:
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:
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:
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 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