MCP Server (Connect AI to WALDO)

The WALDO MCP server lets you point Claude Desktop, Claude.ai, Cursor, ChatGPT, or any other MCP-aware AI directly at your WALDO analyses. Once connected, you can ask the AI things like "summarize my Cuyahoga County analysis," "what's the location quotient for healthcare in Travis County?", or even "run an analysis on Hamilton County, Ohio for Q2 2026" — and the AI does it, reading and writing live data through tools rather than guessing.

TL;DR for CRE professionals: MCP (Model Context Protocol) is a standard way for AI assistants to talk to outside services. Set it up once with your WALDO API key. From then on, the AI can pull your analyses into a chat, summarize them, compare them, and trigger new ones — all without leaving the conversation.


What you get from this

  • Ask the AI about your data instead of digging through dashboards. "Which of my completed analyses has the highest LQ in healthcare?" — the AI calls the tools, finds the answer.
  • Use the AI you already pay for. No new vendor, no new login. If you have Claude Desktop, claude.ai, Cursor, or ChatGPT (with the right plan), you can connect WALDO.
  • Trigger analyses from a chat. "Run an analysis on Madison County, Alabama for Q1 2026" → AI calls
    text
    run_analysis
    , returns the analysis ID, polls for completion, summarizes the result when it's done.
  • Compose with everything else the AI knows. Have the AI pull your latest WALDO numbers AND draft a client email referencing them, in one chat.
  • No code required. This is a config-file-edit + restart operation. About 5 minutes per AI client.

Real-world workflows

Ask the AI…What it does
"What's the population growth and supply gap on my latest Travis County analysis?"Calls
text
list_analyses
, finds the latest Travis County one, calls
text
get_market_metrics
and
text
get_supply
, summarizes.
"Run an analysis on Mecklenburg County, multifamily."Calls
text
run_analysis
with no year/quarter — defaults to the latest available QCEW quarter. Returns the analysis ID; polls
text
get_analysis
until
text
status='completed'
.
"Compare the comparative advantage of Saint Louis vs. Kansas City using 3-digit shift-share for 2019 Q4 → 2025 Q3."Calls
text
search_markets
for both (alias-normalized — "Saint Louis" finds "St. Louis"), kicks off
text
run_analysis
for each, then calls
text
get_shift_share
immediately (no phase wait needed) with explicit base/end periods and
text
naics_level: 3
.
"Compare the supply/demand gap between my Cuyahoga and Hamilton county analyses."Pulls both via
text
get_supply
, computes the diff, explains it.
"Find all my analyses where the investment recommendation is BUY or STRONG_BUY."Calls
text
list_analyses
, then
text
get_analysis
for each, filters by recommendation rating.
"Draft a one-page client email summarizing my Travis County analysis."Pulls the data via MCP tools, formats it as an email.

What you'll need

  1. A WALDO API key with the
    text
    mcp:invoke
    scope.
    Generate one in Settings → API Keys. Pick the scopes you want — typically
    text
    read:analyses
    +
    text
    mcp:invoke
    , plus
    text
    write:analyses
    if you want the AI to be able to trigger new analyses (not just read existing ones).
  2. An MCP-aware AI client. As of April 2026:
    • Claude Desktop (Anthropic) — supports MCP natively. ✓
    • claude.ai (web) — supports MCP via Connectors (web app sidebar). ✓
    • Cursor (code editor) — supports MCP natively. ✓
    • ChatGPT (OpenAI) — supports MCP via Custom Connectors on ChatGPT Pro / Team / Enterprise plans. ✓
    • Other MCP clients (Continue.dev, Goose, custom) — anything speaking the MCP spec works.
  3. Five minutes to edit one config or click through one settings UI per client you want to connect.

Step-by-step: Claude Desktop

This is the most common setup. Claude Desktop reads MCP servers from a config file you edit by hand.

  1. Get your WALDO API key (Settings → API Keys → New key, scopes

    text
    read:analyses
    +
    text
    mcp:invoke
    + optionally
    text
    write:analyses
    ). Copy the full
    text
    waldo_live_...
    value — you'll only see it once.

  2. Open the Claude Desktop config:

    • Click the Claude Desktop menu → Settings…
    • Click Developer in the left sidebar
    • Click Edit Config. This opens
      text
      claude_desktop_config.json
      in your default text editor.
  3. Paste this block into the file (if

    text
    mcpServers
    already exists, just add the
    text
    "waldo"
    entry inside it):

    json
    {
      "mcpServers": {
        "waldo": {
          "command": "npx",
          "args": [
            "-y",
            "mcp-remote",
            "https://www.waldocre.io/api/mcp",
            "--header",
            "Authorization:${AUTH_HEADER}"
          ],
          "env": {
            "AUTH_HEADER": "Bearer waldo_live_PASTE_YOUR_KEY_HERE"
          }
        }
      }
    }

    Claude Desktop launches

    text
    mcp-remote
    , an npm bridge that forwards stdio JSON-RPC to WALDO's streamable-HTTP endpoint and injects the
    text
    Authorization
    header. Claude Desktop has no native config field for HTTP headers, so this bridge is the standard pattern for any remote MCP server with bearer auth.

    Why the odd

    text
    Authorization:${AUTH_HEADER}
    syntax (no space,
    text
    Bearer
    baked into the env value)?
    Claude Desktop's env-var substitution does not interpolate
    text
    ${...}
    placeholders that sit mid-string after a space (
    text
    Bearer ${KEY}
    arrives as the literal string). The workaround is to put the entire header value — including the
    text
    Bearer 
    prefix — into one env var, and pass the argument as a single colon-joined token.

  4. Replace

    text
    waldo_live_PASTE_YOUR_KEY_HERE
    with the real API key value you copied in step 1. Keep the literal
    text
    Bearer 
    prefix in front of it.

  5. Save the file.

  6. Quit Claude Desktop completely and reopen it. (On macOS: ⌘Q, then relaunch from Applications.) MCP servers only load on startup. The first launch takes 10–20 seconds while npx downloads

    text
    mcp-remote
    .

  7. Verify it worked. Open Settings → Developer → "Local MCP servers".

    text
    waldo
    should show a green/connected status (not "Failed"). In a new chat, look for a 🔧 wrench icon or "Search and tools" menu. You should see WALDO tools listed:
    text
    search_markets
    ,
    text
    list_analyses
    ,
    text
    get_analysis
    , etc. If they're not there, see Troubleshooting below.

Try it:

"Use the WALDO tools to list my analyses."

The AI will call

text
list_analyses
, return your analyses, and respond with a summary.


Step-by-step: claude.ai (web)

Anthropic's web app also supports MCP via the Connectors UI:

  1. Generate a WALDO API key as above.
  2. Sign in to claude.ai.
  3. In the conversation sidebar, click the + icon or the integrations menu (location varies as Anthropic ships UI updates — look for "Connectors" or "Integrations").
  4. Click Add custom connector (or similar).
  5. Fill in:
    • Name: WALDO
    • URL:
      text
      https://www.waldocre.io/api/mcp
    • Authentication: Bearer token
    • Token: your
      text
      waldo_live_...
      key
  6. Save. The WALDO tools should appear in the chat tool picker for new conversations.

Step-by-step: Cursor

Cursor is the AI code editor by Anysphere; same MCP support as Claude Desktop.

  1. Generate a WALDO API key.

  2. In Cursor, open SettingsCursor SettingsMCP (or Features → MCP).

  3. Click Add new MCP server.

  4. Paste:

    json
    {
      "name": "waldo",
      "url": "https://www.waldocre.io/api/mcp",
      "headers": {
        "Authorization": "Bearer waldo_live_PASTE_YOUR_KEY_HERE"
      }
    }
  5. Replace the placeholder with your real key.

  6. Save. Restart Cursor. The WALDO tools appear in the chat tool picker.


Step-by-step: ChatGPT (Pro / Team / Enterprise)

OpenAI added MCP support via Custom Connectors in 2025. Available on paid plans only.

  1. Generate a WALDO API key.
  2. Sign in to chatgpt.com.
  3. Click your profile → SettingsConnectors (also called Custom Connectors depending on plan).
  4. Click Add connector or + New custom connector.
  5. Fill in:
    • Name: WALDO
    • MCP Server URL:
      text
      https://www.waldocre.io/api/mcp
    • Authentication: Bearer token / Custom header
    • Header:
      text
      Authorization: Bearer waldo_live_...
      (paste your key)
  6. Save. Enable the connector for the conversations you want to use it in.

ChatGPT's exact UI moves around — if "Connectors" isn't where you expect, search OpenAI's help center for "MCP" or "custom connector."


Troubleshooting

"WALDO tools don't appear in the menu after restart."

  • Confirm you fully quit and reopened the app (not just closed the window).
  • Open the config file again and validate it as JSON (paste into jsonlint.com if unsure — a missing comma or bracket breaks all MCP servers, not just WALDO).
  • Check the Claude Desktop logs: macOS
    text
    ~/Library/Logs/Claude/mcp-server-waldo.log
    , Windows
    text
    %APPDATA%\Claude\logs\mcp-server-waldo.log
    . A 401 means the key is wrong; a 403 means the key lacks
    text
    mcp:invoke
    scope.

"Server disconnected" / "Failed" status in Settings → Developer.

  • Tail
    text
    mcp-server-waldo.log
    . If you see
    text
    npm error 404 Not Found ... @modelcontextprotocol/server-fetch
    , your config is using an outdated bridge package. Switch to the
    text
    mcp-remote
    block above.
  • If you see
    text
    Authorization: Bearer ${...}
    arriving as a literal string in the request, the env var didn't interpolate — make sure the argument is
    text
    Authorization:${AUTH_HEADER}
    (no space after the colon) and the env value carries the full
    text
    Bearer waldo_live_...
    string.
  • First launch takes 10–20 seconds while npx fetches
    text
    mcp-remote
    . Give it a moment before declaring it broken.

"Tools appear but every call fails with 401."

  • The API key is wrong, revoked, or has trailing whitespace. Open Settings → API Keys, confirm the key is Active, regenerate if unsure.

"Tools appear but every call fails with 403."

  • The key doesn't have the
    text
    mcp:invoke
    scope. Revoke it and create a new one with
    text
    read:analyses
    +
    text
    mcp:invoke
    (and
    text
    write:analyses
    if you want
    text
    run_analysis
    ).
  • OR you're on a Basic-tier WALDO plan. MCP requires Professional or Enterprise.

"

text
run_analysis
returns an analysis ID but the analysis never finishes."

  • It runs asynchronously; the 6-phase chain takes 30 seconds to a few minutes depending on the market. The AI should poll
    text
    get_analysis
    (or
    text
    GET /v1/analyses/{id}/status
    ) until
    text
    status === 'completed'
    before calling
    text
    get_supply
    or asking for the recommendation. Tell the AI explicitly: "Wait for the analysis to complete, then summarize it."
  • Note:
    text
    get_shift_share
    ,
    text
    get_economic_base
    , and
    text
    get_market_metrics
    don't require completion — they can be called the moment
    text
    run_analysis
    returns the
    text
    analysis_id
    . Only
    text
    get_supply
    and the final recommendation are phase-dependent.

"I'm getting rate-limited."

  • The MCP endpoint is on the same per-user sliding-window rate limiter as the REST API (default 100 requests / 60 seconds). If the AI is hammering tools in a loop, slow it down with a clearer prompt.

Tier requirements + cost

MCP is included on Professional and Enterprise plans. Each MCP tool call bills 5 cost units against your key's monthly quota. Running an analysis via

text
run_analysis
doesn't double-bill — the QStash phase chain is internal infrastructure. Check usage in Settings → Billing.


For your integrator (or AI agent)

Everything below is the technical reference. Paste a link to this page into Claude / Cursor / ChatGPT — the AI has all the information it needs to wire up additional MCP integrations.

Endpoint + auth

text
POST https://www.waldocre.io/api/mcp
Authorization: Bearer waldo_live_<key>
Content-Type:  application/json
Accept:        application/json, text/event-stream

Stateless streamable-HTTP transport (

text
@modelcontextprotocol/sdk
server with
text
sessionIdGenerator: undefined
). Each POST is a complete JSON-RPC 2.0 request; the server doesn't maintain session state across calls. Auth is identical to the REST API: same
text
waldo_live_...
keys, same
text
withApiAuth
middleware, same scope enforcement, same audit log.

Tool responses are serialized through the same lib functions as the REST routes. A WALDO resource fetched via MCP is byte-equal to the JSON returned by the corresponding

text
/api/v1/*
route.

Tools

ToolScope requiredCost unitsPhase-dependent?Purpose
text
search_markets
none (any active key)5noLook up county/MSA
text
code
values. Common name forms are normalized —
text
Saint Louis
and
text
St. Louis
both resolve to
text
C41180
. Same for
text
Mount
/
text
Mt.
and
text
Fort
/
text
Ft.
text
list_analyses
text
read:analyses
5noList the caller's analyses
text
get_analysis
text
read:analyses
5noSummary (status, phase, market) — call this to poll completion
text
get_economic_base
text
read:analyses
5reads phase 2LQs, multipliers, top sectors — returns
text
not_ready
if phase 2 hasn't run
text
get_market_metrics
text
read:analyses
5reads phase 3Demographics / housing / economics — returns
text
not_ready
if phase 3 hasn't run
text
get_supply
text
read:analyses
5reads phase 5Pipeline + supply/demand gap — returns
text
not_ready
if phase 5 hasn't run
text
get_shift_share
text
read:analyses
5no — computes freshCCIM CI 102 decomposition for any base/end period. Computes from BLS QCEW on every call; only needs the analysis's
text
market_code
/
text
market_type
/
text
market_name
, so it works the moment
text
run_analysis
returns.
text
run_analysis
text
write:analyses
5n/a (creates)Start a new analysis. Required:
text
market_code
,
text
market_type
,
text
market_name
. Optional:
text
analysis_year
,
text
analysis_quarter
(default to the latest available QCEW quarter),
text
property_types
(default
text
["multifamily"]
),
text
analysis_type
,
text
analysis_name
. Returns
text
analysis_id
immediately.

text
run_analysis
returns immediately (HTTP 202-style). The 6-phase chain runs asynchronously through QStash. Quarters/years on
text
get_shift_share
accept either numbers (
text
3
) or strings (
text
"3"
) — clients that JSON-stringify numeric inputs are coerced automatically.

Recommended workflow

text
1. search_markets({ q })                         resolve human name → market_code
2. list_analyses({ status: "completed" })        check whether one already exists for this market/period
3. run_analysis({ market_code, market_type, market_name })
                                                 only if step 2 didn't find one
4a. get_shift_share({ analysis_id, ... })         independent of phase progress —
4a. get_economic_base({ analysis_id })            call any of these immediately
4a. get_market_metrics({ analysis_id })           after run_analysis returns
4b. get_analysis({ analysis_id })                 poll until status === "completed"
                                                 BEFORE calling get_supply
5.  get_supply({ analysis_id })                   only after status === "completed"

The same shape applies whether you're building a script, a webhook consumer, or feeding tools to an LLM. The most common mistake is skipping step 2 and creating a duplicate analysis when a finished one already exists.

Direct curl test

Smoke-test without an MCP client:

bash
# List available tools
curl -X POST "https://www.waldocre.io/api/mcp" \
  -H "Authorization: Bearer $WALDO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

# Call a tool — search_markets
curl -X POST "https://www.waldocre.io/api/mcp" \
  -H "Authorization: Bearer $WALDO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "search_markets",
      "arguments": { "q": "Lafayette", "type": "county", "state": "LA" }
    }
  }'

# Call a tool — run_analysis (year/quarter optional; defaults to latest QCEW)
curl -X POST "https://www.waldocre.io/api/mcp" \
  -H "Authorization: Bearer $WALDO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "run_analysis",
      "arguments": {
        "market_code": "C41180",
        "market_type": "msa",
        "market_name": "St. Louis, MO-IL"
      }
    }
  }'
# → { "analysis_id": "...", "status": "draft", "status_url": "/v1/analyses/.../status" }

# Call a tool — get_shift_share (period args accept "3" or 3; coerced)
curl -X POST "https://www.waldocre.io/api/mcp" \
  -H "Authorization: Bearer $WALDO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tools/call",
    "params": {
      "name": "get_shift_share",
      "arguments": {
        "analysis_id": "PASTE_FROM_RUN_ANALYSIS_RESPONSE",
        "base_year": 2019,
        "base_quarter": 4,
        "end_year": 2025,
        "end_quarter": 3,
        "naics_level": 3
      }
    }
  }'

Rate limiting + audit

  • Per-user sliding window via Upstash Redis (default 100 req / 60 sec, tier-overridable).
  • Each MCP HTTP request writes to
    text
    api_audit_log
    (route=
    text
    /api/mcp
    , cost_units=5, latency_ms, status). Use
    text
    X-Request-ID
    for support correlation.
  • Tier gating in
    text
    authenticateMCPRequest
    — Basic-tier keys 403 with the same problem-details body the REST API uses.

What's NOT in MCP yet

  • Streaming responses — the SDK supports SSE; we currently return JSON-only for simpler curl debugging.
  • Server-pushed notifications — clients poll
    text
    get_analysis
    for completion status when they need a phase-dependent result (
    text
    get_supply
    , recommendation).
    text
    get_shift_share
    ,
    text
    get_economic_base
    ,
    text
    get_market_metrics
    don't need polling. A future
    text
    notifications/...
    channel mirroring webhooks for the duration of the connection is on the roadmap.
  • Binary export resource
    text
    export
    isn't a great fit for MCP text/JSON results. Use
    text
    GET /v1/analyses/{id}/export?format=pdf|excel|pptx
    from REST or the Send-to-webhook/email/CRM tile on the Phase 6 dashboard instead.

Error format

JSON-RPC 2.0 error responses with WALDO-specific codes:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32602,
    "message": "Invalid params: market_code is required",
    "data": { "request_id": "..." }
  }
}

Source of truth

Tool implementations live in

text
lib/mcp/server.ts
. Each tool calls into the same serializer and loader functions as
text
/api/v1/*
so REST + MCP can never drift on a resource shape. Adding a new tool means: register it in
text
buildWaldoMCP(ctx)
, point its handler at the existing serializer, declare its scope.