redflag-mcp
Utilizes OpenAI models (gpt-4o-mini) for extracting structured AML red flag indicators from regulatory documents during the extraction pipeline phase.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@redflag-mcpwhat are red flags for cryptocurrency money laundering?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
redflag-mcp
MCP server exposing AML red flag knowledge as queryable tools. Compliance officers ask natural-language questions; the server returns relevant, sourced red flags from either a local LanceDB vector store or a packaged SQLite FTS5 corpus.
Hosted Connector
Public users should start with the hosted MCP URL:
https://redflag-mcp.up.railway.app/mcpAdd that URL in a hosted MCP client, enable the connector, and ask AML red flag research questions such as:
What red flags apply to TBML invoice mismatch?
Which red flags cover bulk cash movement to Mexico?
List source coverage for the corpus.Public hosted mode is not for confidential customer, transaction, institution, or investigation details. User prompts are sent to the hosted MCP service operator and the host client. Use local desktop or institution-hosted deployments for sensitive institution-specific context.
The hosted connector is backed by a verified packaged corpus. End users do not need Python, repository setup, package downloads, ingestion, OpenAI keys, or environment variables. Operators should use docs/hosted-deployment.md for Railway deployment, corpus activation, rollback, logging, and validation.
Related MCP server: Financial Intelligence MCP Server
Overview
Nine distinct workflows:
URL pipeline — download URLs, optionally inspect local captures, then extract red flags
Source harvesting — bulk-download PDFs and web pages from a catalog CSV into
sources.yamlExtraction — pull AML red flags out of PDFs or web pages using an LLM and save them as YAML
Verification — second-stage LLM classifier removes false positives (compliance guidance, regulatory instructions, case narratives) from extracted candidates
Source registry — rebuild
red_flag_sources/registry.csv, the audit ledger for extracted, downloaded, and not-downloaded sourcesIngestion — embed the YAML files and load them into the local vector database
Corpus packaging — build a versioned SQLite FTS5 package for offline lexical runtime use
Hosted deployment — run the ASGI MCP service from a verified corpus package at one public
/mcpURLQuery — MCP server answers search and filtering requests against the configured local or hosted store
URL Pipeline
Use scripts/pipeline.py for day-to-day source onboarding from URLs. The download and run subcommands accept either a URL file or a single URL as the positional argument. It supports both a one-shot workflow and a review checkpoint between download and extraction.
For batches, create a plain text file with one URL per line:
https://example.gov/report.pdf
https://example.gov/red-flag-guidanceBlank lines and non-HTTP(S) lines are skipped.
One-shot download and extraction
# From a URL file
uv run python scripts/pipeline.py run urls.txt
# Or directly with a single URL
uv run python scripts/pipeline.py run https://example.gov/report.pdfUse this when you trust the source list and want to download each URL, register it in red_flag_sources/sources.yaml, extract red flags, update data/source/.extracted_sources.yaml, and rebuild red_flag_sources/registry.csv.
Download, inspect, then extract
# Download PDFs/web captures and update sources.yaml + registry.csv
uv run python scripts/pipeline.py download urls.txt
uv run python scripts/pipeline.py download https://example.gov/report.pdf
# Inspect red_flag_sources/pdf/ and red_flag_sources/markdown/, then extract downloaded rows
uv run python scripts/pipeline.py extractUse the two-step flow when you want to inspect Jina Reader markdown captures or downloaded PDFs before spending OpenAI extraction calls.
Options
# Bypass registry deduplication and re-download/re-extract
uv run python scripts/pipeline.py run urls.txt --force
# Extract downloaded sources in parallel; default is sequential unless --parallel is present
uv run python scripts/pipeline.py extract --parallel
uv run python scripts/pipeline.py extract --parallel 8
uv run python scripts/pipeline.py run urls.txt --parallel 4
# Skip the verification step entirely
uv run python scripts/pipeline.py extract --no-verify
uv run python scripts/pipeline.py run urls.txt --no-verify
# Force the handcrafted verification prompt (ignores verifier_prompt.json if present)
uv run python scripts/pipeline.py extract --prompt handcrafted
uv run python scripts/pipeline.py run urls.txt --prompt handcraftedVerification runs by default on both extract and run. The optimized prompt (data/verifier_prompt.json) is used when available; --prompt handcrafted forces the built-in prompt for comparison. See Red Flag Verification for details.
Deduplication uses red_flag_sources/registry.csv by source_url. Re-run scripts/build_registry.py first if you manually edited sources.yaml, catalog CSVs, or YAML source files and need the pipeline to see the latest status.
Source Harvesting
scripts/harvest_sources.py is the canonical download utility. It accepts either a catalog CSV path or a single http(s) URL as the positional argument, classifies each URL as a PDF or web page, downloads the file, and registers it in red_flag_sources/sources.yaml. No extraction is triggered — that is pipeline.py's job.
# Bulk download from a catalog CSV
uv run python scripts/harvest_sources.py red_flag_sources/Global_AML_CFT_Sanctions_Red_Flag_Catalog.csv
# Download a single URL
uv run python scripts/harvest_sources.py https://example.gov/report.pdf
# Re-download even if already registered
uv run python scripts/harvest_sources.py --force https://example.gov/report.pdfWhat it does:
Detects whether the positional argument is a URL or a CSV path
CSV mode — reads the
Direct URLcolumn from each row; skips blank, malformed, or already-registered URLsURL mode — skips immediately if the URL is already registered (use
--forceto override)Classifies each URL as PDF via path heuristics (
.pdfsuffix,/download,/file) — falls back to an HTTP HEAD check for ambiguous casesDownloads PDFs to
red_flag_sources/pdf/NNN.pdfFetches web pages via the Jina Reader API and saves cleaned markdown to
red_flag_sources/markdown/NNN.mdAppends each new entry to
sources.yamlIn CSV mode, prints a final summary: PDFs downloaded, web pages fetched, skipped, failed
The script is idempotent — re-running against the same CSV or URL produces no new files or registry entries (unless --force is set). Per-URL failures are logged and skipped without aborting a CSV run.
red_flag_sources/
Global_AML_CFT_Sanctions_Red_Flag_Catalog.csv # input catalog (~218 URLs)
sources.yaml # registry of all harvested URLs
pdf/ # downloaded PDFs (gitignored via *.pdf)
markdown/ # Jina Reader captures (gitignored)After harvesting, rebuild the status registry and pass downloaded files to extraction:
uv run python scripts/build_registry.py
# Extract red flags from all newly downloaded PDFs
uv run python scripts/extract.py --parallel
# Or target a specific serial range
uv run python scripts/extract.py --range 039-060 --parallelWhen to use which script:
Goal | Use |
Download a catalog CSV or a single URL (no extraction) |
|
Download + extract from a URL file or single URL in one step |
|
Extract from already-downloaded local files |
|
Note:
sources.yamlis the shared URL registry forpipeline.py,harvest_sources.py, andbuild_sources_registry.py. Do not run these scripts concurrently — each can overwritesources.yamlafter updating it.⚠️
build_sources_registry.pyis legacy — do not run it in normal workflows. It fully overwritessources.yamlby renumbering001..NNNfromsource_file_catalogue/pdflinks.txt, which is now far out of sync (it holds fewer URLs thansources.yaml). Running it would delete entries and scramble the PDF-filename ↔ key numbering.sources.yamlis maintained directly and appended to byharvest_sources.py. Only use it for a deliberate full rebuild frompdflinks.txt.
Web-page capture via Jina Reader
Web pages (non-PDF URLs) are fetched through Jina Reader, a hosted service that takes a URL, strips navigation/footer/scripts/ads, and returns clean, LLM-ready markdown.
Endpoint:
https://r.jina.ai/<url>— prepend the target URL to capture itCaller:
fetch_web()inscripts/harvest_sources.py; also used internally bypipeline.pyviadownload_single_url()Auth: used unauthenticated in this repo. Jina's free tier works without an API key; supplying one as a
Bearertoken raises rate limits if you hit themWhy this service: regulator pages mix substance with heavy chrome (menus, related-links, cookie banners). Jina Reader gives the LLM only the article body, which materially improves extraction quality and reduces token spend
If a captured markdown file in red_flag_sources/markdown/ looks empty or wrong, inspect it before extraction — the page may have been blocked, paywalled, or rendered client-side. Re-run with --force after fixing the URL.
Extraction Pipeline
scripts/extract.py takes a downloaded regulatory document (local PDF or markdown file path), sends its text to an OpenAI model, and writes a structured YAML file into data/source/. Each extracted entry includes a source_url linking back to the original document (resolved from sources.yaml).
extract.pyno longer downloads URLs. To fetch a URL, useharvest_sources.py(download only) orpipeline.py run(download + extract). This keeps each script's responsibility clean.
Prerequisites
uv sync --extra dev
export OPENAI_API_KEY=sk-...Adding sources in bulk (recommended workflow)
Use scripts/pipeline.py for new URL lists:
uv run python scripts/pipeline.py download urls.txt
uv run python scripts/pipeline.py extract --parallelThis downloads into red_flag_sources/pdf/ or red_flag_sources/markdown/, updates sources.yaml, extracts downloaded registry rows, updates .extracted_sources.yaml, and rebuilds registry.csv.
For catalog CSVs, use scripts/harvest_sources.py first, then scripts/extract.py.
Manual PDF workflow
PDFs are stored in red_flag_sources/pdf/ and should be named with a zero-padded serial prefix:
red_flag_sources/pdf/
001_fincen_alert_russian_sanctions_evasion.pdf
002_ffiec_bsa_aml_examination_manual.pdf
003_fatf_guidance_virtual_assets.pdfEach serial number maps to a public URL for the source document in red_flag_sources/sources.yaml. For the legacy manual flow, maintain that mapping in red_flag_sources/pdflinks.txt — one URL per line, in serial order:
# FinCEN Russian Sanctions Evasion Alert
https://fincen.gov/sites/default/files/2022-06/Alert%20FIN-2022-Alert001_508C.pdf
# FFIEC BSA/AML Examination Manual
https://bsaaml.ffiec.gov/manual
# FATF Guidance on Virtual Assets
https://www.fatf-gafi.org/...Blank lines and lines starting with # are ignored. After editing pdflinks.txt, regenerate sources.yaml and registry.csv:
uv run python scripts/build_sources_registry.py
uv run python scripts/build_registry.pyThen run batch extraction:
uv run python scripts/extract.py --parallelOnly new (unprocessed) PDFs are extracted — previously processed sources are skipped automatically.
Batch extraction commands
# Sequential batch
uv run python scripts/extract.py
# Parallel batch (4 workers by default)
uv run python scripts/extract.py --parallel
# Parallel batch with custom worker count
uv run python scripts/extract.py --parallel 8
# Force re-extract everything
uv run python scripts/extract.py --force --parallel
# Process only PDFs in a serial range (e.g. 001 through 005)
uv run python scripts/extract.py --range 001-005
# Range + parallel
uv run python scripts/extract.py --range 001-005 --parallel
# Force re-extract a range
uv run python scripts/extract.py --force --range 001-005 --parallel
# Skip shaping (descriptions stay verbatim from extraction; useful for A/B comparison)
uv run python scripts/extract.py --no-shape --parallel
# Skip verification (raw extraction output, no false-positive filtering)
uv run python scripts/extract.py --no-verify --parallel
# Skip both passes (raw extractor output only)
uv run python scripts/extract.py --no-shape --no-verify --parallel
# Use handcrafted verifier prompt instead of optimized (useful for A/B comparison)
uv run python scripts/extract.py --prompt handcrafted --parallelNote:
--rangeapplies only to numbered PDFs. Web URLs inWeblinks.mdare excluded when a range is active.
Single source (ad hoc)
# Extract from a local PDF
uv run python scripts/extract.py red_flag_sources/pdf/001_fincen_alert.pdf
# Extract from a local markdown capture
uv run python scripts/extract.py red_flag_sources/markdown/061.md
# Re-extract a source that was already processed
uv run python scripts/extract.py --force red_flag_sources/pdf/001_fincen_alert.pdf
# Re-extract without verification
uv run python scripts/extract.py --force --no-verify red_flag_sources/pdf/001_fincen_alert.pdf
# Re-extract using handcrafted prompt
uv run python scripts/extract.py --force --prompt handcrafted red_flag_sources/pdf/001_fincen_alert.pdfextract.py requires the file to exist locally already. To fetch a URL first:
# Download only, then extract separately
uv run python scripts/harvest_sources.py https://example.gov/report.pdf
uv run python scripts/extract.py red_flag_sources/pdf/NNN.pdf
# Or download + extract in one step
uv run python scripts/pipeline.py run https://example.gov/report.pdfFor single-source PDFs, make sure sources.yaml maps the file's serial prefix to the public URL before extraction so the extractor can populate source_url in the output. If you maintain the legacy pdflinks.txt file, run build_sources_registry.py and then build_registry.py first.
What it does
Reads the document — extracts text from the local PDF via pdfplumber, or reads the body of a Jina Reader markdown capture
Extracts — prompts the configured OpenAI model (override with
OPENAI_EXTRACTION_MODEL) to extract every distinct AML red flag indicator and tag all metadata fields as structured JSON. Descriptions are returned in source-faithful wording.Shapes — a second LLM call rewrites only the
descriptionfield: prepends a noun subject when missing, merges dependent explanatory sentences, generalizes case-specific numbers, and strips stray named facts. Skip with--no-shape. See Red Flag Shaping below.Verifies — a third LLM call classifies each candidate as a genuine red flag or false positive (compliance guidance, regulatory instruction, etc.) and removes false positives. Skip with
--no-verify. See Red Flag Verification below.Infers regulator — when a
source_urlis available, the regulator is inferred deterministically from the URL domain (e.g.ofac.treasury.gov→OFAC), overriding LLM extractionValidates — each returned flag is checked against the
RedFlagSourceschema; invalid entries are skipped with a warningWrites YAML — saves to
data/source/<slug>.yaml, one entry per red flagUpdates the manifest — records the source in
data/source/.extracted_sources.yamlto prevent re-processingRebuilds the source registry — updates
red_flag_sources/registry.csvafter successful batch or single-source extraction
Output schema
Each entry in the YAML file has the following fields:
Field | Type | Required | Description |
| string | yes | Unique identifier, e.g. |
| string | yes | Standalone description of the red flag indicator |
| string | no | Public URL of the source document |
| list[string] | no | Financial products this applies to (e.g. |
| list[string] | no | Customer industries or sectors this applies to (e.g. |
| list[string] | no | Customer archetypes this applies to (e.g. |
| list[string] | no | Relevant geographies or corridors (e.g. |
| string | no | Source document name or authority (e.g. |
| string | no | Abbreviated issuing authority (e.g. |
| string | no | Canonical jurisdiction code deterministically derived from |
| string | no | Publication date of the source document (ISO 8601: YYYY-MM-DD, YYYY-MM, or YYYY). |
| string | no |
|
| string | no | AML typology (e.g. |
| string | no | Optional simulation complexity code (e.g. |
| list[string] | no | Higher-level AML typology families (e.g. |
| list[string] | no | Observable behavioral patterns (e.g. |
| list[string] | no | Short searchable phrases, instruments, thresholds, or acronyms (e.g. |
regulator and issued_date are requested during extraction. regulator_jurisdiction is derived in code from regulator; if the regulator is missing or unmapped, it stays unset and ingestion logs a warning. typology_family, transaction_patterns, and key_terms are added to existing YAML source files by running scripts/ingest.py --write-back-yaml (see Enriching YAML source files below).
Deduplication
data/source/.extracted_sources.yaml tracks every processed source by its canonical path or URL. Sources already in the manifest are skipped in both batch and single-source mode. Use --force to re-extract a source regardless.
Red Flag Shaping
Between extraction and verification, a shaping pass rewrites only the description field on each candidate so the corpus reads consistently. It does not touch metadata.
What it does
Prepends a concrete noun subject ("Customers," "Entities or individuals," "Transactions," etc.) when the source wording starts with a verb phrase or orphaned predicate.
Merges dependent explanatory sentences ("Such…", "Similarly…", "These…") into the preceding sentence so each indicator reads as one unit.
Generalizes case-specific dollar amounts, percentages, and counts (
"$100 million"→"large sums (e.g., $100 million)";"two exchanges"→"exchanges"), while leaving structural numbers like CTR thresholds alone.Strips named persons, companies, or one-off facts that slipped through extraction.
The shaper uses the same default model as extraction (gpt-5.4-mini); override with OPENAI_SHAPING_MODEL.
Skipping shaping
# Single source — keep descriptions verbatim from extraction
uv run python scripts/extract.py --force --no-shape red_flag_sources/pdf/048*.pdf
# Skip both shaping and verification (raw extractor output)
uv run python scripts/extract.py --force --no-shape --no-verify red_flag_sources/pdf/048*.pdfUse --no-shape when debugging the extraction prompt or comparing shaped vs. raw output.
Red Flag Verification
The extraction pipeline includes a third-stage LLM verifier that filters out false positives — items that look like red flags but are actually compliance guidance, regulatory instructions, case narratives, or general background. The verifier makes a single OpenAI call per document batch.
How it works
After extract_red_flags() returns candidate items, verify_red_flags() sends all descriptions to the LLM in one call. Each candidate is classified as a genuine red flag (true) or not (false). Only candidates classified as true proceed to validation and YAML output.
Controlling which prompt is used
By default the verifier loads data/verifier_prompt.json (the DSPy-optimized prompt) when it exists, and falls back to the handcrafted prompt otherwise. Use --prompt to override:
# Force the handcrafted prompt even when verifier_prompt.json exists
uv run python scripts/extract.py --prompt handcrafted red_flag_sources/pdf/001*.pdf
# Explicitly request the optimized prompt (default behaviour, but makes intent clear)
uv run python scripts/extract.py --prompt optimized red_flag_sources/pdf/001*.pdfSkipping verification
# Extract without the verification step at all
uv run python scripts/extract.py --force --no-verify red_flag_sources/pdf/048*.pdfUse --no-verify when you want raw extraction output or are debugging the extraction prompt.
Evaluating verifier accuracy
scripts/eval_verifier.py measures verifier performance against hand-labelled data in data/source/labelled/. Each labelled YAML file contains items with a flag: True/False field.
# Run eval with the active prompt (optimized if available, else handcrafted)
uv run python scripts/eval_verifier.py
# Force the handcrafted prompt for comparison
uv run python scripts/eval_verifier.py --prompt handcrafted
# A/B comparison in one go
uv run python scripts/eval_verifier.py --prompt handcrafted
uv run python scripts/eval_verifier.py --prompt optimized
# Test a specific model
uv run python scripts/eval_verifier.py --model gpt-4o
# Output as JSON for programmatic consumption
uv run python scripts/eval_verifier.py --jsonThe eval reports precision, recall, F1, accuracy, and confusion matrix for both the verifier and a baseline (no verification — all items classified as True). The output header shows which prompt was used.
Optimizing the verifier prompt with DSPy
scripts/optimize_verifier.py uses DSPy to find the best verifier prompt by training on the labelled dataset. It uses BootstrapFewShotWithRandomSearch to optimize few-shot demos and instructions.
# Install the optimize extra
uv sync --extra optimize
# Run optimization (uses gpt-5.4-nano by default)
uv run python scripts/optimize_verifier.py
# Use a different model or strategy
uv run python scripts/optimize_verifier.py --model openai/gpt-4o-mini
uv run python scripts/optimize_verifier.py --strategy predict # direct classification
uv run python scripts/optimize_verifier.py --strategy cot # chain-of-thought (default)
uv run python scripts/optimize_verifier.py --max-demos 6The optimized prompt is saved to data/verifier_prompt.json. Once this file exists, build_verification_prompt() automatically loads and uses it instead of the handcrafted prompt. Delete the file to revert to the handcrafted prompt.
After optimization, re-run the eval to confirm improvement:
uv run python scripts/eval_verifier.pyAdding labelled data
To improve the verifier, add more labelled examples in data/source/labelled/. Each file follows the standard YAML source format with an additional flag field:
- id: example-01
description: "Customer structures transactions below reporting thresholds."
flag: True # genuine red flag
# ... other fields ...
- id: example-02
description: "The organization should conduct an OFAC risk assessment."
flag: False # compliance guidance, not a red flag
# ... other fields ...After adding labelled data, re-run optimization and eval to update the verifier.
Source Registry
scripts/build_registry.py rebuilds red_flag_sources/registry.csv from scratch. The registry is a human-readable audit ledger across three states:
Status | Meaning |
| A YAML file exists in |
| The URL is present in |
| The URL appears in the catalog CSVs, but is not present in |
Run it manually after editing catalog CSVs, sources.yaml, or extracted YAML files outside the normal scripts:
uv run python scripts/build_registry.pyYou usually do not need to run it after pipeline.py extract or extract.py; both rebuild the registry after successful extraction. pipeline.py download rebuilds it after each successful download so newly captured URLs appear as downloaded.
The registry powers pipeline deduplication and extraction auto-discovery:
pipeline.py downloadskips URLs already present inregistry.csvunless--forceis used.pipeline.py extractfinds rows withstatus == "downloaded"and extracts their local PDF or markdown files.
Ingestion
After extraction, embed the YAML files and load them into the vector database:
uv run python scripts/ingest.pyFor the initial local corpus, ingest only the three target files:
uv run python scripts/ingest.py \
data/source/001_federal_child_nutrition_fraud.yaml \
data/source/002_oil_smuggling_cartels.yaml \
data/source/003_bulk_cash_smuggling_repatriation.yamlThis generates embeddings with nomic-embed-text-v1.5 and upserts records into LanceDB at data/vectors/. Run ingestion before connecting the MCP server to a desktop client; the embedding model downloads on first use and is better cached during ingestion than during server startup.
OPENAI_API_KEY is optional for ingestion. When it is set, ingestion can auto-tag missing metadata into the derived LanceDB records. When it is not set, ingestion preserves available YAML metadata and leaves missing rich consultation fields empty. Source YAML files are not rewritten by normal ingestion.
Enriching YAML source files (write-back)
To enrich source YAML files with typology_family, transaction_patterns, key_terms, regulator, regulator_jurisdiction, and issued_date — fields used for offline keyword search and faceted filtering — run ingestion with --write-back-yaml:
export OPENAI_API_KEY=sk-...
uv run python scripts/ingest.py --write-back-yaml data/source/001_federal_child_nutrition_fraud.yamlWrite-back supports the same batch selection styles as extraction:
# All visible YAML files in data/source/
uv run python scripts/ingest.py --write-back-yaml
# Multiple explicit YAML files
uv run python scripts/ingest.py --write-back-yaml \
data/source/001_federal_child_nutrition_fraud.yaml \
data/source/002_oil_smuggling_cartels.yaml
# Serial range by source filename prefix
uv run python scripts/ingest.py --write-back-yaml --range 001-003
# Parallel file-level write-back (4 workers by default, or pass a count)
uv run python scripts/ingest.py --write-back-yaml --range 001-003 --parallel
uv run python scripts/ingest.py --write-back-yaml --parallel 8This enriches each selected source file in-place and exits without updating the vector database. Existing metadata is not overwritten by the LLM; only missing fields are requested, and deterministic fields such as regulator_jurisdiction are derived in code. After write-back, re-run normal ingestion to load the enriched records:
uv run python scripts/ingest.py data/source/001_federal_child_nutrition_fraud.yamlNote: If you deploy this change against an existing
data/vectors/store, delete the store and re-ingest from scratch so the new columns (typology_family,transaction_patterns,key_terms,regulator,regulator_jurisdiction,issued_date) are present in the LanceDB schema:rm -rf data/vectors/ uv run python scripts/ingest.py
Corpus Packaging
Maintainers can build a versioned, verifiable SQLite FTS5 corpus package from approved YAML records:
uv run python scripts/build_corpus.py \
--output-dir dist/corpus \
--version 2026.04.29 \
--all-sources
# Or build a curated corpus from explicit YAML files
uv run python scripts/build_corpus.py \
--output-dir dist/corpus \
--version 2026.04.29 \
data/source/001_federal_child_nutrition_fraud.yaml \
data/source/002_oil_smuggling_cartels.yaml \
data/source/003_bulk_cash_smuggling_repatriation.yaml
uv run python scripts/verify_corpus.py dist/corpus/redflag-corpus-2026.04.29.zipThe package contains manifest.json and redflags.sqlite. The manifest records schema version, build timestamp, source record hashes, file hashes, record/source counts, and source redistribution metadata. Source documents are treated as URL-only unless data/lexicon/source_metadata.yaml explicitly clears them for bundling.
The current SQLite lexical corpus schema version is 4. Rebuild older corpus packages after schema changes that add stored fields or filters.
Run the hosted retrieval smoke benchmark before publishing a corpus package:
uv run python scripts/evaluate_retrieval.py \
--corpus dist/corpus/redflag-corpus-2026.04.29.zip \
--benchmark data/eval/hosted_retrieval_queries.yamlThis benchmark checks representative alias, geography, typology, product/channel, and source-specific queries against the lexical corpus. It is a launch gate, not proof of broad AML retrieval quality.
Repackage and deploy the hosted corpus
From the repository root, set a new release version and deterministic timestamp. Use the current release date for normal hosted corpus releases:
VERSION=2026.06.13
TIMESTAMP=2026-06-13T12:00:00Z
uv run python scripts/build_corpus.py \
--output-dir dist/corpus \
--version "$VERSION" \
--build-timestamp "$TIMESTAMP" \
--all-sourcesRegenerate dist/corpus/releases.json from the packaged ZIP manifests:
uv run python - <<'PY'
import hashlib
import json
import zipfile
from pathlib import Path
out = Path("dist/corpus")
releases = []
for package in sorted(out.glob("redflag-corpus-*.zip")):
with zipfile.ZipFile(package) as archive:
manifest = json.loads(archive.read("manifest.json"))
releases.append(
{
"artifact": f"dist/corpus/{package.name}",
"package_id": manifest["package_id"],
"record_count": manifest["record_count"],
"schema_version": manifest["schema_version"],
"sha256": hashlib.sha256(package.read_bytes()).hexdigest(),
"source_count": manifest["source_count"],
"version": manifest["version"],
}
)
releases.sort(key=lambda item: item["version"], reverse=True)
Path("dist/corpus/releases.json").write_text(
json.dumps(
{
"latest_compatible_version": releases[0]["version"],
"releases": releases,
"schema_version": 1,
},
indent=2,
sort_keys=True,
)
+ "\n"
)
PYPin Railway to the new release:
perl -0pi -e "s/REDFLAG_CORPUS_VERSION = \"[^\"]+\"/REDFLAG_CORPUS_VERSION = \"$VERSION\"/" railway.tomlVerify the package before publishing:
uv run python scripts/verify_corpus.py "dist/corpus/redflag-corpus-$VERSION.zip"
uv run python scripts/evaluate_retrieval.py \
--corpus "dist/corpus/redflag-corpus-$VERSION.zip" \
--benchmark data/eval/hosted_retrieval_queries.yaml
uv run pytest tests/Commit and push the release artifacts:
git add \
"dist/corpus/redflag-corpus-$VERSION.zip" \
dist/corpus/releases.json \
railway.toml
git commit -m "data: package $VERSION hosted corpus"
git push origin mainCheck Railway after the push. /ready should report the new corpus.version and expected record count once the deployment has rolled:
curl -fsS https://redflag-mcp.up.railway.app/health
curl -fsS https://redflag-mcp.up.railway.app/readyRunning from a corpus
The server can run directly against a built SQLite corpus without loading the embedding model:
REDFLAG_CORPUS_PATH=dist/corpus/redflags.sqlite uv run python -m redflag_mcpIt can also verify and install a ZIP package into a local corpus cache:
REDFLAG_CORPUS_PACKAGE=dist/corpus/redflag-corpus-2026.04.29.zip \
REDFLAG_CORPUS_CACHE_DIR=~/.redflag-mcp \
uv run python -m redflag_mcpFor release-index driven activation:
REDFLAG_CORPUS_RELEASE_INDEX=dist/corpus/releases.json \
REDFLAG_CORPUS_VERSION=2026.04.29 \
REDFLAG_CORPUS_CACHE_DIR=~/.redflag-mcp \
uv run python -m redflag_mcpSet REDFLAG_CORPUS_AUTO_UPDATE=0 to reuse the active cached corpus without checking the package or release index. When no corpus environment variables are set, the server falls back to the LanceDB vector store at data/vectors/.
MCP Server
# Start server (stdio mode, for Claude Desktop / Claude Code)
uv run python -m redflag_mcp
# Start in MCP inspector
uv run mcp dev src/redflag_mcp/server.py
# Start as HTTP server (for OpenAI agents or other HTTP clients)
MCP_TRANSPORT=http MCP_HOST=0.0.0.0 MCP_PORT=8000 uv run python -m redflag_mcp
# Start from a packaged corpus instead of LanceDB
REDFLAG_CORPUS_PACKAGE=dist/corpus/redflag-corpus-2026.04.29.zip uv run python -m redflag_mcpThe server exposes hosted-client-compatible tools for request routing, ranked relevance search, exact metadata filtering, source browsing, and filter discovery:
classify_red_flag_requestfor deciding whether an ambiguous request needs more context, exact metadata filtering, filtered ranked relevance search, or direct ranked relevance searchsearch_red_flagsfor natural-language relevance search with sourced, ranked resultsfilter_red_flagsfor exact metadata requests that should not use ranked relevance search. Filters includesubjects,industry_groups,product_types,industry_types,customer_profiles,geographic_footprints,typology_family,transaction_patterns,category,risk_level,regulator,regulator_jurisdiction,issued_after,issued_before,regulatory_source,source_url, andsource_id. Exact filter responses includetotal_matched,returned,truncated, andnext_cursorfor complete pagination, plusdetail="concise"for cheap enumeration before callingget_red_flag.get_red_flagfor the full text and citation metadata for one red flaglist_filtersfor available metadata filter values, including geography tokens such asnorth_koreawhen present in the active corpuslist_sourcesandget_sourcefor ingested source coverage and citation context
Successful search_red_flags and filter_red_flags responses include the raw results records plus presentation helpers for chat clients:
{
"results": [],
"display": {
"suggested_format": "table",
"title": "Red flags for TBML invoice mismatch",
"columns": [
{"key": "description", "label": "Red flag"},
{"key": "risk_level", "label": "Risk"},
{"key": "transaction_patterns", "label": "Pattern"},
{"key": "regulator", "label": "Regulator"},
{"key": "source_url", "label": "Source"}
],
"row_count": 0
},
"markdown_table": "| Red flag | Risk | Pattern | Regulator | Source |..."
}display is a rendering hint for clients or models that choose to build a table UI from structured results. markdown_table is the portable fallback for ChatGPT, Claude, and other Markdown-capable clients; MCP does not guarantee a native table widget.
It is fully offline after ingestion or corpus installation — no API keys required at query time.
Use from Codex
For local Codex threads, prefer stdio so Codex starts the MCP server automatically:
codex mcp add redflag-mcp -- zsh -lc 'cd /Users/learningmachine/Documents/Python-dev/redflag-mcp && HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 uv run python -m redflag_mcp'Verify the registration:
codex mcp list
codex mcp get redflag-mcpThen start a new Codex thread and ask for the server by name, for example:
Use the redflag-mcp MCP server. List the available AML red flag filters.If you already have the HTTP server running, you can register that instead:
codex mcp add redflag-mcp-http --url http://127.0.0.1:8000/mcpLocal smoke checks
After ingesting the three target files, verify the tools with:
list_filters
list_sources
classify_red_flag_request(query="what red flags apply to my crypto product?")
filter_red_flags(product_types=["depository"], category="fraud_nexus", risk_level="medium")
filter_red_flags(subjects=["human_trafficking"], regulator="FINTRAC")
filter_red_flags(industry_groups=["trade_logistics"])
filter_red_flags(typology_family=["trade_based_money_laundering"], transaction_patterns=["trade_document_manipulation"])
filter_red_flags(regulator="FinCEN", issued_after="2024", issued_before="2026")
filter_red_flags(regulator_jurisdiction="FR")
search_red_flags(query="federal child nutrition program sponsor receives reimbursements inconsistent with its profile", product_types=["depository"])
search_red_flags(query="TBML invoice mismatch")
search_red_flags(query="southwest border oil company wires for waste oil or hazardous materials")
search_red_flags(query="bulk cash moved by armored car service to Mexico")
get_red_flag(red_flag_id="001_federal_child_nutrition_fraud-01")For a vague query such as "what should I look for in business accounts?", the calling agent should call classify_red_flag_request and ask a brief consultation question covering product/channel, industry, customer profile, geography, and transaction channel or volume when the route is needs_more_context. Skip the classifier when the user already gives specific metadata filters or a concrete scenario.
category is the primary classification of a record. subjects is a broader eligibility/tagging layer for investigative topics, and typology_family is a broader proceeds or typology grouping. For broad investigative topics such as "human trafficking red flags", use subjects instead of raw category so broader typology-family matches are included. For example, a trafficking-relevant darknet crypto flag may have category="virtual_currency" while still matching subjects=["human_trafficking"].
regulator_jurisdiction describes the issuing regulator's jurisdiction. geographic_footprints describes the affected geography or typology geography. For exact metadata requests such as "show medium-risk fraud nexus red flags for depository products" or "red flags from regulators in France", call filter_red_flags instead of ranked search, translating country names to regulator_jurisdiction codes such as FR, SG, AU, GB, US, and EU when the request is about issuing regulators. For broad sector requests such as "trade logistics red flags", use industry_groups; keep raw industry_types for exact sector values such as maritime_shipping. If a filter_red_flags response has truncated=true, continue with next_cursor until truncated=false before presenting an exhaustive answer. search_red_flags is ranked and limit-based; request a higher limit for more ranked results rather than looking for a cursor. For requests with both usable filters and a rich narrative, call search_red_flags with filters so metadata controls eligibility while the query ranks the matching records.
Development
uv sync --extra dev # Install dev dependencies
uv sync --extra optimize # Install DSPy for verifier optimization
uv run pytest tests/ # Run tests
uv run ruff check src/ # Lint
uv run mypy src/ # Type checkAvailable Tools
7 toolsclassify_red_flag_requestA
Classify an AML red flag request before searching when the user asks which red flags apply to a product, customer, geography, industry, scenario, transaction pattern, or institution profile. Returns one route: needs_more_context, metadata_filter, filtered_relevance_search, or direct_relevance_search, plus the recommended next tool and arguments. Use it for ambiguous 'what red flags apply' requests; skip it when the user already gives specific metadata filters or a concrete scenario.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| category | No | ||
| subjects | No | ||
| risk_level | No | ||
| product_types | No | ||
| industry_types | No | ||
| industry_groups | No | ||
| customer_profiles | No | ||
| geographic_footprints | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the full burden. It states the tool returns one route and recommended next tool, but lacks details on side effects, authentication requirements, rate limits, or idempotency. The read-only nature is implied but not confirmed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tightly written sentences: first for purpose, second for usage guidance. No redundant or superfluous information. Highly efficient.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 10 parameters, no schema coverage, and an output schema (not shown), the description provides purpose and usage but omits parameter semantics and behavioral nuances. It is moderately complete for a classification tool but leaves gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%; the description does not explain individual parameters beyond the general notion of query content. The many optional parameters (limit, category, subjects, etc.) are not described, forcing reliance on schema enums alone.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: to classify ambiguous AML red flag requests and return a route with recommended next tool. It distinguishes from siblings by specifying when to use (ambiguous requests) and when to skip (specific filters or concrete scenario).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly provides when-to-use and when-not-to-use guidelines: 'Use it for ambiguous requests; skip it when the user already gives specific metadata filters or a concrete scenario.' This is clear and actionable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
filter_red_flagsA
Return AML red flags for exact metadata criteria without ranked relevance search. Use this for exact metadata requests, broad investigative subjects, and broad industry groups, such as high-risk depository structuring red flags, FINTRAC human trafficking red flags with subjects, trade logistics red flags with industry_groups, or red flags from regulators in France. category is the primary record classification; subjects is a broader eligibility layer that catches cross-category flags; typology_family is a broader proceeds or typology grouping. For example, a human-trafficking-relevant darknet crypto flag can have category="virtual_currency" while matching subjects=["human_trafficking"]. Paginate with next_cursor whenever truncated is true; search_red_flags is ranked and limit-based, with no cursor. For country or jurisdiction requests, translate names to ISO-style regulator_jurisdiction codes before filtering: France -> FR, Singapore -> SG, Australia -> AU, United Kingdom/UK -> GB, United States/US -> US, and European Union/EU regulators -> EU. Prefer filter_red_flags(regulator_jurisdiction="FR") for requests like "red flags from regulators in France." regulator_jurisdiction describes issuer jurisdiction; geographic_footprints describes affected geography or typology geography. Use search_red_flags instead for open-ended relevance questions. Successful responses include table-ready display hints in display and a portable Markdown fallback in markdown_table; clients decide how to render them.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| cursor | No | ||
| detail | No | full | |
| category | No | ||
| subjects | No | ||
| regulator | No | ||
| source_id | No | ||
| risk_level | No | ||
| source_url | No | ||
| issued_after | No | ||
| issued_before | No | ||
| product_types | No | ||
| industry_types | No | ||
| industry_groups | No | ||
| typology_family | No | ||
| customer_profiles | No | ||
| regulatory_source | No | ||
| transaction_patterns | No | ||
| geographic_footprints | No | ||
| regulator_jurisdiction | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Without annotations, the description carries full burden. It explains that the tool performs exact matching, not ranked search; defines how category, subjects, and typology_family interact; provides country code translation rules; and notes pagination. It also mentions response includes display hints and markdown_table. This is thorough behavioral disclosure.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is relatively long but every sentence serves a purpose: purpose, usage, parameter explanations, pagination, country codes, alternative tool. It is well-structured and front-loaded with the core purpose. Slight wordiness in examples could be trimmed but overall effective.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 20 parameters, 0% schema coverage, and no annotations, the description covers the most critical aspects (core parameters, pagination, output format). It assumes an output schema exists, which handles return values. While not exhaustive for every parameter, it provides enough context for correct usage of the tool's main features.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Given 0% schema description coverage, the description must compensate. It explains key parameters (category, subjects, typology_family, regulator_jurisdiction, geographic_footprints) with examples and contrasts. However, many parameters (limit, cursor, detail, regulator, source_id, risk_level, etc.) are not mentioned, leaving gaps. It adds value for the most important ones but is not complete.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Return AML red flags for exact metadata criteria without ranked relevance search,' clearly stating the verb ('Return'), resource ('AML red flags'), and distinguishing it from the sibling tool search_red_flags. It is specific and leaves no ambiguity about what the tool does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly provides usage scenarios ('Use this for exact metadata requests...') and gives concrete examples (e.g., 'high-risk depository structuring red flags'). It also states when to use the alternative tool ('Use search_red_flags instead for open-ended relevance questions') and covers pagination behavior with next_cursor. This is comprehensive guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_red_flagB
Return one AML red flag by id, including source and citation metadata.
| Name | Required | Description | Default |
|---|---|---|---|
| red_flag_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description carries full burden. It mentions returning source and citation metadata but does not disclose authentication needs, rate limits, or error behavior (e.g., missing ID).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Single sentence, highly concise, no wasted words. Front-loaded with the core action.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With output schema present, return values are covered. However, missing guidance on usage, potential errors, and what 'source and citation metadata' entails reduces completeness for a retrieval tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so description must compensate. It only adds 'by id' but provides no format or constraints for the red_flag_id parameter, missing an opportunity to clarify value semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description clearly states it returns a single AML red flag by ID, including source and citation metadata. It distinguishes from sibling tools like search_red_flags and filter_red_flags.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No explicit when to use vs alternatives. The description implies use for fetching one specific flag by ID, but no guidance on not using it for multiple flags or which sibling to use instead.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_sourceA
Return bounded detail for one source by source_id, including citations, aggregate metadata, related red flag IDs, and short snippets. Use get_red_flag when full text for one red flag is needed.
| Name | Required | Description | Default |
|---|---|---|---|
| source_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description carries the burden. Mentions 'bounded detail' but does not explain what 'bounded' means operationally. Does not explicitly state read-only behavior or side effects, though implied by 'return'.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with action and key details. No unnecessary words. Efficient and easy to parse.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given output schema exists, return values need not be fully described. Covers key output elements and sibling distinction. Lacks error handling or precondition notes, but acceptable for a simple retrieval tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema has one parameter (source_id) with 0% coverage. Description references source_id but adds no further semantics (e.g., format, source, or how to obtain valid IDs). Adds minimal value beyond the parameter name.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states the verb 'Return' and resource 'one source by source_id'. Lists included elements (citations, metadata, red flag IDs, snippets) and distinguishes from sibling get_red_flag.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly directs when to use get_red_flag instead for full text, providing a clear alternative. Does not address other siblings or general when-not-to-use scenarios, but sufficient for the simple context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_filtersA
List available filter values for product_types, industry_types, industry_groups, customer_profiles, geographic_footprints, subjects, typology_family, transaction_patterns, category, risk_level, regulator, and regulator_jurisdiction. Agents should call this before or during consultation when they need valid local filter values. category is the primary record classification; subjects is the broad investigative eligibility layer; typology_family is a broader proceeds or typology grouping. regulator_jurisdiction describes issuer jurisdiction; geographic_footprints describes affected geography or typology geography.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the burden. It explains that the tool returns available filter values and adds semantic clarifications for several fields (e.g., category, subjects, typology_family). This provides useful behavioral context beyond a simple listing.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise with three sentences: first states the action, second gives usage guidance, third clarifies key field semantics. No wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has no parameters and an output schema exists, the description adequately explains the purpose and some field meanings. It could be more complete by describing the expected output format or mentioning that the output is a list of valid values.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has no parameters, so the schema coverage is complete. The description adds value by explaining the meaning of the fields that will appear in the output, which compensates for the lack of parameter documentation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it lists available filter values for a specific set of fields. While it distinguishes from sibling tools implicitly (sibling tools focus on red flags and sources), it does not explicitly differentiate itself.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says to call this before or during consultation when valid local filter values are needed. It provides clear context but does not mention when not to use it or suggest alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_sourcesA
List ingested AML red flag source coverage with citation URLs, source counts, aggregate metadata, and red flag IDs. Use when users ask what sources or citations the corpus covers.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, and the description does not disclose any behavioral traits (e.g., side effects, authorization, rate limits). For a read-only list tool, the omission is a minor gap but still reduces transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences with no filler; front-loads the purpose and usage guidance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Has output schema (not shown) and description lists key elements returned. For a zero-parameter list tool with rich output schema, completeness is high.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Zero parameters (schema coverage 100%), so baseline is 4. No additional parameter meaning needed.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states the tool lists AML red flag source coverage with specific outputs (citation URLs, source counts, aggregate metadata, red flag IDs), and differs from sibling tools like get_source (single source) and search_red_flags.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Includes explicit when-to-use instruction: 'Use when users ask what sources or citations the corpus covers.' Does not specify when not to use, but the context is clear given siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_red_flagsA
Search AML red flags using natural-language context and optional filters.
Successful responses include table-ready display hints in display and a portable Markdown fallback in markdown_table; clients decide how to render them.
Agent guidance: use classify_red_flag_request before searching for ambiguous "what red flags apply" requests; skip that extra call when the user already gives specific metadata filters or a concrete scenario. If the user's request is vague, briefly ask for product/channel, industry, customer profile, geography, and transaction channel or volume before searching. If the request already names those details or has a specific scenario, search directly. Call list_filters when you need valid filter values. Use filter_red_flags for exact metadata requests and exhaustive enumeration; use search_red_flags for ranked relevance questions and increase limit for more ranked results because search has no cursor. For broad investigative topics such as human trafficking red flags, use subjects as an eligibility filter. Category is the primary record classification; subjects is a broader eligibility layer that catches cross-category flags; typology_family is a broader proceeds or typology grouping. For example, a human-trafficking-relevant darknet crypto flag can have category="virtual_currency" while matching subjects=["human_trafficking"]. For broad sector requests such as trade logistics red flags, use industry_groups as an eligibility filter. regulator_jurisdiction describes issuer jurisdiction; geographic_footprints describes affected geography or typology geography. For country or jurisdiction requests about issuing regulators, translate names to regulator_jurisdiction codes before filtering, such as France -> FR, Singapore -> SG, Australia -> AU, United Kingdom/UK -> GB, United States/US -> US, and European Union/EU regulators -> EU.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| category | No | ||
| subjects | No | ||
| risk_level | No | ||
| product_types | No | ||
| industry_types | No | ||
| industry_groups | No | ||
| customer_profiles | No | ||
| geographic_footprints | No | ||
| regulator_jurisdiction | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description fully carries the burden. Discloses response format (display hints, markdown table), lack of cursor (increase limit for more results), and explains semantics of key filters like category, subjects, typology_family, regulator_jurisdiction, geographic_footprints.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Relatively long (~300 words) but well-structured with clear sections: purpose, response format, agent guidance. Each sentence adds value; no fluff. Slightly verbose but efficient.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Thorough guidance for a tool with 11 parameters, no schema descriptions, and 7 siblings. Covers usage scenarios, parameter semantics, response details, and even country code mappings. Fully compensates for missing schema descriptions.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, but description adds significant semantics: explains difference between category, subjects, and typology_family; clarifies regulator_jurisdiction vs geographic_footprints; gives examples of country code translations. Does not cover every parameter individually but compensates well.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states the tool searches AML red flags using natural-language context and optional filters. Differentiates from siblings like classify_red_flag_request, filter_red_flags, and list_filters by specifying distinct use cases.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit guidance on when to use this tool vs alternatives: use classify_red_flag_request for ambiguous requests, skip for specific metadata filters; use filter_red_flags for exact metadata; use list_filters for valid values. Also advises on handling vague requests.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
7 tool updates
v0.1.0- First observed
classify_red_flag_request - First observed
filter_red_flags - First observed
get_red_flag - First observed
get_source - First observed
list_filters - First observed
list_sources - First observed
search_red_flags
TDQS
Scored across 7 tools
Each tool has a clearly distinct purpose: classification, exact filtering, single retrieval, source retrieval, listing filters, listing sources, and semantic search. No overlap or ambiguity.
All tool names follow a consistent verb_noun pattern in snake_case (classify_red_flag_request, filter_red_flags, etc.), with predictable verbs like get_, list_, search_.
7 tools is well-scoped for the AML red flag domain, covering classification, filtering, retrieval, and metadata browsing without unnecessary bloat.
The tool set provides a complete read-only surface: routing ambiguous requests, exact filtering, single record lookup, source detail, filter values, source coverage, and full-text search. No obvious gaps.
Maintenance
Related MCP Connectors
The CustomGPT.ai MCP server is a fully managed, RAG-powered endpoint that connects large language models with private knowledge bases and external data sources. It provides tools for retrieval-augmented generation queries (send_message), data ingestion (upload_file), and source listing, enabling AI agents to query private documents like PDFs with high accuracy and real-time citations.
AI governance MCP server for EU AI Act compliance and jurisdiction verification
MCP server for detecting and redacting PII (Personally Identifiable Information) in PDF documents.
MCP server for VC pitch-deck scoring, thesis-fit matching, and deal-flow management.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA production-grade MCP server that automates financial compliance audits by combining LLMs with structured analysis, enabling policy validation, risk profiling, and automated reporting.-
- FlicenseNot gradedqualityDmaintenanceA production-grade MCP server that provides financial ML tools including RAG search, anomaly detection, contract summarization, vendor graph analysis, and model drift monitoring using entirely free, open-source components.1-

yaml-ai-mcpofficial
FlicenseAqualityBmaintenanceAn MCP server for anti-money laundering (AML) compliance, including customer due diligence, transaction monitoring, and SAR filing, compliant with 6AMLD, UK MLR 2017, and FinCEN.4-- FlicenseNot gradedqualityDmaintenanceA Retrieval Augmented Generation MCP server that ingests documents into a local vector database and enables semantic search queries.10-