mes-mcp 0.3.2 → 0.3.3
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,9 @@ 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
|
+
# 动作目录缓存 TTL(毫秒,兜底),默认 12h。另外跨自然日必刷新:
|
|
6
|
+
# 每天第一次调用 mes_action.* 时自动拉最新动作目录;用户也可用 mes_action.catalog 手动强刷。
|
|
7
|
+
MES_ACTION_CACHE_TTL_MS=43200000
|
|
5
8
|
MES_REGISTER_DYNAMIC_TOOLS=false
|
|
6
9
|
# mes_action.list 动作检索模式:
|
|
7
10
|
# lexical(默认,推荐)= CJK 词元 BM25 + 双语领域词扩展,离线、零额外依赖、确定性;
|
|
@@ -26,6 +26,9 @@ function buildMesErrorMessage(path, status, message) {
|
|
|
26
26
|
export class MesAgentApiClient {
|
|
27
27
|
config;
|
|
28
28
|
businessActionsPromise;
|
|
29
|
+
// 动作目录缓存最近成功刷新时间;驱动每日首用/TTL 自动刷新,并对外报告。
|
|
30
|
+
// undefined 且 promise 存在 = 拉取在途(并发复用同一 promise,避免重复请求)。
|
|
31
|
+
businessActionsFetchedAt;
|
|
29
32
|
constructor(config) {
|
|
30
33
|
this.config = config;
|
|
31
34
|
}
|
|
@@ -68,15 +71,46 @@ export class MesAgentApiClient {
|
|
|
68
71
|
return this.post(normalized, payload);
|
|
69
72
|
}
|
|
70
73
|
async listBusinessActions(options = {}) {
|
|
71
|
-
|
|
74
|
+
// 复用:非强制刷新 + 有缓存/在途 + (在途未定时间 或 仍新鲜)
|
|
75
|
+
if (!options.refresh &&
|
|
76
|
+
this.businessActionsPromise &&
|
|
77
|
+
(this.businessActionsFetchedAt === undefined || this.isCatalogFresh())) {
|
|
72
78
|
return this.businessActionsPromise;
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
// 开新拉取:先清 fetchedAt 标记「在途」,并发调用复用同一 promise 不重复请求
|
|
81
|
+
this.businessActionsFetchedAt = undefined;
|
|
82
|
+
this.businessActionsPromise = this.fetchBusinessActions()
|
|
83
|
+
.then((actions) => {
|
|
84
|
+
this.businessActionsFetchedAt = new Date();
|
|
85
|
+
return actions;
|
|
86
|
+
})
|
|
87
|
+
.catch((error) => {
|
|
75
88
|
this.businessActionsPromise = undefined;
|
|
89
|
+
this.businessActionsFetchedAt = undefined;
|
|
76
90
|
throw error;
|
|
77
91
|
});
|
|
78
92
|
return this.businessActionsPromise;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* 动作目录缓存是否仍新鲜:跨自然日或超过 TTL 视为过期。
|
|
96
|
+
* 跨自然日过期即「每天第一次使用自动拉最新目录」。
|
|
97
|
+
*/
|
|
98
|
+
isCatalogFresh() {
|
|
99
|
+
if (!this.businessActionsFetchedAt)
|
|
100
|
+
return false;
|
|
101
|
+
const now = new Date();
|
|
102
|
+
if (now.toDateString() !== this.businessActionsFetchedAt.toDateString()) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return (now.getTime() - this.businessActionsFetchedAt.getTime() <
|
|
106
|
+
this.config.actionCacheTtlMs);
|
|
107
|
+
}
|
|
108
|
+
/** 动作目录最近一次成功刷新时间(ISO);未拉取过返回 null。 */
|
|
109
|
+
businessActionsRefreshedAt() {
|
|
110
|
+
return this.businessActionsFetchedAt
|
|
111
|
+
? this.businessActionsFetchedAt.toISOString()
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
80
114
|
async getBusinessAction(actionCode) {
|
|
81
115
|
const actions = await this.listBusinessActions();
|
|
82
116
|
const cached = actions.find((action) => action.actionCode === actionCode);
|
package/dist/config.js
CHANGED
|
@@ -81,6 +81,10 @@ export function loadConfig() {
|
|
|
81
81
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
82
82
|
throw new Error("MES_TIMEOUT_MS must be a positive number");
|
|
83
83
|
}
|
|
84
|
+
const actionCacheTtlMs = Number(optionalEnv("MES_ACTION_CACHE_TTL_MS", "43200000"));
|
|
85
|
+
if (!Number.isFinite(actionCacheTtlMs) || actionCacheTtlMs <= 0) {
|
|
86
|
+
throw new Error("MES_ACTION_CACHE_TTL_MS must be a positive number");
|
|
87
|
+
}
|
|
84
88
|
const mesAgentToken = requiredEnv("MES_AGENT_TOKEN");
|
|
85
89
|
const tokenInfo = decodeAgentToken(mesAgentToken);
|
|
86
90
|
const expectedUsername = optionalRawEnv("MES_EXPECTED_USERNAME");
|
|
@@ -96,6 +100,7 @@ export function loadConfig() {
|
|
|
96
100
|
timeoutMs,
|
|
97
101
|
registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
|
|
98
102
|
actionSearchMode: actionSearchModeEnv("MES_ACTION_SEARCH", "lexical"),
|
|
103
|
+
actionCacheTtlMs,
|
|
99
104
|
tokenInfo,
|
|
100
105
|
expectedUsername,
|
|
101
106
|
expectedAgentClientId,
|
|
@@ -15,7 +15,8 @@ export function registerMesPrompts(server) {
|
|
|
15
15
|
text: [
|
|
16
16
|
"你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
|
|
17
17
|
"不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
|
|
18
|
-
"
|
|
18
|
+
"处理 MES 任务前、或用户说「更新一下 mes / 看看有哪些动作」时,先调用 mes_action.catalog 加载当前账号权限内的全部动作紧凑目录(它会强制刷新;mcp 每天第一次使用也会自动拉最新目录),把可用动作装进上下文。",
|
|
19
|
+
"动作目录有数百个动作。已加载目录后要执行具体某件事,用 mes_action.list 把意图用一句自然语言传给 keyword(例如「销售单转生产计划」「采购入库」「报工」)拿到相关性 top-K,再用 mes_action.detail 查看字段。遇到前置步骤、字段归属或业务口径不清楚时,先用 mes_manual.search 或 mes_workflow.detail 查后端手册索引,最后用 mes_action.execute 执行。不要不带意图地翻页或猜路径,也不要依赖工具列表里必须存在 mes.<动作编码>。",
|
|
19
20
|
"mes_action.execute 返回的是执行信封,真实业务返回在 data 字段;confirm_required 产生的 invocationId 必须由同一公司/租户下有权限的账号审批。",
|
|
20
21
|
"mes_api.read 只用于普通业务只读接口,pageSize 最大 100;如果返回 _mcpWarnings 或提示需要动态动作,请按提示调整。",
|
|
21
22
|
"销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、生产入库/库存确认、发货出库。",
|
|
@@ -193,6 +193,21 @@ function listActionView(action, relevance) {
|
|
|
193
193
|
relevance: Number(relevance.toFixed(4)),
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
+
// 全量目录用的紧凑视图:只保留识别动作所需字段,不含 path/method/权限/schema,控上下文预算。
|
|
197
|
+
function catalogActionView(action) {
|
|
198
|
+
const brief = action.description?.trim();
|
|
199
|
+
return {
|
|
200
|
+
actionCode: action.actionCode,
|
|
201
|
+
title: action.title,
|
|
202
|
+
operation: action.operation,
|
|
203
|
+
resource: action.resource,
|
|
204
|
+
isWrite: action.isWrite,
|
|
205
|
+
riskLevel: action.riskLevel,
|
|
206
|
+
...(brief
|
|
207
|
+
? { brief: brief.length > 80 ? `${brief.slice(0, 80)}…` : brief }
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
196
211
|
function registerGenericActionTools(server, client, searchService) {
|
|
197
212
|
server.registerTool("mes_action.list", {
|
|
198
213
|
title: "Search MES business actions",
|
|
@@ -266,6 +281,67 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
266
281
|
}, warnings);
|
|
267
282
|
});
|
|
268
283
|
});
|
|
284
|
+
server.registerTool("mes_action.catalog", {
|
|
285
|
+
title: "Load full MES action catalog",
|
|
286
|
+
description: "拉取并刷新当前 AI 凭证和账号权限内【全部】可执行的 MES 页面业务动作紧凑目录(按模块分组),用于把可用动作一次性加载进上下文。当用户说「更新一下 mes / 看看有哪些动作」、你不确定有哪些动作、或距上次加载较久怀疑目录已变时调用。只返回动作码/名称/模块/操作/读写/一句话用途;执行某动作前用 mes_action.detail 看字段,按意图精准找某个动作用 mes_action.list。",
|
|
287
|
+
inputSchema: z
|
|
288
|
+
.object({
|
|
289
|
+
module: z
|
|
290
|
+
.string()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("只加载某模块的动作(如 sales/purchase/finance),控上下文;不传=全部模块"),
|
|
293
|
+
})
|
|
294
|
+
.passthrough(),
|
|
295
|
+
annotations: {
|
|
296
|
+
readOnlyHint: true,
|
|
297
|
+
destructiveHint: false,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
},
|
|
300
|
+
}, async (args) => {
|
|
301
|
+
return runTool({ toolName: "mes_action.catalog" }, async () => {
|
|
302
|
+
const parsed = z
|
|
303
|
+
.object({ module: z.string().optional() })
|
|
304
|
+
.passthrough()
|
|
305
|
+
.parse(args ?? {});
|
|
306
|
+
const moduleFilter = parsed.module?.trim() || undefined;
|
|
307
|
+
// 强制刷新,保证「更新一下 mes」拿到最新目录
|
|
308
|
+
const all = await client.listBusinessActions({ refresh: true });
|
|
309
|
+
const filtered = moduleFilter
|
|
310
|
+
? all.filter((a) => a.module === moduleFilter)
|
|
311
|
+
: all;
|
|
312
|
+
const warnings = [];
|
|
313
|
+
if (moduleFilter && filtered.length === 0) {
|
|
314
|
+
warnings.push({
|
|
315
|
+
code: "module_no_match",
|
|
316
|
+
message: `模块「${moduleFilter}」下没有可用动作;不传 module 可加载全部模块,或用 mes_action.list 按意图检索。`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const byModule = new Map();
|
|
320
|
+
for (const action of filtered) {
|
|
321
|
+
const list = byModule.get(action.module) ?? [];
|
|
322
|
+
list.push(action);
|
|
323
|
+
byModule.set(action.module, list);
|
|
324
|
+
}
|
|
325
|
+
const modules = [...byModule.entries()]
|
|
326
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
327
|
+
.map(([module, actions]) => ({
|
|
328
|
+
module,
|
|
329
|
+
count: actions.length,
|
|
330
|
+
actions: actions
|
|
331
|
+
.slice()
|
|
332
|
+
.sort((a, b) => a.actionCode.localeCompare(b.actionCode))
|
|
333
|
+
.map(catalogActionView),
|
|
334
|
+
}));
|
|
335
|
+
return formatToolResult({
|
|
336
|
+
refreshedAt: client.businessActionsRefreshedAt(),
|
|
337
|
+
total: filtered.length,
|
|
338
|
+
moduleCount: modules.length,
|
|
339
|
+
filteredModule: moduleFilter ?? null,
|
|
340
|
+
modules,
|
|
341
|
+
note: "以上为当前凭证权限内的全部可用 MES 动作紧凑目录。执行前用 mes_action.detail 看字段/schema;按意图精准找用 mes_action.list。",
|
|
342
|
+
}, warnings);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
269
345
|
server.registerTool("mes_action.detail", {
|
|
270
346
|
title: "Get MES business action detail",
|
|
271
347
|
description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称、输入 schema,以及后端提供的手册引用、字段归属和推荐工作流提示。",
|