{
  "markdown": "<div align=\"center\">\n\n# hilan-mcp\n\n**An MCP server for [Hilan](https://net.hilan.co.il) (Hilanet / חילן, חילנט)** — pull your payslips and Form 106 into any AI assistant, for any Hilan tenant.\n\n[![npm version](https://img.shields.io/npm/v/hilan-mcp.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/hilan-mcp)\n[![npm downloads](https://img.shields.io/npm/dm/hilan-mcp.svg?color=blue)](https://www.npmjs.com/package/hilan-mcp)\n[![license](https://img.shields.io/npm/l/hilan-mcp.svg?color=green)](https://github.com/udah1/hilan-mcp/blob/master/LICENSE)\n[![node](https://img.shields.io/node/v/hilan-mcp.svg)](https://www.npmjs.com/package/hilan-mcp)\n\n</div>\n\n---\n\nThere's no official Hilan API. `hilan-mcp` drives a real (headless) Chromium\nbrowser via [Playwright](https://playwright.dev) to log in, then reuses\nthat same authenticated session to call Hilan's internal endpoints and\ndownload PDFs — no fragile hand-rolled cookie replay, and no plaintext\nsecrets stored anywhere.\n\n## Table of contents\n\n- [Features](#features)\n- [How it works](#how-it-works)\n- [Setup](#setup)\n  - [0. If you're on npm 12 or newer](#0-if-youre-on-npm-12-or-newer)\n  - [1. Register a tenant and enter credentials](#1-register-a-tenant-and-enter-credentials)\n  - [2. Add the server to your MCP client config](#2-add-the-server-to-your-mcp-client-config)\n  - [3. Use it](#3-use-it)\n- [For AI agents setting this up on a user's behalf](#for-ai-agents-eg-claude-setting-this-up-on-a-users-behalf)\n- [MCP tools](#mcp-tools)\n- [Scope](#scope-v1)\n- [Reliability notes](#reliability-notes-learned-from-a-real-end-to-end-run)\n- [Security model](#security-model)\n- [Development](#development)\n- [License](#license)\n\n## Features\n\n- 📄 **Payslips** — structured Bruto/Neto/salary-parts data, plus the PDF, for any month in the tenant's history.\n- 🧾 **Form 106** — annual tax summary PDFs, per year.\n- 🔐 **Encrypted local storage** — SQLCipher-encrypted SQLite, key never written to disk in plaintext; OS-native credential store (Keychain/DPAPI/Secret Service) support.\n- 🏢 **Any Hilan tenant** — login form fields are detected per-tenant instead of hardcoded to one employer.\n- 💬 **Answers, not raw JSON** — tools like `query` answer \"how much did I earn in June?\" straight from local data, no network round-trip.\n- 🔄 **Self-updating awareness** — the server checks for newer versions and lets your AI agent offer to update you, without needing its own UI.\n\n## How it works\n\nThis server drives a real (headless) Chromium browser via\n[Playwright](https://playwright.dev) to log in, then reuses that same\nauthenticated browser context's HTTP client (`context.request`) to call\nHilan's internal `.asmx` JSON endpoints and download PDFs — so it\nautomatically inherits whatever cookies/headers a real page load would\nhave set up, instead of a fragile hand-rolled cookie replay.\n\nLogin form fields differ per Hilan tenant (some have 2 fields, some 3), so\nthe login form is inspected fresh for each tenant the first time you ingest\ncredentials, rather than hardcoded to one company.\n\n## Setup\n\n### 0. If you're on npm 12 or newer\n\nnpm 12 blocks dependency install scripts unless you allow them, and one of\nthis package's dependencies (`better-sqlite3-multiple-ciphers`, the\nencrypted SQLite engine) needs its install script to compile a native\nbinding. Without it, setup appears to succeed and then every tool that\ntouches the database fails. Allow it once, for all future `npx` and global\ninstalls:\n\n```bash\nnpm config set allow-scripts=better-sqlite3-multiple-ciphers --location=user\n```\n\nOr per install, if you'd rather not set it globally:\n\n```bash\nnpm install -g --allow-scripts=better-sqlite3-multiple-ciphers hilan-mcp\n```\n\nOn npm 11 and older this isn't needed — install scripts still run by\ndefault. Check with `npm --version`.\n\n### 1. Register a tenant and enter credentials\n\nRun this **yourself**, directly in your own terminal — never paste real\ncredentials into an AI chat. It installs the Chromium browser Playwright\nneeds (one-time), launches a headless browser, detects your tenant's actual\nlogin fields, and prompts you for each one (passwords are masked). No local\nclone or `npm install` needed — `npx` fetches the package on the fly:\n\n```bash\nnpx hilan-mcp setup --tenant amdocs        # tenant subdomain\n# or\nnpx hilan-mcp setup --tenant 5227          # numeric org code, resolved automatically\n```\n\n> If you already have the package installed some other way, the equivalent\n> lower-level command is `npx hilan-mcp ingest-creds --tenant <...>` —\n> `setup` just also handles the one-time Chromium install first.\n\nThe first time you run this you'll be asked to set an **encryption key**\nfor the local database (min 6 characters) — this key is never written to\ndisk in plaintext.\n\nFor the MCP server to open the database on its own (without a human\npresent to respond to a prompt every time), it looks for the key in this\norder:\n\n1. **OS credential store** (recommended) — run `npx hilan-mcp setup-key`\n   once, yourself, in your own terminal. You'll be asked to type a key (or\n   press Enter to generate a strong random one), and it's stored in your\n   OS's native secure storage: **macOS Keychain** (verified — same file\n   that holds your Wi-Fi/browser passwords, silent, no dialogs),\n   **Windows** (DPAPI, tied to your Windows user account), or **Linux**\n   (Secret Service / `secret-tool`, needs a keyring daemon like GNOME\n   Keyring or KWallet). Nothing ends up in any config file at all.\n2. **`HILAN_DB_KEY` env var** — set it in the server's `env` block (see\n   step 2). Same trust model as any other API key/secret configured for an\n   MCP server in `mcp.json` (local file, not committed to git).\n3. **Desktop notification** (last resort) — the server falls back to\n   asking via a system notification (with a plain terminal prompt as a\n   further fallback), but that requires a human to respond within ~30s of\n   every tool call that needs the database, so it's not recommended for\n   normal use.\n\n### 2. Add the server to your MCP client config\n\nRunning the package with **no arguments** starts the MCP server itself\n(stdio transport) — that's what your MCP client's config actually invokes:\n\n```json\n{\n  \"mcpServers\": {\n    \"hilan\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"hilan-mcp\"]\n    }\n  }\n}\n```\n\n<details>\n<summary><strong>Single-employer setup — skip passing <code>tenant</code> on every tool call</strong></summary>\n\nIf you only ever use this for **one** employer, you can set a default\ntenant in the server's `env` block. Only the **first** of these that's set\nis used, in this order — `COMPANY_URL` → `COMPANY_TENANT` → `COMPANY_CODE`:\n\n```json\n{\n  \"mcpServers\": {\n    \"hilan\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"hilan-mcp\"],\n      \"env\": {\n        \"COMPANY_TENANT\": \"amdocs\",\n        \"HILAN_DB_KEY\": \"the-encryption-key-you-set-during-ingest-creds\",\n        \"NODE_EXTRA_CA_CERTS\": \"/path/to/your-corporate-ca-bundle.pem\"\n      }\n    }\n  }\n}\n```\n\n`HILAN_DB_KEY` is only needed if you didn't run `npx hilan-mcp setup-key`\n(see step 1) — the OS credential store takes priority when both are\npresent. `NODE_EXTRA_CA_CERTS` is only needed on networks with a\nTLS-intercepting corporate proxy (see [Reliability notes](#reliability-notes-learned-from-a-real-end-to-end-run)) — omit it otherwise.\n\n| Env var | Example | Notes |\n|---|---|---|\n| `COMPANY_URL` | `https://amdocs.net.hilan.co.il` | Highest priority. |\n| `COMPANY_TENANT` | `amdocs` | Bare subdomain. Used only if `COMPANY_URL` isn't set. |\n| `COMPANY_CODE` | `5227` | Numeric org code. Used only if neither above is set — requires an extra lookup round-trip. |\n\nThis same fallback also applies to `npx hilan-mcp ingest-creds` —\n`--tenant` is optional if one of these env vars is set. An explicit\n`tenant` argument (or `--tenant` flag) always overrides the env vars.\n\n</details>\n\n<details>\n<summary><strong>Running from a local clone instead of the published package</strong></summary>\n\nUse `\"command\": \"node\", \"args\": [\"/absolute/path/to/hilan-mcp/dist/cli/index.js\"]`\n(after `npm install && npm run build` in that clone) instead of the `npx`\nform above.\n\n</details>\n\n### 3. Use it\n\nAsk your assistant things like \"sync my last 3 payslips from Hilan\",\n\"download my Form 106 for 2025\", or \"how much net/gross did I earn in June\n2026?\" (answered instantly from the local database via the `query` tool,\nno network round-trip, once that month has been synced at least once) — see\nthe full [tool list](#mcp-tools) below.\n\n## For AI agents (e.g. Claude) setting this up on a user's behalf\n\nIf a user asks you to set up `hilan-mcp` for them:\n\n1. **You must not** run `ingest-creds`/`setup`/`setup-key` yourself via a\n   shell tool — they prompt for a password and/or an encryption key\n   interactively, and your shell tool's transcript could capture what the\n   user types. Tell the user to open their **own** terminal and run\n   `npx hilan-mcp setup --tenant <their-org-code-or-subdomain>` themselves,\n   then (recommended) `npx hilan-mcp setup-key`.\n   Check their `npm --version` first: on npm 12+ they also need\n   [step 0](#0-if-youre-on-npm-12-or-newer) or the database will fail to\n   open later, in a way that doesn't look related to installation.\n2. Once that's done, **you** can safely add the MCP server entry to the\n   client's config file (the JSON blocks above) — that part has no secrets\n   in it as long as the user used `setup-key` (OS credential store) rather\n   than the `HILAN_DB_KEY` env var fallback.\n3. After the config is added, the user (or their client) needs to reload/\n   restart the MCP connection for the new tools to appear.\n4. Don't enable `HILAN_ENABLE_DEBUG_TOOLS` unless the user explicitly asks\n   for the raw `.asmx`/PDF exploration tools — they're an intentional\n   escape hatch (see [Security model](#security-model)), not needed for\n   normal use.\n5. If a tool response includes an `_updateNotice` field, tell the user and\n   ask if they'd like to update; if they decline, call `dismissUpdateNotice`\n   with that version (see [MCP tools](#mcp-tools)).\n\n## MCP tools\n\n| Tool | Summary |\n|---|---|\n| `listTenants` | List configured tenants — no secrets. |\n| `syncPayslips` | Fetch structured payslip data + PDFs for the last N months. |\n| `resyncPayslipMonth` | Re-fetch one specific month, overwriting what's stored. |\n| `syncForm106` | Download the Form 106 PDF for a given year. |\n| `downloadCombinedPayslipsPdf` | One PDF covering a whole month range. |\n| `getSalaryTrends` | Yearly averages, YoY %, CAGR, and notable raises — computed locally. |\n| `listTables` / `describeTable` | Inspect the queryable local schema. |\n| `query` | Run read-only SQL against local payslip/Form106 data. |\n| `getPersonalDetails` | Live lookup of personal details (not persisted). |\n| `dismissUpdateNotice` | Suppress a specific version's update notice. |\n| `debugCallAsmx` / `debugDownloadPdf` | Raw API exploration — off by default. |\n\n<details>\n<summary><strong>Full tool reference (arguments, behavior, notes)</strong></summary>\n\n#### `listTenants()`\nConfigured tenants (subdomain, capability, whether login has succeeded before) — no secrets.\n\n#### `syncPayslips(tenant?, monthsBack?, skipPdf?)`\nLogs in (reusing a saved session if it still works), fetches structured Bruto/Neto/salary-parts data for the last N months (default 3), and downloads each payslip PDF. Set `skipPdf: true` for a much faster numbers-only sync (e.g. \"what did I earn this year\"). `tenant` is optional if `COMPANY_URL`/`COMPANY_TENANT`/`COMPANY_CODE` is set. The tenant's own archive length caps how far back this can actually go — no need to guess a start date.\n\n#### `resyncPayslipMonth(tenant?, period, skipPdf?)`\nRe-fetches **one specific month** (e.g. `\"10_2019\"` or `\"10/2019\"`), overwriting whatever's stored. Use this instead of re-running `syncPayslips` over the whole history just to retry one bad/truncated PDF or to answer a one-off \"how much did I make in month X\" question.\n\n#### `syncForm106(tenant?, year?)`\nLogs in and downloads the Form 106 PDF for a given year (or the tenant's default year). Same `tenant` fallback as above.\n\n#### `downloadCombinedPayslipsPdf(tenant?, fromMonth, toMonth, destDir?)`\nDownloads **one PDF covering a whole month range** (e.g. all of 2015–2024) instead of one file per month. Automatically splits into multiple files if the range would otherwise exceed the server's URL length limit (see [Reliability notes](#reliability-notes-learned-from-a-real-end-to-end-run)) — practical ceiling is roughly 10–15 years per chunk.\n\n#### `getSalaryTrends(tenant?, fromYear?, toYear?)`\nComputes salary growth trends **from locally-synced payslips only** — no live login, no network call. Returns yearly average Bruto/Neto, year-over-year % change between adjacent years, CAGR between the first and last full (12-month) calendar year, overall growth (first vs. last synced month), and \"notable jumps\" (≥10% YoY change in Bruto). Optional `fromYear`/`toYear` scope the calendar years considered. Prefer this over hand-rolling aggregation with `query` — grouping/sorting by plain `period` text gets cross-year chronology wrong (e.g. `\"01_2011\"` sorts before `\"02_2010\"`); this tool sorts by actual calendar month internally.\n\n#### `listTables()` / `describeTable(table)`\nLists queryable tables (`payslips`, `form106` only) and their columns/indexes.\n\n#### `query(sql)`\nSELECT-only SQL against `payslips`/`form106`. **`credentials`, `sessions`, and `tenants` are hard-blocked** — see [Security model](#security-model). Good for quick questions like \"how much net/gross did I earn in June 2026\" without touching the network at all.\n\n#### `getPersonalDetails(tenant?)`\nLogs in and returns personal details (name, national ID, birthdate, address, phone, email, work start date, job type). Live lookup only — **not persisted** to the local database.\n\n#### `dismissUpdateNotice(version)`\nCall this if the user declines an update you offered them (see below), so that exact version isn't mentioned again.\n\n#### `debugCallAsmx(tenant?, servicePath, body?)` / `debugDownloadPdf(tenant?, relativeUrl, fileName)`\n**Debug/exploration only, disabled by default** — raw pass-through to any `.asmx` endpoint, or download of an arbitrary relative PDF URL, on a logged-in session. Requires `HILAN_ENABLE_DEBUG_TOOLS=1` in the server's `env` block; not registered otherwise. Filenames are sanitized (no path traversal) and URLs are restricted to the tenant's own origin (no SSRF).\n\n</details>\n\n**Update notifications**: this server has no UI of its own, so update\nchecks piggyback on whatever tool the agent happens to call next. At most\nonce a day, a successful tool response may include an extra\n`_updateNotice` field with a message like *\"A new hilan-mcp version is\navailable: 0.1.0 -> 0.2.0...\"* — if you're the AI agent reading this, tell\nthe user and ask if they'd like to update; if they say no, call\n`dismissUpdateNotice` with that version so it isn't raised again (a newer\nversion will still be announced later). The check itself hits the npm\nregistry with a 3s timeout and silently no-ops if it's unreachable (e.g.\noffline, corporate proxy) — it never fails a tool call.\n\n**Concurrency note**: every tool call for a given tenant is automatically\nqueued and run one-at-a-time internally (see `TenantService.runExclusive`)\n— Hilan's site (and/or the network path to it) doesn't handle concurrent\nrequests from one session well and everything times out otherwise. You\ndon't need to serialize calls yourself; different tenants still run fully\nin parallel.\n\n## Scope (v1)\n\n- ✅ Payslips (list, structured Bruto/Neto/parts, PDF)\n- ✅ Form 106 (PDF, per year)\n- ⛔ General file archive, Form 101 — not built (v1.1+)\n- ⛔ One-time-code / 2FA login — tenants that require it return a clear\n  \"unsupported\" error instead of hanging\n\nTenants whose internal API response doesn't match the structured shape seen\non the reference tenant (Amdocs) are marked `pdf_only`: payslip syncs still\nwork and still save a PDF, just without the parsed Bruto/Neto numbers.\n\n## Reliability notes (learned from a real end-to-end run)\n\n<details>\n<summary>Corporate TLS-intercepting proxies, PDF URL quirks, truncation retries, URL-length limits, and OTP false positives</summary>\n\n- **Corporate TLS-intercepting proxies** (e.g. Amdocs's network) break\n  Playwright's Node-side `context.request` for the internal API/PDF calls\n  with `self-signed certificate in certificate chain`. Set\n  `NODE_EXTRA_CA_CERTS` to your organization's exported CA bundle in the\n  server's `env` block if you hit this.\n- **Direct PDF download URLs are relative to `/Hilannetv2`**, not the\n  domain root — a URL returned by the JSON API like\n  `PersonalFile/PdfPaySlip.aspx/...` actually lives at\n  `<tenant>/Hilannetv2/PersonalFile/PdfPaySlip.aspx/...`. Confirmed via a\n  live browser network capture.\n- PDF downloads go through an **in-page `fetch()`** (same-origin, raw URL)\n  rather than `context.request`, which silently re-encodes literal `/` in\n  query strings to `%2F` and gets 404'd by some endpoints.\n- The same corporate proxy occasionally **truncates a PDF response\n  mid-stream** (observed: suspiciously round 32768-byte cutoffs) without\n  the fetch itself erroring. `downloadPdf` checks the whole buffer for the\n  standard `%%EOF` PDF trailer (not just the tail — some genuine Hilan\n  PDFs have tens/hundreds of KB of trailing null-byte padding *after* a\n  valid `%%EOF`) and retries up to 3 times before giving up.\n- Combined multi-month PDFs (`PaySlipApiapi.asmx/GetMultiplePaySlipData`,\n  one file covering a date range instead of one per month) hit a hard\n  **HTTP 404 once the URL exceeds ~2048 characters** — that's IIS's\n  default `requestFiltering maxQueryString` limit, not a Hilan-specific\n  cap (each month adds ~11 chars to the URL). Verified working at 120\n  months (10 years, ~1.4KB URL); verified failing at 194 months (~16\n  years, 2.2KB URL) — so a ~15-year request is the realistic practical\n  ceiling per combined PDF. Not an issue for per-month syncing\n  (`syncPayslips`), which never builds one huge URL.\n- A tenant's *regular* login page can unconditionally show a \"log in with\n  a one-time code\" button/link even when normal username+password login\n  is fully supported and working — don't treat that text alone as proof\n  OTP is required. Only genuinely new OTP-input fields (or navigating to\n  an OTP-specific route) after a failed login count as OTP being required.\n\n</details>\n\n## Security model\n\n- The whole SQLite database (`~/Library/Application Support/HilanMcp/hilan.db`\n  on macOS, XDG/AppData equivalents elsewhere) is encrypted at rest via\n  SQLCipher (`better-sqlite3-multiple-ciphers`), keyed by a passphrase you\n  choose that is **never persisted to disk**. PDFs live alongside it under\n  `.../HilanMcp/pdfs/<tenant>/`, and rolling log files under\n  `.../HilanMcp/logs/` (redacted — see below). App-data, PDF, and log\n  directories are created with `0700` permissions; the DB and PDF files\n  themselves with `0600`.\n- `credentials` (your login fields) and `sessions` (saved browser cookies)\n  are ordinary tables *inside* that encrypted database, but they are **never\n  reachable through the `query`/`listTables`/`describeTable` tools** — those\n  are hard-allowlisted to `payslips` and `form106` only, validated via a\n  full SQL AST parse (not a regex) so `JOIN`s, subqueries, and quoted\n  identifiers can't be used to sneak past the allowlist (see\n  `src/utils/sqlValidation.ts` and `src/tests/sqlValidation.test.ts`).\n- Cookies and auth headers are redacted from log files and from any error\n  message a tool call returns, so a Playwright error can't leak your\n  session into your chat history (see `src/utils/redact.ts`).\n- Credential entry (`ingest-creds`) must be run directly by you in your own\n  terminal — never through an assistant's shell tool, whose transcript could\n  capture what you typed.\n- `debugCallAsmx` is an intentional escape hatch: it lets a logged-in\n  session call **any** `.asmx` endpoint with **any** body, bypassing the\n  curated tools above. It was added for API exploration during\n  development and is one-employee-scoped (same session, same\n  permissions you already have on the site) — it can't reach other\n  employees' data or other tenants — but it could in principle call a\n  write endpoint (Hilan generally gates writes behind approval flows, but\n  this hasn't been audited). Off by default (`HILAN_ENABLE_DEBUG_TOOLS`);\n  treat it like `query`'s SQL escape hatch — fine for an AI assistant you\n  trust to explore with, not something to expose to untrusted input.\n\n## Development\n\nClone this repo to work on the code itself (not needed just to *use* the\nserver — see [Setup](#setup) above for that):\n\n```bash\nnpm install\nnpm run typecheck\nnpm test                 # unit tests (SQL allowlist, path-traversal, redaction, update checks, ...)\nnpm run build            # compile TypeScript -> dist/\nnpm run start:mcp        # run the server directly from source (stdio transport)\nnpm run setup            # local equivalent of `npx hilan-mcp setup`\nnpm run ingest-creds     # register a tenant + credentials (interactive, run yourself)\nnpm run setup-key        # store the DB encryption key in your OS credential store (interactive, run yourself)\n```\n\n`npm publish` ships only `dist/`, `README.md`, and `LICENSE` (see the\n`files` field in `package.json`) — source, tests, and local research\nartifacts are excluded. `prepublishOnly` runs typecheck + tests + build\nfirst, so a broken build can't be published. The published `bin` entry\n(`dist/cli/index.js`) is what `npx hilan-mcp` and `npx hilan-mcp <subcommand>`\nboth invoke.\n\n## License\n\n[MIT](./LICENSE)\n",
  "bytes": 21693,
  "sha": "830578f538ed667c07480e9c0075b3753dbac620f22c7b236fa235c415c4ff6f",
  "repo_slug": "udah1/hilan-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_udah1_hilan_mcp_03238131/readme"
}