tally-mcp-server
# tally-mcp-server
An MCP (Model Context Protocol) server that exposes TallyPrime data to any
MCP-compatible client (Claude Desktop, Claude Code, etc.) over Tally's
**classic XML HTTP API** - the integration method supported by every
Tally.ERP 9 / TallyPrime release.
## Available tools
| Tool key | What it returns |
| ---------------------- | ------------------------------------------------------------ |
| `list_companies` | Companies known to the running Tally instance |
| `list_ledgers` | All ledger accounts, with opening/closing balances |
| `list_groups` | All accounting groups (chart-of-accounts categories) |
| `list_stock_items` | All inventory items, with closing qty/rate/value |
| `day_book` | Vouchers posted between two dates (`fromDate`, `toDate`) |
| `outstanding_balances` | Ledgers in a group with closing balance (receivables/payables) |
## How it talks to Tally
TallyPrime can act as an HTTP server and accept an XML "envelope":
`<ENVELOPE><HEADER>...</HEADER><BODY>...</BODY></ENVELOPE>`. Two request
shapes are used here:
- **Built-in reports** (e.g. `list_companies`) - just name the report,
no TDL needed.
- **Custom collections** (everything else) - an inline TDL
`<COLLECTION>` naming a native Tally object type (`Ledger`, `Group`,
`StockItem`, `Voucher`) and the fields to fetch. This is the standard
technique for "give me all X" pulls and is what most third-party Tally
integrations use.
Docs: https://help.tallysolutions.com/developer-reference/introduction/integration-with-tallyprime/
## 1. Enable Tally as an HTTP server
In TallyPrime: **F1 (Help) → Settings → Connectivity → Client/Server
configuration** → set "TallyPrime acts as" to **Server**, and note the port
(default `9000`).
## 2. Install
```bash
npm install
cp .env.example .env
# edit .env if your Tally isn't on localhost:9000
```
## 3. Run standalone (for testing)
```bash
npm start
```
The server speaks MCP over stdio, so running it directly won't print
anything to stdout - that's expected. `Ctrl+C` to stop.
## 4. Connect it to an MCP client
Example Claude Desktop config (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"tally": {
"command": "node",
"args": ["/absolute/path/to/tally-mcp-server/src/index.js"]
}
}
}
```
## Project structure
```
src/
├── index.js # MCP server bootstrap - wires everything together
├── config.js # Reads TALLY_HOST / TALLY_PORT / etc. from .env
├── tally/
│ ├── client.js # Generic transport: builds/sends the XML envelope,
│ │ # parses the XML response into a plain object
│ └── endpoints/
│ ├── index.js # Registry - list every supported Tally API here
│ ├── companies.js # Built-in report example
│ ├── ledgers.js # Custom TDL collection example
│ ├── groups.js
│ ├── stockItems.js
│ ├── dayBook.js # Collection example with runtime params (dates)
│ └── outstanding.js # Collection example with a filter
└── tools/
└── index.js # Turns every registered endpoint into an MCP tool
```
The split matters:
- **`tally/client.js`** only knows how to *transport* a request (build the
XML envelope, POST it, parse the XML response, surface errors). It has
zero Tally-report-specific knowledge.
- **`tally/endpoints/*.js`** each describe *one* Tally API: what to
request (`request: { kind, ... }`), any input parameters it needs, and
how to turn Tally's parsed XML into a clean shape.
- **`tools/index.js`** loops over the endpoint registry and calls
`server.tool(...)` for each one - so a new endpoint becomes a working MCP
tool automatically, with no changes to the server or client code.
## Adding a new Tally API
Say you want to add "List Cost Centres":
1. Create `src/tally/endpoints/costCentres.js`, modeled on `groups.js`:
```js
import { asArray } from "../client.js";
export default {
key: "list_cost_centres",
title: "List Cost Centres",
description: "Returns cost centres from TallyPrime.",
inputSchema: {},
request: {
kind: "collection",
collectionName: "MCP CostCentres",
type: "CostCentre",
fetch: ["NAME", "PARENT", "CATEGORY"],
},
parseResponse(parsedXml) {
const entries = asArray(parsedXml?.ENVELOPE?.COSTCENTRE);
return entries.map((e) => ({
name: e["@_NAME"] ?? e.NAME,
parent: e.PARENT,
category: e.CATEGORY,
}));
},
};
```
2. Register it in `src/tally/endpoints/index.js` (import + add to the
`endpoints` array).
3. Restart the server. `list_cost_centres` is now a callable MCP tool -
nothing else needs to change.
If an endpoint needs runtime parameters (dates, a filter value, etc.),
add zod validators to `inputSchema` and read them in `request.staticVariables(params)`
and/or `request.buildFilterValue(params)` - see `dayBook.js` and
`outstanding.js` for examples.
## Troubleshooting
- **"Could not reach TallyPrime"** - Tally isn't running, isn't configured
as a server, or `TALLY_HOST`/`TALLY_PORT` in `.env` don't match.
- **Empty results or `<LINEERROR>`** - the report/collection `id` or TDL
field names may need adjusting for your Tally release or company
configuration. Turn on Tally's "Test Client" / check the Tally log
screen while a request runs to see exactly what it received.
- **Unexpected shape in `list_companies`** - `parseResponse()` tries a
few likely XML shapes and falls back to returning the raw parsed
payload with a `warning` field so you can see exactly what Tally sent
back and adjust the parser.
- **Numbers coming back as strings/numbers inconsistently** - Tally's XML
doesn't strongly type values; `fast-xml-parser` does its best guess.
If you need guaranteed types, cast explicitly inside `parseResponse()`.
## To test MCP sever in browser
```
npx @modelcontextprotocol/inspector node src/index.js
```TDQS
Scored across 6 tools
Each tool targets a distinct Tally entity: companies, ledgers, groups, stock items, vouchers, and balance status. There is no overlap in the data returned, so an agent can easily select the right tool.
Most tools follow a list_* pattern (list_companies, list_ledgers, list_groups, list_stock_items), but day_book and outstanding_balances deviate. Despite this, the names remain descriptive and predictable.
Six tools is a well-scoped set for a TallyPrime integration, covering the primary read operations without being sparse or overwhelming.
The tool set provides comprehensive read-only coverage of accounting entities: masters, transactions, and balances. Minor gaps exist, such as no voucher-level detail beyond day_book, but the core reporting needs are met.