mes-mcp 0.3.1 → 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
- if (!options.refresh && this.businessActionsPromise) {
74
+ // 复用:非强制刷新 + 有缓存/在途 + (在途未定时间 仍新鲜)
75
+ if (!options.refresh &&
76
+ this.businessActionsPromise &&
77
+ (this.businessActionsFetchedAt === undefined || this.isCatalogFresh())) {
72
78
  return this.businessActionsPromise;
73
79
  }
74
- this.businessActionsPromise = this.fetchBusinessActions().catch((error) => {
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
- "动作目录有数百个动作。发现动作时用 mes_action.list,把你当前要做的事用一句自然语言意图传给 keyword(例如「销售单转生产计划」「采购入库」「报工」),它返回按相关性排序的 top-K 动作;再用 mes_action.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
  "销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、生产入库/库存确认、发货出库。",
@@ -37,10 +37,11 @@ export function registerMesResources(server, client) {
37
37
  "## 工具使用顺序",
38
38
  "",
39
39
  "1. 使用 `mes_action.list` 或 `mes://agent/actions` 发现当前凭证和账号权限允许的动作。",
40
- "2. 使用 `mes_action.detail` 查看目标动作的路径、权限、DTO 名称和输入 schema",
41
- "3. 只读查询使用 `mes_api.read` 调用普通业务只读 POST 接口。",
42
- "4. 写入、确认、下达、报工、质检、出入库等动作使用 `mes_action.execute` 或可选的 `mes.<动作编码>`。",
43
- "5. `MES_REGISTER_DYNAMIC_TOOLS=false` 是默认推荐配置;此时工具列表只有 `mes_api.read`、`mes_action.list`、`mes_action.detail`、`mes_action.execute` 等基础工具。只有客户端必须直观看到 `mes.<动作编码>` 时才开启动态工具展开。",
40
+ "2. 使用 `mes_action.detail` 查看目标动作的路径、权限、DTO 名称、输入 schema、字段归属和推荐工作流提示。",
41
+ "3. 如果业务链路或字段归属不清楚,使用 `mes_manual.search` / `mes_manual.get` / `mes_workflow.detail` 查询后端提供的 AI 操作手册索引。",
42
+ "4. 只读查询使用 `mes_api.read` 调用普通业务只读 POST 接口。",
43
+ "5. 写入、确认、下达、报工、质检、出入库等动作使用 `mes_action.execute` 或可选的 `mes.<动作编码>`。",
44
+ "6. `MES_REGISTER_DYNAMIC_TOOLS=false` 是默认推荐配置;此时工具列表包含 `mes_api.read`、`mes_action.list/detail/execute`、`mes_manual.search/get`、`mes_workflow.detail` 等基础工具。只有客户端必须直观看到 `mes.<动作编码>` 时才开启动态工具展开。",
44
45
  "",
45
46
  "## 写入规则",
46
47
  "",
@@ -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,9 +281,70 @@ 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
- description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称和后端提供的输入 schema",
347
+ description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称、输入 schema,以及后端提供的手册引用、字段归属和推荐工作流提示。",
272
348
  inputSchema: z.object({
273
349
  actionCode: z.string().min(1),
274
350
  }),
@@ -288,6 +364,100 @@ function registerGenericActionTools(server, client, searchService) {
288
364
  return formatToolResult(result);
289
365
  });
290
366
  });
367
+ server.registerTool("mes_manual.search", {
368
+ title: "Search MES operation manual",
369
+ description: "检索后端提供的 MES AI 操作手册索引,用于补充字段归属、前置动作、推荐流程和常见误用。适合在只看 action schema 不确定业务链路时调用。",
370
+ inputSchema: z
371
+ .object({
372
+ query: z.string().optional().describe("自然语言查询词"),
373
+ keyword: z.string().optional().describe("兼容字段:自然语言查询词"),
374
+ module: z.string().optional().describe("业务模块,如 schedule"),
375
+ actionCode: z
376
+ .string()
377
+ .optional()
378
+ .describe("相关动作编码,如 schedule.plan.auto"),
379
+ field: z
380
+ .string()
381
+ .optional()
382
+ .describe("字段名或字段归属,如 planStartDate"),
383
+ page: z.number().int().positive().optional(),
384
+ pageSize: z
385
+ .number()
386
+ .int()
387
+ .positive()
388
+ .max(MAX_MES_PAGE_SIZE)
389
+ .optional(),
390
+ })
391
+ .passthrough(),
392
+ annotations: {
393
+ readOnlyHint: true,
394
+ destructiveHint: false,
395
+ idempotentHint: true,
396
+ },
397
+ }, async (args) => {
398
+ return runTool({ toolName: "mes_manual.search" }, async () => {
399
+ const parsed = z
400
+ .object({
401
+ query: z.string().optional(),
402
+ keyword: z.string().optional(),
403
+ module: z.string().optional(),
404
+ actionCode: z.string().optional(),
405
+ field: z.string().optional(),
406
+ page: z.number().int().positive().optional(),
407
+ pageSize: z.number().int().positive().optional(),
408
+ })
409
+ .passthrough()
410
+ .parse(args ?? {});
411
+ const warnings = [];
412
+ const payload = normalizePageSize(parsed, warnings);
413
+ const result = await client.post("/integration/agent/manual/search", payload);
414
+ return formatToolResult(result, warnings);
415
+ });
416
+ });
417
+ server.registerTool("mes_manual.get", {
418
+ title: "Get MES operation manual section",
419
+ description: "按手册章节 ID 获取后端提供的 MES AI 操作手册详情,返回动作链、字段归属、步骤和常见误用。",
420
+ inputSchema: z.object({
421
+ id: z.string().min(1).describe("手册章节 ID"),
422
+ }),
423
+ annotations: {
424
+ readOnlyHint: true,
425
+ destructiveHint: false,
426
+ idempotentHint: true,
427
+ },
428
+ }, async (args) => {
429
+ return runTool({ toolName: "mes_manual.get" }, async () => {
430
+ const parsed = z
431
+ .object({
432
+ id: z.string().min(1),
433
+ })
434
+ .parse(args ?? {});
435
+ const result = await client.post("/integration/agent/manual/detail", parsed);
436
+ return formatToolResult(result);
437
+ });
438
+ });
439
+ server.registerTool("mes_workflow.detail", {
440
+ title: "Get MES recommended workflow",
441
+ description: "按工作流 ID 获取后端推荐的 MES 业务动作链。用于需要多个动作配合完成的场景,例如先更新生产计划开始日期,再从生产计划排产。",
442
+ inputSchema: z.object({
443
+ id: z.string().min(1).describe("业务工作流 ID"),
444
+ }),
445
+ annotations: {
446
+ readOnlyHint: true,
447
+ destructiveHint: false,
448
+ idempotentHint: true,
449
+ },
450
+ }, async (args) => {
451
+ return runTool({ toolName: "mes_workflow.detail" }, async () => {
452
+ const parsed = z
453
+ .object({
454
+ id: z.string().min(1),
455
+ })
456
+ .parse(args ?? {});
457
+ const result = await client.post("/integration/agent/workflows/detail", parsed);
458
+ return formatToolResult(result);
459
+ });
460
+ });
291
461
  server.registerTool("mes_action.execute", {
292
462
  title: "Execute MES business action",
293
463
  description: "执行一个由 mes_action.list 发现的 MES 页面业务动作。后端复用页面 Controller、DTO、权限、租户、状态机和调用日志。返回体是执行信封:真实业务结果在 data 字段;confirm_required 返回待确认 invocationId,必须由同一公司/租户下有权限的账号审批。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mes-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "MES MCP adapter for Marvis and AI agents",
6
6
  "license": "UNLICENSED",