WebMCP Portfolio Integration
This portfolio registers structured tools through the WebMCP browser API, so an AI agent can query it directly instead of scraping the DOM: search projects, read the resume, even change the page.
- ROLE
- Builder
- PERIOD
- 2026
- DOMAIN
- Web and tools
- CODE
- GitHub ↗
OVERVIEW
Seven tools, registered through document.modelContext, let an agent search every project, read the full resume, list skills and lab experiments, and even switch the site into reader mode, each with a JSON Schema for inputs and typed JSON results. The site works normally for everyone; the tools register only when the browser exposes the API (Chrome 146+ with the WebMCP flag) and unregister on cleanup. The site data is a static file built at build time, fetched only by browsers that have the API.
Tools this site registers.
The 7 tools this site registers with the WebMCP browser API. An agent in Chrome with the WebMCP flag on calls them directly; here is the catalog and one sample call.
search_projectsreadSearch projects by query, tech, tag, domain, or featured-only.
get_projectreadFull details for one project by ID: challenge, solution, impact, stack.
get_resumereadResume data by section: experience, education, publications, skills, competencies, contact.
search_skillsreadTechnical skills by category or keyword.
get_contactreadContact info and social links.
list_experimentsreadWhat Jay is currently building, exploring, or watching in the lab.
switch_modewriteToggle reader mode: a calm, motion-free reading view, or back to the full site.
AGENT CALLS search_projects
{
"tool": "search_projects",
"arguments": { "query": "protein", "featured_only": true }
}SITE RETURNS
{
"count": 2,
"projects": [
{
"id": "nobel-dataintelligence",
"title": "Nobel Data Intelligence",
"summary": "Physics-informed deep learning for protein stability and enzyme kinetics. A tri-modal architecture fuses ProtT5 sequence embeddings, a VDOS vibrational spectrum from normal mode analysis, and ChemBERTa chemistry through learned gated attention.",
"tech": ["Python", "PyTorch", "ProDy", "Transformers", "RDKit", "Biopython"],
"tags": ["deep-learning", "molecular-biology", "pytorch", "research", "protein"],
"domain": "Research",
"url": "https://jayhemnani.in/projects/nobel-dataintelligence"
},
{
"id": "biotech-accelerator",
"title": "Biotech Accelerator",
"summary": "Multi-agent AI system for biotech research that routes a query through protein databases, literature, structural analysis, and chemistry, then proposes experiments worth running next.",
"tech": ["Python", "LangGraph", "ProDy", "httpx", "Rich", "Docker"],
"tags": ["langgraph", "agents", "bioinformatics", "research"],
"domain": "AI/ML",
"url": "https://jayhemnani.in/projects/biotech-accelerator"
}
]
}ARRIVED AS
“AI agents that use a website have to scrape its DOM and click buttons, which is brittle, slow, and breaks the moment a layout changes. The emerging WebMCP browser API takes a different route: a site registers structured tools that an agent calls directly, with JSON Schema inputs and typed JSON results. This project works out what it takes to expose a real, content-heavy site through that API.”
The WebMCP proposal lets a website expose tools to an AI agent the same way an MCP server exposes them to a model: a name, a description, a JSON Schema for inputs, and an execute callback that returns structured data. Instead of screenshotting or scraping this site, an agent can call search_projects or get_resume and get clean JSON back. The API is new (document.modelContext, behind a flag in Chrome 146), so part of the work was learning how the registration lifecycle behaves on a real Next.js site rather than inventing a new data layer.
WHAT I BUILT
- 01Registers 7 structured tools on this site through the WebMCP browser API (document.modelContext): project search, single-project lookup, resume, skills, contact, lab experiments, and one that changes the page (reader mode).
- 02Each tool ships a JSON Schema for its inputs and returns typed JSON with canonical jayhemnani.in URLs, so an agent gets structured data instead of parsing rendered HTML.
- 03Progressive enhancement: the site works normally everywhere, and the tools register only when the browser exposes the API (Chrome 146+ with the WebMCP flag), and aborting one signal on cleanup unregisters them all.
- 04Site data (projects, resume, socials, lab) is built once into a static /site-data.json. The browser fetches it, and the tool code, only after it finds the API, so other visitors download neither.
WHAT CHANGED
- Turns the portfolio into a queryable surface: an agent can search every project, pull the full resume, list skills, and read what Jay is currently building, all as structured JSON.
- One of the tools carries a real side effect (the reader-mode switch), so WebMCP is a control surface as well as a read API.
- Doubles as a working reference for an early-stage browser standard, with the whole registration layer in one typed module.
ARCHITECTURE
The integration is two layers. A static route (/site-data.json) is built once at build time: it reads projects from the MDX content loader, the resume object, social links from site config, and lab experiments, and shapes them into one typed SiteData object. A client component (WebMCPProvider) checks on mount whether document.modelContext exists. Only if it does, it fetches that file and the tool module, then registers the seven tools with one AbortSignal; its cleanup aborts the signal, which unregisters them all. Each tool is a name, a human-readable description, a JSON Schema input definition, and an async execute callback that filters or transforms the in-memory SiteData. Six read tools return JSON; the write tool changes the DOM and localStorage and fires readermodechange, an event the site already listens for, so the page updates live.
Data flow
click a stage
At build time the route reads projects (MDX), the resume, social links, and lab experiments into one typed SiteData object, served as a static file.
COMPONENT
site-data.json routeBuilt once at build time: reads projects, resume, socials, and lab data and serves the typed SiteData as a static file.
ALSO IN THE BUILD
get_resume / search_skills / get_contactRead tools over the resume: sectioned resume data, skill search by category or keyword, and contact plus social links.
list_experimentsRead tool over the lab: what Jay is currently building, exploring, or watching.
Decisions, with the cost of each.
A decision without its trade-off is marketing. Each row says what was chosen, why, and what it gave up.
Expose tools through WebMCP rather than an llms.txt file or a separate API
WebMCP gives an agent typed, callable tools inside the page it is already on, with JSON Schema inputs and structured returns, which removes the scraping and clicking layer entirely. A static text file or a standalone REST API would not be discoverable from the browsing session.
INSTEAD OF
llms.txt (static, not interactive); a public REST API (extra surface to host, not tied to the page the agent is viewing).
Progressive enhancement behind an availability guard
The API is behind a flag in one browser channel, so the site has to work for everyone and add tools only when document.modelContext is present. The provider checks first, registers nothing otherwise, and aborts the tools' signal on unmount.
INSTEAD OF
Assume the API exists (breaks the site for normal visitors); feature-detect once globally (misses the React mount and unmount lifecycle).
Build the data once, fetch it only when the API exists
The tools need the full project and resume data, but only a visitor whose browser has the API can use them. A static route builds the data at build time and the provider fetches it after detection, so every other visitor downloads nothing, and the tools still work on in-memory data with no per-call fetch.
INSTEAD OF
Pass the data down from a server component (puts it in every page's HTML for every visitor); fetch inside each execute (a network round-trip per call).
The part that mattered.
The facts behind the work, and the code that produced them.
- registered
- 7 tools
- 6 read · 1 write, via document.modelContext
- typed inputs
- JSON Schema
- structured JSON returns, no DOM scraping
- behind a flag
- Chrome 146+
- progressive enhancement everywhere else
- registration layer
- 1 module
- static data, fetched only when the API exists
mc.registerTool({
name: "search_projects",
description: "Search Jay's projects by query, technology, tags, or domain.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text across title, summary, tech" },
tech: { type: "string", description: "Filter by technology" },
featured_only: { type: "boolean", description: "Only featured projects" },
},
},
execute: async (args) => {
let results = [...data.projects];
// filter by query / tech / tag / domain / featured ...
return {
count: results.length,
projects: results.map((p) => ({
id: p.id, title: p.title, tech: p.tech,
url: `https://jayhemnani.in/projects/${p.id}`,
})),
};
},
}, { signal });
Every tool follows the same contract: a name, a description, a JSON Schema for inputs, and an async execute callback that transforms the in-memory site data. The agent gets back structured JSON with canonical URLs, never rendered HTML.
useEffect(() => {
if (!document.modelContext) return; // API is behind a flag; do nothing otherwise
const controller = new AbortController();
(async () => {
const [{ registerWebMCPTools }, res] = await Promise.all([
import("@/lib/webmcp"),
fetch("/site-data.json", { signal: controller.signal }),
]);
await registerWebMCPTools(await res.json(), controller.signal);
})();
return () => controller.abort(); // unregisters every tool
}, []);
The site renders normally for every visitor. The data and the tool code are fetched, and the tools registered, only when document.modelContext is present. The effect's cleanup aborts the signal they were registered with, which unregisters them, so the lifecycle follows React's mount and unmount.
// switch_mode execute
const on = args.mode === "reader";
setReaderMode(on); // sets data-reader, saves it, fires readermodechange
return { mode: on ? "reader" : "default", reader: on };
Six read tools, one write tool. An agent can turn on reader mode, which dispatches readermodechange, an event the site's motion layer already listens for. That makes WebMCP a control surface rather than only a query layer.
LEARNED
WebMCP reframes a site as a set of typed tools, not a page to scrape. The hard part is shaping existing content into clean tool inputs and returns, not the registration call itself.
Progressive enhancement is non-negotiable for a flagged API: guard on availability, register on mount, abort the signal on cleanup, and the site stays normal for everyone else.
A tool that changes the page is as interesting as one that reads it. The reader-mode tool turns the site into something an agent can operate, not only query.