mes-mcp 0.3.2 → 0.3.4

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,10 @@ function buildMesErrorMessage(path, status, message) {
26
26
  export class MesAgentApiClient {
27
27
  config;
28
28
  businessActionsPromise;
29
+ // 动作目录缓存最近成功刷新时间;驱动每日首用/TTL 自动刷新,并对外报告。
30
+ // undefined 且 promise 存在 = 拉取在途(并发复用同一 promise,避免重复请求)。
31
+ businessActionsFetchedAt;
32
+ businessActionsVersion = 0;
29
33
  constructor(config) {
30
34
  this.config = config;
31
35
  }
@@ -68,15 +72,51 @@ export class MesAgentApiClient {
68
72
  return this.post(normalized, payload);
69
73
  }
70
74
  async listBusinessActions(options = {}) {
71
- if (!options.refresh && this.businessActionsPromise) {
75
+ // 复用:非强制刷新 + 有缓存/在途 + (在途未定时间 仍新鲜)
76
+ if (!options.refresh &&
77
+ this.businessActionsPromise &&
78
+ (this.businessActionsFetchedAt === undefined || this.isCatalogFresh())) {
72
79
  return this.businessActionsPromise;
73
80
  }
74
- this.businessActionsPromise = this.fetchBusinessActions().catch((error) => {
81
+ // 开新拉取:先清 fetchedAt 标记「在途」,并发调用复用同一 promise 不重复请求
82
+ this.businessActionsFetchedAt = undefined;
83
+ this.businessActionsPromise = this.fetchBusinessActions()
84
+ .then((actions) => {
85
+ this.businessActionsFetchedAt = new Date();
86
+ this.businessActionsVersion += 1;
87
+ return actions;
88
+ })
89
+ .catch((error) => {
75
90
  this.businessActionsPromise = undefined;
91
+ this.businessActionsFetchedAt = undefined;
76
92
  throw error;
77
93
  });
78
94
  return this.businessActionsPromise;
79
95
  }
96
+ /**
97
+ * 动作目录缓存是否仍新鲜:跨自然日或超过 TTL 视为过期。
98
+ * 跨自然日过期即「每天第一次使用自动拉最新目录」。
99
+ */
100
+ isCatalogFresh() {
101
+ if (!this.businessActionsFetchedAt)
102
+ return false;
103
+ const now = new Date();
104
+ if (now.toDateString() !== this.businessActionsFetchedAt.toDateString()) {
105
+ return false;
106
+ }
107
+ return (now.getTime() - this.businessActionsFetchedAt.getTime() <
108
+ this.config.actionCacheTtlMs);
109
+ }
110
+ /** 动作目录最近一次成功刷新时间(ISO);未拉取过返回 null。 */
111
+ businessActionsRefreshedAt() {
112
+ return this.businessActionsFetchedAt
113
+ ? this.businessActionsFetchedAt.toISOString()
114
+ : null;
115
+ }
116
+ /** 动作目录缓存版本;每次成功刷新递增,用于让下游检索索引失效。 */
117
+ businessActionsCatalogVersion() {
118
+ return this.businessActionsVersion;
119
+ }
80
120
  async getBusinessAction(actionCode) {
81
121
  const actions = await this.listBusinessActions();
82
122
  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,
@@ -19,16 +19,30 @@ export class ActionSearchService {
19
19
  this.client.listBusinessActions(),
20
20
  createEmbeddingProvider(this.mode),
21
21
  ]);
22
- return new ActionSearchIndex(actions, provider);
22
+ return {
23
+ index: new ActionSearchIndex(actions, provider),
24
+ version: this.client.businessActionsCatalogVersion(),
25
+ };
23
26
  }
24
- async search(query, options) {
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
- const index = await this.indexPromise;
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
  }
@@ -15,7 +15,8 @@ export function registerMesPrompts(server) {
15
15
  text: [
16
16
  "你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
17
17
  "不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
18
- "动作目录有数百个动作。发现动作时用 mes_action.list,把你当前要做的事用一句自然语言意图传给 keyword(例如「销售单转生产计划」「采购入库」「报工」),它返回按相关性排序的 top-K 动作;再用 mes_action.detail 查看字段。遇到前置步骤、字段归属或业务口径不清楚时,先用 mes_manual.search 或 mes_workflow.detail 查后端手册索引,最后用 mes_action.execute 执行。不要不带意图地翻页或猜路径,也不要依赖工具列表里必须存在 mes.<动作编码>。",
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",
@@ -260,12 +275,76 @@ function registerGenericActionTools(server, client, searchService) {
260
275
  return formatToolResult({
261
276
  query: keyword || null,
262
277
  ranked: Boolean(keyword),
278
+ catalogVersion: client.businessActionsCatalogVersion(),
279
+ refreshedAt: client.businessActionsRefreshedAt(),
263
280
  total,
264
281
  returned: results.length,
265
282
  list: results.map((r) => listActionView(r.action, r.score)),
266
283
  }, warnings);
267
284
  });
268
285
  });
286
+ server.registerTool("mes_action.catalog", {
287
+ title: "Load full MES action catalog",
288
+ description: "拉取并刷新当前 AI 凭证和账号权限内【全部】可执行的 MES 页面业务动作紧凑目录(按模块分组),用于把可用动作一次性加载进上下文。当用户说「更新一下 mes / 看看有哪些动作」、你不确定有哪些动作、或距上次加载较久怀疑目录已变时调用。只返回动作码/名称/模块/操作/读写/一句话用途;执行某动作前用 mes_action.detail 看字段,按意图精准找某个动作用 mes_action.list。",
289
+ inputSchema: z
290
+ .object({
291
+ module: z
292
+ .string()
293
+ .optional()
294
+ .describe("只加载某模块的动作(如 sales/purchase/finance),控上下文;不传=全部模块"),
295
+ })
296
+ .passthrough(),
297
+ annotations: {
298
+ readOnlyHint: true,
299
+ destructiveHint: false,
300
+ idempotentHint: true,
301
+ },
302
+ }, async (args) => {
303
+ return runTool({ toolName: "mes_action.catalog" }, async () => {
304
+ const parsed = z
305
+ .object({ module: z.string().optional() })
306
+ .passthrough()
307
+ .parse(args ?? {});
308
+ const moduleFilter = parsed.module?.trim() || undefined;
309
+ // 强制刷新,保证「更新一下 mes」拿到最新目录
310
+ const all = await client.listBusinessActions({ refresh: true });
311
+ const filtered = moduleFilter
312
+ ? all.filter((a) => a.module === moduleFilter)
313
+ : all;
314
+ const warnings = [];
315
+ if (moduleFilter && filtered.length === 0) {
316
+ warnings.push({
317
+ code: "module_no_match",
318
+ message: `模块「${moduleFilter}」下没有可用动作;不传 module 可加载全部模块,或用 mes_action.list 按意图检索。`,
319
+ });
320
+ }
321
+ const byModule = new Map();
322
+ for (const action of filtered) {
323
+ const list = byModule.get(action.module) ?? [];
324
+ list.push(action);
325
+ byModule.set(action.module, list);
326
+ }
327
+ const modules = [...byModule.entries()]
328
+ .sort((a, b) => a[0].localeCompare(b[0]))
329
+ .map(([module, actions]) => ({
330
+ module,
331
+ count: actions.length,
332
+ actions: actions
333
+ .slice()
334
+ .sort((a, b) => a.actionCode.localeCompare(b.actionCode))
335
+ .map(catalogActionView),
336
+ }));
337
+ return formatToolResult({
338
+ refreshedAt: client.businessActionsRefreshedAt(),
339
+ catalogVersion: client.businessActionsCatalogVersion(),
340
+ total: filtered.length,
341
+ moduleCount: modules.length,
342
+ filteredModule: moduleFilter ?? null,
343
+ modules,
344
+ note: "以上为当前凭证权限内的全部可用 MES 动作紧凑目录。执行前用 mes_action.detail 看字段/schema;按意图精准找用 mes_action.list。",
345
+ }, warnings);
346
+ });
347
+ });
269
348
  server.registerTool("mes_action.detail", {
270
349
  title: "Get MES business action detail",
271
350
  description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称、输入 schema,以及后端提供的手册引用、字段归属和推荐工作流提示。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mes-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "MES MCP adapter for Marvis and AI agents",
6
6
  "license": "UNLICENSED",