Skip to main content
Glama
graysonlevino

Addepar MCP Server

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

0.0

Addepar computed a value, and it is zero

W Robert Dahl, Goldman Sachs -400P

null

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

STALE_VALUATION

A position was marked more than 35 days before the requested date

NULL_VALUES_EXCLUDED

One or more positions returned no computed value

AMBIGUOUS_MATCH

A lookup found more than one plausible candidate

PATTERN_MATCH_USED

Name matching was used, which is non-exhaustive by nature

DEPTH_CAP_REACHED

Traversal stopped early, suggesting circular nesting

MIXED_VALUATION_DATES

A total combines values marked on different dates

RESULT_TRUNCATED

More rows exist than were returned; totals still cover all

UNVERIFIED_CITATION

No confirmed UI link pattern exists for this object type


Tools

Tool

Question it answers

resolve_entity

Turn a name into a specific Addepar ID and object type

get_ownership_rollup

Total exposure, targeted holdings, or beneficial ownership

get_group_exposure

Exposure across a family of related funds

get_entity_attributes

How one client's holdings are classified

list_views

Which saved reports exist

get_view_data

Run one of the firm's own saved reports

get_commitments

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/ -q

Run locally over stdio:

TRANSPORT=stdio .venv/bin/python -m addepar_mcp.server

Run over HTTP:

TRANSPORT=http HOST=0.0.0.0 PORT=8080 .venv/bin/python -m addepar_mcp.server

Health 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 (static_headers)

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

/app/tools/details/entity/{entity_id}

Confirmed

View on entity

/app/tools/portfolio/entity/{portfolio_id}/view/{view_id}

Confirmed

View on group

/app/tools/portfolio/group/{group_id}/view/{view_id}

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

486,034,402.38

Loon Point Holdings II LLC, targeted, 4 occurrences summed

21,427,660.34

Pacific Lake family, 6 matches of 4,121 scanned

15,229,060.19

Charlotte's share of the shared LLC

5,356,915.09

Real maximum ownership depth

5

Households at the firm

9

Two cross-checks make these trustworthy rather than merely recorded:

  1. The household total is identical whether reached by grouping on ownership (nested legal hierarchy) or on security (flat holdings). Two entirely different query shapes, same number to the cent.

  2. 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-Firm header, including /v1/users/me.

  • filter[name] on /v1/entities is 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/entities returns 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/query via display_names. That is the only working name search in the API.

  • ownership grouping walks legal entities only. It stops at holding-account style leaves and does not descend into the securities inside them. Use security grouping 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 31643590 and TRUST entity 31643598

Pacific Lake Partners Long-Term Hold Fund One, L.P.

Appears twice

Dahl Family

GROUP 3192711 and HOUSEHOLD entity 31647552

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:

  • FastMCP is now MCPServer, from mcp.server.mcpserver.

  • ToolAnnotations fields moved from camelCase to snake_case (readOnlyHint became read_only_hint).

  • stateless_http and json_response moved off the constructor onto streamable_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_commitments and get_entity_attributes follow the same validated patterns as the other tools but have not been exercised against live data. Commitment columns returned 0.0 during 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.

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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.

View all MCP Connectors

Latest Blog Posts

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