npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mtender-mcp-server

v3.3.0

Published

Modern MCP server for Moldova's MTender public procurement data (OCDS 1.1.5). Protocol revision 2025-11-25. Tools, resources, prompts, vision-OCR for scanned PDFs, SSRF-hardened.

Readme

mtender-mcp-server

Listed on Yoda Digital Open Source CI CodeQL Publish npm version npm downloads Bundle size Trusted publisher License: ISC Node.js TypeScript MCP SDK OCDS GitHub release Last commit Open issues Stars

Production-grade Model Context Protocol server for Moldova's MTender public procurement data, modeled on Open Contracting Data Standard 1.1.5.

Lets an AI agent (Claude Desktop, Cursor, Continue, custom MCP clients, etc.) read, search, audit, and summarize every public procurement in the Republic of Moldova from public.mtender.gov.md. Tiered document extraction delegates scanned-PDF OCR to the host's vision LLM — language-agnostic (Romanian / Russian / English / mixed) without local OCR infrastructure.


Table of contents


What you can ask an agent

| Question to the agent | Wired tool / resource | |---|---| | "What was tendered last week?" | mtender://tenders/latest | | "What's currently being competed for right now?" | mtender://contract-notices/latest | | "What's planned for procurement next quarter?" | mtender://plans/latest | | "Show me tender ocds-b3wdp1-MD-XXX in full" | get_tender | | "Find all road-construction tenders in the last 30 days" | search_tenders_deep with cpvPrefix=45233 | | "Find every tender awarded to S.R.L. Foo" | search_tenders_deep with supplierContains=Foo | | "Which government body spent the most this month?" | aggregate_by_buyer | | "Who are the top suppliers by total awarded value?" | aggregate_by_supplier | | "Find tenders awarded with only one bidder (red flag)" | flag_single_bid_awards | | "Read the actual PDF attached to this tender" | fetch_tender_document (text + vision-OCR fallback) | | "What did bidders ask publicly, and how did the buyer answer?" | list_enquiries | | "Break this multi-lot tender down lot by lot" | list_lots | | "Show me the timeline — when was it amended?" | get_release_history | | "Compare these two tenders side by side" | prompt compare-tenders | | "Audit this supplier's footprint" | prompt audit-supplier |

Install

From npm (recommended for MCP host configs — no clone, no build):

# one-shot, no install
npx -y mtender-mcp-server

# or globally
npm install -g mtender-mcp-server
mtender-mcp                                          # stdio
MCP_TRANSPORT=http mtender-mcp                       # Streamable HTTP

From source (for contributing):

git clone [email protected]:nalyk/mtender-mcp-server.git
cd mtender-mcp-server
npm install
npm run build
npm test

Use with an MCP host

Claude Desktop / Claude Code

Add to your MCP config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):

{
  "mcpServers": {
    "mtender": {
      "command": "npx",
      "args": ["-y", "mtender-mcp-server"]
    }
  }
}

Cursor / Continue / Cline

Same shape — most MCP-aware editors support stdio servers via the same command + args JSON config.

Generic Streamable HTTP host

Run it once as a service, point the host at the URL:

MCP_TRANSPORT=http PORT=8787 HOST=127.0.0.1 npx -y mtender-mcp-server
# host config: { "url": "http://127.0.0.1:8787/mcp" }

Configuration

| Env var | Default | Purpose | |---|---|---| | MCP_TRANSPORT | stdio | stdio or http | | PORT | 8787 | HTTP listen port | | HOST | 127.0.0.1 | HTTP bind host. localhost auto-enables DNS-rebinding protection | | ALLOWED_HOSTS | (auto) | Comma-separated host allow-list when binding to non-localhost | | LOG_LEVEL | info | pino level — trace debug info warn error fatal | | MCP_AUTH_MODE | none | none or bearer (RFC 9068 OAuth 2.1 Bearer token gate on /mcp) | | MCP_AUTH_ISSUER | — | Required when MCP_AUTH_MODE=bearer. URL of the Authorization Server. | | MCP_AUTH_AUDIENCE | — | Required when MCP_AUTH_MODE=bearer. Token audience (RFC 8707) — typically https://your-host.example/mcp. | | MCP_AUTH_JWKS_URL | (auto) | Override of the discovered jwks_uri. Auto-discovered from <issuer>/.well-known/oauth-authorization-server (or /openid-configuration) when unset. | | MCP_AUTH_REQUIRED_SCOPES | — | Comma- or space-separated scopes the token must carry, e.g. mcp:read. Empty = no scope check (still authenticates). |

When MCP_AUTH_MODE=bearer is on, the server also publishes RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource{path} so unauthenticated clients can discover the AS to obtain a token from. The /healthz endpoint stays public (liveness probes have no credentials). Refusing without bearer while bound to a non-localhost interface emits a warning — defense in depth for accidental public exposure.

Capabilities

Resources (5 static + 4 OCID-templated)

| URI | Notes | |---|---| | mtender://tenders/latest | Most recent ~100 procurement notices (last 30 days) | | mtender://contract-notices/latest | Currently tendering (CN releases only) | | mtender://plans/latest | Forward-looking planning records | | mtender://budgets/latest | Recent budgets | | mtender://upstream/health | Live upstream API health + build info | | mtender://tenders/{ocid} | Full compiled OCDS record (parties, lots, items+CPV, documents, awards, contracts, enquiries, bid stats); listable + completable | | mtender://tenders/{ocid}/releases | Release timeline by tag | | mtender://budgets/{ocid} | Planning budget | | mtender://funding/{ocid} | Funding source |

All {ocid} templates support typeahead completion from the live latest list.

Tools (17)

| Tool | Returns | |---|---| | search_tenders | {items, count, nextOffset} + resource_link per result | | search_contract_notices / search_plans / search_budgets | Same shape, scoped to each upstream listing endpoint | | search_tenders_deep | Filter by buyer/supplier/CPV/value/status (slow, fan-out, with progress) | | get_tender | Full compiled OCDS summary | | get_release_history | Chronological releases with tags | | list_lots | Multi-lot breakdown | | list_enquiries | Public Q&A (bidder ↔ buyer) | | list_bid_statistics | OCDS bids extension stats | | list_tender_documents | All doc URLs across tender + awards + contracts | | get_budget / get_funding_source | Planning data | | aggregate_by_buyer | Buyers ranked by total contract value | | aggregate_by_supplier | Suppliers ranked by awards count + value | | flag_single_bid_awards | Limited-competition red-flag scan | | fetch_tender_document | SSRF-guarded PDF/DOCX/text extraction with vision-OCR fallback |

All read tools annotate readOnlyHint: true, idempotentHint: true, openWorldHint: true. Slow tools emit notifications/progress. Every fetch honors AbortSignal for cancellation.

Prompts (8)

| Prompt | Workflow | |---|---| | analyze-procurement | End-to-end OCDS analysis of one tender | | compare-tenders | Side-by-side of two tenders (suspect duplicates / coordinated bids) | | audit-supplier | Recent footprint of a named supplier (top buyers, dominant CPV, single-bid count) | | single-bid-investigation | Surface limited-competition awards, group by buyer-supplier pair | | buyer-spend-overview | Top buyers by spend with drill-down | | enquiry-review | Analyze public Q&A on a tender | | lot-breakdown | Walk a multi-lot tender lot-by-lot | | pipeline-overview | Plans → contract notices → contracts pipeline view |

OCID arguments are autocompleted from the live mtender://tenders/latest list.

Document extraction pipeline

fetch_tender_document is tiered for the realities of Moldovan procurement docs (most are scanned by Canon multi-functions):

| Document type | Strategy | |---|---| | Native-text PDF | unpdf.extractText → text | | Scanned PDF (detected via char-density, scanner-producer signature, or absent Romanian diacritics) | unpdf.extractImages per page → re-encoded with sharp to JPEG (q78) → returned as MCP image content blocks. Host's vision LLM does the OCR — language-agnostic, handles Romanian / Russian / English / mixed without local OCR infra. | | DOCX | mammoth.convertToHtml → minimal HTML→Markdown that preserves GFM tables | | TXT | UTF-8 decode |

Detection combines: char-per-byte density (< 0.005 is almost certainly scanned), scanner-producer keywords in PDF metadata (canon, hp scan, scanjet, scansnap, epson, xerox, kyocera, ricoh, brother, konica, lexmark, gimp, imagemagick, tiff, kodak), and absent Romanian diacritics in long extracted text. The mode: auto | text | image argument lets callers force a strategy. Page-image cap: 20 pages per call. Document size cap: 25 MiB.

Architecture

src/
├── index.ts            entry: dual-transport (stdio | streamable HTTP)
├── server.ts           McpServer + capabilities + instructions
├── tools.ts            17 tools with structured I/O + progress
├── resources.ts        5 static + 4 templated resources, all completable
├── prompts.ts          8 procurement-investigation workflows
├── api/mtender.ts      undici keep-alive client, retry, multi-package
│                       compile, TTL+LRU caches, listing endpoints
├── ssrf.ts             URL parse + DNS lookup + private-IP block
├── document.ts         unpdf + mammoth + sharp tiered extraction
├── cache.ts            tiny TTL+LRU
├── concurrency.ts      bounded fan-out helper
├── schemas.ts          OCDS-aligned Zod types
└── logger.ts           pino → fd 2 (stderr)
  • MCP protocol revision 2025-11-25, SDK @modelcontextprotocol/[email protected]
  • Node.js 22+, TypeScript strict, ESM only
  • 6 runtime deps + express for the HTTP transport. Distroless multi-stage Docker image (gcr.io/distroless/nodejs22-debian12:nonroot)
  • Compiles a real OCDS record by fanning out to upstream packages[] URIs and merging by id-union — compiledRelease from MTender is sparse, so awards/items/parties have to be reassembled

Security

  • Streamable HTTP binds to 127.0.0.1 by default and refuses requests whose Host header isn't in the allow-list (DNS-rebinding mitigation per the MCP 2025-11-25 security best practices)
  • Document fetch validates URL with new URL(), asserts hostname === "storage.mtender.gov.md", then resolves DNS and rejects any RFC1918 / loopback / link-local / 169.254.169.254 (AWS/GCP IMDS) / IPv6 ULA result before issuing the actual request
  • Stateless sessions (sessionIdGenerator: undefined) — no session ID to hijack. Per spec: "MCP servers MUST NOT use sessions for authentication."
  • Logs to stderr; stdout is reserved for JSON-RPC
  • CodeQL (security-and-quality query suite) runs on every push and PR
  • Dependabot weekly + on-CVE auto-PRs
  • No bundled secrets; .env* in .gitignore

For vulnerability reports see SECURITY.md. Use GitHub's private advisory form, not public issues.

Releases & provenance

This package is published to npm via trusted publishers — GitHub Actions authenticates to the npm registry directly via OIDC, no static NPM_TOKEN secret. Every release after the v3.1.0 bootstrap is attested with Sigstore provenance proving the tarball was built in this GitHub workflow from this commit.

Verify the chain locally:

npm view mtender-mcp-server versions --json
npm view mtender-mcp-server@latest dist.attestations
npm audit signatures            # in any project that depends on it

Release flow (one command):

npm version patch -m "Release v%s"        # bumps + commits + tags
git push origin main --follow-tags         # triggers OIDC publish + GitHub release

The publish workflow has built-in guards: tag↔version drift fails the run; re-running on an already-published version skips the publish + release-create steps idempotently.

Test

npm test

20 tests against the live MTender API (resource read + tool calls + completion + aggregation + listings + lots/enquiries + scanned-PDF detection regression) plus the SSRF guard, using the SDK's InMemoryTransport for in-process client/server pairing. Runs in ~30 seconds.

Docker

docker build -t mtender-mcp .
docker run --rm -p 8787:8787 mtender-mcp

Distroless gcr.io/distroless/nodejs22-debian12:nonroot, runs MCP_TRANSPORT=http by default. The CI pipeline rebuilds the image on every push to confirm it still bakes cleanly.

Known upstream limitations

These are out of our control — MTender publishes what MTender publishes:

  • No server-side text search. Upstream /tenders/ accepts only offset. search_tenders_deep does client-side filter after fetching the latest page — the only viable approach.
  • No descending pagination. The API is ascending-by-date only; "latest" requires passing offset≈now, which this server does by default.
  • Implementation/transactions section sparse. MTender doesn't track contract execution stage in this dataset. Reflected in TenderSummary.
  • Romanian-only content. No English / Russian translations of fields.

Upstream Spring Boot version + status is surfaced at mtender://upstream/health for ops visibility.

Contributing & support

  • CONTRIBUTING.md — project shape, contribution norms, how to add a tool / resource / prompt
  • CHANGELOG.md — Keep-a-Changelog entries per version
  • SECURITY.md — private vulnerability reporting + scoped threat model
  • Issues — bug reports and feature requests use structured templates
  • Discussions — questions, design conversations

License & acknowledgements

ISC © Ion (Nalyk) Calmîș.

Built on: