Skip to main content
Glama
ks342

portfolio-mcp

by ks342
README.md
# portfolio-mcp

A template **[MCP](https://modelcontextprotocol.io) server** that puts your
portfolio — projects, experience, skills, and a search tool — inside any AI
assistant. A recruiter using Claude, Cursor, or ChatGPT adds one line of config
and can then ask questions about your work and get answers from *your* content,
with links back to the real repos and live sites.

No AI runs in this server. It just exposes structured facts; the assistant does
the reasoning.

<p>
  <a href="https://github.com/ks342/desktop-portfolio-mcp/generate">
    <img alt="Use this template" src="https://img.shields.io/badge/Use%20this%20template-2ea44f?style=for-the-badge&logo=github">
  </a>
  <a href="https://render.com/deploy?repo=https://github.com/ks342/desktop-portfolio-mcp">
    <img alt="Deploy to Render" src="https://img.shields.io/badge/Deploy%20to%20Render-46E3B7?style=for-the-badge&logo=render&logoColor=white">
  </a>
</p>

---

## Quick start

```bash
npm install
npm run smoke      # 17 in-memory checks against a real MCP client
npm run dev        # stdio server (what an AI app launches)
npm run inspect    # MCP Inspector — connect and poke it by hand
npm run http       # HTTP version at http://localhost:3000/mcp
```

The repo ships with placeholder content so everything runs green immediately.
Replace it with your own (see [Making it yours](#making-it-yours)).

---

## Connect it to an AI client

**Local (stdio)** — the client launches the server as a subprocess:

```json
{
  "mcpServers": {
    "portfolio": { "command": "npx", "args": ["-y", "tsx", "src/server.ts"] }
  }
}
```

**Deployed (HTTP)** — nothing to install, just a URL:

```json
{
  "mcpServers": {
    "portfolio": { "url": "https://your-server.onrender.com/mcp" }
  }
}
```

### Claude Code

```bash
claude mcp add portfolio -- npx -y tsx src/server.ts
# or, once deployed:
claude mcp add portfolio --transport http https://your-server.onrender.com/mcp
```

Then `/mcp` in a chat to confirm, and ask it something about your work.

---

## What it exposes

### Tools — the assistant calls these

| Tool | Args | Returns |
| --- | --- | --- |
| `get_profile` | — | headline facts, links, availability, timezone |
| `get_bio` | — | prose bio |
| `list_projects` | — | every project: slug, stack, impact, links |
| `get_project` | `slug` | full case study (overview / challenge / solution / results) |
| `get_experience` | — | work history, education, achievements |
| `get_skills` | — | skills grouped by area |
| `search_portfolio` | `query`, `limit?` | best-matching snippets across everything |
| `contact` | `note?` | how to reach you |

Every data tool returns typed JSON (`structuredContent`) alongside a
human-readable text fallback.

### Resources — documents the client can attach

`portfolio://bio` · `portfolio://experience` · `portfolio://skills` ·
`portfolio://projects` (index) · `portfolio://projects/{slug}` (one case study)

### Prompts — the user invokes

`evaluate_candidate(role?)` — loads a brief that asks the assistant to assess you
against a role using the tools above, gaps included.

---

## Making it yours

Everything lives in [`content/`](./content) — the single source of truth. No
code changes needed.

```
content/
  meta.json          name, headline, links, availability, timezone
  bio.md             the prose bio
  experience.json    work / education / achievements
  skills.json        skills by group
  projects/*.md      one file per project (frontmatter + markdown body)
```

1. Edit each file. Keep the frontmatter keys in the project files; the headings
   (`## Overview`, `## Challenge`, …) become individually searchable chunks.
2. Update `name`, `description`, and `keywords` in `package.json`.
3. `npm run smoke` — the checks read your content, so they stay green.
4. Add a project any time: drop a new `content/projects/<slug>.md`, rebuild.
   Every tool, resource, and the search index pick it up automatically.

---

## Deploy the HTTP server

`src/http.ts` runs stateless, so it works on any Node host and on serverless.

```bash
npm run build
node dist/http.js        # listens on $PORT (default 3000), endpoint /mcp
```

- **Render** — click the button above, or: New → Blueprint → pick your fork
  (`render.yaml` is included). Set `SOURCE_URL` to your repo for the landing
  page. Free tier sleeps after ~15 min idle; add an uptime pinger if that
  matters ([`.github/workflows/keep-warm.yml`](./.github/workflows/keep-warm.yml)
  is a starting point).
- **Railway / Fly / a VPS** — start command `node dist/http.js`.
- **Vercel** — wrap the Express app as a serverless function.

The HTTP server has open CORS (it's public, read-only data), a 60 req/min rate
limit, a `/health` endpoint, and a landing page.

---

## Publish to npm (optional)

```bash
npm login                       # free account, 2FA required
npm publish --access public     # `prepublishOnly` builds first
```

`files` in `package.json` ships only `dist/`, `content/`, `README`, and
`LICENSE`. Then clients can use `npx <your-package-name>` directly.

---

## How it's built

- `@modelcontextprotocol/sdk` v1, for the widest client compatibility.
- `src/mcp.ts` defines the server once; `src/server.ts` (stdio) and
  `src/http.ts` (HTTP) both reuse it.
- `src/lib/search.ts` is plain lexical search — no embeddings, no API keys, no
  cost. It expands shorthand (`k8s`, `ts`, `postgres`) and tolerates small
  typos. Swap the internals for embeddings later; the `search(query)` signature
  stays.

[`NOTES.md`](./NOTES.md) is a from-scratch explanation of MCP, JSON-RPC, the
handshake, transports, tools vs resources vs prompts, and a file-by-file tour.

## License

MIT — see [`LICENSE`](./LICENSE).