MCP tool reference

Search contacts

search_persons

search_persons finds contacts: deterministic full-text across every descriptive field of the record — name, notes, email, phone, address — combined with relationship-graph filters: restrict by kind, and by the role a linked contact holds (with_role / without_role). It never creates anything.

Semantics

How matching works

The query runs as PostgreSQL full-text search over the contact's indexed descriptive fields. Matching is accent-insensitive ("francois" finds "François"), whole-word and stemmed, not substring. Role filters run inside the same visibility-scoped database function as the text match.

  • The full-text index covers exactly the descriptive fields: name, notes, email, phone and address.
  • Company registration and VAT numbers are not in the full-text index: read them from the record itself (get_person_context) rather than searching for them with query.
  • Accent-insensitive both ways: a query typed without accents matches accented stored text, and an accented query matches unaccented text.
  • A role matches the label of a relationship (read in either direction) OR a tag carried by the linked contact: with_role=supplier finds contacts linked to someone labelled supplier or tagged supplier.
  • without_role is a server-side anti-join: it keeps only contacts with NO link matching the role, evaluated over the whole contact base — not a client-side filter over a first page of results.
  • Visibility applies to the contacts returned AND to both endpoints of every relationship considered: a link to a contact you cannot see neither matches a role nor appears in links.
  • Responses are capped at limit (at most 50) contacts sorted by display name, and there is no pagination cursor: if a search may exceed the cap, narrow it with query, kind or a role filter.
Input

Parameters

Everything the tool accepts, straight from the live inputSchema. All parameters are optional.

ParameterTypeDescription
querystring (<= 200)Full-text search across the contact's name, notes, email, phone and address. Accent-insensitive, whole-word and stemmed, not substring.
kind"individual" | "company"Restrict to individuals or companies.
with_rolestring (<= 100)Only contacts that have a linked contact in this role — matched on the relationship label (either direction) or on a tag of the linked contact.
without_rolestring (<= 100)Only contacts with NO linked contact in this role. Example: kind=company with without_role=sales lists companies without a salesperson.
limitnumberMaximum results. Defaults to 20, capped at 50.
Output

Response

Each contact comes back compact: identity, phone and email, a notes preview, tags, and its relationship links with their direction-aware labels. Fetch the full record with get_person_context.

Example structuredContent
{
  "contacts": [
    {
      "id": "b2ce0d4f-7e19-4c2b-8f3a-5d6e7f8a9b0c",
      "display_name": "François Martin",
      "kind": "individual",
      "phone": "+33 6 12 34 56 78",
      "email": "francois@boulangerie-martin.fr",
      "notes": "Prefers morning deliveries - ring the back door...",
      "tags": [
        "supplier"
      ],
      "links": [
        {
          "id": "0a1b2c3d-4e5f-4789-8bcd-ef0123456789",
          "name": "Boulangerie Martin",
          "kind": "company",
          "relationship": "manager",
          "note": null
        }
      ]
    }
  ]
}
Empty result
{
  "contacts": []
}
outputSchema, as served by tools/list
{
  "type": "object",
  "properties": {
    "contacts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "kind": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "phone": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "notes": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "relationship": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "note": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "additionalProperties": {}
            }
          }
        },
        "required": [
          "id",
          "display_name",
          "kind",
          "phone",
          "email",
          "notes",
          "tags",
          "links"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "contacts"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#",
  "additionalProperties": false
}
Recipes

Usage examples

01

Find a contact from a descriptive detail

Ask your assistant
"Who was the contact who prefers morning deliveries?"
MCP arguments
{
  "query": "morning deliveries"
}
Response (abridged)
{
  "contacts": [
    {
      "id": "b2ce0d4f-...",
      "display_name": "François Martin",
      "kind": "individual",
      "notes": "Prefers morning deliveries - ring the back door...",
      "tags": [
        "supplier"
      ]
    }
  ]
}
02

Search despite a missing accent

Ask your assistant
"Find the contact called francois."
MCP arguments
{
  "query": "francois"
}
Response (abridged)
{
  "contacts": [
    {
      "id": "b2ce0d4f-...",
      "display_name": "François Martin",
      "kind": "individual",
      "email": "francois@boulangerie-martin.fr",
      "tags": [
        "supplier"
      ]
    }
  ]
}
03

Companies having a contact in a role

Ask your assistant
"Which companies have a sales contact?"
MCP arguments
{
  "kind": "company",
  "with_role": "sales"
}
Response (abridged)
{
  "contacts": [
    {
      "id": "7d8e9f0a-...",
      "display_name": "Atelier Lemaire",
      "kind": "company",
      "tags": [
        "client"
      ],
      "links": [
        {
          "id": "3c4d5e6f-...",
          "name": "Claire Lemaire",
          "kind": "individual",
          "relationship": "sales",
          "note": null
        }
      ]
    }
  ]
}
04

Companies missing a role

Ask your assistant
"Which companies have no salesperson yet?"
MCP arguments
{
  "kind": "company",
  "without_role": "sales"
}
Response (abridged)
{
  "contacts": [
    {
      "id": "9e0f1a2b-...",
      "display_name": "Garage Petit",
      "kind": "company",
      "tags": [
        "prospect"
      ],
      "links": []
    }
  ]
}
05

A role can come from a label or a tag

Ask your assistant
"Who are our suppliers?"
MCP arguments
{
  "with_role": "supplier"
}
Response (abridged)
{
  "contacts": [
    {
      "id": "b2ce0d4f-...",
      "display_name": "François Martin",
      "kind": "individual",
      "tags": [],
      "links": [
        {
          "id": "0a1b2c3d-...",
          "name": "Boulangerie Martin",
          "kind": "company",
          "relationship": "supplier",
          "note": null
        }
      ]
    },
    {
      "id": "5f6a7b8c-...",
      "display_name": "Vergers Rousseau",
      "kind": "company",
      "tags": [
        "supplier"
      ],
      "links": []
    }
  ]
}
Contract

Guarantees and limits

  • Both searches are deterministic PostgreSQL full-text search: same query, same data, same results. Nothing semantic, no vector matching — the query matches words, not meanings.
  • Visibility is resolved before matching: the server first scopes the query to the records the caller may see, and only then looks for matches.
  • A record you cannot see is indistinguishable from a record that does not exist: search never confirms the existence of hidden data.
  • Indexes are recomputed in the same transaction as every write: an entry is searchable the instant it is created, with no background indexer and no consistency window.
  • Historis runs no AI model to find results. Your agent decides what to search for and ranks or interprets what comes back; the matching itself is plain database search.
Go further

Two searches, two jobs

search_events
Searches the timeline: what happened, what is due, what changed. Composes full-text with business and time filters, keyset-paginated.
search_persons
Searches the contact records and the relationship graph: who someone is, how they are connected, which role links them.