Personal MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Personal MCP ServerWhat posts have I written about MCP servers?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Personal MCP server
A read-only, no-sign-in MCP server for a personal site. It answers only from one published JSON file, the bundle, that your site's build writes: your profile, posts, case studies, pages, projects and any small datasets you publish. It runs as a Cloudflare Worker, on its own or beside a static site.
A live example runs at https://brentburgess.com/mcp, and the write-up of how it was built is at https://brentburgess.com/work/mcp-server.
Why
AI agents already read personal sites, badly: they scrape HTML, guess at what is current and fill gaps with whatever sounds right. This server gives them exactly what your pages publish, as clean Markdown and structured data, and nothing more. There is no database to query, no account to sign in to and no write path. If something is not in the bundle, no tool can return it, and the server's instructions tell the agent to say the site does not publish it rather than guess.
Related MCP server: Opyt
What an agent gets
Seven read-only tools, in this order:
get_profile: who you are, your availability, skills, how to reach you, and every page of the site.list_projects: your projects, filtered by status if asked.list_content: every post, case study and page, newest first, with each one's sections.search: full-text search (BM25) over every section of every document and project.fetch: a whole document or one section as Markdown, by id, path, URL, resource URI or unique slug.get_site_stats: the headline figures the bundle's optionalstatsrecord holds, and the datasets with their row counts.query_dataset: the rows of one dataset, filtered by date.
Every post, case study and page is also a Markdown resource under a URI scheme of your own, such as example://writing/<slug>.
Design
Stateless. Every request gets a new server built from the same bundle. No session, no cookie, no
Mcp-Session-Id. GET and DELETE on /mcp get 405.Both protocol eras. Modern clients (2026-07-28) and 2025-era clients are both served, statelessly, through the official
@modelcontextprotocol/serverSDK.Everything derived once per isolate. The documents, search index and tool schemas are built from the bundle once, at Worker startup (
warm), and shared by every request after. A request costs well under a millisecond of CPU.Failures an agent can act on. A tool failure is an
isErrorresult that says what to do instead: the ids that exist, the sections a document has, which tool answers a page that is not a document. A missing resource is a -32602 error naming the URI, never an internal message.Nothing logged. The code has no console output, and the Worker config turns observability off. Requests carry what someone asked their agent; a server that only reads public pages has no reason to keep that.
Security headers and checks. Only POST, only for your hosts, and, when a browser sends an Origin, only your own. Every response carries
Cache-Control: no-store, HSTS,nosniff,no-referrer,noindexand adefault-src 'none'CSP. JSON-RPC batches and bodies over 64 KiB are refused before any work.The data ships with the code. The bundle is compiled into the Worker script through a wrangler alias, so code and data deploy as one version, and the server's version is
1.0.0+<bundleId>.Small dependency surface. The code in src/ imports only its own files, the MCP server package and zod, and uses no fetch, no
node:modules and noprocess(a test holds it to that).
Layout
src/config.ts: every site-specific value. Edit this first.
src/bundle.ts: the bundle's zod schema and types, and the validator behind
npm run check:bundle.src/catalog.ts: the tools' names, descriptions and schemas.
src/server.ts:
createServer,createHandler,prepareandwarm.src/tools.ts, src/ids.ts, src/search.ts, src/resources.ts: the tool handlers, id resolution, search and resources.
src/edge.ts, src/worker.ts: the Worker's front door and entry point.
examples/bundle.json: a bundle for a fictional data engineer, Ada Example, at example.com.
examples/server.json: a listing for the MCP Registry.
wrangler.example.jsonc: the Worker config, in two setups.
The bundle
The bundle is one JSON object. examples/bundle.json is a complete one; src/bundle.ts is the exact schema, with every limit. In outline:
format: always1.bundleId: anything that changes when the content does (letters, digits and hyphens), such as the first 16 hex characters of a SHA-256 of the rest of the bundle.site:name,url(https, no trailing slash, the same assiteUrlin src/config.ts),job_title,tagline,intro,location,availability(text,as_ofas YYYY-MM,label),skills, andcontact(emailandprofiles, each alabelandurl).index: every page worth naming, each aurl,titleanddescription.get_profilereturns it.entries: posts (kind: "post", idwriting/<slug>) and case studies (kind: "case_study", idwork/<slug>), newest first. Each haspublishedandupdateddates, abrief(problem, built, changed; null for a post),links, and theprojectit is about (or null).pages: other pages (kind: "page", idpages/<name>), such as About or Now.projects: each withtitle,summary,status(running,plannedorpublished),status_label,outcome(or null),stack,urlandcase_study_url(the URL of a case study inentries, or null). A project with no case study becomes a short document of its own,projects/<slug>.stats(optional): a flat record of headline figures, name to string, number, boolean or null.datasets: small tables, each with aname(lower case and underscores),title,description,grain(day,week,monthor anything else),date_column(or null),rows_from,data_through,url,columns(name,type,description),notes, androwsas arrays in column order, oldest first.
Every document (entry or page) has an id, title, description, url, its sections and its chunks:
chunksare the document's text as Markdown with absolute links, in order. Joined with blank lines, they are the whole text. The first may be the text before any heading (anchor: "",depth: 1); a page's may start with its own# Title, which the resource view drops. Every other chunk is one section, starting with its own heading line (## Heading), with the heading'sanchor,headingtext,depth(2 or more) and thelinksin it.sectionslist the same headings, each withurlset to the document's URL plus#anchor.
Ids are lower case. Keep the bundle public, at a URL of its own beside your pages, and put nothing in it that the pages do not already publish.
Check a bundle before deploying it:
npm run check:bundle -- path/to/bundle.jsonIt prints every problem as a path and a message, such as entries.2.sections.0.url: must be https://example.com/writing/x#intro, and exits 1 if there is any. The limits match the tools' output schemas, so a bundle that passes never makes a tool fail its own schema.
Generating a bundle
Any static site generator can write the bundle at build time, after the HTML exists. In outline:
Collect the published entries and pages (never drafts), with their titles, descriptions, dates and URLs.
For each, take the main content of the built HTML page (not the source), so the bundle says exactly what the page says. Convert it to Markdown, make every link absolute, and split it at each h2 and h3 into chunks, using the heading ids the page already has as anchors.
Add the site facts, projects, any stats and datasets from the same data files the pages render.
Work out
bundleIdfrom the rest, write the file (for example dist/mcp-data.json), and runnpm run check:bundleon it in the same build, so a bad bundle stops the deploy.
Taking the text from the built page keeps the bundle in step with what visitors see, including anything a template adds or a draft filter removes.
Setup
Needs Node 24 or later (the tests run the TypeScript directly).
npm installEdit src/config.ts: the site URL and hosts, the server's name and title, the instructions, the URI scheme, the tool wording that names you, the 405 text, the pointers for pages that are not documents, and the related words search uses.
Replace examples/bundle.json with your own bundle, or point the alias in wrangler.jsonc at the one your build writes.
Rewrite test/retrieval-golden.json with queries a person would ask of your site and the section ids that answer them, then run
npm test.After any change to a tool's name, description or schema (including your config's wording), review the change and rewrite the snapshot:
UPDATE_SNAPSHOT=1 npm test.
To run the tests against another bundle without moving it: MCP_BUNDLE=dist/mcp-data.json npm test.
Deploying
Copy wrangler.example.jsonc to wrangler.jsonc and choose a setup.
(a) Standalone. The active config: a Worker of its own, routed to example.com/mcp on your Cloudflare zone, with the bundle beside it. The rest of your site is served as it is today. If you put the server on a host of its own instead (mcp.example.com, say), add that host to allowedHosts in src/config.ts.
(b) Added to an existing static-assets site. The commented block: copy src/ into your site's repo (as mcp/src, for example), set main to its worker.ts, alias mcp-data to the bundle your build writes, and add "run_worker_first": ["/mcp"] to assets. Only /mcp then runs the script; every other path is a static asset that never does. Never widen that list: anything more puts your pages behind the script.
In both, keep "observability": { "enabled": false }, workers_dev and preview_urls off, and no other bindings; the config test checks wrangler.jsonc for all of it once you have one. Then:
npx wrangler login
npm run deploynpm run deploy checks the bundle, runs the tests and then wrangler deploy. npm run dev serves it locally with wrangler dev (if npm skipped workerd's install script, approve it with npm approve-scripts workerd first).
Say on your privacy page that the server exists, what it answers from, and that it keeps nothing.
Rate limiting
The server is cheap to run, but it is open to anyone. A Cloudflare rate limiting rule (Security, then Security rules) keeps one client from using up your Workers quota:
When incoming requests match: URI path equals
/mcp.Counting: by IP, 20 requests per 10 seconds.
Then: block for 10 seconds.
The free plan allows one such rule. To switch the server off at once, change the rule to block every request to /mcp.
Listing in the MCP Registry
The official MCP Registry lists remote servers under a namespace you prove you own. With a domain, the proof is a DNS TXT record:
Copy examples/server.json and fill in your own
name(your domain reversed, then a name:com.example/site),title,description,websiteUrland theurlof your /mcp.Install
mcp-publisherfrom the registry's releases.Make an Ed25519 key pair, keep the private key outside the repo, and add an apex TXT record
v=MCPv1; k=ed25519; p=<public key, base64>to your domain. It does not affect mail.mcp-publisher login dns --domain example.com --private-key <private key, hex>mcp-publisher publish
To update the listing later, raise version in server.json and publish again.
Tests
npm test runs every test with Node's own runner, against examples/bundle.json (or MCP_BUNDLE). npm run typecheck runs tsc --noEmit.
contract: every tool, resource and error through the official
@modelcontextprotocol/client, in both protocol eras, against values worked out again from the bundle and config.schema: tools/list and the instructions against test/tools.snapshot.json, and every schema in the subset clients handle (no
$ref, closed objects, every property described, every input bounded). Rewrite the snapshot withUPDATE_SNAPSHOT=1 npm testafter an intended change.server-boundary: every tool called with every value it allows and every id the listings give, in both eras. Every call that should work does, no answer passes 100 KB, and no answer names an address the bundle does not hold.
retrieval: search against test/retrieval-golden.json, needing recall@3 of at least 0.9 and a mean reciprocal rank of at least 0.75.
config: src/ imports only its own files, the server package and zod, with no console, process,
node:modules or fetch; only config.ts names the site; and both Worker setups log nothing and send only /mcp to the script.worker: the front door, with a stand-in for the assets: POST only, hosts and origins, security headers, 404 standalone.
bundle: the validator accepts the example and reports each kind of mistake where it is.
perf: CPU time per request against the Workers Free limit of 10 ms. Skipped unless
MCP_PERF=1.
Licence
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Instant blogs for AI agents: a publishing CMS with no dashboard. One signup call returns a blog, an API key and a live URL; then publish markdown posts and get public URLs back.
Web data tools for AI agents: pages as markdown, search, maps, commerce, jobs, AI answers.
Web tools for AI agents: scrape pages to Markdown, audit SEO, detect tech stacks, check sitemaps
Give your agent live data from Twitter, Reddit, the web and GitHub. No API keys, no scraping stack.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes a public, read-only professional profile with tools to search resume evidence, fetch curated links, and generate career briefs for LLM agents.MIT
- AlicenseAqualityBmaintenanceEnables AI clients to search, read, and analyze a local SQLite knowledge base built from the full public archives of people you follow, bookmark, or subscribe to, with tools for onboarding, querying, and topic tracking.128MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to create verifiable, replayable citations, search private knowledge bases, and publish Markdown with verified citation markers.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP-capable AI clients to read and write a self-hosted todo list, quick memos, and a plain-Markdown knowledge base through 16 tools covering search, tasks, notes, and identity checks. Tool visibility is scoped per API key with directory-level permissions, and every call is audit-logged.4MIT