{"id":50491801,"url":"https://github.com/nalyk/mtender-mcp-server","last_synced_at":"2026-06-02T03:05:15.479Z","repository":{"id":355104421,"uuid":"1226730920","full_name":"nalyk/mtender-mcp-server","owner":"nalyk","description":"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.","archived":false,"fork":false,"pushed_at":"2026-05-01T21:54:17.000Z","size":219,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-01T22:26:16.186Z","etag":null,"topics":["anthropic","claude","govtech","mcp","model-context-protocol","moldova","mtender","nodejs","ocds","open-contracting","procurement","transparency","typescript"],"latest_commit_sha":null,"homepage":"https://github.com/nalyk/mtender-mcp-server","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nalyk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-01T19:08:42.000Z","updated_at":"2026-05-01T21:52:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nalyk/mtender-mcp-server","commit_stats":null,"previous_names":["nalyk/mtender-mcp-server"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/nalyk/mtender-mcp-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nalyk%2Fmtender-mcp-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nalyk%2Fmtender-mcp-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nalyk%2Fmtender-mcp-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nalyk%2Fmtender-mcp-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nalyk","download_url":"https://codeload.github.com/nalyk/mtender-mcp-server/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nalyk%2Fmtender-mcp-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33803772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anthropic","claude","govtech","mcp","model-context-protocol","moldova","mtender","nodejs","ocds","open-contracting","procurement","transparency","typescript"],"created_at":"2026-06-02T03:05:12.430Z","updated_at":"2026-06-02T03:05:15.473Z","avatar_url":"https://github.com/nalyk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mtender-mcp-server\n\n[![Listed on Yoda Digital Open Source](https://img.shields.io/badge/listed%20on-opensource.yoda.digital-af9568?style=flat-square)](https://opensource.yoda.digital/en/projects/mtender-mcp-server/)\n[![CI](https://github.com/nalyk/mtender-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/nalyk/mtender-mcp-server/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/nalyk/mtender-mcp-server/actions/workflows/codeql.yml/badge.svg)](https://github.com/nalyk/mtender-mcp-server/actions/workflows/codeql.yml)\n[![Publish](https://github.com/nalyk/mtender-mcp-server/actions/workflows/publish.yml/badge.svg)](https://github.com/nalyk/mtender-mcp-server/actions/workflows/publish.yml)\n[![npm version](https://img.shields.io/npm/v/mtender-mcp-server.svg?logo=npm\u0026color=cb3837)](https://www.npmjs.com/package/mtender-mcp-server)\n[![npm downloads](https://img.shields.io/npm/dm/mtender-mcp-server.svg?color=cb3837)](https://www.npmjs.com/package/mtender-mcp-server)\n[![Bundle size](https://img.shields.io/bundlephobia/minzip/mtender-mcp-server?label=install%20size)](https://bundlephobia.com/package/mtender-mcp-server)\n[![Trusted publisher](https://img.shields.io/badge/npm-trusted%20publisher-success?logo=sigstore)](https://docs.npmjs.com/trusted-publishers)\n[![License: ISC](https://img.shields.io/github/license/nalyk/mtender-mcp-server.svg?color=blue)](./LICENSE)\n[![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg?logo=node.js)](https://nodejs.org)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![MCP](https://img.shields.io/badge/MCP-2025--11--25-purple.svg)](https://modelcontextprotocol.io/specification/2025-11-25)\n[![SDK](https://img.shields.io/badge/%40modelcontextprotocol%2Fsdk-1.29-purple.svg)](https://www.npmjs.com/package/@modelcontextprotocol/sdk)\n[![OCDS](https://img.shields.io/badge/OCDS-1.1.5-success.svg)](https://standard.open-contracting.org/)\n[![GitHub release](https://img.shields.io/github/v/release/nalyk/mtender-mcp-server?include_prereleases\u0026sort=semver\u0026logo=github)](https://github.com/nalyk/mtender-mcp-server/releases)\n[![Last commit](https://img.shields.io/github/last-commit/nalyk/mtender-mcp-server?logo=github)](https://github.com/nalyk/mtender-mcp-server/commits/main)\n[![Open issues](https://img.shields.io/github/issues/nalyk/mtender-mcp-server?logo=github)](https://github.com/nalyk/mtender-mcp-server/issues)\n[![Stars](https://img.shields.io/github/stars/nalyk/mtender-mcp-server?style=social)](https://github.com/nalyk/mtender-mcp-server)\n\nProduction-grade Model Context Protocol server for Moldova's MTender public\nprocurement data, modeled on\n[Open Contracting Data Standard 1.1.5](https://standard.open-contracting.org/).\n\nLets an AI agent (Claude Desktop, Cursor, Continue, custom MCP clients, etc.)\nread, search, audit, and summarize **every public procurement** in the\nRepublic of Moldova from `public.mtender.gov.md`. Tiered document extraction\ndelegates scanned-PDF OCR to the host's vision LLM — language-agnostic\n(Romanian / Russian / English / mixed) without local OCR infrastructure.\n\n---\n\n## Table of contents\n\n- [What you can ask an agent](#what-you-can-ask-an-agent)\n- [Install](#install)\n- [Use with an MCP host](#use-with-an-mcp-host)\n- [Configuration](#configuration)\n- [Capabilities](#capabilities)\n- [Document extraction](#document-extraction-pipeline)\n- [Architecture](#architecture)\n- [Security](#security)\n- [Releases \u0026 provenance](#releases--provenance)\n- [Test](#test)\n- [Docker](#docker)\n- [Known upstream limitations](#known-upstream-limitations)\n- [Contributing \u0026 support](#contributing--support)\n- [License \u0026 acknowledgements](#license--acknowledgements)\n\n---\n\n## What you can ask an agent\n\n| Question to the agent | Wired tool / resource |\n|---|---|\n| \"What was tendered last week?\" | `mtender://tenders/latest` |\n| \"What's currently being competed for right now?\" | `mtender://contract-notices/latest` |\n| \"What's planned for procurement next quarter?\" | `mtender://plans/latest` |\n| \"Show me tender ocds-b3wdp1-MD-XXX in full\" | `get_tender` |\n| \"Find all road-construction tenders in the last 30 days\" | `search_tenders_deep` with `cpvPrefix=45233` |\n| \"Find every tender awarded to S.R.L. Foo\" | `search_tenders_deep` with `supplierContains=Foo` |\n| \"Which government body spent the most this month?\" | `aggregate_by_buyer` |\n| \"Who are the top suppliers by total awarded value?\" | `aggregate_by_supplier` |\n| \"Find tenders awarded with only one bidder (red flag)\" | `flag_single_bid_awards` |\n| \"Read the actual PDF attached to this tender\" | `fetch_tender_document` (text + vision-OCR fallback) |\n| \"What did bidders ask publicly, and how did the buyer answer?\" | `list_enquiries` |\n| \"Break this multi-lot tender down lot by lot\" | `list_lots` |\n| \"Show me the timeline — when was it amended?\" | `get_release_history` |\n| \"Compare these two tenders side by side\" | prompt `compare-tenders` |\n| \"Audit this supplier's footprint\" | prompt `audit-supplier` |\n\n## Install\n\nFrom npm (recommended for MCP host configs — no clone, no build):\n\n```bash\n# one-shot, no install\nnpx -y mtender-mcp-server\n\n# or globally\nnpm install -g mtender-mcp-server\nmtender-mcp                                          # stdio\nMCP_TRANSPORT=http mtender-mcp                       # Streamable HTTP\n```\n\nFrom source (for contributing):\n\n```bash\ngit clone git@github.com:nalyk/mtender-mcp-server.git\ncd mtender-mcp-server\nnpm install\nnpm run build\nnpm test\n```\n\n## Use with an MCP host\n\n### Claude Desktop / Claude Code\n\nAdd to your MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json`\non macOS, `%APPDATA%\\Claude\\claude_desktop_config.json` on Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"mtender\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"mtender-mcp-server\"]\n    }\n  }\n}\n```\n\n### Cursor / Continue / Cline\n\nSame shape — most MCP-aware editors support stdio servers via the same\n`command + args` JSON config.\n\n### Generic Streamable HTTP host\n\nRun it once as a service, point the host at the URL:\n\n```bash\nMCP_TRANSPORT=http PORT=8787 HOST=127.0.0.1 npx -y mtender-mcp-server\n# host config: { \"url\": \"http://127.0.0.1:8787/mcp\" }\n```\n\n## Configuration\n\n| Env var | Default | Purpose |\n|---|---|---|\n| `MCP_TRANSPORT` | `stdio` | `stdio` or `http` |\n| `PORT` | `8787` | HTTP listen port |\n| `HOST` | `127.0.0.1` | HTTP bind host. localhost auto-enables DNS-rebinding protection |\n| `ALLOWED_HOSTS` | (auto) | Comma-separated host allow-list when binding to non-localhost |\n| `LOG_LEVEL` | `info` | pino level — `trace` `debug` `info` `warn` `error` `fatal` |\n| `MCP_AUTH_MODE` | `none` | `none` or `bearer` (RFC 9068 OAuth 2.1 Bearer token gate on `/mcp`) |\n| `MCP_AUTH_ISSUER` | — | Required when `MCP_AUTH_MODE=bearer`. URL of the Authorization Server. |\n| `MCP_AUTH_AUDIENCE` | — | Required when `MCP_AUTH_MODE=bearer`. Token audience (RFC 8707) — typically `https://your-host.example/mcp`. |\n| `MCP_AUTH_JWKS_URL` | (auto) | Override of the discovered `jwks_uri`. Auto-discovered from `\u003cissuer\u003e/.well-known/oauth-authorization-server` (or `/openid-configuration`) when unset. |\n| `MCP_AUTH_REQUIRED_SCOPES` | — | Comma- or space-separated scopes the token must carry, e.g. `mcp:read`. Empty = no scope check (still authenticates). |\n\nWhen `MCP_AUTH_MODE=bearer` is on, the server also publishes RFC 9728\nProtected Resource Metadata at `/.well-known/oauth-protected-resource{path}`\nso unauthenticated clients can discover the AS to obtain a token from. The\n`/healthz` endpoint stays public (liveness probes have no credentials).\nRefusing without `bearer` while bound to a non-localhost interface emits a\nwarning — defense in depth for accidental public exposure.\n\n## Capabilities\n\n### Resources (5 static + 4 OCID-templated)\n\n| URI | Notes |\n|---|---|\n| `mtender://tenders/latest` | Most recent ~100 procurement notices (last 30 days) |\n| `mtender://contract-notices/latest` | Currently tendering (CN releases only) |\n| `mtender://plans/latest` | Forward-looking planning records |\n| `mtender://budgets/latest` | Recent budgets |\n| `mtender://upstream/health` | Live upstream API health + build info |\n| `mtender://tenders/{ocid}` | Full compiled OCDS record (parties, lots, items+CPV, documents, awards, contracts, enquiries, bid stats); listable + completable |\n| `mtender://tenders/{ocid}/releases` | Release timeline by tag |\n| `mtender://budgets/{ocid}` | Planning budget |\n| `mtender://funding/{ocid}` | Funding source |\n\nAll `{ocid}` templates support typeahead completion from the live latest list.\n\n### Tools (17)\n\n| Tool | Returns |\n|---|---|\n| `search_tenders` | `{items, count, nextOffset}` + resource_link per result |\n| `search_contract_notices` / `search_plans` / `search_budgets` | Same shape, scoped to each upstream listing endpoint |\n| `search_tenders_deep` | Filter by buyer/supplier/CPV/value/status (slow, fan-out, with progress) |\n| `get_tender` | Full compiled OCDS summary |\n| `get_release_history` | Chronological releases with tags |\n| `list_lots` | Multi-lot breakdown |\n| `list_enquiries` | Public Q\u0026A (bidder ↔ buyer) |\n| `list_bid_statistics` | OCDS bids extension stats |\n| `list_tender_documents` | All doc URLs across tender + awards + contracts |\n| `get_budget` / `get_funding_source` | Planning data |\n| `aggregate_by_buyer` | Buyers ranked by total contract value |\n| `aggregate_by_supplier` | Suppliers ranked by awards count + value |\n| `flag_single_bid_awards` | Limited-competition red-flag scan |\n| `fetch_tender_document` | SSRF-guarded PDF/DOCX/text extraction with vision-OCR fallback |\n\nAll read tools annotate `readOnlyHint: true, idempotentHint: true,\nopenWorldHint: true`. Slow tools emit `notifications/progress`. Every fetch\nhonors `AbortSignal` for cancellation.\n\n### Prompts (8)\n\n| Prompt | Workflow |\n|---|---|\n| `analyze-procurement` | End-to-end OCDS analysis of one tender |\n| `compare-tenders` | Side-by-side of two tenders (suspect duplicates / coordinated bids) |\n| `audit-supplier` | Recent footprint of a named supplier (top buyers, dominant CPV, single-bid count) |\n| `single-bid-investigation` | Surface limited-competition awards, group by buyer-supplier pair |\n| `buyer-spend-overview` | Top buyers by spend with drill-down |\n| `enquiry-review` | Analyze public Q\u0026A on a tender |\n| `lot-breakdown` | Walk a multi-lot tender lot-by-lot |\n| `pipeline-overview` | Plans → contract notices → contracts pipeline view |\n\nOCID arguments are autocompleted from the live `mtender://tenders/latest` list.\n\n## Document extraction pipeline\n\n`fetch_tender_document` is tiered for the realities of Moldovan procurement\ndocs (most are scanned by Canon multi-functions):\n\n| Document type | Strategy |\n|---|---|\n| Native-text PDF | `unpdf.extractText` → text |\n| 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.** |\n| DOCX | `mammoth.convertToHtml` → minimal HTML→Markdown that preserves GFM tables |\n| TXT | UTF-8 decode |\n\nDetection combines: char-per-byte density (`\u003c 0.005` is almost certainly\nscanned), scanner-producer keywords in PDF metadata (`canon`, `hp scan`,\n`scanjet`, `scansnap`, `epson`, `xerox`, `kyocera`, `ricoh`, `brother`,\n`konica`, `lexmark`, `gimp`, `imagemagick`, `tiff`, `kodak`), and absent\nRomanian diacritics in long extracted text. The `mode: auto | text | image`\nargument lets callers force a strategy. Page-image cap: 20 pages per call.\nDocument size cap: 25 MiB.\n\n## Architecture\n\n```\nsrc/\n├── index.ts            entry: dual-transport (stdio | streamable HTTP)\n├── server.ts           McpServer + capabilities + instructions\n├── tools.ts            17 tools with structured I/O + progress\n├── resources.ts        5 static + 4 templated resources, all completable\n├── prompts.ts          8 procurement-investigation workflows\n├── api/mtender.ts      undici keep-alive client, retry, multi-package\n│                       compile, TTL+LRU caches, listing endpoints\n├── ssrf.ts             URL parse + DNS lookup + private-IP block\n├── document.ts         unpdf + mammoth + sharp tiered extraction\n├── cache.ts            tiny TTL+LRU\n├── concurrency.ts      bounded fan-out helper\n├── schemas.ts          OCDS-aligned Zod types\n└── logger.ts           pino → fd 2 (stderr)\n```\n\n- MCP protocol revision **2025-11-25**, SDK `@modelcontextprotocol/sdk@1.29`\n- Node.js **22+**, TypeScript strict, ESM only\n- 6 runtime deps + `express` for the HTTP transport. Distroless multi-stage\n  Docker image (`gcr.io/distroless/nodejs22-debian12:nonroot`)\n- Compiles a real OCDS record by fanning out to upstream `packages[]` URIs\n  and merging by id-union — `compiledRelease` from MTender is sparse, so\n  awards/items/parties have to be reassembled\n\n## Security\n\n- **Streamable HTTP** binds to `127.0.0.1` by default and refuses requests\n  whose `Host` header isn't in the allow-list (DNS-rebinding mitigation per\n  the MCP 2025-11-25 [security best practices](https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices))\n- **Document fetch** validates URL with `new URL()`, asserts\n  `hostname === \"storage.mtender.gov.md\"`, then resolves DNS and rejects\n  any RFC1918 / loopback / link-local / `169.254.169.254` (AWS/GCP IMDS) /\n  IPv6 ULA result before issuing the actual request\n- **Stateless sessions** (`sessionIdGenerator: undefined`) — no session ID\n  to hijack. Per spec: \"MCP servers MUST NOT use sessions for authentication.\"\n- **Logs to stderr**; stdout is reserved for JSON-RPC\n- **CodeQL** (`security-and-quality` query suite) runs on every push and PR\n- **Dependabot** weekly + on-CVE auto-PRs\n- **No bundled secrets**; `.env*` in `.gitignore`\n\nFor vulnerability reports see [SECURITY.md](./SECURITY.md). Use GitHub's\nprivate advisory form, not public issues.\n\n## Releases \u0026 provenance\n\nThis package is published to npm via [trusted publishers](https://docs.npmjs.com/trusted-publishers)\n— GitHub Actions authenticates to the npm registry directly via OIDC, no\nstatic `NPM_TOKEN` secret. Every release after the v3.1.0 bootstrap is\nattested with [Sigstore provenance](https://docs.npmjs.com/generating-provenance-statements)\nproving the tarball was built in this GitHub workflow from this commit.\n\nVerify the chain locally:\n\n```bash\nnpm view mtender-mcp-server versions --json\nnpm view mtender-mcp-server@latest dist.attestations\nnpm audit signatures            # in any project that depends on it\n```\n\nRelease flow (one command):\n\n```bash\nnpm version patch -m \"Release v%s\"        # bumps + commits + tags\ngit push origin main --follow-tags         # triggers OIDC publish + GitHub release\n```\n\nThe publish workflow has built-in guards: tag↔version drift fails the run;\nre-running on an already-published version skips the publish + release-create\nsteps idempotently.\n\n## Test\n\n```bash\nnpm test\n```\n\n20 tests against the live MTender API (resource read + tool calls +\ncompletion + aggregation + listings + lots/enquiries + scanned-PDF detection\nregression) plus the SSRF guard, using the SDK's `InMemoryTransport` for\nin-process client/server pairing. Runs in ~30 seconds.\n\n## Docker\n\n```bash\ndocker build -t mtender-mcp .\ndocker run --rm -p 8787:8787 mtender-mcp\n```\n\nDistroless `gcr.io/distroless/nodejs22-debian12:nonroot`, runs\n`MCP_TRANSPORT=http` by default. The CI pipeline rebuilds the image on\nevery push to confirm it still bakes cleanly.\n\n## Known upstream limitations\n\nThese are out of our control — MTender publishes what MTender publishes:\n\n- **No server-side text search.** Upstream `/tenders/` accepts only `offset`.\n  `search_tenders_deep` does client-side filter after fetching the latest\n  page — the only viable approach.\n- **No descending pagination.** The API is ascending-by-date only; \"latest\"\n  requires passing `offset≈now`, which this server does by default.\n- **Implementation/transactions section sparse.** MTender doesn't track\n  contract execution stage in this dataset. Reflected in `TenderSummary`.\n- **Romanian-only content.** No English / Russian translations of fields.\n\nUpstream Spring Boot version + status is surfaced at\n`mtender://upstream/health` for ops visibility.\n\n## Contributing \u0026 support\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md) — project shape, contribution norms,\n  how to add a tool / resource / prompt\n- [CHANGELOG.md](./CHANGELOG.md) — Keep-a-Changelog entries per version\n- [SECURITY.md](./SECURITY.md) — private vulnerability reporting + scoped\n  threat model\n- [Issues](https://github.com/nalyk/mtender-mcp-server/issues) — bug reports\n  and feature requests use structured templates\n- [Discussions](https://github.com/nalyk/mtender-mcp-server/discussions) —\n  questions, design conversations\n\n## License \u0026 acknowledgements\n\n[ISC](./LICENSE) © Ion (Nalyk) Calmîș.\n\nBuilt on:\n- [Model Context Protocol](https://modelcontextprotocol.io/) and the\n  [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)\n  by Anthropic + the MCP community\n- [Open Contracting Data Standard](https://standard.open-contracting.org/) 1.1.5\n- [public.mtender.gov.md](https://public.mtender.gov.md/) — Moldova's\n  e-Procurement public data point\n- [unpdf](https://github.com/unjs/unpdf), [mammoth](https://github.com/mwilliamson/mammoth.js),\n  [sharp](https://sharp.pixelplumbing.com/), [undici](https://undici.nodejs.org/),\n  [pino](https://getpino.io/), [zod](https://zod.dev/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnalyk%2Fmtender-mcp-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnalyk%2Fmtender-mcp-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnalyk%2Fmtender-mcp-server/lists"}