The Curriculum / Reader / MCP Server: Give Agents Safe Access to Real Data
LEVEL 2 · INTERMEDIATE · INDIVIDUAL TRACK

MCP Server: Give Agents Safe Access to Real Data

This page compiles 3 files from the repository, verbatim, in reading order. The living version: this folder on GitHub.

level-2-intermediate/individual/05-mcp-server/README.md

MCP Server: Give Agents Safe Access to Real Data

Live code: OrangeOnyx/belle-mcp-server — clone, run locally, then follow the walkthrough. See the repos overview for how the four Level 2 repos fit together.

Model Context Protocol (MCP) is a standard way to expose tools and resources to an AI client. For Belle Realty, the first MCP server should be intentionally boring: read-only, tenant-scoped, rate-limited queries that return typed facts. Boring is a feature. It lets an agent answer “What is the late-fee policy for Unit 204?” without handing it a Supabase service key or unrestricted SQL.

Build a thin policy layer over Supabase, not a database proxy. Each tool has a clear purpose, input schema, authorization check, row limit, response schema, audit event, and redaction behavior. Examples: look up a property, fetch a lease clause, list open maintenance tickets, and retrieve a tenant’s permitted communications context. Tools return facts and stable IDs; the LLM decides how to explain them.

Writes are proposals. An MCP call may construct propose_create_maintenance_ticket, but the actual commit happens through a separate application approval endpoint with an actor, review record, and idempotency key. This boundary keeps a conversational model from becoming an unreviewed operator.

Read the build guide, then implement authentication and rate limits before connecting any client. The server is production infrastructure, not prompt plumbing.

Before adding complexity, run this design through a small representative eval and inspect the trace with the operator who will own failures. Make the boundary, escalation, and rollback visible in the product. That discipline will expose more useful work than another round of prompt cleverness.

level-2-intermediate/individual/05-mcp-server/auth-and-rate-limiting.md

MCP Authentication and Rate Limiting

Use short-lived user or service tokens with audience, organization ID, role, scopes, and expiration. The server verifies the token itself; it does not trust a client-provided tenant, property, or organization identifier. Map scopes to tools: a leasing coordinator may read assigned properties; a maintenance vendor may see only assigned work orders; an internal evaluator may use a sandbox organization only.

Authorize twice. First, the MCP server checks whether the caller may invoke the tool. Second, Supabase row-level security or a security-definer RPC checks which rows it may read. The second check matters because an authorization bug in application code should not become a cross-tenant data breach. Avoid service-role keys in the MCP process whenever possible.

Rate-limit by actor, organization, tool, and response volume. A sensible starting shape is a small burst allowance, a per-minute call cap, and a daily budget for expensive retrieval tools. Reject oversized arguments and return predictable errors: invalid_input, unauthorized, forbidden, not_found, rate_limited, conflict, and internal_error. Do not reveal whether an inaccessible tenant exists.

Protect against tool abuse as well as traffic. Cap search results, statement time, response bytes, and concurrent requests. Cache safe, organization-scoped reads briefly. Emit metrics for denied calls, rate-limit events, tool latency, row counts, and unusual ID enumeration. Provide a server-wide kill switch and per-tool feature flags. When a tool is disabled, return a clear unavailable error rather than silently substituting stale data.

The rule is simple: authentication identifies; authorization constrains; rate limiting contains; auditing proves what happened.

level-2-intermediate/individual/05-mcp-server/build-your-first-mcp.md

Build a Supabase-Backed Belle Realty MCP Server

Scope the first server

Expose four read tools: get_property(property_id), get_lease_clause(lease_id, clause_type), list_open_maintenance(property_id, unit_id?), and get_tenant_context(tenant_id). Return only fields needed by the agent. get_tenant_context might include preferred contact channel, open issues, and lease status; it should not return SSNs, payment instruments, or unrestricted message history.

Each tool validates input with a strict schema, identifies the authenticated actor and organization, then calls a purpose-built Supabase RPC or view using row-level security. Never accept model-produced SQL, table names, filters, or pagination cursors without server validation. Hard-cap results, sort deterministically, and return a truncated flag. A client cannot escape its organization or property assignment by guessing IDs.

Resources and provenance

Expose lease snippets as resources with stable URIs such as belle://leases/{lease_id}/clauses/{type}. Include updated_at, extraction version, page, quote, and review status. This lets the client cite a fact and detect stale data. Write an audit event for every call: request ID, actor, tool, safe inputs, row count, latency, policy decision, and result version. Do not log tenant message bodies or raw tokens unless retention policy explicitly permits it.

Propose writes

A tool such as propose_create_maintenance_ticket returns a typed proposal with deduplication candidates and policy checks. It never persists a ticket. The Belle Realty app renders the proposal, lets an authorized person modify it, and calls a separate approved-write endpoint. That endpoint repeats authorization, applies an idempotency key, records the approver, and returns the final ticket ID. An agent cannot turn a draft into a side effect by rephrasing a request.

Test ownership isolation, malformed inputs, prompt-injected tool arguments, stale lease references, and repeated approval submissions before adding more tools.

← Build Your Third Agent: Diligence Checklist RAG Pipeline: Lease Knowledge That Can Prove Its Work →