{
  "markdown": "[![PersonalKnowHow](design/social/github-social-preview.png)](https://personalknowhow.com)\n\n**[Join the Waitlist](https://personalknowhow.com)** · [Live Demo](#try-the-live-demo) · [Issues](https://github.com/Georgi-Petkov/personalknowhow/issues)\n\nTurns a scattered personal learning/work history — LinkedIn, GitHub, course platforms, Gmail\ncompletion emails, sibling project repos — into a unified knowledge graph, queryable with\nsemantic search instead of keyword matching. Run it locally against your own data in two\ncommands, no account required — or query the real, deployed\n[MCP](https://modelcontextprotocol.io/) servers described further down.\n\n![PersonalKnowHow answering a job-fit question against the graph](design/demo/personalknowhow-demo.gif)\n\n*Asking Claude (via the deployed MCP server) to evaluate a job posting against the graph — real evidence pulled from LinkedIn/GitHub history, not a guess. ([full-resolution video](design/demo/personalknowhow-demo.mp4))*\n\n## Try it locally in 60 seconds\n\nNo Cloudflare account, no signup, nothing deployed — just your own machine.\n\n```bash\ngit clone https://github.com/Georgi-Petkov/personalknowhow.git\ncd personalknowhow\npython quickstart.py\npython ingest/query_local.py \"what do I know about X\"\n```\n\n`quickstart.py` auto-detects whatever sources are already available on your machine and skips\nthe rest with a clear reason — at minimum, an already-authenticated GitHub CLI (`gh auth login`)\nor your sibling project repos are enough to get real results. `query_local.py` embeds your graph\nwith a small local model ([`BAAI/bge-small-en-v1.5`](https://huggingface.co/BAAI/bge-small-en-v1.5)\nvia `sentence-transformers`, downloaded once from Hugging Face on first run — the one real network\ndependency of local mode, distinct from needing a *cloud account*) and ranks results by cosine\nsimilarity, the same approach the deployed MCP servers use.\n\n**Want your own LinkedIn history in the graph, not just GitHub/local-project evidence?** Request\nyour export at linkedin.com → Settings & Privacy → Data privacy → *Get a copy of your data*, then:\n\n```bash\npython quickstart.py --linkedin ~/Downloads/LinkedInDataExport.zip\npython ingest/query_local.py \"what do I know about X\"\n```\n\n`quickstart.py` handles unzipping and routing it to the right ingest script itself — no manual\nfile placement, no flags to figure out. (Requesting the export happens entirely on LinkedIn's\nsite and can take a few minutes for them to prepare — everything after that is the two commands\nabove.)\n\nEvery other source (edX, DataCamp, Gmail) needs its own one-time setup (a hand-populated JSON\nfile or OAuth credentials) — `quickstart.py` detects and skips each one gracefully with a\none-line reason if it's not set up; see [`ingest/CLAUDE.md`](ingest/CLAUDE.md) for per-source\ndetails if you want to add one.\n\n## Try the live demo\n\n`https://personalknowhow-demo.kxtwrdzt6g.workers.dev/mcp` is a real, deployed MCP server — but\n**it's not a webpage.** Opening that URL in a browser sends a plain `GET`, and MCP servers only\nspeak `POST` with JSON-RPC framing, so you'll just see a bare `{\"error\":{\"message\":\"Method not\nallowed.\"}}`. That's expected, not broken — it means you're looking at it the wrong way.\n\nThe actual way to use it is as an MCP connector. In Claude Desktop, edit\n`claude_desktop_config.json` ([config file location](https://modelcontextprotocol.io/quickstart/user)):\n\n```json\n{\n  \"mcpServers\": {\n    \"personalknowhow-demo\": {\n      \"command\": \"npx\",\n      \"args\": [\"mcp-remote\", \"https://personalknowhow-demo.kxtwrdzt6g.workers.dev/mcp\"]\n    }\n  }\n}\n```\n\nRestart Claude Desktop, then ask something like *\"use personalknowhow-demo to check if I have\nDjango experience\"* — Claude calls the `query_knowhow` tool over MCP and gets back\nsemantically-matched evidence (courses, projects, certifications) with similarity scores, no\nauth required.\n\nIf you just want to confirm the server is alive without setting up a client:\n\n```bash\ncurl -s https://personalknowhow-demo.kxtwrdzt6g.workers.dev/mcp \\\n  -X POST -H \"Content-Type: application/json\" -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0\"}}}'\n```\n\nA `200` with a JSON-RPC response back confirms it's live — the `Accept` header above is required;\nwithout it the server correctly returns `406 Not Acceptable`, which is a different, also-expected\nerror from the browser-`GET` one above.\n\n---\n\n## Why this exists\n\nCourse-completion lists and keyword-matched resumes are a weak signal of what someone actually\nknows. This project builds a real knowledge graph from primary sources (not self-reported\nsummaries), embeds every entry with a real embedding model, and exposes it as a queryable MCP\ntool — so \"do I have Django experience?\" gets answered by walking real evidence (a project's\nREADME, a course completion, an endorsement) with a similarity score attached, not a guess.\n\n## Architecture\n\n```\ningest/            Source-specific scripts → common schema\n                    {title, type, provider, date, description, domain_tags}\ncorpus/             Generated markdown, one subfolder per source (not tracked — see Privacy below)\ngraph.json          Extracted nodes/edges from corpus/ (not tracked)\nmcp/                Public MCP server (Cloudflare Worker) — semantic search, no auth\nmcp-private/        Private MCP server — same search, bearer-token gated, adds\n                    signal-only evidence (job applications, career interests)\n```\n\n**Ingestion sources**: LinkedIn (via the [Member Data Portability\nAPI](https://learn.microsoft.com/en-us/linkedin/dma/member-data-portability/overview), EU-only —\nsee [`docs/linkedin-connector-notes.md`](docs/linkedin-connector-notes.md) for notes on the\nmanual-export alternative for other regions), GitHub (via the `gh` CLI, excluding forks — a fork\nis evidence of browsing, not building), DataCamp/edX/Skilljar course completions, Gmail\n(completion emails from other platforms), and sibling project repos (auto-discovered, evidenced\nvia README + tracked filenames + a keyword pass, not self-reported).\n\n`ingest/merge.py` deduplicates across sources (idempotent — safe to re-run). `ingest/build_graph.py`\nextracts nodes/edges from `corpus/` frontmatter into `graph.json`.\n\n## The RAG layer\n\nBoth `mcp/` and `mcp-private/` are stateless Cloudflare Workers (`createMcpHandler`, no Durable\nObject) that embed every corpus entry with Workers AI (`@cf/baai/bge-base-en-v1.5`, 768-dim) at\nexport time, and embed the query string at request time, then rank by cosine similarity. Two MCP\ntools are exposed: `query_knowhow(topic)` for semantic search, and `list_by_type(type)` for a\nplain listing. The private server additionally tags every result with an `evidence_tier`\n(`demonstrated` vs. `signal_only`), so a job application or career-interest entry can never be\nmistaken for proof of a skill.\n\n## Privacy design\n\n`corpus/` and `graph.json` are never public — no public-facing code reads them directly. The\n**only** sanctioned public data source is `mcp/public_entries.json`, built by\n`ingest/build_public_export.py` via a **fail-closed allowlist**: only explicitly listed corpus\ncategories (courses, projects, certifications, education, endorsements, positions, profile,\nrecommendations, articles) get exported. A new corpus category is excluded by default until\nsomeone deliberately adds it to the allowlist — the same discipline that keeps job applications\nand career-interest data out of the public server entirely; that data only exists in\n`mcp-private/`, gated behind a bearer token, and is never committed to this repo either (see\n`.gitignore`).\n\n## Career-agent tooling\n\nA second layer built on top of the same corpus: `ingest/analyze_job_postings.py` scores scraped\njob postings against known skill coverage using the same embeddings (graded `known`/`peripheral`\nsimilarity, not binary keyword matching), `ingest/cv_tailor.py` matches a posting's requirements\nagainst CV bullets with an explicit two-tier system (exact-term matches vs. semantically-related\nmatches, the latter always labeled \"verify before claiming\" rather than asserted), and\n`ingest/recommend_courses.py` cross-references course catalogs against coverage gaps.\n\n## Running individual ingest sources manually\n\n`python quickstart.py` (see the top of this README) runs everything below automatically for\nwhichever sources it detects. For finer control — a single source, non-default flags, or\nre-running just one step after a corpus change — run any of these directly:\n\n```bash\npip install -r requirements.txt\n\n# Run a specific ingest source, e.g.:\npython ingest/github_ingest.py\npython ingest/linkedin_api_ingest.py --domains PROFILE,POSITIONS,SKILLS\n\n# Deduplicate corpus after any ingest run\npython ingest/merge.py\n\n# Build graph.json from corpus/\npython ingest/build_graph.py\n```\n\nEach `ingest/*_ingest.py` script is independent — run whichever sources apply to you. All of them\nwrite markdown into `corpus/<source>/` using the shared schema below.\n\n## Corpus schema\n\nEvery markdown file in `corpus/` uses this YAML frontmatter:\n\n```yaml\n---\ntitle: \"Advanced Python Programming\"\ntype: \"course\"              # course | certification | position | project | education | ...\nprovider: \"LinkedIn Learning\"\ndate: \"2024-01-15\"\ndescription: \"Free-text summary.\"\ndomain_tags:\n  - python\n  - programming\n---\n```\n\n## Deploy your own hosted MCP server (optional, needs a Cloudflare account)\n\nLocal mode (above) is enough to query your own graph — this section is only for hosting it as a\nreal MCP server other clients/people can connect to, the same way the live demo works.\n\n```bash\ncd mcp && npm install && npm run deploy        # public server\ncd mcp-private && npm install && npm run deploy # private server\ncd mcp-private && npm run secret                # set PRIVATE_MCP_TOKEN\n```\n\nBoth need a Cloudflare account with Workers AI access (`[ai]` binding, `remote = true` in\n`wrangler.toml`). Rebuilding the embeddings after a corpus change:\n\n```bash\nCLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_AI_TOKEN=... python ingest/build_public_export.py\nCLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_AI_TOKEN=... python ingest/build_private_export.py\n```\n\n## Adding a new ingest source\n\n1. Create `ingest/<source>_ingest.py` that reads the raw export and writes markdown files to\n   `corpus/<source>/` using the schema above.\n2. `merge.py` and `build_graph.py` require no changes — they scan `corpus/` generically.\n3. If the new category should ever be public, add it deliberately to `ALLOWLIST` in\n   `ingest/build_public_export.py` — it's excluded by default otherwise.\n",
  "bytes": 10713,
  "sha": "8c48c5feaeeb9ea42f3ff53e7d42ae132c3c5dda7e23a4acfb6b123a5a147cf3",
  "repo_slug": "georgi-petkov/personalknowhow",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_georgi_petkov_personalknowhow_5f25a221/readme"
}