varto
OfficialClick on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@vartofind tenders for road construction in Kyiv region"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Varto
An autonomous analyst for Ukrainian public procurement.
Varto continuously watches Prozorro, Ukraine's open public-procurement system, reads the tender documentation that humans currently read by hand, and produces a reasoned go / no-go recommendation — escalating anything uncertain to a person instead of guessing.
The name is a Ukrainian double meaning: варта — a watch or guard, and чи варто — "is it worth it".
Live: https://varto-ai.vercel.app
Status
Honest snapshot, because a portfolio repository that overstates itself is worse than one that doesn't exist.
Component | State |
Prozorro MCP server | ✅ Working — usable today from any MCP client |
Typed API client + schemas | ✅ Working, validated against recorded live responses |
Document reader ( | ✅ Working, with explicit handling for scans and legacy formats |
Deterministic pre-filter (Gate) | ✅ Working, 7 rules, zero token cost |
Ingestion pipeline → Postgres | ✅ Working — running in production against the live feed |
Scheduler | ⚙️ Ships as a migration — Postgres |
Public feed page | ✅ Working — shows what the Gate let through, with links back to Prozorro |
LLM analysis chain, verdicts, human review queue | ⏳ Planned |
There are no AI verdicts yet. Nothing in this codebase calls a language model. What runs today is the deterministic half: the crawler, the schema-validated API client, the rule-based Gate, and the page that shows the result. The reasoning stages in the diagram below are designed but not built.
104 tests, clean tsc --noEmit.
Related MCP server: narajangteo-prespec
What works today: the Prozorro MCP server
A Model Context Protocol server exposing Ukraine's open procurement data as five tools. Point any MCP-compatible client at it and ask about live tenders.
npm install
npm run mcpTo use it from an MCP client, add the server to its configuration. For Claude Desktop this is claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\); for **Cursor** it is .cursor/mcp.json in the project or ~/.cursor/mcp.json globally. Both use the same shape:
{
"mcpServers": {
"prozorro": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/varto/src/mcp/server.ts"]
}
}
}Use an absolute path, and restart the client afterwards. The server speaks stdio and needs no environment variables — Prozorro's data is open by law, so there is no API key and no registration.
Tool | Purpose |
| Walk the Prozorro change feed |
| Full tender object by id |
| Normalised summary: title, CPV, value, buyer, deadline |
| A tender's attached documents |
| Extracted text, or an explicit reason why it could not be read |
Running it locally
npm install
cp .env.example .env # then fill in the Supabase values
npm run dev # http://localhost:3000Only SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required; everything else has a working default compiled into src/lib/config.ts, documented in .env.example. The MCP server needs none of them.
Database schema lives in supabase/migrations/ and is applied with the Supabase CLI:
npx supabase db push --db-url "$DIRECT_URL"Every migration is written to be safe to apply twice.
How the scheduler works
Supabase Postgres Vercel (fra1)
┌────────────────────────────┐ ┌────────────────────────────┐
│ pg_cron every 5 minutes │ │ GET /api/cron/scout │
│ │ │ │ requires Bearer secret │
│ └─ pg_net http_get ─────┼── HTTPS ──►│ walks the change feed │
│ URL + secret read │ │ applies the Gate │
│ from Vault │ │ writes tenders + reasons │
│ │◄── SQL ────┼─────────┘ │
└────────────────────────────┘ └────────────────────────────┘The schedule lives in Postgres rather than in vercel.json because Vercel's Hobby plan caps cron jobs at one run per day, which is far too slow to keep up with the change feed. pg_cron has minute granularity, pg_net makes the outbound HTTPS call, and Supabase is already a hard dependency — so this adds no new service.
The endpoint is a plain authenticated GET. It refuses to run without CRON_SECRET set and refuses to run when the bearer token does not match: a crawler that spends API quota and writes to a database must fail closed, not open.
Neither the URL nor the secret appears in the migration — supabase/migrations/ is public. Both live in Supabase Vault and are read at call time:
select vault.create_secret('https://<your-deployment>/api/cron/scout', 'scout_endpoint_url');
select vault.create_secret('<the same CRON_SECRET that is set on Vercel>', 'scout_cron_secret');Create those two secrets first, then apply supabase/migrations/0003_scout_schedule.sql with db push. Applied before the secrets exist, the job will run and fail loudly every five minutes — deliberately, because a schedule that quietly does nothing is worse than one that complains.
The cadence and the page size are one decision, not two. The feed carries ~60,700 modifications a day, so SCOUT_PAGE_LIMIT × ticks-per-day has to stay above that: 500 every five minutes is 144,000 a day, a 2.4x margin. The same page every ten minutes would be 72,000 — nominally more than the feed produces, but a thin enough margin that a modest tick failure rate would push the cursor backwards. Change one number and recompute the other.
To confirm the job is actually firing — not merely that it exists:
select status, return_message, start_time from cron.job_run_details
where jobid = (select jobid from cron.job where jobname = 'scout-feed-poll')
order by start_time desc limit 5;
select status_code, left(content::text, 300), created
from net._http_response order by created desc limit 5;A 401 there means the secret in Vault and the one on Vercel have drifted apart.
Why the function runs in Frankfurt
Vercel places functions in Washington, D.C. (iad1) by default. Prozorro is in Ukraine and the database is in eu-central-1, so every feed read and every write crossed the Atlantic twice; a full page of the change feed exceeded the 60-second function limit and was killed mid-loop, which meant the cursor never advanced and the next run repeated the same page forever. vercel.json pins the function to fra1, next to the database. The page size is SCOUT_PAGE_LIMIT — an environment variable rather than a constant, precisely because its correct value depends on where the code runs.
What the data actually looks like
Every design decision below came from measuring the live API, not from assuming. Three assumptions turned out to be wrong, and each would have quietly broken the product.
PDF is not the format of Ukrainian tender documentation. A census of 400 documents across 99 tenders:
Format | Share of all documents |
| 49.3% |
| 29.0% |
| 10.0% |
| 6.0% |
| 3.5% |
Narrowed to documents actually tagged as tender documentation, 94% are .docx/.doc and effectively none are PDF. A PDF-only reader would have been blind to almost everything that matters. Varto reads .docx and .pdf, flags legacy .doc as needing a human, and treats .p7s as a signature rather than a document.
Prozorro reports Content-Type: text/plain for every file, whatever it actually is. File type is therefore detected from content magic bytes, never from the response header and never from the filename.
Scanned PDFs with no text layer exist but are rare — 1 in 146 sampled documents. They are detected explicitly and surfaced as "needs a human" rather than silently returning empty text, which would invite a model to hallucinate over nothing.
The change feed is bigger than it looks. Roughly 60,700 modifications a day pass through it. Asking Prozorro for the full record of each one was never going to keep up; requesting the status inline with the feed discards about 85% of items before any per-item fetch, which is what makes a single small function able to out-run the feed.
Architecture
The pipeline is ordered so the cheapest stages discard the most work. Three of them use no language model at all.
Prozorro change feed
│
Scout plain code, 0 tokens ─→ local index ← built
Gate plain code, 0 tokens ─→ rejected, with the reason recorded ← built
Triage small model ─→ rejected, with the reason recorded
DocPicker plain code, 0 tokens ─→ picks 2–4 documents out of a dozen
Locator small model ─→ picks the sections worth reading
Analyst long-context model ─→ requirements + verbatim citations
Risk ×2 two different families ─→ disagreement ⇒ escalate to a human
Arbiter judgement model ─→ go / no-go + confidence
│
└─ uncertain, over budget, or a red flag ⇒ human review queue
└─ every human decision becomes a labelled eval exampleThree decisions worth calling out:
Two risk assessments from different model families. A model that is wrong is usually confident, so self-reported confidence is a weak signal. Two independent families disagreeing is a cheap, honest indicator that a case is genuinely hard — and it routes to a human automatically.
Nothing is discarded silently. Every stage records why it decided what it decided. A rejected tender keeps the rule that rejected it and a human-readable detail; a tender that could not be read at all is recorded as a failure rather than skipped, because the cursor moves on regardless and the trace has to outlive the logs.
The agent never submits a bid. It analyses and recommends; submitting is a human action. That is a product decision, not a missing feature.
Tech stack
TypeScript · Next.js on Vercel · Supabase (Postgres, pg_cron, pg_net, Vault) · OpenRouter · Model Context Protocol · Vitest · Zod
Model selection lives in configuration, never hardcoded, and every model was verified with a live call before being trusted — two of the first four candidates turned out to be unusable, including one model ID that does not exist at all.
Development
npm install
npm test # 104 tests
npm run typecheck # tsc --noEmit, must be clean
npm run build # next build
npm run mcp # start the MCP server over stdioThree conventions this repository holds to:
Tests are checked for whether they can actually fail. A test that passes identically against code where the feature is absent is treated as a defect, not as coverage. Several were found and replaced.
Passing tests are not evidence that the code compiles. Vitest strips types without checking them, so
tsc --noEmitis a separate gate on every change.Comments carry measurements, and stale measurements are bugs. More than one defect here traced back to a comment that had quietly stopped being true.
Unit tests never touch the network. The reconnaissance scripts under scripts/ do, and are marked as not being tests.
Interface text is Ukrainian; code, identifiers and documentation are English.
Licence
MIT
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityBmaintenanceProvides AI models with access to Ukrainian government procurement data from Prozorro, enabling search and retrieval of tender information by organization, date range, and other criteria.11111ISC
- AlicenseAqualityAmaintenanceMCP server that wraps the Korean government's '나라장터 사전규격정보서비스' API, enabling natural language search and retrieval of public procurement pre-specifications through simplified tools.539MIT
- FlicenseAqualityCmaintenanceMCP server for searching and retrieving contracts from the Polish Central Register of Contracts (CRU) via its API.2
- Alicense-qualityCmaintenanceProvides access to Ukraine's public procurement data (ProZorro) via MCP, keyless and integrated with Pipeworx gateway for AI agents.13MIT
Related MCP Connectors
ProZorro MCP — Ukraine government procurement (keyless).
MCP server for French (BOAMP) + EU (TED) public procurement data via TenderAPI.
Read-only MCP server for searching Japan government procurement bid information from the KKJ portal.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/volodeveth/varto'
If you have feedback or need assistance with the MCP directory API, please join our Discord server