Remote MCP
Let Claude, ChatGPT or another MCP client find, create and edit your grains, and run commands in app grains. Sign in with OAuth and choose a whole organization or one grain.
Grain runs a remote MCP server at https://api.rungrain.com/mcp. When you connect an MCP client to it, you choose what it can reach, either every grain in one organization or a single grain, and which permissions it gets. The client can then search and read your grains, create HTML grains, save changes, see and turn off share links, and run commands in app grains.
You don't need the Grain desktop app, the grain CLI or an API key: the client signs you in through your browser.
This is a different server from the local grain mcp server described in MCP Setup. The local server works on grains in folders on your Mac. The remote server works on them directly in Grain, so it runs from anywhere, including web chat clients.
Connect Claude Code
-
Add the server:
claude mcp add --transport http grain-remote https://api.rungrain.com/mcpThe name
grain-remotekeeps it apart fromgrain, the local server from MCP Setup. Connections added earlier under another name keep working. -
Start Claude Code, run
/mcp, selectgrain-remoteand choose Authenticate. A browser window opens. -
Sign in to Grain.
-
Under Access, choose one of these:
- All grains in an organization (the default). Pick the organization if you're in more than one.
- One existing grain.
- One new app workspace, which creates an empty app grain for the agent to work in.
-
Under Permissions, untick anything you don't want to allow. See Permissions.
-
Choose Approve. The browser returns you to Claude Code and the tools become available.
To check the connection, ask Claude to list your grains (search_grains).
Connect another MCP client
Any client that supports remote MCP servers over Streamable HTTP with OAuth can connect. Give it the server URL:
https://api.rungrain.com/mcpThe client finds everything else itself. An unauthenticated request gets a 401 pointing to https://api.rungrain.com/.well-known/oauth-protected-resource/mcp, which names Grain's authorization server and the scopes it supports. Its metadata is at https://api.rungrain.com/.well-known/oauth-authorization-server. Clients sign in with the authorization code flow and PKCE, on the same sign-in page described above. A client identifies itself in one of two ways:
- Client ID metadata document. The client's ID is an
httpsURL where it publishes its name and redirect URLs. Claude (Claude.ai, Claude Desktop and Claude Code), ChatGPT, VS Code and Codex do this. Grain fetches the document when you sign in, and the sign-in page shows the host it came from, for exampleclaude.ai. - Dynamic client registration. The client registers itself first. Cursor and older clients do this. A registration can list
httpsredirect URLs,http://localhostorhttp://127.0.0.1on any port, or an app link such ascursor://…. Grain drops any redirect URL it can't use and keeps the rest.
Clients whose documents are published by claude.ai, claude.com, chatgpt.com or vscode.dev show no badge. Every other client is marked Unverified app. The page always shows where you'll be sent back to, and warns you when that is an app on your computer. Only approve a connection you started yourself.
The server only accepts POST requests. Clients that only speak the older HTTP+SSE transport won't work. Browser-based clients can call it directly: it answers CORS preflight requests from any origin.
For client developers
A client can add these optional parameters to its authorization request to set what the sign-in page selects first. Each is at most 200 characters. They only preselect: the person can still change the choice, and Grain checks their access when they approve.
| Parameter | Effect |
|---|---|
organization_id_hint | Opens on All grains in an organization with this organization selected, if the person can build in it. |
workspace_id_hint | Opens on One existing grain with this grain selected, if the person can build in it. |
workspace_name_hint | Opens on One new app workspace with this name filled in. |
organization_id_hint takes precedence over the other two. A hint for an organization or grain the person can't build in, or an unknown id, is ignored without an error. A client that holds a one-grain connection can read its organization's id from workspace_info to hint it when asking for organization access.
For an organization connection, the token response includes organization_id and organization_name alongside the tokens, on the first exchange and on every refresh. organization_name is the organization's current name, and it can be missing, for example when the person is no longer a member. Treat a missing name as unknown. One-grain connections get neither field.
Permissions
Each permission is an OAuth scope. The sign-in page lists the ones the client asked for, all ticked. A client that asks for none, or only for scopes Grain doesn't know, is offered all four. You can untick any of them, but you can't add one the client didn't ask for.
| Scope | On the sign-in page | Tools |
|---|---|---|
grains:read | Find and read grains | search_grains, get_grain, list_shares |
grains:write | Create grains and save changes to them | create_grain, update_grain |
shares:write | Turn off share links | revoke_share, plus list_shares to find the link |
exec | Run commands and write files in app grains | run_command and write_file for an organization; exec and write_file for one grain |
connections:use | Use your organization's app connections (Gmail, Linear, GitHub…) | app_connections_list, app_connections_search_tools, app_connections_execute, app_connections_connect, for an organization only |
A connection lists only the tools its permissions allow (list_shares comes with either grains:read or shares:write). A one-grain connection also always has workspace_info, and it can't create grains. Every connection has send_feedback.
Tools
Every grain in a result has a grain_id and a url, an https://rungrain.com/w/… link that opens it in Grain. Tools that act on a grain take its grain_id. A one-grain connection can only use its own grain; any other id returns grain_not_found.
Find and read
search_grains: finds grains whose name or description contains every word ofquery, most recently updated first. Without a query it lists every grain. It returns up tolimitresults (1 to 50, default 20), thetotal, and anext_cursorto pass back ascursorfor the next page. Each result shows the first 200 characters of the grain's description;get_grainreturns all of it.get_grain: returns a grain's name, kind (htmlorapp), link, latest revision, and up to 100 of its files. It also returns the text of one file:path, orindex.htmlby default. A long file comes back in parts of about 24,000 characters; passnext_offsetasoffsetto read the next part. Binary files and files over 2 MiB come back without their content. App grains keep their files on their computer, so for themfilesisnull; read their files withrun_commandorexec.list_shares: lists the share links that are turned on for a grain, with each link'saudience.
Create and edit
create_grain: creates an HTML grain from a complete HTML document (html, up to 256 KiB, saved asindex.html), with anameand an optionaldescription. An optionalfoldersuch asReports/2026puts it in that folder, creating it if needed; otherwise it goes at the top of the organization. The grain is private to the organization until you share it. Each call creates a new grain.update_grain: writes up to 20 files (1 MiB each) into an HTML grain and saves them as a new revision. Listed files are created or replaced, and other files are kept. Earlier revisions stay in the grain's history. An optionaltitlesummarizes the change in that history. Withexpected_revision_id, the save is refused withremote_driftif the grain has changed since that revision. Saving content the grain already has records nothing and returnschanged: false. A path can't be both a file and a folder: savingindex.html/app.jswhenindex.htmlis a file, or the reverse, returnspath_conflict.
Share links
revoke_share: turns off a share link by itsshare_id. People who open it afterwards no longer see the grain. Turning off a link that is already off succeeds and changes nothing.
The remote server can't create or change share links. Publishing a grain makes it visible outside your organization, so it stays something a person does in Grain: an agent reading untrusted content could otherwise be tricked into publishing private work.
Feedback
send_feedback: sends feedback about Grain to the Grain team.sourceisuserwhen you asked the AI to send it, oragentwhen the AI files it on its own after repeated Grain failures;kindisissue,wishorpraise;textis up to 4,000 characters of prose. An optionalgrain_idsays which grain it's about. Grain removes anything that looks like a credential before storing it, and each person can send 10 an hour and 30 a day.send_feedbackneeds no scope: it only sends text to the Grain team and can't read or change your grains.
Use connected apps
With connections:use, an organization connection can use the app accounts (Gmail, Linear, GitHub, Slack…) that people connected in Grain Settings → App connections. Grain picks the account (yours first, else the only one; otherwise it asks), blocks writes on read-only accounts, and records every call in the organization's activity log.
app_connections_list: the connected accounts, whose each one is, and whether it is read-only.app_connections_search_tools: find a tool for an app and its inputs.app_connections_execute: run one tool with one account.account_requiredandneeds_connectioninclude aconnect_urlfor the user.app_connections_connect: a link for the user to connect their own account.
Connections approved before this scope existed don't have it; sign in again to add it.
Run commands in app grains
App grains have a Linux computer. HTML grains don't, and commands on them return runtime_required.
run_command(organization connections): runs a shell command on the computer of the app grain named bygrain_id.exec(one-grain connections): runs a shell command on the connection's grain.
Both run the command with bash -lc, as the node user.
| Input | Details |
|---|---|
command | Required. Up to 64 KiB. For anything longer, write a script to a file and run it. |
cwd | Optional. Absolute or relative, but it must be inside /project/workspace and must already exist. Defaults to /project/workspace. |
timeout_seconds | Optional. 1 to 600 (10 minutes). Defaults to 120. |
The result has exit_code, stdout, stderr, timed_out, truncated and duration_ms. stdout and stderr are each capped at 1 MiB, and truncated is true when output was cut. For large output, redirect it to a file and read it in parts.
A grain runs at most two commands at the same time, counting every connection to it. Long commands send progress updates every 20 seconds if the client asks for them.
write_file creates or replaces a UTF-8 text file on an app grain's computer. On an organization connection it writes to the app grain named by grain_id; on a one-grain connection, to the connection's grain. An HTML grain returns runtime_required.
| Input | Details |
|---|---|
grain_id | Organization connections only. Required. The app grain to write to, from search_grains. |
path | Required. Relative to /project/workspace. Absolute paths and .. are rejected. |
content | Required. The full file content, up to 8 MiB. |
create_dirs | Optional. Creates missing parent directories. Defaults to true. |
It returns the file's path and size in bytes.
workspace_info (one-grain connections) takes no input and returns workspace_id, name, organization_id, organization_name and url.
Everything an app grain's computer does happens in /project/workspace, which persists between sessions: the computer pauses when idle and resumes on the next call with its files and installed packages. If the grain has an app, its source is in /project/workspace/app. A workspace created during sign-in uses the empty template, a Vite-style runtime. See Runtime for how app grains work.
Connections made before scopes
Connections approved before organization access and scopes were added keep working exactly as before: they reach their one workspace with exec, write_file and workspace_info, and nothing else. To get the other tools, clear the client's authentication for the server (in Claude Code, /mcp → grain-remote) and sign in again.
Billing
Commands run on an app grain's computer, so usage is billed to the organization that owns the grain, the same as any other runtime use. If that organization can't run grains right now, commands return billing_blocked. Reading, creating, editing and sharing HTML grains doesn't use a computer.
Security
- A connection acts as you. You need builder access (the builder or owner role) in the organization, and Grain checks it on every request. If you lose that access, the connection stops working right away.
- An organization connection can reach every grain in that organization, including ones created after you approved it, but nothing in your other organizations.
- New grains are private to the organization, and a connection can't publish them. It can only see and turn off share links people created in Grain.
- Grain keeps a record of each connection: the app's name and client ID, what it reaches, who approved it, and when it was created and last used. Tokens are stored only as hashes.
- The client holds an access token that lasts one hour and a refresh token that lasts 30 days from its last use. Refresh tokens rotate each time they're used. You don't handle any of these yourself.
Disconnect
Open Connected apps and sign in. It lists every app you've connected, with the workspace or organization it reaches and when it was connected and last used. Choose Disconnect to end a connection. The app's next request fails with 401, and it can't refresh its tokens.
You can also remove the server from your client. In Claude Code:
claude mcp remove grain-remoteThat stops your client from using the connection, but the connection stays valid until the client revokes its tokens or you disconnect it in Connected apps. A refresh token that isn't used expires 30 days after it was last used. Removing someone's builder access cuts off their connections immediately, and deleting a grain cuts off connections limited to it.
To change what a connection reaches, clear the client's authentication for grain-remote and sign in again.
Troubleshooting
HTTP errors come back on the request. Tool errors come back as a failed tool result. exec, write_file, run_command and workspace_info put the code in error_code; the other tools start the error text with it, for example [grain_not_found] ….
| Error | What it means | What to do |
|---|---|---|
401 invalid_token | The token is missing, expired or revoked (including from Connected apps), and the client couldn't refresh it. | Authenticate again from the client. In Claude Code, run /mcp and re-authenticate grain-remote. |
403 access_denied | You no longer have builder access to the organization or grain. | Ask an owner of the organization for access, or connect with an organization you can build in. |
429 rate_limited | More than 300 requests in a minute on this connection. | Wait a minute and retry. |
grain_not_found | The grain doesn't exist, belongs to another organization, or you can't build in it. | Use search_grains to find the right grain_id. |
remote_drift | The grain changed since expected_revision_id, or while saving. | Read it again with get_grain, then save again. |
app_grain | update_grain was used on an app grain, whose files live on its computer. | Change its files with write_file, exec or run_command. |
file_not_found | get_grain was asked for a file the grain doesn't have. | Pick a path from files. |
share_not_found | The grain has no share link with this share_id. | Use list_shares to see the grain's links. |
path_conflict | A save would make one path both a file and a folder. | Pick a path that doesn't sit inside an existing file, or next to a folder of the same name. |
runtime_required | The grain is an HTML grain with no computer. | Run commands on an app grain instead. |
billing_blocked | The organization that owns the grain can't run grains right now. | Check the organization's billing in Grain. |
invalid_cwd | cwd is outside /project/workspace or doesn't exist. | Use a directory inside /project/workspace, and create it first if needed. |
too_many_concurrent_execs | Two commands are already running on this grain. | Wait for one to finish, then retry. |
command_too_large | The command is over 64 KiB. | Write it to a script file and run the script. |
file_too_large | A file is over the tool's limit. | Split the file, or generate it with a command. |
parent_not_found | create_dirs is false and the parent directory doesn't exist. | Create the directory, or leave create_dirs unset. |
workspace_unavailable | Grain couldn't reach the grain's computer. | Try again shortly. |
grain_unavailable | Grain couldn't complete the request. | Try again shortly. |
If create_grain saves the grain but not its HTML, the error includes the new grain_id. Save the HTML with update_grain instead of creating another grain.
If the sign-in page says Grain couldn't use this app's client metadata, Grain couldn't fetch or read the client's metadata document, or the document doesn't list the redirect URL the client used. Try again. If it keeps happening, the problem is in the client.
If the sign-in page says You need builder access to a Grain organization to connect, your account isn't a builder or owner in any organization. Ask an owner to change your role, or sign in with a different account using Switch account.