Addepar MCP Server
Click on "Install 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., "@Addepar MCP ServerShow the total ownership exposure for the Dahl family as of last quarter."
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.
Addepar MCP Server
Read-only MCP server exposing Addepar portfolio and ownership data to Claude.
Built for use in financial reporting at a registered investment advisor. The governing principles, in priority order, are reliability, accuracy, security, then convenience.
What this server guarantees
It does not guarantee that any number is correct in the real world. That is not something any tool can honestly promise, because Addepar itself carries stale marks: a query run "as of today" routinely returns a value marked weeks earlier, because private funds mark quarterly.
What it guarantees is total honesty about provenance:
It never invents a number.
It never silently drops data.
It always states what it does not know.
There are deliberately no confidence scores. A figure labelled "94% confident"
is false precision, and false precision is worse than useless in a compliance
context. Instead every response carries a structured caveats array which is
empty when the result is clean, so its emptiness is an affirmative statement
rather than an absence of checking.
The null rule
A null value and a 0.0 value are different facts and are never merged.
Confirmed in live data, two positions in the same household:
Position | Value | Meaning |
Leslie A Dahl, WRD Capital |
| Addepar computed a value, and it is zero |
W Robert Dahl, Goldman Sachs -400P |
| No value computed, reason unstated |
Coercing that null to zero and summing produces a total that is confidently wrong
and looks entirely plausible. Nulls are therefore excluded from sums, counted,
and named in NULL_VALUES_EXCLUDED.
Caveat codes
Code | Raised when |
| A position was marked more than 35 days before the requested date |
| One or more positions returned no computed value |
| A lookup found more than one plausible candidate |
| Name matching was used, which is non-exhaustive by nature |
| Traversal stopped early, suggesting circular nesting |
| A total combines values marked on different dates |
| More rows exist than were returned; totals still cover all |
| No confirmed UI link pattern exists for this object type |
Tools
Tool | Question it answers |
| Turn a name into a specific Addepar ID and object type |
| Total exposure, targeted holdings, or beneficial ownership |
| Exposure across a family of related funds |
| How one client's holdings are classified |
| Which saved reports exist |
| Run one of the firm's own saved reports |
| Committed, called, and unfunded capital |
All seven declare read_only_hint=True, so a trusted client can skip
confirmation prompts. The real guarantee is structural: see below.
Keep the tool count disciplined. Tool definitions load into the model's context on every request, so each one costs tokens whether used or not, and a bloated surface measurably degrades tool selection. Prefer adding a parameter to an existing tool over adding a near-duplicate.
Architecture
src/addepar_mcp/
config.py Settings from environment. No secrets in code.
errors.py Three failure classes. Extends the SDK ToolError.
models.py The response contract. Caveats, provenance, disclosure.
client.py Read-only HTTP client. Cannot construct a mutating request.
tree.py Traversal and null-safe arithmetic. No network dependency.
citations.py UI links, only for confirmed URL patterns.
audit.py Structured JSON Lines compliance record.
auth.py Per-request identity extraction. Entra ready.
runtime.py Shared runtime container.
server.py Entrypoint, transports, identity middleware.
tools/ One module per tool, each exposing register(mcp).Adding a tool means adding a module and one line in tools/__init__.py.
Read-only, enforced in code
The client exposes only get and query, where query is a POST restricted to
a fixed allowlist of read-only query endpoints. There is no code path that can
issue PATCH, PUT, or DELETE, and no way to POST to an arbitrary path. Attempting
one raises ReadOnlyViolationError.
This is deliberate rather than decorative. No v1 use case writes, and a bug that mutated a client's ownership structure would not be fully recoverable.
Fail closed
On a timeout or a rate limit hit mid-operation, tools return an error and no data. They never return a partial tree or a smaller total, because a truncated ownership total is indistinguishable from a correct one at a glance.
Setup
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
cp .env.example .env # then fill in credentials
.venv/bin/python -m pytest tests/ -qRun locally over stdio:
TRANSPORT=stdio .venv/bin/python -m addepar_mcp.serverRun over HTTP:
TRANSPORT=http HOST=0.0.0.0 PORT=8080 .venv/bin/python -m addepar_mcp.serverHealth check at GET /healthz. MCP endpoint at /mcp.
Deployment and authentication
Deploy remotely over HTTPS so there is one instance to operate rather than one per workstation.
Identity, and why it matters
Two separate identity layers, and conflating them causes confusion later:
Caller identity: who invoked the tool. Extracted per request, written to every audit record.
Upstream identity: what Addepar's own log shows, which is the single service credential regardless of who asked.
This means the server-side audit log is the authoritative answer to "who looked at client data." Addepar's log will not corroborate it at the user level.
Two supported modes:
Mode | Per-user attribution |
Shared organization credential ( | No. An admin enters one credential; every user's request carries it and all users are indistinguishable. |
Per-user OAuth | Yes. Each user consents individually, so the request identifies them. |
If compliance needs to answer "who asked for what," OAuth is not optional. It is the only configuration that produces the answer.
For this deployment the natural authorization server is the firm's Entra ID tenant, since they already run Microsoft 365. That ties access to real corporate accounts and makes it governable through the firm's own SSO rather than a credential held by an outside party.
Set REQUIRE_AUTH=true once OAuth is configured. Until then the server records
calls as unattributed, which is honest but does not satisfy a per-user audit
requirement.
Deployment gotchas worth knowing in advance
Redirect URI for hosted Claude surfaces is
https://claude.ai/api/mcp/auth_callback.Anthropic egress originates from
160.79.104.0/21. Both this server and the authorization server's discovery endpoints must be reachable from that range. A firewall in front of the identity provider breaks the flow even when the MCP server itself is reachable.With Entra ID, the MCP server URL must also be registered as an Application ID URI on the app registration, or the token request fails with
AADSTS9010010.Claude allows roughly 10 seconds for discovery and token endpoints, and 30 seconds for refresh. Slow endpoints appear as intermittent connection failures rather than clean errors.
Signature verification is not implemented
auth.py decodes JWT claims for audit purposes but does not verify
signatures. That is acceptable on a trusted network and is not acceptable
once the server is reachable by untrusted callers. Replace it with real JWKS
verification (fetch tenant keys, verify signature, check issuer, audience, and
expiry) before exposing it. Identity from an unverified token is a claim, not a
fact. This was left visibly incomplete rather than stubbed to look finished.
Audit logging
Structured JSON Lines, one object per tool call, written to AUDIT_LOG_PATH.
Each record carries timestamp, tool, caller identity, arguments, outcome,
duration, Addepar request IDs, entities touched, row count, caveat codes, and
any error.
The log must live server-side. A conversation transcript is not a durable record: it is user-deletable, and on some surfaces not archivable at all. If the only trace of a data access lives in a chat window, it does not exist for compliance purposes.
Raise this in the compliance conversation: these records contain entity names, dollar amounts, and access patterns. The log therefore sits inside the same compliance perimeter as the underlying client data, with the same retention and access questions attached. Decide early whether it writes to its own store or emits into the existing archiving pipeline, because two stores of the same client data doubles the compliance surface.
Citations
Rule: emit a link only when both the object type and the ID namespace are confirmed. Otherwise emit the reproducible query. A confidently wrong citation is worse than none, because it looks authoritative and sends someone to the wrong place.
Object | Pattern | Status |
Entity detail |
| Confirmed |
View on entity |
| Confirmed |
View on group |
| Inferred, not emitted |
Position detail | unknown, may not exist | Not emitted |
Computed aggregates such as total exposure do not exist as Addepar objects and have no native URL. They are cited by deep linking a saved view rooted at the same portfolio, which lands the user in a report the firm built and already trusts.
These patterns cannot be validated programmatically. The Addepar web app is a single page application that returns HTTP 200 for every path, including deliberate nonsense routes, so curl cannot distinguish a valid route from an invalid one. Any new pattern must be confirmed by a human copying a real URL out of the live UI.
Validated behaviour
Verified against the live tenant on 2026-08-26. These are regression fixtures in
tests/test_regression_fixtures.py. If a refactor changes any of them, the
refactor is wrong until proven otherwise.
Assertion | Value |
Dahl household total |
|
Loon Point Holdings II LLC, targeted, 4 occurrences summed |
|
Pacific Lake family, 6 matches of 4,121 scanned |
|
Charlotte's share of the shared LLC |
|
Real maximum ownership depth |
|
Households at the firm |
|
Two cross-checks make these trustworthy rather than merely recorded:
The household total is identical whether reached by grouping on
ownership(nested legal hierarchy) or onsecurity(flat holdings). Two entirely different query shapes, same number to the cent.A shared LLC's own total equals the sum of the four sibling trusts' shares of it, reached from opposite traversal directions, with no double counting.
Addepar API notes
Hard-won behaviour, recorded so it is not rediscovered painfully.
Every endpoint requires the
Addepar-Firmheader, including/v1/users/me.filter[name]on/v1/entitiesis silently ignored. It is not a real filter: it returns arbitrary entities rather than name matches, which is worse than an error because it looks like it worked.filter[entity_types]is real and works.Unfiltered
GET /v1/entitiesreturns a 400 ("cache is not responsible for firm 2142"). Adding any filter parameter avoids it. Addepar-side bug, worked around rather than reported.Name search works on
POST /v1/groups/queryviadisplay_names. That is the only working name search in the API.ownershipgrouping walks legal entities only. It stops at holding-account style leaves and does not descend into the securities inside them. Usesecuritygrouping for actual investment lines.Discrete filters are exact-match only. No prefix or substring matching, so a partial name returns zero rows rather than a fuzzy match.
Errors are clean and specific, for example
"Invalid grouping attribute: nonsense_grouping". They are relayed verbatim.Latency is variable. The household rollup normally completes in about 3 seconds. One run took 47.8 seconds against Addepar's 60 second ceiling. The client timeout is set to 55 seconds so a clean error surfaces rather than the connection being cut mid-response.
Rate limits are firm-wide, 50 requests per 15 minutes and 1,000 per 24 hours, shared with every other integration at the firm. A limit can therefore trigger because of activity unrelated to this server.
Same-name collisions
Three were found in a single session, which makes this the shape of the data rather than bad luck:
Name | Objects |
Dahl 2012 Dynasty Trust | PERSON_NODE entity |
Pacific Lake Partners Long-Term Hold Fund One, L.P. | Appears twice |
Dahl Family | GROUP |
Every response therefore discloses name, ID, and object type. IDs from
different namespaces are not interchangeable, and portfolio_type must match.
SDK version note
This targets MCP Python SDK 2.x. If porting older code in this organisation:
FastMCPis nowMCPServer, frommcp.server.mcpserver.ToolAnnotationsfields moved from camelCase to snake_case (readOnlyHintbecameread_only_hint).stateless_httpandjson_responsemoved off the constructor ontostreamable_http_app().Custom exceptions must inherit from the SDK's
ToolError. Anything else is treated as a crash and its message is kept server-side, so the model receives only a generic failure. This silently breaks any error that was supposed to carry information back, such as an ambiguity candidate list.
Known gaps
get_commitmentsandget_entity_attributesfollow the same validated patterns as the other tools but have not been exercised against live data. Commitment columns returned0.0during earlier probing and may need the period arguments the firm's saved views use.JWT signature verification is not implemented. See above.
The group-rooted view URL pattern is inferred and deliberately not emitted.
Credentials have been exposed in plaintext across multiple working sessions. The code reads from environment variables so rotation is a config change, but the rotation itself still needs doing before production use.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Read-only public financial evidence from LiquiLens, Undertow, Seiche and Palimpsest.
Read-only access to your VortexIQ store data: audits, KPIs, alerts, Brand DNA, reports, Ask VIQ.
Read-only NuMetric.work accounting & ERP data: statements, KPIs, reports, invoices, documents.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/graysonlevino/oakridge-addepar'
If you have feedback or need assistance with the MCP directory API, please join our Discord server