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 +1 -1
- 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 +117 -8
- package/package.json +1 -1
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.
|
|
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
|
|
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,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: "
|
|
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
|
|
88
|
-
|
|
89
|
-
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;
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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 = { ...
|
|
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
|
+
"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",
|