bimp-mcp 0.3.3 → 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/dist/index.js CHANGED
@@ -90,7 +90,7 @@ for (const tool of generatedTools) {
90
90
  }
91
91
  const utilityTools = createUtilityTools(client, toolMap);
92
92
  const nomenclaturesTools = createNomenclaturesTools(client);
93
- const server = new McpServer({ name: "bimp-mcp", version: "0.3.3" }, { capabilities: { logging: {} } });
93
+ const server = new McpServer({ name: "bimp-mcp", version: "0.4.0" }, { capabilities: { logging: {} } });
94
94
  // Register prompts via McpServer (uses Zod, type-safe)
95
95
  for (const [name, prompt] of Object.entries(PROMPT_TEXTS)) {
96
96
  server.registerPrompt(name, { description: prompt.description }, () => ({
@@ -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,51 @@ 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
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" +
53
92
  "IMPORTANT: Always set limit (recommended: 50 with enrich, 200 without) to avoid response truncation. " +
54
93
  "Use skip to paginate through results: first call skip=0 limit=50, then skip=50 limit=50, etc. " +
55
94
  "If the response contains _truncated, use skip and limit to get remaining items.",
@@ -74,7 +113,7 @@ function createFetchAllTool(client, toolMap) {
74
113
  },
75
114
  filters: {
76
115
  type: "object",
77
- 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.",
78
117
  },
79
118
  },
80
119
  required: ["tool"],
@@ -84,9 +123,13 @@ function createFetchAllTool(client, toolMap) {
84
123
  const skip = params.skip ?? 0;
85
124
  const limit = params.limit;
86
125
  const enrich = params.enrich;
87
- const filters = (params.filters ?? {});
88
- // Effective limit accounts for skip: fetch skip+limit items, then slice
89
- const fetchLimit = limit ? skip + limit : undefined;
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;
90
133
  const toolDef = toolMap.get(toolName);
91
134
  if (!toolDef) {
92
135
  throw new Error(`Tool not found: ${toolName}`);
@@ -97,7 +140,7 @@ function createFetchAllTool(client, toolMap) {
97
140
  let offset = 0;
98
141
  while (true) {
99
142
  const requestParams = {
100
- ...filters,
143
+ ...serverFilters,
101
144
  pagination: { offset, count: PAGE_SIZE },
102
145
  };
103
146
  const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
@@ -117,7 +160,7 @@ function createFetchAllTool(client, toolMap) {
117
160
  let cursor;
118
161
  while (true) {
119
162
  const requestParams = {
120
- ...filters,
163
+ ...serverFilters,
121
164
  ...(cursor ? { cursor } : {}),
122
165
  count: PAGE_SIZE,
123
166
  };
@@ -138,7 +181,7 @@ function createFetchAllTool(client, toolMap) {
138
181
  let page = 1;
139
182
  while (true) {
140
183
  const requestParams = {
141
- ...filters,
184
+ ...serverFilters,
142
185
  page,
143
186
  pageSize: PAGE_SIZE,
144
187
  };
@@ -157,7 +200,7 @@ function createFetchAllTool(client, toolMap) {
157
200
  }
158
201
  else {
159
202
  // paginationType === "none" — single request
160
- const requestParams = { ...filters };
203
+ const requestParams = { ...serverFilters };
161
204
  const response = (await client.request(toolDef.metadata.method, toolDef.metadata.path, requestParams));
162
205
  const items = response.data ?? [];
163
206
  allItems.push(...items);
@@ -165,11 +208,19 @@ function createFetchAllTool(client, toolMap) {
165
208
  allItems = allItems.slice(0, fetchLimit);
166
209
  }
167
210
  }
211
+ // Apply client-side filters before skip/limit
212
+ if (hasClientFilters) {
213
+ allItems = allItems.filter((item) => matchesClientFilters(item, clientFilters));
214
+ }
168
215
  // Apply skip — slice off the first `skip` items
169
216
  const totalFetched = allItems.length;
170
217
  if (skip > 0) {
171
218
  allItems = allItems.slice(skip);
172
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
+ }
173
224
  // Enrich: call the read endpoint for each item
174
225
  if (enrich && allItems.length > 0) {
175
226
  const readToolName = deriveReadToolName(toolName);
@@ -286,10 +337,68 @@ function createBulkUpdateTool(client, toolMap) {
286
337
  },
287
338
  };
288
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
+ }
289
397
  export function createUtilityTools(client, toolMap) {
290
398
  return [
291
399
  createFetchAllTool(client, toolMap),
292
400
  createBatchReadTool(client, toolMap),
293
401
  createBulkUpdateTool(client, toolMap),
402
+ createFindByArticleTool(client, toolMap),
294
403
  ];
295
404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimp-mcp",
3
- "version": "0.3.3",
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",