# Minto.Studio

*Agents sent to evaluate an agency site can only scrape and guess, so my own studio's site answers over MCP.*

**Category:** WEB PLATFORM / AGENT INFRASTRUCTURE
**Status:** in progress
**Period:** April – July 2026
**Built by:** HM Ammar, Digital Architect & Head of IT, Kuala Lumpur

**Stack:** Next.js 14 (App Router), TypeScript, Model Context Protocol, JSON-RPC 2.0, OAuth 2.0 client credentials, RS256 JWT / JWKS, WebMCP (navigator.modelContext), Resend

## The challenge

Agency websites are written for human eyes. An agent sent to evaluate one scrapes marketing HTML and guesses. Minto is my own studio, so I built the site to demonstrate the argument rather than make it. Then an audit I ran across my own build came back with something worse than a styling note: I had computed the per-industry metrics on thirty pages from a position in an alphabetical list, and my contact form was dropping every enquiry into a console log while the interface promised a reply.

## What I built

I built a Next.js App Router site with a second, machine-readable surface running alongside the pages. Page routes on an allowlist answer Accept: text/markdown with clean markdown instead of HTML, and /mcp is a real JSON-RPC 2.0 MCP endpoint exposing three tools and three resources over the service catalogue, gated by an OAuth 2.0 client-credentials server I wrote by hand on Node crypto. A .well-known suite carries the MCP server card, an RFC 9727 api-catalog linkset, an OpenAPI 3.1 description and four hosted agent skills with sha256 digests. In the July pass I acted on the audit: I deleted the index-derived metrics and replaced them with plain words behind an explicit disclaimer, and wired the contact form to Resend with graceful degradation when no key is present.

## The detail

### Markdown on request

Middleware I wrote inspects the Accept header on an allowlist of page routes and rewrites matching requests to a markdown renderer, returning text/markdown with Vary: Accept and an x-markdown-tokens length header. The homepage answers in 573 words of structured markdown instead of 158 KB of HTML. I have not finished per-page negotiation: the rewrite loses its __pathname parameter at runtime, so every route currently returns the site-overview document, even though my renderer produces the correct page when called directly.

### The MCP endpoint

On /mcp I implemented initialize, ping, tools/list, tools/call, resources/list and resources/read over JSON-RPC, with notification handling, an origin allowlist and CORS. Tools carry input schemas and return both text content and structuredContent. An unauthenticated call gets a 401 whose WWW-Authenticate header names the authorization server and the protected-resource metadata, so an agent can bootstrap itself from a single failed request.

### OAuth by hand

No auth library. I wrote lib/oauth.ts to build and verify RS256 JWTs on Node crypto directly: base64url header and payload, RSA-SHA256 signing, a public JWK exported for /.well-known/jwks.json, issuer, audience and expiry all checked on the way back in, scopes intersected against the configured allowlist. With no signing key configured the token endpoint returns 503 with a stated reason rather than minting unsigned tokens, and the server card reports its own status as pending_credentials.

### In-browser tools

A client component I wrote declares navigation tools on navigator.modelContext, so an agent already driving the browser can jump to a service or case study without guessing URLs. I generate the service and case-study enums from the same data module the pages render from, which means a route that does not exist cannot be offered as a tool argument. The industry enum is still a list I typed by hand and does not carry that guarantee.

### Removing the numbers

The audit found that the per-industry metrics across thirty pages were literally `42 + offset`%, where offset was the industry's position in an alphabetical list. Rather than sourcing better numbers I deleted them; my builder now returns words such as "Less manual workload" behind an explicit illustrative-outcomes disclaimer. The fix is half done. Eight case-study figures I wrote by hand are still on the site, and they are the next thing to go.

## The outcome

The site builds clean and runs locally: six service pages, thirty programmatic service×industry pages, eight case-study routes, and the full .well-known discovery suite. I checked it against a local production build: the MCP server card, the api-catalog linkset, the OpenAPI 3.1 description, the agent-skills index with its sha256 digests and the 401 challenge on /mcp all respond correctly. Markdown negotiation returns markdown, but not yet the right page's markdown. I have not deployed it. The minto.studio domain does not resolve and the repository carries no deployment configuration.

---

[All work](https://hmammar.world/) · [HTML version](https://hmammar.world/work/minto-studio.html)
