More and more renters start an apartment search by asking an AI assistant: "find me a one-bedroom in Astoria under $3,200 near the N train." Without live data, the assistant answers from old articles and guesses. So we built a remote MCP server for Buzzedin, our NYC apartment search, so ChatGPT, Claude and other MCP clients can query live listings directly.

This post covers how it's built, the guardrails we put in the server itself, and a few things that tripped us up.

The shape of it

The server is a single route in our existing Next.js app, /mcp, speaking MCP over streamable HTTP. There's no separate service and no separate deploy: the same search code that powers the website answers the assistant.

It exposes two public tools:

  • search_listings: price, bedrooms, neighborhoods or boroughs, move-in dates, commute time to an address, distance to places (a subway stop, a grocery store), amenities, rent-stabilized buildings and lease takeovers. It also takes softer qualities like "lots of natural light" or "quiet". It returns a page of listings with how each rent compares to its neighborhood median, a few structured features (laundry, outdoor space, pets) and a public link for each apartment.
  • create_alert_link: turns the same filters into a link that sets up an email or push alert on our site.

When a renter connects their account, three more tools appear: save_alert, list_my_alerts and get_my_matches.

server.registerTool( 'search_listings', { title: 'Search NYC apartments', description: 'Searches active NYC rental listings ...', inputSchema: SearchInputSchema, }, async (input, extra) => { // same search function the website calls } ) 

The input schema comes from the same filter registry the website uses, so the assistant and the search page can never disagree about what "pets allowed" or "within 30 minutes of Midtown" means.

Let the model do the parsing

Our website has a natural-language search box that parses free text into filters. For MCP we deliberately skipped it. The assistant has already understood the request, so the tool asks it to fill the structured fields itself, and the server instructions say so plainly: "Call search_listings with every constraint the user gave. Fill the structured fields yourself."

That removed a whole class of bugs where our parser and the assistant interpreted the same sentence differently.

Put the guardrails in the server

Housing search comes with rules that a general-purpose model doesn't know. US fair housing law means we never describe a neighborhood as "safe" or "good". A building on New York's rent-stabilized list doesn't prove that a particular unit is stabilized. Lease takeovers posted by tenants are "direct from the tenant", not "verified".

We put these rules in the MCP server instructions, which clients read before calling any tool, and we keep the tool output strictly factual so there's nothing for the model to editorialize on. The rules travel with the server, so every client gets them, not just the ones we test against.

Show results inline (MCP Apps)

Clients that support MCP Apps can render a UI resource next to the tool result. Ours shows listing cards with photos, price and neighborhood comparison, plus paging inside the widget. Clients without UI support still get plain text and links, so nothing depends on the widget.

The HTML is built at deploy time with its JavaScript and CSS inlined. It loads nothing from the network and asks for no permissions.

Sign-in without a second auth system

The account tools use OAuth through our existing Supabase auth, so a renter signs in on our site and approves the assistant on a consent page. Two lessons:

  1. Ask only for the scopes you need. Our tokens are HS256-signed, so requesting openid got clients expecting an ID token we couldn't issue the way they wanted. Asking for email only fixed it.
  2. Declare securitySchemes on each tool and in tools/list. Some clients read the per-tool declaration and some read the top-level list, so we repeat it in both places.

Getting listed

The server is in the official MCP Registry as com.buzzedin/nyc-apartments. The registry lets you prove you own a domain namespace over HTTP: you serve a public key at /.well-known/mcp-registry-auth and sign the publish with the matching private key. Directories like Glama copy listings from the registry, so one publish reaches several places.

The listing itself is a small server.json:

{ "name": "com.buzzedin/nyc-apartments", "title": "Buzzedin", "remotes": [ { "type": "streamable-http", "url": "https://buzzedin.com/mcp" } ] } 

We also keep a public repo, Buzzedin/mcp, with the README, that same server.json and client configs. It holds no server code; it's what directories and awesome lists link to.

Try it

Add https://buzzedin.com/mcp as a connector in any MCP client that supports remote servers, then ask something like "Find a two-bedroom in Bushwick under $3,800 with laundry in the building."

Questions about the build are welcome in the comments.