The Fredy MCP Server exposes your real estate jobs and listings data to LLM clients. It supports two transports:
- Stdio: for local LLM clients (Claude Desktop, LM Studio, llm-cli, mcp-cli, etc.)
- Streamable HTTP: for remote LLM clients (ChatGPT, cloud-hosted agents, etc.)
All MCP access is token-based based. Every Fredy user is automatically assigned a permanent, non-expiring MCP token when their account is created. This token is a secret and should be treated like a password.
MCP tokens are displayed in the User Management list (Admin → Users). Each user's token is shown in the "MCP Token" column.
Important: MCP tokens never expire. They are permanent secrets tied to each user account. If a token is compromised, you must change the token! If you chose to use a token from an admin account, the LLM can query information from ALL jobs/listings.
| Tool | Description |
|---|---|
list_jobs |
List real estate search jobs with pagination and text filtering |
get_job |
Get detailed information about a specific job |
list_listings |
Search and list real estate listings with pagination, text search, and filters |
get_listing |
Get full details of a single listing |
calculate_financing |
Work out whether a property is affordable, using a German mortgage model |
get_current_date_time |
Gets the current date/time for the llm to be used |
page(number, optional) - Page number (default: 1)pageSize(number, optional) - Results per page (default: 50, max: 1000). Use pagination to fetch more.filter(string, optional) - Free-text filter on job name
Response: markdown table with columns ID, Name, Enabled, Active Listings. Includes summary and pagination info.
jobId(string, required) - The job ID to retrieve
page(number, optional) - Page number (default: 1)pageSize(number, optional) - Results per page (default: 50, max: 1000). Use pagination to fetch more.filter(string, optional) - Free-text search across title, address, provider, linkjobId(string, optional) - Filter listings by job IDactiveOnly(boolean, optional) - When true, only show active listingsprovider(string, optional) - Filter by provider namecreatedAfter(number, optional) - Only include listings created at or after this unix timestamp in milliseconds (e.g.1772008362564). Useful for queries like "give me all listings from today".createdBefore(number, optional) - Only include listings created at or before this unix timestamp in milliseconds (e.g.1772008362564).minPrice(number, optional) - Only include listings with price >= this value (e.g.500). Numeric, no currency symbol.maxPrice(number, optional) - Only include listings with price <= this value (e.g.1500). Numeric, no currency symbol.sortField(string, optional) - Sort by: created_at, price, size, provider, title, is_activesortDir(string, optional) - Sort direction: asc or desc
Response: markdown table with columns ID, Title, Address, Price, Size, Provider, Active, Created, Job. Includes summary and pagination info. Use get_listing for full details.
Note: All timestamps are unix timestamps in milliseconds (e.g.
1772008362564), not seconds.
listingId(string, required) - The listing ID to retrieve
Prices up a property as a German Annuitätendarlehen and judges it against the 35 % rule.
Every parameter is optional and falls back to the finance profile saved in the UI, so
calculate_financing({ listingId: "abc" }) is usually all you need.
listingId(string) - Listing to price up. Its price overridespurchasePrice.purchasePrice(number) - Purchase price in EUR, when not using a listingequity(number) - Eigenkapital in EURbundesland(string) - Two-letter code (BW,BY,BE,BB,HB,HH,HE,MV,NI,NW,RP,SL,SN,ST,SH,TH), sets the GrunderwerbsteuerannualRate(number) - Sollzins in percent, e.g.3.8tilgung(number) - Anfängliche Tilgung in percent per year, e.g.2fixedYears(number) - Zinsbindung in years, e.g.10monthlyPayment(number) - Monthly instalment in EUR. Overridestilgung; the two are the same quantity from opposite endsmaklerPct(number) - Buyer's agent commission in percent;0for a private salenetIncome(number) - Combined monthly net income, overriding the saved profilelivingCosts(number) - Monthly living costs excluding rent
Response: markdown with the verdict (affordable / a stretch / out of reach), the monthly rate and what share of net income it is, the Kaufnebenkosten breakdown, the loan amount, the years to payoff, the Restschuld at the end of the Zinsbindung, the debt-free age, and a comparison table when several rate scenarios are configured.
Listings are user-scoped exactly as in the other tools: asking for a listing that belongs to someone else returns "Listing not found".
Note: The result is an estimate based on standard German purchase costs. It is not financial advice, and the Grunderwerbsteuer table ships as an editable default.
The stdio transport communicates over stdin/stdout and is ideal for local LLM tools.
MCP_TOKEN=fredy_<your-token> node mcp/stdio.js
# or
MCP_TOKEN=fredy_<your-token> yarn mcp:stdioThe MCP Inspector lets you interactively test your MCP server in a browser UI.
npx @modelcontextprotocol/inspector -e MCP_TOKEN=fredy_<your-token> -- node mcp/stdio.jsOnce the inspector is running, open the URL shown in your terminal (usually http://localhost:6274). You can then:
- Click Connect to establish the stdio connection
- Go to the Tools tab to see all available tools
- Select a tool, fill in parameters, and click Run to test it
LM Studio supports MCP servers natively, allowing your local LLM to access Fredy's jobs and listings data.
-
Open LM Studio and load a model that supports tool use (e.g., Qwen 2.5, Llama 3.1, Mistral, etc.)
-
In the right side under Integrations click on "# install" and "edit mcp.json"
-
Edit the LM Studio MCP config file directly (
~/.lmstudio/config/mcp.jsonor via the UI export):{ "mcpServers": { "fredy": { "command": "node", "args": ["/absolute/path/to/fredy/mcp/stdio.js"], "env": { "MCP_TOKEN": "fredy_<your-token>" } } } } -
Toggle the server on: LM Studio will spawn the stdio process and connect
-
You should see the Fredy tools appear as available tools
After testing numerous LLM's, I got the best results with Qwen 3.5 or Qwen 2.5.. E.g. Qwen2.5-14B-Instruct-1M-8bit.
Once connected, simply ask your LLM about your real estate data in natural language:
- "Show me all my active search jobs"
- "List the latest listings from my Berlin apartment search"
- "Get details for listing XYZ"
- "What are the cheapest listings across all my jobs?"
The LLM will automatically call the appropriate Fredy MCP tools and present the results.
Tip: Make sure Fredy is running and the database is accessible before starting the MCP server in LM Studio. The stdio transport initializes its own database connection, so Fredy's main process does not need to be running, but the database file must exist and be up-to-date (migrations applied).
Claude Desktop supports MCP servers natively via its developer settings.
-
Open Claude Desktop
-
Go to Settings → Developer → Edit Config - this opens the
claude_desktop_config.jsonfile -
Add the
fredyserver to themcpServersobject:{ "mcpServers": { "fredy": { "command": "/opt/homebrew/opt/node@22/bin/node", "args": ["/absolute/path/to/fredy/lib/mcp/stdio.js"], "env": { "MCP_TOKEN": "fredy_<your-token>" } } } }Replace
/absolute/path/to/fredywith the actual path on your machine (e.g./Users/you/dev/fredy).Important: Claude Desktop launches with a restricted
PATHand often cannot findnodeby name. Always use the full absolute path to the node binary. Find yours by runningwhich nodein a terminal. Common locations:- Homebrew (default):
/opt/homebrew/bin/node - Homebrew (versioned, e.g. node@22):
/opt/homebrew/opt/node@22/bin/node - nvm:
/Users/<you>/.nvm/versions/node/<version>/bin/node
- Homebrew (default):
-
Save the file and restart Claude Desktop
-
You should see a hammer icon (🔨) in the chat input - click it to confirm the Fredy tools are listed
Once connected, simply ask Claude about your real estate data:
- "Show me all my active search jobs"
- "List the latest listings from my Berlin apartment search"
- "What are the cheapest apartments added this week?"
Claude will automatically call the appropriate Fredy MCP tools.
Note: Fredy's main web process does not need to be running - the stdio transport opens its own database connection directly. But the SQLite database file must exist and migrations must have been applied.
The HTTP transport is automatically available when Fredy is running. It uses the MCP Streamable HTTP protocol at:
POST /api/mcp - JSON-RPC messages (initialize, tool calls)
GET /api/mcp - SSE stream for server-initiated notifications
DELETE /api/mcp - Terminate session
All requests must include the token as a Bearer token:
Authorization: Bearer fredy_<your-token>
curl -X POST http://localhost:9998/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer fredy_<your-token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0.0" }
}
}'curl -X POST http://localhost:9998/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer fredy_<your-token>" \
-H "Mcp-Session-Id: <session-id-from-init-response>" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_jobs",
"arguments": { "page": 1, "pageSize": 10 }
}
}'- Every user is automatically assigned a permanent MCP token at account creation - tokens never expire
- Tokens are cryptographically random (256-bit) and prefixed with
fredy_ - Each token is scoped to a single user - the LLM can only access that user's data
- Non-admin users only see their own jobs and jobs shared with them
- Tokens are stored in the
mcp_tokencolumn of theuserstable - Tokens are deleted automatically when the owning user is removed
- The
/api/mcpendpoint uses Bearer token auth (independent of cookie-session) - Treat MCP tokens like passwords - do not share them publicly
All tool responses use markdown instead of JSON for maximum LLM readability and token efficiency:
- List responses (list_jobs, list_listings) use markdown tables with a summary line and pagination footer
- Detail responses (get_job, get_listing) use markdown key-value lists
- Error responses include the tool name and error message
Example list response:
**Tool:** list_listings | **Status:** OK
Found **85** listing(s). Showing page 1 of 2 (50 on this page). More pages available - use page=2 to continue.
| ID | Title | Address | Price | Size | Provider | Active | Created | Job |
|----|-------|---------|-------|------|----------|--------|---------|-----|
| abc123 | Nice flat | Berlin | 1200 | 70 | immoscout | yes | 2026-02-25 10:30:00 | My Search |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
Use **get_listing** with an ID for full details (description, link, image).
**Page:** 1/2 | **Has more:** yes
Example detail response:
**Tool:** get_listing | **Status:** OK
### Listing: Nice flat
- **ID:** abc123
- **Title:** Nice flat
- **Address:** Berlin
- **Price:** 1200
- **Size:** 70
- **Provider:** immoscout
- **Link:** https://...
- **Active:** yes
- **Created:** 2026-02-25 10:30:00
Markdown is used because it is significantly more token-efficient than JSON (~40-60% fewer tokens for tabular data) and natively understood by all LLMs.
┌─────────────────┐ stdio ┌──────────────┐
│ Local LLM │◄──────────────►│ mcp/stdio.js│
│ (LM Studio, │ │ (transport) │
│ Claude, etc.) │ │ │
└─────────────────┘ └──────┬───────┘
│
┌─────────────────┐ Streamable HTTP ┌────┴────────┐
│ Remote LLM │◄───────────────►│ /api/mcp │
│ │ (Bearer token) │ (transport) │
└─────────────────┘ └──────┬───────┘
│
┌──────────┴──────────┐
│ mcpAuthentication │
│ (token validation, │
│ access control) │
└──────────┬──────────┘
│
┌────────┴────────┐
│ mcpAdapter.js │
│ (tool routing │
│ + data fetch) │
└────────┬────────┘
│
┌────────┴────────┐
│ mcpNormalizer.js│
│ (markdown │
│ formatting) │
└────────┬────────┘
│
┌──────┴───────┐
│ Fredy DB │
│ (SQLite) │
└──────────────┘