Skip to main content
Glama
alexisvieto

Atrio MCP Server

by alexisvieto

A read-only MCP server over Atrio

test

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

unit_balance

Balance of one unit: charged, paid, overdue, latest charges and payments

Staff: any unit in their building. A resident: their own unit

arrears

Units with an overdue balance at the end of a month, largest first, with the total

Staff only

upcoming_bookings

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:

  1. Read-only transaction. begin read only. An update fails even if a bug or an injected prompt produces one.

  2. A role with no write grants. set local role atrio_agent. The role can only select, and it is subject to Row Level Security.

  3. 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).

  4. 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 15

The 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.ts

Claude 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

aaaaaaaa-0000-0000-0000-000000000001

Admin, Torre Coral

bbbbbbbb-0000-0000-0000-000000000001

Resident of A-101, Torre Coral

cccccccc-0000-0000-0000-000000000001

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 start

The 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

sql/schema.sql

Tables, the atrio_agent role and the RLS policies

sql/seed.sql

Two fictional buildings, three users, dates relative to today

src/database.ts

Runs each call as the user: read-only, atrio_agent, RLS on

src/server.ts

The three MCP tools

src/main.ts

stdio entry point

test/server.test.ts

15 tests over the MCP protocol

All names and figures are fictional.

Available Tools

3 tools
arrearsArrears for a monthA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
monthNoMonth as "YYYY-MM". Omit for the current month.

TDQS

A4.2/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 balanceA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unit_codeYesUnit code, e.g. A-102

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 bookingsA
Read-only

Confirmed and pending bookings of common areas (party room, BBQ, pool) from today through the next N days.

ParametersJSON Schema
NameRequiredDescriptionDefault
daysNoHow many days ahead. Default 30.

TDQS

A3.9/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

  1. 3 tool updatesv0.1.0
    • First observedarrears
    • First observedunit_balance
    • First observedupcoming_bookings

TDQS

A3.9/5.0

Scored across 3 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness3/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server that provides secure, role-based access to PostgreSQL databases for AI agents.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    A 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.
    10
    3
    MIT