@centry-digital/bukku-mcp 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/build/client/bukku-client.d.ts +62 -0
- package/build/client/bukku-client.js +195 -0
- package/build/config/env.d.ts +19 -0
- package/build/config/env.js +36 -0
- package/build/errors/transform.d.ts +14 -0
- package/build/errors/transform.js +141 -0
- package/build/errors/transform.test.d.ts +1 -0
- package/build/errors/transform.test.js +101 -0
- package/build/index.d.ts +13 -0
- package/build/index.js +52 -0
- package/build/tools/cache/reference-cache.d.ts +42 -0
- package/build/tools/cache/reference-cache.js +63 -0
- package/build/tools/configs/account.d.ts +17 -0
- package/build/tools/configs/account.js +28 -0
- package/build/tools/configs/bank-money-in.d.ts +10 -0
- package/build/tools/configs/bank-money-in.js +22 -0
- package/build/tools/configs/bank-money-out.d.ts +10 -0
- package/build/tools/configs/bank-money-out.js +22 -0
- package/build/tools/configs/bank-transfer.d.ts +11 -0
- package/build/tools/configs/bank-transfer.js +23 -0
- package/build/tools/configs/contact-group.d.ts +11 -0
- package/build/tools/configs/contact-group.js +19 -0
- package/build/tools/configs/contact.d.ts +14 -0
- package/build/tools/configs/contact.js +25 -0
- package/build/tools/configs/delivery-order.d.ts +8 -0
- package/build/tools/configs/delivery-order.js +20 -0
- package/build/tools/configs/file.d.ts +18 -0
- package/build/tools/configs/file.js +26 -0
- package/build/tools/configs/goods-received-note.d.ts +8 -0
- package/build/tools/configs/goods-received-note.js +20 -0
- package/build/tools/configs/journal-entry.d.ts +14 -0
- package/build/tools/configs/journal-entry.js +26 -0
- package/build/tools/configs/location.d.ts +20 -0
- package/build/tools/configs/location.js +28 -0
- package/build/tools/configs/product-bundle.d.ts +18 -0
- package/build/tools/configs/product-bundle.js +29 -0
- package/build/tools/configs/product-group.d.ts +14 -0
- package/build/tools/configs/product-group.js +22 -0
- package/build/tools/configs/product.d.ts +24 -0
- package/build/tools/configs/product.js +35 -0
- package/build/tools/configs/purchase-bill.d.ts +9 -0
- package/build/tools/configs/purchase-bill.js +21 -0
- package/build/tools/configs/purchase-credit-note.d.ts +8 -0
- package/build/tools/configs/purchase-credit-note.js +20 -0
- package/build/tools/configs/purchase-order.d.ts +8 -0
- package/build/tools/configs/purchase-order.js +20 -0
- package/build/tools/configs/purchase-payment.d.ts +8 -0
- package/build/tools/configs/purchase-payment.js +20 -0
- package/build/tools/configs/purchase-refund.d.ts +8 -0
- package/build/tools/configs/purchase-refund.js +20 -0
- package/build/tools/configs/sales-credit-note.d.ts +8 -0
- package/build/tools/configs/sales-credit-note.js +20 -0
- package/build/tools/configs/sales-invoice.d.ts +8 -0
- package/build/tools/configs/sales-invoice.js +20 -0
- package/build/tools/configs/sales-order.d.ts +8 -0
- package/build/tools/configs/sales-order.js +20 -0
- package/build/tools/configs/sales-payment.d.ts +8 -0
- package/build/tools/configs/sales-payment.js +20 -0
- package/build/tools/configs/sales-quote.d.ts +8 -0
- package/build/tools/configs/sales-quote.js +20 -0
- package/build/tools/configs/sales-refund.d.ts +8 -0
- package/build/tools/configs/sales-refund.js +20 -0
- package/build/tools/configs/tag-group.d.ts +11 -0
- package/build/tools/configs/tag-group.js +22 -0
- package/build/tools/configs/tag.d.ts +11 -0
- package/build/tools/configs/tag.js +22 -0
- package/build/tools/custom/account-tools.d.ts +21 -0
- package/build/tools/custom/account-tools.js +119 -0
- package/build/tools/custom/contact-archive.d.ts +17 -0
- package/build/tools/custom/contact-archive.js +72 -0
- package/build/tools/custom/control-panel-archive.d.ts +17 -0
- package/build/tools/custom/control-panel-archive.js +72 -0
- package/build/tools/custom/file-upload.d.ts +17 -0
- package/build/tools/custom/file-upload.js +43 -0
- package/build/tools/custom/journal-entry-tools.d.ts +20 -0
- package/build/tools/custom/journal-entry-tools.js +109 -0
- package/build/tools/custom/location-tools.d.ts +20 -0
- package/build/tools/custom/location-tools.js +96 -0
- package/build/tools/custom/product-archive.d.ts +17 -0
- package/build/tools/custom/product-archive.js +124 -0
- package/build/tools/custom/reference-data.d.ts +21 -0
- package/build/tools/custom/reference-data.js +108 -0
- package/build/tools/factory.d.ts +26 -0
- package/build/tools/factory.js +224 -0
- package/build/tools/registry.d.ts +22 -0
- package/build/tools/registry.js +140 -0
- package/build/tools/validation/double-entry.d.ts +46 -0
- package/build/tools/validation/double-entry.js +66 -0
- package/build/types/api-responses.d.ts +21 -0
- package/build/types/api-responses.js +6 -0
- package/build/types/bukku.d.ts +93 -0
- package/build/types/bukku.js +11 -0
- package/build/utils/logger.d.ts +6 -0
- package/build/utils/logger.js +8 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Centry Digital Sdn. Bhd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Bukku MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@centry-digital/bukku-mcp)
|
|
4
|
+
|
|
5
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that connects AI assistants to [Bukku](https://bukku.my), a Malaysian accounting platform. This gives your AI the ability to read, create, and manage your accounting data — invoices, bills, payments, contacts, products, and more.
|
|
6
|
+
|
|
7
|
+
## What can it do?
|
|
8
|
+
|
|
9
|
+
With this MCP server connected, you can ask your AI things like:
|
|
10
|
+
|
|
11
|
+
- "List my unpaid sales invoices"
|
|
12
|
+
- "Create an invoice for RM 5,000 to Acme Corp for consulting services"
|
|
13
|
+
- "Show me all purchase bills from last month"
|
|
14
|
+
- "Record a bank transfer of RM 10,000 from Maybank to CIMB"
|
|
15
|
+
- "Create a new contact for my supplier"
|
|
16
|
+
- "Upload this receipt and attach it to the purchase bill"
|
|
17
|
+
|
|
18
|
+
The server exposes **173 tools** covering the full Bukku API:
|
|
19
|
+
|
|
20
|
+
| Category | Tools | What you can do |
|
|
21
|
+
|----------|-------|-----------------|
|
|
22
|
+
| **Sales** | 42 | Quotes, orders, delivery orders, invoices, credit notes, payments, refunds |
|
|
23
|
+
| **Purchases** | 36 | Purchase orders, bills, credit notes, goods received notes, payments, refunds |
|
|
24
|
+
| **Banking** | 18 | Money in, money out, bank transfers |
|
|
25
|
+
| **Contacts** | 12 | Customers, suppliers, contact groups |
|
|
26
|
+
| **Products** | 18 | Products, product bundles, product groups |
|
|
27
|
+
| **Accounting** | 13 | Journal entries, chart of accounts |
|
|
28
|
+
| **Files** | 3 | Upload and manage file attachments |
|
|
29
|
+
| **Organisation** | 21 | Locations, tags, tag groups |
|
|
30
|
+
| **Reference Data** | 10 | Tax codes, currencies, payment methods, terms, and more |
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Get up and running in under 2 minutes.
|
|
35
|
+
|
|
36
|
+
### Prerequisites
|
|
37
|
+
|
|
38
|
+
- [Node.js](https://nodejs.org) v18 or later
|
|
39
|
+
- A [Bukku](https://bukku.my) account with API access enabled
|
|
40
|
+
- An AI client that supports MCP (e.g. [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.anthropic.com/en/docs/claude-code))
|
|
41
|
+
|
|
42
|
+
### Step 1: Get your Bukku API token
|
|
43
|
+
|
|
44
|
+
1. Log into your Bukku account
|
|
45
|
+
2. Go to **Control Panel > Integrations > API Access**
|
|
46
|
+
3. Generate a new API token (or copy your existing one)
|
|
47
|
+
4. Note your company subdomain — e.g. `mycompany` from `mycompany.bukku.my`
|
|
48
|
+
|
|
49
|
+
### Step 2: Add to your AI client
|
|
50
|
+
|
|
51
|
+
For Claude Desktop, open your config file:
|
|
52
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
53
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
54
|
+
|
|
55
|
+
Add this configuration:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"bukku": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": ["-y", "@centry-digital/bukku-mcp"],
|
|
63
|
+
"env": {
|
|
64
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
65
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Step 3: Restart your AI client
|
|
73
|
+
|
|
74
|
+
Quit and reopen Claude Desktop. That's it — you're ready to go!
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
### npx (recommended)
|
|
79
|
+
|
|
80
|
+
The quickest way to use the server is with `npx`. No installation needed — it downloads and runs the latest version automatically:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @centry-digital/bukku-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is what the Quick Start configuration uses. `npx` ensures you're always running the latest version without manual updates.
|
|
87
|
+
|
|
88
|
+
### npm global install
|
|
89
|
+
|
|
90
|
+
If you prefer a persistent installation:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install -g @centry-digital/bukku-mcp
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then update your AI client configuration to use the installed command instead:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"bukku": {
|
|
102
|
+
"command": "bukku-mcp",
|
|
103
|
+
"env": {
|
|
104
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
105
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Environment Variables
|
|
115
|
+
|
|
116
|
+
| Variable | Required | Description |
|
|
117
|
+
|----------|----------|-------------|
|
|
118
|
+
| `BUKKU_API_TOKEN` | Yes | Your Bukku API token from Control Panel > Integrations > API Access |
|
|
119
|
+
| `BUKKU_COMPANY_SUBDOMAIN` | Yes | Your company subdomain (e.g. `mycompany` from `mycompany.bukku.my`) |
|
|
120
|
+
|
|
121
|
+
### Claude Desktop
|
|
122
|
+
|
|
123
|
+
Open your configuration file:
|
|
124
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
125
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
126
|
+
|
|
127
|
+
**Using npx (recommended):**
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"bukku": {
|
|
133
|
+
"command": "npx",
|
|
134
|
+
"args": ["-y", "@centry-digital/bukku-mcp"],
|
|
135
|
+
"env": {
|
|
136
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
137
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**If you installed globally:**
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"mcpServers": {
|
|
149
|
+
"bukku": {
|
|
150
|
+
"command": "bukku-mcp",
|
|
151
|
+
"env": {
|
|
152
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
153
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
After updating the config, restart Claude Desktop.
|
|
161
|
+
|
|
162
|
+
### Claude Code
|
|
163
|
+
|
|
164
|
+
Add to `.claude/settings.json` in your home directory or project:
|
|
165
|
+
|
|
166
|
+
**Using npx (recommended):**
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"mcpServers": {
|
|
171
|
+
"bukku": {
|
|
172
|
+
"command": "npx",
|
|
173
|
+
"args": ["-y", "@centry-digital/bukku-mcp"],
|
|
174
|
+
"env": {
|
|
175
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
176
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**If you installed globally:**
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"mcpServers": {
|
|
188
|
+
"bukku": {
|
|
189
|
+
"command": "bukku-mcp",
|
|
190
|
+
"env": {
|
|
191
|
+
"BUKKU_API_TOKEN": "your-token-here",
|
|
192
|
+
"BUKKU_COMPANY_SUBDOMAIN": "your-subdomain"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Other MCP Clients
|
|
200
|
+
|
|
201
|
+
Any client that supports the [MCP stdio transport](https://modelcontextprotocol.io/docs/concepts/transports) can use this server. Use the `npx @centry-digital/bukku-mcp` command with the two environment variables shown above.
|
|
202
|
+
|
|
203
|
+
## Troubleshooting
|
|
204
|
+
|
|
205
|
+
**"Configuration Error" on startup**
|
|
206
|
+
- Check that both `BUKKU_API_TOKEN` and `BUKKU_COMPANY_SUBDOMAIN` are set in your client config
|
|
207
|
+
- Verify the environment variables are inside the `"env"` object
|
|
208
|
+
|
|
209
|
+
**"Token validation failed"**
|
|
210
|
+
- Your API token may be invalid or expired
|
|
211
|
+
- Log into Bukku and regenerate your token at Control Panel > Integrations > API Access
|
|
212
|
+
|
|
213
|
+
**Server doesn't appear in your AI client**
|
|
214
|
+
- Verify your configuration JSON syntax is correct (no trailing commas)
|
|
215
|
+
- Make sure you've restarted your AI client after editing the config
|
|
216
|
+
- Check that Node.js v18 or later is installed: `node --version`
|
|
217
|
+
|
|
218
|
+
**"Could not resolve package" with npx**
|
|
219
|
+
- Check that you have Node.js v18 or later installed
|
|
220
|
+
- Verify your network connection and proxy settings if applicable
|
|
221
|
+
- Try running `npm view @centry-digital/bukku-mcp` to confirm the package is accessible
|
|
222
|
+
|
|
223
|
+
**Permission errors with global install**
|
|
224
|
+
- Consider using `npx` instead (no installation needed)
|
|
225
|
+
- Or fix npm permissions: [https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
Want to contribute or run from source? Here's how to set up your development environment.
|
|
230
|
+
|
|
231
|
+
### Clone and build
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git clone https://github.com/centry-digital/bukku-mcp.git
|
|
235
|
+
cd bukku-mcp
|
|
236
|
+
npm install
|
|
237
|
+
npm run build
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Commands
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm run build # Compile TypeScript
|
|
244
|
+
npm test # Run tests
|
|
245
|
+
npm start # Start server (requires env vars)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Project structure
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
src/
|
|
252
|
+
├── client/ # Bukku API HTTP client
|
|
253
|
+
├── config/ # Environment validation
|
|
254
|
+
├── errors/ # Error handling and transformation
|
|
255
|
+
├── tools/ # MCP tool definitions (one folder per category)
|
|
256
|
+
│ ├── sales/
|
|
257
|
+
│ ├── purchases/
|
|
258
|
+
│ ├── banking/
|
|
259
|
+
│ ├── contacts/
|
|
260
|
+
│ ├── products/
|
|
261
|
+
│ ├── accounting/
|
|
262
|
+
│ ├── files/
|
|
263
|
+
│ └── ...
|
|
264
|
+
└── index.ts # Server entry point
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Env } from "../config/env.js";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for Bukku API.
|
|
4
|
+
* Handles authentication via Bearer token and Company-Subdomain header.
|
|
5
|
+
* Base URL: https://api.bukku.my
|
|
6
|
+
*/
|
|
7
|
+
export declare class BukkuClient {
|
|
8
|
+
private readonly baseUrl;
|
|
9
|
+
private readonly token;
|
|
10
|
+
private readonly subdomain;
|
|
11
|
+
constructor(env: Env);
|
|
12
|
+
/**
|
|
13
|
+
* Build headers for all requests.
|
|
14
|
+
* CRITICAL: Never log the actual token value - use "Bearer ***" for debugging.
|
|
15
|
+
*/
|
|
16
|
+
private getHeaders;
|
|
17
|
+
/**
|
|
18
|
+
* Map file extensions to MIME types for common file types.
|
|
19
|
+
* Returns null for unknown extensions.
|
|
20
|
+
*/
|
|
21
|
+
private getMimeType;
|
|
22
|
+
/**
|
|
23
|
+
* Build URL with query parameters.
|
|
24
|
+
*/
|
|
25
|
+
private buildUrl;
|
|
26
|
+
/**
|
|
27
|
+
* GET request with optional query parameters.
|
|
28
|
+
*/
|
|
29
|
+
get(path: string, params?: Record<string, string | number | undefined>): Promise<unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* POST request with JSON body.
|
|
32
|
+
*/
|
|
33
|
+
post(path: string, body: unknown): Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* PUT request with JSON body.
|
|
36
|
+
*/
|
|
37
|
+
put(path: string, body: unknown): Promise<unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* PATCH request with JSON body (for status updates).
|
|
40
|
+
*/
|
|
41
|
+
patch(path: string, body: unknown): Promise<unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* DELETE request.
|
|
44
|
+
*/
|
|
45
|
+
delete(path: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* POST multipart/form-data request for file uploads.
|
|
48
|
+
* Reads file from disk and sends as multipart form data.
|
|
49
|
+
* CRITICAL: Does NOT manually set Content-Type - fetch sets it automatically with boundary.
|
|
50
|
+
*
|
|
51
|
+
* @param path - API endpoint path
|
|
52
|
+
* @param filePath - Absolute path to file on disk
|
|
53
|
+
* @returns API response
|
|
54
|
+
*/
|
|
55
|
+
postMultipart(path: string, filePath: string): Promise<unknown>;
|
|
56
|
+
/**
|
|
57
|
+
* Validate token on startup by making a lightweight API call.
|
|
58
|
+
* Uses GET /contacts with page_size=1 to verify authentication.
|
|
59
|
+
* Exits process if token is invalid (401).
|
|
60
|
+
*/
|
|
61
|
+
validateToken(): Promise<void>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { log } from "../utils/logger.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* HTTP client for Bukku API.
|
|
6
|
+
* Handles authentication via Bearer token and Company-Subdomain header.
|
|
7
|
+
* Base URL: https://api.bukku.my
|
|
8
|
+
*/
|
|
9
|
+
export class BukkuClient {
|
|
10
|
+
baseUrl = "https://api.bukku.my";
|
|
11
|
+
token;
|
|
12
|
+
subdomain;
|
|
13
|
+
constructor(env) {
|
|
14
|
+
this.token = env.BUKKU_API_TOKEN;
|
|
15
|
+
this.subdomain = env.BUKKU_COMPANY_SUBDOMAIN;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build headers for all requests.
|
|
19
|
+
* CRITICAL: Never log the actual token value - use "Bearer ***" for debugging.
|
|
20
|
+
*/
|
|
21
|
+
getHeaders(includeContentType = false) {
|
|
22
|
+
const headers = {
|
|
23
|
+
Authorization: `Bearer ${this.token}`,
|
|
24
|
+
"Company-Subdomain": this.subdomain,
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
};
|
|
27
|
+
if (includeContentType) {
|
|
28
|
+
headers["Content-Type"] = "application/json";
|
|
29
|
+
}
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Map file extensions to MIME types for common file types.
|
|
34
|
+
* Returns null for unknown extensions.
|
|
35
|
+
*/
|
|
36
|
+
getMimeType(extension) {
|
|
37
|
+
const mimeMap = {
|
|
38
|
+
".pdf": "application/pdf",
|
|
39
|
+
".png": "image/png",
|
|
40
|
+
".jpg": "image/jpeg",
|
|
41
|
+
".jpeg": "image/jpeg",
|
|
42
|
+
".gif": "image/gif",
|
|
43
|
+
".txt": "text/plain",
|
|
44
|
+
".csv": "text/csv",
|
|
45
|
+
".json": "application/json",
|
|
46
|
+
".xml": "application/xml",
|
|
47
|
+
".zip": "application/zip",
|
|
48
|
+
};
|
|
49
|
+
return mimeMap[extension.toLowerCase()] || null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build URL with query parameters.
|
|
53
|
+
*/
|
|
54
|
+
buildUrl(path, params) {
|
|
55
|
+
const url = new URL(path, this.baseUrl);
|
|
56
|
+
if (params) {
|
|
57
|
+
for (const [key, value] of Object.entries(params)) {
|
|
58
|
+
if (value !== undefined) {
|
|
59
|
+
url.searchParams.append(key, String(value));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* GET request with optional query parameters.
|
|
67
|
+
*/
|
|
68
|
+
async get(path, params) {
|
|
69
|
+
const url = this.buildUrl(path, params);
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
method: "GET",
|
|
72
|
+
headers: this.getHeaders(),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw response;
|
|
76
|
+
}
|
|
77
|
+
return response.json();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* POST request with JSON body.
|
|
81
|
+
*/
|
|
82
|
+
async post(path, body) {
|
|
83
|
+
const url = this.buildUrl(path);
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: this.getHeaders(true),
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw response;
|
|
91
|
+
}
|
|
92
|
+
return response.json();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* PUT request with JSON body.
|
|
96
|
+
*/
|
|
97
|
+
async put(path, body) {
|
|
98
|
+
const url = this.buildUrl(path);
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: this.getHeaders(true),
|
|
102
|
+
body: JSON.stringify(body),
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw response;
|
|
106
|
+
}
|
|
107
|
+
return response.json();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* PATCH request with JSON body (for status updates).
|
|
111
|
+
*/
|
|
112
|
+
async patch(path, body) {
|
|
113
|
+
const url = this.buildUrl(path);
|
|
114
|
+
const response = await fetch(url, {
|
|
115
|
+
method: "PATCH",
|
|
116
|
+
headers: this.getHeaders(true),
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw response;
|
|
121
|
+
}
|
|
122
|
+
return response.json();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* DELETE request.
|
|
126
|
+
*/
|
|
127
|
+
async delete(path) {
|
|
128
|
+
const url = this.buildUrl(path);
|
|
129
|
+
const response = await fetch(url, {
|
|
130
|
+
method: "DELETE",
|
|
131
|
+
headers: this.getHeaders(),
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw response;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* POST multipart/form-data request for file uploads.
|
|
139
|
+
* Reads file from disk and sends as multipart form data.
|
|
140
|
+
* CRITICAL: Does NOT manually set Content-Type - fetch sets it automatically with boundary.
|
|
141
|
+
*
|
|
142
|
+
* @param path - API endpoint path
|
|
143
|
+
* @param filePath - Absolute path to file on disk
|
|
144
|
+
* @returns API response
|
|
145
|
+
*/
|
|
146
|
+
async postMultipart(path, filePath) {
|
|
147
|
+
const url = this.buildUrl(path);
|
|
148
|
+
// Read file from disk
|
|
149
|
+
const fileBuffer = await readFile(filePath);
|
|
150
|
+
const fileName = basename(filePath);
|
|
151
|
+
const fileExtension = extname(filePath);
|
|
152
|
+
// Determine MIME type, fallback to generic binary
|
|
153
|
+
const mimeType = this.getMimeType(fileExtension) || "application/octet-stream";
|
|
154
|
+
// Create File object and FormData
|
|
155
|
+
const file = new File([fileBuffer], fileName, { type: mimeType });
|
|
156
|
+
const form = new FormData();
|
|
157
|
+
form.append("file", file);
|
|
158
|
+
// Get auth headers WITHOUT Content-Type (fetch sets it with boundary)
|
|
159
|
+
const headers = this.getHeaders(false);
|
|
160
|
+
const response = await fetch(url, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers,
|
|
163
|
+
body: form,
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw response;
|
|
167
|
+
}
|
|
168
|
+
return response.json();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Validate token on startup by making a lightweight API call.
|
|
172
|
+
* Uses GET /contacts with page_size=1 to verify authentication.
|
|
173
|
+
* Exits process if token is invalid (401).
|
|
174
|
+
*/
|
|
175
|
+
async validateToken() {
|
|
176
|
+
try {
|
|
177
|
+
await this.get("/contacts", { page_size: 1 });
|
|
178
|
+
log("Token validated successfully");
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof Response && error.status === 401) {
|
|
182
|
+
log("Authentication Error\n");
|
|
183
|
+
log("The provided BUKKU_API_TOKEN is invalid or expired.\n");
|
|
184
|
+
log("Please check:");
|
|
185
|
+
log(" 1. Token is copied correctly (no extra spaces)");
|
|
186
|
+
log(" 2. API Access is enabled in Bukku Control Panel -> Integrations");
|
|
187
|
+
log(" 3. Token has not been revoked or regenerated\n");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
// For other errors, log and exit
|
|
191
|
+
log("Failed to validate token:", error);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Environment variable schema.
|
|
4
|
+
* Required vars:
|
|
5
|
+
* - BUKKU_API_TOKEN: Bearer token from Bukku API settings
|
|
6
|
+
* - BUKKU_COMPANY_SUBDOMAIN: Company subdomain (e.g., 'mycompany' from mycompany.bukku.my)
|
|
7
|
+
*/
|
|
8
|
+
declare const envSchema: z.ZodObject<{
|
|
9
|
+
BUKKU_API_TOKEN: z.ZodString;
|
|
10
|
+
BUKKU_COMPANY_SUBDOMAIN: z.ZodString;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type Env = z.infer<typeof envSchema>;
|
|
13
|
+
/**
|
|
14
|
+
* Validates environment variables on startup.
|
|
15
|
+
* Exits process with code 1 if validation fails.
|
|
16
|
+
* Prints setup checklist to stderr on failure.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateEnv(): Env;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Environment variable schema.
|
|
5
|
+
* Required vars:
|
|
6
|
+
* - BUKKU_API_TOKEN: Bearer token from Bukku API settings
|
|
7
|
+
* - BUKKU_COMPANY_SUBDOMAIN: Company subdomain (e.g., 'mycompany' from mycompany.bukku.my)
|
|
8
|
+
*/
|
|
9
|
+
const envSchema = z.object({
|
|
10
|
+
BUKKU_API_TOKEN: z.string().min(1, "BUKKU_API_TOKEN is required"),
|
|
11
|
+
BUKKU_COMPANY_SUBDOMAIN: z.string().min(1, "BUKKU_COMPANY_SUBDOMAIN is required"),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Validates environment variables on startup.
|
|
15
|
+
* Exits process with code 1 if validation fails.
|
|
16
|
+
* Prints setup checklist to stderr on failure.
|
|
17
|
+
*/
|
|
18
|
+
export function validateEnv() {
|
|
19
|
+
const result = envSchema.safeParse(process.env);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
log("Configuration Error\n");
|
|
22
|
+
log("Missing required environment variables:");
|
|
23
|
+
for (const issue of result.error.issues) {
|
|
24
|
+
log(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
25
|
+
}
|
|
26
|
+
log("\nSetup checklist:");
|
|
27
|
+
log(" 1. Go to Bukku web app -> Control Panel -> Integrations");
|
|
28
|
+
log(" 2. Turn on API Access and copy the Access Token");
|
|
29
|
+
log(" 3. Set BUKKU_API_TOKEN=<your-token>");
|
|
30
|
+
log(" 4. Set BUKKU_COMPANY_SUBDOMAIN=<your-subdomain>");
|
|
31
|
+
log(" (e.g., 'mycompany' from mycompany.bukku.my)");
|
|
32
|
+
log(" 5. Restart Claude Desktop\n");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return result.data;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP to MCP Error Transformation
|
|
3
|
+
* Converts HTTP error responses into conversational MCP error messages
|
|
4
|
+
*/
|
|
5
|
+
export interface MCPErrorResponse {
|
|
6
|
+
isError: true;
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function transformHttpError(status: number | null, body: unknown, operation: string): MCPErrorResponse;
|
|
14
|
+
export declare function transformNetworkError(error: unknown, operation: string): MCPErrorResponse;
|