krishna@
~/projects
privateproduct · 2026

Bishwopatra

An MCP server with a governed agent behind it.

01 // overview

Private news platform at SoftUp whose admin runs on an MCP server — the platform’s own operations exposed as governed tools, a provider-portable plan → act → observe agent loop over them, and retrieval on pgvector so the agent finds the right operation when the keywords do not match. Built solo.

02 // the.story

the shape of the problem

A newsroom backend is a wide surface: stories, sections, media, comments, moderation, scheduling, ads, and a migration path off an old WordPress install. All of it already exists as API operations with permissions attached.

So the interesting question was never "can a model write an article." It was whether a model can be handed the existing operations of a production system without becoming a liability. That is a governance problem before it is an AI problem, and governance is most of what this project actually is.

tools, not prompts

The platform exposes its operations as MCP tools — the module is around forty-six files — so a client connects with an operator's own credentials and sees exactly the operations that operator is allowed to perform. Nothing is reachable by a model that isn't already reachable by the human it acts for.

Every invocation, wherever it came from, goes through one governed invoker:

  • Scope gating. A run declares what it is for. Tools outside that scope aren't callable, and abilities resolve per user rather than per agent.
  • Plan and confirm before writes. Reads run. Writes produce a plan the human approves — described in ordinary words — before anything changes.
  • Always stop on destructive tools. A small explicit set can never be auto-approved, whatever the run's budget or scope says.
  • A transcript. Every step, argument and result is recorded, so "what did it do" has an answer rather than a reconstruction.
  • Budgets. Step and token ceilings, so a confused run ends instead of looping.

the loop, and why it avoids native tool-calling

The in-admin chat is an MCP client we own, which means we run the plan → act → observe loop ourselves rather than handing it to the provider.

The obvious way to do that is native tool-calling. The problem with native tool-calling is that every provider spells it differently — different request shape, different response shape, different failure modes — so a loop written against one of them is a loop you rewrite when the model changes. Each turn therefore asks the model for a structured next action instead: either a tool call or a final answer, validated against a schema. Swapping Gemini for OpenAI, Anthropic or DeepSeek is a provider entry, not an agent rewrite.

The schema is deliberately lenient about where the model puts things. Models will cheerfully return arguments on the call itself rather than inside an args object, and a strict schema turns that into a failed run for no good reason. It parses loosely and normalises into one shape.

Discovery is built in for the same reason: before deciding, the model is handed a short, ability-filtered list of tools relevant to the instruction, so it picks a real operation instead of inventing a plausible one.

retrieval, and the part I would defend hardest

Discovery by keyword fails the first time somebody asks for something in different words than the tool uses. So the tool capability cards are embedded and retrieved semantically: 768-dimension vectors from Gemini's text-embedding-004, stored in Postgres as vector(768) with an HNSW index over vector_cosine_ops, queried by cosine distance in raw SQL.

The decision I actually care about is what happens when none of that is available. Embeddings need an API key. pgvector is an extension somebody has to install on the database. Either can be missing in a given environment — so every retrieval path degrades: no embeddings or no extension gives an empty semantic result, an empty semantic result means fall back to lexical search, and the agent keeps working. Retrieval makes it better; its absence doesn't make it broken. An availability probe reports which mode is live rather than leaving it to be guessed.

the rest of it

Underneath the AI layer is an ordinary large backend held to the same standard as everything else here: layered architecture with the dependency rule enforced by the linter rather than by review, Postgres through Prisma, Redis for sessions, queues, throttling and cache, passkeys and MFA on auth, permissions through CASL, audit logging that never blocks a request, Prometheus metrics, and the WordPress migration path for the archive.

The platform is private, so this is an honest summary rather than a tour.

what stays with me

Governance is the product. Anyone can wire a model to an API. The reason this one is allowed near production data is the invoker in the middle — scope, confirmation, always-stop, transcript. Those constraints aren't friction bolted onto the AI feature; they are the feature.

Portability is cheap if you buy it early. Choosing structured output over native tool-calling cost about a day and bought provider independence at a moment when the provider landscape changes every few months.

Degrade, don't fail. RAG is the newest and least certain part of the system, so it was built assuming it might not be there at all. That is the same instinct as a health check or a graceful shutdown, applied to a feature fashionable enough to make people forget it.