Claude Silverfin AI: MCP Integration for Live Accounting Data
Step-by-step guide to building a self-hosted Silverfin MCP server in Python. Covers Silverfin OAuth 2.0, per-company request queuing, the 4 core tools, and why hosted alternatives like Peliqan exist.
Educational content, not professional advice — AI output and figures here can be wrong. Verify before you rely on it. Full disclaimer →
Just want to connect Claude to Silverfin? If you are not building your own server, start at Connect Claude to Silverfin — the export-based setup with 20 ready accounting tools, no code, about 10 minutes. This page is for developers building a self-hosted Silverfin MCP server for direct read/write access to the live API. For a no-code hosted server, skip to the already-built options at the bottom.
What You're Building
A Silverfin MCP server is a Python process that sits between Claude Desktop (or any MCP client) and Silverfin's REST API. When an accountant types "Pull the trial balance for ABC Ltd for July and run a variance analysis," Claude calls your MCP server's get_trial_balance tool, which authenticates with Silverfin, fetches the data, and returns it — all transparently, without any CSV export.
The server exposes Silverfin data as structured MCP tools. Claude can chain them: first call get_companies to find the right company ID, then get_trial_balance to pull the data, then pass it to ClaudeFinanceLab's budget_variance_analysis tool for commentary generation.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Claude Desktop / Cursor │
│ (MCP client — accountant's laptop) │
└────────────────────────────┬────────────────────────────────────┘
│ MCP protocol (SSE / stdio)
▼
┌─────────────────────────────────────────────────────────────────┐
│ YOUR Silverfin MCP Server │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FastMCP tools: │ │
│ │ get_companies() get_trial_balance(company_id, date) │ │
│ │ get_period_financials() post_remark() │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Per-company asyncio.Semaphore (1 concurrent call max) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ OAuth token store (encrypted, per-firm) │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS REST (OAuth 2.0 Bearer token)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Silverfin API v4 │
│ live.getsilverfin.com/v4/f/{firm_id}/... │
│ Rate limit: 1 concurrent request per company-ID │
└─────────────────────────────────────────────────────────────────┘
Step 1: Get Silverfin API Credentials
Silverfin does not have a public self-service developer portal. You must email [email protected] with:
- Your firm name and Silverfin account type
- Intended use case (e.g. "AI agent for automated working paper analysis")
- Your redirect URI for OAuth (e.g.
http://localhost:8765/callbackfor local dev)
Silverfin will provide a client_id and client_secret. This typically takes 2–5 business days. Store these as environment variables — never hardcode them.
LLM-friendly docs: Silverfin maintains an LLM-optimised index of their entire API at https://developer.silverfin.com/llms.txt. Load this into Claude to get accurate endpoint details for your implementation.
Step 2: Implement the OAuth 2.0 Flow
Silverfin uses standard OAuth 2.0 authorization code flow. All API endpoints are at https://live.getsilverfin.com/v4/f/<firm_id>/.
import os, httpx, json, asyncio
from pathlib import Path
SILVERFIN_BASE = "https://live.getsilverfin.com"
CLIENT_ID = os.environ["SILVERFIN_CLIENT_ID"]
CLIENT_SECRET = os.environ["SILVERFIN_CLIENT_SECRET"]
REDIRECT_URI = os.environ["SILVERFIN_REDIRECT_URI"]
TOKEN_FILE = Path(".silverfin_tokens.json") # encrypt in production
def get_auth_url(firm_id: str) -> str:
"""Generate the OAuth authorization URL."""
return (
f"{SILVERFIN_BASE}/oauth/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=financials:read+administration:read+communication:write"
f"&response_type=code"
f"&state={firm_id}"
)
async def exchange_code(code: str) -> dict:
"""Exchange authorization code for access + refresh tokens."""
async with httpx.AsyncClient() as client:
resp = await client.post(f"{SILVERFIN_BASE}/oauth/token", data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code",
})
resp.raise_for_status()
tokens = resp.json()
save_tokens(tokens)
return tokens
async def refresh_tokens(refresh_token: str) -> dict:
"""Refresh an expired access token."""
async with httpx.AsyncClient() as client:
resp = await client.post(f"{SILVERFIN_BASE}/oauth/token", data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"refresh_token": refresh_token,
"redirect_uri": REDIRECT_URI,
"grant_type": "refresh_token",
})
resp.raise_for_status()
tokens = resp.json()
save_tokens(tokens)
return tokens
def save_tokens(tokens: dict) -> None:
TOKEN_FILE.write_text(json.dumps(tokens))
def load_tokens() -> dict | None:
if TOKEN_FILE.exists():
return json.loads(TOKEN_FILE.read_text())
return None
Production token storage: Never store OAuth tokens in plaintext files. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) or encrypt the token file with a KMS-managed key. The code above is for local development only.
Step 3: The Per-Company Rate Limit Queue
This is the most critical implementation detail. Silverfin enforces a maximum of 1 concurrent API call per company-ID. Claude will often try to make parallel tool calls. Without a queue, you'll get HTTP 429 errors and cascading failures.
The solution: an asyncio.Semaphore per company, stored in a dict. Every API call acquires the semaphore before hitting Silverfin and releases it on completion.
import asyncio
from collections import defaultdict
# One semaphore per company_id — enforces 1 concurrent call
_company_locks: dict[str, asyncio.Semaphore] = defaultdict(
lambda: asyncio.Semaphore(1)
)
async def silverfin_get(firm_id: str, company_id: str, path: str) -> dict:
"""Make a rate-limited GET request to the Silverfin API."""
tokens = load_tokens()
if not tokens:
raise RuntimeError("Not authenticated. Run the OAuth flow first.")
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
url = f"{SILVERFIN_BASE}/v4/f/{firm_id}/{path}"
# Acquire the per-company lock before calling Silverfin
async with _company_locks[company_id]:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.get(url, headers=headers)
# Token expired — refresh and retry once
if resp.status_code == 401 and "refresh_token" in tokens:
tokens = await refresh_tokens(tokens["refresh_token"])
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
resp = await client.get(url, headers=headers)
resp.raise_for_status()
return resp.json()
Step 4: The MCP Tools
Install FastMCP (pip install fastmcp httpx) and define your tools. Start with these four — they cover 90% of accounting firm workflows.
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("SilverfinMCPServer")
FIRM_ID = os.environ["SILVERFIN_FIRM_ID"]
@mcp.tool()
async def get_companies() -> str:
"""
List all companies (clients) in the Silverfin firm.
Returns company IDs, names, and their periods.
Use this to find the company_id before calling other tools.
"""
data = await silverfin_get(FIRM_ID, "firm", "companies")
companies = data.get("companies", [])
if not companies:
return "No companies found."
lines = ["COMPANIES IN SILVERFIN FIRM", "-" * 50]
for c in companies:
lines.append(f" ID: {c['id']} | Name: {c['name']}")
return "\n".join(lines)
@mcp.tool()
async def get_trial_balance(company_id: str, period_end_date: str) -> str:
"""
Fetch the trial balance for a company at a given period-end date.
Args:
company_id: Silverfin company ID (get from get_companies)
period_end_date: Period end date in YYYY-MM-DD format (e.g. "2026-07-31")
"""
path = f"companies/{company_id}/periods/{period_end_date}/trial_balance"
data = await silverfin_get(FIRM_ID, company_id, path)
accounts = data.get("accounts", [])
if not accounts:
return f"No trial balance data found for company {company_id} at {period_end_date}."
lines = [
f"TRIAL BALANCE — Company {company_id} — {period_end_date}",
"-" * 70,
f"{'Account Code':<14} {'Account Name':<35} {'Balance':>12}",
"-" * 70,
]
total = 0.0
for acct in accounts:
balance = float(acct.get("balance", 0))
total += balance
lines.append(
f"{str(acct.get('number','')):<14} "
f"{str(acct.get('name','')):<35} "
f"{balance:>12,.2f}"
)
lines += ["-" * 70, f"{'TOTAL':<51} {total:>12,.2f}"]
return "\n".join(lines)
@mcp.tool()
async def get_period_financials(
company_id: str,
period_end_date: str,
statement: str = "income_statement",
) -> str:
"""
Fetch P&L or balance sheet for a company from Silverfin.
Args:
company_id: Silverfin company ID
period_end_date: Period end date in YYYY-MM-DD format
statement: "income_statement" | "balance_sheet"
"""
path = f"companies/{company_id}/periods/{period_end_date}/{statement}"
data = await silverfin_get(FIRM_ID, company_id, path)
lines_data = data.get("line_items", data.get("accounts", []))
if not lines_data:
return f"No {statement} data found."
lines = [
f"{statement.upper().replace('_',' ')} — {company_id} — {period_end_date}",
"-" * 65,
]
for item in lines_data:
name = str(item.get("name", item.get("description", "")))
value = float(item.get("value", item.get("balance", 0)))
lines.append(f" {name:<45} {value:>14,.2f}")
return "\n".join(lines)
@mcp.tool()
async def post_working_paper_remark(
company_id: str,
period_end_date: str,
reconciliation_id: str,
remark_text: str,
) -> str:
"""
Post a remark (comment) to a Silverfin working paper reconciliation.
Use this to write AI-generated commentary directly into Silverfin.
Args:
company_id: Silverfin company ID
period_end_date: Period end in YYYY-MM-DD format
reconciliation_id: ID of the reconciliation/working paper
remark_text: The remark to post (e.g. AI-generated variance commentary)
"""
tokens = load_tokens()
if not tokens:
raise RuntimeError("Not authenticated.")
headers = {
"Authorization": f"Bearer {tokens['access_token']}",
"Content-Type": "application/json",
}
url = (
f"{SILVERFIN_BASE}/v4/f/{FIRM_ID}/companies/{company_id}"
f"/periods/{period_end_date}/reconciliations/{reconciliation_id}/remarks"
)
payload = {"remark": {"description": remark_text}}
async with _company_locks[company_id]:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(url, headers=headers, json=payload)
resp.raise_for_status()
return f"Remark posted successfully to reconciliation {reconciliation_id}."
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8766)
Step 5: Add to Claude Desktop Config
Once running locally (or deployed to a server), add it to claude_desktop_config.json:
{
"mcpServers": {
"silverfin": {
"url": "http://localhost:8766/sse",
"headers": {}
}
}
}
For a remote deployment, replace with your server's URL and add any auth headers your deployment requires. The MCP spec (2025) supports OAuth 2.1 for remote servers — use this for production.
Step 6: Complete Workflow Example
Once connected, Claude can chain tools to complete complex workflows:
Example prompt: "Get the list of companies, then pull the July P&L for ABC Ltd, and use the variance analysis tool to generate management accounts commentary, then post the commentary as a remark on their month-end working paper."
Claude will automatically chain: get_companies → get_period_financials → budget_variance_analysis (from ClaudeFinanceLab's Accounting MCP) → post_working_paper_remark. The accountant sees the final commentary appear in Silverfin's working paper interface.
Security Considerations
Never expose Silverfin credentials to Claude
The MCP server must be the only component that holds client_id, client_secret, and access tokens. Claude (the LLM) never sees them — it only calls your tool functions, which handle auth internally.
The Confused Deputy problem
If you expose this server to multiple users, implement authorization at the MCP layer: verify that the requesting user is actually authorized to access the requested company's data before calling Silverfin. Without this, user A could call get_trial_balance(company_id="B_company_id") and see another firm's client data.
GDPR and professional obligations
Accounting client data is regulated. Ensure your server:
- Does not log Silverfin response bodies containing client financial data
- Does not store or cache financial data beyond the request lifecycle
- Runs within your firm's data residency boundary (EU servers for EU firms)
- Is covered by your firm's Data Processing Agreement obligations
Token storage in production
Use a secrets manager (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault). Never write OAuth tokens to disk in plaintext. Rotate refresh tokens on each use.
Already-Built Options (No Code Required)
If you don't want to build and maintain your own MCP server, two services already provide hosted Silverfin MCP access:
| Service | What it does | Setup | Cost |
|---|---|---|---|
| Peliqan | Hosted Silverfin MCP with live data — trial balances, working papers, reconciliations, write-back. SOC 2 Type II. Runs a data warehouse layer. Can query across entire client portfolio. | Create Peliqan account, connect Silverfin via OAuth, add endpoint to Claude. ~5 min. | Paid (see their site) |
| Zapier MCP | Silverfin actions via Zapier's MCP gateway. Covers the Zapier-supported Silverfin actions (companies, accounts, budgets, comments). | Add Silverfin to Zapier, enable MCP, add endpoint to Claude. | Zapier subscription required |
Why ClaudeFinanceLab doesn't host a Silverfin MCP server
Three reasons make this impractical for a multi-tenant hosted service:
1. Firm-specific OAuth credentials. Silverfin issues client_id credentials per firm, not per platform. We cannot obtain a single credential set that covers all accounting firms — each firm must register its own OAuth application with Silverfin.
2. Professional and regulatory obligations. Accounting client data (client financials, trial balances, working papers) is subject to GDPR, professional confidentiality obligations under ISQC1/ISQM1, and in some jurisdictions auditor independence requirements. A third-party platform acting as an intermediary for this data creates compliance risk that individual accounting firms cannot outsource.
3. Reliability at scale. Silverfin's 1-concurrent-request-per-company limit means a multi-tenant proxy serving dozens of accounting firms simultaneously would require complex per-firm-per-company queuing infrastructure. Peliqan has invested in this infrastructure (SOC 2 certified, data warehouse layer). We have not.
What ClaudeFinanceLab does provide: 20 accounting computation tools (variance analysis, lease amortization, VAT compliance, intercompany elimination, etc.) that work with data from any source — including data fetched via a self-hosted or Peliqan-hosted Silverfin MCP. See the Silverfin integration page for details.
API Scopes Reference
| Scope | What it allows | When to request |
|---|---|---|
administration:read | List companies, periods, firm structure | Always (needed for get_companies) |
financials:read | Trial balances, P&L, balance sheet, account data | Always (core data) |
financials:transactions:read | Individual journal entries and bookkeeping transactions | When auditing transaction-level data |
communication:read | Read existing remarks and comments on working papers | When reviewing existing commentary |
communication:write | Post remarks/comments to working papers | When writing AI commentary back to Silverfin |
workflows:read | Read working paper status and workflow state | When checking completion status |
permanent_documents:read | Access permanent file documents | Rarely needed for standard workflows |
Next Steps
- Email [email protected] to request OAuth credentials
- Read the full Silverfin API docs at developer.silverfin.com
- Load developer.silverfin.com/llms.txt into Claude for LLM-optimized endpoint reference
- Add ClaudeFinanceLab's Accounting MCP alongside your Silverfin MCP — the two complement each other: Silverfin MCP fetches live data, ClaudeFinanceLab does the financial computation
- Consider Peliqan if you want a no-code hosted solution with SOC 2 compliance
Connect Claude to live financial data via MCP — EDGAR, FDIC, BIS, CME and 18 more.
New guides & tools — free
Get notified when we add new MCP servers, finance AI guides, and eval results.