@wukongcrm/mcp-server 0.1.4 → 0.2.0

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/README.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - 人力资源 HRM 保留员工、部门、岗位、考勤、请假等查询,并开放 HRM 域内固定路径写入。
15
15
  - 独立财务 FM 保留凭证、账簿、报表、科目、币别、凭证字、仪表盘查询,并开放 FM 域内固定路径写入。
16
16
  - 进销存 JXC 保留产品、供应商、仓库、库存和出入库明细查询,并开放 JXC 域内固定路径写入。
17
+ - 无代码能力覆盖应用、模块、字段、数据、角色、流程、阶段、跟进、打印、消息、开放配置和应用市场的 JSON 业务接口。
17
18
  - 所有写入仍必须传 `confirm=true`;未确认时只返回预览,不调用 72CRM 写接口。
18
19
 
19
20
  ## ChatGPT 网页端(远程 MCP)
@@ -47,9 +48,14 @@ MCP_PUBLIC_URL=https://www.72crm.com/mcp
47
48
  MCP_OAUTH_SECRET=<单独生成的至少32字符随机密钥>
48
49
  MCP_DATA_DIR=/var/lib/wukong-mcp
49
50
 
51
+ # 可选;配置后启用 Redis,不配置时继续使用 JSON 文件
52
+ MCP_REDIS_URL=rediss://default:<password>@<redis-host>:6380/0
53
+ MCP_REDIS_PREFIX=wukong-mcp:oauth
54
+
50
55
  CRM_BASE_URL=https://www.72crm.com/api-11/
51
56
  CRM_BINDING_AUTHORIZE_URL=https://www.72crm.com/mcp-binding.html
52
57
  CRM_BINDING_EXCHANGE_URL=https://www.72crm.com/api-11/adminMcpBinding/exchange
58
+ CRM_BINDING_EXCHANGE_ALLOW_HTTP=false
53
59
  CRM_BINDING_CALLBACK_URL=https://www.72crm.com/mcp/oauth/72crm/callback
54
60
  CRM_BINDING_CLIENT_ID=wukong-mcp
55
61
  CRM_BINDING_CLIENT_SECRET=<与72CRM后端完全一致的至少32字符共享密钥>
@@ -61,8 +67,13 @@ MCP_TRUST_PROXY=1
61
67
  ```
62
68
 
63
69
  - `MCP_OAUTH_SECRET` 用于 AES-256-GCM 加密 OAuth 令牌;更换后,已连接用户需要重新授权。
64
- - `MCP_DATA_DIR` 保存动态 OAuth 客户端和撤销记录,不保存明文 API Key。
70
+ - 未配置 `MCP_REDIS_URL` 时,`MCP_DATA_DIR/oauth-state.json` 保存动态客户端、授权临时状态和撤销记录。
71
+ - 配置 `MCP_REDIS_URL` 后,上述 OAuth 状态改为保存到 Redis;支持 `redis://` 和启用 TLS 的 `rediss://`。
72
+ - `MCP_REDIS_PREFIX` 默认为 `wukong-mcp:oauth`。同一套 MCP 实例必须使用相同的前缀和 `MCP_OAUTH_SECRET`。
73
+ - 授权请求和授权码在写入 Redis 或 JSON 前均使用 AES-256-GCM 加密,不会保存明文 API Key;授权码通过 Redis `GETDEL` 原子消费,因此 Redis/Tair 需要兼容 Redis 6.2 或更高版本。
74
+ - 如果已经配置 Redis 但启动时连接失败,服务会启动失败,不会自动退回 JSON,以避免多个实例产生不一致状态。
65
75
  - `CRM_BINDING_AUTHORIZE_URL` 必须指向 72CRM 前端中转页,不能直接填写后端授权接口。
76
+ - `CRM_BINDING_EXCHANGE_URL` 由 MCP 服务端请求,优先使用 HTTPS;仅当它是可信 VPC 内网 HTTP 地址时,才同时设置 `CRM_BINDING_EXCHANGE_ALLOW_HTTP=true`。此开关不会放开授权页或回调地址的 HTTPS 校验。
66
77
  - `CRM_BINDING_CLIENT_SECRET` 必须与 72CRM `admin-web` 配置完全一致,只保存在服务端。
67
78
  - `CRM_BINDING_CALLBACK_URL` 默认根据 `MCP_PUBLIC_URL` 的路径生成,显式配置时必须与其同域。
68
79
  - `MCP_ALLOWED_HOSTS` 是逗号分隔的额外 Host 白名单;公开域名会由 `MCP_PUBLIC_URL` 自动加入。
@@ -136,7 +147,7 @@ npm pack
136
147
  生成文件示例:
137
148
 
138
149
  ```text
139
- wukongcrm-mcp-server-0.1.4.tgz
150
+ wukongcrm-mcp-server-0.2.0.tgz
140
151
  ```
141
152
 
142
153
  这个包只包含运行所需的 `dist`、`README.md` 和 `package.json`,不会携带源码、测试文件或用户 API Key。
@@ -144,7 +155,7 @@ wukongcrm-mcp-server-0.1.4.tgz
144
155
  使用者拿到 `.tgz` 后安装:
145
156
 
146
157
  ```powershell
147
- npm install -g .\wukongcrm-mcp-server-0.1.4.tgz
158
+ npm install -g .\wukongcrm-mcp-server-0.2.0.tgz
148
159
  ```
149
160
 
150
161
  安装后可以直接用命令启动:
@@ -173,6 +184,24 @@ wukong-mcp
173
184
 
174
185
  ## 支持工具
175
186
 
187
+ ### 无代码工具
188
+
189
+ 无代码工具只调用源码中声明的固定接口,不提供任意 URL 透传。写工具保持既有 MCP 语义:未传 `confirm=true` 时返回将要调用的接口、请求体和查询参数;明确确认后才执行。
190
+
191
+ - 应用与模块:`nocode_list_applications`、`nocode_get_application`、`nocode_list_modules`、`nocode_get_module`、`nocode_write_application`、`nocode_write_module`
192
+ - 字段与数据:`nocode_get_module_schema`、`nocode_query_fields`、`nocode_search_records`、`nocode_get_record`、`nocode_query_records`、`nocode_calculate_formula`、`nocode_check_duplicate`、`nocode_write_field`、`nocode_write_record`
193
+ - 权限与团队:`nocode_list_team_members`、`nocode_get_permissions`、`nocode_write_team`、`nocode_write_role`
194
+ - 场景与页面:`nocode_list_scenes`、`nocode_list_tabs`、`nocode_get_module_file`、`nocode_write_scene`、`nocode_write_tab`
195
+ - 流程与阶段:`nocode_list_flows`、`nocode_list_stages`、`nocode_list_custom_buttons`、`nocode_query_workflow`、`nocode_write_flow`、`nocode_write_stage`、`nocode_write_custom_button`、`nocode_write_workflow`
196
+ - 跟进:`nocode_query_followups`、`nocode_write_followup`
197
+ - 业务配置:`nocode_query_configuration`、`nocode_write_configuration`
198
+ - 打印与消息:`nocode_query_printing`、`nocode_write_printing`、`nocode_query_messages`、`nocode_write_message`
199
+ - 开放能力与应用市场:`nocode_query_integrations`、`nocode_write_integration`、`nocode_query_marketplace`、`nocode_write_marketplace`
200
+
201
+ 文件流仍有意不经 MCP 代理,因此应用导入/导出、Excel 导入/导出、跟进导出、打印 PDF/Word 下载不在工具中;打印内容生成和预览 JSON 接口仍可使用。内部测试接口 `/moduleMQ/send` 也不会暴露。
202
+
203
+ ### CRM、OA、HRM、财务、进销存与工单工具
204
+
176
205
  - `crm_auth_status`
177
206
  - `crm_list_modules`
178
207
  - `crm_list_customer_pools`
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ // Aliyun Function Compute custom runtimes require the HTTP server to listen on
2
+ // 0.0.0.0 and use port 9000 by default. Explicit function environment values
3
+ // still take precedence when a different listener port is configured.
4
+ process.env.MCP_HOST ||= "0.0.0.0";
5
+ process.env.MCP_PORT ||= process.env.PORT || "9000";
6
+ await import("./http-entry.js");
7
+ export {};
@@ -1,12 +1,15 @@
1
1
  import { createWukongHttpApp, loadRemoteServerEnvironment } from "./http.js";
2
2
  const environment = loadRemoteServerEnvironment();
3
- const { app, resourceUrl } = createWukongHttpApp({
3
+ const { app, provider, resourceUrl } = createWukongHttpApp({
4
4
  publicUrl: environment.publicUrl,
5
5
  oauthSecret: environment.oauthSecret,
6
6
  dataDirectory: environment.dataDirectory,
7
+ redisUrl: environment.redisUrl,
8
+ redisKeyPrefix: environment.redisKeyPrefix,
7
9
  crmBaseUrl: environment.crmBaseUrl,
8
10
  crmBindingAuthorizeUrl: environment.crmBindingAuthorizeUrl,
9
11
  crmBindingExchangeUrl: environment.crmBindingExchangeUrl,
12
+ crmBindingExchangeAllowHttp: environment.crmBindingExchangeAllowHttp,
10
13
  crmBindingCallbackUrl: environment.crmBindingCallbackUrl,
11
14
  crmBindingClientId: environment.crmBindingClientId,
12
15
  crmBindingClientSecret: environment.crmBindingClientSecret,
@@ -14,17 +17,35 @@ const { app, resourceUrl } = createWukongHttpApp({
14
17
  allowedHosts: environment.allowedHosts,
15
18
  trustProxy: environment.trustProxy
16
19
  });
20
+ try {
21
+ await provider.initialize();
22
+ }
23
+ catch (error) {
24
+ console.error("Failed to initialize OAuth state store:", error instanceof Error ? error.message : "unknown error");
25
+ process.exit(1);
26
+ }
17
27
  const httpServer = app.listen(environment.port, environment.host, () => {
18
28
  console.log(`WukongCRM remote MCP listening on ${environment.host}:${environment.port}`);
19
29
  console.log(`Public MCP URL: ${resourceUrl.href}`);
20
30
  console.log(`Express trust proxy: ${String(environment.trustProxy)}`);
31
+ console.log(`OAuth state store: ${provider.clientsStore.kind === "redis" ? "Redis" : "JSON file"}`);
21
32
  });
33
+ // Function Compute may keep a request or connection alive for much longer than
34
+ // Node.js defaults. The function-level execution timeout remains the hard limit.
35
+ httpServer.timeout = 0;
36
+ httpServer.keepAliveTimeout = 0;
37
+ httpServer.headersTimeout = 0;
22
38
  httpServer.on("error", (error) => {
23
39
  console.error("Failed to start remote MCP server:", error.message);
24
40
  process.exitCode = 1;
25
41
  });
26
42
  for (const signal of ["SIGINT", "SIGTERM"]) {
27
43
  process.on(signal, () => {
28
- httpServer.close(() => process.exit(0));
44
+ httpServer.close(async () => {
45
+ await provider.close().catch((error) => {
46
+ console.error("Failed to close OAuth state store:", error instanceof Error ? error.message : "unknown error");
47
+ });
48
+ process.exit(0);
49
+ });
29
50
  });
30
51
  }
package/dist/http.d.ts CHANGED
@@ -5,9 +5,12 @@ export interface WukongHttpAppOptions {
5
5
  publicUrl: string | URL;
6
6
  oauthSecret: string;
7
7
  dataDirectory: string;
8
+ redisUrl?: string;
9
+ redisKeyPrefix?: string;
8
10
  crmBaseUrl?: string;
9
11
  crmBindingAuthorizeUrl: string | URL;
10
12
  crmBindingExchangeUrl: string | URL;
13
+ crmBindingExchangeAllowHttp?: boolean;
11
14
  crmBindingCallbackUrl?: string | URL;
12
15
  crmBindingClientId: string;
13
16
  crmBindingClientSecret: string;
@@ -27,9 +30,12 @@ export interface RemoteServerEnvironment {
27
30
  publicUrl: string;
28
31
  oauthSecret: string;
29
32
  dataDirectory: string;
33
+ redisUrl?: string;
34
+ redisKeyPrefix: string;
30
35
  crmBaseUrl?: string;
31
36
  crmBindingAuthorizeUrl: string;
32
37
  crmBindingExchangeUrl: string;
38
+ crmBindingExchangeAllowHttp: boolean;
33
39
  crmBindingCallbackUrl: string;
34
40
  crmBindingClientId: string;
35
41
  crmBindingClientSecret: string;
package/dist/http.js CHANGED
@@ -37,11 +37,14 @@ export function createWukongHttpApp(options) {
37
37
  crmBaseUrl: options.crmBaseUrl,
38
38
  crmBindingAuthorizeUrl: new URL(options.crmBindingAuthorizeUrl.toString()),
39
39
  crmBindingExchangeUrl: new URL(options.crmBindingExchangeUrl.toString()),
40
+ crmBindingExchangeAllowHttp: options.crmBindingExchangeAllowHttp,
40
41
  crmBindingCallbackUrl: bindingCallbackUrl,
41
42
  crmBindingClientId: options.crmBindingClientId,
42
43
  crmBindingClientSecret: options.crmBindingClientSecret,
43
44
  secret: options.oauthSecret,
44
45
  dataDirectory: options.dataDirectory,
46
+ redisUrl: options.redisUrl,
47
+ redisKeyPrefix: options.redisKeyPrefix,
45
48
  fetchImpl: options.fetchImpl
46
49
  });
47
50
  const app = createMcpExpressApp({ host, allowedHosts });
@@ -100,7 +103,13 @@ export function createWukongHttpApp(options) {
100
103
  app.post(resourceUrl.pathname, bearerAuth, async (req, res) => {
101
104
  await handleMcpPost(req, res, options, resourceMetadataUrl);
102
105
  });
103
- app.get(resourceUrl.pathname, bearerAuth, (_req, res) => {
106
+ app.get(resourceUrl.pathname, (req, res, next) => {
107
+ if (!req.accepts("html")) {
108
+ next();
109
+ return;
110
+ }
111
+ sendMcpLandingPage(res, resourceUrl, resourceMetadataUrl);
112
+ }, bearerAuth, (_req, res) => {
104
113
  sendMethodNotAllowed(res);
105
114
  });
106
115
  app.delete(resourceUrl.pathname, bearerAuth, (_req, res) => {
@@ -172,6 +181,281 @@ function setSensitiveResponseHeaders(res) {
172
181
  res.setHeader("Referrer-Policy", "no-referrer");
173
182
  res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'");
174
183
  }
184
+ function sendMcpLandingPage(res, resourceUrl, resourceMetadataUrl) {
185
+ const endpoint = escapeHtml(resourceUrl.href);
186
+ const healthUrl = escapeHtml(new URL(`${resourceUrl.pathname}/healthz`, resourceUrl).href);
187
+ const metadataUrl = escapeHtml(resourceMetadataUrl);
188
+ const authorizationMetadataUrl = escapeHtml(new URL("/.well-known/oauth-authorization-server", resourceUrl).href);
189
+ setSensitiveResponseHeaders(res);
190
+ res.setHeader("X-Content-Type-Options", "nosniff");
191
+ res.status(200).type("html").send(`<!doctype html>
192
+ <html lang="zh-CN">
193
+ <head>
194
+ <meta charset="utf-8">
195
+ <meta name="viewport" content="width=device-width, initial-scale=1">
196
+ <meta name="color-scheme" content="light">
197
+ <title>WukongCRM MCP · 服务说明</title>
198
+ <style>
199
+ :root {
200
+ --ink: #16332b;
201
+ --muted: #60736d;
202
+ --paper: #f6f4ec;
203
+ --panel: #fffdf7;
204
+ --line: #d7ded6;
205
+ --green: #19865c;
206
+ --green-soft: #dff3e9;
207
+ --orange: #e86637;
208
+ --shadow: 0 28px 80px rgba(18, 51, 42, .16);
209
+ }
210
+ * { box-sizing: border-box; }
211
+ body {
212
+ margin: 0;
213
+ min-height: 100vh;
214
+ color: var(--ink);
215
+ background:
216
+ radial-gradient(circle at 85% 12%, rgba(232, 102, 55, .13), transparent 28rem),
217
+ linear-gradient(rgba(22, 51, 43, .045) 1px, transparent 1px),
218
+ linear-gradient(90deg, rgba(22, 51, 43, .045) 1px, transparent 1px),
219
+ var(--paper);
220
+ background-size: auto, 32px 32px, 32px 32px, auto;
221
+ font-family: "Aptos", "PingFang SC", "Microsoft YaHei", sans-serif;
222
+ }
223
+ main {
224
+ width: min(1080px, calc(100% - 36px));
225
+ min-height: 100vh;
226
+ margin: 0 auto;
227
+ display: grid;
228
+ place-items: center;
229
+ padding: 48px 0;
230
+ }
231
+ .shell {
232
+ width: 100%;
233
+ overflow: hidden;
234
+ display: grid;
235
+ grid-template-columns: 11rem 1fr;
236
+ background: var(--panel);
237
+ border: 1px solid rgba(22, 51, 43, .16);
238
+ border-radius: 28px;
239
+ box-shadow: var(--shadow);
240
+ animation: arrive .55s cubic-bezier(.2, .8, .2, 1) both;
241
+ }
242
+ .rail {
243
+ position: relative;
244
+ padding: 34px 28px;
245
+ color: #f8fff9;
246
+ background: var(--ink);
247
+ display: flex;
248
+ flex-direction: column;
249
+ justify-content: space-between;
250
+ gap: 80px;
251
+ }
252
+ .rail::after {
253
+ content: "";
254
+ position: absolute;
255
+ width: 130px;
256
+ height: 130px;
257
+ right: -78px;
258
+ bottom: 62px;
259
+ border: 1px solid rgba(255, 255, 255, .17);
260
+ border-radius: 50%;
261
+ }
262
+ .mark {
263
+ width: 58px;
264
+ height: 58px;
265
+ border: 1px solid rgba(255, 255, 255, .45);
266
+ border-radius: 50%;
267
+ display: grid;
268
+ place-items: center;
269
+ font: 700 22px/1 Georgia, "Noto Serif SC", serif;
270
+ letter-spacing: -.08em;
271
+ }
272
+ .rail-label {
273
+ writing-mode: vertical-rl;
274
+ transform: rotate(180deg);
275
+ font-size: 12px;
276
+ letter-spacing: .18em;
277
+ text-transform: uppercase;
278
+ opacity: .7;
279
+ }
280
+ .content { padding: clamp(34px, 6vw, 72px); }
281
+ .status {
282
+ display: inline-flex;
283
+ align-items: center;
284
+ gap: 9px;
285
+ padding: 7px 11px;
286
+ color: #126544;
287
+ background: var(--green-soft);
288
+ border-radius: 999px;
289
+ font-size: 13px;
290
+ font-weight: 700;
291
+ letter-spacing: .04em;
292
+ }
293
+ .status::before {
294
+ content: "";
295
+ width: 8px;
296
+ height: 8px;
297
+ background: var(--green);
298
+ border-radius: 50%;
299
+ box-shadow: 0 0 0 4px rgba(25, 134, 92, .13);
300
+ }
301
+ h1 {
302
+ max-width: 700px;
303
+ margin: 25px 0 18px;
304
+ font: 700 clamp(38px, 7vw, 72px)/.98 Georgia, "Noto Serif SC", serif;
305
+ letter-spacing: -.055em;
306
+ }
307
+ h1 span { color: var(--orange); }
308
+ .lead {
309
+ max-width: 720px;
310
+ margin: 0;
311
+ color: var(--muted);
312
+ font-size: clamp(16px, 2vw, 19px);
313
+ line-height: 1.75;
314
+ }
315
+ .endpoint {
316
+ margin: 32px 0;
317
+ padding: 17px 19px;
318
+ display: grid;
319
+ grid-template-columns: auto 1fr;
320
+ gap: 14px;
321
+ align-items: center;
322
+ background: #eef1eb;
323
+ border: 1px solid var(--line);
324
+ border-left: 4px solid var(--orange);
325
+ border-radius: 12px;
326
+ }
327
+ .endpoint strong {
328
+ color: var(--muted);
329
+ font-size: 11px;
330
+ letter-spacing: .14em;
331
+ text-transform: uppercase;
332
+ }
333
+ code {
334
+ overflow-wrap: anywhere;
335
+ font-family: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
336
+ font-size: 14px;
337
+ }
338
+ .steps {
339
+ margin: 0;
340
+ padding: 0;
341
+ list-style: none;
342
+ display: grid;
343
+ grid-template-columns: repeat(3, 1fr);
344
+ border-top: 1px solid var(--line);
345
+ border-bottom: 1px solid var(--line);
346
+ }
347
+ .steps li {
348
+ min-height: 124px;
349
+ padding: 22px 20px 22px 0;
350
+ color: var(--muted);
351
+ font-size: 14px;
352
+ line-height: 1.65;
353
+ }
354
+ .steps li + li {
355
+ padding-left: 20px;
356
+ border-left: 1px solid var(--line);
357
+ }
358
+ .steps b {
359
+ display: block;
360
+ margin-bottom: 8px;
361
+ color: var(--ink);
362
+ font: 700 18px/1.2 Georgia, "Noto Serif SC", serif;
363
+ }
364
+ .steps em {
365
+ display: inline-block;
366
+ margin-right: 6px;
367
+ color: var(--orange);
368
+ font: normal 700 12px/1 "Cascadia Mono", monospace;
369
+ }
370
+ nav {
371
+ margin-top: 24px;
372
+ display: flex;
373
+ flex-wrap: wrap;
374
+ gap: 10px 24px;
375
+ }
376
+ nav a {
377
+ color: var(--ink);
378
+ font-size: 13px;
379
+ font-weight: 700;
380
+ text-decoration-color: rgba(232, 102, 55, .65);
381
+ text-underline-offset: 5px;
382
+ }
383
+ nav a:hover { color: var(--orange); }
384
+ footer {
385
+ margin-top: 38px;
386
+ color: #85948f;
387
+ font-size: 12px;
388
+ letter-spacing: .04em;
389
+ }
390
+ @keyframes arrive {
391
+ from { opacity: 0; transform: translateY(14px); }
392
+ to { opacity: 1; transform: translateY(0); }
393
+ }
394
+ @media (max-width: 720px) {
395
+ main { width: min(100% - 22px, 680px); padding: 18px 0; }
396
+ .shell { grid-template-columns: 1fr; border-radius: 20px; }
397
+ .rail { padding: 18px 22px; flex-direction: row; align-items: center; gap: 20px; }
398
+ .mark { width: 44px; height: 44px; font-size: 17px; }
399
+ .rail-label { writing-mode: horizontal-tb; transform: none; }
400
+ .content { padding: 30px 22px 34px; }
401
+ h1 { letter-spacing: -.04em; }
402
+ .endpoint { grid-template-columns: 1fr; gap: 8px; }
403
+ .steps { grid-template-columns: 1fr; }
404
+ .steps li { min-height: auto; padding: 18px 0; }
405
+ .steps li + li { padding-left: 0; border-left: 0; border-top: 1px solid var(--line); }
406
+ }
407
+ @media (prefers-reduced-motion: reduce) {
408
+ .shell { animation: none; }
409
+ }
410
+ </style>
411
+ </head>
412
+ <body>
413
+ <main>
414
+ <section class="shell" aria-labelledby="page-title">
415
+ <aside class="rail" aria-label="WukongCRM MCP">
416
+ <div class="mark" aria-hidden="true">72</div>
417
+ <div class="rail-label">Model Context Protocol</div>
418
+ </aside>
419
+ <div class="content">
420
+ <div class="status">服务在线</div>
421
+ <h1 id="page-title">WukongCRM <span>MCP</span></h1>
422
+ <p class="lead">这是一个受 OAuth 2.0 保护的远程 MCP 端点。请在支持远程 MCP 的客户端中添加服务地址,由客户端发起授权;直接打开本页不会创建授权会话。</p>
423
+
424
+ <div class="endpoint">
425
+ <strong>服务地址</strong>
426
+ <code>${endpoint}</code>
427
+ </div>
428
+
429
+ <ol class="steps" aria-label="接入步骤">
430
+ <li><b><em>01</em>添加服务</b>在客户端的 MCP 设置中填写上方地址。</li>
431
+ <li><b><em>02</em>完成授权</b>按客户端提示登录 72CRM 并确认 OAuth 授权。</li>
432
+ <li><b><em>03</em>开始使用</b>返回客户端,调用只读工具验证连接状态。</li>
433
+ </ol>
434
+
435
+ <nav aria-label="服务诊断">
436
+ <a href="${healthUrl}">健康检查</a>
437
+ <a href="${metadataUrl}">MCP 资源元数据</a>
438
+ <a href="${authorizationMetadataUrl}">OAuth 服务元数据</a>
439
+ </nav>
440
+ <footer>WukongCRM MCP · Streamable HTTP · OAuth 2.0</footer>
441
+ </div>
442
+ </section>
443
+ </main>
444
+ </body>
445
+ </html>`);
446
+ }
447
+ function escapeHtml(value) {
448
+ return value.replace(/[&<>"']/g, (character) => {
449
+ switch (character) {
450
+ case "&": return "&amp;";
451
+ case "<": return "&lt;";
452
+ case ">": return "&gt;";
453
+ case '"': return "&quot;";
454
+ case "'": return "&#39;";
455
+ default: return character;
456
+ }
457
+ });
458
+ }
175
459
  function sendMethodNotAllowed(res) {
176
460
  res.status(405).json({
177
461
  jsonrpc: "2.0",
@@ -206,9 +490,12 @@ export function loadRemoteServerEnvironment(env = process.env) {
206
490
  publicUrl,
207
491
  oauthSecret,
208
492
  dataDirectory: path.resolve(env.MCP_DATA_DIR ?? ".wukong-mcp-data"),
493
+ redisUrl: env.MCP_REDIS_URL?.trim() || undefined,
494
+ redisKeyPrefix: env.MCP_REDIS_PREFIX?.trim() || "wukong-mcp:oauth",
209
495
  crmBaseUrl: env.CRM_BASE_URL,
210
496
  crmBindingAuthorizeUrl,
211
497
  crmBindingExchangeUrl,
498
+ crmBindingExchangeAllowHttp: parseBooleanEnvironment(env.CRM_BINDING_EXCHANGE_ALLOW_HTTP ?? "false", "CRM_BINDING_EXCHANGE_ALLOW_HTTP"),
212
499
  crmBindingCallbackUrl,
213
500
  crmBindingClientId,
214
501
  crmBindingClientSecret,
@@ -244,6 +531,16 @@ function parseTrustProxy(value) {
244
531
  }
245
532
  return normalized;
246
533
  }
534
+ function parseBooleanEnvironment(value, name) {
535
+ const normalized = value.trim().toLowerCase();
536
+ if (normalized === "true") {
537
+ return true;
538
+ }
539
+ if (normalized === "false") {
540
+ return false;
541
+ }
542
+ throw new Error(`${name} 必须为 true 或 false。`);
543
+ }
247
544
  function requiredEnvironment(env, name) {
248
545
  const value = env[name]?.trim();
249
546
  if (!value) {
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { type CrmClientConfig } from "./client.js";
3
+ export type NocodeToolAccess = "read" | "write";
4
+ export interface NocodeToolDefinition {
5
+ name: string;
6
+ description: string;
7
+ schema: z.ZodTypeAny;
8
+ access: NocodeToolAccess;
9
+ destructive?: boolean;
10
+ }
11
+ type AnyArgs = Record<string, any>;
12
+ type Handler = (args: AnyArgs) => Promise<any> | any;
13
+ export type NocodeToolHandlers = Record<string, Handler>;
14
+ export declare const nocodeToolDefinitions: NocodeToolDefinition[];
15
+ export declare function createNocodeToolHandlers(config?: CrmClientConfig): NocodeToolHandlers;
16
+ export {};