Your AI Assistant Can
Query My Portfolio
A public, read-only MCP server at /api/mcp - 8 tools, a stateless hand-rolled JSON-RPC dispatcher, and a spec target chosen for what real clients actually speak, not what's newest.
↓ scroll to read · click a section on the left to jump
Point your assistant at the source, not a stale search index
The Model Context Protocol is an open, JSON-RPC-based way for an AI assistant to call a tool with a defined schema instead of scraping a page and hoping the parse holds up. Claude, Claude Code, Cursor, and a growing list of other clients speak it. I've written about it and list it among my Generative AI skills, so when I wanted a concrete demo of the protocol, wiring one onto my own site felt like the honest way to show it, rather than a résumé line I couldn't back up.
To be clear about scope: this is a small, public demo I built for my own portfolio. It is not professional MCP client integration work - the résumé doesn't claim any, and this article won't either.
The 8 tools this server exposes, why it targets an older-but-actually-implemented protocol version, why it's hand-rolled JSON-RPC instead of the official SDK on a serverless route, and how to connect your own client to it.
Eight tools, every one read-only
Every tool declares readOnlyHint: true and is built directly on this repo's existing data - never a second, hand-maintained copy of a fact that lives somewhere else.
Headline, location, availability, links
Name, headline, one-liner, location, availability, contact/social links, résumé highlights, and a skills summary.
Work history
Roles, companies, periods, and the bullet points for each - my full work history, structured.
Featured project summaries
Id, title, client, period, and tech for every featured project - use get_project(id) for the full write-up.
One project, in full
Full description, tech stack, and links for a single project by id, from list_projects.
Articles and perspectives
Native long-form articles and personal-essay perspectives, with title, url, date, and description, newest first.
The /resources hub
Cheatsheets, the cross-OS lookup, /learn tracks, booking, and anything shipping next.
Full-text search over the whole site
BM25 search - the same retrieval index and chunk data the chatbot uses - returning titles, urls, and snippets.
How to actually reach me
Contact form, LinkedIn, and /book call options, each marked bookable now or not yet - no freelance project work offered here either.
Targeting 2025-06-18, not whatever's newest
This server implements MCP protocol version 2025-06-18: the classic initialize-based lifecycle (initialize with version negotiation, notifications/initialized, ping, tools/list, tools/call), plus minimal resources/list/resources/read stubs since this server registers no MCP resources - its content comes back as tool results instead, so it never declares the resources capability.
Why not the officially "current" spec? modelcontextprotocol.io's own current revision has since moved to 2026-07-28, which replaces the initialize/initialized handshake with a server/discover RPC and per-request _meta protocol versioning - a breaking shape change that, as of writing, no mainstream client this server targets (Claude, Claude Code, Cursor, the MCP Inspector) actually speaks. 2025-06-18's own backward-compatibility section still requires supporting the classic lifecycle, so implementing it is both correct today and forward-compatible. The server also negotiates down to 2025-03-26 or 2024-11-05 for an older client, and rejects JSON-RPC batching outright - removed in 2025-06-18, and never supported here even as a courtesy.
export const LATEST_PROTOCOL_VERSION = '2025-06-18';
export const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
// echo the client's version back if we support it, else our own latest
function negotiateProtocolVersion(requested) {
return SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
}
"Target the newest spec" is the wrong default when the newest spec isn't what your actual clients implement yet. Target what they speak, and design so moving forward later is a version bump, not a rewrite.
Why this isn't built on the official SDK
@modelcontextprotocol/sdk's Streamable HTTP server transport is built around a long-lived Node http.IncomingMessage/ServerResponse pair and in-memory per-session transport state. An Astro APIRoute on the Vercel adapter gets a Web-standard Request and returns a Response, invoked fresh per request with no guaranteed process continuity between calls - there's no persistent process to hold a session in.
Rather than bridge Node req/res emulation into that model for a JSON-RPC surface this small, src/pages/api/mcp.ts is a thin HTTP adapter - CORS, rate limiting, body parsing - around dispatchRequest(), a pure function with no Request/Response types at all. It takes a parsed JSON-RPC object and a tool list, and returns a JSON-RPC response. Nothing about it depends on HTTP, a session id, or anything from a previous call.
- Stateless by construction. Every POST is fully self-contained - no session to create, expire, or lose between cold starts.
- Trivial to unit test. scripts/test-mcp.ts calls dispatchRequest() directly, with a small fixture tool list - no HTTP server, no mocking a transport.
- The route stays thin. api/mcp.ts only does what an HTTP boundary should: parse, rate-limit, check CORS, hand off, serialize the result.
export function dispatchRequest(request, tools) {
switch (request.method) {
case 'tools/call': {
const tool = tools.find(t => t.name === request.params.name);
const args = tool.validate(request.params.arguments);
return ok(request.id, tool.run(args.value));
}
}
} // no Request/Response type in sight
search_site reuses the chatbot's own BM25 index
search_site is built on exactly the same pieces as "Ask Anupam's AI": the small, dependency-free Bm25Index class and the same generated chat-index.json chunk file, built once at deploy time from the whole site's content. One retrieval implementation, two front ends - a chat widget for people, a JSON-RPC tool for AI clients.
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "search_site", "arguments": { "query": "kubectl logs" } } }
// result - shape, abbreviated
{ "results": [
{ "title": "kubectl cheatsheet", "url": ".../cheatsheets/kubectl", "snippet": "..." }
] }
Results are capped at 8 per call, queries at 300 characters - the same defensive limits every other tool here uses, since this is a public endpoint anyone can call.
Wide-open CORS, its own rate limit, a small body cap
CORS here is deliberately permissive - Access-Control-Allow-Origin: *, only POST, GET, OPTIONS - because MCP clients connecting to a public server aren't same-origin browser code the way this site's own chat widget is. The chat widget's stricter origin allowlist simply doesn't apply to a tool meant to be called from anywhere.
Its own bucket, not the chatbot's
60 requests per 10 minutes, 2,000 per day per IP - from the same shared rate-limit implementation the chatbot uses, namespaced separately so one caller running hot never eats the other's quota.
32 KB, checked twice
Checked against the Content-Length header and the actual body length, before JSON parsing ever runs.
One request per POST
A top-level JSON array is rejected outright, matching 2025-06-18's removal of JSON-RPC batching - never silently truncated to "just the first one."
No secrets touch this route, nothing is written anywhere, and nothing is kept between requests - the entire attack surface is "answer a read-only question about public content, within a rate limit."
Connecting your own client
Full copy-paste setup for Claude's custom connector, Claude Code, Cursor, and VS Code lives at anupam.info/mcp - generated from the same tool list this server actually serves, so it can't drift. Here's the raw JSON-RPC, if you'd rather skip a client and just curl it:
$ curl -s https://www.anupam.info/api/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'
# list the 8 tools
$ curl -s https://www.anupam.info/api/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
This was checked against the live endpoint on 2026-09-27: initialize answered with the negotiated protocol version, tools/list returned all 8 tools, and calling search_site with "kubectl logs" returned the kubectl cheatsheet as the top hit - the same retrieval index the chatbot uses, working through the JSON-RPC path instead of the chat UI.
A small server, honestly scoped
I don't think the interesting part of this project is "look, a tool-calling endpoint." It's narrower than that: an AI assistant a recruiter or a fellow engineer already uses can get my actual facts - sourced, current, structured - instead of whatever a general web search or a stale training cutoff hands it. That's a small, concrete win, not a platform.
It has real, stated limits too - worth naming rather than glossing over. list_resources reflects only the curated /resources hub, not a live walk of every content collection on the site; the cheatsheets and /learn tracks already have their own card pointing there, so this is a thin wrapper, not a second index. And this is read-only by design - it will never write, execute, or expose anything not already public.
Targeting the spec version your actual clients speak, instead of the newest one a working group just shipped, felt like the less exciting choice while I was building this. In hindsight it's the only choice that made the demo actually work today instead of working in principle for clients that don't exist yet.
Where to go next
- MCP specification - 2025-06-18The exact lifecycle, JSON-RPC shape, and tool schema this server implements.
- anupam.info/mcpThe live endpoint, its full tool table, and copy-paste setup for Claude, Claude Code, Cursor, and VS Code.
- Building a Grounded Chatbot for My PortfolioThe companion piece - the same retrieval index and site content, exposed instead as a chat widget for people.
Field notes: Anupam Kumar · Backend & Generative AI Engineer. v1.0