Broker tool · Developer preview

Nora search

Brokers describe a client’s requirements in plain language. Nora turns them into editable criteria, the Broker Tool searches live listings over MCP, and every result says why it’s there. Clickable prototypes of the target UX, plus the contracts and rules to build it.

2MCP tools
8screens
16rule tests
4build phases

The request we design for“I’m looking for 5,000–7,000 square feet around Union Square, Flatiron, or NoHo. It needs to be a furnished full-floor opportunity. The client prefers a three-year term.”

01

Nora interprets. The Broker Tool decides.

Plain language in, criteria out

The request becomes a structured criteria object the broker can see and edit before trusting any result.

Must-haves vs nice-to-haves

Every criterion is tagged required or preferred. Required filters; preferred only ranks. The broker can flip either.

One source of truth

Nora never filters or stores listings. It calls search_spaces as the broker, with the broker’s permissions.

Exact and near, never blurred

Two separate groups. Near matches name exactly which requirement they miss. Requirements are never relaxed silently.

Unknown is not a match

Missing, unverified or stale fields show as unknown. Only verified values can satisfy a requirement.

Explainable ranking

Fixed point rules, the same every search, visible to the broker. Nora’s explanation is built only from returned fields.

02

Desktop

Each frame is the live prototype. Click inside to use it, or open it full size.

Dashboard · Ask NoraThe entry point sits under the greeting. “New Search” and the Nora nav item open the same flow. Open full size
Results · exact and nearEditable criteria on the left; Nora’s summary, exact matches, then near matches with their gaps. Open full size
No exact matchesNora says so plainly and shows the closest options, each with what it misses. Open full size
Listing detailLoaded with get_space_details. Each field shows verified, reported or unknown. Open full size
03

Mobile prototype

Each phone is live: type a request, edit criteria, tap a result. Scroll sideways for all four.

1 · Ask NoraComposer and example requests. Open
2 · ResultsCriteria stack above results. Open
3 · Near matches onlyNothing relaxed silently. Open
4 · Listing detailFull-screen sheet. Open
04

Build notes

Flow

  1. Ask. The broker types a request on the dashboard and starts a search.
  2. Interpret. Nora returns a criteria object, splitting requirements from preferences. The UI renders it as editable controls.
  3. Search. The server calls search_spaces on the Broker Tool MCP server as the broker.
  4. Show. The dashboard renders exact matches, then near matches, each with checks, key fields and a one-line reason. Editing a criterion re-runs step 3 only.

Criteria contract

Nora’s only output. The model returns structured output only; the server validates it against the schema and checks every neighborhood name against the Broker Tool’s list before searching. Listing data never goes into the interpretation prompt.

{
  "rsf":           { "min": 5000, "max": 7000, "required": true },
  "full_floor":    { "value": true, "required": true },
  "furnished":     { "value": true, "required": true },
  "neighborhoods": { "values": ["Union Square", "Flatiron", "NoHo"], "required": false },
  "term_months":   { "min": 36, "max": 36, "required": false },
  "statuses": ["Active"], "include_near_matches": true, "limit": 10
}
CriterionDefault when the request doesn’t sayBecomes required onBecomes preferred on
SizeRequired. A single figure becomes ±10%.“needs”, “must”, “at least”“ideally”, “prefers”
Full floorRequired“needs to be”, “must”“would like”, “nice to have”
FurnishedRequired. Plug-and-play and turnkey count.“needs”, “must”“prefers”, “ideally”
NeighborhoodsPreferred, ordered by mention. First listed is the first choice.“must be in”, “only in”“around”, “near”
TermPreferred“must”, “non-negotiable”“prefers”, “ideally”

Anything Nora can’t map to a field is returned in unparsed and shown to the broker, never guessed into a filter.

Match rules (first match wins)

  1. Excluded: status is not in statuses (default Active only), or the broker can’t see the listing.
  2. Exact: every required criterion is met, and the listing was verified within 60 days.
  3. Near: one or two required criteria are missed or unknown, and size, if missed, is within 15% of the range.
  4. Not shown: everything else.

Preferred criteria never decide which group a listing is in. They only rank within a group.

Field states

StateMeaningAgainst a requirementUI
verifiedConfirmed in the listing recordmet or missedValue, green or red check
unverifiedReported by the listing broker, not confirmedunknown“Listed as furnished, not verified”
unknownNot in the recordunknown“Unknown”, dashed grey
stalelast_verified older than 60 daysblocks exact“Verified 83 days ago, over 60-day limit”

Ranking

Computed in the Broker Tool so every client gets the same order. Score is points earned over points available, 0–100. Ties go to the most recently verified listing. Near matches sort by fewest gaps first, then score.

SignalPointsApplies when
Area preference1st choice 40, other listed area 30, else 0Neighborhoods are preferred (20 / 14 if required)
Term preference20 if the ranges overlapTerm is preferred
Furnished / full floor15 eachEither is preferred
Size fitUp to 15, highest at the middle of the rangeRange has min and max
Recency10 if ≤14 days, 6 if ≤30, 3 if ≤60Always

MCP tools

Both are read-only and idempotent. Full JSON Schemas are in mcp/tools.json; real request and response pairs from the mock are in mcp/examples/.

search_spaces(criteria) →
{
  "searched_at": "2026-10-21T21:14:00Z",
  "listings_scanned": 21,
  "excluded_by_status": 1,
  "exact_matches": [ { "listing": {…}, "match": "exact", "checks": [...], "score": 99, "score_parts": [...] } ],
  "near_matches":  [ { "listing": {…}, "match": "near",  "checks": [...], "score": 74 } ]
}

listing.furnished = { "state": "verified" | "unverified" | "unknown", "value": true | false | null }
check = { "key": "furnished", "required": true, "result": "met" | "missed" | "unknown", "detail": "…" }

get_space_details({ "id": "L-1115" }) → full record + asking_rent_psf, building_class,
                                        ceiling_height, listing_broker, notes
                                        error: not_found (missing or not visible)

Server routes

Two routes on the current Next.js app. Editing criteria calls only the second, so the model isn’t re-run on every change.

POST /api/nora/interpret
{ "input": "I'm looking for 5,000–7,000 square feet around…" }
200 { "criteria": { … see contract … }, "unparsed": [] }

POST /api/nora/search
{ "criteria": { … }, "request_id": "uuid" }
  → calls search_spaces over MCP with the broker's session
200 { "result": { …search_spaces output… },
      "summary": "6 listings meet every requirement…",
      "reasons": { "L-1041": "Meets every requirement. It's in Union Square…" } }

Summaries and per-listing reasons are templated from checks and returned fields (see explain() in src/nora.js). If a model rewrites them later, validate that every figure and field it mentions appears in the result.

Permissions and data

  • The MCP call carries the broker’s identity. The Broker Tool applies its existing visibility rules; Nora adds none and bypasses none.
  • get_space_details runs the same check, so a link to a result can’t leak a listing.
  • Results are not cached across brokers. Re-running a search always hits the Broker Tool.
  • Rent and lease economics stay out of model prompts.

Acceptance

MVP criterionHow it’s metTest
Start from natural languageAsk Nora on the dashboard, New Search, Nora nav itemInterpretation suite
Editable criteriaCriteria panel; each row toggles on and must/nice; edits re-searchManual
Current, clickable, rankedLive MCP call with timestamp; cards open get_space_details; fixed ranking“ranking is deterministic”
Exact vs near distinctSeparate sections, solid vs dashed cards, gaps in red or grey“near matches state their gaps”
Never invents detailsUnknown and unverified never count as met; reasons built from fieldsMatching suite

Run node tests/engine.test.js (Node 18+, no installs). Port the same cases to the real server’s test suite.

Edge cases

  • Nothing understood: no search runs; Nora asks for size, area, floor or term.
  • No exact matches: say so first, then near matches. Never move a near match into the exact group.
  • Neighborhood not in the Broker Tool’s list: show it in unparsed with “not a known area”.
  • Listing changes between search and open: details reflect the new record; if it’s gone or no longer visible, say so and offer to re-run.
  • MCP timeout or error: keep the criteria, show the error and a retry. Don’t show stale results as current.

Phases

1
Broker Tool MCP server. search_spaces and get_space_details, field states, match rules, ranking, permission checks, tests.
2
Interpretation. /api/nora/interpret with structured output, schema validation, neighborhood validation, unparsed.
3
Dashboard UI. Ask Nora, criteria panel, exact and near groups, detail drawer, ranking and trace disclosures.
4
After MVP. Save as a saved search, add results to a Space Book, alerts when a new listing becomes an exact match.