SocialMCP
by SciTechDude
README.md
# SocialMCP
**A local-first personal CRM for your professional network, delivered entirely as an MCP server.**
No scraping. No cookies. No browser automation. No web UI. You drop the data
export that LinkedIn already owes you into a folder, and **any MCP client**
becomes the interface to a searchable CRM that remembers how you met everyone.
```
You: Who did I meet at Seattle AI Week, and who haven't I followed up with?
Assistant: 7 people, 13-15 Oct. Four are still cold:
• Marcus Webb — Staff ML Engineer, Cascade Robotics
"Great meeting you at Seattle AI Week today!"
• Hana Suzuki — Director of Product, Elliott Bay Health
...
Also: Priya Raghavan moved from Northwind AI to Aurora Labs as VP GTM
since your last export.
```
*(Illustrative — the names above come from the synthetic fixtures in
`tests/fixtures/`, not from anyone's real network.)*
## Why this exists
Networking at a fast-paced event creates a memory gap: three days later you have
forty new connections and no idea which conversation went with which name.
LinkedIn used to let you tag and annotate connections privately — that is now
behind Sales Navigator.
The workaround most people already use by hand:
1. A QR code on your lock screen so people can scan instead of typing.
2. A clipboard template in the connection invite — *"Great meeting you at
Seattle AI Week today!"*
That second habit is the key. Your official LinkedIn data export contains
`Invitations.csv`, and **the `Message` column preserves exactly what you pasted,
with a timestamp**. That is a dated, venue-stamped note about how you met every
single person — already written, just never surfaced. SocialMCP extracts it,
clusters the connections into the events they came from, and makes the whole
thing queryable in natural language.
## Principles
| | |
|---|---|
| **MCP-first, headless** | No SPA, no REST API, no listening socket. The chat window is the UI. |
| **Zero scraping, ToS-compliant** | Only user-owned GDPR/CCPA export files. Never a session cookie, headless browser, or live API call against a social network. |
| **Local-first privacy** | Contacts and DM history live in one SQLite file on your machine. Embeddings default to local Ollama. Cloud LLM keys are strictly opt-in. |
| **Contributor-extensible** | New sources are one module implementing `BaseAdapter`. No schema changes. |
## Install
Requires Python 3.11+.
```bash
git clone https://github.com/SciTechDude/socialMCP.git
cd socialMCP
pip install -e ".[dev]"
```
Optional, for semantic search (recommended):
```bash
# sqlite-vec comes with the install; you also need a local embedding model
ollama pull nomic-embed-text
```
Without Ollama, SocialMCP runs fine — search falls back to SQLite FTS5 keyword
matching plus exact filters.
## Get your data
LinkedIn → **Settings & Privacy → Data Privacy → Get a copy of your data** →
request the **larger archive** (it is the one with `Invitations.csv` and
`messages.csv`). It arrives by email in minutes to 24 hours.
Drop the `.zip` — unopened, as downloaded — into:
```
drop-archive/linkedin/
```
That is the whole ingestion UX. The background harness notices the file, waits
for the copy to finish, parses it, and files it away under `ingested/linkedin/`.
## Connect it to any MCP client
SocialMCP is a standard **stdio MCP server** with no client-specific code, so
anything that speaks MCP can drive it. The whole integration is one command:
```
python /absolute/path/to/socialMCP/run.py
```
Most clients configure that as JSON in the now-conventional `mcpServers` shape:
```json
{
"mcpServers": {
"socialmcp": {
"command": "python",
"args": ["/absolute/path/to/socialMCP/run.py"]
}
}
}
```
Where that JSON goes, by client:
| Client | Location |
|---|---|
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
| Claude Code | `claude mcp add socialmcp -- python /abs/path/run.py` |
| Cursor | `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally |
| VS Code (Copilot) | `.vscode/mcp.json`, under a `servers` key instead of `mcpServers` |
| Zed | `context_servers` in `settings.json` |
| Continue | `mcpServers` in `~/.continue/config.yaml` |
| Windsurf, Cline, LibreChat, Goose, … | their own MCP config file, same command |
| Your own code | `mcp` SDK (Python/TS), Pydantic-AI, LangChain MCP adapters, … |
Check your client's docs for the exact key name — the command and args never
change. Restart the client afterwards, then ask it to call
`get_network_briefing`.
If a client won't start the server, run `python run.py stats` first: that
exercises the same database and config without the protocol in the way.
### Verifying it from your own script
No AI client needed to confirm the server works — this is the reference client
from the `mcp` SDK:
```python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="python", args=["/abs/path/to/socialMCP/run.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
print([t.name for t in (await session.list_tools()).tools])
print((await session.call_tool("network_stats", {})).content[0].text)
asyncio.run(main())
```
`tests/test_mcp_protocol.py` is this same flow as a test, so client-neutral
compatibility is enforced on every CI run rather than assumed.
## What any MCP client can do
| Tool | Purpose |
|---|---|
| `search_network` | Hybrid semantic + keyword search with structured filters (company, title, status, event, date range). |
| `get_event_cluster` | Everyone connected during one event window, with their invite notes and who is still un-followed-up. |
| `get_contact` | One person's full record: invite note, DMs, your notes, job changes. |
| `log_contact_note` | Append a timestamped private note. |
| `list_pending_invitations` | Invites you sent that were never accepted — your follow-up list. |
| `rename_event_cluster` | Turn `"13-15 Oct 2025 burst (7 connections)"` into `"Seattle AI Week 2025"`. |
| `trigger_archive_ingest` | Ingest on demand instead of waiting for the watcher. |
| `list_event_clusters`, `network_stats`, `reindex_embeddings`, `get_network_briefing` | Housekeeping and session priming. |
Resources: `socialmcp://briefing`, `socialmcp://stats`, `socialmcp://clusters`,
`socialmcp://adapters`.
## Also usable without an AI client
```bash
python run.py ingest # process the drop zone now
python run.py search "enterprise AI sales" --limit 5
python run.py search "" --event seattle-ai-week-2025
python run.py clusters
python run.py briefing
python run.py watch # harness only, foreground
python run.py # MCP server over stdio
```
## How it works
```
drop-archive/linkedin/export.zip
│
▼
core/harness.py ──── watchdog + debounce (never reads a half-copied zip)
│
▼
core/ingest.py ───── safe extract → sha256 dedupe → adapter → merge → upsert
│ │
│ adapters/linkedin/adapter.py
│ (CSV quirks live here, only here)
▼
SQLite: contacts, interactions, contact_history, event_clusters,
contact_identities, harness_events, contacts_fts, vec_contacts
│
├── core/clustering.py deterministic event detection
├── core/search.py FTS5 + sqlite-vec fused with RRF
└── mcp_server/server.py the only thing the model talks to
```
### Repository layout
```
socialMCP/
├── run.py # entry point: MCP server, or CLI subcommands
├── drop-archive/linkedin/ # ← drop LinkedIn .zip exports here
├── ingested/ # processed archives, structure mirrored
├── socialmcp/
│ ├── config.py # TOML + env config, drop-zone routing
│ ├── cli.py
│ ├── core/
│ │ ├── schema.sql # the full data model, commented
│ │ ├── db.py # connection, migrations, sqlite-vec loading
│ │ ├── models.py # the records every adapter emits
│ │ ├── normalize.py # URL/date/name canonicalization
│ │ ├── ingest.py # extract → merge → upsert → history
│ │ ├── clustering.py # temporal event detection
│ │ ├── search.py # hybrid search + RRF fusion
│ │ ├── embeddings.py # Ollama / OpenAI / none
│ │ └── harness.py # watcher, activity log, briefing
│ ├── adapters/
│ │ ├── base.py # BaseAdapter + CSV header-detection helpers
│ │ └── linkedin/adapter.py # v1 adapter
│ └── mcp_server/server.py # FastMCP tools and resources
└── tests/ # 63 tests, anonymized fixtures, no network
```
### The LinkedIn quirks it handles
These are the things that break naive parsers, all covered by tests:
- **Multi-line preamble.** `Connections.csv` opens with a human-readable
"Notes:" disclaimer. The line count has changed between export vintages, so
the parser scans for the first row containing the real headers instead of
skipping a hard-coded three lines.
- **UTF-8 BOM** glued to the first header cell.
- **Header drift.** `Direction` is absent in older `Invitations.csv`; profile-URL
columns appear in camelCase when they appear at all. Headers are normalized
(`SENDER PROFILE URL` → `sender_profile_url`) rather than matched literally.
- **Invitations have no profile URL.** The same person therefore arrives as
`in/jane-doe` from Connections and `name/jane-doe` from Invitations. These are
merged — otherwise your clipboard note ends up orphaned on a phantom "pending"
duplicate. The merge refuses to fire when a name is ambiguous.
- **Opaque member URNs.** `messages.csv` uses `/in/ACoAAB…` URLs that never match
the vanity slugs in `Connections.csv`, so DMs are matched through an alias
table (URL → email → unambiguous name).
- **Dates everywhere.** `"13 Oct 2025"`, `"10/13/25, 6:42 PM"`,
`"2025-10-20 17:04:11 UTC"`, bare epochs. One bad date warns and keeps going.
- **Monthly re-drops.** Archives are deduped by SHA-256; overlapping rows are
idempotent; a changed company or title is written to `contact_history` so you
can see who moved.
- **Untrusted zips.** Extraction rejects path traversal (`../`) and absurd
compression ratios. Exports are downloaded files, not trusted input.
### Event clustering
A greedy pass over connection dates: extend a burst while each gap is ≤
`max_gap_days` (2) and the total span stays ≤ `max_span_days` (5); emit it if it
holds ≥ `min_contacts` (5) people. Fifteen connections over a conference weekend
becomes one cluster; one connection a week never does. Tunable in
`socialmcp.toml`. Clusters you rename are never renamed back.
### Search
Keyword (FTS5, BM25-weighted toward names and invite notes) and semantic
(sqlite-vec cosine) rankings are fused with Reciprocal Rank Fusion, so
`"GTM background"` works without keyword overlap and `"Stripe"` still ranks the
literal match first. Structured filters are plain SQL predicates applied before
fusion — exact, never approximate.
## Privacy
Everything is a local file. The server speaks stdio to its parent process and
opens no network socket. The only outbound requests SocialMCP can make are to
your configured embedding provider — `127.0.0.1:11434` by default — and nothing
at all if you set `provider = "none"`. `.gitignore` blocks exports, the
database, and local config so you cannot accidentally commit your network.
## Development
```bash
pip install -e ".[dev]"
pytest # 63 tests, no network access required
ruff check .
```
Fixtures in `tests/fixtures/linkedin/` are a hand-written, fully synthetic
export that reproduces every quirk above. Use them instead of your real data.
## Roadmap
- **Phase 1 & 2 (done):** schema, hardened ingest pipeline, LinkedIn adapter,
event clustering, harness, MCP tools and resources.
- **Phase 3:** more adapters (Instagram/Facebook JSON, X archives, vCard/CSV),
richer follow-up prompting, optional recurring reminders.
Adapters are the easiest place to contribute — one module, no schema change.
See [CONTRIBUTING.md](CONTRIBUTING.md).
## License
Apache License 2.0 — see [LICENSE](LICENSE).
Apache-2.0 rather than MIT for the explicit patent grant and because its
contribution clause means contributors don't need to sign a separate CLA.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues