mes-mcp 0.3.4 → 0.3.5

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/.env.example CHANGED
@@ -2,6 +2,8 @@ MES_BASE_URL=http://127.0.0.1:6033
2
2
  MES_API_PREFIX=/api
3
3
  MES_AGENT_TOKEN=
4
4
  MES_TIMEOUT_MS=30000
5
+ MES_MAX_CONCURRENT_REQUESTS=4
6
+ MES_MAX_QUEUED_REQUESTS=8
5
7
  # 动作目录缓存 TTL(毫秒,兜底),默认 12h。另外跨自然日必刷新:
6
8
  # 每天第一次调用 mes_action.* 时自动拉最新动作目录;用户也可用 mes_action.catalog 手动强刷。
7
9
  MES_ACTION_CACHE_TTL_MS=43200000
package/README.md CHANGED
@@ -18,6 +18,8 @@ MES_BASE_URL=http://127.0.0.1:6033
18
18
  MES_API_PREFIX=/api
19
19
  MES_AGENT_TOKEN=BearerTokenWithoutBearerPrefix
20
20
  MES_TIMEOUT_MS=30000
21
+ MES_MAX_CONCURRENT_REQUESTS=4
22
+ MES_MAX_QUEUED_REQUESTS=8
21
23
  MES_REGISTER_DYNAMIC_TOOLS=false
22
24
  ```
23
25
 
@@ -1,3 +1,4 @@
1
+ import { ConcurrencyLimiter } from "../utils/concurrency-limiter.js";
1
2
  function buildMesErrorMessage(path, status, message) {
2
3
  const hints = [];
3
4
  if (/pageSize must not be greater than 100/i.test(message)) {
@@ -30,36 +31,40 @@ export class MesAgentApiClient {
30
31
  // undefined 且 promise 存在 = 拉取在途(并发复用同一 promise,避免重复请求)。
31
32
  businessActionsFetchedAt;
32
33
  businessActionsVersion = 0;
34
+ requestLimiter;
33
35
  constructor(config) {
34
36
  this.config = config;
37
+ this.requestLimiter = new ConcurrencyLimiter(config.maxConcurrentRequests, config.maxQueuedRequests);
35
38
  }
36
39
  async post(path, payload) {
37
- const controller = new AbortController();
38
- const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
39
- try {
40
- const response = await fetch(this.buildUrl(path), {
41
- method: "POST",
42
- headers: {
43
- Authorization: `Bearer ${this.config.mesAgentToken}`,
44
- "Content-Type": "application/json",
45
- },
46
- body: JSON.stringify(payload ?? {}),
47
- signal: controller.signal,
48
- });
49
- const text = await response.text();
50
- const body = this.parseBody(text);
51
- if (!response.ok) {
52
- const message = body?.msg || body?.message || text || response.statusText;
53
- throw new Error(buildMesErrorMessage(path, response.status, message));
40
+ return this.requestLimiter.run(async () => {
41
+ const controller = new AbortController();
42
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
43
+ try {
44
+ const response = await fetch(this.buildUrl(path), {
45
+ method: "POST",
46
+ headers: {
47
+ Authorization: `Bearer ${this.config.mesAgentToken}`,
48
+ "Content-Type": "application/json",
49
+ },
50
+ body: JSON.stringify(payload ?? {}),
51
+ signal: controller.signal,
52
+ });
53
+ const text = await response.text();
54
+ const body = this.parseBody(text);
55
+ if (!response.ok) {
56
+ const message = body?.msg || body?.message || text || response.statusText;
57
+ throw new Error(buildMesErrorMessage(path, response.status, message));
58
+ }
59
+ if (body && typeof body.code === "number" && body.code !== 0) {
60
+ throw new Error(buildMesErrorMessage(path, body.code, body.msg || body.message || `MES code ${body.code}`));
61
+ }
62
+ return (body && "data" in body ? body.data : body);
54
63
  }
55
- if (body && typeof body.code === "number" && body.code !== 0) {
56
- throw new Error(buildMesErrorMessage(path, body.code, body.msg || body.message || `MES code ${body.code}`));
64
+ finally {
65
+ clearTimeout(timeout);
57
66
  }
58
- return (body && "data" in body ? body.data : body);
59
- }
60
- finally {
61
- clearTimeout(timeout);
62
- }
67
+ });
63
68
  }
64
69
  async postApiPath(path, payload) {
65
70
  if (/^https?:\/\//i.test(path) || path.includes("..")) {
package/dist/config.js CHANGED
@@ -24,6 +24,20 @@ function optionalBooleanEnv(name, fallback) {
24
24
  return false;
25
25
  throw new Error(`${name} must be a boolean`);
26
26
  }
27
+ function positiveIntEnv(name, fallback) {
28
+ const value = Number(optionalEnv(name, String(fallback)));
29
+ if (!Number.isInteger(value) || value <= 0) {
30
+ throw new Error(`${name} must be a positive integer`);
31
+ }
32
+ return value;
33
+ }
34
+ function nonNegativeIntEnv(name, fallback) {
35
+ const value = Number(optionalEnv(name, String(fallback)));
36
+ if (!Number.isInteger(value) || value < 0) {
37
+ throw new Error(`${name} must be a non-negative integer`);
38
+ }
39
+ return value;
40
+ }
27
41
  function actionSearchModeEnv(name, fallback) {
28
42
  const value = process.env[name]?.trim().toLowerCase();
29
43
  if (!value)
@@ -77,10 +91,9 @@ function assertExpectedTokenValue(label, actual, expected) {
77
91
  throw new Error(`${label} mismatch for MES_AGENT_TOKEN: expected ${expected}, got ${actual ?? "<missing>"}`);
78
92
  }
79
93
  export function loadConfig() {
80
- const timeoutMs = Number(optionalEnv("MES_TIMEOUT_MS", "30000"));
81
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
82
- throw new Error("MES_TIMEOUT_MS must be a positive number");
83
- }
94
+ const timeoutMs = positiveIntEnv("MES_TIMEOUT_MS", 30000);
95
+ const maxConcurrentRequests = positiveIntEnv("MES_MAX_CONCURRENT_REQUESTS", 4);
96
+ const maxQueuedRequests = nonNegativeIntEnv("MES_MAX_QUEUED_REQUESTS", 8);
84
97
  const actionCacheTtlMs = Number(optionalEnv("MES_ACTION_CACHE_TTL_MS", "43200000"));
85
98
  if (!Number.isFinite(actionCacheTtlMs) || actionCacheTtlMs <= 0) {
86
99
  throw new Error("MES_ACTION_CACHE_TTL_MS must be a positive number");
@@ -98,6 +111,8 @@ export function loadConfig() {
98
111
  mesApiPrefix: optionalEnv("MES_API_PREFIX", "/api").replace(/\/+$/, ""),
99
112
  mesAgentToken,
100
113
  timeoutMs,
114
+ maxConcurrentRequests,
115
+ maxQueuedRequests,
101
116
  registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
102
117
  actionSearchMode: actionSearchModeEnv("MES_ACTION_SEARCH", "lexical"),
103
118
  actionCacheTtlMs,
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ function logStartupDiagnostics(config) {
28
28
  `[mes-mcp] token username=${config.tokenInfo.username ?? "<unknown>"} apiKeyId=${maskId(config.tokenInfo.apiKeyId)} agentClientId=${config.tokenInfo.agentClientId ?? "<unknown>"} expiresAt=${formatUnixSeconds(config.tokenInfo.expiresAt)}`,
29
29
  `[mes-mcp] dynamicTools=${config.registerDynamicTools ? "enabled" : "disabled"} (${config.registerDynamicTools ? "mes.<actionCode> tools will be registered" : "use mes_action.list/detail/execute; set MES_REGISTER_DYNAMIC_TOOLS=true only when the client needs expanded tools"})`,
30
30
  `[mes-mcp] actionSearch=${config.actionSearchMode} (mes_action.list 按相关性检索动作目录;hybrid/semantic 需安装可选依赖 @huggingface/transformers,否则自动退化为 lexical)`,
31
+ `[mes-mcp] requestConcurrency=${config.maxConcurrentRequests} queued=${config.maxQueuedRequests}`,
31
32
  ];
32
33
  if (config.expectedUsername) {
33
34
  lines.push(`[mes-mcp] expected username=${config.expectedUsername}`);
@@ -51,6 +51,11 @@ function errorMessage(error) {
51
51
  }
52
52
  function errorHints(message) {
53
53
  const hints = [];
54
+ if (message.includes("并发保护") ||
55
+ message.includes("并发请求已达上限") ||
56
+ message.includes("写入请求正在执行")) {
57
+ hints.push("MES/MCP 正在保护数据库并发;不要立即并行重试,稍后再重试或拆分为串行步骤。");
58
+ }
54
59
  if (/pageSize/i.test(message)) {
55
60
  hints.push("MES 分页上限是 100;MCP 会对 mes_api.read 和 mes_action.list 自动裁剪 pageSize。");
56
61
  }
@@ -148,7 +153,9 @@ function normalizeDynamicArgs(args) {
148
153
  return {
149
154
  executionMode,
150
155
  idempotencyKey,
151
- payload: payload ?? rest,
156
+ // 兼容 MCP 客户端把 DTO 字段放在顶层、同时附带 payload 的调用形态。
157
+ // payload 中的同名字段优先,避免显式请求体被顶层兼容字段覆盖。
158
+ payload: { ...rest, ...(payload ?? {}) },
152
159
  };
153
160
  }
154
161
  function buildIdempotencyKey(actionCode) {
@@ -497,7 +504,8 @@ function registerGenericActionTools(server, client, searchService) {
497
504
  (shouldEnsureIdempotencyKey(action, executionMode)
498
505
  ? buildIdempotencyKey(actionCode)
499
506
  : undefined),
500
- payload: payload ?? rest,
507
+ // 与动态工具保持一致:顶层 DTO 字段不能因为存在 payload 而丢失。
508
+ payload: { ...rest, ...(payload ?? {}) },
501
509
  });
502
510
  return formatToolResult(result);
503
511
  });
@@ -0,0 +1,49 @@
1
+ export class ConcurrencyLimiter {
2
+ maxConcurrent;
3
+ maxQueue;
4
+ active = 0;
5
+ queue = [];
6
+ constructor(maxConcurrent, maxQueue) {
7
+ this.maxConcurrent = maxConcurrent;
8
+ this.maxQueue = maxQueue;
9
+ }
10
+ async run(task) {
11
+ await this.acquire();
12
+ try {
13
+ return await task();
14
+ }
15
+ finally {
16
+ this.release();
17
+ }
18
+ }
19
+ acquire() {
20
+ if (this.active < this.maxConcurrent) {
21
+ this.active += 1;
22
+ return Promise.resolve();
23
+ }
24
+ if (this.queue.length >= this.maxQueue) {
25
+ return Promise.reject(new Error("MES MCP 并发保护已触发,请稍后重试;请求未发送到 MES"));
26
+ }
27
+ return new Promise((resolve, reject) => {
28
+ this.queue.push({
29
+ grant: () => {
30
+ this.active += 1;
31
+ resolve();
32
+ },
33
+ reject,
34
+ });
35
+ });
36
+ }
37
+ release() {
38
+ this.active = Math.max(0, this.active - 1);
39
+ const next = this.queue.shift();
40
+ if (!next)
41
+ return;
42
+ try {
43
+ next.grant();
44
+ }
45
+ catch (error) {
46
+ next.reject(error);
47
+ }
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mes-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "description": "MES MCP adapter for Marvis and AI agents",
6
6
  "license": "UNLICENSED",