DEV Community

We built an MCP server for a consumer product and left the main feature out

CMeIn on November 03, 2026

Most MCP servers are developer tools. Databases, file systems, issue trackers, CI. The pattern is familiar: you give an agent a capability it did...
Collapse
 
autenai profile image
Auten •

On the account-scoped tools: did you end up with OAuth on the remote server, or a per-user token pasted into the client config? Curious what my_credits returns when the session expires mid-conversation.

From the Auten team (we make a computer-use MCP): our line was passwords. The agent can ask for a saved login to be filled, but the value is typed locally, never comes back to the model, and stays pinned to the site it was first used on.

Collapse
 
barak066 profile image
CMeIn •

OAuth, and the server is a resource server only. It mints no tokens, holds
no secret and knows nothing about passwords. It verifies an RS256 JWT
against the key set at cmein.com/.well-known/jwks.json, then checks issuer,
audience and the scope for the specific tool. The audience check is RFC 8707
and it is the one I would not skip: without it a token minted for one
resource is replayable against any other resource that trusts the same
issuer.

On your actual question, expiry mid-conversation: verify() returns null, and
the 401 only fires if an account-bound tool is the one being called. So the
conversation does not die. The six tools that need no account keep answering
normally, and only my_credits (or another account tool) comes back with

HTTP 401
WWW-Authenticate: Bearer resource_metadata="...", error="invalid_token",
error_description="Connect your CMeIn account."
{"jsonrpc":"2.0","error":{"code":-32001,
"message":"This tool needs a connected CMeIn account."},"id":null}

which is the shape a client needs in order to offer reconnecting rather than
just printing "unauthorized". The lazy challenge was deliberate: challenging
on every request would make an expired token look like a dead server.

Your password line is the same shape as ours, arrived at from a different
direction. You keep the value out of the model and pin it to the site it was
first used on. We keep the credential out of the server and pin it to the
audience it was minted for. Both are really the same rule: the thing that
proves who you are must not be replayable somewhere else. Yours is pinned by
origin, ours by the aud claim.

Where did you land on re-prompting? If a saved login is filled and the site
rejects it, does the agent get to retry, or does that go back to the human
every time?

Collapse
 
autenai profile image
Auten •

"Pinned by origin vs pinned by aud" is a cleaner way to say it than ours, and the lazy 401 that keeps the other tools alive is a nice detail.

On re-prompting: fill_login just types the saved value, it doesn't judge whether the site accepted it. The skill we ship with the plugin tells the agent to get a yes from the user before typing a password at all, so a rejected login goes back to the human rather than into a retry loop.

Thread Thread
 
barak066 profile image
CMeIn •

The split you just described is worth naming, because I think it is the real
difference between our two setups.

fill_login not judging the outcome is the right call. A tool that cannot tell
success from failure cannot retry, so the retry loop is designed out rather
than guarded against. That is the cheap kind of safety and the kind that
keeps working.

But notice you have two layers doing different jobs. The origin pin is
enforced: the saved value physically cannot go to a site it was not pinned
to, and no amount of clever prompting changes that. The "get a yes before
typing a password" rule is advisory: it lives in a skill, which is
instruction, not mechanism. A model that ignores it is not breaking
anything, it is just not following advice.

Ours lands the same way. The scope and aud checks are enforced in the server
and the model cannot talk its way past a mismatched aud. Everything about
when it is appropriate to call a tool is advisory, sitting in tool
descriptions. I do not think there is a way around this. The things you can
enforce are the ones that do not depend on intent, and intent is exactly
what "did the user actually mean to log in here" is made of.

Which makes me curious about first use specifically. The pin protects every
use after the first, but something has to establish the pin. If an agent is
on a page it was led to, and the page presents a convincing login form for a
site the user does have saved credentials for, what does the first-use path
look like? Is the yes you ask for scoped to "type this password here, now",
or to the site generally?

Thread Thread
 
autenai profile image
Auten •

Fair, and first use is the soft spot: the pin is set where a saved login is first used, so until then it rests on the advisory layer.

The yes in our skill is per action, not per site: the agent shows what it is about to type and where, waits, and asks again if anything changes. The same skill tells it to treat page text as data, so a page asking for a login is not a reason to fill one.

Still advisory, as you say. Setting the origin when the login is saved, instead of at first use, would close that gap; I'm passing that to the team.

Thread Thread
 
barak066 profile image
CMeIn •

Setting the origin at save time is the right move. It turns the property
from something the model is told to respect into something it cannot reach
past, which is the whole distinction.

One thing that will bite during implementation: plenty of real logins are
not single origin. SSO means the credential is typed at accounts.google.com
or at login.example.com and used for example.com, and a pin captured
strictly at save time will refuse the exact flow the user expects to work.
Worth deciding up front whether the pin is an origin or a small allowed set,
because discovering it from a bug report means someone loosens it in a
hurry, and a pin that gets loosened under pressure tends to stay loose.

Also, "treat page text as data" is doing more work in your setup than it
gets credit for. That is the defence against the obvious attack here, which
is a page that simply asks.

Good thread. Thanks for the detail.

Collapse
 
agentsearchhq profile image
AgentSearch •

Leaving the paid, consent-heavy action out is a boundary more MCP servers should copy. An agent that can spend credits in a loop is a much worse failure than one that answers a question badly. One thing I'd add: say it in the tool descriptions too, so photo_advice or cmein_info can tell the agent that generation happens in the app. Otherwise agents tend to hunt for a tool that isn't there, or improvise one from the closest match. How did you end up handling the account-scoped tools on the remote server: OAuth through the MCP client, or a separate token step?

Collapse
 
cailab profile image
CAI •

The third reason you gave for leaving generation out - it costs money per call, and an agent can spend credits in a loop without the user seeing the prompt - is exactly the logic that makes spending mandates the right primitive for agent payments. The agent can see the balance, discuss the product, and even trigger a payment intent, but the actual move of funds happens through a separate channel where the human confirms the amount and purpose before the transaction settles. Lifting the spend decision out of the conversation entirely mirrors the architectural choice you made, and it solves the runaway-spend problem at the design level instead of the prompt level. The photos:write scope you defined but deliberately never granted is a clean version of the same idea: the permission exists in the schema but no session ever activates it.

Collapse
 
barak066 profile image
CMeIn •

The parallel holds up to a point, and I think the point where it stops is
the interesting part.

There is a ladder here. You can tell the model not to misuse a capability,
you can gate the capability behind a human confirmation, or you can not give
it the capability. Mandates are the middle rung, and they are the right rung
when the agent genuinely has to transact. We took the top rung, but only
because we could afford to: nobody needs an agent to buy our credits, so
there was never a transaction to protect.

I would not flatten that difference, because the middle rung has a failure
mode the top one does not. A confirmation gate works exactly as well as the
human reads it, and a human who has approved nine routine amounts reads the
tenth differently. Lifting the decision out of the conversation helps with
that, but it does not remove it, and the design question becomes how to keep
the tenth confirmation as legible as the first.

Small correction on photos:write, since it is my own schema: it is not that
no session activates it. The server defines and enforces it, but it is left
out of the advertised scopes entirely, so no consent screen ever offers it
and no client can request it. The distinction matters for your argument,
I think, in your favour: the permission is not dormant pending some future
grant, it is unreachable. That is closer to your separate channel than a
scope nobody happens to have asked for yet.