Skip to main content
Glama

portfolio-mcp

A production-ready Model Context Protocol server exposing Karthikeyan K's portfolio — profile, skills, experience, education, projects, certifications, and resume — as MCP tools, resources, and prompts, so ChatGPT, Claude, and any other MCP-compatible client can query it directly instead of browsing the website.

It also ships Gemini-powered AI tools for job matching: compare a job description against the profile, estimate an ATS score, generate interview questions, get a learning plan, and draft tailored summaries.

Built with the official @modelcontextprotocol/sdk, TypeScript, and Zod, deployed on Cloudflare Workers.

Architecture

                          ┌─────────────────────────┐
                          │   MCP Client            │
                          │ (ChatGPT / Claude /      │
                          │  MCP Inspector)          │
                          └───────────┬─────────────┘
                                      │ JSON-RPC over
                                      │ Streamable HTTP (POST /mcp)
                                      ▼
        ┌───────────────────────────────────────────────────────┐
        │                Cloudflare Worker (src/index.ts)        │
        │  routes: POST /mcp · GET /health · /version · /metrics │
        └───────────────────────┬─────────────────────────────────┘
                                 │ per-request
                                 ▼
        ┌───────────────────────────────────────────────────────┐
        │      StatelessHttpTransport (src/transport/)           │
        │  one JSON-RPC message in → one response out            │
        └───────────────────────┬─────────────────────────────────┘
                                 ▼
        ┌───────────────────────────────────────────────────────┐
        │            McpServer (src/server.ts)                   │
        │  ┌───────────┐ ┌────────────┐ ┌──────────────────┐    │
        │  │  tools/   │ │ resources/ │ │     prompts/      │    │
        │  │ 23 tools  │ │ 8 resources│ │    7 prompts       │    │
        │  └─────┬─────┘ └─────┬──────┘ └─────────┬──────────┘    │
        │        └─────────────┴──────────────────┘               │
        │                      ▼                                  │
        │  ┌───────────────────────────────────────────────────┐  │
        │  │                  services/                        │  │
        │  │  data.ts (JSON → Zod) · gemini.ts · github.ts ·    │  │
        │  │  search.ts (keyword + optional embeddings) ·       │  │
        │  │  cache.ts (in-memory TTL) · metrics.ts             │  │
        │  └───────────────────────────────────────────────────┘  │
        └───────────────────────────────────────────────────────┘
                    ▲                              ▲
                    │                              │
         Gemini API (generateContent,      GitHub REST + GraphQL
         embedContent) — optional,         (repo metadata always;
         GEMINI_API_KEY                    pinned repos / contributions
                                            need GITHUB_TOKEN)

For local development, src/local.ts connects the same McpServer (from src/server.ts) over stdio instead of HTTP — no Worker or network required.

Related MCP server: mcp-me

Folder structure

portfolio-mcp/
  src/
    index.ts                    # Cloudflare Worker fetch entry (HTTP routes)
    local.ts                    # stdio entry point for local MCP clients
    server.ts                   # createMcpServer(env) — registers everything
    transport/
      statelessHttpTransport.ts # MCP Transport adapter for one-shot HTTP
    tools/                      # one module per domain, one per tool group
      portfolio.ts  skills.ts  experience.ts  education.ts
      projects.ts   certifications.ts  resume.ts  ai.ts
    resources/index.ts          # 8 resources reading from data/*.json
    prompts/index.ts            # 7 reusable prompt templates
    services/
      data.ts        # loads + Zod-validates data/*.json, builds resume markdown
      gemini.ts       # Gemini REST client (generate, embed)
      github.ts       # GitHub REST + GraphQL client (repo/pinned/contributions)
      search.ts       # keyword scoring + optional embedding blend
      cache.ts        # in-memory TTL cache
      metrics.ts       # per-isolate tool call counters
    utils/
      logger.ts  errors.ts  sanitize.ts  pagination.ts  config.ts
    types/
      env.ts  portfolio.ts       # Zod schemas + inferred types
  data/                          # the actual portfolio content (edit these to update)
    profile.json  contact.json  skills.json  experience.json
    education.json  projects.json  certifications.json  resume.md
  tests/
    services/  utils/  integration/
  .github/workflows/deploy.yml
  wrangler.jsonc  package.json  tsconfig.json  eslint.config.js  .prettierrc

Installation

Requires Node.js 22+.

npm install
cp .env.example .env          # for reference; wrangler dev uses .dev.vars instead (see below)

Development

Local stdio (Claude Desktop, MCP Inspector)

npm run dev:stdio

This runs src/local.ts directly with tsx, connecting the server over stdio. Point the MCP Inspector at it:

npx @modelcontextprotocol/inspector npm run dev:stdio

To use it from Claude Desktop, add to claude_desktop_config.json:

{
  "mcpServers": {
    "portfolio": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/portfolio-mcp/src/local.ts"],
      "env": {
        "GEMINI_API_KEY": "your-key-here",
        "GITHUB_TOKEN": "optional-token-here"
      }
    }
  }
}

Local Worker (Cloudflare Workers runtime)

# put local secrets in .dev.vars (git-ignored), one KEY=value per line:
echo "GEMINI_API_KEY=your-key-here" >> .dev.vars

npm run dev

Then test the HTTP endpoint directly:

curl http://localhost:8787/health

curl -X POST http://localhost:8787/mcp \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

curl -X POST http://localhost:8787/mcp \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_profile","arguments":{}}}'

Other commands

npm run typecheck   # tsc --noEmit
npm run lint         # eslint .
npm run lint:fix
npm run format       # prettier --write .
npm test             # vitest run
npm run test:watch

Deployment (Cloudflare Workers)

  1. Authenticate wrangler once locally: npx wrangler login.

  2. Set secrets (never in wrangler.jsonc):

    npx wrangler secret put GEMINI_API_KEY
    npx wrangler secret put GITHUB_TOKEN   # optional
  3. Deploy:

    npm run deploy:dry-run   # sanity check the build first
    npm run deploy

GitHub Actions (CI/CD)

.github/workflows/deploy.yml runs lint/typecheck/test on every push and PR to main, and deploys to Cloudflare Workers on push to main. Add these repository secrets first:

  • CLOUDFLARE_API_TOKEN — a token with Workers Scripts:Edit permission

  • CLOUDFLARE_ACCOUNT_ID — your Cloudflare account ID

Environment variables

Variable

Required

Purpose

GEMINI_API_KEY

No

Enables all AI tools (compare_job, ats_match, interview_questions, recommend_learning, portfolio_summary, experience_summary, generate_resume_summary) and semantic search. Without it, AI tools return a clear MCP error and search falls back to keyword-only.

GITHUB_TOKEN

No

Enables pinned repos + contribution calendar (GraphQL), and raises the REST rate limit for live repo metadata on get_project. No scopes beyond public read access needed.

SITE_URL

No

Defaults to https://karthikeyan.vercel.app/. Used in /version and a couple of generated links.

Tools

Tool

Description

get_profile

Full profile: name, role, company, location, tagline, bio, focus areas

get_about

About-me narrative

get_contact

Email, phone, location

get_social_links

GitHub, LinkedIn, Instagram, X, Hugging Face, Linktree

get_skills

Skills grouped by category, optional category filter

search_skills

Keyword/semantic search across all skills

get_experience

Full work experience timeline

experience_summary

Natural-language summary of experience (AI or templated)

get_education

Education timeline

get_projects

Projects with filter (cluster/status/featured), sort, pagination

get_project

One project by id, optionally enriched with live GitHub metadata

search_projects

Keyword/semantic search across projects

recommend_projects

Best-matching projects for a query or skill list

latest_projects

Most recent projects

get_certifications

45 certifications/courses/badges/publications/achievements, filter + pagination

search_certifications

Keyword/semantic search across certifications

get_resume

Resume as markdown + PDF link

generate_resume_summary

AI-tailored resume summary paragraph

compare_job

Fit score, strengths, gaps, best-matching projects vs. a job description

ats_match

ATS keyword-match score, matched/missing keywords, suggestions

interview_questions

Likely interview questions grounded in real projects/experience

recommend_learning

Skills to learn + certifications to pursue

portfolio_summary

Natural-language portfolio overview tailored to an audience

All AI tools (compare_job, ats_match, interview_questions, recommend_learning, portfolio_summary, plus the AI paths of experience_summary/generate_resume_summary) require GEMINI_API_KEY. Every tool validates input with Zod and returns a proper MCP error result (never an uncaught exception) on bad input, missing config, or unexpected failures.

Resources

portfolio://resume.md · portfolio://profile.json · portfolio://skills.json · portfolio://experience.json · portfolio://education.json · portfolio://projects.json · portfolio://certifications.json · portfolio://contact.json

Prompts

professional_bio · linkedin_summary · resume_summary · interview_introduction · project_explanation · cover_letter · portfolio_overview

Connecting from ChatGPT

  1. Deploy the Worker (see above) so you have a public URL, e.g. https://portfolio-mcp.<you>.workers.dev.

  2. In ChatGPT, open Settings → Connectors → Advanced → Developer mode (requires a ChatGPT plan that supports custom connectors) and add a new connector pointing at:

    https://portfolio-mcp.<you>.workers.dev/mcp
  3. Enable the connector in a chat and ask things like "What are Karthikeyan's featured projects?" or "Compare this job description against Karthikeyan's profile: ...".

Example tool responses

get_profile:

{
  "name": "Karthikeyan K",
  "headline": "AI Engineer",
  "currentRole": "Associate Data Analyst",
  "currentCompany": "Zinnov",
  "tagline": "Building intelligent systems that act — not just answer."
}

ats_match (with GEMINI_API_KEY set):

{
  "atsScore": 78,
  "matchedKeywords": ["LangChain", "RAG", "Python", "Vector Databases"],
  "missingKeywords": ["Kubernetes", "Terraform"],
  "suggestions": ["Add measurable infra/deployment experience if applicable."]
}

Adding a new tool

  1. Add (or extend) a module under src/tools/, exporting a register*Tools(server, env) function.

  2. Call server.registerTool(name, { title, description, inputSchema }, safeTool(name, handler))inputSchema is a Zod raw shape (object of Zod validators, not z.object(...)), and safeTool (from src/utils/errors.ts) converts thrown errors/Zod failures into proper MCP error results automatically.

  3. Register the module in src/server.ts's createMcpServer.

  4. Add a test under tests/ (unit test the underlying logic, or extend tests/integration/server.test.ts for a full round-trip check).

Adding a new resource

Add an entry to the RESOURCES array in src/resources/index.ts with a unique name/uri, a getContent() function, and register it — registerResources handles the rest.

Troubleshooting

  • AI tools return "requires GEMINI_API_KEY": expected without a key configured — set it via .dev.vars (local) or wrangler secret put GEMINI_API_KEY (deployed).

  • get_project's liveMetadata is always null: GitHub REST is unauthenticated by default and rate-limited; set GITHUB_TOKEN to raise the limit. Pinned repos / contribution summary specifically require GITHUB_TOKEN (GraphQL) — expected to be null without it.

  • CORS errors from a browser-based MCP client: src/index.ts already sends permissive CORS headers on every response including OPTIONS; check the client is hitting /mcp with POST, not GET.

  • /metrics counters reset unexpectedly: they're per-isolate, in-memory only — a Cloudflare Workers cold start resets them. This is a documented limitation, not a bug.

  • wrangler dev can't find secrets: local secrets go in a git-ignored .dev.vars file (KEY=value per line), not .env.env is only for the stdio dev entry (npm run dev:stdio).

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Exposes Aniket Charjan's resume as a queryable API for AI assistants, implementing MCP tools, resources, and prompts for detailed resume queries.
    11
    78 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Transforms professional data (CV, projects) into MCP tools for LLMs to query, list, match job descriptions, and ask about experience.
    52 npm
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Exposes a personal portfolio of projects, skills, and resume as callable tools for MCP-compatible AI assistants like Claude Desktop.
    4
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to discover and retrieve professional portfolio information including profile, experience, skills, projects, and contact details through MCP tools.
    -