Skip to main content
Glama
savekikiz

PEAK Account MCP Server

by savekikiz
README.md
# PEAK Account MCP Server

MCP (Model Context Protocol) server for [PEAK Account](https://www.peakaccount.com) — a Thai cloud accounting platform. It lets AI assistants like Claude manage your PEAK data through natural language, using the **official [PEAK Open API](https://developers.peakaccount.com/)**.

Unlike a browser-scraping approach, this server talks to PEAK's documented REST API and authenticates with HMAC-SHA1 request signing — no headless browser required. **136 tools** cover the full API surface.

## Features

- **Master data** — Contacts, Products, Services, Payment Methods
- **Sales / Income** — Quotations, Invoices, Receipts, Credit Notes, Billing Notes (create/get/list/edit/void/approve, payments, e-Tax Invoice / e-Receipt, file attachments, async queue)
- **Purchases / Expense** — Purchase Orders, Expenses, Credit Note Expenses, Billing Note Expenses (create/get/list/edit/void/approve, payments, received tax invoice, attachments)
- **Accounting & misc** — Daily Journals, Account Codes, Tags, Classifications, Invitations, File Vault

> **Note on "delete":** PEAK is an accounting system — posted documents are **voided (`void_*`)**, never hard-deleted, to preserve the audit trail.

## How authentication works

Every PEAK API request carries four headers:

| Header | Value |
|---|---|
| `Time-Stamp` | Current UTC time, `yyyyMMddHHmmss` |
| `Time-Signature` | `HMAC-SHA1(Time-Stamp)`, secret = your **Connect ID** (default; switchable to Connect Key) |
| `User-Token` | Issued by PEAK for your business |
| `Client-Token` | Fetched from `POST /clienttoken` (valid 24h, cached in memory) |

The **client token** is obtained by `POST /api/v1/clienttoken` with body `{ "peakClientToken": { "connectId": "...", "connectKey": "..." } }`, signed with `Time-Stamp` + `Time-Signature`. The success response is `{ "PeakClientToken": { "token": "…", "resCode": "200", "resDesc": "Token Authorized" } }`.

The server generates `Time-Stamp`/`Time-Signature` fresh per request, caches the client token, and refreshes it automatically on a `401`.

## Rate limits (enforced by PEAK)

| Policy | Scope | Limit |
|---|---|---|
| Client token | `POST /clienttoken` | 10 requests / minute |
| File upload | `*-insertfile`, `FileVault` | 2 requests / minute |
| GET concurrency | per User-Token | 10 concurrent |
| POST concurrency | per User-Token | 5 concurrent |

Exceeding a limit returns `HTTP 429`. The client automatically backs off (honoring `retryAfterSeconds`) and retries.

## Setup

### Prerequisites
- Node.js 18+
- A PEAK Developer account with **Connect ID**, **Connect Key**, and **User Token** — see [peakaccount.com/developers](https://www.peakaccount.com/developers). API access requires the **PEAK PRO Plus** package (฿12,000/year) or higher; UAT/sandbox is free for 3 months.

### Install & build

```bash
npm install
npm run build
```

### Configure

Copy `.env.example` to `.env` and fill in your credentials:

```bash
PEAK_ENV=uat                 # "uat" (sandbox) or "production"
PEAK_CONNECT_ID=...          # sent in /clienttoken body
PEAK_CONNECT_KEY=...         # sent in /clienttoken body
PEAK_USER_TOKEN=...          # per-business token, sent as header
# PEAK_SIGNATURE_ENCODING=base64   # switch to "hex" if auth fails
# PEAK_SIGNATURE_SECRET=connectId  # switch to "connectKey" if auth fails
```

### Verify auth (recommended first step)

```bash
export $(grep -v '^#' .env | xargs)   # load .env into the shell
npm run auth-check
```

This acquires a client token and prints masked headers. If it fails:
- `TimeSignature Unauthorized` → try `PEAK_SIGNATURE_ENCODING=hex`, then `PEAK_SIGNATURE_SECRET=connectKey`.
- `Validate User Failed` → check `PEAK_CONNECT_ID` / `PEAK_CONNECT_KEY`.

### Claude Desktop / Claude Code integration

```json
{
  "mcpServers": {
    "peakaccount": {
      "command": "node",
      "args": ["/path/to/PeakAccountMcp/dist/index.js"],
      "env": {
        "PEAK_ENV": "uat",
        "PEAK_CONNECT_ID": "your-connect-id",
        "PEAK_CONNECT_KEY": "your-connect-key",
        "PEAK_USER_TOKEN": "your-user-token"
      }
    }
  }
}
```

## Environments

`PEAK_ENV` selects the base URL (confirmed from the PEAK OpenAPI `servers` block):

| `PEAK_ENV` | Default base URL | Override env var |
|---|---|---|
| `uat` | `https://peakengineapidev.azurewebsites.net/api/v1` | `PEAK_BASE_URL_UAT` |
| `production` | `https://api.peakaccount.com/api/v1` | `PEAK_BASE_URL_PROD` |

## Tool design

- **Read** tools (`get_*`, `list_*`) take an optional `params` object mapped to the endpoint's query string (e.g. `{ "id": "…" }` or `{ "currentPage": 1, "pageSize": 20 }`).
- **Write** tools (`create_*`, `update_*`, `void_*`, `approve_*`, `record_*_payment`, …) take a `data` object sent as the JSON request body. Field names must match the PEAK API schema for that endpoint.
- **File** tools (`attach_file_to_*`, `upload_file_vault`) take a `filePath` plus optional `fields`.

---

## 📋 Payload cheat sheet

Field names below are taken from the PEAK OpenAPI definitions. **Numeric enums** (`type`, `taxStatus`, `vatType`, `isCheque`, `bankId`, …) must be looked up in the [numeric reference](https://developers.peakaccount.com/reference/contact-numeric). Always confirm against the [API reference](https://developers.peakaccount.com/reference) for your account.

### Create a contact — `create_contact`

Documents are wrapped in `PeakContacts.contacts[]`:

```json
{
  "data": {
    "PeakContacts": {
      "contacts": [
        {
          "name": "บริษัท ตัวอย่าง จำกัด",
          "type": 2,
          "taxNumber": "0105557000000",
          "branchCode": "00000",
          "address": "123 ถนนตัวอย่าง",
          "subDistrict": "ท่าแร้ง",
          "district": "บางเขน",
          "province": "กรุงเทพมหานคร",
          "country": "Thailand",
          "postCode": "10220",
          "email": "contact@example.com",
          "callCenterNumber": "021234567",
          "contactFirstName": "สมชาย",
          "contactLastName": "ใจดี"
        }
      ]
    }
  }
}
```

Key fields: `name`* (name only, no legal prefix), `type`* (2 = juristic, 5 = individual — see numeric ref), `prefixNameType` (for individuals), `taxNumber`, `branchCode` (5 digits), address parts, `contactFirstName/LastName/Email`, `purchaseAccount`/`sellAccount` (chart-of-accounts codes), `bankAccount` (`bankId`, `bankBranch`, `bankAccountNo`, `bankAccountName`).

### Issue an invoice (income) — `create_invoice`

Wrapped in `peakInvoices.invoices[]`; `getResult` trims the response:

```json
{
  "data": {
    "peakInvoices": {
      "invoices": [
        {
          "issuedDate": "2026-07-19",
          "dueDate": "2026-08-18",
          "contactId": "917c3e8f-b315-4968-b8cc-08e2220d8682",
          "reference": "SO-2026-001",
          "remark": "ค่าบริการที่ปรึกษา",
          "taxStatus": 1,
          "isTaxInvoice": 1,
          "products": [
            {
              "productCode": "SV001",
              "description": "ค่าบริการที่ปรึกษา เดือน ก.ค.",
              "quantity": 1,
              "price": 10000,
              "discount": "0",
              "vatType": 1
            }
          ]
        }
      ],
      "getResult": 1
    }
  }
}
```

Header fields: `code` (auto if omitted), `issuedDate`*, `dueDate`, `contactId` **or** `contactCode`, `reference`, `remark`, `taxStatus` (VAT status — numeric), `isTaxInvoice`, `discountTotal`, `tags[]`. `preTaxAmount`/`vatAmount`/`netAmount` auto-calc if null.
Line item (`products[]`): `productId`/`productCode`, `description`, `accountCode`, `quantity`, `price`, `discount`, `vatType`, `withHoldingTaxAmount`.

> `create_invoice_allinone` uses the same body but also approves the document in one call. To bill from a quotation, use `create_invoice_by_quotation`.

### Record a payment on an invoice — inline or `record_invoice_payment`

Payments can be embedded in the document via `paidPayments`, or recorded later against an approved invoice:

```json
{
  "data": {
    "paidPayments": {
      "paymentDate": "20260720",
      "withHoldingTaxAmount": "0",
      "payments": [
        {
          "amount": 10700,
          "isCheque": 0,
          "paymentMethodCode": "PM-CASH",
          "note": "รับโอนพร้อมเพย์"
        }
      ]
    }
  }
}
```

Payment fields: `amount`*, `paymentMethodId`/`paymentMethodCode`, `isCheque` (0/1), `cheque` (object when `isCheque=1`), `accountCode`/`accountSubCode`, `note`. `paymentDate` is `yyyyMMdd`.

### Record an expense (รายจ่าย) — `create_expense`

Same document shape as invoices, wrapped in `peakExpenses.expenses[]`:

```json
{
  "data": {
    "peakExpenses": {
      "expenses": [
        {
          "issuedDate": "2026-07-19",
          "dueDate": "2026-07-19",
          "contactId": "917c3e8f-b315-4968-b8cc-08e2220d8682",
          "reference": "BILL-8842",
          "remark": "ค่าอินเทอร์เน็ตสำนักงาน",
          "taxStatus": 1,
          "isTaxInvoice": 1,
          "products": [
            {
              "description": "ค่าบริการอินเทอร์เน็ต 1 เดือน",
              "accountCode": "540104",
              "quantity": 1,
              "price": 800,
              "vatType": 1
            }
          ],
          "paidPayments": {
            "paymentDate": "20260719",
            "payments": [
              { "amount": 856, "isCheque": 0, "paymentMethodCode": "PM-CASH" }
            ]
          }
        }
      ],
      "getResult": 1
    }
  }
}
```

For input-VAT (ภาษีซื้อ), use `create_expense_received_tax_invoice`. `create_expense_allinone` creates + approves.

### VAT / Tax

VAT is embedded in each line item (`vatType`) and document (`taxStatus`), not a separate resource. To issue electronic tax documents:
- Output VAT (ภาษีขาย): `create_invoice_etax`, `create_receipt_etax`, `create_credit_note_etax`
- Input VAT (ภาษีซื้อ): `create_expense_received_tax_invoice`

> VAT return reports (ภ.พ.30) are **not** exposed by the API.

### Reads

```json
// get_contact
{ "params": { "id": "917c3e8f-b315-4968-b8cc-08e2220d8682" } }

// list_invoices
{ "params": { "currentPage": 1, "pageSize": 20 } }
```

### Async queue

Some documents process asynchronously. Submit with `enqueue_*` and poll with `get_*_queue` (e.g. `enqueue_invoice` → `get_invoice_queue`).

---

## 🧰 All 136 tools

Legend: 🔍 read (GET, takes `params`) · ✏️ write (POST, takes `data`) · 📎 file (multipart, takes `filePath`)

### Master Data — Contacts, Products, Services, Payment Methods (19)

| # | Tool | Type | Description |
|---|---|---|---|
| 1 | `create_contact` | ✏️ write | Create a contact (customer/supplier) in PEAK. |
| 2 | `get_contact` | 🔍 read | Get a single contact by id (pass { id }). |
| 3 | `list_contacts` | 🔍 read | List contacts with optional paging/search params. |
| 4 | `update_contact` | ✏️ write | Edit an existing contact. |
| 5 | `list_contact_groups` | 🔍 read | List contact groups. |
| 6 | `create_product` | ✏️ write | Create a product. |
| 7 | `get_product` | 🔍 read | Get a single product by id. |
| 8 | `list_products` | 🔍 read | List products with optional paging/search params. |
| 9 | `update_product` | ✏️ write | Edit an existing product. |
| 10 | `adjust_product` | ✏️ write | Adjust product stock quantity. |
| 11 | `list_product_units` | 🔍 read | List product units. |
| 12 | `create_service` | ✏️ write | Create a service. |
| 13 | `get_service` | 🔍 read | Get a single service by id. |
| 14 | `list_services` | 🔍 read | List services with optional paging/search params. |
| 15 | `update_service` | ✏️ write | Edit an existing service. |
| 16 | `list_service_units` | 🔍 read | List service units. |
| 17 | `create_payment_method` | ✏️ write | Create a payment method. |
| 18 | `list_payment_methods` | 🔍 read | Get/list payment methods. |
| 19 | `transfer_payment_method` | ✏️ write | Transfer funds between payment methods. |

### Sales / Income — Quotations, Invoices, Receipts, Credit Notes, Billing Notes (61)

| # | Tool | Type | Description |
|---|---|---|---|
| 1 | `create_quotation` | ✏️ write | Create a quotation (draft). |
| 2 | `create_quotation_allinone` | ✏️ write | Create + approve a quotation in one call. |
| 3 | `get_quotation` | 🔍 read | Get a single quotation by id. |
| 4 | `list_quotations` | 🔍 read | List quotations with optional paging/filter params. |
| 5 | `update_quotation` | ✏️ write | Edit an existing quotation. |
| 6 | `void_quotation` | ✏️ write | Void a quotation. |
| 7 | `approve_quotation` | ✏️ write | Approve a quotation. |
| 8 | `enqueue_quotation` | ✏️ write | Submit a quotation to the async processing queue. |
| 9 | `get_quotation_queue` | 🔍 read | Poll the status of a queued quotation. |
| 10 | `set_quotation_queue_priority` | ✏️ write | Set queue priority for a quotation. |
| 11 | `attach_file_to_quotation` | 📎 file | Attach a file to a quotation. |
| 12 | `create_invoice` | ✏️ write | Create an invoice (draft). |
| 13 | `create_invoice_allinone` | ✏️ write | Create + approve an invoice in one call. |
| 14 | `create_invoice_by_quotation` | ✏️ write | Create an invoice from a quotation. |
| 15 | `get_invoice` | 🔍 read | Get a single invoice by id. |
| 16 | `list_invoices` | 🔍 read | List invoices with optional paging/filter params. |
| 17 | `list_invoices_by_contact` | 🔍 read | List invoices for a specific contact. |
| 18 | `update_invoice` | ✏️ write | Edit an existing invoice. |
| 19 | `void_invoice` | ✏️ write | Void an invoice. |
| 20 | `approve_invoice` | ✏️ write | Approve an invoice. |
| 21 | `record_invoice_payment` | ✏️ write | Record a payment against an invoice. |
| 22 | `void_invoice_payment` | ✏️ write | Void a recorded invoice payment. |
| 23 | `create_invoice_etax` | ✏️ write | Issue an e-Tax Invoice for an invoice. |
| 24 | `enqueue_invoice` | ✏️ write | Submit an invoice to the async processing queue. |
| 25 | `get_invoice_queue` | 🔍 read | Poll the status of a queued invoice. |
| 26 | `set_invoice_queue_priority` | ✏️ write | Set queue priority for an invoice. |
| 27 | `attach_file_to_invoice` | 📎 file | Attach a file to an invoice. |
| 28 | `create_receipt` | ✏️ write | Create a receipt (draft). |
| 29 | `create_receipt_allinone` | ✏️ write | Create + approve a receipt in one call. |
| 30 | `create_receipt_by_invoice` | ✏️ write | Create a receipt from an invoice. |
| 31 | `create_receipt_by_quotation` | ✏️ write | Create a receipt from a quotation. |
| 32 | `create_receipt_by_billing_note` | ✏️ write | Create a receipt from a billing note. |
| 33 | `get_receipt` | 🔍 read | Get a single receipt by id. |
| 34 | `list_receipts` | 🔍 read | List receipts with optional paging/filter params. |
| 35 | `update_receipt` | ✏️ write | Edit an existing receipt. |
| 36 | `void_receipt` | ✏️ write | Void a receipt. |
| 37 | `approve_receipt` | ✏️ write | Approve a receipt. |
| 38 | `create_receipt_ereceipt` | ✏️ write | Issue an e-Receipt for a receipt. |
| 39 | `create_receipt_etax` | ✏️ write | Issue an e-Tax Invoice for a receipt. |
| 40 | `enqueue_receipt` | ✏️ write | Submit a receipt to the async processing queue. |
| 41 | `get_receipt_queue` | 🔍 read | Poll the status of a queued receipt. |
| 42 | `set_receipt_queue_priority` | ✏️ write | Set queue priority for a receipt. |
| 43 | `attach_file_to_receipt` | 📎 file | Attach a file to a receipt. |
| 44 | `create_credit_note` | ✏️ write | Create a credit note. |
| 45 | `get_credit_note` | 🔍 read | Get a single credit note by id. |
| 46 | `list_credit_notes` | 🔍 read | List credit notes with optional paging/filter params. |
| 47 | `void_credit_note` | ✏️ write | Void a credit note. |
| 48 | `create_credit_note_ereceipt` | ✏️ write | Issue an e-Receipt for a credit note. |
| 49 | `create_credit_note_etax` | ✏️ write | Issue an e-Tax Invoice for a credit note. |
| 50 | `enqueue_credit_note` | ✏️ write | Submit a credit note to the async processing queue. |
| 51 | `get_credit_note_queue` | 🔍 read | Poll the status of a queued credit note. |
| 52 | `attach_file_to_credit_note` | 📎 file | Attach a file to a credit note. |
| 53 | `create_billing_note` | ✏️ write | Create a billing note. |
| 54 | `get_billing_note` | 🔍 read | Get a single billing note by id. |
| 55 | `list_billing_notes` | 🔍 read | List billing notes with optional paging/filter params. |
| 56 | `record_billing_note_payment` | ✏️ write | Record a payment against a billing note. |
| 57 | `void_billing_note_payment` | ✏️ write | Void a recorded billing note payment. |
| 58 | `void_billing_note` | ✏️ write | Void a billing note. |
| 59 | `enqueue_billing_note` | ✏️ write | Submit a billing note to the async processing queue. |
| 60 | `get_billing_note_queue` | 🔍 read | Poll the status of a queued billing note. |
| 61 | `attach_file_to_billing_note` | 📎 file | Attach a file to a billing note. |

### Purchases / Expense — Purchase Orders, Expenses, Credit/Billing Note Expenses (43)

| # | Tool | Type | Description |
|---|---|---|---|
| 1 | `create_purchase_order` | ✏️ write | Create a purchase order (draft). |
| 2 | `create_purchase_order_allinone` | ✏️ write | Create + approve a purchase order in one call. |
| 3 | `get_purchase_order` | 🔍 read | Get a single purchase order by id. |
| 4 | `list_purchase_orders` | 🔍 read | List purchase orders with optional paging/filter params. |
| 5 | `update_purchase_order` | ✏️ write | Edit an existing purchase order. |
| 6 | `void_purchase_order` | ✏️ write | Void a purchase order. |
| 7 | `approve_purchase_order` | ✏️ write | Approve a purchase order. |
| 8 | `enqueue_purchase_order` | ✏️ write | Submit a purchase order to the async processing queue. |
| 9 | `get_purchase_order_queue` | 🔍 read | Poll the status of a queued purchase order. |
| 10 | `set_purchase_order_queue_priority` | ✏️ write | Set queue priority for a purchase order. |
| 11 | `attach_file_to_purchase_order` | 📎 file | Attach a file to a purchase order. |
| 12 | `create_expense` | ✏️ write | Create an expense (draft). |
| 13 | `create_expense_allinone` | ✏️ write | Create + approve an expense in one call. |
| 14 | `create_expense_by_purchase_order` | ✏️ write | Create an expense from a purchase order. |
| 15 | `get_expense` | 🔍 read | Get a single expense by id. |
| 16 | `list_expenses` | 🔍 read | List expenses with optional paging/filter params. |
| 17 | `list_expenses_by_contact` | 🔍 read | List expenses for a specific contact. |
| 18 | `update_expense` | ✏️ write | Edit an existing expense. |
| 19 | `void_expense` | ✏️ write | Void an expense. |
| 20 | `approve_expense` | ✏️ write | Approve an expense. |
| 21 | `record_expense_payment` | ✏️ write | Record a payment against an expense. |
| 22 | `void_expense_payment` | ✏️ write | Void a recorded expense payment. |
| 23 | `create_expense_received_tax_invoice` | ✏️ write | Create a received tax invoice for an expense. |
| 24 | `enqueue_expense` | ✏️ write | Submit an expense to the async processing queue. |
| 25 | `get_expense_queue` | 🔍 read | Poll the status of a queued expense. |
| 26 | `set_expense_queue_priority` | ✏️ write | Set queue priority for an expense. |
| 27 | `attach_file_to_expense` | 📎 file | Attach a file to an expense. |
| 28 | `create_credit_note_expense` | ✏️ write | Create a credit note expense. |
| 29 | `get_credit_note_expense` | 🔍 read | Get a single credit note expense by id. |
| 30 | `list_credit_note_expenses` | 🔍 read | List credit note expenses. |
| 31 | `void_credit_note_expense` | ✏️ write | Void a credit note expense. |
| 32 | `enqueue_credit_note_expense` | ✏️ write | Submit a credit note expense to the queue. |
| 33 | `get_credit_note_expense_queue` | 🔍 read | Poll the status of a queued credit note expense. |
| 34 | `attach_file_to_credit_note_expense` | 📎 file | Attach a file to a credit note expense. |
| 35 | `create_billing_note_expense` | ✏️ write | Create a billing note expense. |
| 36 | `get_billing_note_expense` | 🔍 read | Get a single billing note expense by id. |
| 37 | `list_billing_note_expenses` | 🔍 read | List billing note expenses. |
| 38 | `record_billing_note_expense_payment` | ✏️ write | Record a payment against a billing note expense. |
| 39 | `void_billing_note_expense_payment` | ✏️ write | Void a recorded billing note expense payment. |
| 40 | `void_billing_note_expense` | ✏️ write | Void a billing note expense. |
| 41 | `enqueue_billing_note_expense` | ✏️ write | Submit a billing note expense to the queue. |
| 42 | `get_billing_note_expense_queue` | 🔍 read | Poll the status of a queued billing note expense. |
| 43 | `attach_file_to_billing_note_expense` | 📎 file | Attach a file to a billing note expense. |

### Accounting & Misc — Daily Journals, Tags, Classifications, File Vault (13)

| # | Tool | Type | Description |
|---|---|---|---|
| 1 | `create_daily_journal` | ✏️ write | Create a daily journal entry. |
| 2 | `get_daily_journal` | 🔍 read | Get a single daily journal by id. |
| 3 | `void_daily_journal` | ✏️ write | Void a daily journal entry. |
| 4 | `get_account_codes` | 🔍 read | Get the chart of account codes. |
| 5 | `enqueue_daily_journal` | ✏️ write | Submit a daily journal to the async processing queue. |
| 6 | `insert_tag` | ✏️ write | Attach a tag to a record. |
| 7 | `remove_tag` | ✏️ write | Remove a tag from a record. |
| 8 | `list_classification_groups` | 🔍 read | Get classification groups. |
| 9 | `get_transaction_classification` | 🔍 read | Get classifications for a transaction. |
| 10 | `create_transaction_classification` | ✏️ write | Create a transaction classification. |
| 11 | `remove_transaction_classification` | ✏️ write | Void/remove a transaction classification. |
| 12 | `create_invitation` | ✏️ write | Create an invitation. |
| 13 | `upload_file_vault` | 📎 file | Upload a file to the PEAK File Vault. |

---

## Notes / open items

- The passthrough `data`/`params` design keeps the server correct across all endpoints. As per-endpoint schemas are confirmed against UAT, individual tools can be given stricter typed inputs.
- `Time-Signature` encoding (base64 vs hex) and secret source (connectId vs connectKey) are configurable — confirm the working combination on your account via `npm run auth-check`.
- Numeric enums (`type`, `taxStatus`, `vatType`, `bankId`, `prefixNameType`, …) — see the [numeric reference](https://developers.peakaccount.com/reference/contact-numeric).

## License

MIT