---
title: "Public tools"
description: "Reference for the Matchai MCP tools search_listings, get_listing and get_professional. Inputs, outputs, examples and the listing-card widget."
last_updated: "2026-10-21"
lang: "en"
canonical: "https://matchai.co/docs/developers/mcp-server/public-tools"
---

# Public tools

> Reference for the Matchai MCP tools search_listings, get_listing and get_professional. Inputs, outputs, examples and the listing-card widget.

Source: https://matchai.co/docs/developers/mcp-server/public-tools

The [MCP server](https://matchai.co/docs/developers/mcp-server) offers three tools. All of them are read-only, need no account, and return published data only.

| Tool | Purpose |
|---|---|
| `search_listings` | Find available listings by place, price, bedrooms, bathrooms, property type, and sale or rent. |
| `get_listing` | Get the full public details of one listing from any of its addresses or from its id. |
| `get_professional` | Get a broker's public profile and their available listings. |

## How the tools behave [#how-the-tools-behave]

- **Read-only.** Every tool is annotated `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: false`. They only read Matchai's own published catalogue.
- **Three parts in every result.** `content` is plain text a model can quote. `structuredContent` is typed data that matches the tool's `outputSchema`. `_meta` is for the [listing-card widget](#the-listing-card-widget) and can be ignored by other clients.
- **Unknown means null.** A fact that the broker did not state is `null`, never `0`. A listing with no stated price has `price: null`.
- **Forgiving arguments.** `null` values and empty strings are dropped, and numbers sent as strings, such as `"600000"` or `"$600,000"`, are converted before validation.
- **Errors a model can fix** come back as a result with `isError: true` and a message. A call that names an unknown tool, or whose `arguments` is not an object, is a JSON-RPC error `-32602`. See [errors](https://matchai.co/docs/developers/mcp-server#errors).

## The listing object [#the-listing-object]

`search_listings` and `get_professional` return listing objects, and `get_listing` returns the same object with extra detail fields.

<TypeTable
  type={{
    id: { type: 'string', description: 'The listing id, a UUID. Pass it to get_listing.' },
    title: { type: 'string', description: 'The listing title.' },
    url: { type: 'string', description: "The listing page. This is the broker's own domain once it is live, and the address to cite and link to." },
    price: { type: 'number | null', description: 'The asking price, or the rent, in the listing currency. null when the broker set none.' },
    currency: { type: 'string', description: 'ISO 4217 currency code, usually CAD.' },
    priceText: { type: 'string | null', description: 'The price as people read it, for example $589,000. null when there is no price.' },
    transaction: { type: '"sale" | "rent" | null', description: 'Whether the property is for sale or for rent.' },
    status: { type: '"available" | "sold" | "rented"', description: 'Availability. Search returns available listings only.' },
    propertyType: { type: '"house" | "condo" | "apartment" | "townhouse" | "plex" | "land" | "cottage" | "commercial" | null', description: 'Kind of property. plex covers duplexes up to quintuplexes.' },
    bedrooms: { type: 'number | null', description: 'Number of bedrooms.' },
    bathrooms: { type: 'number | null', description: 'Number of bathrooms. A half bathroom counts 0.5.' },
    livingAreaSqft: { type: 'number | null', description: 'Living area in square feet, converted when the broker entered square metres.' },
    neighbourhood: { type: 'string | null', description: 'Neighbourhood or borough.' },
    city: { type: 'string | null', description: 'City or municipality.' },
    region: { type: 'string | null', description: 'Province or region.' },
    location: { type: 'string | null', description: 'The locality line the listing page shows, for example Plateau-Mont-Royal, Montréal.' },
    image: { type: 'string | null', description: 'URL of the cover photo.' },
    broker: { type: '{ name: string, brokerage: string | null } | null', description: 'The listing broker.' },
    site: { type: '{ name: string | null, url: string }', description: "The broker's Matchai Site that publishes the listing." },
    updatedAt: { type: 'string', description: 'When the listing last changed, as an ISO 8601 timestamp.' },
  }}
/>

## search_listings [#search_listings]

Searches published listings that are available now. It returns up to 10, best match first.

### Input [#input]

Every field is optional. With no arguments, the most recently updated listings are returned.

<TypeTable
  type={{
    query: { type: 'string', description: 'What the person asked for, in their own words and in any language, for example 3 bedroom condo near a metro in Laval. Up to 300 characters.' },
    city: { type: 'string', description: 'City or municipality, for example Montréal or Gatineau. Up to 80 characters. Matched as a whole place name, ignoring accents.' },
    neighbourhood: { type: 'string', description: 'Neighbourhood or borough, for example Plateau-Mont-Royal. Up to 80 characters.' },
    transaction: { type: '"sale" | "rent"', description: 'sale to buy, rent to rent.' },
    propertyType: { type: '"house" | "condo" | "apartment" | "townhouse" | "plex" | "land" | "cottage" | "commercial"', description: 'Kind of property.' },
    minPrice: { type: 'integer', description: 'Lowest price in dollars, or monthly rent for rentals. 0 to 1,000,000,000.' },
    maxPrice: { type: 'integer', description: 'Highest price in dollars, or monthly rent for rentals. 0 to 1,000,000,000.' },
    minBedrooms: { type: 'integer', description: 'At least this many bedrooms. 0 to 20.' },
    minBathrooms: { type: 'number', description: 'At least this many bathrooms. 0 to 20. A half bathroom counts 0.5.' },
    limit: { type: 'integer', default: '6', description: 'How many listings to return. 1 to 10.' },
  }}
/>

`query` is read with the same parser as [listing search](https://matchai.co/docs/developers/listing-search#what-the-query-understands): prices, bedrooms, bathrooms, sale or rent and property type are pulled out of the sentence, in English, French or Spanish. An explicit argument always wins over what the sentence says, and the sentence fills in whatever the arguments leave out. If `minPrice` is higher than `maxPrice`, the two are swapped.

### Output [#output]

<TypeTable
  type={{
    kind: { type: '"listings"', description: 'Identifies the result type.' },
    filters: { type: 'object', description: 'The filters the search applied: keywords, city, neighbourhood, transaction, propertyType, minPrice, maxPrice, minBedrooms and minBathrooms. Each is null when it was not applied. keywords holds up to 6 lowercase words, without accents, that were matched against titles, places, property types and descriptions.' },
    count: { type: 'integer', description: 'The number of listings returned.' },
    listings: { type: 'listing[]', description: 'The matching listings, best match first. See the listing object above.' },
  }}
/>

The text in `content` starts with the number of listings and a statement of how they are ordered. It then lists one entry per listing with the title, price and type, the facts, the place, the listing broker and the URL.

An empty result says whether Matchai checked every listing or only the most recently updated ones. It never reports "none exist" for a search that did not reach further back. A city or a price range lets the search look further.

### Example [#example]

Send this as a `tools/call` request:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "search_listings",
    "arguments": { "query": "3 bedroom condo in Laval under 600k" }
  }
}
```

An abridged result. This search matched one listing, and the `filters` object shows how the sentence was read:

```json
{
  "content": [
    {
      "type": "text",
      "text": "1 published listing on Matchai. Results are ordered by relevance to the request and by how recently the listing was updated. No broker can pay to change the order or to be included.\n\n1. Bright 3-bedroom condo near Montmorency metro — $585,000, condo for sale\n   3 bedrooms · 1.5 bathrooms · 1,120 sq ft · Montmorency, Laval, Québec\n   Listed by Alice Tremblay (Agence Exemple)\n   https://alice.matchai.co/bright-condo-montmorency"
    }
  ],
  "structuredContent": {
    "kind": "listings",
    "filters": {
      "keywords": ["laval"],
      "city": null,
      "neighbourhood": null,
      "transaction": null,
      "propertyType": "condo",
      "minPrice": null,
      "maxPrice": 600000,
      "minBedrooms": 3,
      "minBathrooms": null
    },
    "count": 1,
    "listings": [
      {
        "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "title": "Bright 3-bedroom condo near Montmorency metro",
        "url": "https://alice.matchai.co/bright-condo-montmorency",
        "price": 585000,
        "currency": "CAD",
        "priceText": "$585,000",
        "transaction": "sale",
        "status": "available",
        "propertyType": "condo",
        "bedrooms": 3,
        "bathrooms": 1.5,
        "livingAreaSqft": 1120,
        "neighbourhood": "Montmorency",
        "city": "Laval",
        "region": "Québec",
        "location": "Montmorency, Laval, Québec",
        "image": "https://media.example.com/listings/bright-condo-montmorency/cover.jpg",
        "broker": { "name": "Alice Tremblay", "brokerage": "Agence Exemple" },
        "site": { "name": "Alice Tremblay Immobilier", "url": "https://alice.matchai.co" },
        "updatedAt": "2026-10-12T14:03:22.000Z"
      }
    ]
  },
  "_meta": {
    "ui": { "resourceUri": "ui://matchai/listing-cards.html?v=3f9a1c2b7d10" }
  }
}
```

The real `_meta` object has a few more keys for the widget. The names, addresses and photo host in these examples are made up.

## get_listing [#get_listing]

Returns the full public details of one published listing.

### Input [#input-1]

Send `url` or `id`. If you send both, `id` is used.

<TypeTable
  type={{
    url: { type: 'string', description: 'Any address of the listing page. Up to 2048 characters. The scheme can be left out.' },
    id: { type: 'string', description: 'The listing id, a UUID, from an earlier result.' },
  }}
/>

These addresses all name the same listing, with or without a trailing slash or tracking parameters:

- the broker's Matchai Site, such as `https://alice.matchai.co/bright-condo-montmorency`;
- the older form with `/listing/` before the slug, such as `https://alice.matchai.co/listing/bright-condo-montmorency`;
- the broker's own domain, when the site has one;
- the path form on the platform, such as `https://matchai.co/site/alice/bright-condo-montmorency`;
- the Markdown copy of any of these, ending in `.md`;
- the bare listing id.

### Output [#output-1]

<TypeTable
  type={{
    kind: { type: '"listing"', description: 'Identifies the result type.' },
    listing: { type: 'listing object + details', description: 'The listing object above, plus the fields below.' },
  }}
/>

The extra fields on `listing`:

<TypeTable
  type={{
    description: { type: 'string | null', description: "The broker's description, shortened to 1,500 characters." },
    highlights: { type: 'string[]', description: 'Up to 8 highlights the broker chose.' },
    yearBuilt: { type: 'number | null', description: 'Year the building was built.' },
    lotSizeSqft: { type: 'number | null', description: 'Lot size in square feet.' },
    address: { type: 'string | null', description: 'The street line. Present only when the broker published the address.' },
    postalCode: { type: 'string | null', description: 'Present only when the broker published the address.' },
    images: { type: 'string[]', description: 'Up to 8 photo URLs, cover photo first.' },
    datePosted: { type: 'string | null', description: 'When the listing was first posted.' },
  }}
/>

Search returns available listings only. `get_listing` can also return a published listing that has since been sold or rented, so check `status`.

If no published listing matches, the result has `isError: true` and says that the listing may have been unpublished or that the address may be incomplete.

### Example [#example-1]

```json
{
  "name": "get_listing",
  "arguments": { "url": "https://alice.matchai.co/bright-condo-montmorency" }
}
```

The `structuredContent` of the result, abridged:

```json
{
  "kind": "listing",
  "listing": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "title": "Bright 3-bedroom condo near Montmorency metro",
    "url": "https://alice.matchai.co/bright-condo-montmorency",
    "price": 585000,
    "currency": "CAD",
    "priceText": "$585,000",
    "transaction": "sale",
    "status": "available",
    "propertyType": "condo",
    "bedrooms": 3,
    "bathrooms": 1.5,
    "livingAreaSqft": 1120,
    "neighbourhood": "Montmorency",
    "city": "Laval",
    "region": "Québec",
    "location": "Montmorency, Laval, Québec",
    "image": "https://media.example.com/listings/bright-condo-montmorency/cover.jpg",
    "broker": { "name": "Alice Tremblay", "brokerage": "Agence Exemple" },
    "site": { "name": "Alice Tremblay Immobilier", "url": "https://alice.matchai.co" },
    "updatedAt": "2026-10-12T14:03:22.000Z",
    "description": "A bright corner unit on the fourth floor, five minutes from the metro. …",
    "highlights": ["Two balconies", "Indoor parking", "Five minutes from the metro"],
    "yearBuilt": 2012,
    "lotSizeSqft": null,
    "address": null,
    "postalCode": null,
    "images": [
      "https://media.example.com/listings/bright-condo-montmorency/cover.jpg",
      "https://media.example.com/listings/bright-condo-montmorency/living-room.jpg"
    ],
    "datePosted": "2026-09-30"
  }
}
```

Here the broker did not publish the street address, so `address` and `postalCode` are `null`.

## get_professional [#get_professional]

Returns a broker's public profile with their available listings, up to 10.

### Input [#input-2]

Send `username` or `url`. If you send both, `username` is used.

<TypeTable
  type={{
    username: { type: 'string', description: "The broker's Matchai username, for example alice_tremblay. A leading @ is ignored. Up to 64 characters." },
    url: { type: 'string', description: "The broker's Matchai Site, such as https://alice.matchai.co, their Matchai Page, such as https://alice.matchai.pro, their business card or profile, or any one of their listing pages. Up to 2048 characters." },
  }}
/>

### Output [#output-2]

<TypeTable
  type={{
    kind: { type: '"professional"', description: 'Identifies the result type.' },
    professional: { type: 'object', description: 'The broker. See the fields below.' },
    listings: { type: 'listing[]', description: "The broker's available published listings, up to 10." },
  }}
/>

The fields of `professional`:

<TypeTable
  type={{
    name: { type: 'string', description: "The broker's name." },
    username: { type: 'string | null', description: 'Their Matchai username.' },
    jobTitle: { type: 'string | null', description: 'Their professional title.' },
    brokerage: { type: 'string | null', description: 'The agency they work with.' },
    licence: { type: '{ number: string, body: string | null } | null', description: 'Their licence number and the regulator, for example OACIQ. null when no licence is on file.' },
    photo: { type: 'string | null', description: 'URL of their profile photo.' },
    siteUrl: { type: 'string | null', description: 'Their Matchai Site, when it is published.' },
    pageUrl: { type: 'string | null', description: 'Their Matchai Page, when it is published.' },
    contact: { type: '{ email: string | null, phone: string | null }', description: 'Their email address and phone number. Both are null unless the broker published them on their business card.' },
  }}
/>

A broker is returned only if they chose to be public somewhere: a published site, a published Matchai Page or an enabled business card. Otherwise the result has `isError: true` and says that no broker with a public Matchai profile matches.

### Example [#example-2]

```json
{
  "name": "get_professional",
  "arguments": { "username": "alice_tremblay" }
}
```

The `professional` part of the result's `structuredContent`. The `listings` array next to it holds listing objects like those in `search_listings`.

```json
{
  "name": "Alice Tremblay",
  "username": "alice_tremblay",
  "jobTitle": "Real estate broker",
  "brokerage": "Agence Exemple",
  "licence": { "number": "123456", "body": "OACIQ" },
  "photo": "https://media.example.com/avatars/alice-tremblay.jpg",
  "siteUrl": "https://alice.matchai.co",
  "pageUrl": "https://alice.matchai.pro",
  "contact": { "email": null, "phone": null }
}
```

## The listing-card widget [#the-listing-card-widget]

When a tool returns listings, assistants that support MCP Apps can show them as cards inside the conversation. The cards come from one resource that every tool points to.

| Detail | Value |
|---|---|
| URI | `ui://matchai/listing-cards.html?v=…`, where `v` is a version that changes whenever the widget changes |
| Type | `text/html;profile=mcp-app` |
| Name | `listing-cards` (title "Listing cards") |
| Referenced from | `_meta.ui.resourceUri` on each tool in `tools/list` and on each result. ChatGPT reads the same URI from its own key in `_meta`. |

Read the current URI from `tools/list` instead of hard-coding it. `resources/read` also accepts the URI without `v`, and any older `v`, and answers with the current widget.

`resources/list` returns the one resource, including the content security policy the assistant should apply:

```json
{
  "resources": [
    {
      "uri": "ui://matchai/listing-cards.html?v=3f9a1c2b7d10",
      "name": "listing-cards",
      "title": "Listing cards",
      "description": "Interactive cards for the listings a Matchai tool returned.",
      "mimeType": "text/html;profile=mcp-app",
      "_meta": {
        "ui": {
          "csp": {
            "connectDomains": ["https://matchai.co"],
            "resourceDomains": ["https://matchai.co", "https://media.example.com"]
          },
          "prefersBorder": true
        }
      }
    }
  ]
}
```

`resources/read` returns the widget as one self-contained HTML document in `contents[0].text`. It uses no framework and no external font or script.

What the widget does:

- **Shows the result by type.** `kind: "listings"` becomes a scrolling row of cards, `kind: "listing"` becomes one larger card with a short description, and `kind: "professional"` becomes the broker's name, role and a **Visit website** link above a row of cards.
- **Follows the assistant.** It uses the assistant's colour theme and language (English, French or Spanish) and opens links through the assistant.
- **Handles failure.** A result with `isError: true`, or no `structuredContent`, shows "Nothing to show".
- **Reports that a card was seen.** When a card has been on screen long enough to be read, the widget tells Matchai which listings were visible, for how long and how much of each, and the assistant's coarse platform (web, desktop or mobile). If the result carries no token for this report, nothing is sent. It never sends conversation text or a user identifier.

An assistant that does not render MCP Apps ignores the widget. The text in `content` and the data in `structuredContent` carry the whole answer. If you build your own cards, link each one to the listing's `url`.

<Cards>
  <Card title="MCP server" href="https://matchai.co/docs/developers/mcp-server">
    Endpoints, transport and how to connect an assistant.
  </Card>
  <Card title="Listing search" href="https://matchai.co/docs/developers/listing-search">
    The same search as a plain HTTP endpoint.
  </Card>
</Cards>

## Sitemap

- [Every page of Matchai Docs](https://matchai.co/docs/sitemap.md)
- [llms.txt](https://matchai.co/docs/llms.txt)
