Stretch Goal · 25 min

Add an MCP Resource

Expose authoritative documents the model can reference — passive context, not invocation.

⏱ 25 minutes 📂 Read-only resources 🔗 Built on the main tutorial
Prerequisite

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:

ToolsResources
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
Why this matters for compliance

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__"::

server.py (additions)
# ---------- 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 (internal policy; the BSA minimum is 5 years under 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)
What changed

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:

shell
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.

The "resources discovery" pattern

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:

example prompts
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.
Compare to the tool experience

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:

server.py
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

Sample answer — ~60 seconds

"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."

✓ Check yourself

Answer from memory before opening each one.

  1. After Step 1, which two resource entries should the inspector's Resources tab show, and how do you use them to discover and then read the KYC Tier 2 policy?

    Show answer

    The directory resource at compliance://policies and the templated resource compliance://policies/{doc_id}. Read the directory to see the available ids, then fetch compliance://policies/kyc-tier-2. You get markdown with the title, effective date and source URI, which is what the model will see. See test with the inspector.

  2. Why does the guide put the effective date and source URI inside the text each resource returns, and what does that give an auditor?

    Show answer

    The model can cite the policy with its version, such as "per the policy effective 2025-09-01", and a regulator can resolve the URI to the exact content the model saw. The versioning travels with the content, with no extra metadata plumbing. See add resource handlers.

  3. In Claude Desktop, how does a resource reach the model, and how does that differ from a tool call?

    Show answer

    The host surfaces resources as attachable context, so the content is in the conversation from the start and the model cites from it. A tool is chosen by the model at run time, with arguments, and returns a computed result. See try it in Claude.

  4. You must expose each customer's latest KYC review as a resource. What does the case-file example show you to copy, and what must you add that the policy demo skips?

    Show answer

    Copy the dynamic handler that builds the content on request and stamps it with a snapshot time, so the citation says when the data was read. Add per-user authorization at fetch time, because a customer review is sensitive in a way the shared policy documents are not, and log every read. See make a resource dynamic.

  5. The policy dictionary is hardcoded for the demo. When it moves to production, what should stay the same and what should change?

    Show answer

    Keep the URI scheme, the directory resource and the effective date in the returned text. Change the source: read from the system of record so the date and wording come from there, and keep superseded versions retrievable so a past decision can be traced to the text in force at the time. See make a resource dynamic.

Takeaway

  • Tools are verbs the model invokes, while resources are nouns the host puts in context, each identified by a URI.
  • Resources let the model cite authoritative documents instead of recalling regulations from training data.
  • A templated resource lets one handler serve many documents, and a directory resource lets the model discover what exists.
  • Putting the effective date and source URI in the content gives every citation a version.
  • A dynamic resource serves current state, so stamp it with a snapshot time and check the caller's access.

Resources give the model reference material; prompts package a whole workflow around it. → Add an MCP Prompt Template

Further reading

  • Resources — Model Context Protocol, 2025-06-18 revision. URIs, templates, listing and subscriptions as the spec defines them.
  • MCP Python SDK — Model Context Protocol, accessed 2026. The resource decorator and its URI template rules.
  • Citations — Anthropic docs, accessed 2026. How Claude can point to the exact passage a claim came from.
  • 31 CFR 1010.430, Nature of records and retention period — Legal Information Institute, accessed 2026. The five-year retention rule behind the chapter's policy example.