mes-mcp 0.2.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 +1 -0
- package/README.md +13 -6
- package/dist/clients/mes-agent-api.client.js +21 -1
- package/dist/config.js +11 -0
- package/dist/index.js +3 -1
- package/dist/prompts/register-prompts.js +2 -2
- package/dist/resources/register-resources.js +44 -5
- package/dist/tools/register-tools.js +22 -4
- package/package.json +1 -1
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -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,22 +33,26 @@ npm run dev
|
|
|
30
33
|
|
|
31
34
|
## 工具
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
默认使用后端动态业务动作接口:
|
|
34
37
|
|
|
35
38
|
- `mes_action.list`:查询当前凭证和当前账号权限允许调用的 MES 页面业务动作。
|
|
36
39
|
- `mes_action.detail`:查看某个业务动作的路径、权限、DTO 名称和输入 schema。
|
|
37
40
|
- `mes_action.execute`:按动作编码执行业务动作。
|
|
38
|
-
- `mes
|
|
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` 为准。
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
写入类动作通过 `confirm_required` 或 `commit` 执行时需要幂等键。调用方没有显式传入时,`mes-mcp` 会为当前工具调用生成一条集成幂等键;需要跨重试严格复用同一幂等键的批处理场景,仍建议调用方显式传入 `idempotencyKey`。
|
|
41
46
|
|
|
42
47
|
- `mes_api.read`:按当前 MES 账号权限读取普通业务接口,例如 `/sale-order/list`、`/material/detail`。后端只放行只读类接口,写入动作必须使用动态业务动作;读取调用会在 MES AI 调用日志里记录路径、请求摘要和结果摘要。
|
|
43
48
|
|
|
44
49
|
写入类工具支持 `executionMode`:
|
|
45
50
|
|
|
46
51
|
- `preview`:只预览,不写入。
|
|
47
|
-
- `confirm_required
|
|
48
|
-
- `commit`:提交到 MES
|
|
52
|
+
- `confirm_required`:生成待确认单,不写入;后端要求幂等键,未传时 MCP 会为本次工具调用自动生成。
|
|
53
|
+
- `commit`:提交到 MES,按当前账号权限、工具白名单、业务规则和幂等键执行;后端要求幂等键,未传时 MCP 会为本次工具调用自动生成。
|
|
54
|
+
|
|
55
|
+
跨重试或批处理场景如果需要严格复用同一次业务请求,调用方应显式传入同一个 `idempotencyKey`;单次交互式调用可以依赖 MCP 自动生成。
|
|
49
56
|
|
|
50
57
|
## Marvis 接入建议
|
|
51
58
|
|
|
@@ -53,4 +60,4 @@ npm run dev
|
|
|
53
60
|
2. `agentClientId` 建议填写 `marvis` 或具体渠道名。
|
|
54
61
|
3. 授权范围建议先选“跟随账号权限”,最高执行模式建议先用 `confirm_required`;如果只想开放少量动作,再切换为“仅允许指定工具”。只读型 AI 凭证可以只选择 `mes_api.read`。
|
|
55
62
|
4. 将返回的 JWT 配置为 `MES_AGENT_TOKEN`。
|
|
56
|
-
5. Marvis 侧通过 MCP 调用工具;图片、PDF、Excel 可先由 Marvis 识别成结构化草稿,再用 `mes_api.read` 或动态列表动作查询客户、物料等主数据,最后通过 `
|
|
63
|
+
5. Marvis 侧通过 MCP 调用工具;图片、PDF、Excel 可先由 Marvis 识别成结构化草稿,再用 `mes_api.read` 或动态列表动作查询客户、物料等主数据,最后通过 `mes_action.detail` 和 `mes_action.execute` 执行创建、审批、确认、排产、质检、出库等页面业务动作。
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export class MesAgentApiClient {
|
|
2
2
|
config;
|
|
3
|
+
businessActionsPromise;
|
|
3
4
|
constructor(config) {
|
|
4
5
|
this.config = config;
|
|
5
6
|
}
|
|
@@ -41,7 +42,26 @@ export class MesAgentApiClient {
|
|
|
41
42
|
}
|
|
42
43
|
return this.post(normalized, payload);
|
|
43
44
|
}
|
|
44
|
-
async listBusinessActions() {
|
|
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() {
|
|
45
65
|
const pageSize = 100;
|
|
46
66
|
const firstPage = await this.post("/integration/agent/actions/list", { page: 1, pageSize });
|
|
47
67
|
if (firstPage.total <= firstPage.list.length)
|
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
|
@@ -13,7 +13,9 @@ async function main() {
|
|
|
13
13
|
name: "mes-mcp",
|
|
14
14
|
version: "0.2.0",
|
|
15
15
|
});
|
|
16
|
-
await registerAllAgentTools(server, client
|
|
16
|
+
await registerAllAgentTools(server, client, {
|
|
17
|
+
registerDynamicTools: config.registerDynamicTools,
|
|
18
|
+
});
|
|
17
19
|
registerMesResources(server, client);
|
|
18
20
|
registerMesPrompts(server);
|
|
19
21
|
await server.connect(new StdioServerTransport());
|
|
@@ -15,9 +15,9 @@ export function registerMesPrompts(server) {
|
|
|
15
15
|
text: [
|
|
16
16
|
"你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
|
|
17
17
|
"不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
|
|
18
|
-
"
|
|
18
|
+
"默认先用 mes_action.list 或 mes://agent/actions 发现当前账号能做的页面业务动作,再用 mes_action.detail 查看字段,最后用 mes_action.execute 执行;不要依赖工具列表里必须存在 mes.<动作编码>。",
|
|
19
19
|
"销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、发货出库。",
|
|
20
|
-
"写入类动作默认先使用 confirm_required 或 preview;confirm_required
|
|
20
|
+
"写入类动作默认先使用 confirm_required 或 preview;preview 不写入,confirm_required 生成待确认记录,commit 只有凭证允许时才真实提交。",
|
|
21
21
|
context ? `当前上下文:${context}` : "",
|
|
22
22
|
]
|
|
23
23
|
.filter(Boolean)
|
|
@@ -4,18 +4,57 @@ export function registerMesResources(server, client) {
|
|
|
4
4
|
description: "当前 AI 凭证和账号权限可调用的 MES 页面业务动作目录,来源于 mes-node 后端 Controller、权限和 DTO 元数据。",
|
|
5
5
|
mimeType: "application/json",
|
|
6
6
|
}, async (uri) => {
|
|
7
|
-
const actions = await client.
|
|
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(
|
|
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,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
const ExecutionModeSchema = z
|
|
3
4
|
.enum(["preview", "confirm_required", "commit"])
|
|
@@ -10,7 +11,7 @@ const DynamicActionSchema = z
|
|
|
10
11
|
.min(1)
|
|
11
12
|
.max(160)
|
|
12
13
|
.optional()
|
|
13
|
-
.describe("写入动作 confirm_required 或 commit
|
|
14
|
+
.describe("写入动作 confirm_required 或 commit 时用于后端幂等;不传时 MCP 会为本次工具调用自动生成。"),
|
|
14
15
|
payload: z
|
|
15
16
|
.record(z.string(), z.unknown())
|
|
16
17
|
.optional()
|
|
@@ -71,12 +72,19 @@ function normalizeDynamicArgs(args) {
|
|
|
71
72
|
payload: payload ?? rest,
|
|
72
73
|
};
|
|
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
|
+
}
|
|
74
82
|
function actionDescription(action) {
|
|
75
83
|
const permissionText = action.permissions.length > 0
|
|
76
84
|
? `需要权限:${action.permissions.join("、")}。`
|
|
77
85
|
: "按后端业务方法自行校验权限。";
|
|
78
86
|
const modeText = action.isWrite
|
|
79
|
-
? "写入动作,confirm_required 会生成待确认单,commit 会真实写入 MES
|
|
87
|
+
? "写入动作,confirm_required 会生成待确认单,commit 会真实写入 MES;两种模式可显式提供 idempotencyKey,不传时 MCP 会为本次工具调用自动生成,preview 不写入。"
|
|
80
88
|
: "只读动作,固定按 preview 执行。";
|
|
81
89
|
return [
|
|
82
90
|
action.description,
|
|
@@ -154,10 +162,14 @@ function registerGenericActionTools(server, client) {
|
|
|
154
162
|
.passthrough()
|
|
155
163
|
.parse(args ?? {});
|
|
156
164
|
const { actionCode, executionMode, idempotencyKey, payload, ...rest } = parsed;
|
|
165
|
+
const action = await client.getBusinessAction(actionCode);
|
|
157
166
|
const result = await client.post("/integration/agent/actions/execute", {
|
|
158
167
|
actionCode,
|
|
159
168
|
executionMode,
|
|
160
|
-
idempotencyKey
|
|
169
|
+
idempotencyKey: idempotencyKey ??
|
|
170
|
+
(shouldEnsureIdempotencyKey(action, executionMode)
|
|
171
|
+
? buildIdempotencyKey(actionCode)
|
|
172
|
+
: undefined),
|
|
161
173
|
payload: payload ?? rest,
|
|
162
174
|
});
|
|
163
175
|
return formatToolResult(result);
|
|
@@ -185,14 +197,20 @@ async function registerDynamicBusinessActionTools(server, client) {
|
|
|
185
197
|
const result = await client.post("/integration/agent/actions/execute", {
|
|
186
198
|
actionCode: action.actionCode,
|
|
187
199
|
...normalized,
|
|
200
|
+
idempotencyKey: normalized.idempotencyKey ??
|
|
201
|
+
(shouldEnsureIdempotencyKey(action, normalized.executionMode)
|
|
202
|
+
? buildIdempotencyKey(action.actionCode)
|
|
203
|
+
: undefined),
|
|
188
204
|
});
|
|
189
205
|
return formatToolResult(result);
|
|
190
206
|
});
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
|
-
export async function registerAllAgentTools(server, client) {
|
|
209
|
+
export async function registerAllAgentTools(server, client, options = {}) {
|
|
194
210
|
registerAgentTools(server, client);
|
|
195
211
|
registerGenericActionTools(server, client);
|
|
212
|
+
if (!options.registerDynamicTools)
|
|
213
|
+
return;
|
|
196
214
|
try {
|
|
197
215
|
await registerDynamicBusinessActionTools(server, client);
|
|
198
216
|
}
|