Case study
LivingCV: the portfolio that agents can read
The self-hosted platform behind mattdas.dev, with a public read-only MCP endpoint for recruiter agents and an authenticated admin endpoint that maintains the site.
Highlights
- Public read-only MCP endpoint for recruiter-side agents
- Server-side tool allow-listing as the injection defense
- This case study was written into the database by an AI agent over MCP
The premise
Recruiters increasingly read candidates through AI agents, not just browsers. A static portfolio serves the human and leaves the agent scraping HTML. LivingCV serves both natively: the site you are reading, plus a public MCP endpoint exposing the same projects and case studies as structured tools an agent can query directly.
Two endpoints, two trust levels
The public concierge endpoint is read-only by construction. The defense against prompt injection is not a system prompt asking the model to behave; it is server-side tool allow-listing, where the public surface simply contains no tool capable of writing or reaching private data. A separate authenticated admin endpoint (bearer master key) exposes the full surface, including raw SQL against the portfolio database. Secrets in the settings table are AES-encrypted so even admin-level SQL cannot usefully read them.
That admin endpoint is how this page exists: the project rows and this case study were written into the SQLite database by an AI agent in a chat session, over MCP, against the live site. The platform maintaining itself through its own API is the point.
Under the hood
A Next.js monorepo serves the site; an Express orchestrator handles the MCP transports and the concierge, with multi-provider LLM support so the assistant is not tied to one vendor. State is a single SQLite database: projects, case studies, site content, and templates, each with JSON columns the renderer parses into cards and sections.
The reliability story
The honest part: the orchestrator used to die silently. An Express 4 async route handler awaited an MCP message without a try/catch, so any processing error became an unhandled promise rejection, which in modern Node silently kills the process. With no supervisor, the server stayed down and every client call hung until timeout. The fix was boring and correct: catch at the route, global handlers that log to a crash file and exit nonzero, all console output routed to stderr so stdio transports cannot be corrupted, a keepalive wrapper that respawns on crash, and a health script that performs a real MCP handshake and query rather than pinging a port. Distributed-systems lessons apply at portfolio scale too.