Skip to main content
Glama
Portfolio MCP Server

Portfolio MCP Server

by Yunwcy

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.

Related MCP server: Bijon Portfolio MCP Server

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 33 portfolio items — 8 deep-dive case studies (shipped systems, this MCP server itself, the thesis, the NSTC research project, an award paper) plus 25 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.py vs. server.py) so the logic is unit-testable without a running MCP server or transport.

  • SDK note: the official mcp Python SDK moved its high-level server API from FastMCP to mcp.server.mcpserver.MCPServer at v2.0.0 — this project targets mcp>=2.0.0 and that current API. If you've seen older MCP tutorials using from mcp.server.fastmcp import FastMCP, that's the pre-2.0 API and won't import against what pip install mcp gives 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: OpenAI Responses API + a small tool-calling loop 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.

  1. Push this repo to GitHub.

  2. On render.com: New → Web Service → connect this repo.

  3. Render auto-detects the Dockerfile and builds/runs it as a container.

  4. Pick the Free instance type → you get a https://<something>.onrender.com URL.

  5. Verify it's live:

    npx @modelcontextprotocol/inspector https://<something>.onrender.com/mcp
  6. (Optional) Enable Render's GitHub auto-deploy so git push to main redeploys 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.py is hardcoded to yun-portfolio-mcp.onrender.com by default (DNS-rebinding protection rejects any other Host header with a 421). Set the MCP_ALLOWED_HOSTS env var to your own deployment's hostname, or edit ALLOWED_HOSTS directly.

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 OpenAI's Responses API (with a small hand-written tool-calling loop, since the Responses API has no built-in agent-loop helper) to let the model 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 OpenAI 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):

  1. Get an API key from the OpenAI Platform dashboard and add it to Render as the OPENAI_API_KEY environment variable (Render dashboard → this service → Environment). Without it, /chat returns 503 {"error": "not_configured"} rather than crashing the server.

  2. CHAT_ALLOWED_ORIGINS (comma-separated) controls CORS — defaults to https://yunwcy.github.io. Set it if the widget ever lives somewhere else.

  3. OPENAI_CHAT_MODEL (default gpt-5.6) — a solid general-purpose choice, but this is a simple, potentially high-volume, cost-sensitive public widget, so a cheaper nano/mini-tier model is worth considering here specifically (check platform.openai.com/docs/pricing for the current cheapest option — model names and pricing shift often). This is a deliberate choice left to whoever runs the server, not hardcoded.

  4. CHAT_RATE_LIMIT_PER_HOUR (default 30) — 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 /chat endpoint does call an LLM, using this server's own OpenAI API key — that's the whole point (a browser can't hold a key safely). Cost is bounded by a per-IP rate limit, a small max_output_tokens, and a capped tool-calling loop; 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.

Available Tools

4 tools
get_project_detailsA

Get the full record for one portfolio item. For a flagship project this includes role, tech stack, the problem it solved, challenges and how they were solved, outcomes, and links; for a lighter item (a course report, a smaller competition entry) it returns whatever is on file — at minimum a description and its links.

Args: name: A project name, id, or known alias/alternate name — e.g. "IM Your Buddy", "knovyra", "lab handover", "NTPU OPE Assistant", or a competition name like "North Taiwan University Alliance AI Agent Competition". Matching is forgiving (case-insensitive, partial, alias-aware), so you don't need the exact id from list_projects — but calling list_projects first helps pick the right one when unsure.

Inputs: name (required).

get_resume_summaryA

Get a self-introduction / resume summary, plus name, title, and contact info. Use this to answer "tell me about yourself" or "give me a summary of this person's background" style questions.

Args: length: "short" for 1-2 sentences, "medium" for a paragraph, or "long" for a full narrative summary covering research, shipped projects, publications, and certifications. Defaults to "short".

Inputs: length.

list_projectsA

List every item in the portfolio — shipped systems, competition entries, research projects, published papers, and course reports, not just the flagship case studies — with id, name, tagline, category, year, a one-sentence summary, and its links (live system, GitHub, report, demo video, etc., whichever apply). Links are included right here, so a system or report can be pointed to without a second call. An entry's related_project (when present) is the id of a fuller case study it's a stage or companion piece of — pass that id to get_project_details for the deep-dive version. Call this first for any broad question like "what has this person worked on?" or "does a system exist for X?".

Inputs: no named parameters. Returns: result

search_skillsA

Search the skills/technology taxonomy by keyword and return matches ranked by relevance, each with the projects that demonstrate it. Use this to answer questions like "does this person know RAG / Docker / vector databases / iOS development?".

Args: keyword: A skill, technology, or category to search for, e.g. "RAG", "Docker", "vector database", "iOS", "Next.js".

Inputs: keyword (required). Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.6/5.0

    Scored across 4 tools

    Disambiguation5/5

    Each tool has a distinct, well-defined purpose. list_projects provides an overview, get_project_details provides deep dives on individual entries, search_skills queries the technology taxonomy, and get_resume_summary returns background info. There is no overlap or ambiguity between any of these tools.

    Naming Consistency5/5

    All tool names follow a consistent verb_noun pattern (list_projects, get_project_details, search_skills, get_resume_summary). The naming clearly indicates what action is being taken and on what resource, making the API predictable and easy to navigate.

    Tool Count5/5

    With exactly 4 tools covering portfolio browsing, detail retrieval, skill search, and resume summary, the number is well-scoped for a personal portfolio MCP server. No tools are missing, and every tool serves a distinct, necessary function without redundancy.

    Completeness5/5

    The tool set provides a complete coverage of the portfolio domain: listing all entries, retrieving full details for any entry, searching across skills/tags, and providing a professional summary. There are no obvious gaps—a user can explore projects, drill into details, assess expertise, and get background information.

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      Exposes personal portfolio data as tools for Claude to answer questions about the developer, including profile, skills, experience, projects, and contact information.
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      Exposes a structured professional resume as a set of AI-queryable tools, enabling AI clients like Claude Desktop to query summary, experience, skills, projects, and tailor resumes to job descriptions.
      1
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      MCP server that exposes a resume as callable tools and resources, enabling AI agents to query experience, skills, projects, and contact information via natural language.
      3
      ISC