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

mcp-ansc-server

v1.0.3

Published

MCP server for Moldova's National Agency for Solving Complaints (ANSC) — appeals, decisions, hearing schedule, and multi-modal PDF extraction.

Readme

mcp-ansc-server

npm version npm downloads GitHub release CI License: MIT Node MCP SDK

A Model Context Protocol (MCP) server that exposes Moldova's National Agency for Solving Complaints (ANSC, Agenția Națională pentru Soluționarea Contestațiilor) public-procurement data — appeals, decisions, hearing schedule, and PDF documents — to LLMs and other MCP clients.

Conformant with MCP spec 2025-11-25 and the TypeScript SDK 1.29.x. Published with Sigstore provenance via npm OIDC trusted publishing.


Quickstart

npx -y mcp-ansc-server

That's it — no install needed. The server speaks stdio and is ready for any MCP client to spawn it.

For Claude Desktop, drop this into your config:

// ~/.config/claude-desktop/config.json (Linux/macOS) or %APPDATA%\Claude\config.json (Windows)
{
  "mcpServers": {
    "ansc": {
      "command": "npx",
      "args": ["-y", "mcp-ansc-server"]
    }
  }
}

Same shape works in Cursor, Continue, Zed, and any other stdio-MCP host.


Table of contents


What it ships

Tools (12)

Search (4)

| name | what it does | |---|---| | search_appeals | Filter appeals by year / contracting authority / challenger / OCDS procedure ID / status. Paginated, 30 items per page. | | search_decisions | Filter decisions by year / authority / challenger / procurement object / decision status / decision content / appeal grounds (the 42-ground catalog) / complaint object / appeal number. Paginated. | | search_orders | Procedural orders ("încheieri") issued during a case. kind="general" (default) or kind="suspension" (incheieri-de-suspendare). | | search_suspended_decisions | Court-suspended decisions (decizii-suspendate-{year}). The authoritative signal that a court has paused enforcement of an ANSC ruling. |

Direct lookup (3)

| name | what it does | |---|---| | get_appeal_by_registration | Direct lookup by 02/1245/24-style number. Year is parsed from the suffix; pages are scanned with bounded concurrency (cached). | | get_decision_by_number | Direct lookup by 03D-962-24-style number. | | get_procurement_history | Given an OCDS procurement ID, return every appeal and every decision tied to that tender. The OCDS timestamp seeds the year range we scan. |

Hearing schedule (3)

| name | what it does | |---|---| | list_upcoming_hearings | All days for which ANSC has published a hearing agenda. | | get_hearings_for_day | The agenda for a specific day (by URL or ISO date) — list of cases with time, parties, registration number, object, panel. | | find_hearing_for_appeal | The "when is my hearing?" feature — scans every published agenda day for a matching appeal registration number. |

Documents (2)

| name | what it does | |---|---| | check_decision_court_status | Looks up a decision and cross-checks the suspended-decisions listing. Returns { decision, suspension, isSuspended }. Closes a real correctness gap: search_decisions alone can report decisionStatus: "În vigoare" for items a court has since suspended. | | fetch_ansc_decision | Download an ANSC decision PDF and return its content. Native-text PDFs return extracted text. Scanned PDFs (Canon/HP/etc., common for older filings — typically with broken Unicode CMap that maps Romanian to garbled Cyrillic) return per-page JPEG image content blocks for the host vision-LLM to OCR — language-agnostic, no local Tesseract install. Force a path with mode: 'auto' \| 'text' \| 'image'. Uses unpdf.extractImages (raster bytes already embedded — no canvas backend needed) + sharp for re-encoding. |

What every tool guarantees

  • declares both inputSchema (Zod) and outputSchema (Zod), so clients receive validated structuredContent alongside the human-readable text;
  • carries the right tool annotations (readOnlyHint: true, idempotentHint: true, openWorldHint: true|false, title);
  • honors cancellation via the AbortSignal from the SDK;
  • normalizes Romanian dates to ISO 8601 (entryDateIso, dateIso) alongside the original dd/mm/yyyy;
  • strips trailing punctuation from appealNumber / registrationNumber;
  • emits notifications/progress for long PDF downloads.

Prompts (3)

Pre-canned LLM workflows that clients surface as slash-commands:

| name | args | what it sets the LLM up to do | |---|---|---| | summarize_ansc_decision | identifier (decision number or ELO URL) | Fetch the decision and produce a structured Romanian/English summary: parties / procurement / grounds / ruling / legal basis / status. | | procurement_audit | procedureNumber (OCDS ID) | Walk through get_procurement_history and produce a chronological narrative of every appeal + decision (with PDF reads when needed). | | compare_appeals | firstRegistration, secondRegistration | Side-by-side comparison: parties, grounds, outcomes, divergences. |

Resources

Static shortcuts:

  • ansc://appeals/current — current-year appeals, page 0
  • ansc://decisions/current — current-year decisions, page 0

RFC 6570 templates with complete callbacks for year (2014→current) and page (0→20):

  • ansc://appeals/{year}
  • ansc://appeals/{year}/page/{page}
  • ansc://decisions/{year}
  • ansc://decisions/{year}/page/{page}

Transports

Two transports, chosen via MCP_TRANSPORT:

  • stdio (default) — for desktop / IDE clients (Claude Desktop, Cursor, Continue, Zed, …) that spawn the server as a subprocess.
  • http — Streamable HTTP per spec 2025-03-26+, with stateful sessions (Mcp-Session-Id header), SSE streaming on GET /mcp, session resumability via Last-Event-ID. DNS-rebinding protection is enabled automatically when binding to localhost.

Authentication (optional)

ANSC's data is public — anyone can browse https://www.ansc.md. The default deployment shape is MCP_TRANSPORT=stdio (or http with AUTH_MODE=none behind a trusted reverse proxy / Tailscale).

OAuth here only protects the server itself from abuse, not the data. If you deploy this on the open internet and want per-principal rate limiting and audit logs, set MCP_TRANSPORT=http and AUTH_MODE=oauth. The server then runs as an OAuth 2.1 Resource Server per spec 2025-11-25:

  • publishes RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource;
  • validates Bearer JWTs with jose against the issuer's JWKS;
  • enforces the aud claim per RFC 8707 (the audience must be HTTP_PUBLIC_URL) — token passthrough is forbidden by spec and rejected here;
  • on missing/invalid token, returns 401 with WWW-Authenticate: Bearer resource_metadata="…".

The MCP server itself is only a Resource Server. Bring any OAuth 2.1 Authorization Server (Auth0, Keycloak, Logto, Hanko, Cognito with static client IDs, …).

Testing the OAuth integration

# Inspect the published metadata
curl -fsSL https://mcp.example.com/.well-known/oauth-protected-resource | jq

# Successful call
TOKEN=…
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
  https://mcp.example.com/mcp

# Hit /mcp without a token to see the spec-compliant 401 + WWW-Authenticate
curl -i https://mcp.example.com/mcp

Configuration

All env vars are documented in .env.example. Highlights:

MCP_TRANSPORT=stdio|http
HTTP_HOST=127.0.0.1
HTTP_PORT=3030
HTTP_PUBLIC_URL=https://mcp.example.com   # required when AUTH_MODE=oauth
AUTH_MODE=none|oauth
OAUTH_ISSUER=https://auth.example.com     # required when AUTH_MODE=oauth
OAUTH_REQUIRED_SCOPES=mcp:read

ANSC_HTTP_TIMEOUT_MS=30000
ANSC_TLS_BYPASS_HOSTS=www.ansc.md,elo.ansc.md   # ANSC's cert is invalid
ANSC_USER_AGENT="…"

CACHE_TTL_CURRENT_S=300       # 5 min
CACHE_TTL_HISTORICAL_S=86400  # 24 h
CACHE_MAX_ENTRIES=500

LOG_LEVEL=info

Config is parsed by Zod at startup; misconfigured envs fail fast.


Running it

From npm (recommended)

npx -y mcp-ansc-server                # one-shot, no install
# or
npm install -g mcp-ansc-server
mcp-ansc-server                        # uses the bin
# or
MCP_TRANSPORT=http npx -y mcp-ansc-server   # Streamable HTTP on :3030

From source

git clone [email protected]:nalyk/ansc-mcp-server.git
cd ansc-mcp-server
nvm use                                # honors .nvmrc (Node 24)
npm ci
npm run build
npm start                              # stdio
npm run start:http                     # MCP_TRANSPORT=http
npm run inspect                        # MCP Inspector against the built server

In Docker

docker build -t mcp-ansc-server .
docker run -i --rm mcp-ansc-server     # stdio (typical MCP usage)
docker run --rm -p 3030:3030 \
  -e MCP_TRANSPORT=http -e HTTP_HOST=0.0.0.0 \
  mcp-ansc-server                      # Streamable HTTP

The image is multi-stage node:24-alpine, runs as a non-root mcp user.


Wiring into MCP clients

Claude Desktop / Cursor / Continue / Zed (stdio)

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

For a pinned version (recommended for stability):

"args": ["-y", "[email protected]"]

As a remote (Streamable HTTP)

Put the server behind TLS (caddy / nginx / Cloudflare Tunnel / fly.io). Point the MCP client at https://mcp.example.com/mcp. If AUTH_MODE=oauth, the client must include the resource parameter (RFC 8707) when exchanging tokens, with value https://mcp.example.com.


Operational notes

  • Logs are JSON on stderr (pino, level via LOG_LEVEL). Stdout is reserved for the MCP wire. Any code path that writes to stdout in stdio mode is a bug — please file it.
  • Outbound HTTP uses undici with a single hostname-pinned TLS-bypass agent (only ANSC_TLS_BYPASS_HOSTS skip cert validation; everything else is verified normally). Retries on 5xx / ECONNRESET / socket / header / body timeouts with exponential backoff + jitter, capped at 3 attempts; honors Retry-After on 429.
  • Caching — HTML responses are cached in-process via lru-cache, year- tiered: 5 min TTL for the current year (active data), 24 h for historical years (rarely changes). Configurable.
  • PDF extraction — for native-text PDFs, returns extracted text. For scanned PDFs, returns embedded page images for the host vision-LLM to OCR. The heuristic detector triggers on scanner-brand producer strings, low char/byte density, zero Romanian diacritics in a multi-page body, or per-page text under 80 chars. No local Tesseract install required.
  • HTML parser matches columns by <th> text first (resilient to ANSC reordering); falls back to positional with parserMode: 'partial' flag in tool output, so callers can detect when ANSC's layout has drifted.

Project layout

src/
  index.ts                    # bootstrap — picks transport, wires shutdown
  config.ts                   # Zod env schema
  logging.ts                  # pino on stderr (+ errMsg helper)
  api/
    ansc-client.ts            # undici + retries + lru-cache + lookups
    pdf-fetcher.ts            # PDF download + unpdf text/image extract + sharp JPEG
  handlers/
    tools.ts                  # 12 tools — Zod input/output, annotations
    resources.ts              # RFC 6570 templates + completions
    prompts.ts                # 3 LLM workflow templates
  http/
    server.ts                 # Express + StreamableHTTPServerTransport (stateful)
    auth.ts                   # JoseTokenVerifier + PRM router (opt-in)
  models/
    appeals.ts                # AppealStatus + Zod
    decisions.ts              # Decision enums + Zod
    orders.ts                 # Order (încheieri) + Zod
    suspended.ts              # SuspendedDecision + Zod
    hearings.ts               # Hearing / HearingDay + Zod
    pagination.ts             # Pagination + Zod
  utils/
    html-parser.ts            # parseTable<T,F> generic — header-name + positional
    identifiers.ts            # Romanian id + date helpers (incl. OCDS timestamp)
    retry.ts                  # exponential backoff with jitter
__tests__/
  fixtures/*.html             # synthetic ANSC pages for parser tests
  *.test.ts                   # html-parser, identifiers, config — 16 tests
.github/workflows/
  ci.yml                      # tsc + jest + audit + Dockerfile smoke
  publish.yml                 # tag-driven OIDC publish to npm + GitHub Release
  dependabot.yml              # weekly npm + actions updates
Dockerfile                    # node:24-alpine, multi-stage, non-root

Releasing

Releases are tag-driven and fully automated via OIDC trusted publishing — no static NPM_TOKEN involved.

npm version patch -m "Release v%s"     # bumps package.json + lockfile, creates v* tag
git push origin main --follow-tags     # triggers .github/workflows/publish.yml

The workflow runs tsc, tests, build, then publishes to npm with auto-Sigstore provenance and creates the matching GitHub Release with auto-generated notes. The tag↔package.json version drift is caught up front; re-runs are idempotent.


Project meta