Add an MCP Resource
Expose authoritative documents the model can reference — passive context, not invocation.
You've completed the main MCP build tutorial and have the compliance-toolkit server working in Claude Desktop. This guide extends that same project.
Tools vs Resources — the key distinction
Tools are actions the model invokes. Resources are references the model consults. The mental shift:
| Tools | Resources |
|---|---|
| Verb-shaped (do something) | Noun-shaped (here is data) |
| Model decides to call | Host surfaces to model |
| Has input schema | Has URI |
| Returns computed result | Returns stored content |
screen_sanctions(name) |
compliance://policies/kyc-tier-2 |
Resources give you a clean way to surface authoritative documents — policies, regulations, internal SOPs — that the model should cite, not paraphrase from training. The audit story is: every claim grounded in a known resource URI. That's exactly what regulators want.
1Add resource handlers~10 min
Open your server.py from the main tutorial. Add this block after the existing tool definitions but before if __name__ == "__main__"::
# ---------- Resources: authoritative documents ----------
POLICY_DOCS = {
"kyc-tier-2": {
"title": "KYC Tier 2 — Enhanced Due Diligence Requirements",
"effective_date": "2025-09-01",
"content": """\
KYC Tier 2 — Enhanced Due Diligence (EDD)
SCOPE: Customers classified Tier 2 require enhanced due diligence at onboarding and
on a 12-month review cadence. Triggers include cumulative volume > $50,000/year,
exposure to medium-risk jurisdictions, or politically-exposed-person (PEP) status.
REQUIRED EVIDENCE:
1. Government-issued ID verified against authoritative source
2. Proof of address dated within 90 days
3. Source-of-wealth documentation (employment letter, business registration, or tax return)
4. Sanctions screening against OFAC SDN, EU Consolidated, UN, and HMT lists
5. Adverse media screening covering the last 5 years
REVIEW CADENCE: Annual; immediate re-review on any material change in profile.
DOCUMENTATION RETENTION: 7 years from account closure per BSA 31 CFR 1010.430.""",
},
"sar-narrative-template": {
"title": "SAR Narrative Template — Internal SOP",
"effective_date": "2024-11-15",
"content": """\
SAR Narrative Template — Internal Standard Operating Procedure
The narrative section of a Suspicious Activity Report must answer five questions
concisely. The recommended order:
1. WHO — Subject(s) of the report. Full legal name, customer ID, role.
2. WHAT — Suspicious activity. Type, channels, amounts, time window.
3. WHERE — Jurisdictions involved. Counterparties' locations.
4. WHEN — Date range. Frequency. Velocity changes.
5. WHY — What makes this suspicious. Reference policy or red-flag indicator.
TONE: Factual. No speculation. No conclusory language ("Customer is a money
launderer" — NO; "Activity is inconsistent with stated source of wealth" — YES).
LENGTH: Aim for 5-15 sentences. Be specific, not comprehensive.""",
},
"high-risk-jurisdictions": {
"title": "High-Risk Jurisdictions — Current List",
"effective_date": "2026-04-01",
"content": """\
High-Risk Jurisdictions (effective 2026-04-01)
PROHIBITED (Comprehensive Sanctions):
- Iran (IR), North Korea (KP), Cuba (CU), Syria (SY)
- Russia (RU) — sectoral sanctions per OFAC EO 14024
HIGH RISK (Enhanced Monitoring):
- FATF black list: as published current cycle
- FATF grey list: as published current cycle
- Countries with weak AML/CFT regimes per internal risk assessment
This list is reviewed quarterly. The authoritative source is the Compliance
Policy Manual section 4.2. Refer all edge cases to the compliance lead.""",
},
}
@mcp.resource("compliance://policies/{doc_id}")
def get_policy_document(doc_id: str) -> str:
"""Return the full text of an internal compliance policy document.
Use these as authoritative references when drafting case narratives, EDD reports,
or regulatory responses. Cite the URI and effective_date in any output that
relies on the document.
"""
if doc_id not in POLICY_DOCS:
return f"# Policy not found\n\nNo policy document with ID '{doc_id}' is registered."
doc = POLICY_DOCS[doc_id]
return (
f"# {doc['title']}\n"
f"Effective: {doc['effective_date']}\n"
f"Source URI: compliance://policies/{doc_id}\n\n"
f"{doc['content']}"
)
@mcp.resource("compliance://policies")
def list_policies() -> str:
"""Return a directory of all available compliance policy documents."""
lines = ["# Available Compliance Policies\n"]
for doc_id, doc in POLICY_DOCS.items():
lines.append(f"- **compliance://policies/{doc_id}**: {doc['title']} (effective {doc['effective_date']})")
return "\n".join(lines)
1. @mcp.resource(uri_template) registers a resource the client can fetch. The path parameter {doc_id} makes it a templated resource — one handler serves many URIs.
2. Resources return text (or bytes). No JSON schema, no input args beyond the URI itself. Simpler contract than tools.
3. Effective dates baked into the content. The model now has versioning context for free — it can say "per the policy effective 2025-09-01" without you wiring extra metadata.
2Test with the inspector~5 min
Restart the inspector to pick up the changes:
mcp dev server.py
In the inspector UI, switch from the Tools tab to the Resources tab. You should see:
compliance://policies— the directory listing- The templated URI
compliance://policies/{doc_id}
Click compliance://policies first, then try fetching compliance://policies/kyc-tier-2. You'll get back the markdown content. This is what the model will see.
By exposing both a directory listing (compliance://policies) and individual documents (compliance://policies/{doc_id}), the model can discover what's available before requesting specific items. It's the MCP equivalent of ls + cat.
3Try it in Claude~5 min
Restart Claude Desktop (fully quit, relaunch). Open a new chat. Look for the attachment icon — Claude Desktop surfaces MCP resources there as attachable context.
Attach kyc-tier-2 and try prompts like:
Using the KYC Tier 2 policy I just attached, walk me through what
evidence I need to collect for a new customer with $80K projected annual
volume from the UAE.
I'm drafting a SAR narrative. Attach the SAR template and the high-risk
jurisdictions list, then help me structure a narrative for a customer
in Russia with unexplained $50K wire activity over 30 days.
Notice how Claude treats the resource differently from a tool. It doesn't call the resource — the resource content is in the context. Claude cites from it: "Per the KYC Tier 2 policy effective 2025-09-01..." That citation pattern is exactly what compliance documentation needs.
4Make a resource dynamic~5 min
So far our resources are hardcoded. Let's make one dynamic — pulling from a "case database" (simulated). Add this:
from datetime import datetime
# Simulated case database
CASES = {
"CASE-2026-00012": {
"subject": "Vladimir Petrov",
"opened": "2026-04-22",
"alerts": ["ALT-2026-00042", "ALT-2026-00045"],
"transactions": [
{"date": "2026-04-18", "amount_usd": 12500, "counterparty": "Atlas Crypto Exchange", "type": "wire"},
{"date": "2026-04-20", "amount_usd": 9800, "counterparty": "Atlas Crypto Exchange", "type": "wire"},
{"date": "2026-04-21", "amount_usd": 25000, "counterparty": "Global Shadow Holdings", "type": "wire"},
],
"status": "under-investigation",
},
}
@mcp.resource("compliance://cases/{case_id}")
def get_case_file(case_id: str) -> str:
"""Return the full case file for an investigation, including subject, alerts,
transactions, and current status. Use this for drafting case narratives.
"""
if case_id not in CASES:
return f"# Case not found\n\nNo case with ID '{case_id}'."
c = CASES[case_id]
lines = [
f"# Case File: {case_id}",
f"Subject: {c['subject']}",
f"Opened: {c['opened']}",
f"Status: {c['status']}",
f"Snapshot: {datetime.utcnow().isoformat()}Z",
"",
"## Related Alerts",
*[f"- {a}" for a in c['alerts']],
"",
"## Transactions",
]
for t in c['transactions']:
lines.append(f"- {t['date']}: ${t['amount_usd']:,} {t['type']} ↔ {t['counterparty']}")
return "\n".join(lines)
Restart Claude Desktop. Attach compliance://cases/CASE-2026-00012 alongside the SAR template, then ask:
Using the attached case file and SAR template, draft a 10-sentence SAR narrative. Cite specific transactions and reference the policy URIs you used.
Watch Claude produce a draft with explicit citations to both the SAR template URI and the case-file URI. That's audit-ready output.
What you can now say in the interview
"I extended my compliance MCP server with resources — read-only content addressed by URI. The pattern that matters: tools are actions, resources are references. For compliance, resources are how you give the model authoritative documents — policies, SAR templates, case files — and force it to cite a URI rather than paraphrase from training data. I exposed both a directory resource and templated per-document resources so the model can discover what's available before requesting specific items, and made the case-file resource dynamic so it serves the current state of an investigation. The audit story falls out for free: every claim in an AI-drafted narrative cites a URI with an effective date, and the regulator can resolve any URI back to the exact content the model saw."