mes-mcp 0.1.0 → 0.2.1

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,3 +2,4 @@ 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_REGISTER_DYNAMIC_TOOLS=false
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MES MCP
2
2
 
3
- MES MCP 是 MES 开放集成体系的 MCP 协议适配层,用于 Marvis 或其他 AI Agent 调用 MES 的 Agent OpenAPI 合同。
3
+ MES MCP 是 MES 开放集成体系的 MCP 协议适配层,用于 Marvis 或其他 AI Agent 调用 MES 的后端动态业务动作目录。
4
4
 
5
5
  ## 边界
6
6
 
@@ -18,8 +18,11 @@ 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_REGISTER_DYNAMIC_TOOLS=false
21
22
  ```
22
23
 
24
+ `MES_REGISTER_DYNAMIC_TOOLS` 默认关闭。关闭时 MCP 只注册稳定接口工具,通过后端动作目录发现并执行全部授权业务能力;打开后会把所有授权页面动作额外展开成 `mes.<动作编码>` 工具,适合明确需要工具直出且客户端能承载大量工具的场景。
25
+
23
26
  ## 开发
24
27
 
25
28
  ```bash
@@ -30,26 +33,26 @@ npm run dev
30
33
 
31
34
  ## 工具
32
35
 
33
- 工具名与 `mes-node` Agent 合同编码一致:
34
-
35
- - `document_intake.parse`
36
- - `customer.match`
37
- - `material.match`
38
- - `sale_order.create`
39
- - `sale_order.confirm`
40
- - `production_plan.create_from_sale_order`
41
- - `work_order.create_from_plan`
42
- - `work_order.release`
43
- - `sale_order.query`
44
- - `work_order.query`
45
- - `inventory.query`
46
- - `mes_api.read`:按当前 MES 账号权限读取普通业务接口,例如 `/sale-order/list`、`/material/detail`。后端只放行只读类接口,写入动作必须使用专门 Agent 工具;读取调用会在 MES AI 调用日志里记录路径、请求摘要和结果摘要。
36
+ 默认使用后端动态业务动作接口:
37
+
38
+ - `mes_action.list`:查询当前凭证和当前账号权限允许调用的 MES 页面业务动作。
39
+ - `mes_action.detail`:查看某个业务动作的路径、权限、DTO 名称和输入 schema。
40
+ - `mes_action.execute`:按动作编码执行业务动作。
41
+ - `mes.<动作编码>`:仅在 `MES_REGISTER_DYNAMIC_TOOLS=true` 时,从 `mes-node` 自动发现并注册的动态工具,例如 `mes.sale_order.create`、`mes.approval.handle`、`mes.quality_inspection.submit`、`mes.sale_delivery.confirm`。
42
+
43
+ 动态动作只调用 `mes-node` 的 `/integration/agent/actions/*` 接口。MCP 不保存字段 schema,不复制状态机,不做业务默认值;后端会复用页面 Controller、DTO、权限、租户、状态、审批、库存、质量和 AI 调用日志。默认不把全部动作展开成独立工具,是为了避免 MCP 客户端一次性加载数百个工具;能力覆盖仍以 `mes_action.list/detail/execute` 为准。
44
+
45
+ 写入类动作通过 `confirm_required` 或 `commit` 执行时需要幂等键。调用方没有显式传入时,`mes-mcp` 会为当前工具调用生成一条集成幂等键;需要跨重试严格复用同一幂等键的批处理场景,仍建议调用方显式传入 `idempotencyKey`。
46
+
47
+ - `mes_api.read`:按当前 MES 账号权限读取普通业务接口,例如 `/sale-order/list`、`/material/detail`。后端只放行只读类接口,写入动作必须使用动态业务动作;读取调用会在 MES AI 调用日志里记录路径、请求摘要和结果摘要。
47
48
 
48
49
  写入类工具支持 `executionMode`:
49
50
 
50
51
  - `preview`:只预览,不写入。
51
- - `confirm_required`:返回待确认内容,不写入。
52
- - `commit`:提交到 MES,按当前账号权限、工具白名单、业务规则和幂等键执行。
52
+ - `confirm_required`:生成待确认单,不写入;后端要求幂等键,未传时 MCP 会为本次工具调用自动生成。
53
+ - `commit`:提交到 MES,按当前账号权限、工具白名单、业务规则和幂等键执行;后端要求幂等键,未传时 MCP 会为本次工具调用自动生成。
54
+
55
+ 跨重试或批处理场景如果需要严格复用同一次业务请求,调用方应显式传入同一个 `idempotencyKey`;单次交互式调用可以依赖 MCP 自动生成。
53
56
 
54
57
  ## Marvis 接入建议
55
58
 
@@ -57,4 +60,4 @@ npm run dev
57
60
  2. `agentClientId` 建议填写 `marvis` 或具体渠道名。
58
61
  3. 授权范围建议先选“跟随账号权限”,最高执行模式建议先用 `confirm_required`;如果只想开放少量动作,再切换为“仅允许指定工具”。只读型 AI 凭证可以只选择 `mes_api.read`。
59
62
  4. 将返回的 JWT 配置为 `MES_AGENT_TOKEN`。
60
- 5. Marvis 侧通过 MCP 调用工具;图片、PDF、Excel 可先由 Marvis 识别成结构化草稿,再调用 `document_intake.parse`、`customer.match`、`material.match` 和后续写入工具。
63
+ 5. Marvis 侧通过 MCP 调用工具;图片、PDF、Excel 可先由 Marvis 识别成结构化草稿,再用 `mes_api.read` 或动态列表动作查询客户、物料等主数据,最后通过 `mes_action.detail` 和 `mes_action.execute` 执行创建、审批、确认、排产、质检、出库等页面业务动作。
@@ -1,81 +1,6 @@
1
1
  export class MesAgentApiClient {
2
2
  config;
3
- static WRITE_PATH_TOKENS = new Set([
4
- "create",
5
- "update",
6
- "delete",
7
- "remove",
8
- "revoke",
9
- "toggle",
10
- "confirm",
11
- "cancel",
12
- "approve",
13
- "reject",
14
- "release",
15
- "start",
16
- "complete",
17
- "close",
18
- "submit",
19
- "import",
20
- "run",
21
- "cleanup",
22
- "rotate",
23
- "test",
24
- "retry",
25
- "allocate",
26
- "unallocate",
27
- "reverse",
28
- "convert",
29
- "send",
30
- "recall",
31
- "scrap",
32
- "activate",
33
- "deprecate",
34
- "purge",
35
- "adjust",
36
- "freeze",
37
- "end",
38
- "configure",
39
- "generate",
40
- "save",
41
- "copy",
42
- "archive",
43
- "publish",
44
- "restore",
45
- "execute",
46
- "handle",
47
- "verify",
48
- "pass",
49
- "fail",
50
- "lock",
51
- "unlock",
52
- "enable",
53
- "disable",
54
- "bind",
55
- "unbind",
56
- "link",
57
- "unlink",
58
- "upload",
59
- "assign",
60
- "claim",
61
- "dispatch",
62
- "receive",
63
- "issue",
64
- "return",
65
- "split",
66
- "mark",
67
- "change",
68
- "transfer",
69
- "in",
70
- "out",
71
- ]);
72
- static WRITE_PATH_SEGMENTS = new Set([
73
- "report",
74
- "quick-report",
75
- "report-operation",
76
- "report-direct",
77
- "batch-report",
78
- ]);
3
+ businessActionsPromise;
79
4
  constructor(config) {
80
5
  this.config = config;
81
6
  }
@@ -115,25 +40,39 @@ export class MesAgentApiClient {
115
40
  if (/^\/integration\/agent(\/|$)/i.test(normalized)) {
116
41
  throw new Error("Use dedicated Agent tools for /integration/agent paths");
117
42
  }
118
- if (this.isWriteLikePath(normalized)) {
119
- throw new Error("mes_api.read only supports read-only MES API paths");
120
- }
121
43
  return this.post(normalized, payload);
122
44
  }
123
- isWriteLikePath(path) {
124
- const actionSegments = path
125
- .toLowerCase()
126
- .split(/[?#]/)[0]
127
- .split("/")
128
- .filter(Boolean);
129
- const lastSegment = actionSegments[actionSegments.length - 1] ?? "";
130
- if (MesAgentApiClient.WRITE_PATH_SEGMENTS.has(lastSegment)) {
131
- return true;
45
+ async listBusinessActions(options = {}) {
46
+ if (!options.refresh && this.businessActionsPromise) {
47
+ return this.businessActionsPromise;
48
+ }
49
+ this.businessActionsPromise = this.fetchBusinessActions().catch((error) => {
50
+ this.businessActionsPromise = undefined;
51
+ throw error;
52
+ });
53
+ return this.businessActionsPromise;
54
+ }
55
+ async getBusinessAction(actionCode) {
56
+ const actions = await this.listBusinessActions();
57
+ const cached = actions.find((action) => action.actionCode === actionCode);
58
+ if (cached)
59
+ return cached;
60
+ const refreshedActions = await this.listBusinessActions({ refresh: true });
61
+ return (refreshedActions.find((action) => action.actionCode === actionCode) ??
62
+ null);
63
+ }
64
+ async fetchBusinessActions() {
65
+ const pageSize = 100;
66
+ const firstPage = await this.post("/integration/agent/actions/list", { page: 1, pageSize });
67
+ if (firstPage.total <= firstPage.list.length)
68
+ return firstPage.list;
69
+ const actions = [...firstPage.list];
70
+ const pageCount = Math.ceil(firstPage.total / pageSize);
71
+ for (let page = 2; page <= pageCount; page += 1) {
72
+ const nextPage = await this.post("/integration/agent/actions/list", { page, pageSize });
73
+ actions.push(...nextPage.list);
132
74
  }
133
- return lastSegment
134
- .split(/[._-]+/)
135
- .filter(Boolean)
136
- .some((token) => MesAgentApiClient.WRITE_PATH_TOKENS.has(token));
75
+ return actions;
137
76
  }
138
77
  buildUrl(path) {
139
78
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
package/dist/config.js CHANGED
@@ -10,6 +10,16 @@ function requiredEnv(name) {
10
10
  }
11
11
  return value;
12
12
  }
13
+ function optionalBooleanEnv(name, fallback) {
14
+ const value = process.env[name]?.trim().toLowerCase();
15
+ if (!value)
16
+ return fallback;
17
+ if (["1", "true", "yes", "on"].includes(value))
18
+ return true;
19
+ if (["0", "false", "no", "off"].includes(value))
20
+ return false;
21
+ throw new Error(`${name} must be a boolean`);
22
+ }
13
23
  export function loadConfig() {
14
24
  const timeoutMs = Number(optionalEnv("MES_TIMEOUT_MS", "30000"));
15
25
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
@@ -20,5 +30,6 @@ export function loadConfig() {
20
30
  mesApiPrefix: optionalEnv("MES_API_PREFIX", "/api").replace(/\/+$/, ""),
21
31
  mesAgentToken: requiredEnv("MES_AGENT_TOKEN"),
22
32
  timeoutMs,
33
+ registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
23
34
  };
24
35
  }
package/dist/index.js CHANGED
@@ -5,15 +5,17 @@ import { loadConfig } from "./config.js";
5
5
  import { MesAgentApiClient } from "./clients/mes-agent-api.client.js";
6
6
  import { registerMesPrompts } from "./prompts/register-prompts.js";
7
7
  import { registerMesResources } from "./resources/register-resources.js";
8
- import { registerAgentTools } from "./tools/register-tools.js";
8
+ import { registerAllAgentTools } from "./tools/register-tools.js";
9
9
  async function main() {
10
10
  const config = loadConfig();
11
11
  const client = new MesAgentApiClient(config);
12
12
  const server = new McpServer({
13
13
  name: "mes-mcp",
14
- version: "0.1.0",
14
+ version: "0.2.0",
15
+ });
16
+ await registerAllAgentTools(server, client, {
17
+ registerDynamicTools: config.registerDynamicTools,
15
18
  });
16
- registerAgentTools(server, client);
17
19
  registerMesResources(server, client);
18
20
  registerMesPrompts(server);
19
21
  await server.connect(new StdioServerTransport());
@@ -15,8 +15,9 @@ export function registerMesPrompts(server) {
15
15
  text: [
16
16
  "你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
17
17
  "不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
18
- "推荐流程:document_intake.parse -> customer.match / material.match -> sale_order.create -> sale_order.confirm -> production_plan.create_from_sale_order -> work_order.create_from_plan -> work_order.release。",
19
- "写入类动作默认先使用 confirm_required 或 preview;用户明确确认后再使用 commit,并传 idempotencyKey。",
18
+ "默认先用 mes_action.list mes://agent/actions 发现当前账号能做的页面业务动作,再用 mes_action.detail 查看字段,最后用 mes_action.execute 执行;不要依赖工具列表里必须存在 mes.<动作编码>。",
19
+ "销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、发货出库。",
20
+ "写入类动作默认先使用 confirm_required 或 preview;preview 不写入,confirm_required 生成待确认记录,commit 只有凭证允许时才真实提交。",
20
21
  context ? `当前上下文:${context}` : "",
21
22
  ]
22
23
  .filter(Boolean)
@@ -1,21 +1,60 @@
1
1
  export function registerMesResources(server, client) {
2
- server.registerResource("mes_agent_contracts", "mes://agent/contracts", {
3
- title: "MES Agent Contracts",
4
- description: "当前 AI 凭证允许调用的 MES Agent 合同列表。",
2
+ server.registerResource("mes_business_actions", "mes://agent/actions", {
3
+ title: "MES Business Actions",
4
+ description: "当前 AI 凭证和账号权限可调用的 MES 页面业务动作目录,来源于 mes-node 后端 Controller、权限和 DTO 元数据。",
5
5
  mimeType: "application/json",
6
6
  }, async (uri) => {
7
- const contracts = await client.post("/integration/agent/contracts/list", {
8
- page: 1,
9
- pageSize: 100,
10
- });
7
+ const actions = await client.listBusinessActions();
11
8
  return {
12
9
  contents: [
13
10
  {
14
11
  uri: uri.href,
15
12
  mimeType: "application/json",
16
- text: JSON.stringify(contracts, null, 2),
13
+ text: JSON.stringify({
14
+ list: actions,
15
+ total: actions.length,
16
+ page: 1,
17
+ pageSize: actions.length,
18
+ }, null, 2),
17
19
  },
18
20
  ],
19
21
  };
20
22
  });
23
+ server.registerResource("mes_agent_context", "mes://agent/context", {
24
+ title: "MES Agent Context",
25
+ description: "MES AI Agent 操作边界、标准链路和安全规则摘要,用于提示客户端按受控接口完成业务闭环。",
26
+ mimeType: "text/markdown",
27
+ }, async (uri) => ({
28
+ contents: [
29
+ {
30
+ uri: uri.href,
31
+ mimeType: "text/markdown",
32
+ text: [
33
+ "# MES Agent Context",
34
+ "",
35
+ "MES MCP 只做协议适配,业务事实、权限、租户隔离、状态机、库存、质量、幂等和审计均由 mes-node 负责。",
36
+ "",
37
+ "## 工具使用顺序",
38
+ "",
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
+ "",
44
+ "## 写入规则",
45
+ "",
46
+ "- `preview` 不写入业务数据。",
47
+ "- `confirm_required` 生成待确认调用,不直接写入。",
48
+ "- `commit` 只有凭证最高执行模式允许时才真实提交。",
49
+ "- 写入类 `confirm_required` 和 `commit` 必须有幂等键;MCP 会为单次工具调用自动生成,跨重试批处理应显式传入同一个 `idempotencyKey`。",
50
+ "",
51
+ "## 禁止事项",
52
+ "",
53
+ "- 不猜客户、物料、BOM、工艺路线、仓库、批次、数量、价格等关键业务事实。",
54
+ "- 不通过 `mes_api.read` 调用创建、更新、删除、确认、下达等写入接口。",
55
+ "- 不绕过当前账号权限、公司隔离、状态机、库存规则、质量规则和审计。",
56
+ ].join("\n"),
57
+ },
58
+ ],
59
+ }));
21
60
  }
@@ -1,5 +1,23 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { z } from "zod";
2
- import { AGENT_TOOL_SPECS } from "./agent-tools.js";
3
+ const ExecutionModeSchema = z
4
+ .enum(["preview", "confirm_required", "commit"])
5
+ .optional();
6
+ const DynamicActionSchema = z
7
+ .object({
8
+ executionMode: ExecutionModeSchema.describe("写入动作执行模式;只读动作固定按 preview 执行。"),
9
+ idempotencyKey: z
10
+ .string()
11
+ .min(1)
12
+ .max(160)
13
+ .optional()
14
+ .describe("写入动作 confirm_required 或 commit 时用于后端幂等;不传时 MCP 会为本次工具调用自动生成。"),
15
+ payload: z
16
+ .record(z.string(), z.unknown())
17
+ .optional()
18
+ .describe("MES 页面业务动作请求体;也可以直接把 DTO 字段放在顶层。"),
19
+ })
20
+ .passthrough();
3
21
  function formatToolResult(result) {
4
22
  return {
5
23
  structuredContent: result && typeof result === "object" && !Array.isArray(result)
@@ -14,21 +32,6 @@ function formatToolResult(result) {
14
32
  };
15
33
  }
16
34
  export function registerAgentTools(server, client) {
17
- for (const spec of AGENT_TOOL_SPECS) {
18
- server.registerTool(spec.name, {
19
- title: spec.title,
20
- description: spec.description,
21
- inputSchema: spec.schema,
22
- annotations: {
23
- readOnlyHint: spec.readOnlyHint ?? false,
24
- destructiveHint: spec.destructiveHint ?? false,
25
- idempotentHint: true,
26
- },
27
- }, async (args) => {
28
- const result = await client.post(spec.path, args ?? {});
29
- return formatToolResult(result);
30
- });
31
- }
32
35
  server.registerTool("mes_api.read", {
33
36
  title: "Read MES API",
34
37
  description: "按当前 MES 账号权限读取系统内业务信息。仅用于 list/detail/view/read/export/stats/options 等只读类 POST 接口;创建、更新、删除、确认、下达等写入动作必须使用专门的 Agent 工具。",
@@ -60,3 +63,159 @@ export function registerAgentTools(server, client) {
60
63
  return formatToolResult(result);
61
64
  });
62
65
  }
66
+ function normalizeDynamicArgs(args) {
67
+ const parsed = DynamicActionSchema.parse(args ?? {});
68
+ const { executionMode, idempotencyKey, payload, ...rest } = parsed;
69
+ return {
70
+ executionMode,
71
+ idempotencyKey,
72
+ payload: payload ?? rest,
73
+ };
74
+ }
75
+ function buildIdempotencyKey(actionCode) {
76
+ const normalizedActionCode = actionCode.replace(/[^a-zA-Z0-9._-]+/g, "_");
77
+ return `mcp-${normalizedActionCode}-${Date.now().toString(36)}-${randomUUID()}`.slice(0, 160);
78
+ }
79
+ function shouldEnsureIdempotencyKey(action, executionMode) {
80
+ return Boolean(action?.isWrite && executionMode !== "preview");
81
+ }
82
+ function actionDescription(action) {
83
+ const permissionText = action.permissions.length > 0
84
+ ? `需要权限:${action.permissions.join("、")}。`
85
+ : "按后端业务方法自行校验权限。";
86
+ const modeText = action.isWrite
87
+ ? "写入动作,confirm_required 会生成待确认单,commit 会真实写入 MES;两种模式可显式提供 idempotencyKey,不传时 MCP 会为本次工具调用自动生成,preview 不写入。"
88
+ : "只读动作,固定按 preview 执行。";
89
+ return [
90
+ action.description,
91
+ `页面接口:POST ${action.path}。`,
92
+ action.inputDto ? `请求 DTO:${action.inputDto}。` : undefined,
93
+ permissionText,
94
+ modeText,
95
+ ]
96
+ .filter(Boolean)
97
+ .join("\n");
98
+ }
99
+ function registerGenericActionTools(server, client) {
100
+ server.registerTool("mes_action.list", {
101
+ title: "List MES business actions",
102
+ description: "动态查询当前 AI 凭证和当前账号权限允许调用的 MES 页面业务动作。新业务页面只要后端有权限和 DTO 元数据,就会出现在这里。",
103
+ inputSchema: z
104
+ .object({
105
+ page: z.number().int().positive().optional(),
106
+ pageSize: z.number().int().positive().max(100).optional(),
107
+ keyword: z.string().optional(),
108
+ module: z.string().optional(),
109
+ resource: z.string().optional(),
110
+ operation: z.string().optional(),
111
+ write: z.boolean().optional(),
112
+ })
113
+ .passthrough(),
114
+ annotations: {
115
+ readOnlyHint: true,
116
+ destructiveHint: false,
117
+ idempotentHint: true,
118
+ },
119
+ }, async (args) => {
120
+ const result = await client.post("/integration/agent/actions/list", args ?? {});
121
+ return formatToolResult(result);
122
+ });
123
+ server.registerTool("mes_action.detail", {
124
+ title: "Get MES business action detail",
125
+ description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称和后端提供的输入 schema。",
126
+ inputSchema: z.object({
127
+ actionCode: z.string().min(1),
128
+ }),
129
+ annotations: {
130
+ readOnlyHint: true,
131
+ destructiveHint: false,
132
+ idempotentHint: true,
133
+ },
134
+ }, async (args) => {
135
+ const result = await client.post("/integration/agent/actions/detail", args ?? {});
136
+ return formatToolResult(result);
137
+ });
138
+ server.registerTool("mes_action.execute", {
139
+ title: "Execute MES business action",
140
+ description: "执行一个由 mes_action.list 发现的 MES 页面业务动作。后端复用页面 Controller、DTO、权限、租户、状态机和调用日志。",
141
+ inputSchema: z
142
+ .object({
143
+ actionCode: z.string().min(1),
144
+ executionMode: ExecutionModeSchema,
145
+ idempotencyKey: z.string().min(1).max(160).optional(),
146
+ payload: z.record(z.string(), z.unknown()).optional(),
147
+ })
148
+ .passthrough(),
149
+ annotations: {
150
+ readOnlyHint: false,
151
+ destructiveHint: true,
152
+ idempotentHint: true,
153
+ },
154
+ }, async (args) => {
155
+ const parsed = z
156
+ .object({
157
+ actionCode: z.string().min(1),
158
+ executionMode: ExecutionModeSchema,
159
+ idempotencyKey: z.string().min(1).max(160).optional(),
160
+ payload: z.record(z.string(), z.unknown()).optional(),
161
+ })
162
+ .passthrough()
163
+ .parse(args ?? {});
164
+ const { actionCode, executionMode, idempotencyKey, payload, ...rest } = parsed;
165
+ const action = await client.getBusinessAction(actionCode);
166
+ const result = await client.post("/integration/agent/actions/execute", {
167
+ actionCode,
168
+ executionMode,
169
+ idempotencyKey: idempotencyKey ??
170
+ (shouldEnsureIdempotencyKey(action, executionMode)
171
+ ? buildIdempotencyKey(actionCode)
172
+ : undefined),
173
+ payload: payload ?? rest,
174
+ });
175
+ return formatToolResult(result);
176
+ });
177
+ }
178
+ async function registerDynamicBusinessActionTools(server, client) {
179
+ const actions = await client.listBusinessActions();
180
+ const registeredNames = new Set();
181
+ for (const action of actions) {
182
+ const toolName = `mes.${action.actionCode}`;
183
+ if (registeredNames.has(toolName))
184
+ continue;
185
+ registeredNames.add(toolName);
186
+ server.registerTool(toolName, {
187
+ title: action.title,
188
+ description: actionDescription(action),
189
+ inputSchema: DynamicActionSchema,
190
+ annotations: {
191
+ readOnlyHint: !action.isWrite,
192
+ destructiveHint: action.isWrite,
193
+ idempotentHint: true,
194
+ },
195
+ }, async (args) => {
196
+ const normalized = normalizeDynamicArgs(args);
197
+ const result = await client.post("/integration/agent/actions/execute", {
198
+ actionCode: action.actionCode,
199
+ ...normalized,
200
+ idempotencyKey: normalized.idempotencyKey ??
201
+ (shouldEnsureIdempotencyKey(action, normalized.executionMode)
202
+ ? buildIdempotencyKey(action.actionCode)
203
+ : undefined),
204
+ });
205
+ return formatToolResult(result);
206
+ });
207
+ }
208
+ }
209
+ export async function registerAllAgentTools(server, client, options = {}) {
210
+ registerAgentTools(server, client);
211
+ registerGenericActionTools(server, client);
212
+ if (!options.registerDynamicTools)
213
+ return;
214
+ try {
215
+ await registerDynamicBusinessActionTools(server, client);
216
+ }
217
+ catch (error) {
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ console.error(`MES dynamic business actions unavailable: ${message}`);
220
+ }
221
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mes-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "MES MCP adapter for Marvis and AI agents",
6
6
  "license": "UNLICENSED",
@@ -14,7 +14,7 @@
14
14
  ".env.example"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc -p tsconfig.json",
17
+ "build": "rm -rf dist && tsc -p tsconfig.json",
18
18
  "dev": "tsx src/index.ts",
19
19
  "prepublishOnly": "npm run build",
20
20
  "start": "node dist/index.js"
@@ -1,209 +0,0 @@
1
- import { z } from "zod";
2
- const ExecutionModeSchema = z
3
- .enum(["preview", "confirm_required", "commit"])
4
- .optional();
5
- const IdempotencySchema = z.string().min(1).max(160).optional();
6
- const QuerySchema = z
7
- .object({
8
- page: z.number().int().positive().optional(),
9
- pageSize: z.number().int().positive().max(100).optional(),
10
- keyword: z.string().optional(),
11
- })
12
- .passthrough();
13
- export const AGENT_TOOL_SPECS = [
14
- {
15
- name: "document_intake.parse",
16
- title: "Parse order document",
17
- description: "接收 Marvis/上游 AI 从图片、PDF、Excel、文本中识别出的订单资料,返回可确认的销售订单草稿。",
18
- path: "/integration/agent/document-intake/parse",
19
- readOnlyHint: true,
20
- schema: z
21
- .object({
22
- executionMode: ExecutionModeSchema,
23
- idempotencyKey: IdempotencySchema,
24
- rawText: z.string().optional(),
25
- imageUrl: z.string().url().optional(),
26
- fileUrl: z.string().url().optional(),
27
- extractedOrder: z
28
- .object({
29
- orderNo: z.string().optional(),
30
- customerId: z.string().uuid().optional(),
31
- customerName: z.string().optional(),
32
- deliveryDate: z.string().optional(),
33
- remarks: z.string().optional(),
34
- items: z
35
- .array(z
36
- .object({
37
- materialId: z.string().uuid().optional(),
38
- materialCode: z.string().optional(),
39
- materialName: z.string().optional(),
40
- quantity: z.number().positive().optional(),
41
- unitPrice: z.number().min(0).optional(),
42
- remarks: z.string().optional(),
43
- })
44
- .passthrough())
45
- .optional(),
46
- })
47
- .passthrough()
48
- .optional(),
49
- sourceMeta: z.record(z.string(), z.unknown()).optional(),
50
- })
51
- .passthrough(),
52
- },
53
- {
54
- name: "customer.match",
55
- title: "Match customer",
56
- description: "按当前 MES 账号权限匹配启用中的客户主数据;创建销售单前用于确认客户 ID,避免 AI 直接猜测客户。",
57
- path: "/integration/agent/customer/match",
58
- readOnlyHint: true,
59
- schema: QuerySchema.extend({
60
- customerCode: z.string().optional(),
61
- name: z.string().optional(),
62
- contactName: z.string().optional(),
63
- contactPhone: z.string().optional(),
64
- }).passthrough(),
65
- },
66
- {
67
- name: "material.match",
68
- title: "Match material",
69
- description: "按当前 MES 账号权限匹配启用中的物料主数据;创建销售单前用于确认物料 ID,避免用图片识别文本直接写入。",
70
- path: "/integration/agent/material/match",
71
- readOnlyHint: true,
72
- schema: QuerySchema.extend({
73
- code: z.string().optional(),
74
- name: z.string().optional(),
75
- spec: z.string().optional(),
76
- barcode: z.string().optional(),
77
- }).passthrough(),
78
- },
79
- {
80
- name: "sale_order.create",
81
- title: "Create sale order",
82
- description: "按当前 MES 账号权限创建销售订单;commit 才写入,preview/confirm_required 只返回待确认内容。",
83
- path: "/integration/agent/sale-order/create",
84
- destructiveHint: true,
85
- schema: z
86
- .object({
87
- executionMode: ExecutionModeSchema,
88
- idempotencyKey: IdempotencySchema,
89
- orderNo: z.string().optional(),
90
- customerId: z.string().uuid().optional(),
91
- salesRepId: z.string().uuid().optional(),
92
- deliveryDate: z.string().optional(),
93
- paymentTerms: z.string().optional(),
94
- remarks: z.string().optional(),
95
- items: z.array(z
96
- .object({
97
- materialId: z.string().uuid(),
98
- quantity: z.number().positive(),
99
- unitPrice: z.number().min(0).optional(),
100
- customerMaterialCode: z.string().optional(),
101
- customerMaterialName: z.string().optional(),
102
- customerMaterialSpec: z.string().optional(),
103
- remarks: z.string().optional(),
104
- })
105
- .passthrough()),
106
- })
107
- .passthrough(),
108
- },
109
- {
110
- name: "sale_order.confirm",
111
- title: "Confirm sale order",
112
- description: "确认销售订单,复用 MES 的审批门控、状态规则和明细校验;销售订单转生产计划前必须先确认。",
113
- path: "/integration/agent/sale-order/confirm",
114
- destructiveHint: true,
115
- schema: z
116
- .object({
117
- executionMode: ExecutionModeSchema,
118
- idempotencyKey: IdempotencySchema,
119
- id: z.string().uuid(),
120
- })
121
- .passthrough(),
122
- },
123
- {
124
- name: "production_plan.create_from_sale_order",
125
- title: "Create production plan from sale order",
126
- description: "将已确认销售订单转为生产计划,复用 MES 原有数量、BOM 和工艺校验。",
127
- path: "/integration/agent/production-plan/create-from-sale-order",
128
- destructiveHint: true,
129
- schema: z
130
- .object({
131
- executionMode: ExecutionModeSchema,
132
- idempotencyKey: IdempotencySchema,
133
- saleOrderId: z.string().uuid(),
134
- planStartDate: z.string(),
135
- planEndDate: z.string(),
136
- priority: z.number().int().min(1).max(10).optional(),
137
- description: z.string().optional(),
138
- items: z.array(z
139
- .object({
140
- saleOrderItemId: z.string().uuid(),
141
- requiredQty: z.number().positive().optional(),
142
- plannedLossRate: z.number().min(0).optional(),
143
- plannedQty: z.number().positive().optional(),
144
- bomHeaderId: z.string().uuid().optional(),
145
- routeId: z.string().uuid().optional(),
146
- })
147
- .passthrough()),
148
- })
149
- .passthrough(),
150
- },
151
- {
152
- name: "work_order.create_from_plan",
153
- title: "Create work order from production plan",
154
- description: "确认生产计划并自动创建工单,复用 MES 的 BOM 展开和工艺工序生成规则。",
155
- path: "/integration/agent/work-order/create-from-plan",
156
- destructiveHint: true,
157
- schema: z
158
- .object({
159
- executionMode: ExecutionModeSchema,
160
- idempotencyKey: IdempotencySchema,
161
- planId: z.string().uuid(),
162
- routeId: z.string().uuid().optional(),
163
- bomHeaderId: z.string().uuid().optional(),
164
- assigneeId: z.string().uuid().optional(),
165
- receiptWarehouseId: z.string().uuid().optional(),
166
- })
167
- .passthrough(),
168
- },
169
- {
170
- name: "work_order.release",
171
- title: "Release work order",
172
- description: "下达工单,复用 MES 状态机、审批门控、物料需求展开和事件推送。",
173
- path: "/integration/agent/work-order/release",
174
- destructiveHint: true,
175
- schema: z
176
- .object({
177
- executionMode: ExecutionModeSchema,
178
- idempotencyKey: IdempotencySchema,
179
- id: z.string().uuid(),
180
- assigneeId: z.string().uuid().optional(),
181
- receiptWarehouseId: z.string().uuid().optional(),
182
- })
183
- .passthrough(),
184
- },
185
- {
186
- name: "sale_order.query",
187
- title: "Query sale orders",
188
- description: "按当前 MES 账号权限查询销售订单列表。",
189
- path: "/integration/agent/sale-order/query",
190
- readOnlyHint: true,
191
- schema: QuerySchema,
192
- },
193
- {
194
- name: "work_order.query",
195
- title: "Query work orders",
196
- description: "按当前 MES 账号权限查询工单列表。",
197
- path: "/integration/agent/work-order/query",
198
- readOnlyHint: true,
199
- schema: QuerySchema,
200
- },
201
- {
202
- name: "inventory.query",
203
- title: "Query inventory",
204
- description: "按当前 MES 账号权限查询库存现有量和可用量。",
205
- path: "/integration/agent/inventory/query",
206
- readOnlyHint: true,
207
- schema: QuerySchema,
208
- },
209
- ];