Skip to content

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/mcp

Claude 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

Prompts are templates a user picks from their MCP client's slash menu (Claude Desktop, Cursor, etc.). The client renders the argument form, materializes the instructions, and the AI receives a fully-framed conversation starter — grounded in the tools below.
/evaluate_fit

Recruiter / 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_interview

Generate 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_kevin

Write 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_kevin

Persona 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

Tools are functions the AI invokes on its own — typically when responding to a question that needs grounded data. The prompts above orchestrate the tools below.
get_profile

Return Kevin's profile basics: name, headline, tagline, location, and contact links.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_summary

Return the long-form summary paragraphs from Kevin's profile.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_experience

Return Kevin's work experience, newest first. Each entry has company, role, dates, summary, highlights, and tags.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_projects

Return 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_skills

Return Kevin's skill categories (Languages, Frontend, Backend, Data, Cloud & DevOps) with the skills in each.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_education

Return Kevin's education history.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_certifications

Return Kevin's certifications, newest first.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_testimonials

Return recommendations from people Kevin has worked with.

Input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
list_tailored_portfolios

List 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_portfolio

Return 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_posts

List 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_post

Return 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_json

Return 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_jd

Given 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

Auth-gated tools (marked Auth required above) need an 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

Forward-looking clients can fetch a machine-readable manifest to auto-discover this server's endpoint, capabilities, and auth scheme — no client-side config copy-paste.
https://kevinshelley.me/.well-known/mcp.json

The /.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.