bimp-mcp 0.3.1 → 0.4.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/README.md +41 -22
- package/dist/client.js +3 -3
- package/dist/index.js +57 -5
- package/dist/nomenclatures-extended.d.ts +6 -0
- package/dist/nomenclatures-extended.js +93 -4
- package/dist/utilities.d.ts +1 -0
- package/dist/utilities.js +152 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/header.png" alt="HEYLOVE x BIMP" width="480" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">BIMP MCP</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
MCP server for <a href="https://bimpsoft.com">BIMP ERP</a> — a Ukrainian cloud ERP for small and medium businesses
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/dutchakdev/bimp-mcp/actions/workflows/ci.yml"><img src="https://github.com/dutchakdev/bimp-mcp/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
13
|
+
<a href="https://github.com/dutchakdev/bimp-mcp/actions/workflows/release.yml"><img src="https://github.com/dutchakdev/bimp-mcp/actions/workflows/release.yml/badge.svg" alt="Release" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/bimp-mcp"><img src="https://img.shields.io/npm/v/bimp-mcp" alt="npm" /></a>
|
|
15
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/bimp-mcp" alt="Node.js" /></a>
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
Enable LLMs to interact with BIMP data through the <a href="https://modelcontextprotocol.io">Model Context Protocol</a>:<br/>
|
|
21
|
+
read, create, update entities, perform bulk operations, and analyze business data.
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
6
25
|
|
|
7
26
|
## Features
|
|
8
27
|
|
|
9
|
-
- **~135 auto-generated tools** from OpenAPI 3.1 spec
|
|
28
|
+
- **~135 auto-generated tools** from OpenAPI 3.1 spec — edit `bimp-api.json`, restart, done
|
|
10
29
|
- **3 utility tools** for bulk operations: `bimp_fetch_all`, `bimp_batch_read`, `bimp_bulk_update`
|
|
11
|
-
- **6 MCP prompts**
|
|
12
|
-
- **Auto-authentication**
|
|
30
|
+
- **6 MCP prompts** with ERP domain context, workflow guides, and data analysis patterns
|
|
31
|
+
- **Auto-authentication** — login on first call, transparent token refresh on 401
|
|
13
32
|
|
|
14
33
|
## Quick Start
|
|
15
34
|
|
|
16
35
|
### Prerequisites
|
|
17
36
|
|
|
18
37
|
- Node.js 20+
|
|
19
|
-
- A BIMP ERP account with API access
|
|
38
|
+
- A [BIMP ERP](https://bimpsoft.com) account with API access
|
|
20
39
|
|
|
21
|
-
###
|
|
40
|
+
### 1. Configure environment
|
|
22
41
|
|
|
23
42
|
Create a `.env` file (see `.env.example`):
|
|
24
43
|
|
|
@@ -29,22 +48,22 @@ BIMP_PASSWORD=your-password
|
|
|
29
48
|
BIMP_COMPANY_CODE=000001398
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
### Install and
|
|
51
|
+
### 2. Install and run
|
|
33
52
|
|
|
34
53
|
```bash
|
|
35
54
|
npm install
|
|
36
55
|
npm start
|
|
37
56
|
```
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
For development with auto-reload:
|
|
40
59
|
|
|
41
60
|
```bash
|
|
42
61
|
npm run dev
|
|
43
62
|
```
|
|
44
63
|
|
|
45
|
-
### MCP
|
|
64
|
+
### 3. Connect your MCP client
|
|
46
65
|
|
|
47
|
-
Add to
|
|
66
|
+
Add to `claude_desktop_config.json` (Claude Desktop) or equivalent:
|
|
48
67
|
|
|
49
68
|
```json
|
|
50
69
|
{
|
|
@@ -63,14 +82,14 @@ Add to your `claude_desktop_config.json` (Claude Desktop) or equivalent MCP clie
|
|
|
63
82
|
}
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
##
|
|
85
|
+
## Tools
|
|
67
86
|
|
|
68
|
-
### Auto-
|
|
87
|
+
### Auto-generated (~135 tools)
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
Generated from `bimp-api.json` at startup. Naming: `bimp_{entity}_{action}`.
|
|
71
90
|
|
|
72
91
|
| Domain | Examples |
|
|
73
|
-
|
|
92
|
+
|---|---|
|
|
74
93
|
| **Sales** | `bimp_salesInvoice_readList`, `bimp_salesInvoice_create`, `bimp_invoiceForCustomerPayment_readList` |
|
|
75
94
|
| **Inventory** | `bimp_nomenclature_readList`, `bimp_inventory_readList_cursor`, `bimp_movementOfInventories_create` |
|
|
76
95
|
| **Finance** | `bimp_customerPayment_readList`, `bimp_supplierPayment_create`, `bimp_cashBox_readList` |
|
|
@@ -79,20 +98,20 @@ Tools are generated from `bimp-api.json` at startup. Naming convention: `bimp_{e
|
|
|
79
98
|
| **Reference Data** | `bimp_counterparty_readList`, `bimp_employee_readList`, `bimp_warehouse_readList` |
|
|
80
99
|
| **Auth** | `bimp_auth_listCompanies`, `bimp_auth_switchCompany` |
|
|
81
100
|
|
|
82
|
-
### Utility
|
|
101
|
+
### Utility tools
|
|
83
102
|
|
|
84
103
|
| Tool | Purpose |
|
|
85
|
-
|
|
86
|
-
| `bimp_fetch_all` | Auto-paginate any readList
|
|
104
|
+
|---|---|
|
|
105
|
+
| `bimp_fetch_all` | Auto-paginate any readList. Supports offset/count, cursor, page/pageSize. Optional `enrich` mode fetches full details per record. |
|
|
87
106
|
| `bimp_batch_read` | Parallel read of full details for an array of UUIDs with configurable concurrency. |
|
|
88
107
|
| `bimp_bulk_update` | Mass update records with batched concurrency and per-item error reporting. |
|
|
89
108
|
|
|
90
|
-
##
|
|
109
|
+
## Prompts
|
|
91
110
|
|
|
92
111
|
| Prompt | Description |
|
|
93
|
-
|
|
112
|
+
|---|---|
|
|
94
113
|
| `bimp_erp_context` | Entity structure, relationships, Ukrainian terminology mapping |
|
|
95
|
-
| `bimp_data_analysis` |
|
|
114
|
+
| `bimp_data_analysis` | Data analysis patterns, pagination quirks, enrichment strategies |
|
|
96
115
|
| `bimp_bulk_operations` | Mass operation patterns: price updates, bulk edits, imports |
|
|
97
116
|
| `bimp_sales_workflow` | Sales process: order, realization, payment, returns |
|
|
98
117
|
| `bimp_production_workflow` | Production: specification, order, assembly, material write-offs |
|
package/dist/client.js
CHANGED
|
@@ -17,7 +17,7 @@ export class BimpClient {
|
|
|
17
17
|
async request(method, path, params = {}, options) {
|
|
18
18
|
await this.ensureAuthenticated();
|
|
19
19
|
const result = await this.executeRequest(method, path, params, options?.timeout);
|
|
20
|
-
if (result.status === 401) {
|
|
20
|
+
if (result.status === 401 || result.status === 498) {
|
|
21
21
|
await this.refreshAuth();
|
|
22
22
|
const retry = await this.executeRequest(method, path, params, options?.timeout);
|
|
23
23
|
if (!retry.ok) {
|
|
@@ -110,8 +110,8 @@ export class BimpClient {
|
|
|
110
110
|
}
|
|
111
111
|
async executeRequest(method, pathTemplate, params, timeout) {
|
|
112
112
|
const resp = await this.rawFetch(method, pathTemplate, params, { "access-token": this.tokens.companyAccessToken }, timeout);
|
|
113
|
-
if (resp.status === 401) {
|
|
114
|
-
return { ok: false, status:
|
|
113
|
+
if (resp.status === 401 || resp.status === 498) {
|
|
114
|
+
return { ok: false, status: resp.status, data: null };
|
|
115
115
|
}
|
|
116
116
|
const json = (await resp.json());
|
|
117
117
|
return { ok: resp.ok, status: resp.status, data: json };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,58 @@ import { createUtilityTools } from "./utilities.js";
|
|
|
12
12
|
import { createNomenclaturesTools } from "./nomenclatures-extended.js";
|
|
13
13
|
import { PROMPT_TEXTS } from "./prompts.js";
|
|
14
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
/**
|
|
16
|
+
* Maximum response size in characters.
|
|
17
|
+
* MCP clients (Claude Code, Claude Desktop) have tool result size limits.
|
|
18
|
+
* We truncate large responses to stay within safe bounds.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_RESPONSE_SIZE = 80_000;
|
|
21
|
+
/**
|
|
22
|
+
* Truncate a tool result if its JSON representation exceeds MAX_RESPONSE_SIZE.
|
|
23
|
+
* For array-based results (items/data), uses binary search to fit max items.
|
|
24
|
+
*/
|
|
25
|
+
function truncateResponse(result) {
|
|
26
|
+
const json = JSON.stringify(result, null, 2);
|
|
27
|
+
if (json.length <= MAX_RESPONSE_SIZE)
|
|
28
|
+
return json;
|
|
29
|
+
if (typeof result === "object" && result !== null) {
|
|
30
|
+
const obj = result;
|
|
31
|
+
const arrayKey = ["items", "data"].find((k) => Array.isArray(obj[k]));
|
|
32
|
+
if (arrayKey) {
|
|
33
|
+
const arr = obj[arrayKey];
|
|
34
|
+
const totalCount = arr.length;
|
|
35
|
+
// Binary search for max number of items that fit
|
|
36
|
+
let lo = 0;
|
|
37
|
+
let hi = arr.length;
|
|
38
|
+
while (lo < hi) {
|
|
39
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
40
|
+
const test = JSON.stringify({ ...obj, [arrayKey]: arr.slice(0, mid), count: mid, _truncated: { total: totalCount, returned: mid } }, null, 2);
|
|
41
|
+
if (test.length <= MAX_RESPONSE_SIZE) {
|
|
42
|
+
lo = mid;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
hi = mid - 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
...obj,
|
|
50
|
+
[arrayKey]: arr.slice(0, lo),
|
|
51
|
+
count: lo,
|
|
52
|
+
_truncated: {
|
|
53
|
+
total: totalCount,
|
|
54
|
+
returned: lo,
|
|
55
|
+
message: `Response truncated: showing ${lo} of ${totalCount} items. ` +
|
|
56
|
+
`To get all data, re-call with limit=${lo} and iterate using skip parameter: ` +
|
|
57
|
+
`skip=0 limit=${lo}, then skip=${lo} limit=${lo}, etc. ` +
|
|
58
|
+
`For bimp_fetch_all: recommended limit is 50 with enrich=true, 200 without.`,
|
|
59
|
+
},
|
|
60
|
+
}, null, 2);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Fallback: hard truncate for non-array responses
|
|
64
|
+
return (json.slice(0, MAX_RESPONSE_SIZE) +
|
|
65
|
+
"\n\n... [TRUNCATED: response exceeded size limit. Use limit parameter or filters to reduce result size.]");
|
|
66
|
+
}
|
|
15
67
|
const config = {
|
|
16
68
|
email: process.env.BIMP_EMAIL ?? "",
|
|
17
69
|
password: process.env.BIMP_PASSWORD ?? "",
|
|
@@ -38,7 +90,7 @@ for (const tool of generatedTools) {
|
|
|
38
90
|
}
|
|
39
91
|
const utilityTools = createUtilityTools(client, toolMap);
|
|
40
92
|
const nomenclaturesTools = createNomenclaturesTools(client);
|
|
41
|
-
const server = new McpServer({ name: "bimp-mcp", version: "0.
|
|
93
|
+
const server = new McpServer({ name: "bimp-mcp", version: "0.4.0" }, { capabilities: { logging: {} } });
|
|
42
94
|
// Register prompts via McpServer (uses Zod, type-safe)
|
|
43
95
|
for (const [name, prompt] of Object.entries(PROMPT_TEXTS)) {
|
|
44
96
|
server.registerPrompt(name, { description: prompt.description }, () => ({
|
|
@@ -175,7 +227,7 @@ lowLevelServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
175
227
|
const result = await utilityTool.handler(params);
|
|
176
228
|
return {
|
|
177
229
|
content: [
|
|
178
|
-
{ type: "text", text:
|
|
230
|
+
{ type: "text", text: truncateResponse(result) },
|
|
179
231
|
],
|
|
180
232
|
};
|
|
181
233
|
}
|
|
@@ -185,7 +237,7 @@ lowLevelServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
185
237
|
const result = await nomenclaturesTool.handler(params);
|
|
186
238
|
return {
|
|
187
239
|
content: [
|
|
188
|
-
{ type: "text", text:
|
|
240
|
+
{ type: "text", text: truncateResponse(result) },
|
|
189
241
|
],
|
|
190
242
|
};
|
|
191
243
|
}
|
|
@@ -206,7 +258,7 @@ lowLevelServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
206
258
|
const result = await client.request(toolDef.metadata.method, toolDef.metadata.path, callParams);
|
|
207
259
|
return {
|
|
208
260
|
content: [
|
|
209
|
-
{ type: "text", text:
|
|
261
|
+
{ type: "text", text: truncateResponse(result) },
|
|
210
262
|
],
|
|
211
263
|
};
|
|
212
264
|
}
|
|
@@ -221,7 +273,7 @@ lowLevelServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
221
273
|
const result = await client.request(toolDef.metadata.method, toolDef.metadata.path, params);
|
|
222
274
|
return {
|
|
223
275
|
content: [
|
|
224
|
-
{ type: "text", text:
|
|
276
|
+
{ type: "text", text: truncateResponse(result) },
|
|
225
277
|
],
|
|
226
278
|
};
|
|
227
279
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { BimpClient } from "./client.js";
|
|
2
2
|
import type { UtilityTool } from "./utilities.js";
|
|
3
3
|
export declare const FIELD_MAP: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Encode a UUID into BIMP's `Ссылка` format: base64 of the UUID bytes
|
|
6
|
+
* reordered using 1C "inside-out" layout (clock_seq + node + time_hi + time_mid + time_low).
|
|
7
|
+
* The server silently accepts payloads with missing/wrong Ссылка but kit composition breaks.
|
|
8
|
+
*/
|
|
9
|
+
export declare function uuidToReference(uuidStr: string): string;
|
|
4
10
|
export declare function toEnglish(obj: Record<string, unknown>): Record<string, unknown>;
|
|
5
11
|
export declare function toCyrillic(obj: Record<string, unknown>): Record<string, unknown>;
|
|
6
12
|
export declare function createNomenclaturesTools(client: BimpClient): UtilityTool[];
|
|
@@ -25,6 +25,29 @@ export const FIELD_MAP = {
|
|
|
25
25
|
inventoryAccountUuid: "СчетУчетаЗапасов.GUID",
|
|
26
26
|
docType: "ТипДокумента",
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Encode a UUID into BIMP's `Ссылка` format: base64 of the UUID bytes
|
|
30
|
+
* reordered using 1C "inside-out" layout (clock_seq + node + time_hi + time_mid + time_low).
|
|
31
|
+
* The server silently accepts payloads with missing/wrong Ссылка but kit composition breaks.
|
|
32
|
+
*/
|
|
33
|
+
export function uuidToReference(uuidStr) {
|
|
34
|
+
const hex = uuidStr.replace(/-/g, "").toLowerCase();
|
|
35
|
+
if (hex.length !== 32) {
|
|
36
|
+
throw new Error(`Invalid UUID: ${uuidStr}`);
|
|
37
|
+
}
|
|
38
|
+
const bytes = new Uint8Array(16);
|
|
39
|
+
for (let i = 0; i < 16; i++) {
|
|
40
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
41
|
+
}
|
|
42
|
+
// Inside-out: bytes[8..10] + bytes[10..16] + bytes[6..8] + bytes[4..6] + bytes[0..4]
|
|
43
|
+
const reordered = new Uint8Array(16);
|
|
44
|
+
reordered.set(bytes.subarray(8, 10), 0);
|
|
45
|
+
reordered.set(bytes.subarray(10, 16), 2);
|
|
46
|
+
reordered.set(bytes.subarray(6, 8), 8);
|
|
47
|
+
reordered.set(bytes.subarray(4, 6), 10);
|
|
48
|
+
reordered.set(bytes.subarray(0, 4), 12);
|
|
49
|
+
return Buffer.from(reordered).toString("base64");
|
|
50
|
+
}
|
|
28
51
|
const REVERSE_MAP = Object.fromEntries(Object.entries(FIELD_MAP).map(([en, cyrillic]) => [cyrillic, en]));
|
|
29
52
|
export function toEnglish(obj) {
|
|
30
53
|
const result = {};
|
|
@@ -63,14 +86,44 @@ function createNomenclaturesReadTool(client) {
|
|
|
63
86
|
},
|
|
64
87
|
};
|
|
65
88
|
}
|
|
89
|
+
async function buildKitCompositionItem(client, component) {
|
|
90
|
+
const read = (await client.request("POST", "/org2/nomenclatures/read", {
|
|
91
|
+
lang: "ru",
|
|
92
|
+
uid: component.nomenclatureUuid,
|
|
93
|
+
}));
|
|
94
|
+
const n = read.data ?? {};
|
|
95
|
+
return {
|
|
96
|
+
Номенклатура: {
|
|
97
|
+
Ссылка: uuidToReference(component.nomenclatureUuid),
|
|
98
|
+
GUID: component.nomenclatureUuid,
|
|
99
|
+
Наименование: n["Наименование"] ?? "",
|
|
100
|
+
Артикул: n["Артикул"] ?? "",
|
|
101
|
+
Код: n["Код"] ?? "",
|
|
102
|
+
ЕдиницаИзмерения: n["ЕдиницаИзмерения"] ?? null,
|
|
103
|
+
ЭтоНабор: false,
|
|
104
|
+
ЭтоУслуга: n["ЭтоУслуга"] ?? false,
|
|
105
|
+
ЭтоХарактеристика: false,
|
|
106
|
+
},
|
|
107
|
+
Характеристика: component.characteristicUuid
|
|
108
|
+
? { GUID: component.characteristicUuid }
|
|
109
|
+
: null,
|
|
110
|
+
Количество: component.quantity,
|
|
111
|
+
leftover: 0,
|
|
112
|
+
ОстатокНаСкладах: 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
66
115
|
function createNomenclaturesUpsertTool(client) {
|
|
67
116
|
return {
|
|
68
117
|
name: "bimp_nomenclatures_upsert",
|
|
69
|
-
description: "Create or update a product
|
|
118
|
+
description: "Create or update a product via /org2/nomenclatures/upsert (plural). " +
|
|
70
119
|
"For update: uuid is required. For create: uuid is optional. " +
|
|
71
|
-
"Supports
|
|
120
|
+
"Supports planning fields (minStock, maxStock, speedOfDemand, insuranceReserve, deliveryTerm), " +
|
|
121
|
+
"kit creation (isKit + kitComposition), and direct passthrough of Russian/1C fields. " +
|
|
122
|
+
"When kitComposition is provided, the tool fetches each component via /org2/nomenclatures/read " +
|
|
123
|
+
"to populate the Ссылка and other required fields.",
|
|
72
124
|
inputSchema: {
|
|
73
125
|
type: "object",
|
|
126
|
+
additionalProperties: true,
|
|
74
127
|
properties: {
|
|
75
128
|
uuid: { type: "string", description: "Product UUID (required for update, optional for create)" },
|
|
76
129
|
name: { type: "string", description: "Product name (required for create)" },
|
|
@@ -82,11 +135,47 @@ function createNomenclaturesUpsertTool(client) {
|
|
|
82
135
|
deliveryTerm: { type: "number", description: "Delivery time in days" },
|
|
83
136
|
unitOfMeasurementUuid: { type: "string", description: "Unit of measurement UUID" },
|
|
84
137
|
type: { type: "number", description: "Product type: 1=goods, 2=service" },
|
|
138
|
+
isKit: { type: "boolean", description: "Mark as kit (набір). Requires kitComposition when true." },
|
|
139
|
+
kitComposition: {
|
|
140
|
+
type: "array",
|
|
141
|
+
description: "Kit components — the tool resolves each UUID to a full 1C component block.",
|
|
142
|
+
items: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
nomenclatureUuid: { type: "string", description: "Component product UUID" },
|
|
146
|
+
quantity: { type: "number", description: "Quantity of this component in the kit" },
|
|
147
|
+
characteristicUuid: { type: "string", description: "Optional product characteristic UUID" },
|
|
148
|
+
},
|
|
149
|
+
required: ["nomenclatureUuid", "quantity"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
vatRateUuid: { type: "string", description: "VAT rate UUID (maps to СтавкаНДС.GUID)" },
|
|
153
|
+
expenseAccountUuid: { type: "string", description: "Expense account UUID (maps to СчетУчетаЗатрат.GUID)" },
|
|
154
|
+
inventoryAccountUuid: { type: "string", description: "Inventory account UUID" },
|
|
85
155
|
},
|
|
86
156
|
},
|
|
87
157
|
handler: async (params) => {
|
|
88
|
-
const { docType:
|
|
89
|
-
const body = toCyrillic(
|
|
158
|
+
const { docType: _docType, unitOfMeasurementUuid, expenseAccountUuid, inventoryAccountUuid, vatRateUuid, isKit, kitComposition, ...rest } = params;
|
|
159
|
+
const body = toCyrillic(rest);
|
|
160
|
+
if (unitOfMeasurementUuid) {
|
|
161
|
+
body["ЕдиницаИзмерения"] = { GUID: unitOfMeasurementUuid };
|
|
162
|
+
}
|
|
163
|
+
if (expenseAccountUuid) {
|
|
164
|
+
body["СчетУчетаЗатрат"] = { GUID: expenseAccountUuid };
|
|
165
|
+
}
|
|
166
|
+
if (inventoryAccountUuid) {
|
|
167
|
+
body["СчетУчетаЗапасов"] = { GUID: inventoryAccountUuid };
|
|
168
|
+
}
|
|
169
|
+
if (vatRateUuid) {
|
|
170
|
+
body["СтавкаНДС"] = { GUID: vatRateUuid };
|
|
171
|
+
}
|
|
172
|
+
if (isKit === true || Array.isArray(kitComposition)) {
|
|
173
|
+
body["ЭтоНабор"] = true;
|
|
174
|
+
if (Array.isArray(kitComposition)) {
|
|
175
|
+
const composition = await Promise.all(kitComposition.map((c) => buildKitCompositionItem(client, c)));
|
|
176
|
+
body["СоставНабора"] = composition;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
90
179
|
body["ТипДокумента"] = "101";
|
|
91
180
|
const response = (await client.request("POST", "/org2/nomenclatures/upsert", body));
|
|
92
181
|
return { uuid: response.data.GUID };
|
package/dist/utilities.d.ts
CHANGED
package/dist/utilities.js
CHANGED
|
@@ -44,12 +44,54 @@ async function batchReadUuids(client, toolDef, uuids, concurrency) {
|
|
|
44
44
|
}
|
|
45
45
|
return { items, errors };
|
|
46
46
|
}
|
|
47
|
+
// Filters the BIMP server ignores on /org2/nomenclature/api-readList (confirmed by
|
|
48
|
+
// integration testing). We paginate unfiltered and apply these locally. Other keys
|
|
49
|
+
// (nameContains, periodable, etc.) still flow through to the API.
|
|
50
|
+
const CLIENT_FILTER_KEYS = ["article", "articleContains", "isKit"];
|
|
51
|
+
function splitFilters(filters) {
|
|
52
|
+
const serverFilters = {};
|
|
53
|
+
const clientFilters = {};
|
|
54
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
55
|
+
if (CLIENT_FILTER_KEYS.includes(key)) {
|
|
56
|
+
clientFilters[key] = value;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
serverFilters[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { serverFilters, clientFilters };
|
|
63
|
+
}
|
|
64
|
+
function matchesClientFilters(item, filters) {
|
|
65
|
+
if (Object.keys(filters).length === 0)
|
|
66
|
+
return true;
|
|
67
|
+
const article = String(item.article ?? item["Артикул"] ?? "").toLowerCase();
|
|
68
|
+
const isKit = item.isKit ?? item["ЭтоНабор"];
|
|
69
|
+
if (typeof filters.article === "string") {
|
|
70
|
+
if (article !== filters.article.toLowerCase())
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (typeof filters.articleContains === "string") {
|
|
74
|
+
if (!article.includes(filters.articleContains.toLowerCase()))
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (typeof filters.isKit === "boolean") {
|
|
78
|
+
if (Boolean(isKit) !== filters.isKit)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
47
83
|
function createFetchAllTool(client, toolMap) {
|
|
48
84
|
return {
|
|
49
85
|
name: "bimp_fetch_all",
|
|
50
86
|
description: "Auto-paginate any readList endpoint to fetch all items. " +
|
|
51
87
|
"Supports offset, cursor, page, and none pagination types. " +
|
|
52
|
-
"Use enrich=true to call the corresponding read endpoint for full details on each item
|
|
88
|
+
"Use enrich=true to call the corresponding read endpoint for full details on each item.\n\n" +
|
|
89
|
+
"Client-side filters (applied AFTER pagination, because the server ignores them on /org2/nomenclature/*): " +
|
|
90
|
+
"filters.article (exact), filters.articleContains, filters.isKit. " +
|
|
91
|
+
"Other filter keys (nameContains, periodable, etc.) are passed through to the API as-is.\n\n" +
|
|
92
|
+
"IMPORTANT: Always set limit (recommended: 50 with enrich, 200 without) to avoid response truncation. " +
|
|
93
|
+
"Use skip to paginate through results: first call skip=0 limit=50, then skip=50 limit=50, etc. " +
|
|
94
|
+
"If the response contains _truncated, use skip and limit to get remaining items.",
|
|
53
95
|
inputSchema: {
|
|
54
96
|
type: "object",
|
|
55
97
|
properties: {
|
|
@@ -57,9 +99,13 @@ function createFetchAllTool(client, toolMap) {
|
|
|
57
99
|
type: "string",
|
|
58
100
|
description: "Name of a readList tool to paginate (e.g. bimp_nomenclature_readList)",
|
|
59
101
|
},
|
|
102
|
+
skip: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Number of items to skip from the beginning (default: 0). Use with limit to paginate: skip=0 limit=50, then skip=50 limit=50, etc.",
|
|
105
|
+
},
|
|
60
106
|
limit: {
|
|
61
107
|
type: "number",
|
|
62
|
-
description: "Maximum number of items to return
|
|
108
|
+
description: "Maximum number of items to return. RECOMMENDED: 50 with enrich=true, 200 without enrich. Avoid omitting to prevent response truncation.",
|
|
63
109
|
},
|
|
64
110
|
enrich: {
|
|
65
111
|
type: "boolean",
|
|
@@ -67,16 +113,23 @@ function createFetchAllTool(client, toolMap) {
|
|
|
67
113
|
},
|
|
68
114
|
filters: {
|
|
69
115
|
type: "object",
|
|
70
|
-
description: "
|
|
116
|
+
description: "Filter parameters. Known client-side keys (article, articleContains, name, nameContains, isKit) are applied locally after pagination. All others are passed to the API.",
|
|
71
117
|
},
|
|
72
118
|
},
|
|
73
119
|
required: ["tool"],
|
|
74
120
|
},
|
|
75
121
|
handler: async (params) => {
|
|
76
122
|
const toolName = params.tool;
|
|
123
|
+
const skip = params.skip ?? 0;
|
|
77
124
|
const limit = params.limit;
|
|
78
125
|
const enrich = params.enrich;
|
|
79
|
-
const
|
|
126
|
+
const rawFilters = (params.filters ?? {});
|
|
127
|
+
const { serverFilters, clientFilters } = splitFilters(rawFilters);
|
|
128
|
+
const hasClientFilters = Object.keys(clientFilters).length > 0;
|
|
129
|
+
// Effective limit accounts for skip: fetch skip+limit items, then slice.
|
|
130
|
+
// With client-side filters we can't pre-limit the API fetch because the
|
|
131
|
+
// server returns unfiltered pages — we have to paginate fully then filter.
|
|
132
|
+
const fetchLimit = limit && !hasClientFilters ? skip + limit : undefined;
|
|
80
133
|
const toolDef = toolMap.get(toolName);
|
|
81
134
|
if (!toolDef) {
|
|
82
135
|
throw new Error(`Tool not found: ${toolName}`);
|
|
@@ -87,14 +140,14 @@ function createFetchAllTool(client, toolMap) {
|
|
|
87
140
|
let offset = 0;
|
|
88
141
|
while (true) {
|
|
89
142
|
const requestParams = {
|
|
90
|
-
...
|
|
143
|
+
...serverFilters,
|
|
91
144
|
pagination: { offset, count: PAGE_SIZE },
|
|
92
145
|
};
|
|
93
146
|
const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
|
|
94
147
|
const page = response.data ?? [];
|
|
95
148
|
allItems.push(...page);
|
|
96
|
-
if (
|
|
97
|
-
allItems = allItems.slice(0,
|
|
149
|
+
if (fetchLimit && allItems.length >= fetchLimit) {
|
|
150
|
+
allItems = allItems.slice(0, fetchLimit);
|
|
98
151
|
break;
|
|
99
152
|
}
|
|
100
153
|
if (page.length < PAGE_SIZE) {
|
|
@@ -107,15 +160,15 @@ function createFetchAllTool(client, toolMap) {
|
|
|
107
160
|
let cursor;
|
|
108
161
|
while (true) {
|
|
109
162
|
const requestParams = {
|
|
110
|
-
...
|
|
163
|
+
...serverFilters,
|
|
111
164
|
...(cursor ? { cursor } : {}),
|
|
112
165
|
count: PAGE_SIZE,
|
|
113
166
|
};
|
|
114
167
|
const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
|
|
115
168
|
const page = response.data ?? [];
|
|
116
169
|
allItems.push(...page);
|
|
117
|
-
if (
|
|
118
|
-
allItems = allItems.slice(0,
|
|
170
|
+
if (fetchLimit && allItems.length >= fetchLimit) {
|
|
171
|
+
allItems = allItems.slice(0, fetchLimit);
|
|
119
172
|
break;
|
|
120
173
|
}
|
|
121
174
|
cursor = response.cursor;
|
|
@@ -128,15 +181,15 @@ function createFetchAllTool(client, toolMap) {
|
|
|
128
181
|
let page = 1;
|
|
129
182
|
while (true) {
|
|
130
183
|
const requestParams = {
|
|
131
|
-
...
|
|
184
|
+
...serverFilters,
|
|
132
185
|
page,
|
|
133
186
|
pageSize: PAGE_SIZE,
|
|
134
187
|
};
|
|
135
188
|
const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
|
|
136
189
|
const items = response.data ?? [];
|
|
137
190
|
allItems.push(...items);
|
|
138
|
-
if (
|
|
139
|
-
allItems = allItems.slice(0,
|
|
191
|
+
if (fetchLimit && allItems.length >= fetchLimit) {
|
|
192
|
+
allItems = allItems.slice(0, fetchLimit);
|
|
140
193
|
break;
|
|
141
194
|
}
|
|
142
195
|
if (items.length < PAGE_SIZE) {
|
|
@@ -147,14 +200,27 @@ function createFetchAllTool(client, toolMap) {
|
|
|
147
200
|
}
|
|
148
201
|
else {
|
|
149
202
|
// paginationType === "none" — single request
|
|
150
|
-
const requestParams = { ...
|
|
203
|
+
const requestParams = { ...serverFilters };
|
|
151
204
|
const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
|
|
152
205
|
const items = response.data ?? [];
|
|
153
206
|
allItems.push(...items);
|
|
154
|
-
if (
|
|
155
|
-
allItems = allItems.slice(0,
|
|
207
|
+
if (fetchLimit && allItems.length > fetchLimit) {
|
|
208
|
+
allItems = allItems.slice(0, fetchLimit);
|
|
156
209
|
}
|
|
157
210
|
}
|
|
211
|
+
// Apply client-side filters before skip/limit
|
|
212
|
+
if (hasClientFilters) {
|
|
213
|
+
allItems = allItems.filter((item) => matchesClientFilters(item, clientFilters));
|
|
214
|
+
}
|
|
215
|
+
// Apply skip — slice off the first `skip` items
|
|
216
|
+
const totalFetched = allItems.length;
|
|
217
|
+
if (skip > 0) {
|
|
218
|
+
allItems = allItems.slice(skip);
|
|
219
|
+
}
|
|
220
|
+
// When client filters were applied we didn't pre-limit the API fetch; trim now.
|
|
221
|
+
if (hasClientFilters && limit && allItems.length > limit) {
|
|
222
|
+
allItems = allItems.slice(0, limit);
|
|
223
|
+
}
|
|
158
224
|
// Enrich: call the read endpoint for each item
|
|
159
225
|
if (enrich && allItems.length > 0) {
|
|
160
226
|
const readToolName = deriveReadToolName(toolName);
|
|
@@ -166,9 +232,19 @@ function createFetchAllTool(client, toolMap) {
|
|
|
166
232
|
.map((item) => item.uuid)
|
|
167
233
|
.filter(Boolean);
|
|
168
234
|
const { items: enrichedItems } = await batchReadUuids(client, readToolDef, uuids, 10);
|
|
169
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
items: enrichedItems,
|
|
237
|
+
count: enrichedItems.length,
|
|
238
|
+
skip,
|
|
239
|
+
hasMore: totalFetched > skip + enrichedItems.length,
|
|
240
|
+
};
|
|
170
241
|
}
|
|
171
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
items: allItems,
|
|
244
|
+
count: allItems.length,
|
|
245
|
+
skip,
|
|
246
|
+
hasMore: totalFetched > skip + allItems.length,
|
|
247
|
+
};
|
|
172
248
|
},
|
|
173
249
|
};
|
|
174
250
|
}
|
|
@@ -261,10 +337,68 @@ function createBulkUpdateTool(client, toolMap) {
|
|
|
261
337
|
},
|
|
262
338
|
};
|
|
263
339
|
}
|
|
340
|
+
function createFindByArticleTool(client, toolMap) {
|
|
341
|
+
return {
|
|
342
|
+
name: "bimp_nomenclature_findByArticle",
|
|
343
|
+
description: "Find nomenclature records by article (SKU). Server-side article filtering on " +
|
|
344
|
+
"/org2/nomenclature/api-readList is unreliable, so this tool paginates and filters " +
|
|
345
|
+
"locally. Use exact=false for substring match.",
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {
|
|
349
|
+
article: { type: "string", description: "Article value to match" },
|
|
350
|
+
exact: {
|
|
351
|
+
type: "boolean",
|
|
352
|
+
description: "When true (default), only exact case-insensitive matches are returned; when false, substring.",
|
|
353
|
+
},
|
|
354
|
+
limit: {
|
|
355
|
+
type: "number",
|
|
356
|
+
description: "Max matching items to return (default 50).",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required: ["article"],
|
|
360
|
+
},
|
|
361
|
+
handler: async (params) => {
|
|
362
|
+
const article = String(params.article ?? "").trim();
|
|
363
|
+
if (!article) {
|
|
364
|
+
throw new Error("article is required");
|
|
365
|
+
}
|
|
366
|
+
const exact = params.exact ?? true;
|
|
367
|
+
const limit = params.limit ?? 50;
|
|
368
|
+
const toolDef = toolMap.get("bimp_nomenclature_readList");
|
|
369
|
+
if (!toolDef) {
|
|
370
|
+
throw new Error("bimp_nomenclature_readList not available");
|
|
371
|
+
}
|
|
372
|
+
const needle = article.toLowerCase();
|
|
373
|
+
const matches = [];
|
|
374
|
+
let offset = 0;
|
|
375
|
+
while (matches.length < limit) {
|
|
376
|
+
const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, { pagination: { offset, count: PAGE_SIZE } }));
|
|
377
|
+
const page = response.data ?? [];
|
|
378
|
+
if (page.length === 0)
|
|
379
|
+
break;
|
|
380
|
+
for (const item of page) {
|
|
381
|
+
const itemArticle = String(item.article ?? item["Артикул"] ?? "").toLowerCase();
|
|
382
|
+
const hit = exact ? itemArticle === needle : itemArticle.includes(needle);
|
|
383
|
+
if (hit) {
|
|
384
|
+
matches.push(item);
|
|
385
|
+
if (matches.length >= limit)
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (page.length < PAGE_SIZE)
|
|
390
|
+
break;
|
|
391
|
+
offset += PAGE_SIZE;
|
|
392
|
+
}
|
|
393
|
+
return { items: matches, count: matches.length };
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
264
397
|
export function createUtilityTools(client, toolMap) {
|
|
265
398
|
return [
|
|
266
399
|
createFetchAllTool(client, toolMap),
|
|
267
400
|
createBatchReadTool(client, toolMap),
|
|
268
401
|
createBulkUpdateTool(client, toolMap),
|
|
402
|
+
createFindByArticleTool(client, toolMap),
|
|
269
403
|
];
|
|
270
404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bimp-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for BIMP ERP API — ~140 tools dynamically generated from OpenAPI spec, plus planning/accounting fields and bulk operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|