Requirements
- Streamable HTTP, at your deployment’s origin plus
/mcp. The endpoint answersPOST,GETandDELETEand keeps sessions byMcp-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: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:/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 OAuth path
A client that implements the MCP authorization specification (2025-06-18 or later) connects with the URL alone. In order:- It calls
/mcpwithout a token and is refused with401andWWW-Authenticate: Bearer resource_metadata="https://app.getgraft.ai/.well-known/oauth-protected-resource/mcp". - It reads that document (RFC 9728), which names
https://app.getgraft.ai/mcpas the resource andhttps://app.getgraft.aias its authorization server. The document is also served without the/mcpsuffix. - It reads
https://app.getgraft.ai/.well-known/oauth-authorization-server(RFC 8414), which names the four endpoints below and sayscodewith PKCES256only. - It registers itself at
POST /mcp/oauth/register(RFC 7591), sending itsclient_nameandredirect_uris. Registration is open; the answer is aclient_idand, unless the client asked fortoken_endpoint_auth_method: none, aclient_secretshown once. - It opens
GET /mcp/oauth/authorizein the person’s browser withresponse_type=code, theclient_id, a registeredredirect_uri, acode_challengewithcode_challenge_method=S256,resource=https://app.getgraft.ai/mcp, and astate. Graft sends the browser to the console’s consent page, through sign-in if there is no session. - The person consents and names the agent; the browser returns to the redirect URI with
code,stateandiss. - The client posts the code to
POST /mcp/oauth/tokenas a form body withgrant_type=authorization_code,code_verifierandredirect_uri, authenticating as it registered to: nothing beyondclient_idfor a public client, orclient_secret_basicorclient_secret_postfor a confidential one. It receives an access token,expires_in: 3600, and a refresh token. - It sends the access token as the bearer on every MCP request. When it expires,
grant_type=refresh_tokenat 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. POST /mcp/oauth/revoke(RFC 7009) with a refresh token revokes the grant; with an access token, that token alone.
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 a401 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. Theinitialize 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_toolfirst when a task has no tool;promotea demoted match rather than authoring again;request_connectionwhen the vendor has no connection in the agent’s scope;acquireonly when nothing fits, thenacquire_status, relaying each new progress line to you in one sentence. Never a secondacquirefor the same goal, and never the authoring tools unless you asked the agent to author by hand. - Handoffs. Any answer with a
urland anawaiting_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 handlestools/list_changedrefreshes 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.
acquireasks once per agent per connection.
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.mdbeside 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. Therequest_connectiondescription 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_serversentry, the token in~/.hermes/.env, and themcp_graft_prefix Hermes gives every Graft tool.
Optional: re-fetching the tool list
Graft advertisestools.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 itsinitialize 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, withstructuredContent 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.