🔌 Featured Case Study · Developer Tool

Portfolio MCP Server

This whole portfolio, exposed as tools any MCP-compatible AI assistant can query directly
Solo project · Built 2026 · Live remote MCP server, no login required
Problem Architecture Tech Stack Try It Live Sample Questions Trade-offs My Role Challenges Demo

Problem Background

System Architecture

Claude Desktop local install, runs locally Claude.ai Connector the one you'd actually use MCP Inspector / Client SDK used for dev-time testing stdio standard I/O, local only Streamable HTTP · POST/GET /mcp a public URL, any remote client can connect MCPServer instance server.py · registers 4 tools, handles the protocol handshake tools.py plain logic, no MCP dependency, unit-testable on its own data_loader.py lru_cache: read from disk once per process, not once per request projects.json skills.json resume.json JSON-RPC over HTTP JSON-RPC over stdio calls the matching tool function reads data
Four layers: any MCP client connects over stdio (local) or Streamable HTTP (remote) to one MCPServer instance, which routes to plain, testable Python functions that read three JSON data files. Client and transport can vary, but everything underneath is the same code — local testing and the live deployment run the exact same logic.

Tech Stack

Protocol & Transport

MCP Python SDK stdio Streamable HTTP

Application Logic

4 registered tools Docstring-driven routing Alias-aware fuzzy match

Infrastructure

Docker Render GitHub Actions

Data

Flat JSON files In-process cache
FROM python:3.12-slim WORKDIR /app COPY pyproject.toml README.md ./ COPY src ./src RUN pip install --no-cache-dir -e . COPY data ./data ENV TRANSPORT=http HOST=0.0.0.0 PORT=8000 EXPOSE 8000 CMD uvicorn portfolio_mcp.server:app --host 0.0.0.0 --port $PORT docker build -t portfolio-mcp-server . 🐳 Image (immutable snapshot) docker run -p 8000:8000 \ portfolio-mcp-server Render: auto build + deploy production, $PORT injected by Render for local verification
The Dockerfile runs top to bottom and produces one immutable image. That same image is verified locally with docker run, then Render builds and ships it — both sides run the exact same thing.
git push origin main GitHub Actions (CI) checkout set up Python 3.12 pip install -e .[dev] ruff check pytest Render (Auto-Deploy) GitHub webhook docker build start new container health check switch traffic, retire old container ⚠️ These two pipelines are independent — neither waits on the other A red CI run does not stop Render from deploying the new version anyway — right now that's guarded by habit (test locally before pushing), not by the pipeline itself
One git push triggers two pipelines: GitHub Actions only reports whether tests pass. What actually ships the new version is Render's own webhook — the two don't gate each other.

Try It Live

This isn't a normal website — opening the live URL directly in a browser shows "Not Found", and that's expected. It's a protocol endpoint, not a page: an MCP client has to speak the Streamable HTTP protocol to it, not just visit the URL. Here are three ways to actually try it:

Claude.ai — Custom Connector

Settings → Connectors → Add custom connector → paste https://yun-portfolio-mcp.onrender.com/mcp → enable it from the + menu in any chat, then just ask a question about my work.

MCP Inspector (no install needed)

npx @modelcontextprotocol/inspector https://yun-portfolio-mcp.onrender.com/mcp — opens a local web page where you can call each tool directly and see the raw request and response.

Claude Desktop (local + source)

Clone the repo, follow the README to run it over stdio, and add it to your own claude_desktop_config.json.

Free-tier note: the server falls asleep after about 15 minutes idle, and the first request after that takes 30–60 seconds to wake it back up. That's a deliberate cost trade-off for a demo project, not a bug — if the first question seems slow or times out, just ask again.
Client Server (Docker container on Render) ① POST /mcp { method: "initialize" } 🔒 Host header allowlist check (hit this once, see Challenges) ② capabilities + serverInfo ③ tools/list ④ returns all 4 tools' name + schema ⑤ tools/call { name: "list_projects" } internally calls tools.py → data_loader.py ⑥ 31 content blocks (one per project) Claude receives the result and starts writing an answer
Asking "what projects has this person worked on?" is actually six protocol round-trips under the hood — not one single request-response.

Try Asking

"What projects has this person worked on?"
"Has she built anything with RAG or vector search?"
"Tell me about her master's thesis."
"Does she have any iOS development experience?"
"Give me a one-paragraph summary of her background."
"What's IM Your Buddy, and what was her role in it?"

Engineering Trade-offs

Cost
    Speed
      Accuracy
        Stability

          My Role

          Solo Developer

          Built entirely on my own, end to end:

          Challenges & Solutions

          1 The MCP SDK's API had already moved on from the tutorials

          Problem

          Solution

          2 The Docker image couldn't find its own data files

          Problem

          Solution

          3 The live deployment returned 421 "Invalid Host header"

          Problem

          Solution

          It Actually Works

          Two real questions asked through the Claude.ai Connector, answered live from the MCP server, not staged screenshots.

          Asking whether this person has iOS development experience, answered via the MCP connector
          Asking what this person's featured projects are, answered via the MCP connector