mes-mcp 0.3.3 → 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 +2 -0
- package/README.md +2 -0
- package/dist/clients/mes-agent-api.client.js +35 -24
- package/dist/config.js +19 -4
- package/dist/discovery/action-search.service.js +17 -3
- package/dist/index.js +1 -0
- package/dist/tools/register-tools.js +13 -2
- package/dist/utils/concurrency-limiter.js +49 -0
- package/package.json +1 -1
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
|
@@ -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)) {
|
|
@@ -29,36 +30,41 @@ export class MesAgentApiClient {
|
|
|
29
30
|
// 动作目录缓存最近成功刷新时间;驱动每日首用/TTL 自动刷新,并对外报告。
|
|
30
31
|
// undefined 且 promise 存在 = 拉取在途(并发复用同一 promise,避免重复请求)。
|
|
31
32
|
businessActionsFetchedAt;
|
|
33
|
+
businessActionsVersion = 0;
|
|
34
|
+
requestLimiter;
|
|
32
35
|
constructor(config) {
|
|
33
36
|
this.config = config;
|
|
37
|
+
this.requestLimiter = new ConcurrencyLimiter(config.maxConcurrentRequests, config.maxQueuedRequests);
|
|
34
38
|
}
|
|
35
39
|
async post(path, payload) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
|
53
63
|
}
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timeout);
|
|
56
66
|
}
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
clearTimeout(timeout);
|
|
61
|
-
}
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
async postApiPath(path, payload) {
|
|
64
70
|
if (/^https?:\/\//i.test(path) || path.includes("..")) {
|
|
@@ -82,6 +88,7 @@ export class MesAgentApiClient {
|
|
|
82
88
|
this.businessActionsPromise = this.fetchBusinessActions()
|
|
83
89
|
.then((actions) => {
|
|
84
90
|
this.businessActionsFetchedAt = new Date();
|
|
91
|
+
this.businessActionsVersion += 1;
|
|
85
92
|
return actions;
|
|
86
93
|
})
|
|
87
94
|
.catch((error) => {
|
|
@@ -111,6 +118,10 @@ export class MesAgentApiClient {
|
|
|
111
118
|
? this.businessActionsFetchedAt.toISOString()
|
|
112
119
|
: null;
|
|
113
120
|
}
|
|
121
|
+
/** 动作目录缓存版本;每次成功刷新递增,用于让下游检索索引失效。 */
|
|
122
|
+
businessActionsCatalogVersion() {
|
|
123
|
+
return this.businessActionsVersion;
|
|
124
|
+
}
|
|
114
125
|
async getBusinessAction(actionCode) {
|
|
115
126
|
const actions = await this.listBusinessActions();
|
|
116
127
|
const cached = actions.find((action) => action.actionCode === actionCode);
|
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 =
|
|
81
|
-
|
|
82
|
-
|
|
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,
|
|
@@ -19,16 +19,30 @@ export class ActionSearchService {
|
|
|
19
19
|
this.client.listBusinessActions(),
|
|
20
20
|
createEmbeddingProvider(this.mode),
|
|
21
21
|
]);
|
|
22
|
-
return
|
|
22
|
+
return {
|
|
23
|
+
index: new ActionSearchIndex(actions, provider),
|
|
24
|
+
version: this.client.businessActionsCatalogVersion(),
|
|
25
|
+
};
|
|
23
26
|
}
|
|
24
|
-
async
|
|
27
|
+
async getIndex() {
|
|
25
28
|
if (!this.indexPromise) {
|
|
26
29
|
this.indexPromise = this.buildIndex().catch((error) => {
|
|
27
30
|
this.indexPromise = undefined;
|
|
28
31
|
throw error;
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
let indexed = await this.indexPromise;
|
|
35
|
+
if (indexed.version !== this.client.businessActionsCatalogVersion()) {
|
|
36
|
+
this.indexPromise = this.buildIndex().catch((error) => {
|
|
37
|
+
this.indexPromise = undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
indexed = await this.indexPromise;
|
|
41
|
+
}
|
|
42
|
+
return indexed.index;
|
|
43
|
+
}
|
|
44
|
+
async search(query, options) {
|
|
45
|
+
const index = await this.getIndex();
|
|
32
46
|
const results = await index.search(query, options);
|
|
33
47
|
return { total: index.size, results };
|
|
34
48
|
}
|
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
|
-
|
|
156
|
+
// 兼容 MCP 客户端把 DTO 字段放在顶层、同时附带 payload 的调用形态。
|
|
157
|
+
// payload 中的同名字段优先,避免显式请求体被顶层兼容字段覆盖。
|
|
158
|
+
payload: { ...rest, ...(payload ?? {}) },
|
|
152
159
|
};
|
|
153
160
|
}
|
|
154
161
|
function buildIdempotencyKey(actionCode) {
|
|
@@ -275,6 +282,8 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
275
282
|
return formatToolResult({
|
|
276
283
|
query: keyword || null,
|
|
277
284
|
ranked: Boolean(keyword),
|
|
285
|
+
catalogVersion: client.businessActionsCatalogVersion(),
|
|
286
|
+
refreshedAt: client.businessActionsRefreshedAt(),
|
|
278
287
|
total,
|
|
279
288
|
returned: results.length,
|
|
280
289
|
list: results.map((r) => listActionView(r.action, r.score)),
|
|
@@ -334,6 +343,7 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
334
343
|
}));
|
|
335
344
|
return formatToolResult({
|
|
336
345
|
refreshedAt: client.businessActionsRefreshedAt(),
|
|
346
|
+
catalogVersion: client.businessActionsCatalogVersion(),
|
|
337
347
|
total: filtered.length,
|
|
338
348
|
moduleCount: modules.length,
|
|
339
349
|
filteredModule: moduleFilter ?? null,
|
|
@@ -494,7 +504,8 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
494
504
|
(shouldEnsureIdempotencyKey(action, executionMode)
|
|
495
505
|
? buildIdempotencyKey(actionCode)
|
|
496
506
|
: undefined),
|
|
497
|
-
|
|
507
|
+
// 与动态工具保持一致:顶层 DTO 字段不能因为存在 payload 而丢失。
|
|
508
|
+
payload: { ...rest, ...(payload ?? {}) },
|
|
498
509
|
});
|
|
499
510
|
return formatToolResult(result);
|
|
500
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
|
+
}
|