mcpapinpmagentsaitamperlens

An MCP Server for Your API: the packaging decisions that matter

Douglas Haruo 11 min 8/26/2026

MCP (Model Context Protocol) has become the default answer to “how does an AI agent use your product”. Packaging an existing REST API as an MCP server, the package that hands that API to an agent, is a small project with disproportionately important decisions. This is the field note of tamperlens-mcp. It exposes the Tamperlens API as four tools installable via npx, and it is published to npm and to the official MCP Registry.

The usual format: the decisions with their whys and the two incidents that became guardrails. At the end, the honest read on registries as a distribution channel, less exciting and more useful than the hype.


Decision 1: minimal surface — tools, not endpoints

The API has more routes than the MCP server exposes. The server has four tools: inspect a document in full, triage one cheaply before ingesting it, check a redaction, compare two documents. An agent tool is not a mirror of an API: it is a verb a model can pick safely.

Every extra endpoint on the MCP surface is one more decision the model can get wrong. It is also one more description competing for attention in the context window, and one more path to maintain. The design question is not “what does the API do?”, it is “what are the few things an agent would do with this in a real workflow?”. The rest remains available on the REST API for people who program. MCP is for those who converse.

Correction, 26 August 2026. This section was published saying three tools, and listing three. There are four: triage_document — the cheap pre-flight that reads structure, metadata and signatures without the per-page content walk, and returns the risk band alongside the measured cost — landed in engine 1.34 on 24 August, two days before this post went live. The text did not catch up. It is precisely the failure mode of Decision 4 below, with one difference that stings: there, a test binds the published artifact to the version of the system it describes, and a blog post has no such test. The count and the list have been corrected everywhere in the text.

Decision 2: file paths, never inline base64

The most structural decision in the package: the tools accept absolute file paths, and refuse inline binary content.

The reason is cost in the wrong place. Documents arrive in megabytes. Base64 would inflate the binary by ~33% and push it through the model’s context. That means paying tokens to transport bytes the model should never read, blowing past message limits, and degrading exactly the workflow MCP was supposed to enable. With a file path, the binary goes from disk to the API through the server, and the model only carries what concerns it: the path, and later the report.

Except “the server reads local files the model names” is a sentence that should raise hairs. This is where the decision demands its security counterpart:

  • TAMPERLENS_ALLOWED_DIRS confines the reads. The server only reads inside the directories the user listed in the configuration. Without an explicit allowlist, a confused agent (or a prompt injected inside a document, an irony this product knows well) could request an inspection of ~/.ssh/id_rsa.
  • Symlinks are resolved before deciding. A symbolic link inside an allowed directory pointing outside of it is resolved and refused. A path allowlist without symlink resolution is a locked door with the key hanging in the lock.
  • And the same rigor for URLs: the SSRF-guard module validates any remote origin. An MCP server that downloads whatever it is told to is an internal-network proxy waiting to happen.

The general rule: when the agent’s convenience requires the server to touch the world (disk, network), the confinement becomes the package’s main feature, not a footnote.

Decision 3: it works without a key — on the free tier’s anonymous quota

The server works without an API key, falling back to the same anonymous quota as the product’s free checker (10 documents/hour). With a key, it uses the account.

This matters because an agent’s first contact with the tool is exploratory: someone installs it, points it at a PDF, looks at the report. Requiring signup before that moment kills the exploration; leaving it unlimited invites abuse. The anonymous quota the product already had solves both. It is also an argument for designing the free tier in the API, not in the client: every packaging (site, MCP, integrations) inherits the same funnel with no duplicated logic.

Decision 4: the package version IS the engine version — by test

The most idiosyncratic decision, born of an incident. The MCP package reports the version of the engine that answers for the analysis. A test guarantees that the npm package version is identical to the engine version. Publishing the package is asserting “this describes the behavior of this version of the detector.”

The incident that made the rule: a publish went out from a stale checkout, seven releases behind main. It pushed to npm a package whose version said one thing and whose code was another. The guardrails that remained, in layers:

  1. prepublishOnly runs the suite, and the suite includes the version test. A stale-tree publish dies before leaving the machine.
  2. The publish order is a documented contract: engine deploy → npm publish → MCP registry publish. Never out of order, because each step asserts something about the previous one.
  3. And the most unexpected guardrail lives in the backup script. The operations machine’s nightly routine runs git pull --ff-only on the main checkouts, with safeguards for switched branches, dirty trees, and mid-flight rebases. That way, “checkout forgotten in the past” stops being a state that survives until the next human publish.

The generalized lesson: a published artifact that describes another system needs a verifiable link to that system’s version. Without the link, divergence is not a risk: it is a schedule. And the defense does not live only in CI: it lives in every place where a human can operate on stale state.

Decision 5: the registry, the DNS namespace, and the mirror repo

Publishing to the official MCP Registry, the catalog where servers are listed, required two moves worth recording:

  • The namespace is proven via DNS. The name com.tamperlens/mcp was claimed with a TXT record on the domain apex. The registry verifies that whoever publishes controls the domain the name invokes. It is the right model, proving control instead of paying a fee. Publishing an “official” MCP server for your product starts in your DNS, not in your code.
  • The public repository is a dedicated mirror. The product lives in a private monorepo, and a “source” link that 404s for the public undermines the trust the registry exists to create. The package points to its own public repo. It is one more artifact to maintain, and the correct price of listing something as open.

Once published, the server was findable in the registry’s search within the same minute. Which brings us to the final part.

The honest read: the registry is not a channel — and I built it anyway

The product’s internal review records the conclusion without anesthesia: MCP registries are disconfirmed as a distribution channel. In their current state, nobody discovers a new product by browsing an MCP server registry. What they are, today, is something else: the ready answer to a procurement objection, the purchasing side of the company evaluating you.

The scene that justifies the investment is not “user discovers the product through the registry”. It is another one. A company evaluating the product asks: can our agents use this? The answer is npx tamperlens-mcp, with an official listing, a DNS-verified namespace, and a version locked to the engine. The same read applies to the automation node I packaged for another ecosystem: platform hygiene, not an acquisition channel. I built both with that expectation: small, cheap to maintain, objection-proof. That is different from building them hoping for a funnel the numbers do not support.


The checklist, for packaging your API

  1. A few verbs, not thirty endpoints. An MCP surface is an agent decision, not a REST mirror.
  2. Binary by reference (path/URL), never inline. The counterpart is mandatory: directory allowlist, symlinks resolved, SSRF guard.
  3. Work without a key, inside the free tier the API already enforces. The funnel lives in the API; the clients inherit it.
  4. Lock the package version to the described system’s version, by test. And close the stale-tree publish paths: prepublish gate, documented order, checkouts that update themselves.
  5. Namespace via DNS, a genuinely public source.
  6. Calibrated expectations: package it as a procurement answer and ecosystem hygiene. If it becomes a channel, that’s a bonus. Plan as if it won’t.

Need a custom technical project?

Architecture, TypeScript, APIs and automation, from prototype to production. The person answering your email is the one writing the code, and the deadline I promise is the one I can meet.

Send me a message →