No mobile developer would ship database credentials inside an APK. Yet that's roughly what we do when we hand an AI agent a shell, a filesystem and a connection string, and ask it to be careful.
MCP, the Model Context Protocol, gives us a better option. It's an open standard for exposing tools to a model through a server, and mobile developers already know the pattern behind it: it's an API.
Access versus tools
Before
Access
- The agent gets a shell and a working directory
- It can read anything the process can read
- Safety lives in the prompt: "please don't"
- You find out what it touched afterwards
After
Tools
- The agent gets a short list of named, typed calls
- Each call returns one known shape of data
- Safety lives in the server: it refuses
- Every call is a visible step you can log
Your app never queries the production database directly. It calls an endpoint, the backend checks who's asking, and it returns only what that user may see. An MCP server is the same idea, with a model as the client.
If you've built a mobile backend, you've already made every decision MCP asks of you. You just made them for a different client.
A working directory is not a cage
I learned this one in my own system. When I built the AI analysis of my journal, a review caught a bad assumption: that an agent started inside my notes folder, with edits allowed, would stay inside that folder.
Nothing makes it. A working directory is where a process starts, not a wall around it. The permissions you grant are the real boundary, and edit rights for a job that only needed to read were far too much.
The fix was the pattern this article is about. The agent became read-only: it analyses an entry and returns structured JSON. Plain code validates that JSON and does every write itself, with append-only history enforced in code, not requested in a prompt.
Three rules for agent tools
- 1Typed reads, not raw access
get_order(id),list_open_tasks(due_before),search_notes(query). Each with a schema, each returning only the fields it promises, each answer saying where it came from. Notread_file(path), and neverrun_sql(query). - 2One tool list per agentThe support agent gets order lookups. The analytics agent gets aggregates. Neither gets the other's tools. Keep the lists in config, one per agent, where a reviewer can see them.
- 3The server refuses, not the promptSome data must never be served to any agent: card details, health records, private messages. That rule lives inside the server, as code with a test. A prompt is a request the model can miss. A refusal in the server can't be missed.
agent: support
get_order(order_id) -> status, items, delivery date
list_orders(since) -> id, status, total
refund_eligibility(order_id) -> yes or no, and the rule that decided it
agent: analytics
orders_per_day(from, to) -> counts only, no customer fields
never served, to any agent:
card details, health data, private messages, auth tokensNotice what's missing from that list: read_file, run_sql, shell. A generic tool is just access with extra steps, and it puts you right back to trusting the prompt.
How this maps to your app's backend
- Tools are endpoints. Same auth, same scoping, same rate limits. If your API already answers "what may this user see?", the MCP server should call that code, not go around it.
- Tool lists are scopes. Think of them like OAuth scopes per agent. An agent without a scope doesn't get a softer answer; it gets no tool at all.
- Row-level security still applies. My own app's cloud data is locked to one account in the database itself, so even a buggy client can't read more than it should. Agents deserve the same floor.
- Writes are a different door. Start read-only. When an agent needs to change something, have it return a structured request that your code validates, and keep a rule or a human on the decision.
None of this is exotic. It's the discipline you'd apply to any third-party integration you didn't fully trust, which is exactly what an agent is.
And the phone itself? Treat it as the least trusted surface in the system. My own phone chat has no tools at all: it sends each question through a server function that holds the model key, and the app never sees that key.
- MeShow me the card used on my last order.
- AgentI can't. None of my tools return payment details, so I can't see them. I can show you the order's status, or check whether it's eligible for a refund.
- MeCheck the refund.
- AgentIt's eligible.
refund_eligibilitysays the order is still inside the return window.
That's the behaviour you want: a plain no, the reason, and the useful thing it can do instead. You only get it reliably when the refusal lives in the server, because then there is nothing for the model to talk its way around.
Give an agent access and you're trusting the prompt. Give it tools and you're trusting your code.
The rule I design agent features around
Mobile developers spent a decade learning never to trust the client. An agent is just a new client. Give it an API.
Building with AI?Let's talk about your appBook a free intro call


