Connect Safely to Claude
An MCP server so Claude can query your Safely properties, reservations, guest verifications, and damage claims.
What you'll be able to ask Claude
Prerequisites
- Safely account with at least one property. The API is included with Safely's standard plans.
- Claude Desktop installed, or the Claude Code CLI. Download Claude Desktop.
- Node.js 18 or later. Check with
node --version.
Generate your Safely API key
Safely uses a single Bearer API key issued from your account portal. No OAuth flow, no multi-step setup.
- Log in to the Safely portal.
- Navigate to the Account page from the main menu.
- Find the API key section and copy the key.
Rate limits: Safely enforces 300 API calls per minute per key. Exceeding that will return 429 errors or temporarily disable your key, so don't hammer it in loops. For MCP use with Claude Desktop this is effectively unlimited (each user prompt results in a small number of calls).
Set up the Node.js project
Two dependencies: the MCP SDK and zod. Safely has no official Node SDK, so we'll make HTTP calls directly.
mkdir safely-mcp
cd safely-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
Add "type": "module" to package.json:
{
"name": "safely-mcp",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
}
}
Create the MCP server file
Save as index.js. The server exposes four tools mapped to the four key Safely resources: list-properties, list-reservations, list-verifications, and list-claims.
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_KEY = process.env.SAFELY_API_KEY;
const BASE_URL = "https://api.safely.com/api/v1";
if (!API_KEY) {
console.error("Missing SAFELY_API_KEY environment variable");
process.exit(1);
}
async function safelyGet(path, params = {}) {
const url = new URL(`${BASE_URL}${path}`);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.append(key, String(value));
}
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${API_KEY}`,
Accept: "application/json",
"User-Agent": "rapideye-safely-mcp/1.0",
},
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Safely API ${response.status}: ${body}`);
}
return response.json();
}
const server = new McpServer({
name: "safely",
version: "1.0.0",
});
// Tool 1: List properties
server.tool(
"list-properties",
"List all properties on the Safely account. Returns property ID, name, address, type, and coverage status. Use this as the entry point for any property-level question.",
{
page: z.number().optional().describe("Page number (default: 1)"),
limit: z.number().optional().describe("Results per page (default: 20)"),
},
async ({ page, limit }) => {
const data = await safelyGet("/properties/", { page, limit });
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Tool 2: List reservations
server.tool(
"list-reservations",
"List reservations tracked in Safely. Returns reservation details including guest, dates, property, and associated verification status.",
{
page: z.number().optional(),
limit: z.number().optional(),
property_id: z
.string()
.optional()
.describe("Optional property ID filter"),
},
async ({ page, limit, property_id }) => {
const data = await safelyGet("/reservations/", {
page,
limit,
property_id,
});
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Tool 3: List verifications
server.tool(
"list-verifications",
"List guest verifications. Each verification ties a guest to a reservation and has a status (pending, passed, failed, etc). Use this to surface guests who haven't been verified yet or whose verification flagged an issue.",
{
page: z.number().optional(),
limit: z.number().optional(),
},
async ({ page, limit }) => {
const data = await safelyGet("/verifications/", { page, limit });
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Tool 4: List claims
server.tool(
"list-claims",
"List damage claims filed through Safely. Returns claim ID, status, loss type, filing date, and associated reservation.",
{
page: z.number().optional(),
limit: z.number().optional(),
},
async ({ page, limit }) => {
const data = await safelyGet("/claims/", { page, limit });
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Safely's API also exposes lookup endpoints (/claims/losstypes/, /reservations/types/, /properties/types/, /countries/, /currencies/) and per-resource detail endpoints (/properties/{id}/, /claims/{id}/, etc.). Add more tools by copying the existing pattern and pointing at those paths.
Register the server with Claude Desktop
Open your Claude Desktop config file:
~/Library/Application Support/Claude/claude_desktop_config.json
%APPDATA%\Claude\claude_desktop_config.json
Add the Safely entry (merge into your existing mcpServers block if present):
{
"mcpServers": {
"safely": {
"command": "node",
"args": ["/absolute/path/to/safely-mcp/index.js"],
"env": {
"SAFELY_API_KEY": "your_safely_api_key_here"
}
}
}
}
Replace the path and API key with your real values. Then fully quit and reopen Claude Desktop.
Try it
"Which verifications are still pending right now?"
Troubleshooting
401 Unauthorized
Your Bearer token is wrong or expired. Generate a fresh one from the Safely portal Account page.
Also verify the header format in the code is exactly Authorization: Bearer <key> with a space between "Bearer" and the key. A missing space or wrong casing will 401.
429 Too Many Requests
Safely enforces 300 API calls per minute per key. If you're hitting this from normal Claude usage, something is wrong (probably Claude is in a loop). Check the Claude Desktop conversation for repeated identical tool calls.
If you're extending the server with batch operations that legitimately need more throughput, batch your requests and stay under the limit.
Claude says it doesn't see any Safely tools
Claude Desktop only reads the config file at startup. Fully quit Claude (Cmd+Q on macOS, right-click tray icon → Quit on Windows) and reopen it. Closing the window doesn't reload the config.
Still broken? Check Help → Open Logs Folder. Look for JSON parse errors in the config, a wrong absolute path to index.js, or node not in PATH.
Empty results on a resource I know has data
Safely paginates. The default page size is small. Try passing limit: 100 in your prompt ("list my properties with a limit of 100") or check the next pagination link in the response.
I want to add write operations (create claims, file verifications)
Safely's API supports write operations. Add tools using method: "POST" and a JSON body. Consult developer.safely.com for the exact request schemas.
We'd recommend being deliberate about write access. Filing a claim through Claude is possible but probably not something you want to enable casually.
Other guides
Connect PriceLabs to Claude
Read-only queries against your listings and current dynamic prices. The pricing category Cole skipped.
Connect OwnerRez to Claude
Bookings, properties, guests, and payments. Biggest US PMS Cole Rubin didn't cover.
Connect Seam to Claude
One connector wraps 12+ lock brands plus Minut, NoiseAware, and ecobee/Nest thermostats.