fundgraph
Ingests email data from Gmail exports (mbox files) or live via gog/Google APIs to build the knowledge graph of people, organizations, and relationships.
Ingests live Gmail, Calendar, and Drive data via Google APIs or Google Workspace/Takeout exports to build the fund's knowledge graph.
Ingests Granola (macOS) local cache for meeting notes as a data source for entity resolution and knowledge graph building.
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., "@fundgraphFind the warmest path from Dana Whitfield to Priya Nair"
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.
fein
The fund graph for venture capital — an open-source agentic data layer for investment teams. (formerly fundgraph; the old CLI name, FUNDGRAPH_* environment variables, and data directories keep working) An entity-resolved knowledge graph over your fund's scattered data — email, calendar, meeting notes, docs, CRM — with a relationship-intelligence dashboard and a single MCP endpoint for Claude, ChatGPT, or Cursor.

Agents can't operate over millions of documents by guessing with vector search: they fetch the wrong things and don't know what exists. fein gives them a deterministic map of reality instead — who exists, who knows whom, and how strongly — so retrieval is structured graph traversal, not similarity roulette.
systems of record fein consumers
───────────────── ────────────────────────── ──────────────────────
gmail ─┐ ┌─ metadata layer web dashboard
calendar ─┤ ingest │ (what was ingested, Claude / ChatGPT / Cursor
drive ─┼────────▶ │ when, who's mentioned) via one MCP endpoint
granola ─┤ ├─ entity resolution
crm ─┤ │ (blocking → candidates →
mbox/ics/csv │ matching → human review)
│ └─ knowledge graph
│ (people + orgs, weighted edges,
│ warm-path traversal)Quickstart
Requires Node 20+. No database setup — embedded Postgres (PGlite) under ./data/; set DATABASE_URL to use real Postgres.
npm install
npm start # → http://localhost:4321First run shows onboarding: load the bundled (fictional) sample dataset with one click — it seeds the whole demo world, including a two-member team with a private layer — or drop in your own export. The dashboard also serves the MCP endpoint at /mcp, so agents and the UI share one process. Embedded mode is single-process (a lockfile enforces this): stop the web server before running CLI ingests, or use DATABASE_URL to run several processes.
Related MCP server: Crypto Funds MCP
Deploying for a team
docker compose up -d with one env var is a production deploy: token-gated
dashboard + MCP endpoint, data in a volume, fail-closed if the token is
missing. DEPLOY.md is the client-onboarding runbook — TLS,
connecting agents, backups, Postgres mode, upgrades.
The dashboard
|
|
Explore — search, click a node, get a brief: strongest relationships with the signals behind each score ("3 meetings, 2 emails, 1 co-authored doc"), recent shared documents, one-click Markdown export.
Radar — which relationships need attention now, judged against each pair's own learned cadence.
Warm path — the best route to an introduction, maximizing end-to-end relationship strength, with introducers ranked by their weaker leg.
Reviews — matches scoring between the review floor and the auto-merge threshold (default 0.70–0.95, tunable in Settings) wait for a human; the system never merges identities on a guess. Decisions are audited and survive rebuilds.
Data — drag-and-drop ingestion, live connectors, team members and their privacy layers, per-source breakdown, audit trail.
Settings — what "a strong relationship" means differs by firm: signal weights, recency half-life, and saturation are editable live; saving rebuilds the graph instantly.
Ingesting your data
Source | Command | Setup needed |
Gmail export (Takeout) |
| none — streamed, so multi-GB archives are fine |
Calendar export |
| none |
Contacts (Google Contacts, Attio, Affinity, any CSV) |
| none |
LinkedIn connections export |
| none — auto-detected; "Connected On" becomes timing signal |
Granola (macOS) |
| none — reads the local cache |
Attio workspace (live) |
|
|
Affinity workspace (live) |
|
|
Live Gmail/Calendar/Drive via gog |
| gog already authenticated (local, or remote via |
Live Gmail/Calendar/Drive via Google APIs |
| a Desktop OAuth client JSON in |
From a Google Workspace / Takeout export
Works on any account, including one you no longer actively use — you only need to be able to sign in once.
At takeout.google.com (signed in as that account) choose Mail, Calendar, and Contacts. For Mail, use "All Mail data included" or select specific labels; export as
.zip.Unzip. You'll get
Takeout/Mail/All mail Including Spam and Trash.mbox,Takeout/Calendar/*.ics, andTakeout/Contacts/contacts.csv.Ingest — order doesn't matter, entity resolution links them:
fein ingest "Takeout/Mail/All mail Including Spam and Trash.mbox"
for f in Takeout/Calendar/*.ics; do fein ingest "$f"; done
fein ingest Takeout/Contacts/contacts.csv
fein syncThe mbox is streamed and ingested in batches, so archive size is not bounded by
memory (measured ~420 messages/sec — a 100k-message account takes a few
minutes). Contacts exports use Google's own column names
(E-mail 1 - Value, Organization Name, split first/last), which the CSV
adapter handles directly; multiple addresses on one contact become one entity.
From an Attio workspace

In the dashboard (no terminal): open the Data tab → Attio workspace → paste your access token → Connect & sync. The key is verified against Attio before anything is stored, the first pull runs immediately, and the panel then shows the workspace, last sync, and a Sync now button for later refreshes.
Or from the CLI: export ATTIO_API_KEY=... && fein ingest-attio && fein sync.
To create the token: in Attio go to Workspace settings → Developers → Create
an integration, and grant read scopes for record and object_configuration
(add note to include notes).
A key pasted into the dashboard is stored in your local database and is
write-only across the API — no endpoint ever returns it, status shows a
masked hint (····1234) only, and it never reaches the audit log. It is stored
in plain text at the same trust level as the graph itself, so for shared or
server deployments prefer the ATTIO_API_KEY environment variable (which the
dashboard will detect and use without storing anything). Disconnect deletes
the stored key and leaves already-ingested data in place.
Pulls people, companies, and notes. A person's linked company becomes their org
hint, and all of a contact's addresses are attached to one entity — so an Attio
contact and their emails in Gmail resolve to the same person. Pass
--no-notes to skip notes (or if your token lacks the scope, notes are skipped
with a warning rather than failing the pull).
Then fein sync (resolve + rebuild edges).
Affinity works the same way: its card sits next to Attio's on the Data
tab (paste a key from Affinity Settings → API), or
export AFFINITY_API_KEY=... && fein ingest-affinity && fein sync. Same
write-only key handling, same privacy stance — people, organizations, and
note participants; note bodies are never read.
What gets read: live connectors (Granola, gog, Google APIs, Attio people/companies) read metadata and participant identities only. File exports (.mbox, .ics, .csv notes, .jsonl) also capture a size-capped plain-text body per document — stored locally in your database and mined only when you explicitly run unstructured extraction. Set FEIN_NO_BODIES=1 to skip body capture entirely and keep the old metadata-only behavior.
Adapters emit a common JSONL shape (see sample/seed.jsonl); to add a source, emit that shape and fein ingest file.jsonl. Ingestion is idempotent: re-ingesting updates in place, and review history is preserved.
Fixing what resolution missed
Resolution is deliberately conservative — conflicting evidence queues for review rather than merging — so real data always leaves a few duplicates (a work address and a personal one for the same person, resolved apart). Merge them:
fein merge "Alex Rivera" "alex@northgate.io" # keep the first, absorb the second
fein unmerge "alex@northgate.io" # reversible
fein merges # what's been mergedOr in the dashboard: open a profile → Merge a duplicate…. Documents,
relationships, and addresses move to the survivor; the loser is kept as a
tombstone rather than deleted, so the merge stays reversible and unmerging gives
back exactly what the merge took (leaving an address behind would misroute
future mentions). Like review decisions, merges are human input — they're
recorded and replayed after a full reresolve instead of being lost.
fein also flags automated senders (no-reply robots, notification
services) and hides them from relationship views — on a real inbox they're
otherwise half the graph. Role addresses like team@ or a client's hello@ are
treated as hints only and need broadcast behaviour (never replies, never in a
meeting) to be flagged, because a shared mailbox usually has a human behind it.
Nothing is deleted, every flag carries its reason, and fein automated --list shows the lot.
Relationship radar — the timing layer

Strength answers who do I know well. Radar answers who should I contact now. Each pair's natural cadence is learned from real contact history, so overdue means overdue for them: three weeks of silence is unremarkable with a quarterly contact and alarming with a weekly one.
fein radar # whole graph, most actionable first
fein radar "Maya Chen" # one person's relationshipsEvery row carries its receipts — "last contact 16d ago · usually every 10d ·
6d overdue · 3 touches" — and statuses (active, due, overdue, cold,
dormant, new) plus a warming/steady/cooling trend comparing the last 90 days
with the 90 before. With no contact in either window the trend is null rather
than a fabricated "steady". Entirely deterministic: intervals and dates, no
model in the loop. Radar respects privacy layers, and agents get it as the
relationship_radar MCP tool.
Privacy layers

A relationship graph is only useful if people are willing to put their inbox in it — and nobody wants to hand their personal email to the whole team. So each member connects their own sensitive sources into a private layer that lives inside the shared graph:
Evidence is private. Connection strengths, signals, and documents from a member's layer are visible only to them. Another member's brief on the same person shows a
withheldDocumentscount and nothing else.Existence is shared. If the only route to someone runs through a colleague's private layer, you're told the route exists, which hop is locked, and who to ask — with no strength attached. That's the whole point of a relationship graph: "Seb can reach Priya, ask him."
Layers combine, they don't replace. Your own evidence is summed with the shared layer before saturation, so private data reinforces public data.
Absorption is layered. Resolution folds what it learns about a person into their entity record — but evidence absorbed from a private mailbox (a second address, an org affiliation, a fuller name) is visible only to that mailbox's owner, overlaid at read time. The shared record only learns what a shared source has independently witnessed, and display names upgrade only from shared mentions. Databases written by v0.4.0 or earlier predate this policy — run
fein reresolveonce after upgrading (the server warns on boot until it is done).
fein members add "Seb Larkin" seb@ridgeline.vc
fein ingest seb-inbox.mbox --as "Seb Larkin" # → Seb's private layer
fein path "Tom Merrill" "Priya Nair" --as "Tom Merrill"All of this works without the terminal too: the Data tab manages members, the
Uploads land in selector above the dropzone targets a member's private
layer, and the Viewing as switch in the header changes whose view you see.
For agents, ?as=<member> on the MCP URL (or FEIN_VIEWER=<member> for
stdio) binds an MCP server to one person's view. Every ?as= — API reads,
uploads, and /mcp alike — accepts a member id, exact name, or email (names
and emails match case-insensitively; ids are exact), and an unknown or
ambiguous ref is a hard 400 listing the candidates, never a silent answer
from the shared layer.
What "existence is shared" actually means. A person the firm already knows
(they appear in any shared document) stays visible to everyone, and a colleague's
private correspondence with them surfaces as a locked hop — that is the feature.
But an entity that appears only inside one member's private layer is hidden
entirely by default, because the name itself can be the secret ("Project
Nightjar"). Set privateEntityVisibility: "reveal" in Settings to opt into the
fully-shared-names model instead; it is a deliberate choice, not a default.
Removing a member forces an explicit choice: delete their documents, or move
them into the shared layer where everyone will see them. Enforcement is
server-side on every query and npm test includes a leak probe that stuffs
markers into a private layer and greps every endpoint's response as another
member, but this is a cooperative model for a trusted
team on one local database, not a hostile-tenant boundary: anyone with
filesystem access to ./data or the ability to pass an arbitrary ?as= can
read any layer. Real multi-tenant isolation needs authentication, which is on
the roadmap below.
Design principles
Everything resolves to two entities: people and organizations. Deals, funds, docs hang off those two.
Two-layer data model. A metadata layer tracks what was ingested and who was mentioned; the knowledge graph holds resolved entities and weighted connections. The graph is a read model — rebuilt deterministically, never hand-edited.
Four-stage entity resolution: blocking → candidate generation → probabilistic matching → human review. Deterministic auto-merge above a confidence threshold; the band below it queues for a human (defaults 0.95 and 0.70–0.95, tunable in Settings); conflicting evidence (same name, different work domain) always asks. Without this, one person appears as 100+ duplicates across sources.
Never let an LLM score a relationship. Connection strength is computed from observable signals — meeting frequency, email reciprocity, co-authorship, recency decay — because models will confidently hallucinate a 3/10 relationship as a 10/10.
Graph-based retrieval, not pure vector. "Who can intro me to X?" is a weighted shortest-path query, answered with the evidence behind each hop.
MCP — agents on the graph
The dashboard serves the graph as a Streamable-HTTP MCP endpoint at
http://localhost:4321/mcp — same process, same live database, no conflict
with the embedded single-process rule. With npm start running:
claude mcp add --transport http fein http://localhost:4321/mcp(Claude Desktop: Settings → Connectors → add the same URL. The Data tab shows
this command with a copy button.) Append ?as=Seb%20Larkin to bind the agent
to that member's private layer — unknown members are rejected, never silently
given the shared view.
Without the dashboard running, the stdio flavor works anywhere:
claude mcp add fein -- node /path/to/fein/src/cli.js mcpTools: meeting_prep (one call: profile + relationship history + receipts + your warm paths to them), company_memory (every recorded deal signal for a company — investments and passes with their reasoning — with document provenance), relationship_radar, find_warm_path, find_introducers, entity_brief, search_entities, strongest_connections, graph_stats, review_queue, review_resolve.
CLI
fein web [port] dashboard (default 4321)
fein ingest <file> .jsonl | .mbox | .ics | .csv
fein ingest-granola [path] Granola local cache (macOS)
fein ingest-gog <service> live pull via gog: gmail | calendar | drive
fein ingest-google <service> live pull via Google APIs
fein sync [--extract] resolve + rebuild edges (--extract mines bodies first)
fein extract [--limit N] LLM mention extraction over unprocessed bodies
fein reresolve rebuild entities from scratch (decisions replayed)
fein memory <company> fund memory: deal signals — investments + passes with reasoning
fein entities | brief | path | intros | review | stats
fein mcp MCP server (stdio)Unstructured extraction
Headers and attendee lists are a fraction of what a fund knows. The bodies — "our
IC chair Alistair Penhale has asked…", "Sam Okafor at Halcyon co-invested with us
on three deals" — name people and organizations no structured field ever sees.
fein extract mines them with an LLM and feeds the results through the
same resolution, review, and edge pipeline as everything else:
export ANTHROPIC_API_KEY=... # or `ant auth login`
fein extract # mine all unprocessed bodies
fein sync --extract # or as part of a sync(Or press Extract pending documents on the dashboard's Data tab.)
Extraction never gets to bend the graph's rules:
Structured output, not free text — the model can only return typed mention candidates; a prompt-injected document can at worst distort which candidates come back, never make the pipeline do something.
Deterministic grounding — every candidate must literally appear in the document text. Names not in the text are dropped; emails are kept only if the exact address string is present (a model can never "complete"
name@domaininto existence); low-confidence candidates are dropped.Same trust model as any mention — extracted mentions carry
origin='extracted', a confidence, and a verbatim source quote; they resolve through blocking → matching → human review like structured mentions, and co-occurrence is damped by the merely-mentionedfactor. Connection strength stays deterministic (principle 4): the LLM proposes candidates; it never scores a relationship.Idempotent + resumable — each document records a hash of (prompt version, model, body); re-runs skip clean documents, re-extract changed ones, and retry failures. Three consecutive failures abort the run.
Extraction also mines fund memory: when a document records an investment
decision (an IC memo's INVEST or PASS, a board pack, a round discussion), a
deal record is kept — company, stage, status, the stated reasoning, and the
document it came from. fein memory <company> or the company_memory
MCP tool answers the question every fund eventually asks: "have we seen this
company before, and why did we say no?" Deals hang off organizations
(principle 1) and link to entities at query time, so rebuilds never orphan
them; passes are first-class, because a recorded no is the memory that saves
the next diligence cycle.
Configuration: FEIN_EXTRACT_MODEL (default claude-opus-5;
claude-haiku-4-5 is the budget option), FEIN_EXTRACT_EFFORT
(default low), FEIN_EXTRACT_MIN_CONFIDENCE (default 0.6).
Details, cost notes, and the threat model: docs/extraction.md.
How connection strength works
Each co-occurrence contributes weight(kind) × decay(age): meetings 3, calendar events 2, direct emails 2.5 (cc'd 1), co-authored docs 1.5, merely-mentioned participants halved — 180-day half-life. Strength is 1 − e^(−W/6), saturating toward 1. Warm paths maximize the product of hop strengths (hop-bounded Dijkstra over −ln(strength)). Every number is tunable in Settings, per database.
Testing
npm test # 13 suites: resolution, ingest, API, extraction, privacy, radar, automated, merge, leak probe, auth, connectors, edges, schedulerThe extraction suite runs the full pipeline against a scripted fake model — grounding, idempotency, failure isolation, and resolution integration are all covered offline; no API key needed.
All suites run on throwaway databases. Recent releases have each gone through adversarial multi-agent review; confirmed findings are fixed, the most serious pinned by regression tests (see the CHANGELOG).
Status & roadmap
Working today: everything above, including token-gated client deployments (see DEPLOY.md). Per-member privacy is view scoping behind one shared deployment token — real multi-tenant isolation (per-user login) is on the roadmap. What's next, in priority order: ROADMAP.md. PRs welcome.
License
MIT
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 Servers
AlicenseAqualityCmaintenanceMCP server for the Graphite Financial Knowledge Graph, enabling natural language queries about companies, supply chains, executives, regulations, and patents via MCP-compatible clients.7MIT- Alicense-qualityDmaintenanceAn MCP server that provides AI agents with structured, real-time data on cryptocurrency investment funds, enabling deeper due diligence and portfolio intelligence.3MIT
- AlicenseAqualityBmaintenanceA money-in-politics knowledge graph, as an MCP server. It links entities across federal campaign donations, lobbying disclosures, and government contracts.7MIT
Related MCP Connectors
Knowledge coverage map and health score. Ingest docs into a governed knowledge graph via MCP.
Identity resolution MCP server for phone/email lookups across 31+ services. Global + India coverage.
Remote MCP server to enrich company profiles with structured B2B data and confidence scores.
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/clippycommits/fein'
If you have feedback or need assistance with the MCP directory API, please join our Discord server

