POC · Model Context Protocol
A small MCP server for this portfolio
Read-only MCP server that exposes the portfolio and blog as a handful of tools any MCP client — Claude Desktop, Cursor, a custom agent — can introspect and call. The whole server is a single Next.js route handler running on the Cloudflare Worker; the tool definitions are in lib/mcp.ts.
Endpoint
Connect to it
URL (Streamable HTTP transport, JSON-RPC 2.0 over POST)
https://kevinshelley.me/api/mcpClaude Desktop config
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json (on macOS):
{
"mcpServers": {
"kevin-portfolio": {
"transport": {
"type": "http",
"url": "https://kevinshelley.me/api/mcp"
}
}
}
}curl smoke test
# Handshake + list tools
curl -sS -X POST https://kevinshelley.me/api/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
curl -sS -X POST https://kevinshelley.me/api/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# Call a tool
curl -sS -X POST https://kevinshelley.me/api/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_profile","arguments":{}}}'User-driven
4 prompts
/evaluate_fitRecruiter / hiring manager evaluation: compare Kevin's portfolio against a job description and return strengths, gaps, interview questions, and a hiring pitch.
role*The role being hired for (e.g. 'Senior Frontend Engineer').job_description*Full job description text, pasted in.focusOptional area to emphasize (e.g. 'leadership', 'performance').
/prep_interviewGenerate grounded interview questions for Kevin in a specific focus area, with what-to-listen-for notes per question.
focus*Interview focus area (e.g. 'system design', 'frontend leadership', 'API design').depthSeniority target — 'junior' | 'mid' | 'senior' | 'principal'. Default 'senior'.countNumber of questions to generate. Default 5.
/pitch_kevinWrite a 2-paragraph third-person pitch tailored to a specific role, grounded in the portfolio data.
role*The role you're pitching Kevin for.audience'recruiter' | 'hiring_manager' | 'team'. Affects tone.length'short' (~80w), 'medium' (~150w, default), or 'long' (~250w).
/chat_with_kevinPersona conversation starter — turns the AI into a grounded portfolio assistant that answers questions about Kevin using only the data the tools return.
topicOptional opening topic or question.persona'recruiter' | 'engineer' | 'mentor'. Tunes the depth/framing.
AI-driven
14 tools
get_profileReturn Kevin's profile basics: name, headline, tagline, location, and contact links.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_summaryReturn the long-form summary paragraphs from Kevin's profile.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_experienceReturn Kevin's work experience, newest first. Each entry has company, role, dates, summary, highlights, and tags.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_projectsReturn Kevin's projects. Optionally filter to `featured` ones or by a tag (case-insensitive substring match).
Input schema
{
"type": "object",
"properties": {
"featured": {
"type": "boolean",
"description": "Only featured projects."
},
"tag": {
"type": "string",
"description": "Filter by tag (substring match)."
}
},
"additionalProperties": false
}get_skillsReturn Kevin's skill categories (Languages, Frontend, Backend, Data, Cloud & DevOps) with the skills in each.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_educationReturn Kevin's education history.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_certificationsReturn Kevin's certifications, newest first.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_testimonialsReturn recommendations from people Kevin has worked with.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}list_tailored_portfoliosList Kevin's tailored portfolio variants (each highlights a different role/audience). Each result has `slug`, `label`, and `audience`.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_tailored_portfolioReturn a tailored portfolio (curated experience/projects/skills) by slug. Use `list_tailored_portfolios` to see slugs.
Input schema
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Tailored portfolio slug."
}
},
"required": [
"slug"
],
"additionalProperties": false
}list_blog_postsList Kevin's blog posts, newest first. Each summary has slug, title, date, excerpt, and tags. Optional `tag` filter.
Input schema
{
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "Only posts with this tag."
}
},
"additionalProperties": false
}get_blog_postReturn the full body of a blog post by slug (Markdown source).
Input schema
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "The post's URL slug."
}
},
"required": [
"slug"
],
"additionalProperties": false
}get_resume_jsonReturn Kevin's resume in JSON Resume v1.0.0 format. Optional `tailoredSlug` returns the role-specific version (use `list_tailored_portfolios` first).
Input schema
{
"type": "object",
"properties": {
"tailoredSlug": {
"type": "string",
"description": "Tailored portfolio slug for a role-specific resume."
}
},
"additionalProperties": false
}tailor_resume_to_jdGiven a job description, pick the best-matching tailored portfolio variant by tag/skill overlap and return a JSON Resume tailored for it. Result includes the matched keywords and the recommended slug so the AI can explain its reasoning.
Input schema
{
"type": "object",
"properties": {
"job_description": {
"type": "string",
"description": "The full job description text."
}
},
"required": [
"job_description"
],
"additionalProperties": false
}Auth
Bearer token for write tools
Authorization: Bearer … header matching the server's MCP_API_TOKEN secret.Configure your MCP client (Claude Desktop shown):
{
"mcpServers": {
"kevin-portfolio": {
"transport": {
"type": "http",
"url": "https://kevinshelley.me/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN_HERE"
}
}
}
}
}On the server side, set the token with wrangler secret put MCP_API_TOKEN. Without it, every auth-gated tool is permanently unavailable — the server fails closed.
Discovery
.well-known manifest
https://kevinshelley.me/.well-known/mcp.jsonThe /.well-known/ URL pattern is RFC 8615; MCP discovery is not yet standardized, but the manifest follows a draft convention used by emerging tooling.
Notes
Caveats for a POC
• Mostly read-only. Write tools (e.g. publish_post) require a bearer token and are hidden from unauthenticated clients entirely.
• No SSE. The Streamable HTTP transport supports server-sent events for push notifications; we don't need them for a stateless tool server, so GET on the endpoint returns 405.
• No batching. Batched JSON-RPC requests would just loop the dispatcher; left unimplemented because no client we care about needs it.