Portfolio MCP Server
Exposes a personal portfolio of projects, skills, and resume as callable tools for MCP-compatible AI assistants like Claude Desktop.
README
Portfolio MCP Server
An MCP (Model Context Protocol) server that exposes Cheng-Yun Wu's portfolio — projects, skills, and resume — as tools any MCP-compatible AI assistant (Claude Desktop, Claude.ai Connectors, MCP Inspector, etc.) can call directly, instead of scraping a website.
Why this exists
I wanted to actually understand how MCP works end to end, not just read about it — so I built a small server that turns my portfolio site's content into structured tools. It's also a deliberate excuse to pick up two things I hadn't touched much before: Docker and a basic CI/CD pipeline, both of which show up repeatedly in job postings I'm targeting.
What is MCP, briefly
MCP is an open protocol (from Anthropic) that lets an AI assistant call external "tools" — typed functions with a name, a description, and a schema — to fetch live information or take actions, instead of relying only on what's in its training data or a pasted document. A server declares its tools; any MCP-aware client can discover and call them. This project is one such server: it declares four tools backed by my own portfolio data.
Tools
| Tool | What it does |
|---|---|
list_projects() |
Every portfolio item — shipped systems, competition entries, research projects, published papers, and course reports, not just the flagship case studies — with id, name, tagline, category, year, one-line summary, and its links right in the listing (live system, GitHub, report, demo video, etc.) |
get_project_details(name) |
Full record for one item. For a flagship project: role, tech stack, problem, challenges & solutions, outcome, links. For a lighter item: whatever's on file — at minimum a description and links. Matching is forgiving and alias-aware ("lab handover" → ifit-lab-handover; "NTPU OPE Assistant" → the thesis system it actually is) |
search_skills(keyword) |
Keyword search across the skills taxonomy, ranked by relevance, each result naming the projects that demonstrate it |
get_resume_summary(length) |
Self-introduction at "short" / "medium" / "long", plus contact info |
Each tool's docstring is what the AI assistant actually reads to decide when to call it — see src/portfolio_mcp/server.py.
Coverage: data/projects.json holds all 31 portfolio items — 7 deep-dive case studies (shipped systems, the thesis, the NSTC research project, an award paper) plus 24 lighter entries (other competition entries, course reports, conference papers). Every single one carries at least one link. Course-stage reports and companion papers carry a related_project id pointing at the fuller case study they belong to, so an assistant can drill from a report into the full story.
Architecture
Claude Desktop / Claude.ai / MCP Inspector
│ (stdio locally, or Streamable HTTP remotely)
▼
MCPServer instance (server.py)
│ registers 4 tools
▼
tools.py (pure, unit-tested logic)
│
▼
data_loader.py → data/*.json (projects, skills, resume)
- Transport: Streamable HTTP, not stdio — the point is that a remote client (e.g. Claude.ai's Connectors) can reach this server over a public URL, not just a locally-spawned process. Stdio is still supported for local Claude Desktop / MCP Inspector testing.
- Data layer: three flat JSON files under
data/, loaded once and cached (functools.lru_cache). No database — the data is small, public, and changes rarely. - Tool logic vs. MCP wiring: kept separate on purpose (
tools.pyvs.server.py) so the logic is unit-testable without a running MCP server or transport. - SDK note: the official
mcpPython SDK moved its high-level server API fromFastMCPtomcp.server.mcpserver.MCPServerat v2.0.0 — this project targetsmcp>=2.0.0and that current API. If you've seen older MCP tutorials usingfrom mcp.server.fastmcp import FastMCP, that's the pre-2.0 API and won't import against whatpip install mcpgives you today.
Project structure
portfolio-mcp-server/
├── data/ # projects.json, skills.json, resume.json
├── src/portfolio_mcp/
│ ├── server.py # MCPServer app: registers tools, stdio/HTTP entrypoints, /chat route
│ ├── tools.py # MCP tool logic (testable, no MCP dependency)
│ ├── chat.py # /chat: Claude + Tool Runner over the same data, for the site's Q&A widget
│ └── data_loader.py # cached JSON loading
├── tests/ # pytest suite run in CI (tools, server security, chat, chat route)
├── Dockerfile # python:3.12-slim + uvicorn, Streamable HTTP
├── .github/workflows/ci.yml # lint (ruff) + test (pytest) on every push
└── claude_desktop_config.json # example config for local stdio testing
Running locally
# from the repo root
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
Option A — stdio, with MCP Inspector
npx @modelcontextprotocol/inspector python -m portfolio_mcp.server
Opens a local web UI where you can call each tool directly and inspect the request/response.
Option B — stdio, with Claude Desktop
Merge the mcpServers entry from claude_desktop_config.json into your own Claude Desktop config (Settings → Developer → Edit Config), fixing the paths for your machine, then restart Claude Desktop and ask something like "What projects has this person worked on?"
Option C — Streamable HTTP, locally
TRANSPORT=http python -m portfolio_mcp.server
# equivalent — both serve the exact same ASGI app, /chat included:
uvicorn portfolio_mcp.server:app --host 0.0.0.0 --port 8000
Tests
pytest -v
ruff check .
Running with Docker
docker build -t portfolio-mcp-server .
docker run -p 8000:8000 portfolio-mcp-server
The container always serves Streamable HTTP (that's the point of containerizing it — a portable, publicly-servable unit, not a stdio process tied to one machine).
Deploying (Render)
Chosen deployment target: Render, free tier — it runs long-lived containers (not serverless functions with execution-time limits), which Streamable HTTP's persistent connections need, and it needs no credit card to start.
- Push this repo to GitHub.
- On render.com: New → Web Service → connect this repo.
- Render auto-detects the
Dockerfileand builds/runs it as a container. - Pick the Free instance type → you get a
https://<something>.onrender.comURL. - Verify it's live:
npx @modelcontextprotocol/inspector https://<something>.onrender.com/mcp - (Optional) Enable Render's GitHub auto-deploy so
git pushtomainredeploys automatically — combined with the CI workflow below, that's the full CI/CD story.
Free-tier note: Render's free web services sleep after ~15 minutes idle and take 30-60s to wake on the next request. Fine for a portfolio demo; worth mentioning as a deliberate cost/latency trade-off if asked.
Live deployment: https://yun-portfolio-mcp.onrender.com/mcp — connect an MCP client to this URL (note the /mcp path; the bare domain 404s, that's expected — Streamable HTTP only serves that one path). Verify it yourself with npx @modelcontextprotocol/inspector https://yun-portfolio-mcp.onrender.com/mcp.
If you fork this: the Host-header allowlist in
server.pyis hardcoded toyun-portfolio-mcp.onrender.comby default (DNS-rebinding protection rejects any other Host header with a 421). Set theMCP_ALLOWED_HOSTSenv var to your own deployment's hostname, or editALLOWED_HOSTSdirectly.
Chat endpoint (/chat) — the portfolio site's Q&A widget
A second, separate door on the same Render service, for a plain chat widget embedded on yunwcy.github.io — not part of the MCP protocol surface above. A browser POSTs {"message": "..."} to /chat; the server uses the Anthropic Tool Runner to let Claude decide which of the same four tools to call (by calling tools.py directly — no MCP handshake involved), then returns {"reply": "..."}. See src/portfolio_mcp/chat.py for the full implementation.
Why this needs a real backend, and GitHub Pages alone can't do it: answering in natural language means an LLM has to see the question and decide which tool to call, which needs an Anthropic API key — and a key can never go in client-side JS on a static site, since anyone can view-source it and burn the account. /chat keeps the key server-side (a Render environment variable, never sent to the browser) and only ships the widget the browser needs down to GitHub Pages.
Setup (required before this endpoint works):
- Get an API key from the Anthropic Console and add it to Render as the
ANTHROPIC_API_KEYenvironment variable (Render dashboard → this service → Environment). Without it,/chatreturns503 {"error": "not_configured"}rather than crashing the server. CHAT_ALLOWED_ORIGINS(comma-separated) controls CORS — defaults tohttps://yunwcy.github.io. Set it if the widget ever lives somewhere else.ANTHROPIC_CHAT_MODEL(defaultclaude-opus-5) — the strongest general-purpose choice, but this is a simple, potentially high-volume, cost-sensitive public widget, soclaude-haiku-4-5is worth considering here specifically. This is a deliberate choice left to whoever runs the server, not hardcoded.CHAT_RATE_LIMIT_PER_HOUR(default30) — a simple in-memory per-IP cap so one visitor can't run up the bill alone. It resets on every restart/redeploy and isn't shared across instances — enough for a low-traffic personal site, not a general abuse defense.
CI/CD
.github/workflows/ci.yml runs on every push/PR to main: installs the package, lints with ruff, and runs the pytest suite. Render's GitHub auto-deploy (see above) handles the CD half.
Security / cost notes
- The MCP tool surface (
/mcp) does not call any LLM itself — it only reads local JSON and returns it. Whoever connects to it (their Claude, their tokens) bears that cost, not this server. - The
/chatendpoint does call an LLM, using this server's own Anthropic API key — that's the whole point (a browser can't hold a key safely). Cost is bounded by a per-IP rate limit,effort: "low", and a smallmax_tokens; see the Chat endpoint section above for the knobs. - All data is already public on my portfolio site — no auth is implemented on either endpoint, since there's nothing private to protect.
/chat's CORS allowlist exists to control who can spend the API budget, not to protect the data.
Updating the data
Edit the JSON files under data/ directly — id is the stable identifier get_project_details matches against; every other field is free-form. No code changes needed for content updates.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。