LeashPass MCP server

The Model Context Protocol (MCP) is a small open standard that lets an LLM call your APIs as tools, with structured input schemas and rich descriptions written for AI consumption. LeashPass is built MCP-first: every read and workflow operation the REST API exposes is also available as an MCP tool, so an agent can drive the entire screening workflow without any custom integration glue.

Status: shipped (Phase 1d). Read access and workflow transitions are live. Document upload via MCP and entity creation tools (create_pet, create_profile) are deferred to Phase 1d.5; use the REST API for those for now.

Why MCP

Three reasons MCP is the right shape for screening:

  1. Tool descriptions written for AI. MCP carries rich English descriptions on every tool and field. The agent doesn't have to guess what a parameter does — the description tells it.
  2. Structured input/output. Every tool ships a JSON Schema with additionalProperties: false, explicit required lists, and ULID pattern constraints. The MCP client validates inputs before they reach the server; the server validates again at the wire boundary.
  3. One identity per session. A single API key authenticates the SSE connection; every tool call inherits the key's scopes. The LeashPass-On-Behalf-Of and LeashPass-Acting-User headers map the agent's actions to a specific human for audit purposes.

Phase 1f.1 parallel surface. LeashPass also exposes an in-app chat backend at /chat/conversations/... that reuses the same tool catalog through Server.ExecuteTool. The MCP server here is for external agents (Claude Desktop, customer PMS integrations); the in-app chat is for internal LeashPass users with a session cookie. Both surfaces emit the same mcp.tool.<name>.* audit rows. See docs/chat.md for the chat HTTP+SSE contract.

Connection

The MCP server accepts Server-Sent Events (SSE) connections at:

GET https://your-leashpass-host/mcp/sse
Authorization: Bearer lp_live_...

The first SSE event the server sends is event: endpoint whose data is the URL the client should POST JSON-RPC requests to (e.g. /mcp/messages?sessionId=...). Each POST is acknowledged with 202 Accepted; the actual JSON-RPC response is pushed back over the SSE stream as event: message. A heartbeat comment is emitted every 25 s to keep proxies from killing idle connections.

The wire protocol is JSON-RPC 2.0; LeashPass speaks the 2024-11-05 revision of the MCP spec. The hand-rolled transport is documented in docs/adr/0010-mcp-protocol-handroll.md; clients like Claude Desktop, Claude Code, and the official MCP SDKs handle the wire format for you.

Configuration: Claude Desktop

Add LeashPass to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "leashpass": {
      "transport": "sse",
      "url": "https://demo.leashpass.com/mcp/sse",
      "headers": {
        "Authorization": "Bearer lp_live_abcdef...",
        "LeashPass-Acting-User": "agent:claude-desktop"
      }
    }
  }
}

Restart Claude Desktop and the LeashPass tool catalog appears in the MCP picker. The first request fetches the tool list; subsequent calls are cheap. See docs/mcp-demo.md for a full step-by-step walkthrough including the API-key minting flow.

Configuration: agent backends

For agent backends (LangChain, CrewAI, custom orchestration) use any MCP-compatible client library:

The same SSE endpoint and headers apply; the SDK abstracts the wire format.


Audit-grade contract

Every MCP tool call writes to the same audit log REST handlers write to. Workflow tools (today: transition_profile; tomorrow: any new write operation) emit two audit rows per invocation:

Row Action Resource Written by
MCP-context (transport-scoped) mcp.tool.<tool_name>.success / .failure (mcp_tool, <tool_name>) Dispatcher audit hook
Domain (resource-scoped) profile.approved (or .rejected, .under_review, .needs_info, .submitted, .draft) (pet_profile, <profile_id>) Tool handler

Both rows carry the API key ID, the on-behalf-of header, the acting-user header, the originating IP, the User-Agent, and the X-Request-Id from the JSON-RPC POST.

This is deliberate: an admin querying the audit feed by (resource_type, resource_id) for "every event on this profile" must see every transition regardless of which API surface drove it. If the only audit row written were the MCP-context one, that filter would silently miss MCP-driven transitions and the audit log would no longer be a single source of truth. Future write tools must follow this same dual-event pattern.

Read tools (everything except transition_profile) emit only the MCP-context row — no domain event applies because no state changed.


Tool catalog

Tools fall into three groups: reads (12 tools, scoped by *:read), workflow (2 tools, scoped by profiles:reviewtransition_profile forward-flow and reopen_profile reverse-flow), and manager write-back (3 tools, Phase 1e — scoped by pets:write, profiles:write, profiles:notify respectively). Every tool's inputSchema is a JSON Schema document with additionalProperties: false; the schemas are embedded in the binary at build time and shipped as part of the tools/list response.

Parameter naming: one canonical spelling per field

LeashPass schemas declare exactly one accepted name for each parameter. Aliases (e.g. target_status alongside to_status, or pet_id alongside id) are deliberately NOT supported.

  • The status-change parameter is to_status in both transition_profile and reopen_profile. Calls that send target_status, new_status, status, or anything else are rejected by the validator with Additional property X is not allowed.
  • The profile reference is profile_id (never id or profile).
  • The reopen justification is reason_note (never reason or justification).
  • The transition note is note (never manager_note or comment on the wire — manager_note is the column name written internally by update_profile_manager_note).

The trade-off: aliases are convenient for agents but they bloat the API surface, double the test matrix, and make migrations harder. Instead, every tool description and every field description explicitly names the parameter — when an agent's first call uses the wrong spelling, the error message points at the schema's canonical name and a retry succeeds.

If a single-name convention causes an agent friction in your deployment, the right fix is usually to update the agent's system prompt with a one-line cheat sheet of the canonical names rather than adding aliases on the server.

Human-readable display fields (Phase 1e.1)

Every entity-returning tool result includes a display field with a pre-formatted, one-line summary suitable for rendering directly to users. Agents should prefer this for conversational output; the id field stays in the result for follow-up tool calls.

The canonical formats:

Entity Example
Property Acme Apartments East — 1100 Eastview Drive, San Diego, CA
User Maria Garcia (maria.garcia@example.com)  ·  non-residents get a role suffix: [admin], [manager], [operator]
Pet Buddy (Golden Retriever, 5y, spayed/neutered) — owned by Maria Garcia
Profile Buddy (Golden Retriever) for Maria Garcia at Acme Apartments East — under_review, score 4
Document vaccination-buddy.pdf (PDF, 192 KB) — uploaded 2026-03-15

Variations covered by the renderer:

  • Service animal / ESA: the score is replaced by the FHA-exempt classification. … — approved, service_animal (FHA exempt) / … — approved, esa (FHA exempt).
  • Draft profile: … — draft, not yet scored (the rubric only runs on submit).
  • Empty optional fields: skipped. A pet with no breed and no spay/neuter info renders as Whiskers (10y) — owned by … rather than Whiskers (, 10y, ) — owned by ….
  • Missing related entity: graceful [unknown] placeholders, e.g. Buddy (Golden Retriever) for [unknown] owner at Acme Apartments East — needs_info. We don't fail the call because an orphan row exists.
  • Date format: ISO 8601 calendar days (2026-03-15). Times are omitted unless meaningful — the upload moment of a document is not.
  • Content types: normalised to short labels for documents — PDF, JPEG, PNG, HEIC. Unknown MIME types pass through unchanged.
  • Byte sizes: IEC binary units (1 KB = 1024 B), single-decimal precision at MB and above.

The display field is English-only for v0.1 (no i18n). It is NOT emitted on workflow tools that return status confirmations rather than entity records: transition_profile, update_pet, update_profile_manager_note, reopen_profile, and notify_resident_about_profile return entity DTOs (so the field IS present there) or simple {sent: true} payloads. get_profile_audit and explain_profile_score have their own response shapes and do not carry display.

Read tools

list_properties

Lists every (non-archived) property in this organization, sorted by name. The agent should call this first when the user references a property by name to discover the property_id needed by other tools.

  • Scope: properties:read
  • Input: {}
  • Output: {"properties": [Property…]}

get_property

Fetches a single property by ID: name, slug, address, status (active or archived).

  • Scope: properties:read
  • Input: { "property_id": "prop_..." }
  • Output: Property

list_residents

Lists users with the resident role in this organization, sorted by email. Pass property_id to limit to residents who have at least one pet profile at that property.

  • Scope: users:read
  • Input: { "property_id": "prop_..." } (optional)
  • Output: {"residents": [User…]}

get_user

Fetches a single user (resident, manager, admin, or operator) by ID: email, role, timestamps. Password material and session data are never returned.

  • Scope: users:read
  • Input: { "user_id": "<26-char ULID>" }
  • Output: User

list_pets

Lists pets in this organization, sorted by name. Pass owner_user_id to filter to a single resident's pets.

  • Scope: pets:read
  • Input: { "owner_user_id": "<26-char ULID>" } (optional)
  • Output: {"pets": [Pet…]}

get_pet

Fetches a single pet record: name, species, breed, sex, spayed/neutered, weight, microchip ID, date of birth.

  • Scope: pets:read
  • Input: { "pet_id": "pet_..." }
  • Output: Pet

list_profiles

Lists pet screening profiles, sorted by creation time (newest first). Filters: property_id, status, resident_user_id. Without filters, returns the full org list.

  • Scope: profiles:read
  • Input:
    {
      "property_id":      "prop_...",
      "status":           "submitted | under_review | approved | rejected | needs_info | draft",
      "resident_user_id": "<26-char ULID>"
    }
    
  • Output: {"profiles": [ProfileSummary…]}

get_profile

Fetches a complete pet screening profile: pet info (name, species, breed), every answered question (vaccinations, vet contact, behavior, time alone, bite history, training), current review status, manager notes, score breakdown if scored, and the IDs/types of every attached document.

  • Scope: profiles:read
  • Input: { "profile_id": "petprof_..." }
  • Output: ProfileDetail (embeds Pet and []DocumentRef)

get_profile_audit

Returns the recent audit history for a profile (newest first): state transitions, document attachments, manager notes, updates. Default limit 50, max 200.

  • Scope: profiles:read
  • Input: { "profile_id": "petprof_...", "limit": 50 }
  • Output: {"events": [AuditEvent…]}

explain_profile_score

Returns a detailed explanation of why a profile received its score, including each rule that fired and how it contributed to the total. Use this when a manager asks 'why' a profile was scored a certain way.

Service animals and ESAs return classification only with no score (Fair Housing Act exemption). Profiles still in draft return NotFound.

  • Scope: profiles:read
  • Input: { "profile_id": "petprof_..." }
  • Output: { "score": 4, "label": "Approve", "classification": "regular", "rubric_version": "0.1.0", "rubric_hash": "…", "scored_at": "…", "breakdown": [ {"id":"vaccinations_current","points":25,…} ] }

See docs/scoring.md for the rubric definition.

list_documents_for_profile

Lists every (non-deleted) document attached to a profile, with metadata only. Use get_document_url to mint a download link.

  • Scope: documents:read
  • Input: { "profile_id": "petprof_..." }
  • Output: {"documents": [Document…]}

get_document_metadata

Fetches metadata for a single document: type, original filename, content type, size, status, uploader, timestamps. The bytes are NOT included.

  • Scope: documents:read
  • Input: { "document_id": "doc_..." }
  • Output: Document

get_document_url

Mints a short-lived (5 minute) signed download URL for a document. The agent surfaces the URL to the user as a clickable link in chat — it does not store the URL or include it in long-lived outputs.

  • Scope: documents:read
  • Input: { "document_id": "doc_..." }
  • Output: { "document_id", "url", "expires_at", "original_filename", "content_type", "size_bytes" }
  • Errors: CodeConflict if the document is not yet ready; CodeValidation if document storage is not configured on the server.

Workflow tools

transition_profile

Changes a pet profile's review status. Allowed transitions: draft→submitted, submitted→under_review, under_review→{approved, rejected, needs_info}, needs_info→draft. Other transitions return CodeConflict.

The agent must pass a non-empty note when transitioning to rejected or needs_info — the resident reads it. The handler returns CodeValidation if the note is missing for those transitions.

  • Scope: profiles:review
  • Input:
    {
      "profile_id": "petprof_...",
      "to_status":  "approved | rejected | needs_info | submitted | under_review | draft",
      "note":       "Pet rent of $50/month required."
    }
    
  • Output: refreshed ProfileSummary
  • Side effects: writes both mcp.tool.transition_profile.success AND profile.<status> audit rows (see Audit-grade contract).
  • Auto-email: when to_status=needs_info and note is non-empty, the resident receives a templated email containing the manager's note and a deep link back to the application. Failures are fail-soft (the status change is durable; the failed send is captured in the audit log under email.failed.notify_resident). Phase 1e §Step 4.

reopen_profile (Phase 1e)

Reopens a previously-decided profile, returning it to under_review (most common: "we got it wrong but the answers don't need to change") or draft ("the situation has changed; the resident must update their answers"). Profiles still in review return CodeConflict.

The handler clears the prior reviewed_at + reviewed_by_user_id so the per-resource timeline reflects the reversal cleanly. The manager_note from the prior decision stays on the row (the rationale for the original approve/reject is still discoverable from history); the reason_note for THIS reopen lands in the audit row, not on the profile.

  • Scope: profiles:review (same scope as forward flow — reversing a decision is a review action).
  • Input:
    {
      "profile_id":  "petprof_...",
      "to_status":   "under_review | draft",
      "reason_note": "Approval was based on a vaccination record we have since determined to be expired."
    }
    
    reason_note minimum 10 characters of meaningful prose (handler-level whitespace check; schema's minLength would let "10 spaces" pass).
  • Output: refreshed ProfileSummary
  • Side effects: writes BOTH profile.reopened (with the reason_note in AfterJSON — the "why") AND profile.<new_status> (the canonical per-status transition row) audit events. Audit-feed consumers querying by (resource_type=pet_profile, resource_id=…) see a contiguous timeline regardless of forward vs reverse flow.

Manager write-back tools (Phase 1e)

update_pet

Patch fields on a pet record: name, species, breed, color/markings, sex, spayed/neutered, weight, date of birth, microchip ID. Pass only the fields you want to change; omitted fields are left alone. A no-op edit (every supplied value matches what's already on the row) does not write an audit row — keeps the per-pet audit history clean.

Manager identities are gated on property assignment: a manager who has no profile for the pet at any of their assigned properties sees CodeNotFound (not CodePermissionDenied) so the gate doesn't leak existence across properties. Operators and admins skip the gate.

  • Scope: pets:write
  • Input: every field optional except pet_id; see internal/mcp/schemas/update_pet.json for the full list.
  • Output: Pet (full DTO so the agent confirms what landed)
  • Side effects: pet.updated audit row, BeforeJSON / AfterJSON containing only the fields that actually changed.

update_profile_manager_note

Set or clear the internal manager_note on a profile. The note is not visible to the resident; it's a manager-only observation field ("resident on vacation until Friday"). Pass an empty string to clear.

The audit row fires every call — including no-op edits where the new value matches the old. Manager-note edits are rare enough that visibility wins over noise reduction; the audit trail shows the manager looked at this profile and confirmed the note.

  • Scope: profiles:write
  • Input:
    { "profile_id": "petprof_...", "note": "Resident on vacation until Friday." }
    
  • Output: refreshed ProfileSummary
  • Side effects: profile.manager_note_updated audit row with before/after diff.

notify_resident_about_profile

Send a templated email to the resident about their pet application. The agent picks message_type and supplies a single custom_message; everything else (greeting, subject, footer) is template-driven so the agent cannot smuggle markup, links, or footer noise past the manager. HTML in custom_message is escaped before rendering.

message_type Use custom_message
request_document Ask the resident to upload a specific document required (the request)
general_question Ask a clarifying question about the application required (the question)
status_check_in Proactive update with current status optional (note alongside status)

Unlike the auto-email side-effect of transition_profile→needs_info (which is fail-soft because the state change is durable), this tool is the agent's primary intent — send failures DO surface to the agent so it can retry or fall back. The email.failed.notify_resident audit row is written before the error returns so the failure is recoverable from the audit log alone.

  • Scope: profiles:notify — deliberately separate from profiles:review. An integration can be granted "email residents" power without also granting "approve / reject" power. Example: a customer-success agent that follows up on stale applications.
  • Input:
    {
      "profile_id":     "petprof_...",
      "message_type":   "request_document | general_question | status_check_in",
      "custom_message": "Could you confirm whether Buddy has had a flea treatment in the last 30 days?"
    }
    
  • Output: { "profile_id", "recipient", "message_type", "sent": true }
  • Side effects: on success, email.sent.notify_resident audit row with the rendered recipient, template, and custom_message in AfterJSON. On failure, email.failed.notify_resident audit row with the error string.

Wire codes

Tool failures map to JSON-RPC error codes (the error.code field on the response):

Code Constant Meaning
-32602 CodeInvalidParams Input failed JSON-Schema validation
-32000 CodePermissionDenied Identity is missing the per-tool scope
-32001 CodeNotFound Resource does not exist or is not visible to this org
-32002 CodeConflict State machine refuses the transition; concurrent write
-32003 CodeValidation Input passes schema but fails domain rule (note required, document not ready, storage unconfigured)
-32603 CodeInternalError Unexpected error

CodeNotFound is also returned for cross-org access — we do not distinguish "exists in another org" from "does not exist" so an attacker cannot enumerate identifiers across tenants.


Example agent flow

A property manager asks Claude to triage the review queue:

User: Show me what's in the Acme Apartments review queue and approve anything where everything checks out.

Claude's tool sequence:

  1. list_properties → finds prop_... for Acme Apartments
  2. list_profiles with property_id=prop_... and status=submitted → 4 profiles
  3. get_profile for each → reads pet info, vaccination status, vet contact, bite history, attached documents
  4. For 3 profiles where everything checks out: transition_profile with to_status=approved and a one-line note
  5. For the 4th: transition_profile with to_status=needs_info and a note explaining what the resident must add
  6. Returns a summary: "Approved 3 profiles; sent the 4th back for a missing rabies record."

Every tool call lands in the audit table. The three approvals each produce two rows: mcp.tool.transition_profile.success (transport-scoped) and profile.approved (resource-scoped). A compliance officer reviewing the audit feed for that property six months later sees the same history they'd see if a human had clicked through the admin UI — only with the API key ID and acting-user header attribution telling the story of the agent.

Server-rendered widget tools (Phase 2.4)

Three read-only tools designed for the in-app chat surface. The chat handler wraps each response in a fenced widget block so the frontend renders a styled component automatically. External MCP clients (Claude Desktop, custom integrations) get the raw JSON response — the wrap is chat-handler-specific. See docs/chat.md § Server-rendered visuals for the architecture rationale and the per-tool wire shapes.

All three carry profiles:read scope and auto-scope by caller role: resident → own profiles; manager → assigned properties; admin / operator+pin → org-wide. API-key callers behave as admin within the org (the dispatcher scope check is the authorisation; no per-row role gate).

list_review_queue

Returns a scoped queue of profiles needing attention.

  • Scope: profiles:read
  • Input: { "scope": "my_queue" | "all" | "needs_action" } (optional; default my_queue)
  • Output: { "kind": "review-queue", "title", "rows": [...] } — per-row fields: profile_id, pet_name, pet_breed, resident_name, property_name, status, score, score_label, days_in_status, urgency, can_write
  • Sort: urgency desc → days_in_status desc (oldest highest within tier)
  • Urgency tiers: >14d high, 7-14d medium, <7d low
  • Cap: 20 rows in v0.1 (no pagination — longer queues drop into /admin/profiles for the paginated view)

get_priority_item

Returns the single highest-priority profile in scope, for the briefing's lead callout. Reuses the same candidate set

  • filter logic as list_review_queue so the lead always appears in the queue list too.
  • Scope: profiles:read
  • Input: { "scope": "my_queue" | "all" | "needs_action" } (optional; default my_queue)
  • Output: { "kind": "priority-item", "icon", "urgency", "title", "detail", "action_hint", "profile_id" } for a real lead, OR { "kind": "priority-item", "icon": "✅", "urgency": "low", "title": "Queue clear", "queue_clear": true, ... } when no profile matches the scope
  • Icon mapping: 🔥 high, ⚠️ medium, 🟢 low, ✅ queue-clear

get_profile_summary

Returns the summary-card payload for a single profile — title, subtitle, score chip, status, plus highlights + concerns derived from screening fields.

  • Scope: profiles:read (also subject to CheckCanReadProfile: admin / operator+pin / manager-assigned-to-property / resident-on-profile)
  • Input: { "profile_id": "petprof_..." }
  • Output: { "kind": "profile-summary", "profile_id", "title", "subtitle", "score", "score_label", "status", "highlights": [...], "concerns": [...] }
  • Highlights are derived from positive screening fields (vaccinations_current=true, house_trained=true, FHA flags, etc.). Concerns are derived from definite negatives, missing critical fields (no vet on file), and the outstanding manager note when present.
  • Unanswered fields stay SILENT — only explicit false values or resident absence notes count as concerns. Score breakdown intentionally omitted (drill-down lives in explain_profile_score).

Phase 1e write-back example

A manager asks Claude to follow up on a stale application:

User: We approved Buddy a month ago but the vet record turned out to be expired. Reopen the application and ask the resident to upload a current rabies certificate.

Claude's tool sequence:

  1. list_profiles filter by status=approved + resident name → finds petprof_... for Buddy
  2. reopen_profile with to_status=under_review and a 10+ char reason_note ("Approval was based on vaccination paperwork that has since been determined to be expired.")
  3. notify_resident_about_profile with message_type=request_document and a custom message naming the rabies certificate

The audit feed now carries five rows for Buddy's profile:

profile.reopened              resource_type=pet_profile  resource_id=petprof_...
profile.under_review          resource_type=pet_profile  resource_id=petprof_...
mcp.tool.reopen_profile.success                          resource_id=reopen_profile
email.sent.notify_resident    resource_type=pet_profile  resource_id=petprof_...
mcp.tool.notify_resident_about_profile.success           resource_id=notify_resident_about_profile

The pair on each domain action — profile.reopened plus profile.<status>, email.sent.notify_resident plus mcp.tool.<name>.success — is the audit-grade promise: a per-resource query (WHERE resource_type='pet_profile') and a per-tool query (WHERE resource_type='mcp_tool') both reconstruct the agent's session, with no gap between what landed and what the agent did.


Demo dataset

Connect an agent to a deploy seeded with leashpass seed --demo --reset --seed-logs --yes and the catalog has real, queryable answers for prompts like:

  • "What pet screening profiles need my review at Acme Apartments East?"list_profiles filtered by property + status
  • "Who's been most active in reviews this month?"audit:read aggregated by actor_user_id and action="profile.under_review"
  • "Show me approved profiles with scores below 70."list_profiles (status=approved) + get_profile for the score field
  • "What's the audit trail for Buddy's approval?"get_profile_audit for Buddy's profile ID — returns the full draft_created → submitted → under_review → approved arc the synthetic log lays down
  • "Are there any duplicate documents across applications?"audit:read for action="document.duplicate_detected" — the Bella/Buddy hash collision is in there by design
  • "What was the average time-to-approve at Acme East last month?"list_profiles + per-profile audit timestamps; the trail spans 6 months so "last month" returns a real cohort
  • "Has Aaron Bell logged in recently? Has he reviewed anything yet?"get_user + audit:read filtered by Aaron's user ID. Aaron is the deliberate "new hire we haven't trained yet" — login events only, no review actions.

The dataset is fully deterministic (PRNG seeded by SHA-256 of "leashpass-demo-v1"), so a re-run after --reset produces identical events and the same prompts return the same answers. That makes the demo reproducible across machines and architectures.

See docs/seeding.md for the full inventory and docs/mcp-demo.md for the step-by-step Claude Desktop walkthrough.

What is NOT exposed via MCP

By design, the v1 catalog is read + workflow only. The following are NOT available as MCP tools today:

  • Document upload — Phase 1d.5 once we have practical experience with how Claude Desktop serializes attached files.
  • Entity creation (create_pet, create_profile, create_resident) — needs UX design for what defaults the agent chooses.
  • Operator-level admin (creating organizations, plan changes)
  • Server configuration (encryption keys, rate-limit policy)
  • API key minting — agents can use keys but cannot create them
  • Direct SQL or migration access

If you have a use case that requires crossing one of these boundaries, open an issue describing the workflow and we'll discuss the right shape for it.


Security model

  • Scoped keys. The same scope vocabulary as REST applies. A key with only profiles:read cannot transition profiles, regardless of what the agent asks. The mcp:connect scope gates SSE connection establishment; per-tool scopes gate individual tool calls.
  • Per-key rate limits. The same buckets as REST apply to MCP tool calls.
  • No long-lived credentials in the channel. The Authorization header is sent on every tool-call HTTP request behind the SSE wrapper, never cached client-side beyond the connection lifetime.
  • Audit on every call. There is no "read-only no-audit" mode. Every read is logged at the same fidelity as a write; workflow tools land two rows so the resource-scoped audit feed is complete.
  • Cross-org isolation. Every tool query is scoped by the API key's organization_id. The integration tests in internal/server/handlers_mcp_test.go exercise this end-to-end: org A's key cannot read org B's resources via any tool, even by guessing IDs.

Roadmap

Milestone Phase Status
MCP server skeleton 1d ✓ shipped
Read-only tool catalog 1d ✓ shipped
Workflow / transition tool 1d ✓ shipped
Multi-org key isolation 1d ✓ shipped
Dual audit events on writes 1d ✓ shipped
Hosted MCP at demo.leashpass.com 1d in progress
Manager write-back tools (update_pet, update_profile_manager_note, reopen_profile, notify_resident_about_profile) 1e ✓ shipped
Auto-email on needs_info transitions 1e ✓ shipped
profiles:notify scope (split from profiles:review) 1e ✓ shipped
Document upload via MCP 1d.5 planned
Entity creation tools 1d.5 planned
Streamable-HTTP transport (2025-03-26) post-1.0 planned

For status updates, follow the milestones page on GitHub.


See also