Key takeaways
- Every environment of an app is its own MCP server, at /v1/apps/<app>/environments/<env>/mcp, with tools for its API reference, records, aggregates, actions, events and stored files, and on development the app's files and schemas too.
- Claude Code connects with one command and signs you in through the browser, so no key goes into the client's config. CI can send a key as a header instead.
- Writes go through the model's own create, update and delete actions, exactly as the REST API's do, and every call is logged as an mcp event.
- Production's server changes the live app's data. Members without Edit, and members on production, get the reading tools only.
A coding agent working on an app can read its code, but not its data. It can't see that a third of the reviews are stuck in pending, that the approve action failed twice this morning, or what a review record actually holds. So it guesses, or you paste records into the chat.
Apps closes that gap with an MCP server per environment. Your agent gets the environment's API reference, records, actions and event log as tools, and works against the same data your app does. Development's server also carries the app's files and schemas, so an agent with no checkout can build the app as well as use it.
One server per environment
The server for an environment is at:
https://api.inferencebureau.com/v1/apps/<app>/environments/<env>/mcp
It speaks Streamable HTTP with no sessions. Development and production are separate servers, because they're separate environments with their own records, files and logs. Name them so you can tell them apart. The dashboard's convention is <app>-dev and <app>-prod.
The app's Settings → API keys page shows the command with the app filled in. For an app called reviews, in Claude Code:
claude mcp add --transport http reviews-dev https://api.inferencebureau.com/v1/apps/reviews/environments/development/mcp
# then /mcp in Claude Code and choose Authenticate
Clients that read an mcp.json take the same server as JSON:
{
"mcpServers": {
"reviews-dev": {
"type": "http",
"url": "https://api.inferencebureau.com/v1/apps/reviews/environments/development/mcp"
}
}
}
Signing in without a key
Neither config holds a key. A request with no credentials gets a 401 carrying standard OAuth metadata, and the client takes it from there: your browser opens the dashboard, you approve and pick the team, and the client gets a session that acts as you. Only clients on your own computer are accepted, since the redirect has to be a loopback address.
For CI, or anything without a browser, send a header instead:
claude mcp add --transport http reviews-dev https://api.inferencebureau.com/v1/apps/reviews/environments/development/mcp \
--header "Authorization: Bearer $IB_API_KEY"
The server takes the same credentials as the records API: a key linked to the app, a key with Apps Permissions, or the environment's own token. The environment token reaches only that one environment, so it suits an agent the app itself runs.
The tools
Tool descriptions are written from the environment's schemas each time the client connects. The agent sees your real model names, field names and vector fields, not a generic "model" argument.
Reading:
get_reference: the environment's API reference as Markdown, whole or for one model: how to call it, every field with its type, filters and description, who may read each model, and every action with its params. Agents are told to start here.list_modelsandlist_actions: the same as data. Each model's fields, with types, descriptions, required, the filters each takes, enum options and defaults; each action's params as JSON Schema, which roles may call it, and the webhook or schedule that queues it.find_records: a page of records, withsearch(words in text fields),filter,sort,selectfor fields and related records, and paging byafterorskip.get_record: one record by id.count_recordsandaggregate_records: how many match, and counts, sums, averages, minimums and maximums in groups or date buckets.search_records: records nearest to some text by meaning, on a vector field. A text query is embedded with the field's model, billed as an ordinary embedding.list_events: the newest event log entries, optionally of one type or only the failures.list_stored_files: the files the app's code wrote to storage.
Changing data:
create_record(one record, or a list of up to 250),update_recordanddelete_record.run_action: a model action, withidfor one that works on a record, or a global action. A Shopify app can passshopfor the action's Shopify client.
Building the app, on development's server:
list_files,read_fileandsearch_files: the app's code. Production's server has these three, since its files change only by a deploy.write_file,edit_fileanddelete_file: a change goes the way a dashboard save does, so the generated client types follow and the dev server hot-reloads it. Each write names the copy it read, and a file changed meanwhile comes back as a conflict instead of being overwritten.get_problems: the type errors from the dev server's last check.create_model,add_field,change_field,create_actionandgrant_role: a well-formed schema, action or permissions file from a description, with storage keys set and kept on a rename. Each answers with the reference entry it produced and what a schema change does to the records that exist.list_env_vars, and for a team owner, admin or a key with Apps Permissions,set_env_varanddeploy.
Working through an example
Take a product reviews app with a review model: product, rating, body, status (pending, approved or rejected) and a few moderation fields. You ask your agent why the storefront shows so few reviews.
It calls get_reference, sees the status enum, then counts the pending ones and finds the low-rated ones:
{
"model": "review",
"filter": { "status": { "equals": "pending" }, "rating": { "lessThanOrEqual": 2 } },
"select": { "rating": true, "body": true, "product": { "select": { "title": true } } },
"first": 20
}
To see which products are affected, it asks for an aggregate rather than reading every record:
{
"model": "review",
"filter": { "status": { "equals": "pending" } },
"aggregates": { "reviews": { "count": true }, "rating": { "avg": "rating" } },
"groupBy": ["product"],
"sort": { "reviews": "Descending" }
}
Then it checks list_events with type: "action" and failed: true, and finds the approve action's onSuccess throwing while it publishes reviews to the product's metafields. It fixes the action file with edit_file (or in a checkout kept in sync by ib-apps dev), checks get_problems, and runs the action on one record to confirm:
{ "model": "review", "action": "approve", "id": "rec_…", "params": { "moderatedBy": "agent" } }
The answer has the record as the action left it, and the run's event id, so the agent can look the run up in list_events. If development's dev server is asleep, the run wakes it and waits up to 150 seconds instead of failing.
Writes go through your actions
A write from the MCP server is the same write the REST API makes. update_record on a review runs api/models/review/actions/update.ts when the model has one, and writes the fields directly when it doesn't. The same goes for create and delete. Your validations, onSuccess hooks and side effects all run, so an agent can't save a record your own code would refuse.
When a write is refused, the tool error lists each field's problem. When an action throws, the error carries its message, its event id and its logs, so the agent can fix the cause and try again. A run that throws changes nothing.
Every call is logged as an mcp event in the environment's Logs, with its arguments (secret fields redacted), its duration and whether it failed. Writes and runs also log their own action events under it, so you can see later exactly what an agent did.
Production is writable, so choose who connects
Production's server isn't read-only. A key, or a team owner or admin, can create, update and delete production records and run production actions through it. That's useful for an assistant that works with live data, and it's also why the dashboard warns that production's writes change the live app's data.
Members are narrower. The server checks the same Edit rule as the API:
- A member with Edit on the app gets every tool on development, and only the reading tools on production.
- A member with view access gets the reading tools on both.
The writing tools aren't listed for them at all, so the agent never tries a call that would be refused.
For building and testing, connect development. Connect production when you want an agent to read live data, and give a key only what it needs.
Your agent also knows the layout
The reference tells an agent about the data. The guides tell it how the app is built. Every new app starts with an AGENTS.md at the root, a CLAUDE.md that imports it for Claude Code, and skills under .agents/skills/: actions, models and schemas, records, background jobs, testing, debugging and security, plus Shopify or web skills for the platform. They sync like any other file and are never part of the build.
An agent with a checkout reads them from disk. An agent on the MCP server alone is told, when it connects, to read AGENTS.md with read_file and then the guide for the area it's about to work in, so it follows the same rules either way: records change through actions, a field's storage key never changes, every members-only route checks who is asking. The one thing it can't do from the server is run the app's tests; those run from a checkout with ib-apps test.
Frequently asked questions
Does using the MCP server cost anything?
Each tool call counts as one request toward the app's plan, like an API call. search_records with a text query, and writes to models with sourced vector fields, also embed text, billed at the gateway's price to the caller's key or the team's Apps key.
Can I make production's server read-only?
Connect as someone who can't change production: a member's session gets only the reading tools there. Keys and team owners or admins get every data tool; the file and schema tools write on development only, and production's server lists just the reading ones.
Which clients work?
Any MCP client that speaks Streamable HTTP. Claude Code connects with claude mcp add and signs in with /mcp. Other clients take the server as JSON, or a key in an Authorization header.
Can one server reach both development and production?
No. Each server is one environment: it describes that environment's models and reaches only its records, actions and logs. Add one server for each environment you want the agent to see.