Atrio MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Atrio MCP ServerWhat are my unit's overdue fees and latest payments?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
A read-only MCP server over Atrio
A sample from Atrio, a multi-tenant SaaS I built for managing residential buildings in Panama (accounting, payroll, resident portal). It shows how I connect an AI agent to a business system: through the Model Context Protocol, with the database, not the prompt, deciding what the agent can see.
Any MCP client (Claude Desktop, Claude Code, Cursor and others) can plug into it and answer questions like "Who's behind on fees this month?" or "What's booked in the party room next week?" for the person who's signed in, and only for them.
The three tools
Tool | Answers | Who can use it |
| Balance of one unit: charged, paid, overdue, latest charges and payments | Staff: any unit in their building. A resident: their own unit |
| Units with an overdue balance at the end of a month, largest first, with the total | Staff only |
| Bookings of common areas for the next N days | Everyone in the building. Residents don't see which unit booked |
All three are marked readOnlyHint, so clients can call them without asking for confirmation.
Related MCP server: AgenticMCP
How the agent is kept in its lane
The model can be talked into anything, so the rules don't live in the model or in the tool code. Each tool call runs inside the database like this:
Read-only transaction.
begin read only. Anupdatefails even if a bug or an injected prompt produces one.A role with no write grants.
set local role atrio_agent. The role can onlyselect, and it is subject to Row Level Security.The caller's identity.
set_config('app.user_id', …, true), scoped to the transaction. In production this is the verified JWT (auth.uid()in Supabase).Row Level Security on every table. The policies use that identity. Staff see their whole building, a resident sees their own unit, and nobody sees another building. With no identity, every table looks empty.
When a resident asks for a neighbour's unit, the answer is the same as for a unit that doesn't exist: No unit "A-102" is visible to you. That way the answer doesn't reveal whether the unit exists.
The tool layer adds one check of its own (arrears is staff-only) and input validation with Zod. Even if a tool's SQL were wrong, it still couldn't return rows the policies hide.
Run the tests
You need Node.js 22.18+ and nothing else. The tests use PGlite, real PostgreSQL compiled to WebAssembly, so RLS and roles behave as in production.
npm install
npm test
# ℹ tests 15
# ℹ pass 15The tests talk to the server over the MCP protocol as three different users (a Torre Coral admin, a Torre Coral resident and an admin of another building). They check the numbers, the isolation between users and buildings, and that the agent role can't write even with hand-written SQL.
Try it in Claude
With no DATABASE_URL the server starts with the fictional demo data in memory.
Claude Code
claude mcp add atrio -- node /path/to/atrio-mcp-sample/src/main.tsClaude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"atrio": {
"command": "node",
"args": ["/path/to/atrio-mcp-sample/src/main.ts"],
"env": { "ATRIO_USER_ID": "aaaaaaaa-0000-0000-0000-000000000001" }
}
}
}Switch ATRIO_USER_ID to see the same questions from different people:
User | Who |
| Admin, Torre Coral |
| Resident of A-101, Torre Coral |
| Admin, PH Mirador del Mar |
Against a real PostgreSQL
psql "$DATABASE_URL" -f sql/schema.sql -f sql/seed.sql
psql "$DATABASE_URL" -c "create role atrio_mcp login password '…'; grant atrio_agent to atrio_mcp;"
DATABASE_URL=postgres://atrio_mcp:…@host/db ATRIO_USER_ID=… npm startThe server's login role only needs to be able to set role atrio_agent. It never reads a table with its own privileges.
What changes in production
Transport. It runs over Streamable HTTP instead of stdio, and the user id comes from the client's OAuth token rather than an environment variable.
Schema. It's the full Atrio schema. The in-app assistant uses twelve read-only tools under the same model: finances, maintenance, packages, visitor passes, votes and more.
Writes. Anything that writes, such as a payment reminder or a booking, would be a separate tool that drafts the action for a person to approve.
Files
File | Content |
| Tables, the |
| Two fictional buildings, three users, dates relative to today |
| Runs each call as the user: read-only, |
| The three MCP tools |
| stdio entry point |
| 15 tests over the MCP protocol |
All names and figures are fictional.
Available Tools
3 toolsarrearsArrears for a monthARead-only
Units with an overdue balance at the end of a month (charges due by then minus payments made by then), largest first, with the building total. Staff only. For the current month the cut-off is today.
| Name | Required | Description | Default |
|---|---|---|---|
| month | No | Month as "YYYY-MM". Omit for the current month. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare a safe read (readOnlyHint=true, destructiveHint=false, openWorldHint=false), so the bar is lower. The description still adds meaningful behavior: an authorization requirement ('Staff only'), the ordering guarantee, inclusion of a building total row, and the today-based cut-off for the current month.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the core definition, then audience, then the default-month edge case. No filler and every clause carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description does the work of describing the return set (ordered units plus a building total), which is the main thing an agent needs. It leaves the specific fields of each row implicit, but the inclusion of the authorization and cut-off details makes it comfortably complete for a one-parameter read tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There is a single optional parameter and schema description coverage is 100%, so the schema already documents the YYYY-MM format and the omit-for-current-month default. The description only reinforces the current-month semantics (cut-off is today), which is a small addition beyond the schema; baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific resource (units with an overdue balance at month end) and even defines the metric precisely as charges due minus payments made. The 'largest first, with the building total' clause pins down what it returns, which clearly separates it from the sibling unit_balance (single unit) and upcoming_bookings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives real usage context: 'Staff only' constrains the caller, and 'For the current month the cut-off is today' tells the agent what happens when no month is passed. It does not explicitly route between this and unit_balance, but the scoping sentence makes the distinction inferable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unit_balanceUnit balanceARead-only
Account balance of one unit in the building, by unit code (e.g. 'A-102'): total charged, total paid, balance, the part already overdue, and the latest charges and payments.
| Name | Required | Description | Default |
|---|---|---|---|
| unit_code | Yes | Unit code, e.g. A-102 |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, destructiveHint=false and openWorldHint=false, so safety is covered. The description adds genuinely useful behavioral context beyond that by disclosing the returned breakdown (total charged, total paid, balance, overdue portion, latest charges and payments), which matters because no output schema exists.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One front-loaded sentence that names the resource first and then lists the returned fields; no filler, no redundancy, nothing that could be cut without losing information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter read tool with no output schema, the description covers purpose and return contents adequately, and annotations cover the safety profile. It does not say what happens for an unknown unit code or whether results are scoped to the caller's building context, which are minor gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% for the single parameter, and the description repeats the schema's own example ('A-102') without adding format rules, failure behavior, or lookup semantics. Baseline 3 is appropriate when the schema already documents the parameter fully.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource: the account balance of one unit, keyed by unit code, and enumerates the components (charged, paid, balance, overdue, latest activity). It is clear enough to separate from siblings like arrears, but it never names or contrasts them explicitly, so differentiation is left to inference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The scope ("one unit in the building, by unit code") implies this is the single-unit lookup, but there is no explicit when-to-use statement, no guidance on how it differs from arrears, and no exclusion conditions. Usage must be inferred from the phrase "one unit".
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
upcoming_bookingsUpcoming bookingsARead-only
Confirmed and pending bookings of common areas (party room, BBQ, pool) from today through the next N days.
| Name | Required | Description | Default |
|---|---|---|---|
| days | No | How many days ahead. Default 30. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare this as a safe, read-only, non-destructive, non-open-world operation. The description adds useful behavioral context beyond annotations by specifying that both confirmed and pending bookings are included, that only common areas are covered, and that the window starts today. It does not discuss return format, pagination, or authorization requirements.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, front-loaded sentence with no filler. It states the key scope, statuses, area examples, and time range immediately.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple, read-only list tool with one optional parameter, full schema coverage, rich annotations, and no output schema, the description is complete enough. It tells the agent what bookings are returned and over what time horizon, and it need not explain return values because no output schema exists.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the single 'days' parameter is already fully documented in the schema with its default, minimum, and maximum. The description mentions 'next N days', but adds no syntax or meaning beyond what the schema provides, making the baseline 3 appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the resource (bookings), scope (common areas such as party room, BBQ, pool), statuses (confirmed and pending), and time window (today through the next N days). It does not explicitly name or distinguish itself from the sibling tools unit_balance and arrears, though its domain is obviously different.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the scope: retrieve upcoming bookings over a forward-looking window. There is no explicit when-to-use guidance, no when-not-to-use guidance, and no reference to alternatives such as past bookings or other booking-related queries.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
arrears - First observed
unit_balance - First observed
upcoming_bookings
TDQS
Scored across 3 tools
Each tool targets a clearly distinct question: one unit's balance, the building's arrears list, and upcoming common-area bookings. There is no meaningful overlap in purpose or output.
All names use lowercase snake_case and are noun-oriented, which is readable and consistent overall. 'arrears' is a single-word noun while the others are compound noun phrases, a minor deviation but not confusing.
Three tools are within a reasonable range for a focused read-only reporting server, and each earns its place. It is somewhat thin for a building-management domain, but not mismatched.
The surface covers key read-only views: unit balances, arrears, and upcoming bookings. However, it lacks write/lifecycle operations such as creating or updating bookings, recording payments, listing units, or producing broader building summaries, which may leave agents at dead ends for management tasks.
Maintenance
Related MCP Connectors
Ask your Rent Manager portfolio anything: live, read-only financials, rent roll, leasing.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
- HAVNOAuthapp.havnre
Read-only AI access to HAVN properties, leads, tasks, files, media, and analytics.
The HubSpot MCP Server acts as a bridge that enables AI assistants and Large Language Models to securely interact with HubSpot CRM data through natural conversation, without requiring users to understand complex API structures. It provides read-only access to standard CRM objects (contacts, companies, deals, tickets, products, invoices, and more) and their associations, secured via OAuth 2.0, allowing AI agents to perform tasks like summarizing deals, fetching company updates, and looking up record changes.
Related MCP Servers
- AlicenseAqualityDmaintenanceA read-only MCP server that connects AI assistants to the OfficeRnD coworking and flex-space management platform. It enables natural language queries for community members, space bookings, billing records, and office resources.51MIT
- AlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server that provides secure, role-based access to PostgreSQL databases for AI agents.MIT
- AlicenseNot gradedqualityCmaintenanceA secure, multi-tenant Model Context Protocol server that gives AI clients controlled access to enterprise knowledge bases, SQL databases, REST APIs, files, and GitHub.Apache 2.0
- AlicenseAqualityAmaintenanceA drop-in Model Context Protocol (MCP) server that lets an AI agent query PostgreSQL — read-only, enforced at the database level, with real SQL validation, timeouts, cost limits, OAuth 2.1, and an audit trail.103MIT