bimp-mcp 0.2.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.
@@ -0,0 +1,23 @@
1
+ export interface BimpClientConfig {
2
+ email: string;
3
+ password: string;
4
+ companyCode: string;
5
+ baseUrl?: string;
6
+ timeout?: number;
7
+ }
8
+ export declare class BimpClient {
9
+ private config;
10
+ private tokens;
11
+ private loginPromise;
12
+ constructor(config: BimpClientConfig);
13
+ request(method: string, path: string, params?: Record<string, unknown>, options?: {
14
+ timeout?: number;
15
+ }): Promise<unknown>;
16
+ switchCompany(codeOrUuid: string): Promise<void>;
17
+ listCompanies(): Promise<unknown>;
18
+ private ensureAuthenticated;
19
+ private login;
20
+ private refreshAuth;
21
+ private executeRequest;
22
+ private rawFetch;
23
+ }
package/dist/client.js ADDED
@@ -0,0 +1,160 @@
1
+ export class BimpClient {
2
+ config;
3
+ tokens = null;
4
+ loginPromise = null;
5
+ constructor(config) {
6
+ this.config = {
7
+ ...config,
8
+ baseUrl: config.baseUrl ?? "https://app.bimpsoft.com",
9
+ timeout: config.timeout ?? 30_000,
10
+ };
11
+ }
12
+ async request(method, path, params = {}, options) {
13
+ await this.ensureAuthenticated();
14
+ const result = await this.executeRequest(method, path, params, options?.timeout);
15
+ if (result.status === 401) {
16
+ await this.refreshAuth();
17
+ const retry = await this.executeRequest(method, path, params, options?.timeout);
18
+ if (!retry.ok) {
19
+ throw new Error(`BIMP API error: ${retry.status} on ${method} ${path}`);
20
+ }
21
+ return retry.data;
22
+ }
23
+ if (!result.ok) {
24
+ throw new Error(`BIMP API error: ${result.status} on ${method} ${path}`);
25
+ }
26
+ return result.data;
27
+ }
28
+ async switchCompany(codeOrUuid) {
29
+ if (!this.tokens) {
30
+ throw new Error("Must be logged in before switching company");
31
+ }
32
+ const body = codeOrUuid.includes("-")
33
+ ? { uuid: codeOrUuid }
34
+ : { code: codeOrUuid };
35
+ const resp = await this.rawFetch("POST", "/org2/auth/api-selectCompany", body, { "access-token": this.tokens.accessToken });
36
+ const json = (await resp.json());
37
+ if (!json.success) {
38
+ throw new Error("Failed to switch company");
39
+ }
40
+ this.tokens.companyAccessToken = json.data.companyAccessToken;
41
+ }
42
+ async listCompanies() {
43
+ await this.ensureAuthenticated();
44
+ const resp = await this.rawFetch("POST", "/org2/company/api-readDetailedList", {}, { "access-token": this.tokens.accessToken });
45
+ const json = (await resp.json());
46
+ return json.data;
47
+ }
48
+ async ensureAuthenticated() {
49
+ if (this.tokens)
50
+ return;
51
+ if (this.loginPromise) {
52
+ await this.loginPromise;
53
+ return;
54
+ }
55
+ this.loginPromise = this.login();
56
+ try {
57
+ await this.loginPromise;
58
+ }
59
+ finally {
60
+ this.loginPromise = null;
61
+ }
62
+ }
63
+ async login() {
64
+ const loginResp = await this.rawFetch("POST", "/org2/auth/api-login", { email: this.config.email, password: this.config.password }, {});
65
+ const loginJson = (await loginResp.json());
66
+ if (!loginJson.success) {
67
+ throw new Error("BIMP login failed");
68
+ }
69
+ const selectResp = await this.rawFetch("POST", "/org2/auth/api-selectCompany", { code: this.config.companyCode }, { "access-token": loginJson.data.accessToken });
70
+ const selectJson = (await selectResp.json());
71
+ if (!selectJson.success) {
72
+ throw new Error("BIMP company selection failed");
73
+ }
74
+ this.tokens = {
75
+ accessToken: loginJson.data.accessToken,
76
+ refreshToken: loginJson.data.refreshToken,
77
+ companyAccessToken: selectJson.data.companyAccessToken,
78
+ };
79
+ }
80
+ async refreshAuth() {
81
+ if (!this.tokens) {
82
+ await this.login();
83
+ return;
84
+ }
85
+ try {
86
+ const refreshResp = await this.rawFetch("POST", "/org2/auth/api-refresh", { refreshToken: this.tokens.refreshToken }, { "access-token": this.tokens.accessToken });
87
+ if (!refreshResp.ok) {
88
+ this.tokens = null;
89
+ await this.login();
90
+ return;
91
+ }
92
+ const refreshJson = (await refreshResp.json());
93
+ const selectResp = await this.rawFetch("POST", "/org2/auth/api-selectCompany", { code: this.config.companyCode }, { "access-token": refreshJson.data.accessToken });
94
+ const selectJson = (await selectResp.json());
95
+ this.tokens = {
96
+ accessToken: refreshJson.data.accessToken,
97
+ refreshToken: refreshJson.data.refreshToken,
98
+ companyAccessToken: selectJson.data.companyAccessToken,
99
+ };
100
+ }
101
+ catch {
102
+ this.tokens = null;
103
+ await this.login();
104
+ }
105
+ }
106
+ async executeRequest(method, pathTemplate, params, timeout) {
107
+ const resp = await this.rawFetch(method, pathTemplate, params, { "access-token": this.tokens.companyAccessToken }, timeout);
108
+ if (resp.status === 401) {
109
+ return { ok: false, status: 401, data: null };
110
+ }
111
+ const json = (await resp.json());
112
+ return { ok: resp.ok, status: resp.status, data: json };
113
+ }
114
+ async rawFetch(method, pathTemplate, params, headers, timeout) {
115
+ let path = pathTemplate;
116
+ const bodyParams = { ...params };
117
+ const pathParamRegex = /\{(\w+)\}/g;
118
+ let match;
119
+ while ((match = pathParamRegex.exec(pathTemplate)) !== null) {
120
+ const paramName = match[1];
121
+ if (paramName in bodyParams) {
122
+ path = path.replace(`{${paramName}}`, String(bodyParams[paramName]));
123
+ delete bodyParams[paramName];
124
+ }
125
+ }
126
+ let url = `${this.config.baseUrl}${path}`;
127
+ const fetchOptions = {
128
+ method,
129
+ headers: {
130
+ "accept-language": "uk-UA",
131
+ "content-type": "application/json",
132
+ ...headers,
133
+ },
134
+ };
135
+ if (method === "GET") {
136
+ const queryParams = new URLSearchParams();
137
+ for (const [key, value] of Object.entries(bodyParams)) {
138
+ if (value !== undefined && value !== null) {
139
+ queryParams.set(key, String(value));
140
+ }
141
+ }
142
+ const qs = queryParams.toString();
143
+ if (qs)
144
+ url += `?${qs}`;
145
+ }
146
+ else {
147
+ fetchOptions.body = JSON.stringify(bodyParams);
148
+ }
149
+ const controller = new AbortController();
150
+ const timeoutMs = timeout ?? this.config.timeout;
151
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
152
+ fetchOptions.signal = controller.signal;
153
+ try {
154
+ return await fetch(url, fetchOptions);
155
+ }
156
+ finally {
157
+ clearTimeout(timer);
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import * as z from "zod";
9
+ import { BimpClient } from "./client.js";
10
+ import { generateTools } from "./tool-generator.js";
11
+ import { createUtilityTools } from "./utilities.js";
12
+ import { createNomenclaturesTools } from "./nomenclatures-extended.js";
13
+ import { PROMPT_TEXTS } from "./prompts.js";
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const config = {
16
+ email: process.env.BIMP_EMAIL ?? "",
17
+ password: process.env.BIMP_PASSWORD ?? "",
18
+ companyCode: process.env.BIMP_COMPANY_CODE ?? "",
19
+ baseUrl: process.env.BIMP_BASE_URL,
20
+ };
21
+ const specPath = resolve(__dirname, "..", "bimp-api.json");
22
+ const spec = JSON.parse(readFileSync(specPath, "utf-8"));
23
+ const client = new BimpClient(config);
24
+ const generatedTools = generateTools(spec);
25
+ const toolMap = new Map();
26
+ for (const tool of generatedTools) {
27
+ toolMap.set(tool.name, tool);
28
+ }
29
+ const utilityTools = createUtilityTools(client, toolMap);
30
+ const nomenclaturesTools = createNomenclaturesTools(client);
31
+ const server = new McpServer({ name: "bimp-mcp", version: "0.1.0" }, { capabilities: { logging: {} } });
32
+ // Register prompts via McpServer (uses Zod, type-safe)
33
+ for (const [name, prompt] of Object.entries(PROMPT_TEXTS)) {
34
+ server.registerPrompt(name, { description: prompt.description }, () => ({
35
+ messages: [
36
+ {
37
+ role: "user",
38
+ content: { type: "text", text: prompt.text },
39
+ },
40
+ ],
41
+ }));
42
+ }
43
+ // Register auth tools via McpServer
44
+ server.registerTool("bimp_auth_listCompanies", {
45
+ description: "List all companies accessible to the current user",
46
+ inputSchema: z.object({}),
47
+ }, async () => {
48
+ const companies = await client.listCompanies();
49
+ return {
50
+ content: [
51
+ { type: "text", text: JSON.stringify(companies, null, 2) },
52
+ ],
53
+ };
54
+ });
55
+ server.registerTool("bimp_auth_switchCompany", {
56
+ description: "Switch to a different company by code (e.g. '000001398') or UUID",
57
+ inputSchema: z.object({
58
+ codeOrUuid: z
59
+ .string()
60
+ .describe("Company code or UUID to switch to"),
61
+ }),
62
+ }, async ({ codeOrUuid }) => {
63
+ await client.switchCompany(codeOrUuid);
64
+ return {
65
+ content: [
66
+ { type: "text", text: `Switched to company: ${codeOrUuid}` },
67
+ ],
68
+ };
69
+ });
70
+ // Use low-level server for dynamic tool registration (raw JSON Schema from OpenAPI)
71
+ const lowLevelServer = server.server;
72
+ lowLevelServer.setRequestHandler(ListToolsRequestSchema, async () => ({
73
+ tools: [
74
+ ...generatedTools.map((t) => ({
75
+ name: t.name,
76
+ description: t.description,
77
+ inputSchema: t.inputSchema,
78
+ })),
79
+ // Auth tools are registered via McpServer, but we need them in the list too
80
+ {
81
+ name: "bimp_auth_listCompanies",
82
+ description: "List all companies accessible to the current user",
83
+ inputSchema: { type: "object", properties: {} },
84
+ },
85
+ {
86
+ name: "bimp_auth_switchCompany",
87
+ description: "Switch to a different company by code (e.g. '000001398') or UUID",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ codeOrUuid: {
92
+ type: "string",
93
+ description: "Company code or UUID to switch to",
94
+ },
95
+ },
96
+ required: ["codeOrUuid"],
97
+ },
98
+ },
99
+ ...utilityTools.map((t) => ({
100
+ name: t.name,
101
+ description: t.description,
102
+ inputSchema: t.inputSchema,
103
+ })),
104
+ ...nomenclaturesTools.map((t) => ({
105
+ name: t.name,
106
+ description: t.description,
107
+ inputSchema: t.inputSchema,
108
+ })),
109
+ ],
110
+ }));
111
+ lowLevelServer.setRequestHandler(CallToolRequestSchema, async (request) => {
112
+ const { name, arguments: args } = request.params;
113
+ const params = (args ?? {});
114
+ try {
115
+ // Auth tools
116
+ if (name === "bimp_auth_listCompanies") {
117
+ const companies = await client.listCompanies();
118
+ return {
119
+ content: [
120
+ { type: "text", text: JSON.stringify(companies, null, 2) },
121
+ ],
122
+ };
123
+ }
124
+ if (name === "bimp_auth_switchCompany") {
125
+ await client.switchCompany(params.codeOrUuid);
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `Switched to company: ${params.codeOrUuid}`,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ // Utility tools
136
+ const utilityTool = utilityTools.find((t) => t.name === name);
137
+ if (utilityTool) {
138
+ const result = await utilityTool.handler(params);
139
+ return {
140
+ content: [
141
+ { type: "text", text: JSON.stringify(result, null, 2) },
142
+ ],
143
+ };
144
+ }
145
+ // Nomenclatures extended tools
146
+ const nomenclaturesTool = nomenclaturesTools.find((t) => t.name === name);
147
+ if (nomenclaturesTool) {
148
+ const result = await nomenclaturesTool.handler(params);
149
+ return {
150
+ content: [
151
+ { type: "text", text: JSON.stringify(result, null, 2) },
152
+ ],
153
+ };
154
+ }
155
+ // Generated API tools
156
+ const toolDef = toolMap.get(name);
157
+ if (!toolDef) {
158
+ return {
159
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
160
+ isError: true,
161
+ };
162
+ }
163
+ const result = await client.request(toolDef.metadata.method, toolDef.metadata.path, params);
164
+ return {
165
+ content: [
166
+ { type: "text", text: JSON.stringify(result, null, 2) },
167
+ ],
168
+ };
169
+ }
170
+ catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ return {
173
+ content: [{ type: "text", text: `Error: ${message}` }],
174
+ isError: true,
175
+ };
176
+ }
177
+ });
178
+ async function main() {
179
+ const transport = new StdioServerTransport();
180
+ await server.connect(transport);
181
+ console.error("BIMP MCP server started");
182
+ }
183
+ main().catch((error) => {
184
+ console.error("Fatal error:", error);
185
+ process.exit(1);
186
+ });
@@ -0,0 +1,6 @@
1
+ import type { BimpClient } from "./client.js";
2
+ import type { UtilityTool } from "./utilities.js";
3
+ export declare const FIELD_MAP: Record<string, string>;
4
+ export declare function toEnglish(obj: Record<string, unknown>): Record<string, unknown>;
5
+ export declare function toCyrillic(obj: Record<string, unknown>): Record<string, unknown>;
6
+ export declare function createNomenclaturesTools(client: BimpClient): UtilityTool[];
@@ -0,0 +1,118 @@
1
+ export const FIELD_MAP = {
2
+ uuid: "GUID",
3
+ name: "Наименование",
4
+ fullName: "НаименованиеДляПечати",
5
+ code: "Код",
6
+ article: "Артикул",
7
+ comment: "Комментарий",
8
+ barcode: "Штрихкод",
9
+ minStock: "МинимальныйОстаток",
10
+ maxStock: "МаксимальныйОстаток",
11
+ speedOfDemand: "speedOfDemand",
12
+ insuranceReserve: "insuranceReserve",
13
+ deliveryTerm: "deliveryTerm",
14
+ weight: "Вес",
15
+ height: "Высота",
16
+ width: "Ширина",
17
+ length: "Длина",
18
+ plannedCost: "ПлановаяСебестоимость",
19
+ isKit: "ЭтоНабор",
20
+ isService: "ЭтоУслуга",
21
+ archived: "Архив",
22
+ type: "ТипНоменклатуры",
23
+ unitOfMeasurementUuid: "ЕдиницаИзмерения.GUID",
24
+ expenseAccountUuid: "СчетУчетаЗатрат.GUID",
25
+ inventoryAccountUuid: "СчетУчетаЗапасов.GUID",
26
+ docType: "ТипДокумента",
27
+ };
28
+ const REVERSE_MAP = Object.fromEntries(Object.entries(FIELD_MAP).map(([en, cyrillic]) => [cyrillic, en]));
29
+ export function toEnglish(obj) {
30
+ const result = {};
31
+ for (const [key, value] of Object.entries(obj)) {
32
+ result[REVERSE_MAP[key] ?? key] = value;
33
+ }
34
+ return result;
35
+ }
36
+ export function toCyrillic(obj) {
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(obj)) {
39
+ result[FIELD_MAP[key] ?? key] = value;
40
+ }
41
+ return result;
42
+ }
43
+ function createNomenclaturesReadTool(client) {
44
+ return {
45
+ name: "bimp_nomenclatures_read",
46
+ description: "Read full product card including planning/accounting fields " +
47
+ "(minStock, maxStock, speedOfDemand, insuranceReserve, deliveryTerm) " +
48
+ "from the extended nomenclatures endpoint.",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ uuid: { type: "string", description: "Product UUID" },
53
+ },
54
+ required: ["uuid"],
55
+ },
56
+ handler: async (params) => {
57
+ const uuid = params.uuid;
58
+ const response = (await client.request("POST", "/org2/nomenclatures/read", {
59
+ lang: "ru",
60
+ uid: uuid,
61
+ }));
62
+ return toEnglish(response.data);
63
+ },
64
+ };
65
+ }
66
+ function createNomenclaturesUpsertTool(client) {
67
+ return {
68
+ name: "bimp_nomenclatures_upsert",
69
+ description: "Create or update a product with planning/accounting fields. " +
70
+ "For update: uuid is required. For create: uuid is optional. " +
71
+ "Supports: minStock, maxStock, speedOfDemand, insuranceReserve, deliveryTerm, and all standard fields.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ uuid: { type: "string", description: "Product UUID (required for update, optional for create)" },
76
+ name: { type: "string", description: "Product name (required for create)" },
77
+ article: { type: "string", description: "Product article/SKU" },
78
+ minStock: { type: "number", description: "Minimum stock level" },
79
+ maxStock: { type: "number", description: "Maximum stock level" },
80
+ speedOfDemand: { type: "number", description: "Demand rate" },
81
+ insuranceReserve: { type: "number", description: "Safety stock" },
82
+ deliveryTerm: { type: "number", description: "Delivery time in days" },
83
+ unitOfMeasurementUuid: { type: "string", description: "Unit of measurement UUID" },
84
+ type: { type: "number", description: "Product type: 1=goods, 2=service" },
85
+ },
86
+ },
87
+ handler: async (params) => {
88
+ const { docType: _ignored, ...fields } = params;
89
+ const body = toCyrillic(fields);
90
+ body["ТипДокумента"] = "101";
91
+ const response = (await client.request("POST", "/org2/nomenclatures/upsert", body));
92
+ return { uuid: response.data.GUID };
93
+ },
94
+ };
95
+ }
96
+ function createNomenclaturesReadListTool(client) {
97
+ return {
98
+ name: "bimp_nomenclatures_readList",
99
+ description: "List all products from the extended nomenclatures endpoint. " +
100
+ "Returns English-mapped items including minStock field.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {},
104
+ },
105
+ handler: async () => {
106
+ const response = (await client.request("POST", "/org2/nomenclatures/readList", {}));
107
+ const items = (response.data ?? []).map(toEnglish);
108
+ return { items, count: items.length };
109
+ },
110
+ };
111
+ }
112
+ export function createNomenclaturesTools(client) {
113
+ return [
114
+ createNomenclaturesReadTool(client),
115
+ createNomenclaturesUpsertTool(client),
116
+ createNomenclaturesReadListTool(client),
117
+ ];
118
+ }
@@ -0,0 +1,25 @@
1
+ interface PromptDef {
2
+ name: string;
3
+ description: string;
4
+ arguments?: Array<{
5
+ name: string;
6
+ description: string;
7
+ required?: boolean;
8
+ }>;
9
+ }
10
+ interface PromptResult {
11
+ messages: Array<{
12
+ role: "user" | "assistant";
13
+ content: {
14
+ type: "text";
15
+ text: string;
16
+ };
17
+ }>;
18
+ }
19
+ export declare const PROMPT_TEXTS: Record<string, {
20
+ description: string;
21
+ text: string;
22
+ }>;
23
+ export declare function getPrompts(): PromptDef[];
24
+ export declare function handleGetPrompt(name: string, _args?: Record<string, string>): PromptResult;
25
+ export {};