Skip to main content

building an MCP server to manage my portfolio

Introduction

I built an MCP server that lets me create, edit, and delete blog posts and projects on this portfolio through an API. It's deployed on Vercel so it runs 24/7. Every change commits directly to GitHub through the GitHub Contents API.

Instead of manually editing MDX files and pushing to git, I can manage everything through a REST API or through Claude using MCP tools.

Architecture

The server has two parts:

  1. Local MCP Server that runs on stdio for Claude Desktop

  2. Vercel API as a serverless function that talks to GitHub directly

Since Vercel serverless functions have no persistent filesystem, the handler uses the GitHub Contents API to read and write files directly in the repo.

How It Works

Reading Posts

Posts are MDX files with YAML frontmatter. The API fetches them from GitHub, base64 decodes the content, and parses the frontmatter:

async function getPost(slug: string) {
  const fileData = await ghFetch(
    `/contents/${POSTS_DIR}/${slug}.mdx?ref=${GITHUB_BRANCH}`
  );
  const raw = b64decode(fileData.content);
  const { frontmatter, content } = parseFrontmatter(raw);
  return { slug, frontmatter, content, sha: fileData.sha };
}

The sha is important. GitHub requires it when you want to update or delete a file so it knows you're modifying the correct version.

Creating Posts

Creating a post builds the frontmatter string, base64 encodes the full file, and PUTs it to the GitHub API:

async function createPost(
  frontmatter: Record<string, any>,
  content: string
) {
  const slug = titleToSlug(frontmatter.title);
  const fm = buildFrontmatter(frontmatter);
  const fileContent = `${fm}\n\n${content}`;

  await ghFetch(`/contents/${POSTS_DIR}/${slug}.mdx`, {
    method: 'PUT',
    body: JSON.stringify({
      message: `Create post: ${frontmatter.title}`,
      content: b64encode(fileContent),
      branch: GITHUB_BRANCH,
    }),
  });

  return { slug, frontmatter, content };
}

Each API call creates a commit on GitHub automatically. No need for git add, git commit, git push.

Parsing Projects

The projects file (projectsData.js) is a JS module, not JSON. Parsing it required stripping comment lines without destroying // inside URLs like https://:

// only strip lines where // is the FIRST non-whitespace content
const stripped = raw.replace(/^\s*\/\/.*$/gm, '');

Using /\/\/.*$/gm would have turned https://github.com/... into just https:.

Authentication

Every request except /health requires a Bearer token:

function validateAuth(req: VercelRequest): boolean {
  if (!API_KEY) return true;
  const token = req.headers.authorization?.replace('Bearer ', '');
  return token === API_KEY;
}
curl -H "Authorization: Bearer YOUR_KEY" \
  https://portfolio-mcp.vercel.app/api/posts

API Endpoints

MethodPathDescription
GET/healthHealth check
GET/api/postsList all posts
GET/api/posts/:slugGet post by slug
POST/api/postsCreate post
PUT/api/posts/:slugUpdate post
DELETE/api/posts/:slugDelete post
GET/api/projectsList projects
POST/api/projectsCreate project
PUT/api/projects/:titleUpdate project
DELETE/api/projects/:titleDelete project

Stack

  • TypeScript for the MCP server and API handler

  • Vercel Serverless Functions for always-on deployment

  • GitHub Contents API for reading/writing files directly

  • MCP SDK for Claude Desktop integration

  • Express for the local REST API

Source

sohumsuthar/portfolio-mcp