Back to Directory/Developer Tools

io.github.cyanheads/sanctions-screening-mcp-server

Screen names against OFAC, EU, UK, UN sanctions lists; resolve entities via GLEIF. Screening aid.

Developer ToolsTypeScriptv0.5.0

@cyanheads/sanctions-screening-mcp-server

Screen names against the consolidated OFAC, EU, UK, and UN sanctions lists and resolve legal entities against GLEIF, fuzzy-matched offline over a local SQLite + FTS5 mirror. A screening aid, not a compliance determination.

6 Tools • 3 Resources • 1 Prompt

Version License MCP SDK TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


Overview

Entity screening and resolution over the consolidated OFAC, EU, UK, and UN sanctions lists plus the GLEIF legal-entity registry, matched offline against a local mirror. Screen a name for potential watchlist hits, resolve a company to its LEI, and trace its beneficial-ownership chain from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.

Tools

ToolDescription
sanctions_screen_nameScreen a name (person, company, vessel, aircraft) against all loaded watchlists at once — OFAC SDN + Consolidated, EU, UK, UN — alias- and fuzzy-aware, with source provenance on every hit.
sanctions_get_designationFetch the full record for one sanctions designation by source list + entry ID — aliases, identifiers, addresses, dates/places of birth, nationalities, program, and legal basis.
sanctions_resolve_entityResolve a company / organization name (+ optional jurisdiction) to ranked candidate GLEIF LEIs.
sanctions_get_entityFetch the full GLEIF Level 1 record for one LEI, plus a sanctions cross-reference screened on the legal name.
sanctions_trace_ownershipTrace the GLEIF Level 2 corporate-ownership graph for an LEI (parents and/or children), optionally screening every node for beneficial ownership.
sanctions_list_sourcesList the loaded watchlists and GLEIF datasets with record counts, source URLs, licenses, and mirror readiness/freshness.

Resources

ResourceDescription
sanctions://designation/{source}/{entryId}One sanctions designation by source + entry ID (URI mirror of sanctions_get_designation).
sanctions://entity/{lei}One GLEIF Level 1 entity by LEI (URI mirror of sanctions_get_entity's entity payload, without the screening cross-reference).
sanctions://sourcesLoaded lists + GLEIF datasets with counts and refresh timestamps (URI mirror of sanctions_list_sources).

All resource data is also reachable via the tools, which are the primary path for tool-only MCP clients.

Prompts

PromptDescription
sanctions_vet_counterpartySequence the tools into a full counterparty due-diligence pass: resolve → trace ownership → screen the entity and every beneficial owner → summarize with provenance and the decision-support caveat.

Capability reference

sanctions_screen_name tool

  • Fans out across OFAC SDN, OFAC Consolidated, EU, UK, and UN in one call; filterable by sources, entityType, and minScore
  • Alias-aware: matches primary names, a.k.a., and f.k.a. — not just the canonical name
  • matchMode: "strict" (default) is exact-normalized then all-tokens-present via FTS5; "fuzzy" adds Jaro-Winkler + Double-Metaphone and auto-triggers when strict finds nothing
  • Hits carry matchType (exact / strong / approximate); approximate hits add the raw Jaro-Winkler score (0–1) and queryTokenCoverage for tie-breaking
  • Paged: up to 100 per page (limit), totalAvailable / hasMore / nextOffset, with totalAvailableBasis marking the count exact or a scanned lower bound
  • Every response carries caveat — a hit is a candidate to verify, never a clearance

sanctions_get_designation tool

  • Full record by source + entryId (the sourceEntryId from a sanctions_screen_name hit)
  • Returns all published aliases, identifiers (passport / national ID / tax / registration), addresses, dates and places of birth, nationalities, program, legal basis, and designation date
  • Missing fields mean the source omitted them — never padded with fabricated data
  • Errors: designation_not_found, mirror_not_ready

sanctions_resolve_entity tool

  • Resolves a company / organization name (+ optional ISO 3166-1 alpha-2 jurisdiction) to ranked GLEIF LEI candidates
  • status filter: issued (default), lapsed, or any; matches against legal and other/trading names
  • Same strict-then-fuzzy matching model as sanctions_screen_name, with the same matchType, score, and queryTokenCoverage fields
  • Paged: up to 50 per page (limit), same totalAvailable / totalAvailableBasis / hasMore / nextOffset contract

sanctions_get_entity tool

  • Full GLEIF Level 1 record by 20-character LEI (regex-validated: 18 alphanumerics + 2 check digits)
  • Legal name, other/trading names, legal + headquarters addresses, registration status, jurisdiction, registration authority, last-update date
  • Cross-references the legal name against all watchlists, strict-only (auto-fuzzy on a generic legal name would flood the result with false positives); capped at 25 hits
  • screeningStatus (screened / not_ready) says whether the cross-reference ran at all — an empty hit list under not_ready is not a clean screen
  • sanctionsScreen.hasMore flags a capped cross-reference; re-screen the legal name with sanctions_screen_name for the full set
  • Errors: lei_not_found, mirror_not_ready

sanctions_trace_ownership tool

  • Breadth-first GLEIF Level 2 ownership traversal from a root LEI; direction (parents / children / both, default both), depth 1–5 (default 3)
  • Returns nodes (with role and depth) and directed edges with relationshipType
  • screenNodes: true screens every node's legal name against all watchlists, strict-only, capped at 10 hits per node
  • complete is true only when nothing was truncated by depth AND every node resolved to a GLEIF Level 1 record; truncated and missingEntityLeis say which is false
  • screeningStatus (screened / not_requested / not_ready) plus screenedNodeCount / flaggedNodeCount report per-node screening coverage
  • Errors: lei_not_found, mirror_not_ready

sanctions_list_sources tool

  • No input — lists every loaded sanctions source (OFAC SDN, OFAC Consolidated, EU, UK, UN) plus the GLEIF dataset
  • Each source reports record count, upstream source URL, and redistribution license
  • sanctionsReady / sanctionsAsOf and leiReady / leiAsOf report mirror readiness and last-sync timestamp for freshness checks

sanctions://designation/{source}/{entryId} resource

  • Read-only URI mirror of sanctions_get_designation's payload
  • source is one of ofac_sdn / ofac_consolidated / eu / uk / un; entryId is the source list's own entry ID
  • Cached an hour, scoped private (the deployment may be auth-gated)
  • Errors: designation_not_found, mirror_not_ready

sanctions://entity/{lei} resource

  • Read-only URI mirror of sanctions_get_entity's GLEIF Level 1 payload, without the screening cross-reference (tool-only)
  • lei is regex-validated: 20 chars, 18 alphanumerics + 2 check digits
  • Cached an hour, scoped private
  • Errors: lei_not_found, mirror_not_ready

sanctions://sources resource

  • Read-only URI mirror of sanctions_list_sources — loaded lists + GLEIF dataset with counts, URL, license, and readiness timestamps
  • ttlMs: 0 — never cached, since mirror readiness and the as-of timestamps are the payload itself

sanctions_vet_counterparty prompt

  • Arguments: name required; jurisdiction optional (ISO 3166-1 alpha-2) to disambiguate
  • Sequences the tools into a due-diligence pass: screen the name, resolve it to an LEI, trace ownership with screenNodes: true, pull the full designation record for any hit, then summarize with provenance and the decision-support caveat
  • Returns one user message carrying the workflow instructions — no new capability, a reusable framing over the existing tools

Source lists

The server aggregates five upstream sources behind the screening surface. All are bulk, keyless, and clear for redistribution.

SourceRoleLicense
OFAC SDN + Consolidated (US Treasury)Primary US sanctions/watchlist — individuals, entities, vessels, aircraft, with a.k.a. aliasesUS Government public domain
EU Consolidated Financial Sanctions ListEU-designated persons and entitiesFreely redistributable
UK Sanctions List (UKSL, FCDO)UK sanctions targets — persons, entities, shipsOpen Government Licence v3.0
UN Security Council Consolidated ListUN-designated individuals and entities across all regimesFreely redistributable
GLEIF LEI (Level 1 + Level 2)Who-is-who (entity reference) and who-owns-whom (corporate ownership)CC0 1.0 Universal

The UK source is the UK Sanctions List (UKSL), the single authoritative UK source since the OFSI Consolidated List closed on 28 January 2026.

First run: populate the mirror

The mirror is not bundled — the sanctions lists and the GLEIF golden copy are downloaded and normalized on first run. Run the init lifecycle script out-of-band before screening:

bun run mirror:init

This streams all five sanctions lists in full, rebuilds the per-alias name index, then streams the GLEIF golden copy (Level 1 entities + Level 2 ownership relationships). It is resumable and intended to run once, off the request path.

ScriptPurpose
bun run mirror:initFull initial load of all sources (sanctions lists + GLEIF golden copy).
bun run mirror:refreshRe-harvest the sanctions lists and apply GLEIF deltas. The sanctions half (lists + name index) also runs on a cron under HTTP transport; GLEIF deltas are manual.
bun run mirror:verifyReport mirror readiness and per-source record counts.
bun run mirror:seedLoad a small synthetic fixture for local smoke tests (no downloads).

Set SANCTIONS_INIT_SKIP_GLEIF=1 on mirror:init to load the sanctions lists only and skip GLEIF.

Memory note: every leg of mirror:init streams. The sanctions documents total roughly 172 MB, of which OFAC SDN_ADVANCED.XML is about 120 MB on its own; the GLEIF Level 1 golden copy is roughly 3.3M LEI records (~892 MB compressed, several GB decompressed). Each source is scanned one record at a time and ingested in bounded batches, so peak resident memory tracks the batch size rather than the size of any source document. Size disk for the mirror accordingly — GLEIF dominates there — or skip GLEIF with SANCTIONS_INIT_SKIP_GLEIF=1 if you only need watchlist screening.

Features

Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.

Sanctions-screening-specific:

  • Multi-source, workflow-organized surface — one screen fans out across OFAC, EU, UK, and UN internally; the matching source surfaces only as provenance per hit
  • Local SQLite + FTS5 mirror via the framework MirrorService — offline, keyless, no per-request rate limit
  • Normalized common schema across the four sanctions lists, with a per-alias name index so a query matches any of an entity's names in one FTS scan
  • Strict-then-fuzzy matching: exact-normalized → all-tokens-present (FTS5) → Jaro-Winkler + Double-Metaphone, capped to bound work on short queries
  • GLEIF Level 1 + Level 2 ingest for entity resolution and beneficial-ownership tracing

Agent-friendly output:

  • Real signal, not synthetic confidence — approximate hits carry the raw Jaro-Winkler similarity (0–1) and a literal query-token coverage count, never a blended verdict
  • Provenance on every hit — source list, sanctioning program, designation date, and the exact name/alias that matched, typed (primary / aka / fka / low-quality-aka)
  • Decision-support caveat carried in every screening tool's output — a hit is a candidate to verify, an empty result is not a clearance
  • Freshness surfaced via sanctions_list_sources — each source's record count and the mirror's as-of timestamp

Getting started

Public Hosted Instance

A public instance is available at https://sanctions-screening.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP, with this client config:

{
  "mcpServers": {
    "sanctions-screening-mcp-server": {
      "type": "streamable-http",
      "url": "https://sanctions-screening.caseyjhand.com/mcp"
    }
  }
}

Self-Hosted / Local

Add the following to your MCP client configuration file. The server is offline-first — populate the mirror with bun run mirror:init before screening (see Source lists).

{
  "mcpServers": {
    "sanctions-screening-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["@cyanheads/sanctions-screening-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}

Or with npx (no Bun required):

{
  "mcpServers": {
    "sanctions-screening-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@cyanheads/sanctions-screening-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}

For Streamable HTTP, set the transport and start the server:

MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp

Prerequisites

  • Bun v1.4.0 or higher (or Node.js v24+).
  • Disk for the local mirror (the populated SQLite files; GLEIF Level 1 dominates). No API key for any source.

Installation

  1. Clone the repository:
git clone https://github.com/cyanheads/sanctions-screening-mcp-server.git
  1. Navigate into the directory:
cd sanctions-screening-mcp-server
  1. Install dependencies:
bun install
  1. Configure environment:
cp .env.example .env
# edit .env if you need to override defaults (all optional)
  1. Populate the mirror:
bun run mirror:init

Configuration

All sources are keyless — there is no required API key. Every variable below is optional with a sensible default.

VariableDescriptionDefault
SANCTIONS_MIRROR_PATHFilesystem path for the SQLite mirror; a persistent volume on a hosted deployment../data/sanctions.db
SANCTIONS_REFRESH_CRONCron for the scheduled refresh of the sanctions lists + name index (HTTP transport only). GLEIF deltas are refreshed manually via mirror:refresh.0 4 * * *
SANCTIONS_FUZZY_MIN_SCOREDefault Jaro-Winkler similarity floor for fuzzy matches when minScore is omitted.0.85
SANCTIONS_FUZZY_MAX_RESULTSHard cap on fuzzy candidates scored per query, to bound work on short queries.50
OFAC_SDN_URLOverride for the OFAC SDN advanced-XML file.official SLS URL
OFAC_CONSOLIDATED_URLOverride for the OFAC Consolidated advanced-XML file.official SLS URL
EU_FSF_URLOverride for the EU consolidated XML file (includes the static public token path component).official EU URL
UK_SANCTIONS_URLOverride for the UK Sanctions List (UKSL) XML file.official FCDO URL
UN_SC_URLOverride for the UN Security Council consolidated XML file.official UN URL
GLEIF_GOLDEN_COPY_BASE_URLOverride for the GLEIF golden-copy / delta download API.https://goldencopy.gleif.org
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_SESSION_MODESession mode: stateful, stateless, or auto (the schema default, which resolves to stateful). createApp() declares stateless in src/index.ts, and the shipped .env.example and Docker image set it too — no tool here needs a multi-round-trip input. Setting the variable overrides the declaration.stateless
MCP_HTTP_PORTPort for the HTTP server.3010
MCP_LOG_LEVELLog level (RFC 5424).info

Source URLs default to the verified official endpoints; overrides exist for testing and for pinning a mirror in restricted environments. The EU "token" is a static public path component, not a credential.

See .env.example for the full list of optional overrides.

Running the server

Local development

  • Build and run:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:stdio
    # or
    bun run start:http
    
  • Run checks and tests:

    bun run devcheck   # Lint, format, typecheck, security, changelog sync
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    

Docker

docker build -t sanctions-screening-mcp-server .
docker run --rm -p 3010:3010 -v sanctions-data:/usr/src/app/data sanctions-screening-mcp-server

The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/sanctions-screening-mcp-server. The image runs under Bun, so the mirror uses bun:sqlite (no native build). Mount a volume at the mirror path (/usr/src/app/data by default) so the populated mirror survives container restarts, and run bun run mirror:init inside the container (docker exec) to populate it. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.

Project structure

DirectoryPurpose
src/index.tscreateApp() entry point — registers tools/resources/prompts, inits the screening service, schedules the HTTP refresh.
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts) — the six screening/resolution tools.
src/mcp-server/resourcesResource definitions (*.resource.ts) — the three URI mirrors.
src/mcp-server/promptsPrompt definitions (*.prompt.ts) — the counterparty vetting prompt.
src/services/screeningThe screening service — local mirror, normalized schema, source ingesters (OFAC/EU/UK/UN/GLEIF), and the strict/fuzzy matching engine.
scripts/mirror-*.tsMirror lifecycle CLI — init, refresh, verify, seed.
tests/Unit and integration tests mirroring src/.

Development guide

See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:

  • Handlers throw, framework catches — no try/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
  • Register new tools and resources via the barrels in src/mcp-server/*/definitions/index.ts
  • Wrap external sources: validate raw → normalize to the common schema → return the output schema; never fabricate fields a source omits, and never synthesize a confidence score

Disclaimer

[!IMPORTANT] This is a screening aid, not legal or compliance certification. Every tool returns potential matches with a transparent score and source provenance — never a verdict. A hit means "review this candidate against the official source"; an empty result never means "cleared." Real sanctions compliance is a legal process — it requires human review and a qualified compliance determination. This server feeds that process; it does not perform it, and its output is not a compliance record.

Attribution

This server redistributes open data from the following sources, cited here per their terms:

  • OFAC SDN and Consolidated lists — US Department of the Treasury, Office of Foreign Assets Control (US Government public domain).
  • EU Consolidated Financial Sanctions List — European Commission / EEAS (freely redistributable).
  • UK Sanctions List — UK Foreign, Commonwealth & Development Office, licensed under the Open Government Licence v3.0 (attribution required).
  • UN Security Council Consolidated List — United Nations Security Council (freely redistributable).
  • GLEIF LEI data — Global Legal Entity Identifier Foundation, CC0 1.0 Universal.

Contributing

Issues are welcome. Run checks and tests before submitting:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y @cyanheads/sanctions-screening-mcp-server

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-cyanheads-sanctions-screening-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@cyanheads/sanctions-screening-mcp-server"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

@cyanheads/sanctions-screening-mcp-servernpm

Compatible MCP Clients

io.github.cyanheads/sanctions-screening-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More