One Source, Two Surfaces: Anthropic's Financial Services Toolkit
TL;DR
- What it solves: The “last mile” problem - getting an LLM to produce structured, auditable work product (XLSX, DOCX, branded decks) from real financial data, staged for human sign-off.
- Why it matters: Every output is a concrete file, not a chat reply. Nothing posts to a ledger, executes a trade, or approves onboarding without a human decision.
- Best for: Investment bankers, equity researchers, fund admins, and compliance teams who want AI output that fits existing sign-off workflows - not raw API wrappers.
- Main differentiator: One YAML manifest, two runtime surfaces - analyst Cowork plugin or headless Managed Agent behind Temporal/Airflow - from the same source, zero duplication.
- Use case example: Paste an onboarding packet ID; the KYC Screener returns
escalation-<packet>.xlsxwith flagged sanctions hits and missing fields, ready for compliance sign-off.
I talked to a fund administrator who was reconciling a GL against the subledger at 11 p.m. the night before LP distribution. Three Excel windows open. A Slack thread with 47 unread messages. A break list that nobody on the team could fully explain.
She wasn’t using bad software. She was using good software - just a lot of it, manually.
I kept thinking about her when anthropics/financial-services crossed 18,000 GitHub stars in under three months. That kind of traction is not organic. Something landed differently.
The Foundation
The repo is Anthropic’s official reference implementation of Claude for FSI workflows. Ten named agents. Seven vertical skill bundles. Eleven live MCP data connectors covering FactSet, Morningstar, PitchBook, Aiera, Daloopa, Moody’s, and five others. Two partner plugins for LSEG and S&P Global.
But the architectural decision that explains the star count is quieter than all of that.
Every agent is defined in a single agent.yaml manifest. That same file powers a one-click Cowork plugin an analyst installs from the marketplace and a headless Claude Managed Agent a platform team deploys behind its own workflow engine. No duplication. No drift. The system prompt lives in one place and is inlined at deploy time.
This is the task: give financial firms a reference implementation they can run in analyst mode today and promote to production orchestration tomorrow - without rewriting anything.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Real-World Use Cases
Five representative scenarios, then a detailed walkthrough:
| Workflow | Agent | Concrete output |
|---|---|---|
| Sell-side M&A | Pitch Agent | Branded pitch deck with comps, precedents, LBO summary |
| Equity research | Earnings Reviewer | model-<ticker>.xlsx + note-<ticker>.docx |
| Fund administration | GL Reconciler | Break list with root-cause trace, routed for controller sign-off |
| Private equity | Valuation Reviewer | GP package ingested, LP reporting staged |
| Compliance / KYC | KYC Screener | escalation-<packet>.xlsx with flagged gaps and sanctions hits |
Detailed walkthrough - KYC Screener:
Before: a compliance analyst downloads an onboarding packet PDF, manually cross-references a sanctions watchlist, fills a Word template with gaps, escalates via email. Forty minutes per packet on a good day.
After, the analyst fires a single steering event:
Screen onboarding packet OP-2026-04-871
The KYC Screener dispatches three leaf workers in sequence. The doc-reader - the only subagent that ever touches the untrusted document - reads and returns length-capped, schema-validated JSON. It cannot emit arbitrary strings or file paths. The rules-engine receives that structured data and evaluates it against the compliance grid, pulling sanctions and PEP data from the screening MCP. The escalator - which has never seen the original document - writes escalation-OP-2026-04-871.xlsx with every flagged gap labeled, ready for a compliance officer to review and sign off.
The output is not a chat summary. It is a file, in the format the compliance team already uses, staged for a human decision.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());How to Use It
Three deployment paths. Pick your surface.
Surface 1 - Cowork plugin (analyst-facing, no code)
# Register the marketplace and install the core plugin (all 11 MCP connectors live here)
claude plugin marketplace add anthropics/claude-for-financial-services
claude plugin install financial-analysis@claude-for-financial-services
# Add the vertical you need
claude plugin install investment-banking@claude-for-financial-services
claude plugin install equity-research@claude-for-financial-services
# Add specific agents
claude plugin install pitch-agent@claude-for-financial-services
claude plugin install gl-reconciler@claude-for-financial-services
After install, slash commands appear immediately in Cowork dispatch: /financial-analysis:comps, /investment-banking:ic-memo, /equity-research:earnings.
Surface 2 - Claude Managed Agents API (headless, platform-team-facing)
export ANTHROPIC_API_KEY=sk-ant-...
export SCREENING_MCP_URL=https://your-screening-mcp/
# Preview the resolved manifest JSON without touching the API
scripts/deploy-managed-agent.sh kyc-screener --dry-run
# Deploy for real
scripts/deploy-managed-agent.sh kyc-screener
The deploy script parses agent.yaml, inlines system prompts from their source files, uploads each skills directory as a zip to /v1/skills, creates depth-1 leaf workers first, then POSTs the orchestrator to /v1/agents with the anthropic-beta: managed-agents-2026-04-01 header.
Surface 3 - Microsoft 365 add-in
claude plugin install claude-for-msft-365-install@claude-for-financial-services
/claude-for-msft-365-install:setup # interactive wizard
Walks an IT admin through Azure admin consent, manifest XML generation, and per-user MCP routing via Microsoft Graph extension attributes. Works inside Excel, PowerPoint, Word, and Outlook once provisioned.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Configuration & Customization
A few decisions that will save you an afternoon:
-
Install
financial-analysisfirst, always. It is the core plugin and the only thing that loads all 11 MCP connectors. Every vertical plugin and named agent depends on it being present. -
--dry-runbefore you deploy. Thedeploy-managed-agent.shscript prints the fully resolved JSON payload - inlined prompts, expanded skills, subagent IDs - without making a single API call. Run it every time before a real deploy. -
Env var substitution uses a strict allowlist. The deploy script only interpolates values matching
[A-Za-z0-9._/:@-]*. Anything outside that pattern is rejected. This is intentional injection prevention, not a bug to work around. -
Do not hand-edit the bundled skill copies. Skills are authored in
vertical-plugins/<vertical>/skills/and synced toagent-plugins/<slug>/skills/. Thecheck.pylinter blocks commits if any copy has drifted. Usesync-agent-skills.pywhen you make changes. -
MCP connectors require provider credentials. FactSet, Morningstar, PitchBook, Daloopa, Aiera, and the rest all require a paid API key from the provider. The connector endpoints are wired; the credentials are yours to supply via environment variables.
Where It Fits (And Where It Doesn’t)
Who this is built for:
| Role | Primary surface |
|---|---|
| Investment bankers / M&A | Pitch Agent, Meeting Prep Agent, investment-banking vertical |
| Equity researchers | Earnings Reviewer, Market Researcher, equity-research vertical |
| Private equity analysts | Valuation Reviewer, Market Researcher, private-equity vertical |
| Financial modelers | Model Builder, financial-analysis core |
| Wealth managers / RIAs | Meeting Prep Agent, wealth-management vertical |
| Fund administrators | GL Reconciler, Month-End Closer, Statement Auditor, fund-admin vertical |
| KYC / compliance officers | KYC Screener, operations vertical |
| Platform / API teams | All agents via Managed Agents API + orchestrate.py |
| IT admins (M365 tenants) | claude-for-msft-365-install provisioning plugin |
Where it does not fit:
This is not a general-purpose AI wrapper. It is FSI-domain-specific. The agents produce files in workflows that already exist - they do not replace those workflows or the humans who sign off on them.
It also does not replace your execution systems. Nothing in this repo posts to a ledger, executes a trade, or approves an onboarding packet. The outputs are staged, not applied.
The Rough Edges
callable_agents is a preview feature. The multi-agent delegation capability that makes the orchestrator → leaf worker pattern work requires anthropic-beta: managed-agents-2026-04-01. Preview betas change. Build on it, but watch the changelog.
Depth-1 only, by design. Orchestrators can call leaf workers. Leaf workers cannot delegate further. Exactly one level. This is a deliberate constraint - it simplifies the security audit surface and prevents unbounded recursion. It also means you cannot compose agents into deeper pipelines with this reference implementation.
orchestrate.py is a reference script, not a production runtime. The Python event loop that drives cross-agent handoffs uses a regex to extract handoff_request blobs from SSE streams. The docstring says clearly: in production, emit handoffs via a dedicated tool call or typed SSE event to eliminate any risk of echo-parsing attacker-controlled document content as a handoff target. Do not deploy orchestrate.py verbatim.
Human review is not optional, it is the architecture. Every agent produces a staged output file. If your workflow requires an LLM to post accruals directly to the GL without a controller reviewing the break list first, this toolkit is not the right fit - and honestly, neither is any current model.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Getting Started
git clone https://github.com/anthropics/financial-services
cd financial-services
# Install core plugin (carries all 11 MCP connectors)
claude plugin marketplace add anthropics/claude-for-financial-services
claude plugin install financial-analysis@claude-for-financial-services
# Add one vertical and one agent to start
claude plugin install equity-research@claude-for-financial-services
claude plugin install earnings-reviewer@claude-for-financial-services
# Set credentials
export ANTHROPIC_API_KEY=sk-ant-...
export FACTSET_API_KEY=your-key-here # or whichever connector you have access to
# Fire your first session in Cowork
/equity-research:earnings AAPL Q1-2026
You need at least one data connector credential to get real market data. If you have a FactSet key, the mcp.factset.com connector is live the moment you export FACTSET_API_KEY.
FAQ
What is the difference between the Cowork plugin and the Managed Agents API deployment?
Same agent.yaml source, different runtime. The Cowork plugin installs into an analyst’s Claude desktop app. The Managed Agents API deployment runs headless and is triggered via API call, Temporal workflow, or Airflow DAG. Both share identical system prompts because they are inlined from the same file at deploy time.
Does this repo require a Claude Enterprise subscription?
Claude Managed Agents (/v1/agents) requires API access with the beta header. Cowork plugins require a Cowork seat. The 11 MCP data connectors - FactSet, Morningstar, PitchBook, and the rest - each require a separate paid subscription from the provider.
Can these agents post directly to production financial systems? No. Every agent produces a staged output file for human review. None of the 10 agents in this repo post to ledgers, execute trades, or approve onboarding. That constraint is explicit in the architecture, not an oversight.
How does the three-tier security model protect against prompt injection? The Tier 1 Reader - the only subagent that ever touches untrusted documents - returns length-capped, schema-validated JSON and has zero write access and zero connector access. The Tier 3 Write-holder, which produces the output file, never sees the original document. An attacker-controlled document cannot smuggle a malicious instruction through the schema boundary.
What MCP data sources are included and where are they configured?
Eleven connectors: Daloopa, Morningstar, S&P Global/Kensho, FactSet, Moody’s, MT Newswires, Aiera, LSEG, PitchBook, Chronograph, and Egnyte. All defined in the financial-analysis core plugin’s .mcp.json and shared across every agent that uses the core plugin.
Final Thoughts
The fund administrator reconciling that break list at 11 p.m. still has a problem to solve. The repo does not make that problem disappear.
What it does is give her a structured, auditable output from an agent that already spoke to FactSet and Daloopa, traced the root cause, and produced the break list in the format her controller expects - so the conversation starts from a resolved file, not a blank spreadsheet.
18,052 stars in three months says something. Mostly it says that a lot of people in finance have been waiting for someone to build the last-mile infrastructure instead of another chat UI.
The repo is at https://github.com/anthropics/financial-services. Apache-2.0. Run --dry-run first.
Hoang Yell
A software developer and technical storyteller. I spend my time exploring the most interesting open-source repositories on GitHub and presenting them as accessible stories for everyone.