Harborops is the warehouse API I have been on-call for since 2019. Django 3.2, DRF, a Celery fleet, a half-finished django-ninja module from 2023, and zero OpenAPI. The closest thing we have to a spec is a Confluence page last edited during the first lockdown.
I pointed Elva https://getelva.ai at that repo on a Sunday and tried to turn a slice of it into something an agent could call without me sitting in Slack translating field names.
Disclosure: Elva gave me 30 days of Startup-plan credits and asked for a write-up on my own site. They did not pay me, they did not review this, and I had not used the product before this session.
The repo I actually pointed it at
Private GitHub repo, ~890 Python files that matter. Public surface is /api/v1/. Internally we also serve health checks off the WSGI wrapper, a Channels consumer for live job progress, and a dusty XML-RPC path a desktop client still hits.
There is no openapi.yaml. DRF spectacular was on a spike branch that died in 2022. If Elva only works when you already have a spec, this is the wrong repo. That was the point.
Elva launched this month, September 2026. Most tools in this category still start from a file you already wrote. I wanted to see what happens when you start from git instead.
Setup was npx elva init, GitHub App on the repo, then SYNC. First pass took a little over eleven minutes. The marketing number of "42 seconds" is a different size of tree.
They scanned the code and built a catalog
Elva walked the Django URL conf and serializers and produced OpenAPI 3.1 plus a catalog. No spec required. Harborops did not have one.
SYNC reported 41 endpoints, grouped by AI into seven collections: Orders, Inventory, Shipments, Carriers, ASN, Billing, Internal. Each collection has flags. Unpaginated list. Missing description. PII on a partner facing payload. No auth class.
I have lived in this repo for years and I have never had that screen. Not a YAML file in a branch. A dashboard of the whole public surface, categorized, with flags on the rows that will hurt you.
Open one endpoint and you get more than a path:
- the generated internal doc
- an API editor sitting on the spec they derived
- a consumer list, services and MCP clients that appear to call this route
- the flags and the insight scores for that operation
The consumer map is the ambitious part. On Harborops it marked three internal services on GET /api/v1/orders/. Two are real. One was a test helper that imports the serializer and never hits HTTP. They are inferring usage from the repo, not from production traffic. Early, and still the right idea. Most catalogs cannot answer who depends on a field.
Insights: design, security, AI readiness
After SYNC the catalog scored:
| Dimension | Score |
|---|---|
| Design | 71% |
| Developer experience | 54% |
| AI readiness | 38% |
| Security | 61% |
| Performance | 48% |
| Agent-ready | 41% (D) |
That matched the repo. We never wrote descriptions. Eleven responses use SerializerMethodField with no help_text. Four routes still use AllowAny behind a VPN. GET /api/v1/orders/ will return 18,000 rows.
I applied the AI fixes. Twenty nine descriptions appeared. Several method fields got types. Auth on billing got documented. AI readiness moved to 67%. I read every generated description on the 14 endpoints I later put in a contract.
Two held up. POST /api/v1/orders/{id}/ship/ correctly said it creates a shipment, needs carrier and service_level, and is not idempotent. GET /api/v1/inventory/skus/ correctly split on_hand from available.
Two were wrong in a way that matters for agents. hold_code is a CharField we treat as an enum (WEATHER, SHORT, CREDIT, DAMAGE, CUSTOMS, OTHER). The generated text called it "a numeric warehouse hold identifier used by the WMS putaway loop." That is bin_hold_id on a different serializer. eta was called a "guaranteed delivery timestamp." It is a carrier estimate.
The scores are honest. The generated prose is a first draft. Review it like a pull request from someone who has never been in the warehouse.
What the scanner missed
The 41 endpoints were real. I checked them against apps/*/urls.py. The misses are the more useful data.
Should have caught these:
-
POST /api/v1/webhooks/stripe/inbilling/hooks.py.@csrf_exemptfunction view, included through a tiny local package. The include never resolved. -
POST /api/v1/inventory/bulk-adjust/. A DRF@actionwhoseurl_pathis a constant imported frominventory.constants. The action exists. The path never appeared. -
GET /api/v1/reports/cycle-count.xlsx. AnAPIViewthat returns aFileResponse. No serializer. Dropped.
Fair misses:
-
GET /internal/readyandGET /internal/live, mounted on the WSGI wrapper, not in Django urls. -
ws/jobs/{id}/, a Channels consumer. -
/legacy/xmlrpc/, not REST.
Vanilla DefaultRouter ViewSets scan well. A five year old Django app is not only that. I did not find a "this path exists, add it" control on first pass, so I patched the spec by hand. Discovery from source is the innovation. A one click add for the last messy routes is the gap.
Contracts are the serious feature
This is the point where I stopped treating Sunday as a toy session.
You pick a use case (partner, internal, public, or AI agent / MCP), pick tools and fields, and you can let AI propose the cut. Hidden fields stay hidden. PII can be redacted. From that contract you generate OpenAPI and choose a destination: a docs tool, Postman, a mock server, or an MCP server.
I built 3pl-partner v1 for the 3PL that already polls shipments. Fourteen endpoints. On GET /api/v1/shipments/{id}/ I hid internal_cost and margin_bps, and redacted customer_email.
The part that matters after publish: if the source changes, Elva diffs the new catalog against the contract, notifies you of drift, and you review before a new version ships. The contract is versioned. The destination artifacts do not silently move under a partner.
I tested that the rude way. Branch, rename tracking_number to carrier_tracking in the shipment serializer, open PR #218.
Elva re-ran SYNC, classified two removals as breaking, and blocked publish of 3pl-partner v1. elva-bot commented on the PR. The check Elva contract gate / 3pl-partner went red. I could still merge to main. We do not require that check yet. The hosted MCP server and the partner artifacts did not update. That split is correct. Branch protection is yours. Contract publish is theirs.
A follow up PR that adds carrier_tracking and keeps tracking_number classified as compatible. Additive is fine. Rename in place is a version bump. One early wrinkle: the impact line said "1 partner build" when the only consumer of that contract was me. The counter is inferred. It over counted.
MCP: pick tools, they handle the client
From the contract I generated an MCP server, harborops-3pl. Flow:
- Select the tools the agent is allowed to see.
- Configure authorization per client. Scoped keys, OAuth if you want it.
- Get a client facing install page.
- Elva sits in front of those tool calls. Identity, scope, field redaction from the contract, rate limit, log. For agent traffic they are acting as a gateway. Your Django process, nginx, and existing bearer tokens for human clients do not move.
I installed it in Cursor with a scoped key. The install page offered OAuth. I skipped that for a Sunday staging test. Write tools needed an extra scope I only noticed after a denied call. Document that on the install page more loudly.
Before I handed anything to Cursor I used the MCP playground. You run the same server against different models, Claude, Cursor, GPT-4.1 in my session, without burning a customer key. That is the feature a launch week product does not usually have. I found the warehouse_code enum problem there, not in front of a partner.
They also ship an MCP feedback tool. The agent can leave a note when a call goes sideways: wrong shape, missing filter, description that lied. I triggered it after the failed ASN create. The note landed next to the call in the log. If you are going to let models hit a warehouse API, you want the model to be able to say "this schema does not match the live server" without opening a Slack thread.
Then four calls from Cursor against staging:
- "Which shipments are in transit to the Oakland node?"
list_shipments. Status worked.dest_nodeexists on the list view and never became a tool argument. 188ms. - "Open shipment SHP-18422."
get_shipment. Partner shaped object: nointernal_cost, nomargin_bps, email stripped. The contract is behavior, not a PDF. 94ms. - "Create an ASN for PO-9921 into warehouse Oakland."
create_asn. Failed onwarehouse_code. The live API acceptsOAK-3or the slugoakland. The tool only accepts the code as a tight enum. Playground had already shown this. 240ms. The agent feedback note is what I kept. - "What SKUs at OAK-3 are under 20 available?"
list_inventory_skus. Worked. Noavailable_ltargument even though the view supportsavailable_max. 156ms.
The live log is the other half of the gateway. Slowest tools, error rate, which client used which tool, no sampling. When create_asn failed I did not have to ask Cursor what it sent.
What I would tighten, and why I would still turn it on
I went looking for a scanner. I ended up with a catalog, an editor, insight scores, a versioned contract that can ship to docs or Postman or a mock server or MCP, a gateway in front of the agent, a playground, and a way for the agent to complain when the schema is wrong. That is a lot for a product that launched this month. It is also why I am writing this down instead of closing the tab.
What I would fix next, in order:
- Let me add a missed route without exporting YAML. Django apps grow paths through string includes, imported
url_pathconstants, and views with no serializer. Those three Harborops misses are the test. - Keep generated descriptions behind a review.
hold_codeandetawere confident and wrong. Agents copy confident sentences. - Map list query params onto tool inputs.
dest_nodeandavailable_maxexist in the view and vanished in the tool. - Separate inferred consumers from observed consumers, or label the test helper that never makes an HTTP call.
- Say on the install page which scopes a write tool needs, before the first deny.
None of those are reasons to wait a year. They are reasons to treat the first SYNC as a draft catalog, which is how you should treat any scan of a five year old service.
I would keep Elva on Harborops with the contract gate required on the 3PL paths. I would not merge Apply AI fixes to main without reading the diff. I would run every new MCP server through the playground on two models before I sent an install link.
If your spec is already perfect and your only job is hosting MCP, you have other options. Harborops is not that repo. No spec, mixed routers, a partner who will notice a renamed field. This is the first tool I have pointed at that tree that started at the git remote and ended at a versioned contract plus a logged agent gateway. For something that launched this month, that is enough reason to try it on your own messy service, not on their demo.



Top comments (0)