内容
# InsuranceXDate MCP
A TypeScript [Model Context Protocol](https://modelcontextprotocol.io) server that wraps [InsuranceXDate](https://www.insurancexdate.com) research, benefits, SERFF, account-read, and account-action surfaces. Works with MCP clients over local stdio or the included remote HTTP entrypoint.
> Unofficial third-party client. Not affiliated with InsuranceXDate. MIT licensed. Personal project — PRs welcome but no SLA on response times.
Built on the lockfile-pinned `@modelcontextprotocol/sdk`, Node 20+. Ships as both a pre-packaged Anthropic [`.mcpb` Desktop Extension](https://www.anthropic.com/engineering/desktop-extensions) for one-click install on Claude Desktop, and as plain Node source you can wire into any other MCP client's config.
## What it does
Exposes seventeen tools in full mode. The default surface contains nine free read tools; paid reads and account writes are enabled independently:
| Tool | Cost | Purpose |
|---|---|---|
| `search` | Free | Workers'-comp prospect search by state, renewal window, class, SIC, industry, county, carriers, carrier groups, agents, PEO providers, premium range, mod range, employee band (0-9), policy options (AR / multi-state / PEO), additional-data filters (BENEFITS / DOT / NPO / OSHA / PEO) |
| `native_search` | Free, advanced | Guarded access to the useful 37-field subset of XDate's current 43-field native search. Requires `datamode` 0 (WC), 1 (retirement), or 2 (health), rejects fields from the wrong mode, and adds native name/NAICS and extended benefits filters. Prefer `search` for normal WC because the native WC universe is materially smaller; six currently broken native filters remain excluded |
| `benefits_search` | Free per upstream declaration | Form 5500 benefits-plan search via upstream MCP `datamode` 1 (retirement: participants, assets, commissions, provider name) or 2 (health/welfare: premiums, commission %, loss ratios, broker name; `fromdate`/`todate` filter the insurance renewal date). Only behavior-verified params exposed (see Q3 drift notes) |
| `match` | Free | Find a specific business by at least one of `name[]` / `fein` / `phone` / `address[]`, optionally scoped by `state`. Routes to `/api2/Match`. No additional service required per XDate support; fuzzy lookup returns the highest-score match |
| `filter` | Free | Look up valid filter values: carriers, carrier groups, classes, SIC codes, industries, counties, agents, PEO providers, NAICS codes (for `serff_search` industry filters — not WC search), SERFF insurance-type (TOI) codes, SERFF severity-type categories (response-side reference). Policy options and additional-data options are fixed `search` enums, not filter lookups |
| `account_status` | Free, experimental | Reads undocumented `/api2/Account` through a strict sanitizer. Returns only prepaid `apiBalance` and monthly allowance size `apiFreeMonthly`; it never exposes the raw account object or credential, session, Stripe, or unreliable MCP-access fields |
| `company_details` | $0.25 (ledger-confirmed; gated) | Full account detail by UID: `summary`, `user_status`, `details` (field count varies by record: ~77 observed 2026-06-12, 46 on records probed 2026-07-03), `carrier_history[]` (per-policy-term rows, multi-year multi-state), `_meta` field docs. Optional `scope` blocks VERIFIED in paid responses 2026-07-03: `contacts` (names/emails/phones/LinkedIn), `altloc` → `other_locations`, `tabs` → osha + benefits blocks + a `dot[]` block on **DOT-flagged** companies (keys on the dot flag, not industry — pest-control/pool-care/NEMT fleets carry it too; 35 MCS-150 fields incl. safety rating, power units, drivers, coverage, policy number), `comments`. Dedupe: same-day repeats free (ledger-confirmed); advertised 90-day window failed a live test |
| `talkpoints` | $0.10 (ledger-confirmed; gated) | Prospecting talking points + percentile flags by UID. Assume same-day dedupe only |
| `serff_search` | $0.05 (ledger-confirmed — metadata's "free" claim disproven; NO dedupe; gated) | SERFF rate-filing search. `carrier_naic` now optional (statewide all-carrier queries verified); severity comma-lists, `policyholders_min`, `industry_naic_prefix`, `naics3` verified 2026-07-03; `industry_naic`, `policyholders_max` upstream-declared |
| `serff_filing` | $0.10 (ledger-confirmed; gated) | Full SERFF filing detail by integer `filing_id`. Same-day same-filing dedupe ledger-confirmed |
| `flagged_companies` | Free per upstream declaration | List companies you or your agency flagged (save / contacted / quoting / written / nextyear / followup / appt), with sort + pagination. Behavior-probed 2026-07-03 |
| `groups` | Free per upstream declaration | List saved company groups, incl. groups shared by agency members. Behavior-probed 2026-07-03 |
| `group_companies` | Upstream-declared free, unverified — gated | Companies in a saved group (search-format results). Executes stored account content; could not be behavior-verified (no saved groups existed to probe) |
| `saved_searches` | Free per upstream declaration | List saved prospect-search definitions. Behavior-probed 2026-07-03 |
| `run_saved_search` | Upstream-declared free, unverified — gated | Execute a saved search by id (search-format results). Executes a stored definition the wrapper cannot inspect; could not be behavior-verified (no saved searches existed to probe) |
| `add_note` | Free write; default off | Adds an agency-visible persistent note. Requires `XDATE_ENABLE_WRITES=1` and `confirm=true`; remote connections also require `?writes=1`. Never automatically retried |
| `set_flag` | Free write; default off | Sets/removes flags or schedules follow-up/appointment state. Same three controls as `add_note`; `hide` and `remove` are destructive, and scheduled states may sync through XDate's connected calendar workflow. `appttime` is accepted only for follow-up/appointment; XDate's contradictory null/0 clear-time description is not exposed |
The write tools are present but hidden by default. `confirm=true` and MCP annotations express call intent; they do not prove that a human approved the exact change. Enable `XDATE_ENABLE_WRITES` only for a trusted single-user session whose client shows tool calls before execution. A timeout can be ambiguous after XDate commits a mutation, so the wrapper never retries either action.
The action schemas were reconciled to XDate's current native declarations, but
this release's verification uses stub clients only and performs no live
mutation. In particular, follow-up/appointment scheduling may propagate through
XDate's configured Outlook or Google calendar integration.
## Architecture
```
MCP client (Claude Desktop, Cursor, Continue, Zed, custom...)
│ (stdio JSON-RPC)
▼
InsuranceXDate MCP server (this repo)
│
├──► /api2/Search (REST) for `search`
│ translates MCP-style param names (premfrom/premto/modfrom/
│ modto/limit/offset) to REST equivalents (fromprem/toprem/
│ frommod/tomod/pagelimit/pageon)
│
├──► /api2/Match (REST) for `match`
│ find-by-name endpoint (the REST `/Search` endpoint ignores
│ `name`/`city`/`zipcode` — re-verified 2026-07-03 — so `/Match`
│ is the correct route for find-by-identifier lookups)
│
├──► /api2/Account (REST) for experimental `account_status`
│ fixed allowlist returns only apiBalance and apiFreeMonthly;
│ the raw account response is never exposed
│
└──► /api2/McpData (MCP) for `filter`, `company_details`,
`talkpoints`, `serff_search`, `serff_filing`,
`benefits_search` (upstream `search` with datamode locked
to 1|2), guarded `native_search`, `flagged_companies`,
`groups`, `group_companies`, `saved_searches`,
`run_saved_search`, `add_note`, and `set_flag` — passes
parameters through using the upstream MCP schema
```
The split exists because the upstream MCP at `/api2/McpData` and the REST endpoint at `/api2/Search` use different parameter naming conventions and have different filter behavior on prospect search. This wrapper bridges both surfaces with a consistent client-facing schema.
### Production-grade defaults
- **HTTP timeout:** 30s via `AbortSignal.timeout()` so slow upstream calls surface as clean errors instead of silent hangs
- **HTTP error handling:** non-2xx and oversized upstream responses surface as stable `isError: true` MCP results without exposing upstream body or cause details
- **`structuredContent`:** `search` and `match` return both `content` (text JSON fallback) and typed `structuredContent` so LLMs can reason over records without re-parsing
- **URL-decode for UIDs:** company UIDs from `/api2/Search` come URL-encoded (`%2B`, `%2F`); wrapper decodes before forwarding to upstream MCP for paid lookups, which expect raw `+`/`/`
- **Schema validation:** zod-validated input on every tool (state codes uppercase regex, dates MM-DD regex, premium/mod numeric, employee band 0-9, addloptions/policyoptions enum)
- **Paid tools default denied:** the six gated tools are omitted from `tools/list` unless an operator explicitly sets `XDATE_DISABLE_PAID=0` (also accepts `false` / `no` / `off`, case-insensitive). Blank, true, and unrecognized values keep them disabled. Nine free read tools remain enabled; account-read tools among them can expose agency workflow names or prepaid-account status to the connected client
- **Write tools default denied:** `add_note` and `set_flag` are omitted unless `XDATE_ENABLE_WRITES` is explicitly true. Every write also requires `confirm=true`; remote connections additionally require `?writes=1`. No action is automatically retried
- **Account-response allowlist:** `account_status` discards the full undocumented account object immediately and publishes only `apiBalance` and `apiFreeMonthly`; tests seed password, session, Stripe, and misleading MCP-field canaries and prove they cannot reach MCP output
- **Sensitive credential storage:** when installed via `.mcpb` on Claude Desktop, the API key flows through `user_config.api_key` with `"sensitive": true` and is stored in the OS keychain (Windows Credential Manager / macOS Keychain). On other MCP clients the server reads `INSURANCEXDATE_API_KEY` from `process.env`, so use whatever secret-handling pattern your client supports (env-var injection, secret store, etc.) — never hard-code keys in JSON config files committed to source control
## Install
The server runs as a Node.js process speaking MCP over stdio. Any MCP client can launch it. Pick the install path that matches your client.
### Prerequisites (all paths)
- Node.js 20+ (only required for Options B / C / D — Option A bundles its own runtime)
- An InsuranceXDate API key from your account's Settings → API / MCP page
### Option A: Claude Desktop (one-click `.mcpb`)
1. Download the `.mcpb` from the latest [Release](../../releases)
2. Double-click the `.mcpb` file → Claude Desktop opens an install dialog
3. Click Install
4. Paste your InsuranceXDate API key when prompted
The key is stored in the OS keychain via the manifest's `user_config.api_key` with `"sensitive": true`. No Developer Mode toggle required — `.mcpb` is designed for one-click end-user install.
> **⚠️ Windows Store / MSIX build of Claude Desktop:** Option A may not work. If Claude Desktop was installed from the Microsoft Store (MSIX, under `C:\Program Files\WindowsApps\Claude_*`), the "Install Extension" dialog can silently fail to fire — the file picker closes and nothing installs, with no error. That build also spawns MCP servers with a **stripped environment** that does not inherit your user variables. Use the config-file path instead:
>
> 1. Build from source (Option D) so you have `server\dist\index.js`.
> 2. Add an `mcpServers` entry to `%APPDATA%\Claude\claude_desktop_config.json` pointing `node` at that file. **Put the key in the entry's `env` block** — on MSIX the server will not pick up a system-set `INSURANCEXDATE_API_KEY` on its own:
> ```json
> {
> "mcpServers": {
> "insurancexdate": {
> "command": "node",
> "args": ["C:\\path\\to\\insurancexdate-mcp\\server\\dist\\index.js"],
> "env": { "INSURANCEXDATE_API_KEY": "your-key-here" }
> }
> }
> }
> ```
> 3. Fully quit Claude Desktop (system tray → Quit, not just close the window), then relaunch. Note: Claude rewrites this file from memory, so if an edit seems to revert, make it while the app is fully quit.
>
> **To keep the key out of plaintext config**, point `command` at a small wrapper script (`.cmd`) that reads `INSURANCEXDATE_API_KEY` from your OS credential store / `HKCU\Environment` and then launches node — the server reads the key from its own environment, so the wrapper only needs to set it before exec. Verify after launch: the tools register under the lowercase prefix `mcp__insurancexdate__*` (from the config key), not the `.mcpb` display name.
### Option B: Cursor
Build from source (see Option D), then add to `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"insurancexdate": {
"command": "node",
"args": ["/absolute/path/to/insurancexdate-mcp/server/dist/index.js"],
"env": {
"INSURANCEXDATE_API_KEY": "your-key-here"
}
}
}
}
```
For team setups, store the key in a secret store and inject via Cursor's env handling rather than committing the key.
### Option C: Continue, Zed, Cline, or any other MCP client with a config file
Same shape as Option B — point the client's MCP config at `server/dist/index.js` and pass `INSURANCEXDATE_API_KEY` via the env block. Consult your client's MCP-config docs for the exact file path and JSON schema (most converge on something close to Cursor's format).
For a custom client (Python, TypeScript, Go) launching the server directly, you only need:
```sh
INSURANCEXDATE_API_KEY=your-key-here node /path/to/server/dist/index.js
```
The server speaks standard MCP JSON-RPC on stdio. Anything that follows the protocol works.
### Option D: Build from source
```sh
git clone https://github.com/toddshaner/insurancexdate-mcp.git
cd insurancexdate-mcp/server
npm ci
npm run build # tsc emits server/dist/*.js (noEmitOnError prevents broken builds)
```
`server/dist/index.js` is the entry point for Options B / C and any custom client.
To repack the `.mcpb` for Claude Desktop after a source change:
```sh
cd .. # back to repo root
npx -y @anthropic-ai/mcpb@2.1.2 pack .
```
For a slimmer `.mcpb`, run `npm prune --omit=dev` after build to strip TypeScript and `@types/*` from `node_modules` — `.mcpbignore` covers them anyway, but pruning is cleaner.
### Option E: Remote server (streamable HTTP)
Options A–D run locally. Option E hosts `server/dist/http.js` for a compatible
remote-MCP client. It is stateless and has two mutually exclusive modes:
- **BYOK** (`MCP_BYOK=1`): the server stores no vendor key. Each individual
caller supplies their own vendor-authorized InsuranceXDate key by Bearer
header or user-scoped URL configuration.
- **Private**: one operator's `INSURANCEXDATE_API_KEY` is protected by a secret
`MCP_PATH_TOKEN` (minimum 16 characters). The path token is a bearer
capability, not multi-user authentication: it has no per-user identity,
revocation, quota, or attribution. Do not present it as a safe shared-org
auth layer.
Do not place one user's API key in an organization-wide connector URL.
InsuranceXDate's [MCP guide](https://insurancexdate.helpjuice.com/en_US/integrations/mcp)
describes user-specific keys, and its
[Terms of Service](https://www.insurancexdate.com/terms-of-service/) restrict
credential sharing. For multiple users, require individual client
configuration, an InsuranceXDate-native/OAuth flow, or explicit vendor-approved
delegation behind a real identity gateway. If a client only supports one shared
organization URL, this BYOK implementation cannot provide per-user identity or
credential exchange.
Remote HTTP startup requires exact allowlists. `MCP_ALLOWED_HOSTS` is a
comma-separated list of `host[:port]`; `MCP_ALLOWED_ORIGINS` is a comma-separated
list of absolute origins. A present Origin must match exactly. Requests without
Origin are accepted only after Host validation succeeds. A host entry without a
port accepts that exact host with any numeric port; use `host:port` to require a
specific port. `/healthz` is exempt for platform health probes.
Paid tools default disabled in the provided Pulumi deployment in both modes.
The operator must explicitly set `XDATE_DISABLE_PAID=0` to expose them; then a
connection's `?paid=1` expresses caller intent. It cannot override an
operator-level disable. MCP clients are not guaranteed to prompt before each
call, so use the operator-level disable rather than relying on client UI.
Persistent actions are separately disabled. The operator must set
`XDATE_ENABLE_WRITES=1`; the connection URL must include `?writes=1` (combine as
`?paid=1&writes=1` when needed); and each action must pass `confirm=true`.
These controls are suitable only for a trusted single-user deployment. They do
not establish per-user identity or prove a human approved the exact mutation.
Rate limiting applies per credential and, in BYOK, across the host. Operational
JSON logs cover accepted authenticated POSTs and selected errors/rejections,
not every HTTP request or per-user attribution. Treat them as diagnostics, not
a complete audit log.
```sh
docker build -t insurancexdate-mcp server
docker run --rm -p 8080:8080 \
-e MCP_ALLOWED_HOSTS=localhost:8080,127.0.0.1:8080 \
-e MCP_ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 \
-e MCP_BYOK=1 -e XDATE_DISABLE_PAID=1 \
-e XDATE_ENABLE_WRITES=0 insurancexdate-mcp
```
Without Docker, set the same allowlist variables before running
`node server/dist/http.js` (`PORT` defaults to 8080).
The included Pulumi example targets AWS App Runner. AWS says App Runner is no
longer open to new customers; it is usable only in accounts with existing
access. New customers need another container host. See
[`deploy/pulumi/`](deploy/pulumi/README.md) for the availability warning and
deployment controls.
## Usage examples
After install, in any MCP-enabled chat client:
**Discover IL workers'-comp prospects, $250K+ annual premium, renewing in the next 60-120 days, with size-signal flags pre-filtered server-side:**
```
search(
statelist=["IL"],
fromdate="06-24",
todate="08-23",
premfrom=250000,
addloptions=["BENEFITS","DOT","OSHA"],
limit=50
)
```
**Find a specific company by name + state:**
```
match(state="PA", name=["Acme Logistics", "Acme Logistics Inc"])
```
**Pull rate filings for Berkley Casualty in IL, workers' comp only, broker-attack severity range:**
```
serff_search(carrier_naic=15911, state="IL", insurance_type="16.0", severity="4,5")
```
The `severity` filter takes a single value or a comma-separated list (verified server-side 2026-07-03: PA WC `severity="3,4,5"` returned 204 filings vs 105 for `"4"` alone). A single value is still exact-match, not a threshold.
**Statewide market scan — all carriers' WC filings hitting Construction, market-moving only:**
```
serff_search(state="PA", insurance_type="16.0", industry_naic_prefix=["23"], policyholders_min=1000)
```
`carrier_naic` is optional since Q3 2026 — statewide all-carrier queries verified working 2026-07-03.
**Mid-size 401(k) plans in NJ for retirement prospecting (free):**
```
benefits_search(datamode=1, statelist=["NJ"], partmin=50, partmax=500, assetmin=1000000)
```
**Health plans with a poor loss ratio renewing soon (renewal-increase wedge):**
```
benefits_search(datamode=2, statelist=["TX"], lossratiomin=85, fromdate="07-03", todate="10-01")
```
**Use a native-only WC name/NAICS filter when you accept the smaller native WC universe:**
```
native_search(datamode=0, statelist=["PA"], name="Acme", naicslist=["238"])
```
`native_search` is not a drop-in replacement for `search`. Its `offset` is a
record count, not a page number, and the wrapper rejects benefit-only fields in
WC mode (and vice versa) instead of letting XDate silently ignore them.
**Check the prepaid balance without exposing the raw account object:**
```
account_status()
```
With writes deliberately enabled, an action still requires explicit call intent:
```
add_note(uid="company-uid", note="Called; renewal review scheduled.", confirm=true)
set_flag(uid="company-uid", flag="followup", appttime="2026-08-20T10:00:00-07:00", confirm=true)
```
Do not retry either action after a timeout. Read back through
`company_details(scope=["comments"])` or `flagged_companies` when confirmation is
important; the former is a paid call.
## State-data coverage
InsuranceXDate's data depth varies by state. Some filters only have data to operate on in specific states:
- **Premium data** (`premfrom` / `premto`): CO, GA, IL, NV, NJ, OK, TX, VT (8 states)
- **Mod data** (`modfrom` / `modto`): DE, MA, MN, NJ, NY, NC, OH, PA (8 states)
- **WC class code data** (`classlist`): 21 states (CA, CO, CT, DE, FL, GA, IL, KY, ME, MD, NV, NH, NJ, OH, OK, OR, PA, SC, TX, VT, VA)
- **SIC codes, employee count, carrier, sales** (most filters): 44 states (broadest)
Outside these footprints the corresponding filters have no data to operate on. This is upstream data availability, not a wrapper limitation.
## Upstream response notes (observed 2026-06-12, reconciled with Q3 metadata 2026-07-03)
XDate revises its response surfaces without versioning or notice. Three things to know from the Q2 2026 platform release, each updated where the Q3 metadata changed the picture:
- **`company_details` envelope changed.** The default response is `{ summary, user_status, details, carrier_history, _meta }` (previously `{ status, data: { details, contacts, carriers, altloc } }`). `carriers` became `carrier_history` (full per-policy-term rows, each carrying the same ~77 fields as `details` — hundreds of rows for multi-state operators, easily 1-2 MB). `contacts` and `altloc` stopped returning in the default response as of the 2026-06-12 verification; **the Q3 `scope` opt-ins were verified in a live paid response 2026-07-03**: `scope=['contacts']` returns a contacts[] array (name/email/position/phone/LinkedIn profileUrl), `scope=['altloc']` returns under the key `other_locations`, `scope=['tabs']` returns `osha`/`benefits_health`/`benefits_retirement` as top-level blocks (no `tabs` key exists in the response; no DOT block observed on a non-DOT risk). See Q3 2026 upstream drift notes below.
- **Free-tier field masking on `search`/`match` results.** `name`, `fein`, `location`, `expyear`, `carrier`, and `carriergroup` return the literal string `"available"` — present-but-withheld markers, not values. Don't treat `"available"` as data; pull `company_details` for real values.
- **The Q2 2026 DOT and NPO datasets did not appear in any API response as of the 2026-06-12 verification.** The Q3 `tabs` scope opt-in was verified 2026-07-03 to return OSHA and Form 5500 blocks, and the `dot[]` block was verified the same day on a live DOT-flagged risk (35 MCS-150 fields — see REST counterpart probes below); it keys on the company's DOT flag, not its industry, and no add-on gate was encountered. The `addloptions` `NPO` flag does filter server-side, but it matches companies with *linked* 990 data — including for-profit companies with affiliated foundations — and search results carry no per-record `npo` flag.
## Q3 2026 upstream drift notes (verified 2026-07-03)
XDate shipped a ["Q3 Search Menu Update"](https://www.insurancexdate.com/2026/06/23/q3-searchmenu/) on June 23, 2026: dedicated DOT (~1.2M filings) and NPO (~556K orgs) databases and reworked search menus. What that means for this wrapper, verified against the live API:
- **No dedicated callable DOT/NPO contract was observed.** XDate markets MCP/API access across its datasets, but the current native `tools/list` has no DOT or NPO search mode (`datamode` covers only 0=WC, 1=retirement, 2=health). The proven API-side paths remain `search.addloptions` and `company_details(scope=["tabs"])`; a dedicated tool will wait for a supported schema rather than scrape the UI.
- **The current native MCP has 13 tools.** Full wrapper mode covers all 13 native tool families, keeps the fuller REST `search`, adds `match`, retains the focused `benefits_search` alias, and adds sanitized experimental `account_status` for 17 tools total. Its guarded `native_search` intentionally omits six broken fields, and ambiguous schedule-clearing semantics remain deferred. The two native actions remain hidden unless writes are explicitly enabled.
- **The upstream MCP's WC search mode is still not trustworthy as the default.** It applies useful name/NAICS filters but searches a materially smaller universe than REST. Normal WC stays on `search`; `native_search(datamode=0, ...)` is an explicit advanced alternative. Benefits users should prefer the verified `benefits_search` subset unless they need an experimental native-only list field.
- **Declared ≠ honored.** Free read-only probes on 2026-08-16 reconfirmed that `city`, `zipcode`, and `planyear` leave totals unchanged; both insurance-commission-percent bounds return zero; and `lossratiomax=90` is inconsistent with the population above 95. Those six fields are excluded from `native_search`. Native list fields without a working value lookup remain exposed only as clearly labeled experimental inputs; `instypelist` is also labeled because the filter tool's identically named lookup returns SERFF TOI codes from a different value domain.
- **Pricing: the ledger is the only reliable source.** The tools/list metadata declared `serff_search` free while the pricing page said $0.05; the account's XChange ledger settled it at $0.05 per call (see "Pricing tested against the account's own ledger" above). Same lesson for dedupe: advertised at 90 days in two written sources, ledger-verified at same-day only. The pricing page also says company_details includes "current flag/appointment status": the MCP route this wrapper uses does return that (`user_status`); the REST counterpart does not (see REST counterpart probes below). General rule for this vendor: metadata and marketing text lag or contradict actual behavior — verify against the ledger or a live probe before relying on any claim.
## Pricing tested against the account's own ledger (2026-07-03)
XDate's self-descriptions are not reliable — its pricing page and tools/list metadata disagreed on `serff_search` the same day — so pricing was verified against the account's XChange usage ledger (the per-call charge log in the web UI), reconciled 1:1 against a known 14-call sequence:
- **Published per-call prices are real and charge as listed** ($0.25 company_details, $0.10 serff_filing, $0.05 serff_search — all in the Jul 3 reconciliation; talkpoints' $0.10 is confirmed on prior-day Jun 26-27 ledger rows, and notably the one fresh Jul 3 talkpoints call produced **no visible charge** as of the readback — unresolved, re-check on the next ledger read). The earlier API-side balance-delta test read $0.00 because **charges draw from a $5.00/month included allowance first**, then from the prepaid XChange balance; `GET /api2/Account`'s `apiFreeMonthly` is the allowance *size* (static), not a live remaining counter, so an API balance bracket only detects charges after the allowance is exhausted. The UI ledger is the authoritative per-call record.
- **`serff_search` is $0.05, ledger-confirmed** (9 calls → 9 charges) — the tools/list metadata's "FREE" claim is disproven. It also has **no dedupe**: an identical same-day repeat was charged.
- **Dedupe is same-day-verified only.** Same-day same-record repeats of `company_details` and `serff_filing` were not charged, but a company last pulled 57 days earlier WAS re-charged — the advertised 90-day window did not apply on this account. Plan spend assuming same-day dedupe only.
- **Cross-surface billing (REST `/api2/Company` vs MCP `company_details`) is inconsistent.** Two companies were pulled on both surfaces the same day; the ledger shows exactly one API-source charge — so one company double-billed across surfaces while the other's REST pulls produced no API charge at all (and the ledger rows carry no company id, so which was which is unverifiable). Budget MCP and REST pulls of the same UID as separately billable; rely on cross-surface dedupe in neither direction.
- `GET /api2/Account` exposes `apiBalance` (prepaid $) — useful as a post-allowance backstop check, not as a per-call meter. `mcpAccess`/`mcpMonthlyBudget` fields exist but were 0/null while MCP calls worked and billed normally.
## REST counterpart probes (2026-07-03)
The vendor pricing page lists REST counterparts (Search / Company / Filter) for three MCP tools. Both were probed head-to-head against the MCP routes this wrapper uses, on the same UIDs, same day:
- **`GET /api2/Search/Filter` exists but is silently empty.** It returns a well-formed `{status, data:{results,pagination}}` envelope with ZERO results for every param probed (12 types, with search terms, statelist, and alternate query keys) — including params that return real values via the MCP filter (classlist 'roofing' 5, instypelist 52, severitylist 15, naicslist 'landscap' 2). POST returns 404. Do not route filter lookups here; a reader following this README's schema-audit advice would otherwise conclude the values don't exist.
- **`GET /api2/Company?uid=` exists** (accepts the percent-encoded UID exactly as `/api2/Match` returns it; invalid UID → HTTP 419) and returns the pre-Q2 envelope `{status, data:{details, contacts, carriers, altloc, tabs}}` with contacts included by default. Field-name set-diffs on the same records show **full parity with the MCP route on every shared block**: details (46=46), dot (35=35, incl. Max Coverage and Policy Num on both), osha, retirement (69=69), health (50=50); contacts/carriers/altloc row-identical. MCP-only: `summary`, `user_status`, `_meta`, and scope selectivity. REST-only: `tabs.related` (2 rows of Form-5500 filing references {ackid, year, type} on the probed records — not surfaced by MCP). Net: `company_details` stays on the MCP route (more blocks, flag status, field docs, selectable scope); REST /api2/Company is a documented fallback for contacts if a future unversioned vendor change drops them from the MCP path again.
- **DOT tab verified on a live trucking risk via the CURRENT build**: `company_details(scope=['tabs'])` on a DOT-flagged company returns a `dot[]` block — one row per MCS-150 filing, 35 fields (US DOT #, Safety Rating, Power Units, Drivers, Mileage, Carrier Operation, Insurance Providers/Type, Max Coverage (x1000), Policy Num, ...). The earlier "DOT unobserved" caveat applied only to non-DOT risks. No hybrid routing change is needed to reach DOT data.
- Same-day cross-surface billing of the same UID was ledger-probed and is inconsistent (see Pricing above: one company double-billed, the other's REST pulls showed no API charge); cross-day behavior is unprobed. Budget MCP and REST pulls of the same UID as separately billable.
- **Account workflow tools** (`flagged_companies`, `groups`, `saved_searches` — plus gated `group_companies`, `run_saved_search`) surface the flag/group/saved-search workflows from the web UI, read-only.
## SERFF response notes
`serff_search` and `serff_filing` route to the upstream MCP and return XDate's structured shape of SERFF rate filings. A few things worth knowing before you build against the response:
### Sentiment language is policyholder-perspective, not broker-perspective
The `sentiment` field uses `bad` / `good` / `neutral` from the policyholder's point of view. The XRate web UI shows the same data with broker-perspective labels.
| API `sentiment` | XRate UI label | Meaning | Broker-side action |
|---|---|---|---|
| `bad` | Offensive | Unfavorable to policyholders (rate up, coverage cut) | Attack opportunity (filing carrier's renewal is exposed) |
| `good` | Defensive | Favorable to policyholders (rate down, expanded coverage) | Retention play (don't displace a filing helping the insured) |
| `neutral` | Neutral | Administrative, insignificant, or new program | Watch only |
A consumer reading `sentiment: "good"` and assuming it's good for the broker reaches the wrong conclusion. Document this translation in any client code that surfaces SERFF data to brokers.
### `severity_types` observed values
Filings can carry multiple. Use as a client-side post-fetch filter:
`RATE_CHANGE`, `LCM_ADJUSTMENT` (WC-specific repricing), `TIERING_REVISION` (winners/losers within a carrier book), `COVERAGE_MODIFICATION`, `UNDERWRITING_REVISION` (eligibility shifts), `ADMINISTRATIVE` (typically skip), `NEW_PROGRAM`, `MARKET_EXIT` (carrier exiting line/state/segment), `DIVIDEND_PLAN`.
### What `serff_filing` returns vs. what's NOT in the response
`serff_filing` surfaces structured fields suitable for programmatic triage at scale, but is curated rather than exhaustive. Verified empirically against filing 21434 (Berkley Casualty IL WC, public SERFF tracking BNIC-134422662):
**Structured and reliable:**
- `carrier_names` + `naic_codes` — full per-paper list when a filing covers a multi-paper carrier group
- `disproportionately_affected` — text array describing the harmed papers and class codes (e.g., `"Carolina Casualty Insurance Company (CCIC) facing a 22.4% increase"`); explicit % values for the harmed direction
- `affected_naics` — array of NAICS sector codes hit by the filing
- `actuarial_justifications` — short summary bullets of the carrier's stated reasoning
- `key_coverage_changes` — coverage form / endorsement changes
- `narrative` — pre-shaped markdown text (multiple labeled sections, written for downstream broker-attack use cases, not actuarial prose)
**NOT in the response (manual XRate UI or SERFF Filing Access required):**
- **Per-tier % impacts for favorable-direction papers.** The response itemizes the harmed direction; the full per-tier table is UI-only. (For filing 21434 verified 2026-04-26: API surfaced CCIC +22.4% and BCC +18.1%; UI also exposes KRIC +6.8%, StarNet -0.6%, MECC -12.2%.)
- **Per-paper Loss Cost Multiplier (LCM) values.** UI shows the new LCM alongside the % impact — the underlying repricing mechanism. API's `actuarial_justifications` references LCM alignment qualitatively but doesn't expose values per paper.
- **Specific reallocation movement rates.** Carriers using a multi-paper reallocation strategy (move accounts between papers to mask the headline rate impact) disclose specific transition % values in the UI (e.g., "45% of CCIC renewals to BCC, 30% of BCC renewals to KRIC"). API narrative references "reallocation" qualitatively but doesn't expose these as structured fields.
- **Per-paper policyholder counts.** UI breaks the affected-policyholder count down by paper. API gives total policyholders but not the per-paper distribution.
- **Full filing memorandum / actuarial exhibits.** Only the 3-bullet `actuarial_justifications` summary is in the response.
- **Parent carrier-group identifier.** The response carries per-paper NAICs but no group-level ID; group-level lookups require a separate `filter` call enumerating groups.
- **Class codes as a structured array.** Class codes appear inside `disproportionately_affected` as unstructured text — regex-parseable, but not a typed field.
If you're building an automated pipeline that depends on the full per-tier table or actuarial exhibits, plan for a manual SERFF Filing Access lookup as a final step. The API gets you ~3.5 of 4 things you'd want for programmatic triage; the last 0.5 still requires a manual read.
### NAICS code formatting
`affected_naics` returns 3-digit raw integers (`[238, 482, 485, 711]`). The XRate UI displays the same codes 4-digit zero-padded (`0238`, `0482`, etc.). Same codes, different formats — normalize before comparison if you parse both surfaces.
## Development
### Project structure
```
.
├── manifest.json # .mcpb manifest (Node type, sensitive config, 17 tools)
├── .mcpbignore # excludes src/, devDeps, source maps from the bundle
├── LICENSE # MIT
├── README.md # this file
├── CHANGELOG.md # version history
└── server/
├── package.json # @modelcontextprotocol/sdk + zod runtime; tsc + types as devDeps
├── tsconfig.json # strict + noEmitOnError
└── src/
├── index.ts # stdio entrypoint
├── server.ts # shared tool registration and annotations
├── http.ts # streamable-HTTP entrypoint and admission controls
├── xdate-client.ts # REST + MCP HTTP clients, sanitization, bounds, cancellation
└── tools.ts # zod schemas, handler factory, paid/write gates
```
### Build, test, pack
```sh
cd server
npm install
npm run build # tsc with noEmitOnError; broken builds fail fast
npm test # no-network smoke test: tool registration, search schema shape, version consistency (same gate CI runs)
node dist/index.js # manual stdio poke (set INSURANCEXDATE_API_KEY in env)
cd ..
npx -y @anthropic-ai/mcpb@2.1.2 pack .
```
Version bumps touch `server/src/server.ts` (serverInfo), `server/package.json` plus its lockfile, and `manifest.json`. The smoke test asserts all three published versions agree, so a missed update fails `npm test` rather than shipping.
### Schema audit pattern
When adding or modifying a tool, validate the wrapper's schema against the upstream by curling `tools/list` directly:
```sh
curl -s -X POST https://www.insurancexdate.com/api2/McpData \
-H "Content-Type: application/json" \
-H "X-API-Key: $YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .
```
Diff every param name and type against the wrapper's zod schemas. Mismatches in name or type at the upstream surface silently as "filter not applied" symptoms — the call succeeds, the param is dropped, and the response is the unfiltered universe. The CHANGELOG documents two cases this audit caught (`serff_search`'s `naic`/`type` vs. upstream `carrier_naic`/`insurance_type`, and `search`'s `naicslist` advertised but no-op upstream).
When integrating against a 3rd-party API where you have an alternative source of truth (a public registry, a documented spec, a UI you can verify against), cross-validate at least one record end-to-end. For SERFF, public SERFF Filing Access (free) carries the same filing data and the API's `disposition_date` should match exactly. Drift between the two is itself a signal worth investigating.
## Acknowledgments
Built against the InsuranceXDate API. Their public OpenAPI spec is at https://insurancexdate.stoplight.io/docs/insurancexdate/branches/main/4d5c0ab0222e5-insurance-x-date (the bare /docs/insurancexdate/ URL 404s). Reference architecture validated alongside the [openbnb-org/mcp-server-airbnb](https://github.com/openbnb-org/mcp-server-airbnb) MCPB install pattern.
## License
MIT — see [LICENSE](LICENSE).
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器