Skip to main content
Glama
lokesh-sparrow

PNPC-MCP-Tally-Prime

README.md
<p align="center">
  <img src="assets/icon.png" alt="PNPC-MCP-Tally-Prime" width="180">
</p>

<h1 align="center">PNPC-MCP-Tally-Prime</h1>

<p align="center">
  Tally Prime MCP (Model Context Protocol) Server implementation to feed
  TallyPrime ERP data to Claude — and unlike most Tally MCP servers, it can
  also <b>post vouchers</b> (Sales, Purchase, Payment, Receipt, Journal), not
  just read reports.
</p>

> Built and maintained by **Lokesh Sparrow**. Public so anyone can use or
> self-host it for their own TallyPrime setup. New here? The
> **[Tool Playbook](https://lokesh-sparrow.github.io/PNPC-MCP-Tally-Prime/tool-playbook.html)**
> shows what to actually type in chat, before you get to the full reference below.

## Prerequisites

- **TallyPrime** (Silver / Gold — avoid the Educational edition, its date-range
  limitation feeds partial/invalid data to the LLM and causes degraded, incorrect answers)
- **Claude Desktop** (Pro / Team / Max / Enterprise recommended — MCP makes
  many calls to Tally per question, which can exhaust a Free plan's limits quickly)
- TallyPrime's **XML/HTTP gateway enabled**:
  `F1 (Help) → Settings → Connectivity → Client/Server configuration`
  - **TallyPrime acts as** = `Server` (or `Both`)
  - **Port** = `9000`

You do **not** need Node.js installed to *run* this — Claude Desktop bundles
its own Node.js runtime for extensions. You only need Node.js if you're
building from source (see [Building from source](#building-from-source)).

## Download

Get the latest release — no cloning or building required:

- **[Latest release](https://github.com/lokesh-sparrow/PNPC-MCP-Tally-Prime/releases/latest)** — download the `.mcpb` (one-click installer extension for Claude Desktop) from the Assets section

## Supported platforms

| Platform | Local | Remote |
|---|---|---|
| Claude Desktop | ✔️ | ✔️ |
| Claude web (claude.ai) | — | ✔️ |
| ChatGPT (Developer Mode) | — | ✔️ |
| Grok (SuperGrok / X Premium+) | — | ✔️ |

(Remote/cloud deployment runs via the HTTP entry point, confirmed live
against all three remote clients above — see
[docs/HTTP_DEPLOYMENT.md](docs/HTTP_DEPLOYMENT.md) and
[docs/OAUTH_CONNECTORS.md](docs/OAUTH_CONNECTORS.md). Local is still the
simpler, recommended path if you're only using Claude Desktop. Claude Desktop is the only
client here with a local/no-network install option (the `.mcpb`
extension); claude.ai, ChatGPT, and Grok all connect only to remote HTTPS
MCP endpoints — none of them can run a local stdio server on your PC the
way Claude Desktop does, so remote deployment is the only path into any
of them.)

## Setup (Local)

Use this when Claude Desktop and TallyPrime are both on the same PC — Claude
Desktop runs the MCP server internally for you.

### One-click installation (via Extension)

1. Claude Desktop → menu → **File → Settings**
2. **Extensions → Advanced settings**
3. Click **Install Extension**
4. Browse to and select **`PNPC-MCP-Tally-Prime.mcpb`** (downloaded above)
5. A dialog will appear asking *"Do you want to install PNPC-MCP-Tally-Prime?"* — click **Install**
6. It'll prompt for the **TallyPrime Gateway URL** (leave as `http://localhost:9000` unless your gateway runs elsewhere), **Read-only mode** (on by default — this connector can look but not change anything until you turn this off), and an optional **Disabled tools** list
7. Verify by clicking the **Tools** (hammer) icon in a chat — `PNPC-MCP-Tally-Prime` should appear in the list

> **Read-only mode is on by default.** A fresh install can read reports, ledgers, and vouchers immediately, but every write tool (`create_*`, `update_*`, `delete_*`) will be refused until you go to this connector's settings, turn Read-only mode off, and **fully quit and reopen Claude Desktop** — saving the settings screen alone isn't enough.

If step 4–5 don't produce that confirmation dialog (accepted silently, nothing
visible happens), see **Troubleshooting** below — clone this repo, run
`npm install && npm run build` ([Building from source](#building-from-source)),
then use `Install Unpacked Extension` on the same screen, pointed at the repo
folder itself.

### Other MCP clients (no `.mcpb` support)

Clients without a one-click extension format read a plain JSON config
pointing at the built server directly. First clone this repo and run
`npm install && npm run build` ([Building from source](#building-from-source))
— `dist/index.js` only exists after that build step. Then add:
```json
{
  "mcpServers": {
    "PNPC-MCP-Tally-Prime": {
      "command": "node",
      "args": ["D:\\Path\\To\\PNPC-MCP-Tally-Prime\\dist\\index.js"]
    }
  }
}
```
(Use double backslashes in the path, as shown.) Where this config file
lives, and how to reload it, depends on the client — check its own docs.

## Setup (Cloud)

For browser-based or mobile LLM clients that can't reach a TallyPrime
instance running on your local PC (claude.ai, ChatGPT, Grok), the server
can run as a small web service instead. The underlying credential is
still a single bearer token you set yourself (`TALLY_MCP_TOKEN`) — treat
it like a password — but this server also supports a real OAuth login
flow on top of it, since that's what those clients' "Add custom
connector" flows expect. This is more involved and puts your Tally
gateway on the network — see [docs/HTTP_DEPLOYMENT.md](docs/HTTP_DEPLOYMENT.md)
for running it, [docs/CLOUDFLARE_TUNNEL.md](docs/CLOUDFLARE_TUNNEL.md) for
exposing it without opening a router port, and
[docs/OAUTH_CONNECTORS.md](docs/OAUTH_CONNECTORS.md) for the OAuth login
flow itself.

## Available tools (86 total)

You don't call these directly — just describe what you want in chat and Claude
picks the right one. For a plain-English example prompt per tool, grouped by
what they're for, see the
**[Tool Playbook](https://lokesh-sparrow.github.io/PNPC-MCP-Tally-Prime/tool-playbook.html)**.

Dates use `DD-MM-YYYY` format, matching Tally's own convention. Full
machine-readable schemas: [docs/TOOLS.md](docs/TOOLS.md).

### Read

| Tool | Input | Output |
|---|---|---|
| `get_ledgers` | `query?` | All ledgers (including VAT TRN, VAT dealer type, state, country, GSTIN, PAN, GST registration type, GST type of supply, contact person), or (with `query`) a fuzzy-ranked shortlist of the closest-matching ledger names — top 20, best match first. GSTIN/PAN/GST registration type/GST type of supply are India's tax fields, `trn`/VAT dealer type are UAE's — a ledger normally has one country's set or the other |
| `get_stock_items` | — | All stock items (name, parent, closing balance, description, part number, costing method, GST type of supply, GST applicable (read-only), plus `GSTDETAILS: {hsnCode, taxability, applicableFrom, isReverseChargeApplicable, gstIneligibleItc}` or `null`) — the latest dated GST Details entry from Tally's own date-versioned list, set via `create_stock_item`'s/`update_stock_item`'s `hsnCode`/`taxability`/`isReverseChargeApplicable`/`gstIneligibleItc` fields |
| `get_groups` | — | Account groups (e.g. Sundry Debtors, Fixed Assets) |
| `get_voucher_types` | — | Configured voucher types (Payment, Sales, Journal, ...) |
| `get_cost_centres` | — | All cost centres |
| `get_company_info` | — | Currently open company's details |
| `get_vouchers` | `from`, `to` | Day Book — vouchers in that date range |
| `get_ledger_vouchers` | `ledgerName`, `from`, `to` | Voucher entries posted to one ledger |
| `get_profit_and_loss` | `from`, `to` | P&L statement |
| `get_trial_balance` | `from`, `to` | Trial Balance |
| `get_balance_sheet` | `asOf` | Balance Sheet as of a date |
| `get_stock_summary` | `asOf` | Stock Summary as of a date |
| `get_bills_receivable` | `asOf` | Outstanding Bills Receivable |
| `get_bills_payable` | `asOf` | Outstanding Bills Payable |
| `get_cash_flow` | `from`, `to` | Cash Flow statement — Tally's own canned report |
| `get_funds_flow` | `from`, `to` | Funds Flow statement — Tally's own canned report |
| `get_ratio_analysis` | `from`, `to` | Standard ratios (Current Ratio, Quick Ratio, Inventory/Debtors/Creditors Turnover, etc.) — Tally's own canned report |
| `get_sales_register` | `from`, `to` | Month-by-month Sales voucher summary — Tally's own canned report |
| `get_purchase_register` | `from`, `to` | Month-by-month Purchase voucher summary — Tally's own canned report |
| `get_journal_register` | `from`, `to` | Month-by-month Journal voucher summary — Tally's own canned report |
| `get_payment_register` | `from`, `to` | Month-by-month Payment voucher summary — Tally's own canned report |
| `get_receipts_and_payments` | `from`, `to` | Combined cash/bank Receipts and Payments view — the closest reachable equivalent to Cash Book/Bank Book, which aren't reachable as standalone reports |
| `get_reorder_status` | `from`, `to` | Stock items that actually have a reorder level configured, and how their quantity stands against it — filtered down from Tally's own report, which returns every item regardless (confirmed live on a 10,770-item company) |
| `get_vat_liability_summary` | `from`, `to` | UAE VAT liability for a period — Input/Output/RCM/other VAT ledgers found via Tally's own tax-type field or name pattern (whichever actually catches this company's real ledgers), plus a net total |
| `get_gst_liability_summary` | `from`, `to` | India GST liability for a period — same hybrid approach as VAT, for CGST/SGST/IGST input/output/payable/receivable/RCM ledgers |

### Write — vouchers

Every write tool below reads back what it just wrote and reports the actual
values Tally now has (or, for a delete, confirms it's really gone) — not
just whether Tally accepted the request. See
[docs/TOOLS.md](docs/TOOLS.md#write-tools) for details.

| Tool | Input | Output |
|---|---|---|
| `create_voucher` | `voucherType`, `date`, `voucherNumber?`, `reference?`, `narration?`, `debitLedger`/`creditLedger`/`amount` (simple 2-leg) **or** `entries?` (3+ legs), plus `debitBillName?`, `debitBillType?`, `creditBillName?`, `creditBillType?`, `debitCostCentre?`, `creditCostCentre?`, `costCategory?` | Creates a Payment/Receipt/Journal/Contra voucher — either a simple debit+credit pair, or any number of lines via `entries` (e.g. one payment split across three expense ledgers). Bill-wise allocation (`New Ref` / `Agst Ref`) requires `maintainBillWise` to have been set on the ledger. `voucherNumber` overrides Tally's auto-numbering; `reference` sets Tally's voucher-level `<REFERENCE>` field (e.g. a supplier's bill number) |
| `update_voucher` | `voucherType`, `voucherNumber`, `date`, `reference?`, `narration?`, `debitLedger`/`creditLedger`/`amount` **or** `entries?` | Replaces an existing voucher's entries/reference in place — matched by type + date + voucher number |
| `delete_voucher` | `voucherType`, `voucherNumber`, `date` | Permanently deletes a voucher — no trace left, distinct from cancelling (which keeps it visible, marked Cancelled) |
| `create_stock_journal` | `date`, `narration?`, `sources` (array of `stockItem`, `qty`, `rate`, `unit`, `godown?`, `batchName?`), `destinations` (same shape), `additionalCosts?` (array of `ledgerName`, `amount`, `allocationType?`), `voucherType?`, `voucherNumber?` | Creates a Stock Journal (or Manufacturing Journal, via `voucherType`) voucher moving inventory from one or more source items to one or more destination items — supports multiple raw materials in and multiple finished/by-products out in a single voucher, plus optional additional costs (labour, freight) folded into the produced items' valuation. Inventory-only, no ledger balance effect from `additionalCosts` itself — see the note below |
| `update_stock_journal` | Same fields as `create_stock_journal`, plus required `voucherNumber` | Replaces an existing Stock Journal/Manufacturing Journal's source/destination lines in place |
| `create_material_in` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `godown?`, `batchName?`), `voucherNumber?` | Creates a Material In voucher — stock received back from a job worker, tracked against their ledger without a real accounting posting. Verified live: stock quantity increases and the party ledger's balance stays unchanged |
| `update_material_in` | Same fields as `create_material_in`, plus required `voucherNumber` | Replaces an existing Material In voucher's item lines in place |
| `create_material_out` | Same shape as `create_material_in` | Creates a Material Out voucher — stock sent out to a job worker. Mirror of `create_material_in`, same verified-live behavior |
| `update_material_out` | Same fields as `create_material_out`, plus required `voucherNumber` | Replaces an existing Material Out voucher's item lines in place |
| `create_rejections_in` | `date`, `narration?`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `godown?`, `batchName?`), `voucherNumber?` | Creates a Rejections In voucher — goods rejected and returned to you. Inventory-only. Verified live: stock quantity increases correctly |
| `update_rejections_in` | Same fields as `create_rejections_in`, plus required `voucherNumber` | Replaces an existing Rejections In voucher's item lines in place |
| `create_rejections_out` | Same shape as `create_rejections_in` | Creates a Rejections Out voucher — goods you're rejecting outward. Mirror of `create_rejections_in`, same verified-live behavior |
| `update_rejections_out` | Same fields as `create_rejections_out`, plus required `voucherNumber` | Replaces an existing Rejections Out voucher's item lines in place |
| `create_sales_invoice` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `salesLedger`, `godown?`, `batchName?`, `discountPercent?`, `vatLedger?`, `vatRatePercent?`), `vatLedger?`, `vatRatePercent?`, `billName?`, `billType?`, `voucherNumber?` | Creates a real item-invoice Sales voucher — stock item lines with quantity/rate/discount, each posted to its own Sales ledger, grouped into one VAT line per distinct rate. Distinct from `create_voucher`, which has no stock item support. `voucherNumber`: some Tally configurations stop auto-numbering item-invoice vouchers via the XML gateway — pass it explicitly if creation fails with a blank `EXCEPTIONS:1`. **Note:** using the *same* party ledger/stock item in both a Sales and a Purchase invoice can make it return `Cannot be deleted!` afterward — a Tally **Company Data → Rewrite** clears this (not a permanent lock) |
| `update_sales_invoice` | Same fields as `create_sales_invoice`, plus required `voucherNumber` | Replaces an existing Sales invoice's item lines/party/narration in place, instead of delete+recreate |
| `create_purchase_invoice` | Same shape as `create_sales_invoice`, with `purchaseLedger` per item instead of `salesLedger` | Creates a real item-invoice Purchase voucher — the buying-side mirror of `create_sales_invoice`. Same dual-role deletion caveat applies |
| `update_purchase_invoice` | Same fields as `create_purchase_invoice`, plus required `voucherNumber` | Replaces an existing Purchase invoice's item lines in place |
| `create_credit_note` | Same shape as `create_sales_invoice`, `billType` defaults to `'Agst Ref'` | Creates a Sales-return Credit Note — sign convention mirrors Purchase's. Returning 5 units increases book quantity by 5 |
| `update_credit_note` | Same fields as `create_credit_note`, plus required `voucherNumber` | Replaces an existing Credit Note's item lines in place |
| `create_debit_note` | Same shape as `create_purchase_invoice`, `billType` defaults to `'Agst Ref'` | Creates a Purchase-return Debit Note — sign convention mirrors Sales's. Returning 3 units decreases book quantity by 3 |
| `update_debit_note` | Same fields as `create_debit_note`, plus required `voucherNumber` | Replaces an existing Debit Note's item lines in place |
| `create_delivery_note` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `salesLedger`, `godown?`, `batchName?`, `discountPercent?`), `voucherNumber?` | Creates a Delivery Note — item-line dispatch of goods before/without a full Sales invoice. ⚠️ The voucher type must be active in the company first (a company-level toggle) — once active, `get_vouchers`/`delete_voucher` find it correctly; `get_ledger_vouchers` never shows it, by design. ⚠️ Can also silently stop auto-numbering — pass `voucherNumber` explicitly if a create call fails with a blank `EXCEPTIONS:1` |
| `update_delivery_note` | Same fields as `create_delivery_note`, plus required `voucherNumber` | Replaces an existing Delivery Note's item lines in place. Same voucher-type-active and auto-numbering caveats |
| `create_receipt_note` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `purchaseLedger`, `godown?`, `batchName?`, `discountPercent?`), `voucherNumber?` | Creates a Receipt Note — buying-side mirror of `create_delivery_note`, same caveats |
| `update_receipt_note` | Same fields as `create_receipt_note`, plus required `voucherNumber` | Replaces an existing Receipt Note's item lines in place |
| `create_sales_order` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `salesLedger`, `dueDate`, `godown?`, `batchName?`, `discountPercent?`), `orderNumber`, `voucherNumber?` | Creates a Sales Order — a future commitment to sell, no stock/ledger movement yet. `orderNumber` and each item's `dueDate` are required (Tally rejects an Order-class voucher without them). Same voucher-type-active caveat as `create_delivery_note`. Tally silently reassigns its own voucher number for Order-class vouchers regardless of what's passed — check the real number via `get_vouchers` after creating one |
| `update_sales_order` | Same fields as `create_sales_order`, plus required `voucherNumber` (the existing voucher's number to match by) | Replaces an existing Sales Order's item lines, party, and order number in place |
| `create_purchase_order` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `purchaseLedger`, `dueDate`, `godown?`, `batchName?`, `discountPercent?`), `orderNumber`, `voucherNumber?` | Creates a Purchase Order — buying-side mirror of `create_sales_order`, same caveats |
| `update_purchase_order` | Same fields as `create_purchase_order`, plus required `voucherNumber` | Replaces an existing Purchase Order's item lines, party, and order number in place |
| `create_sales_quotation` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `salesLedger`, `dueDate`, `godown?`, `batchName?`, `discountPercent?`), `orderNumber`, `voucherNumber?` | Creates a Sales Quotation — a pre-order price quote, one step before `create_sales_order`. Tally treats it as an Order-class voucher, so `orderNumber` and each item's `dueDate` are required, same as `create_sales_order`. Same voucher-type-active caveat |
| `update_sales_quotation` | Same fields as `create_sales_quotation`, plus required `voucherNumber` | Replaces an existing Sales Quotation's item lines, party, and order number in place |
| `create_job_work_in_order` | `date`, `narration?`, `partyLedger`, `items` (array of `stockItem`, `qty`, `rate`, `unit`, `dueDate`, `godown?`, `batchName?`, `components` — array of `stockItem`, `qty`, `rate`, `unit`, `godown?`, `batchName?`), `orderNumber`, `voucherNumber?` | Creates a Job Work In Order — this company is the job worker, booking an order to process raw materials a customer will supply into a finished item delivered back. Each item's `components` list is the raw material the customer is expected to supply. `orderNumber` and each item's `dueDate` are required |
| `update_job_work_in_order` | Same fields as `create_job_work_in_order`, plus required `voucherNumber` | Replaces an existing Job Work In Order's item lines (and their component lists), party, and order number in place |
| `create_job_work_out_order` | Same fields as `create_job_work_in_order` | Creates a Job Work Out Order — mirror image: this company is the principal, sending raw materials (each item's `components`) out to a job worker and expecting a finished item back |
| `update_job_work_out_order` | Same fields as `create_job_work_out_order`, plus required `voucherNumber` | Replaces an existing Job Work Out Order's item lines (and their component lists), party, and order number in place |
| `create_physical_stock` | `date`, `narration?`, `items` (array of `stockItem`, `actualQty`, `unit`, `godown?`, `batchName?`), `voucherNumber?` | Creates a Physical Stock voucher — updates the item's book quantity to match a physical count (that's the point of the voucher). Counting 95 of an item with 100 in stock closes it at 95. Doesn't post any monetary write-off for the shortage/excess value itself — see [Troubleshooting](docs/TROUBLESHOOTING.md) if you're on an older build than this |
| `update_physical_stock` | Same fields as `create_physical_stock`, plus required `voucherNumber` | Replaces an existing Physical Stock voucher's counted lines in place |

> ℹ️ **`additionalCosts` on `create_stock_journal`/`update_stock_journal`:** this does **not** post a real transaction against the named ledger — its balance stays unchanged. It's a costing/valuation instruction only, telling Tally's stock valuation reports to fold that amount into the produced item's effective cost. The actual expense (e.g. paying labour) still needs recording separately, e.g. via `create_voucher`.

> ℹ️ **Voucher type collision:** `update_voucher`, `update_sales_invoice`,
> `update_purchase_invoice`, `update_credit_note`, `update_debit_note`,
> `update_stock_journal`, `update_physical_stock`, and `delete_voucher` all
> check for date+number ambiguity across voucher types before touching
> anything, and refuse rather than risk altering/deleting the wrong one.
> If you hit that refusal, see [Troubleshooting](docs/TROUBLESHOOTING.md)
> for why it happens and how to resolve it in Tally.

### Write — masters

| Tool | Input | Output |
|---|---|---|
| `create_ledger` | `name`, `oldName?`, `parent`, `openingBalance?`, `maintainBillWise?`, `trn?`, `email?`, `website?`, `phone?`, `mobile?`, `contactPerson?`, `gstin?`, `pan?`, `gstRegistrationType?`, `gstTypeOfSupply?`, `vatDealerType?`, `billCreditPeriod?`, `creditLimit?`, `address?`, `state?`, `country?`, `pincode?`, `mailingName?`, `addressApplicableFrom?`, `extraFields?` | Creates a ledger under the given group — or, if `oldName` is passed, alters/renames that existing ledger instead. `gstin`/`pan`/`gstRegistrationType`/`gstTypeOfSupply` are India's tax fields, distinct from `trn`/`vatDealerType` (UAE). `extraFields` is an escape hatch for any other native Tally ledger field by exact tag name |
| `create_group` | `name`, `oldName?`, `parent` | Creates an account group nested under a parent — or renames/reparents an existing one if `oldName` is passed |
| `create_stock_group` | `name`, `parent` | Creates a Stock Group (the category `create_stock_item`'s `group` field references) — distinct from `create_group`'s account groups |
| `create_stock_item` | `name`, `group`, `unit`, `openingBalance?`, `openingRate?`, `description?`, `partNumber?`, `costingMethod?`, `hsnCode?`, `taxability?`, `gstDetailsApplicableFrom?`, `gstTypeOfSupply?`, `isReverseChargeApplicable?`, `gstIneligibleItc?`, `rateOfVat?`, `ignoreNegativeStock?`, `extraFields?` | Creates a stock item. `hsnCode`/`taxability`/`isReverseChargeApplicable`/`gstIneligibleItc` write to Tally's date-versioned GST Details list; `gstTypeOfSupply` is a separate flat field. `extraFields` is an escape hatch for any other native field by exact tag name |
| `update_stock_item` | `name`, `group?`, `unit?`, `description?`, `partNumber?`, `costingMethod?`, `hsnCode?`, `taxability?`, `gstDetailsApplicableFrom?`, `gstTypeOfSupply?`, `isReverseChargeApplicable?`, `gstIneligibleItc?`, `rateOfVat?`, `ignoreNegativeStock?`, `extraFields?` | Updates any subset of an existing stock item's fields — same coverage as `create_stock_item`, all optional except `name` |
| `delete_stock_item` | `name` | Deletes a stock item (fails if it has transactions posted) |
| `create_unit` | `symbol`, `formalName?`, `decimalPlaces?` **or** `baseUnit`+`additionalUnit`+`conversion` for a compound unit | Creates a Unit of Measure — simple (e.g. `'Kg'`) by default, or compound (e.g. `'Box of 12 Nos'`) when `baseUnit` is passed. Both simple units must already exist before creating the compound unit that references them — required before using a unit that doesn't exist yet |
| `set_bill_of_materials` | `stockItem`, `componentListName?`, `basicQty?`, `unit?`, `components` (array of `stockItem`, `qty`, `unit`, `natureOfItem?`, `godown?`) | Attaches a recipe to an existing finished-goods stock item. Pure convenience layer over `create_stock_journal` — doesn't move stock or post anything itself. Verified live: attaches without error and causes no stock movement on its own. `natureOfItem`'s four values (`Component`/`Co-Product`/`By-Product`/`Scrap`) were each checked against Tally's own BoM screen — all map to the matching "Type of Item" label |
| `create_godown` | `name`, `parent?` | Creates a Godown/Location, optionally nested under a parent godown — pass the parent's plain name, not a dotted path |
| `create_cost_category` | `name`, `allocateToRevenue?`, `allocateToNonRevenue?` | Creates a Cost Category (grouping of cost centres) |
| `create_cost_centre` | `name`, `category?`, `parent?` | Creates a Cost Centre for tagging voucher entries (see `create_voucher`'s cost centre fields) |
| `create_voucher_type` | `name`, `oldName?`, `parent`, `numberingMethod?`, `abbreviation?`, `preventDuplicates?`, `useAsManufacturingJournal?`, `extraFields?` | Creates a custom Voucher Type derived from a base type (e.g. `'Bank Payment'` from `'Payment'`, or a proper `'Manufacturing Journal'` type from `'Stock Journal'` via `useAsManufacturingJournal`) — or renames/reconfigures an existing one if `oldName` is passed. Pass the resulting name as `voucherType` to `create_stock_journal`/`update_stock_journal` to post against it |
| `delete_master` | `collection`, `names` | Deletes one or more masters of any type — `LEDGER`, `GROUP`, `STOCKGROUP`, `STOCKITEM`, `VOUCHERTYPE`, `UNIT`, `GODOWN`, `COSTCATEGORY`, `COSTCENTRE`, etc. — by exact name |

> ⚠️ `create_ledger` / `update_voucher` / `delete_stock_item` / `delete_master` /
> `delete_voucher` modify or remove existing data. Keep a Tally backup before
> letting the model use these on data you care about — `delete_master` and
> `delete_voucher` in particular have no undo.

### Context switching

| Tool | Input | Output |
|---|---|---|
| `set_company` | `companyName` | Switches TallyPrime's active company — affects every subsequent call until changed again |
| `set_period` | `from`, `to` | Switches TallyPrime's active reporting period — affects every subsequent report until changed again |

> These change **global, persistent state in Tally itself** (the same as
> switching company/period from Tally's own UI) — not a per-call parameter.
> If you're running multiple tools/sessions against the same Tally
> instance, a `set_company`/`set_period` call from one affects what every
> other caller sees next.
>
> `set_company` can only switch to a company that is **already open** in
> Tally (multiple companies can be open at once) — naming one that isn't
> loaded returns an error rather than silently doing nothing. Open it in
> Tally first (File → Select Company), then switch to it via this tool.

### SQL cache

| Tool | Input | Output |
|---|---|---|
| `sync_to_sql` | — | Pulls ledgers, groups, and stock items into a **session-only, in-memory** SQL cache |
| `sync_vouchers_to_sql` | `from`, `to` | Pulls voucher headers (date, type, number, party, amount, narration — not line items) for one date range into the same cache. Call it once per chunk (e.g. per quarter) to build up full multi-year history within a session — each call only replaces vouchers in its own date range, so calling it for 2024 then 2025 gives you both |
| `sync_voucher_items_to_sql` | `from`, `to` | Pulls voucher **inventory line items** (stock item, qty, rate, amount, godown, batch — one row per item per batch allocation) for one date range into the same cache. This is the raw data for movement analysis, godown-wise stock, and batch detail — there's no separate report tool for those, it's a `query_sql` SELECT over this table. `qty`/`amount` are unsigned as Tally stores them; use `is_deemed_positive` with `voucher_type` to work out inward vs outward |
| `sync_voucher_ledger_entries_to_sql` | `from`, `to` | Pulls voucher **ledger lines** (which ledger, amount, cost centre, bill allocation — one row per ledger line per bill allocation) for one date range into the same cache. This is what fills the gap voucher totals alone can't: splitting a combined ledger's balance apart by voucher (e.g. VAT into Output vs Input), or reconciling a party ledger's movements voucher by voucher. `amount` is signed — negative for a debit line, positive for a credit line — so it sums directly |
| `query_sql` | `sql` (SELECT only) | Runs a read-only query against that cache — tables: `ledgers(name, parent, closing_balance, trn, state, country)`, `groups(name, parent)`, `stock_items(name, parent, closing_balance)`, `vouchers(guid, date, voucher_type, voucher_number, party_ledger, amount, narration)`, `voucher_items(voucher_guid, date, voucher_type, voucher_number, stock_item, qty, rate, amount, is_deemed_positive, godown, batch)`, `voucher_ledger_entries(voucher_guid, date, voucher_type, voucher_number, ledger, amount, is_deemed_positive, cost_centre, bill_name, bill_type)` |

`get_profit_and_loss`, `get_stock_summary`, `get_balance_sheet`,
`get_trial_balance`, `get_vat_liability_summary`, and
`get_gst_liability_summary` also cache themselves into this same store
automatically — `profit_and_loss(ledger_name, group_name, closing_balance,
period_from, period_to)`, `stock_summary(name, parent, opening_qty,
closing_qty, opening_value, closing_value, as_of_date)`,
`balance_sheet(group_name, amount, as_of_date)`, `trial_balance(name,
debit_amount, credit_amount, period_from, period_to)`,
`vat_summary(ledger_name, category, match_method, closing_balance,
period_from, period_to)`, and `gst_summary` (same shape as `vat_summary`)
— no separate sync call needed. Each holds only the most recent call's
result, replaced whenever you call that report tool again.

> The cache is **in-memory and session-scoped only** — it's gone as soon as
> the server process exits, and there's no persistence to disk. This is
> deliberate: since one Tally connection can be pointed at many different
> client companies over time (`set_company`), nothing here tracks *which*
> company a cached row came from. If you switch companies, re-sync (or, for
> the six automatic tables, re-call the report tool) before querying —
> don't run `query_sql` against a cache that spans a company switch, since
> the rows won't be distinguishable by company.

### Audit & permissions

| Tool | Input | Output |
|---|---|---|
| `get_audit_log` | `limit?` (default 50), `toolFilter?`, `writesOnly?`, `fromDate?`, `toDate?`, `company?`, `format?` (`'json'` default or `'summary'`) | Reads this connector's audit log — every tool call made through it, read or write, with timestamp, arguments, outcome (`success`/`error`/`denied`), and a best-effort company tag. `company` filters to one Tally company; `writesOnly` + a date range + `format: 'summary'` gives a compact reviewer-facing table instead of raw JSON — "what changed between these two dates" |
| `get_health_check` | — | Reports whether Tally's gateway is actually reachable (not just "something answered" — this catches cases like Tally's own license server responding on a misconfigured port with an HTML page that looks like success), which company is open, the active `TALLY_URL`, current read-only/disabled-tools state, and the audit log's file path. Always allowed, even in read-only mode |
| `preview_write` | `toolName`, `args` | Builds the exact XML any create_*/update_*/delete_*/`set_bill_of_materials` tool would send — without sending it. Returns a `previewId`, a plain-English description, and the raw XML, so a batch of changes can be reviewed before anything posts. Runs the same pre-checks the real tool would (e.g. the voucher-collision check on update_*/`delete_voucher`) at preview time. Never sends a write of its own, so it still works in read-only mode — unlike every other write tool |
| `confirm_write` | `previewId` | Posts a previewed write to Tally, unchanged, using its `previewId`. The only tool that actually writes when a batch went through `preview_write` first. Single-use — a `previewId` already confirmed, or expired (15 minutes), fails rather than reposting |


Every tool call — read or write — is appended to a local JSONL log file, so
there's always a plain-text record of exactly what an agent did. All
companies share this one log file; each entry carries a best-effort
`company` tag (updated whenever `get_company_info`, `get_health_check`, or
`set_company` succeeds — not a live lookup on every call). Once per server
start, entries older than 90 days are permanently deleted by rewriting the
file — this is a hard delete, not an archive. A cheap size check on every
write also triggers the same deletion if the file grows past 50MB, so a
long-lived process doesn't have to wait for a restart for this to kick in.

**Safe by default:** a fresh `.mcpb` install starts in **read-only mode** —
this connector can look at your books but cannot change anything until you
deliberately turn writes on. If you installed via the `.mcpb`, Claude
Desktop's Extensions settings screen for this connector shows a **"Read-only
mode"** toggle (on by default) and an optional **"Disabled tools"** field
directly — no config editing required either way.

> ⚠️ **Settings changes take effect only after you fully quit and reopen
> Claude Desktop** — saving the settings screen alone does *not* apply the
> new value. This connector runs as a long-lived child process that only
> reads its configuration once, at startup; Claude Desktop doesn't push new
> values into an already-running extension. Toggling "Read-only mode" on
> and expecting it to take effect immediately will fail silently (the next
> write still goes through) until you restart the app.

Running this outside Claude Desktop (HTTP mode, manual config)? Use the
environment variables in the table below instead — see `TALLY_PERMISSION_MODE`
and `TALLY_DISABLED_TOOLS`.

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `TALLY_URL` | `http://localhost:9000` | Tally's HTTP gateway address. Installed via `.mcpb`? This is the "Tally Gateway URL" field in Claude Desktop's Extensions settings — no manifest editing needed |
| `TALLY_READ_ONLY` | `true` for `.mcpb` installs via Claude Desktop's Extensions UI (safe-by-default); `false` if unset entirely (e.g. bare HTTP/manual deployments) | Set to `true` to block every write tool before it reaches Tally. This is the "Read-only mode" toggle in Claude Desktop's Extensions settings for `.mcpb` installs, **on by default** — you have to deliberately turn it off before this connector can write anything |
| `TALLY_PERMISSION_MODE` | `read_write` | String-form equivalent of `TALLY_READ_ONLY`, for HTTP/manual deployments — set to `read_only` for the same block. Either variable blocks writes; you don't need to set both |
| `TALLY_DISABLED_TOOLS` | _(unset)_ | Comma-separated exact tool names to block regardless of mode, e.g. `delete_voucher,delete_master` to allow writes but forbid deletion. This is the "Disabled tools (advanced)" field in Claude Desktop's Extensions settings for `.mcpb` installs |
| `TALLY_AUDIT_LOG_PATH` | `audit.log.jsonl` next to the installed package | Where the append-only audit log is written. Point multiple connector instances at one shared path if you want a single combined log |
| `PORT` | `3939` | Port for `npm run start:http` (remote mode only) |
| `TALLY_MCP_TOKEN` | _(unset)_ | Bearer token required on the HTTP server's `/mcp` endpoint if set (remote mode only) |
| `PUBLIC_URL` | `http://localhost:<PORT>` | This server's externally-reachable HTTPS address (remote mode only) — required for the OAuth login flow in [docs/OAUTH_CONNECTORS.md](docs/OAUTH_CONNECTORS.md) to work |

> **Running this alongside another Tally MCP connector?** Each connector needs
> its own gateway port open in TallyPrime (`F1 → Settings → Connectivity`) —
> two connectors can't share port 9000. If you already have one connector
> using 9000, open a second gateway on e.g. 9001 for this one, edit
> `mcp_config.env.TALLY_URL` in `manifest.json` to match before packing, or
> set `TALLY_URL` directly wherever your MCP client lets you configure this
> server's environment.

## Building from source

Only needed if you're modifying the code rather than using the downloaded `.mcpb`.

```bash
npm install --omit=dev   # production deps only
npm run build             # compiles src/ -> dist/
```

**Node version note:** if your system `node --version` reports something
ancient, check `where node` (Windows) / `which -a node` (macOS/Linux) for a
newer install elsewhere on `PATH` and use it explicitly for the commands
above — Claude Desktop's *own* bundled Node is what runs the installed
extension regardless of your system `PATH`, so this only affects building.

### Packing a `.mcpb` yourself

```bash
npm install -g @anthropic-ai/mcpb   # one-time
mcpb validate manifest.json
mcpb pack . PNPC-MCP-Tally-Prime.mcpb
```

## Troubleshooting

The most common issues at a glance:

- **"Could not reach TallyPrime"** — Tally isn't running, or the gateway isn't enabled on port 9000.
- **"Tally returned an empty response"** — Tally is running but no company is open.
- **A write call returns `CREATED:0`/`EXCEPTIONS:1` with no error text** — most often a missing `godown` on a company with location tracking enabled.
- **`create_ledger` / `create_voucher` fails** — parent group / ledger names must match Tally *exactly* (case- and whitespace-sensitive).

For the full FAQ and every other real-world gotcha (shared-server port
conflicts, silent write failures, deletion quirks, voucher-numbering
surprises, the audit log, extension install issues, and more), see
**[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)**.

## Project structure

```
src/
  tally.ts        Tally HTTP client: sends XML, handles connection/timeout errors
  clean.ts        Normalizes Tally's raw XML->JSON into predictable JSON
  templates.ts    Renders the Nunjucks XML templates in templates/
  db.ts           PGLite SQL cache: sync_to_sql / sync_vouchers_to_sql / query_sql
  audit.ts        Append-only JSONL audit log (every tool call, read or write)
  permissions.ts  Write-scoping via TALLY_PERMISSION_MODE / TALLY_DISABLED_TOOLS
  tools.ts        MCP tool definitions + XML request builders
  server.ts       Shared MCP Server construction (used by both entry points) — wires audit logging + permission checks around every call
  index.ts        stdio entry point (local Claude Desktop)
  http-server.ts  HTTP entry point (remote clients)
templates/
  *.xml.njk       Nunjucks templates for each Tally XML request shape
manifest.json     Claude Desktop Extension manifest (manifest_version 0.3)
```

## Docs

- [docs/INSTALL_GUIDE.md](docs/INSTALL_GUIDE.md) — background on why the install steps above are structured this way
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — request flow, file responsibilities
- [docs/TALLY_XML_GUIDE.md](docs/TALLY_XML_GUIDE.md) — how Tally's XML gateway works, gotchas
- [docs/TOOLS.md](docs/TOOLS.md) — full tool reference + how to add a new tool
- [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) — full FAQ + every real-world gotcha, by category
- [docs/SQL_CACHE.md](docs/SQL_CACHE.md) — the PGLite SQL cache, schema, examples
- [docs/HTTP_DEPLOYMENT.md](docs/HTTP_DEPLOYMENT.md) — running as a remote HTTP server
- [docs/CLOUDFLARE_TUNNEL.md](docs/CLOUDFLARE_TUNNEL.md) — exposing that server without opening a router port
- [docs/OAUTH_CONNECTORS.md](docs/OAUTH_CONNECTORS.md) — connecting from claude.ai, ChatGPT, or Grok
- [docs/EXTENSION_PACKAGING.md](docs/EXTENSION_PACKAGING.md) — packaging as a Claude Desktop Extension
- [skills/tally-doc-import](skills/tally-doc-import) — a Claude Skill that turns a folder of client paperwork (invoices, bank statements, credit/debit notes) into a reviewed batch of vouchers, using `preview_write`/`confirm_write` so nothing posts without approval

## Roadmap / not yet supported

- GST/VAT-specific statutory reports (e.g. GSTR-1, GSTR-3B, VAT return format)
- One-click packaging (a `.mcpb` equivalent) for clients other than Claude Desktop — claude.ai, ChatGPT, and Grok are already reachable via the remote/OAuth setup above, but only through manual connector configuration; there's no installer for them the way `.mcpb` is for Claude Desktop

## License

ISC — see [LICENSE](LICENSE). Early development was inspired by ideas
from [vaijaaaaa/Tally-MCP-Server](https://github.com/vaijaaaaa/Tally-MCP-Server)
and [dhananjay1405/tally-mcp-server](https://github.com/dhananjay1405/tally-mcp-server).

---

<p align="center">
  Built and maintained by <b>Lokesh Sparrow</b>, <a href="https://www.pnpcglobal.com">PNPC Global</a> —
  questions or issues? <a href="https://github.com/lokesh-sparrow/PNPC-MCP-Tally-Prime/issues">Open an issue</a>.
</p>

TDQS

A3.7/5.0

Scored across 86 tools

Disambiguation4/5

With 86 tools there is inherent overlap risk (create/update pairs across ~15 voucher types, four sync_*_to_sql variants, several register and report tools), but the descriptions are unusually meticulous at differentiating them, explicitly cross-referencing siblings (e.g. create_voucher vs create_sales_invoice, get_vouchers vs get_ledger_vouchers, material_in vs receipt_note vs purchase_invoice). An agent can generally tell which tool applies, though the sheer number of near-parallel create_/update_ pairs raises the chance of a near-miss selection.

Naming Consistency5/5

Names follow a clean, predictable snake_case verb_noun convention throughout: get_*, create_*, update_*, delete_*, set_*, sync_*_to_sql, plus preview_write/confirm_write and query_sql. No casing or verb-style mixing; the pattern is fully predictable.

Tool Count2/5

At 86 tools this is far above the 3-15 sweet spot and into the 50+ 'extreme mismatch' band, even though TallyPrime is a genuinely large domain. Several tools clearly overlap in scope (delete_stock_item vs delete_master, multiple sync variants), so consolidation is possible and the surface feels bloated.

Completeness4/5

Coverage is broad: create/update for most voucher types, generic create/update/delete for vouchers, and create_ledger/create_group/create_stock_item/update_stock_item/delete_master for masters. Minor gaps exist (some masters like godowns, cost centres, units and voucher types have create but only generic delete_master, and some have no update path), but core lifecycles are covered and agents can work around them.

Maintenance

ActivityMaintained
ResponsivenessNo issues