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 CHANGED
@@ -1,24 +1,43 @@
1
- # bimp-mcp
1
+ <p align="center">
2
+ <img src="assets/header.png" alt="HEYLOVE x BIMP" width="480" />
3
+ </p>
2
4
 
3
- MCP server for [BIMP ERP](https://bimpsoft.com) -- a Ukrainian cloud-based ERP system for small and medium businesses covering sales, inventory, finance, manufacturing, and procurement.
5
+ <h1 align="center">BIMP MCP</h1>
4
6
 
5
- This server enables LLMs to interact with BIMP data through the [Model Context Protocol](https://modelcontextprotocol.io): read, create, update entities, perform bulk operations, and analyze business data.
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 -- adding a new endpoint requires only editing `bimp-api.json` and restarting
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** providing ERP domain context, workflow guides, and data analysis instructions
12
- - **Auto-authentication** with token refresh -- login triggered on first API call, tokens refreshed transparently on 401
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
- ### Environment Variables
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 Run
51
+ ### 2. Install and run
33
52
 
34
53
  ```bash
35
54
  npm install
36
55
  npm start
37
56
  ```
38
57
 
39
- The server starts on stdio transport. For development with auto-reload:
58
+ For development with auto-reload:
40
59
 
41
60
  ```bash
42
61
  npm run dev
43
62
  ```
44
63
 
45
- ### MCP Client Configuration
64
+ ### 3. Connect your MCP client
46
65
 
47
- Add to your `claude_desktop_config.json` (Claude Desktop) or equivalent MCP client config:
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
- ## Available Tools
85
+ ## Tools
67
86
 
68
- ### Auto-Generated (~135 tools)
87
+ ### Auto-generated (~135 tools)
69
88
 
70
- Tools are generated from `bimp-api.json` at startup. Naming convention: `bimp_{entity}_{action}`.
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 Tools
101
+ ### Utility tools
83
102
 
84
103
  | Tool | Purpose |
85
- |------|---------|
86
- | `bimp_fetch_all` | Auto-paginate any readList endpoint. Supports offset/count, cursor, and page/pageSize pagination. Optional `enrich` mode fetches full details for each record. |
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
- ## Available Prompts
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` | How to analyze BIMP data effectively, pagination quirks, enrichment |
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: 401, data: null };
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.3.1" }, { capabilities: { logging: {} } });
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: JSON.stringify(result, null, 2) },
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: JSON.stringify(result, null, 2) },
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: JSON.stringify(result, null, 2) },
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: JSON.stringify(result, null, 2) },
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 with planning/accounting fields. " +
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: minStock, maxStock, speedOfDemand, insuranceReserve, deliveryTerm, and all standard fields.",
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: _ignored, ...fields } = params;
89
- const body = toCyrillic(fields);
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 };
@@ -7,6 +7,7 @@ export interface UtilityTool {
7
7
  type: "object";
8
8
  properties: Record<string, unknown>;
9
9
  required?: string[];
10
+ additionalProperties?: boolean;
10
11
  };
11
12
  handler: (params: Record<string, unknown>) => Promise<unknown>;
12
13
  }
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 (default: unlimited)",
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: "Additional filter parameters to pass through to the API",
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 filters = (params.filters ?? {});
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
- ...filters,
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 (limit && allItems.length >= limit) {
97
- allItems = allItems.slice(0, limit);
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
- ...filters,
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 (limit && allItems.length >= limit) {
118
- allItems = allItems.slice(0, limit);
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
- ...filters,
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 (limit && allItems.length >= limit) {
139
- allItems = allItems.slice(0, limit);
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 = { ...filters };
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 (limit && allItems.length > limit) {
155
- allItems = allItems.slice(0, limit);
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 { items: enrichedItems, count: enrichedItems.length };
235
+ return {
236
+ items: enrichedItems,
237
+ count: enrichedItems.length,
238
+ skip,
239
+ hasMore: totalFetched > skip + enrichedItems.length,
240
+ };
170
241
  }
171
- return { items: allItems, count: allItems.length };
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.1",
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",