cd /news/ai-tools/i-built-an-mcp-server-and-had-to-des… · home topics ai-tools article
[ARTICLE · art-50754] src=sharemypage.app ↗ pub= topic=ai-tools verified=true sentiment=· neutral

I built an MCP server and had to design UX with no screen

A developer built an MCP server called ShareMyPage that lets Claude create, edit, and comment on hosted web pages, with the entire user experience designed solely through tool names, input schemas, and return messages since there is no graphical interface. The server runs as a single Next.js route on Vercel Fluid Compute, exposing twelve tools, and the developer faced the challenge of measuring UX without a screen, including a near-miss where an analytics hookup risked leaking page content.

read13 min views1 publishedJul 8, 2026
I built an MCP server and had to design UX with no screen
Image: source

July 3, 2026 · 15 min read

I built an MCP server that lets Claude create, edit, and comment on real hosted pages. There is no screen, so every UX decision lives in a tool schema, an error message, or a single line of returned text, and measuring whether any of it actually works needed the same kind of design. Here is both halves of that story.

The code that makes this work is not the hard part. The hard part was the experience. An MCP server has no screen, no button, no hover state, no red versus grey. Every decision that would normally be a UI decision has to be made instead in a tool's name, its input schema, or the single sentence it hands back. That is the whole thing, out in the open: every decision that shapes what an agent actually experiences using it. Then a harder question showed up after I shipped it. How do you know any of that is working, when nothing about the experience ever appears on a screen for you to watch? Both halves live here, including the moment a routine analytics hookup nearly started leaking real page content out of the server before I caught it.

A tool is the entire interface #

An MCP exposes a set of tools to an AI client. Each tool has a name, a schema for its arguments, and a function that runs on your server. Claude connects, reads what is available, and calls a tool on the user's behalf. That much is mechanism.

The design problem sits one layer up. Everything a person will ever perceive about this server comes from three surfaces: a tool's name and description, the shape of its schema, and the sentence it returns. Does it read as careful or careless? Fast or fumbling? Trustworthy or reckless? All three answers live in those same three places. There is no fourth surface. No layout, no icon, no color, no spacing. Get those three things wrong and the experience is wrong. No amount of correct backend logic fixes that afterward.

The request path itself is not specific to this server. It is the same shape for any MCP: a person asks in plain language, Claude picks a tool, the connector carries the call over HTTP with authentication attached, the server runs the tool, and it reaches the actual data. What varies from one MCP to the next is entirely in how well those three surfaces are designed.

Twelve tools, one route handler file, reachable two ways (an OAuth connection or a scoped token), and no extra servers to run.

The whole server is one route handler #

There is no separate service behind this. The MCP lives at a single Next.js route, the same app that already serves the product. A helper library, mcp-handler

, does the protocol plumbing. You register tools, it speaks MCP. Keeping the surface this small also keeps it something I can hold in my head as one coherent set of twelve decisions, not twelve unrelated ones.

Where this is needed.Any HTTP host that can run a serverless function works. This one runs on Vercel Fluid Compute, so there is no long lived server to babysit. The connector calls the URL, the function wakes, runs a tool, returns.

The schema is the form the AI fills out #

Write it clearly and Claude calls create_page

with the right shape on the first attempt. Write it vaguely and every call turns into a small negotiation the person waiting on the other end has to sit through: a guess, a correction, a retry. That difference is entirely design work, done in field names and descriptions instead of labels and placeholder text.

The annotations do a second, quieter job. destructiveHint

and readOnlyHint

are the tool-calling equivalent of a red delete button versus a plain grey one. They tell the client how much friction an action deserves before it just does the thing versus stopping to confirm first.

The reply is the interface.A tool has no screen to render a result on, so its return value has to carry both the data and the message. "Created. Shareable URL: ..." is something Claude can read once and repeat back to the person it is helping, verbatim. A raw JSON object, or worse an unstructured 500, forces the model to interpret and reword it. That interpretation step is exactly where confident sounding nonsense creeps in.

Two doors, one scoped identity #

People reach the MCP two ways. Inside Claude, an OAuth connection identifies the user, and their workspace gets resolved from that identity. From a script, a scoped token, an sp_live_

secret, carries its own workspace. Both funnel through one function that produces a single trusted context.

This is the single most important line in the whole server. A caller never passes "which workspace." It is derived from the credential. That one rule means a leaked or malicious token still cannot reach across into someone else's data. It fails closed.

None of this is ever visible to the person using the tool. That is exactly the point. The parts of an experience that matter most are often the ones nobody ever sees. Get them wrong and the result is not a rough edge. It is an incident.

The scoped token itself is never stored in plaintext either. It carries about 240 bits of entropy, nothing close to brute-forceable, so a fast SHA-256 hash is the right call here, not a slow password hash meant for guessable secrets. Only that hash is ever stored. The plaintext is shown exactly once, at creation.

Twelve tools, grouped by what a person is trying to do #

This is information architecture, not database design. If the tools mirrored the schema, there would be one for pages, one for folders, one for comments. Both the AI and anyone reading the tool list would then have to reconstruct the actual workflow from the data model themselves. Instead the twelve tools are grouped by intent: look at what is there, make something, fix one thing without redoing it, and talk it through with a teammate.

Look around

Tool What it does
status who am I connected as, and which workspace
list_pages everything in the workspace
list_folders the folder structure
get_page one page's current HTML

Make and change

Tool What it does
create_page a new page from HTML
update_page replace the whole document
delete_page remove a page, marked destructive

Edit in place

Tool What it does
patch_page change one exact span, leave the rest untouched

Collaborate

Tool What it does
list_comments read comments and their anchors
comment_on_page post a note or reply to a thread
resolve_comment close feedback once it is handled
send_feedback tell the ShareMyPage team something is confusing, broken, or missing

The decision with the most obvious effect on how this feels to use #

Early on, every edit meant the AI had to hold the entire page in its context, change a few words, and send all of it back. On a large page, that is long enough that a person waiting on the other end notices it. It also burns through a long chat's context window fast enough to end the session early. Both of those are UX problems dressed up as engineering ones. patch_page

exists because of that: the agent sends a tiny find and replace, and the document never leaves the server.

Same one word edit on a 38 KB page: update_page

resends 38,400 characters. patch_page

sends 41 characters. By reference, not by value. The difference is felt directly, as how long someone waits for a reply.

Where this is needed.Any MCP that edits large text or files. The speed is the obvious win here. The guard is the one that protects trust. An AI editing by find and replace will eventually hit an ambiguous match. The right response at that moment is a clear refusal, not a page quietly mangled somewhere the person only discovers later.

A comment becomes an instruction the agent can target #

A comment is someone's intent, written once, in their own words. Turning it into a find

/replace

call is a translation the person leaving feedback never has to think about. They write "make this final." The agent locates the exact span the anchor points at, then closes the loop by resolving the thread. That handoff, from a loose human note to a precise, verifiable edit, is the entire value of exposing the anchor at all.

The reply is the only UI this server has #

None of what follows shows up in a screenshot, because there is no screen. It shows up entirely in what each tool says back. That is the one place a carefully built MCP and a merely functional one are impossible to tell apart, looking at the schema alone.

status          -> Connected as Henning · workspace "NewStore" · plan pro · 42 page(s).
create_page     -> Created. Shareable URL: https://sharemypage.app/p/gckcg27xwxwntn
comment_on_page -> Comment posted.
(no token)      -> 401 Unauthorized
(rate limited)  -> Rate limit exceeded for this token. Slow down a moment.

A cheap "who am I." The status

tool does nothing but confirm the connection. It is the first thing a person runs, and it removes all doubt about which workspace and plan they are on before they act. That is the same job a connection indicator does in any ordinary app, just spoken instead of shown.

Everything is reversible. Every edit writes a new version and keeps the old one. That is why update_page

and patch_page

are marked not destructive: nothing is ever truly overwritten. That is what lets an agent act with less hesitation, and a person undo their way out of a bad edit instead of asking for one up front.

One honest gotcha #

The client asks "what tools do you have" once, when it connects, and remembers the answer. So shipping a new tool does not reach an already connected client, and a fresh chat will not pick it up on its own. A serverless server also cannot push the update, because there is no open connection to push through.

The fix is one click.In the Claude connector settings there is a "Refresh tools list" action. It re-fetches the tools without a full disconnect. Worth knowing before you spend an afternoon wondering why your new tool is invisible. Behavior changes inside an existing tool need no refresh at all.

Measuring an interface with no screen to watch #

A normal product's UX shows up in things you can watch happen: a rage click, an abandoned form, a support ticket. None of that exists here. An agent either succeeds or fails silently, and a person only ever sees whatever the model decided to say about it. Answering "is this actually good" needed its own design, on the server, same as everything else in this piece.

Two decisions, both server driven, since nothing about a chat carries over to the next one.

Track every call, but only what is safe to leave the building. PostHog's own MCP analytics SDK wraps the server and captures every tool call, discovery request, and initialize handshake automatically: tool name, duration, whether it errored. Its default capture also includes the raw arguments and raw response of every call, and several of these tools carry real page content there. get_page

returns a full document. comment_on_page

's body is someone's actual comment. Before any of that leaves the process, a hook strips both fields unconditionally.

That is a real answer to "where does this actually hurt": which tool nobody calls, which one errors more than the rest, whether a session ends right after a failure.

Ask, but only once real usage earns it, and only through a reply that was already happening. A rating prompt on every connection is exactly the friction this whole design has tried to avoid. So there is a send_feedback

tool, gated in its own description: only call this once the person agrees, summarize what happened instead of pasting page content. But a chat has no memory of the last one, so the invitation to use it cannot live in a system prompt either. It has to come from the server itself. The threshold: twenty tool calls, then a quiet thirty days. Cross it, and status

's own reply grows one extra line.

Not a popup. Not a forced choice. A sentence appended to a reply that was already going to be read.

If you build your own #

None of this is protocol specific. It is what building an MCP that people actually trust to touch their work ends up teaching, and most of the lessons are design lessons that happen to be enforced in code.

There is no screen. Every decision you would normally make in a mockup, you now make in a tool description, a schema field, or a returned string. Give that the same seriousness.The schema is the form. Write it so the AI can fill it out correctly without a clarifying question first.Annotate intent.readOnlyHint

anddestructiveHint

are your confirmation dialogs. Use them instead of hoping the model infers the stakes from context.Scope the principal, never the request. Derive the workspace from the credential, not from an argument the caller controls. Fail closed.The reply is the whole interface. Write it like someone is going to read it out loud to a person, because someone will.Respect the model's context. Prefer a small find and replace over resending a whole document. Context is the scarce resource, and the person waiting feels every extra second of it.Make everything reversible. Undo is a design decision before it is an engineering one. It is what lets an agent act with confidence instead of asking permission for everything.Give people a cheap "who am I." A read only status tool removes doubt before anyone makes a change. Confidence beats guessing.Remember discovery is cached. New tools need a refresh, not faith. Tell your users where that button is.Redact by default, not by allowlist. If any tool can carry real content in its arguments or response, strip those fields unconditionally before they leave the process. A blanket rule cannot miss a field the way a per-tool list can.Earn the ask, then let the server do the asking. A chat has no memory of the last one, so a feedback prompt that depends on the client remembering it will not survive to the next session. Track real usage server side and surface the invitation in a reply that was already happening.

It is built with mcp-handler

on Vercel Fluid Compute, the Next.js App Router, Drizzle and Neon Postgres, Vercel Blob for the HTML bodies, and Clerk for auth. Want to see it live? Connect it in Claude, try status

a few times, and watch for the line at the bottom of the reply.

── more in #ai-tools 4 stories · sorted by recency
── more on @sharemypage 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-an-mcp-serve…] indexed:0 read:13min 2026-07-08 ·