# Background check MCP server tutorial: run a registry search from Claude or Cursor

> Connect an MCP client to the OffenderSearch server, see which of the eight tools an agent reaches for, give it the rules that keep a registry answer honest, and read one run against the sandbox.

- **HTML:** https://offendersearch.app/docs/learn/background-check-mcp-server-tutorial
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## What the background check MCP server is, and its status today

The OffenderSearch MCP server is a Model Context Protocol endpoint that exposes the [sex offender API](/sex-offender-api) and the [sex offender map API](/sex-offender-map-api) as tools an MCP client can call. Point Claude Desktop, Cursor or any client that supports remote MCP servers with custom headers at it, and the model can search 58 US registries, run a list of people, ask what is covered, and read a map, with the same scored, cited records the REST API returns. The server is open source at [github.com/offendersearch/mcp](https://github.com/offendersearch/mcp), which is where the tool schemas, the per-client install notes and the stdio variant live.

> **Private beta.** The server is built and the tools below are final, but the hosted endpoint is not open to self-serve sign-ups yet. Create an account, then ask for MCP access from the [MCP server page](/mcp-server). It uses the same API key and the same billing as the REST API, and an `os_test_` key routes to the [sandbox](/sandbox), where nothing is billed.

Two things to be clear about before you build. First, a tool result is a public-records lookup, not a consumer report; an agent must never decide a hire, a tenancy, credit or insurance from it, and the rules below make that explicit in the system prompt. Second, the tools cover the registry and the map. Criminal records (jail bookings, court cases, custody, warrants) are on the same key through the REST API only, which the last section shows. A fuller description of both layers is on the [background check API](/background-check-api) page.

## Configure Claude or Cursor for the MCP server

One config block. The key goes in the `X-API-Key` header, exactly as it would on a REST call; the client sends it with every tool invocation. Use the test key while you develop and swap it for the live key when you go live.

*MCP client config (Claude Desktop, Cursor and compatible clients)*
```json
{
  "mcpServers": {
    "offendersearch": {
      "url": "https://mcp.offendersearch.app",
      "headers": { "X-API-Key": "os_live_••••" }
    }
  }
}
```

Keep the key in the client's secret configuration, never in a shared prompt or a repository. The key is the account: anyone who can read it can run metered searches on it, and keys are minted and revoked in the dashboard. Once the client connects it lists the tools with their input schemas and the model picks one when a question fits.

## The eight MCP server tools and what each one costs

Three registry tools and five map tools. Each is metered like the REST call it wraps: a registry search is $0.15 a call, $0.11 past 2,000 calls in a month, with 25 free calls on sign-up and no card; the coverage tool is free; the map tools are billed at the Map API prices, for accounts that have turned the map on.

| Tool | What it does | REST equivalent | Billing |
| --- | --- | --- | --- |
| `search_offenders` | Search 58 US registries by name, date of birth or age, address, ZIP or a latitude/longitude radius. Returns the same scored, de-duplicated, cited records as the REST API. | `POST /v1/search` | One search per call |
| `batch_search` | Up to 1,000 people in one call, one scored row out per row in, for an agent working through a roster, an applicant list or a sign-up sheet. | `POST /v1/batch` | One search per row |
| `get_registry_coverage` | The catalog of the 58 registries with scope, legal status and health, so the agent can say what it searched before it says what it found. | `GET /v1/sources` | Free, no key required |
| `map_viewport` | What to draw for one map view, as a bounding box or a centre and radius: clusters zoomed out, pins closer in, names close up. | `POST /v1/map/viewport` | One map request |
| `map_group` | The people behind a pin that stands for several people at one address or an approximate area, one page at a time. | `GET /v1/map/groups/{groupId}` | One map request per page |
| `map_area` | The same answer a map view gets, for a drawn polygon or a buffered line. | `POST /v1/map/area` | One map request |
| `map_route` | Counts per leg and per stop along a walking or bus route, with pins and distances but no names. | `POST /v1/map/route` | Route units |
| `map_open_record` | The complete record behind one marker id or saved record reference. Opening the same marker again is free. | `GET /v1/map/records/{markerId}` | One record open |

Tool results are the REST envelopes, unchanged: the agent reads `status`, `counts`, `sourceStatus[]` and `records[]` exactly as a program would, and every record carries `matchState`, `source.recordUrl` and `source.lastCheckedAt`. That is deliberate. A tool that returned a summary would hide the fields that make the answer checkable.

## From prompt to tool: how an agent picks an MCP background check tool

The model chooses a tool from its description and the shape of the question. These are the mappings you should expect, and the ones to test against when you write evaluations for your agent.

| Someone asks the agent | What it calls |
| --- | --- |
| "Is Jordan Rivera, born 1988-04-12, on any sex offender registry?" | `search_offenders` with name and `dob`: one person, one scored answer with its citation. On the sandbox this synthetic name returns a complete answer with zero records. |
| "Check these 40 volunteers before Saturday." | `batch_search` over the list, one row per volunteer, in input order, which the agent then summarises row by row. |
| "Who is registered within a mile of 123 Main St, Springfield?" | `search_offenders` with an address or a `lat`, `lng` and `radiusMiles`, returning registrants and their distances. |
| "Which registries did you actually check for Texas?" | `get_registry_coverage`, so the agent cites the catalog instead of guessing. |
| "How many registered addresses are in this part of Columbus, and who is at this pin?" | `map_viewport` for the view, then `map_open_record` for the one marker the user picks. |
| "Count registered addresses along this bus route." | `map_route`: counts per leg and stop, no names. |

A question that mixes a list with a place ("which of these 40 people live near the school?") is two tools: `batch_search` for the people, then the addresses on each record compared with the place. A question about criminal history is not a tool at all today; the agent should say so and your integration should route it to the REST call in the last section.

## System-prompt rules for a background check agent

The tools make an answer checkable; the system prompt makes the agent say the checkable parts. Put rules like these in front of any agent that can call them.

*System prompt*
```plain
You have registry search tools from OffenderSearch. Rules:
1. The results are a public-records lookup, not a consumer report. Never decide, recommend
   or score a hiring, housing, credit or insurance outcome from them. If asked to, say that
   such a decision has to run as an FCRA order and stop.
2. Ask for a date of birth (or an age) before searching a person when the user has one.
3. Only a record whose matchState is "dob_match" is an identification. A record with
   matchState "no_dob_age_year", or any record returned without a date of birth, is a
   possible match for a human to review: say "possible match, not verified".
4. Cite sources[].recordUrl for every record you mention. Tell the user to confirm there.
5. If counts.sourcesIncomplete is above zero, say the search is incomplete. Never say
   "no match" over an incomplete search.
6. Say which registries were searched (counts.sourcesQueried) before saying what was found.
7. Use batch_search for a list of people, not a loop of search_offenders calls.
```

Rule 3 is the one that matters most. `dob_match` means the full date of birth on the record agrees with the one the user gave, and it is the only automatic identification. `year_match` and `age_match` are weaker agreements; `no_dob_age_year` means the registry published nothing to verify against, so the record is a name match and nothing more; `null` means no date of birth or age was sent at all. A record whose date of birth contradicts the one sent is not returned. The [matching and confidence](/docs/guides/sex-offender/matching-and-confidence) guide has the full state table, and the [no-match and multiple-match results](/docs/learn/no-match-and-multiple-match-results) tutorial shows what the "Name Only" sandbox person (three possible records in three states, no date of birth on any) should look like in a UI, which is also what the agent should say in words.

## A sample MCP run against the sandbox: Exact Match, 1980-01-01

The transcript below is an illustration of the documented sandbox scenario: the "Exact Match" person with date of birth 1980-01-01 returns one record with `matchState` `dob_match` from the Ohio sample registry, two addresses, a photo, offense "Sexual battery", Tier III. The tool result is abridged from the captured sandbox response; the agent's wording is what the rules above produce.

*Illustrative transcript*
```plain
# Illustration of the documented sandbox scenario "Exact Match", 1980-01-01 (os_test_ key).
# The tool result is abridged from the captured sandbox response.

User:   Is Exact Match, born 1 January 1980, on a sex offender registry?

Agent:  Calling search_offenders …
        → { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" }

Tool:   {
          "searchId": "srch_sbx_so_exact", "status": "complete",
          "counts": { "records": 1, "sourcesQueried": 58, "sourcesIncomplete": 0, "sourcesComplete": 58, "recordsReturned": 1 },
          "records": [ {
            "recordId": "rec_28db9639d712c4c61e8e", "personRef": "prs_28db9639d712c4c61e8e",
            "recordType": "sex_offender", "matchState": "dob_match", "unverified": false, "matchConfidence": 0.9,
            "name": { "full": "Exact Sample Match" }, "dob": "1980-01-01", "registrationState": "OH",
            "addresses": [ { "type": "residence", "city": "Sampleville", "state": "OH" }, { "type": "employment", "city": "Sampleville", "state": "OH" } ],
            "offense": { "crime": "Sexual battery", "tier": "Tier III", "convictionDate": "2004-05-12" },
            "source": { "jurisdiction": "OH", "registryName": "Sandbox sample registry (OH)",
                        "recordUrl": "https://offendersearch.app/docs/sandbox", "lastCheckedAt": "2026-10-01T00:00:00+00:00" }
          } ]
        }

Agent:  Yes. 58 registries were searched and all 58 answered completely. One record matched
        on name and full date of birth (matchState dob_match), so this is an identification,
        not a namesake: Exact Sample Match, born 1980-01-01, registered in Ohio, Tier III,
        offense "Sexual battery", convicted 2004-05-12, with a residence and an employment
        address in Sampleville, OH. Source: https://offendersearch.app/docs/sandbox (last
        checked 2026-10-01). Please confirm on that registry page before acting. This is a
        public-records lookup, not a consumer report, and must not be used to decide a hire,
        a tenancy, credit or insurance.
```

Three other sandbox scenarios are worth running through the same agent before you trust it: "Clear Visitor" (a complete answer, zero records, and the agent must say "58 searched, none matched", not "clean"), "Exact Match" with date of birth 1990-05-05 (zero records because the date contradicts the registry's, which the agent should explain), and "Name Only" (three possible matches the agent must present as unverified). The [test a background check integration](/docs/learn/test-a-background-check-integration) tutorial turns them into a test suite.

## Criminal records are not MCP tools today: the REST fallback for a background check

Criminal records and monitoring are not exposed as MCP tools yet. They are on the same key through the REST API: `POST /v1/criminal/search` for jail bookings, court cases, custody and warrants, and `POST /v1/monitors` for a standing watch. Every MCP tool also maps to a REST endpoint, so an agent framework that is not MCP, or a client you cannot give a remote server, gets identical results from a one-function tool that calls `POST /v1/search`. The two calls below are the whole fallback.

*REST: the registry call and the criminal call*
```bash
# The same call without the MCP server: POST /v1/search on the sandbox
curl -X POST https://sandbox.offendersearch.app/v1/search \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"}, "match": "balanced"}'

# The criminal side, REST only today
curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "Felony", "lastName": "Record", "dob": "1975-06-15"}, "page": 1, "perPage": 50}'
```

When you wrap the REST call as your own tool, keep the envelope fields the model needs to reason and cite, and drop the rest. A trimmed result is also a cheaper one for the model to read.

*A one-function tool definition over POST /v1/search*
```javascript
// A one-function tool for a framework that is not MCP: the agent gets the same envelope
export const searchOffendersTool = {
  name: "search_offenders",
  description: "Search 58 US sex offender registries by name and date of birth. Returns scored, cited records. Not a consumer report.",
  parameters: {
    type: "object",
    properties: {
      firstName: { type: "string" },
      lastName:  { type: "string" },
      dob:       { type: "string", description: "YYYY-MM-DD" },
      state:     { type: "string", description: "USPS code, optional" },
    },
    required: ["lastName"],
  },
  async run(args) {
    const res = await fetch("https://sandbox.offendersearch.app/v1/search", {
      method: "POST",
      headers: { "X-API-Key": process.env.OFFENDERSEARCH_TEST_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ query: args, match: "balanced" }),
    });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    const body = await res.json();
    // Hand the model only what it needs to reason and cite
    return {
      status: body.status,
      sourcesQueried: body.counts.sourcesQueried,
      sourcesIncomplete: body.counts.sourcesIncomplete,
      records: body.records.map((r) => ({
        name: r.name.full, dob: r.dob, matchState: r.matchState, unverified: r.unverified,
        registrationState: r.registrationState, offense: r.offense?.crime, tier: r.offense?.tier,
        recordUrl: r.source.recordUrl, lastCheckedAt: r.source.lastCheckedAt,
      })),
    };
  },
};
```

The criminal envelope reads the same way, with one addition: every criminal response carries `legal.notice` ("Not a consumer report…"), and the record's `cases[].charges[].disposition` is what decides whether a charge became a conviction. The [search criminal records by name and date of birth](/docs/learn/search-criminal-records-by-name-and-dob) tutorial reads one, and the [criminal records API](/criminal-records-api) page describes the record kinds. Switching from your own tool to the MCP server later changes nothing the agent sees.

## Run a background check from Claude with MCP, step by step

1. **Create an account and ask for MCP access.** Sign up (25 free calls, no card), mint an `os_test_` key in the dashboard, and ask for MCP access from the [MCP server page](/mcp-server). The server is in private beta.
2. **Add the config block to your MCP client.** Paste the `mcpServers` block into Claude Desktop, Cursor or any client that supports remote servers with custom headers, with your key in the `X-API-Key` header. Use the `os_test_` key while developing.
3. **Put the agent rules in the system prompt.** Not a consumer report; `dob_match` is the only identification; cite `sources[].recordUrl`; incomplete means incomplete; `batch_search` for lists.
4. **Run a sandbox search.** Ask the agent about "Exact Match", born 1980-01-01. It calls `search_offenders` and gets one record with `matchState` `dob_match` from the Ohio sample registry.
5. **Read the result the way a program would.** `status`, `counts.sourcesQueried` and `counts.sourcesIncomplete` first, then each record's `matchState` and `source.recordUrl`. Confirm on the registry page before acting.
6. **Go live.** Swap the `os_test_` key for an `os_live_` key. Each tool call is metered like a REST call: $0.15 a call, $0.11 past 2,000 calls in a month; the map tools at Map API prices.

Sandbox limits are 60 calls a minute and 2,000 a day per test key; an agent that loops `search_offenders` over a list will hit them, which is one more reason rule 7 says `batch_search`. Python and Node.js SDKs ship with the first registry release; see [SDKs](/sdk). Until then the raw HTTP calls above are the integration, and the [Learn APIs by example](/docs/learn) hub has the rest of the series.

## Background check MCP server: questions developers ask

### What is a background check MCP server?

An MCP endpoint that gives an AI agent background-check tools it can call with your API key. This one exposes sex offender registry search across 58 US registries, a batch tool, a coverage tool and five map tools, returning the same scored, cited records as the REST API.

### Can I use it from Claude or Cursor today?

Once you have beta access, yes: the config block above works in Claude Desktop, Cursor and any client that supports remote MCP servers with custom headers. Access is requested from the [MCP server page](/mcp-server). The source is open at github.com/offendersearch/mcp for a local stdio run.

### Does the MCP server include criminal records?

Not as tools yet. Criminal records are on the same key through `POST /v1/criminal/search`, and the REST fallback section shows a one-function tool that gives an agent both today.

### How are MCP tool calls billed?

The same as REST: $0.15 a call, $0.11 past 2,000 calls in a month, 25 free calls, no card. `batch_search` is one search per row. `get_registry_coverage` is free. The map tools are billed at Map API prices for accounts that have turned the map on. An `os_test_` key is never billed.

### Is an agent's answer from these tools a consumer report?

No. It is a public-records lookup. An agent must not decide hiring, housing, credit or insurance from it; those decisions run as FCRA orders through the [FCRA API](/fcra-api), and whether your use needs one is a question for your counsel.

### What should the agent say when a name matches but there is no date of birth?

"Possible match, not verified", with the registry link. `matchState` `no_dob_age_year` means the registry published nothing to verify against; only `dob_match` is an identification.

### Can I build the same thing without MCP?

Yes. Every tool maps to a REST endpoint; the one-function tool definition above calls `POST /v1/search` and returns the same envelope. Switching to the MCP server later changes nothing the agent sees.

## What this page does not cover

- The Map API itself (viewport, area, route and group semantics, marker ids): the [sex offender map API](/sex-offender-map-api) page.
- Batch mechanics (body shapes, CSV input, chunking a large roster): the [bulk background check from CSV](/docs/learn/bulk-background-check-csv) tutorial.
- Standing watches and webhook delivery: [monitor a person with webhooks](/docs/learn/monitor-a-person-with-webhooks).
- Running the server locally over stdio and per-client install notes: the repository README.

## Related

- [MCP server (product page, request access)](https://offendersearch.app/mcp-server)
- [Sex offender API](https://offendersearch.app/sex-offender-api)
- [Background check API](https://offendersearch.app/background-check-api)
- [Sex offender map API](https://offendersearch.app/sex-offender-map-api)
- [Public API for testing (the sandbox)](https://offendersearch.app/sandbox)
- [Background check API tutorial](https://offendersearch.app/docs/learn/background-check-api-tutorial)
- [Check if someone is a registered sex offender with the API](https://offendersearch.app/docs/learn/check-registered-sex-offender-api)
- [Test a background check integration](https://offendersearch.app/docs/learn/test-a-background-check-integration)
- [No-match and multiple-match results](https://offendersearch.app/docs/learn/no-match-and-multiple-match-results)
- [Bulk background check from a CSV file](https://offendersearch.app/docs/learn/bulk-background-check-csv)
- [Matching and confidence (sex offender)](https://offendersearch.app/docs/guides/sex-offender/matching-and-confidence)
- [Learn APIs by example](https://offendersearch.app/docs/learn)
