# saagarpatel.dev Portfolio [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/saagpatel/portfolio-mcp  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/saagarpatel-dev-portfolio

## Description
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "saagarpatel-dev-portfolio": {
    "command": "npx",
    "args": ["-y","saagar-portfolio-mcp"]
  }
}
```

## Documentation & README

# portfolio-mcp

[![Glama MCP server](https://glama.ai/mcp/servers/saagpatel/portfolio-mcp/badge)](https://glama.ai/mcp/servers/saagpatel/portfolio-mcp)

The agent-native layer of [saagarpatel.dev](https://saagarpatel.dev): a Model
Context Protocol server that lets any AI agent query Saagar's writing, projects,
public-safe repo profiles, and benchmark results directly, instead of scraping HTML.

Read-only. Stateless. Public. No auth, no tracking, no database, no runtime egress.

## How it fits

The website stays a pure static site. This server is a **sibling**, not a backend
bolted onto it:

- **Layer 0** (in the `portfolio-index` repo): the build emits a static machine
  corpus — `corpus-index.json`, per-document `corpus/<id>.json`, and
  `.well-known/mcp.json` — plus public-safe repo profile artifacts when present,
  served alongside the HTML. Already public.
- **Layer 1** (this repo, `src/index.ts`): a stateless Cloudflare Worker that bakes
  the Layer 0 corpus into its bundle and serves it over MCP (streamable HTTP, the
  `WebStandardStreamableHTTPServerTransport`). Zero runtime fetches.
- **Layer 2** (this repo, `src/stdio.ts`): the same server over stdio, for running
  locally via `npx saagar-portfolio-mcp`. Identical tool/resource/prompt surface.

The shared core (`src/server.ts`) is transport-agnostic; both layers wrap it.

## Tool surface

All read-only (`readOnlyHint: true`). No tool takes a URL or filesystem path (no
SSRF / exfil surface).

| Tool | Purpose |
|---|---|
| `search` | BM25 over the whole corpus; optional `section` filter, `limit` |
| `get_document` | Full Markdown of one document by `id` |
| `list_corpus` | The table of contents; optional `type` filter |
| `get_profile` | The "who is this" card (about / now / uses) |
| `list_projects` | Curated public-safe projects + anonymized aggregates |
| `list_repo_profiles` | Public-safe repo profile index with freshness and proof counts |
| `get_repo_profile` | One repo answering profile by `repo_id` |
| `get_operant_results` | Public, sanitized OPERANT calibration results (per-model OCS) |

Documents are also exposed as **Resources** (`portfolio://essays/{slug}`, `book/{slug}`,
`notes/{slug}`, `portfolio://profile`), and there are two **Prompts**:
`introduce_saagar` and `summarize_writing_on` (grounded in a live `search`).

## Retrieval

BM25 over a baked index (no embeddings in v1 — the corpus is ~50 small docs and the
calling LLM supplies the semantics). Titles are boosted. Embeddings are a measured
Phase 3 upgrade, added only if retrieval quality proves insufficient.

## Layout

```
src/
  types.ts            corpus + projects + operant shapes
  bm25.ts             dependency-free BM25 + snippet (pure)
  tools.ts            createTools(corpus) -> the 8 tools (pure, injectable)
  corpus.ts           loads the baked corpus + accessors
  corpus.generated.ts AUTO-GENERATED by build:corpus
  server.ts           buildServer(): shared MCP core (tools + resources + prompts)
  index.ts            Cloudflare Worker transport (streamable HTTP)
  stdio.ts            Layer 2 stdio transport (the npx CLI)
scripts/
  build-corpus.mjs    bakes Layer 0 (+ OPERANT) into corpus.generated.ts
  probe-mcp.mjs        probes an MCP HTTP endpoint (saagar-mcp-kit driver + domain calls)
  audit-mcp.sh        connected MCPAudit scan of this server (dogfood)
test/                 vitest: bm25, tools, full-protocol server tests
```

## Develop

```sh
npm install
npm run build:corpus          # bake from ../portfolio-index (or --url=https://saagarpatel.dev)
npm run typecheck
npm test
npm run dev                   # wrangler dev -> http://localhost:8787/mcp
npm run smoke                 # end-to-end MCP smoke under the real workerd runtime (saagar-mcp-kit)
npm run probe:mcp             # live Worker probe, or set PORTFOLIO_MCP_ENDPOINT
```

Inspect either transport with the MCP inspector:

```sh
npx @modelcontextprotocol/inspector http://localhost:8787/mcp   # Layer 1 (HTTP)
npx @modelcontextprotocol/inspector node dist/stdio.js          # Layer 2 (stdio, after build:cli)
```

## Deploy (Layer 1)

```sh
npm run build:corpus && npm run deploy   # wrangler deploy
npm run probe:mcp                        # post-deploy live MCP readback
```

Operator-gated (needs Cloudflare auth). v1 still deploys to the default
`portfolio-mcp.<account>.workers.dev` URL, and `npm run probe:mcp` uses that stable
Worker URL by default. Public discovery now advertises the verified custom endpoint
`https://mcp.saagarpatel.dev/mcp`; after any deploy, verify both the Worker and the
website manifest/readback path before changing `.well-known/mcp.json`.

`wrangler.jsonc` pins `workers_dev: true` so the public Worker URL stays live during any
future custom-domain experiments; do not remove it unless the website manifest has already
moved to a verified replacement endpoint.

## Publish (Layer 2)

```sh
npm run build:corpus && npm run build:cli   # -> dist/stdio.js
npm login && npm publish                     # public package: saagar-portfolio-mcp
```

Once published, anyone can run it locally with `npx saagar-portfolio-mcp` (no install).

## Sign the manifest (optional trust signal)

Ed25519-sign `.well-known/mcp.json` so an agent or registry can verify it authentically
comes from Saagar (via `saagar-mcp-kit`'s signing CLI, Node built-in crypto):

```sh
npx mcp-kit-sign gen-key --manifest=../portfolio-index/.well-known/mcp.json  # one-time; private key -> .signing/ (gitignored, NEVER commit)
npm run sign                             # writes <manifest>.sig + publishes mcp-ed25519.pub
npm run sign:verify                      # checks manifest bytes against .sig + public key
```

Override paths with `--manifest=`/`--key=`/`--pub=`/`--sig=`.
Commit the `.sig` + `mcp-ed25519.pub` (never the private key) into portfolio-index next to the manifest, then
redeploy the site. Re-run `sign` whenever the manifest changes (it signs the exact served bytes).

## Audit posture

Designed to pass `MCPAudit` / `mcp-trust` (Saagar's own tools): only the inbound MCP
transport, no `shell_execution` / `file_access` / `destructive` / `exfiltration`, and no
caller-controlled egress (the corpus is baked). All tools are annotated read-only with
plain, non-injectable descriptions. `bash scripts/audit-mcp.sh` runs a connected scan.

Dogfooding this server surfaced a substring-matching false-positive bug in MCPAudit (it
matched `port` inside `portfolio://`); that fix lives in the MCPAudit repo and cut this
server's findings 62 → 14. The genuine tool surface scans clean (`high_risk_servers: 0`).

## Status

- **Built + locally verified:** Layers 0–2. Shared core + 8 tools + Resources + 2 prompts +
  `get_operant_results`. typecheck clean; test suite passes (incl. full MCP protocol via the
  fetch handler). Live Worker probe and deploy remain operator-gated. Public discovery
  advertises `mcp.saagarpatel.dev` with a valid Ed25519-signed manifest.
- **Gated / next:** publish the stdio package (`npm publish`, after removing
  `"private": true` by explicit operator approval only), glama.ai registry listing, and
  continued signed-manifest readback checks after website manifest changes.

