Answering questions with MCP
This site now answers questions over MCP through a tool called `ask_matt`. It quotes from published articles, hands over the hand-kept numbers when a question wants one, cites everything, and refuses the rest. The refusal took the most design effort.
A growing share of this site's visitors do not have eyes. Recruiters send agents to read CVs. Engineering leaders paste articles into a chat window and ask for the short version. Somebody's research assistant is deciding whether anything I've written is worth surfacing to its human.
I've been preparing for that shift. The site already exposes an MCP endpoint, publishes an llms.txt, and serves every article as raw markdown alongside structured access to my projects and CV. All of that makes the site easier for an agent to read. The obvious next step was to stop making them reconstruct my views from twenty pages and let them ask a question directly.
So there is now a tool on the MCP endpoint called ask_matt. Pass it a question and it searches everything I've published, returns the most relevant passages verbatim with citations, and, when my writing doesn't answer the question, says so instead of guessing.
You can try it below against the live endpoint.
Under the hood
The implementation is intentionally simple. When a question arrives, the tool scores every article twice.
The first pass is keyword matching: title hits carry the most weight, then the standfirst, tags and body occurrences, capped so one obsessive paragraph can't dominate the results. The second pass is semantic, which is how "how should I hire engineers?" finds an essay that never once says "hire". More on that shortly.
The three strongest articles come back with their most relevant paragraphs quoted word-for-word, each carrying its title, canonical URL and publication date. There is no vector database and, more importantly, no generation: a relevance score over a collection of articles and a quotation engine. For a collection this size, that's enough.
Retrieval belongs on my side of the wire, where the source of truth lives. Generation belongs on the caller's side, where the agent understands the user's broader context. My site knows what I wrote. Your agent knows why you're asking. Neither should do the other's job.
Designing the failure mode
The hardest part wasn't answering questions. It was deciding when not to.
Below a relevance threshold, ask_matt does not return weak matches with an apologetic shrug. It returns:
{
"answered": false
}
along with a suggestion to browse the articles or ask me directly. The threshold is deliberately conservative: a single keyword buried in one article is not evidence that I've written about a topic. Ask about engineering metrics and you'll get quotations with citations. Ask about Kubernetes or football and you'll get a refusal, because I've never published my views on either. A wrong answer with my name attached is strictly worse than no answer.
Teaching it synonyms
The keyword pass can only find words I actually used, so an essay about interviewing stays invisible to "how should I hire engineers?". Embeddings close that gap. An embedding is a sentence compressed into a list of numbers, 384 of them here, arranged so that sentences meaning similar things end up with similar numbers. Comparing a question against a paragraph stops being string matching and becomes measuring an angle.
The model doing the compressing is all-MiniLM-L6-v2, a sentence transformer that quantises down to twenty-three megabytes and runs through Transformers.js inside the same serverless function as everything else. A build script chunks every article into paragraphs, embeds each one, and commits the lot to the repo next to the model weights. At question time the model embeds the query, takes a dot product against roughly a thousand paragraphs, and is done in a few milliseconds. No vector database, no embeddings API, no network calls, no bill.
The refusal threshold was set by measurement rather than taste. Genuine matches score between 0.48 and 0.80; off-topic questions top out around 0.44. The threshold sits at 0.45 and fails closed.
Calibrating it produced one lesson worth passing on. The closest any bad question came was "what does Matt think about Kubernetes?", which scored 0.437 against this very article, because a section above names Kubernetes as its refusal example. Then I drafted a few more mentions, rebuilt the index, and watched the same question sail past the threshold on the strength of prose about refusing it. An essay that lists the questions it refuses is a magnet for exactly those questions. So this article sits out of the semantic index: the keyword pass still finds it, but its paragraphs don't get to vouch for topics they only mention while turning them away.
If the index goes missing or the model fails to load, the keyword scorer carries every request alone. Semantic search here is an upgrade, not a dependency.
Quotation, not paraphrase
Alongside the excerpts, the tool tells the calling agent to quote the passages with their citations rather than paraphrase them as my current opinion. Whether the agent obeys is outside my control, but the provenance travels with the payload.
Everything ask_matt returns is visible to any human visiting the cited page. The endpoint holds no hidden prompts, private context or machine-only instructions; every answer can be verified by opening the source URL. That matters for trust, and it matters for security: an endpoint that only returns published, human-visible content cannot quietly smuggle instructions to another agent.
The numbers desk
The first version of this tool had a blind spot, and it took exactly one question to find it: "What is Matt's rating?"
The honest answer is 2249, FIDE standard, a peak I now defend by not playing. But the writing never states it, so the tool did precisely what it was designed to do and refused. Technically correct, practically useless: the number sits on the hobbies page in plain view, published by the same site that was busy insisting it had nothing to say.
So ask_matt now has a second source. When a question asks for a number, my chess rating, the running totals and personal bests, the chess centre, the response includes the matching block of hand-kept data alongside any excerpts. The block is labelled as data rather than quotation, because the distinction matters: an excerpt is something I wrote, a figure is something I measured. Each block carries the date a human last checked it, and the running numbers ride the same live Strava feed as the rest of the site.
The refusal rule hasn't moved. Ask about something neither the writing nor the data covers and you still get answered: false. The tool has simply stopped refusing questions the site already answers three clicks away.
Try it yourself
The terminal below isn't a mock-up. Type a question or choose a suggestion and it shows the exact curl command sent to the live endpoint before displaying the raw response. If you're suspicious, copy the command into your own terminal; you'll get exactly the same result.
# ask_matt: answers come from the published writing or the hand-kept numbers, or not at all. # Press a number, or type your own question at the prompt.
Point your own MCP client at https://mdwebb.io/mcp/ and you'll find ask_matt alongside tools for browsing articles, projects, my CV and the rest of the site's published content. More details are on the For Agents page.
Not a chatbot
To be clear about what this isn't: I'm not building a chatbot. Bolting a language model onto the back and letting the site chat would be the obvious next step, and the moment it generates prose the answers stop being my writing and become a plausible imitation of it. I'd rather publish another article.
Every answer this endpoint gives traces back to something I've actually written, or to a figure I keep by hand. Everything else is a refusal. That sounds limiting. I think it's the feature.
Further reading
- Model Context Protocol specification: particularly the Streamable HTTP transport.
- llms.txt: a simple convention for helping language models discover content.
- Introducing the Model Context Protocol: the original announcement from Anthropic.
- all-MiniLM-L6-v2: twenty-three megabytes of opinions about which sentences mean the same thing.
- Transformers.js: runs the model in plain JavaScript, which still feels faintly illegal.
Keep reading
New writing, now and then
Occasional notes on platform engineering, building dependable software and that constant buzz word we doom scroll past on LinkedIn! No cadence promised.