{
  "markdown": "# portfolio-mcp\n\n[![Glama MCP server](https://glama.ai/mcp/servers/saagpatel/portfolio-mcp/badge)](https://glama.ai/mcp/servers/saagpatel/portfolio-mcp)\n\nThe agent-native layer of [saagarpatel.dev](https://saagarpatel.dev): a Model\nContext Protocol server that lets any AI agent query Saagar's writing, projects,\npublic-safe repo profiles, and benchmark results directly, instead of scraping HTML.\n\nRead-only. Stateless. Public. No auth, no tracking, no database, no runtime egress.\n\n## How it fits\n\nThe website stays a pure static site. This server is a **sibling**, not a backend\nbolted onto it:\n\n- **Layer 0** (in the `portfolio-index` repo): the build emits a static machine\n  corpus — `corpus-index.json`, per-document `corpus/<id>.json`, and\n  `.well-known/mcp.json` — plus public-safe repo profile artifacts when present,\n  served alongside the HTML. Already public.\n- **Layer 1** (this repo, `src/index.ts`): a stateless Cloudflare Worker that bakes\n  the Layer 0 corpus into its bundle and serves it over MCP (streamable HTTP, the\n  `WebStandardStreamableHTTPServerTransport`). Zero runtime fetches.\n- **Layer 2** (this repo, `src/stdio.ts`): the same server over stdio, for running\n  locally via `npx saagar-portfolio-mcp`. Identical tool/resource/prompt surface.\n\nThe shared core (`src/server.ts`) is transport-agnostic; both layers wrap it.\n\n## Tool surface\n\nAll read-only (`readOnlyHint: true`). No tool takes a URL or filesystem path (no\nSSRF / exfil surface).\n\n| Tool | Purpose |\n|---|---|\n| `search` | BM25 over the whole corpus; optional `section` filter, `limit` |\n| `get_document` | Full Markdown of one document by `id` |\n| `list_corpus` | The table of contents; optional `type` filter |\n| `get_profile` | The \"who is this\" card (about / now / uses) |\n| `list_projects` | Curated public-safe projects + anonymized aggregates |\n| `list_repo_profiles` | Public-safe repo profile index with freshness and proof counts |\n| `get_repo_profile` | One repo answering profile by `repo_id` |\n| `get_operant_results` | Public, sanitized OPERANT calibration results (per-model OCS) |\n\nDocuments are also exposed as **Resources** (`portfolio://essays/{slug}`, `book/{slug}`,\n`notes/{slug}`, `portfolio://profile`), and there are two **Prompts**:\n`introduce_saagar` and `summarize_writing_on` (grounded in a live `search`).\n\n## Retrieval\n\nBM25 over a baked index (no embeddings in v1 — the corpus is ~50 small docs and the\ncalling LLM supplies the semantics). Titles are boosted. Embeddings are a measured\nPhase 3 upgrade, added only if retrieval quality proves insufficient.\n\n## Layout\n\n```\nsrc/\n  types.ts            corpus + projects + operant shapes\n  bm25.ts             dependency-free BM25 + snippet (pure)\n  tools.ts            createTools(corpus) -> the 8 tools (pure, injectable)\n  corpus.ts           loads the baked corpus + accessors\n  corpus.generated.ts AUTO-GENERATED by build:corpus\n  server.ts           buildServer(): shared MCP core (tools + resources + prompts)\n  index.ts            Cloudflare Worker transport (streamable HTTP)\n  stdio.ts            Layer 2 stdio transport (the npx CLI)\nscripts/\n  build-corpus.mjs    bakes Layer 0 (+ OPERANT) into corpus.generated.ts\n  probe-mcp.mjs        probes an MCP HTTP endpoint (saagar-mcp-kit driver + domain calls)\n  audit-mcp.sh        connected MCPAudit scan of this server (dogfood)\ntest/                 vitest: bm25, tools, full-protocol server tests\n```\n\n## Develop\n\n```sh\nnpm install\nnpm run build:corpus          # bake from ../portfolio-index (or --url=https://saagarpatel.dev)\nnpm run typecheck\nnpm test\nnpm run dev                   # wrangler dev -> http://localhost:8787/mcp\nnpm run smoke                 # end-to-end MCP smoke under the real workerd runtime (saagar-mcp-kit)\nnpm run probe:mcp             # live Worker probe, or set PORTFOLIO_MCP_ENDPOINT\n```\n\nInspect either transport with the MCP inspector:\n\n```sh\nnpx @modelcontextprotocol/inspector http://localhost:8787/mcp   # Layer 1 (HTTP)\nnpx @modelcontextprotocol/inspector node dist/stdio.js          # Layer 2 (stdio, after build:cli)\n```\n\n## Deploy (Layer 1)\n\n```sh\nnpm run build:corpus && npm run deploy   # wrangler deploy\nnpm run probe:mcp                        # post-deploy live MCP readback\n```\n\nOperator-gated (needs Cloudflare auth). v1 still deploys to the default\n`portfolio-mcp.<account>.workers.dev` URL, and `npm run probe:mcp` uses that stable\nWorker URL by default. Public discovery now advertises the verified custom endpoint\n`https://mcp.saagarpatel.dev/mcp`; after any deploy, verify both the Worker and the\nwebsite manifest/readback path before changing `.well-known/mcp.json`.\n\n`wrangler.jsonc` pins `workers_dev: true` so the public Worker URL stays live during any\nfuture custom-domain experiments; do not remove it unless the website manifest has already\nmoved to a verified replacement endpoint.\n\n## Publish (Layer 2)\n\n```sh\nnpm run build:corpus && npm run build:cli   # -> dist/stdio.js\nnpm login && npm publish                     # public package: saagar-portfolio-mcp\n```\n\nOnce published, anyone can run it locally with `npx saagar-portfolio-mcp` (no install).\n\n## Sign the manifest (optional trust signal)\n\nEd25519-sign `.well-known/mcp.json` so an agent or registry can verify it authentically\ncomes from Saagar (via `saagar-mcp-kit`'s signing CLI, Node built-in crypto):\n\n```sh\nnpx mcp-kit-sign gen-key --manifest=../portfolio-index/.well-known/mcp.json  # one-time; private key -> .signing/ (gitignored, NEVER commit)\nnpm run sign                             # writes <manifest>.sig + publishes mcp-ed25519.pub\nnpm run sign:verify                      # checks manifest bytes against .sig + public key\n```\n\nOverride paths with `--manifest=`/`--key=`/`--pub=`/`--sig=`.\nCommit the `.sig` + `mcp-ed25519.pub` (never the private key) into portfolio-index next to the manifest, then\nredeploy the site. Re-run `sign` whenever the manifest changes (it signs the exact served bytes).\n\n## Audit posture\n\nDesigned to pass `MCPAudit` / `mcp-trust` (Saagar's own tools): only the inbound MCP\ntransport, no `shell_execution` / `file_access` / `destructive` / `exfiltration`, and no\ncaller-controlled egress (the corpus is baked). All tools are annotated read-only with\nplain, non-injectable descriptions. `bash scripts/audit-mcp.sh` runs a connected scan.\n\nDogfooding this server surfaced a substring-matching false-positive bug in MCPAudit (it\nmatched `port` inside `portfolio://`); that fix lives in the MCPAudit repo and cut this\nserver's findings 62 → 14. The genuine tool surface scans clean (`high_risk_servers: 0`).\n\n## Status\n\n- **Built + locally verified:** Layers 0–2. Shared core + 8 tools + Resources + 2 prompts +\n  `get_operant_results`. typecheck clean; test suite passes (incl. full MCP protocol via the\n  fetch handler). Live Worker probe and deploy remain operator-gated. Public discovery\n  advertises `mcp.saagarpatel.dev` with a valid Ed25519-signed manifest.\n- **Gated / next:** publish the stdio package (`npm publish`, after removing\n  `\"private\": true` by explicit operator approval only), glama.ai registry listing, and\n  continued signed-manifest readback checks after website manifest changes.\n",
  "bytes": 7193,
  "sha": "1cb38db25bfd5fdb2139cc0298399fcef00e256e02edc138c8ed1052b3125748",
  "repo_slug": "saagpatel/portfolio-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_saagpatel_portfolio_mcp_62cf8aa4/readme"
}