dingtalk-dws-mcp 1.0.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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +21 -0
  3. package/dist/src/config.d.ts +5 -0
  4. package/dist/src/config.js +99 -0
  5. package/dist/src/delivery/webhook.d.ts +7 -0
  6. package/dist/src/delivery/webhook.js +20 -0
  7. package/dist/src/directory/search.d.ts +4 -0
  8. package/dist/src/directory/search.js +78 -0
  9. package/dist/src/errors.d.ts +6 -0
  10. package/dist/src/errors.js +22 -0
  11. package/dist/src/handshake/instructions.d.ts +1 -0
  12. package/dist/src/handshake/instructions.js +9 -0
  13. package/dist/src/index.d.ts +2 -0
  14. package/dist/src/index.js +8 -0
  15. package/dist/src/internal/dws-auth-command.d.ts +34 -0
  16. package/dist/src/internal/dws-auth-command.js +98 -0
  17. package/dist/src/internal/dws-robot-command.d.ts +4 -0
  18. package/dist/src/internal/dws-robot-command.js +27 -0
  19. package/dist/src/internal/exec.d.ts +21 -0
  20. package/dist/src/internal/exec.js +59 -0
  21. package/dist/src/notify/bind.d.ts +7 -0
  22. package/dist/src/notify/bind.js +44 -0
  23. package/dist/src/notify/content.d.ts +2 -0
  24. package/dist/src/notify/content.js +24 -0
  25. package/dist/src/notify/idempotency-store.d.ts +16 -0
  26. package/dist/src/notify/idempotency-store.js +57 -0
  27. package/dist/src/patrol/setup.d.ts +27 -0
  28. package/dist/src/patrol/setup.js +213 -0
  29. package/dist/src/prompts.d.ts +21 -0
  30. package/dist/src/prompts.js +37 -0
  31. package/dist/src/server.d.ts +44 -0
  32. package/dist/src/server.js +126 -0
  33. package/dist/src/service.d.ts +33 -0
  34. package/dist/src/service.js +276 -0
  35. package/dist/src/tools/registry.d.ts +49 -0
  36. package/dist/src/tools/registry.js +275 -0
  37. package/dist/src/types.d.ts +40 -0
  38. package/dist/src/types.js +1 -0
  39. package/dist/src/version.d.ts +2 -0
  40. package/dist/src/version.js +30 -0
  41. package/package.json +44 -0
@@ -0,0 +1,27 @@
1
+ import * as path from "node:path";
2
+ import { sendRobotMessage as libSendRobotMessage, } from "agent-compose-notify-delivery";
3
+ export async function sendRobotMessage(config, request, runner, env = process.env) {
4
+ if (!config.clientId || !config.clientSecret || !config.robotCode) {
5
+ return {
6
+ status: "failed",
7
+ errorCode: "ADAPTER_UNAUTHORIZED",
8
+ error: "Robot application credentials are required",
9
+ };
10
+ }
11
+ return libSendRobotMessage({
12
+ dwsCommand: config.dwsCommand,
13
+ clientId: config.clientId,
14
+ clientSecret: config.clientSecret,
15
+ robotCode: config.robotCode,
16
+ configDir: path.join(config.stateDir, "dws-robot"),
17
+ timeoutMs: config.timeoutMs,
18
+ maxOutputBytes: config.maxOutputBytes,
19
+ runner,
20
+ env,
21
+ }, {
22
+ routeType: request.routeType,
23
+ targetId: request.targetId,
24
+ title: request.title,
25
+ content: request.content,
26
+ });
27
+ }
@@ -0,0 +1,21 @@
1
+ export type CommandRunner = (file: string, args: readonly string[], options: {
2
+ timeout: number;
3
+ maxBuffer: number;
4
+ env: NodeJS.ProcessEnv;
5
+ windowsHide: boolean;
6
+ }) => Promise<{
7
+ stdout: string;
8
+ stderr: string;
9
+ }>;
10
+ export declare function pickEnv(names: readonly string[], env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
11
+ export declare function userDwsEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
12
+ export declare function runCommand(command: string, args: readonly string[], options: {
13
+ timeoutMs: number;
14
+ maxOutputBytes: number;
15
+ env: NodeJS.ProcessEnv;
16
+ runner?: CommandRunner;
17
+ }): Promise<{
18
+ stdout: string;
19
+ stderr: string;
20
+ }>;
21
+ export declare function parseJsonObject(stdout: string): Record<string, unknown>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Internal runtime dependency only.
3
+ * Not an exposed command-execution interface.
4
+ * Agent must never gain arbitrary `dws <anything>` through this layer.
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { redact } from "agent-compose-notify-delivery";
9
+ const execFileAsync = promisify(execFile);
10
+ const USER_DWS_ENV = [
11
+ "HOME", "USERPROFILE", "PATH", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
12
+ "XDG_CONFIG_HOME", "XDG_DATA_HOME", "BROWSER", "DISPLAY", "WAYLAND_DISPLAY",
13
+ "NODE_EXTRA_CA_CERTS", "DWS_CONFIG_DIR",
14
+ ];
15
+ export function pickEnv(names, env) {
16
+ const result = {};
17
+ for (const name of names) {
18
+ if (env[name] !== undefined)
19
+ result[name] = env[name];
20
+ }
21
+ return result;
22
+ }
23
+ export function userDwsEnv(env = process.env) {
24
+ return pickEnv(USER_DWS_ENV, env);
25
+ }
26
+ export async function runCommand(command, args, options) {
27
+ const runner = options.runner ?? execFileAsync;
28
+ try {
29
+ return await runner(command, args, {
30
+ timeout: options.timeoutMs,
31
+ maxBuffer: options.maxOutputBytes,
32
+ env: options.env,
33
+ windowsHide: true,
34
+ });
35
+ }
36
+ catch (error) {
37
+ const details = error && typeof error === "object" ? error : {};
38
+ const stdout = typeof details.stdout === "string" ? details.stdout : "";
39
+ const stderr = typeof details.stderr === "string" ? details.stderr : "";
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ const wrapped = Object.assign(new Error(redact(`${message}\n${stdout}\n${stderr}`)), {
42
+ code: details.code,
43
+ killed: details.killed,
44
+ stdout: redact(stdout),
45
+ stderr: redact(stderr),
46
+ });
47
+ throw wrapped;
48
+ }
49
+ }
50
+ export function parseJsonObject(stdout) {
51
+ const trimmed = stdout.trim();
52
+ if (!trimmed)
53
+ throw new Error("Command produced no JSON output");
54
+ const value = JSON.parse(trimmed);
55
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
56
+ throw new Error("Command returned non-object JSON");
57
+ }
58
+ return value;
59
+ }
@@ -0,0 +1,7 @@
1
+ import type { AppConfig, RouteName, RouteType, TargetCandidate } from "../types.js";
2
+ export declare function candidateRef(candidate: TargetCandidate): string;
3
+ export declare function readTargets(config: AppConfig): Record<string, string>;
4
+ export declare function writeTarget(config: AppConfig, route: RouteName, targetId: string): void;
5
+ export declare function resolveRouteTarget(config: AppConfig, route: RouteName): string;
6
+ export declare function routeType(route: RouteName): RouteType;
7
+ export declare function isRouteConfigured(config: AppConfig, route: RouteName): boolean;
@@ -0,0 +1,44 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { DingtalkDwsError } from "../errors.js";
5
+ export function candidateRef(candidate) {
6
+ return `candidate_${createHash("sha256").update(`${candidate.type}\n${candidate.target_id}`).digest("hex")}`;
7
+ }
8
+ export function readTargets(config) {
9
+ try {
10
+ const parsed = JSON.parse(fs.readFileSync(config.targetsPath, "utf8"));
11
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
12
+ return parsed;
13
+ }
14
+ catch {
15
+ // first bind
16
+ }
17
+ return {};
18
+ }
19
+ export function writeTarget(config, route, targetId) {
20
+ fs.mkdirSync(path.dirname(config.targetsPath), { recursive: true, mode: 0o700 });
21
+ const targets = readTargets(config);
22
+ targets[route] = targetId;
23
+ fs.writeFileSync(config.targetsPath, `${JSON.stringify(targets, null, 2)}\n`, { mode: 0o600 });
24
+ }
25
+ export function resolveRouteTarget(config, route) {
26
+ if (config.adapter === "webhook") {
27
+ if (route !== "notify_group") {
28
+ throw new DingtalkDwsError("ADAPTER_UNSUPPORTED", "Webhook only supports notify_group");
29
+ }
30
+ return "webhook";
31
+ }
32
+ const target = readTargets(config)[route];
33
+ if (!target)
34
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `Route ${route} has no bound target`);
35
+ return target;
36
+ }
37
+ export function routeType(route) {
38
+ return route === "notify_group" ? "group" : "user";
39
+ }
40
+ export function isRouteConfigured(config, route) {
41
+ if (config.adapter === "webhook")
42
+ return route === "notify_group" && Boolean(config.webhookUrl);
43
+ return Boolean(readTargets(config)[route]);
44
+ }
@@ -0,0 +1,2 @@
1
+ import type { AppConfig, SendInput } from "../types.js";
2
+ export declare function validateSendContent(input: SendInput, config: AppConfig): void;
@@ -0,0 +1,24 @@
1
+ import { DingtalkDwsError } from "../errors.js";
2
+ const SENSITIVE = [
3
+ { name: "private key", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i },
4
+ { name: "bearer token", pattern: /\bBearer\s+[A-Za-z0-9._~+/-]{16,}={0,2}\b/i },
5
+ { name: "secret assignment", pattern: /\b(?:clientSecret|appSecret|api[_-]?key|access[_-]?token|password)\s*[:=]\s*[^\s]{8,}/i },
6
+ ];
7
+ export function validateSendContent(input, config) {
8
+ if (!input.title || input.title.length > config.maxTitleLength) {
9
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `title must be 1-${config.maxTitleLength} characters`);
10
+ }
11
+ if (!input.content || input.content.length > config.maxContentLength) {
12
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `content must be 1-${config.maxContentLength} characters`);
13
+ }
14
+ if (input.dedupe_key && !/^[A-Za-z0-9._:-]{1,160}$/.test(input.dedupe_key)) {
15
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "dedupe_key has an invalid format");
16
+ }
17
+ for (const value of [input.title, input.content]) {
18
+ for (const rule of SENSITIVE) {
19
+ if (rule.pattern.test(value)) {
20
+ throw new DingtalkDwsError("CONTENT_REJECTED", `Message contains a possible ${rule.name}`);
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,16 @@
1
+ export interface ClaimResult {
2
+ claimed: boolean;
3
+ state?: "pending" | "sent" | "failed" | "unknown";
4
+ messageId?: string;
5
+ }
6
+ /** Tiny JSON file store (no SQLite) for optional send dedupe_key. */
7
+ export declare class IdempotencyStore {
8
+ private readonly memory;
9
+ private readonly filePath;
10
+ constructor(databasePath: string);
11
+ private persist;
12
+ claim(key: string, nowMs: number, ttlSeconds: number): ClaimResult;
13
+ mark(key: string, state: NonNullable<ClaimResult["state"]>, messageId?: string): void;
14
+ close(): void;
15
+ }
16
+ export declare function sha256(value: string): string;
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ /** Tiny JSON file store (no SQLite) for optional send dedupe_key. */
5
+ export class IdempotencyStore {
6
+ memory = new Map();
7
+ filePath;
8
+ constructor(databasePath) {
9
+ if (databasePath === ":memory:") {
10
+ this.filePath = null;
11
+ return;
12
+ }
13
+ this.filePath = databasePath;
14
+ fs.mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
15
+ try {
16
+ const raw = JSON.parse(fs.readFileSync(databasePath, "utf8"));
17
+ for (const [key, entry] of Object.entries(raw)) {
18
+ if (entry && typeof entry.expiresAt === "number")
19
+ this.memory.set(key, entry);
20
+ }
21
+ }
22
+ catch {
23
+ // first use
24
+ }
25
+ }
26
+ persist() {
27
+ if (!this.filePath)
28
+ return;
29
+ const obj = {};
30
+ for (const [key, entry] of this.memory)
31
+ obj[key] = entry;
32
+ fs.writeFileSync(this.filePath, `${JSON.stringify(obj)}\n`, { mode: 0o600 });
33
+ }
34
+ claim(key, nowMs, ttlSeconds) {
35
+ const row = this.memory.get(key);
36
+ if (row && row.expiresAt > nowMs && row.state !== "failed") {
37
+ return { claimed: false, state: row.state, messageId: row.messageId };
38
+ }
39
+ this.memory.set(key, { state: "pending", expiresAt: nowMs + ttlSeconds * 1000 });
40
+ this.persist();
41
+ return { claimed: true };
42
+ }
43
+ mark(key, state, messageId) {
44
+ const row = this.memory.get(key);
45
+ if (!row)
46
+ return;
47
+ row.state = state;
48
+ row.messageId = messageId;
49
+ this.persist();
50
+ }
51
+ close() {
52
+ this.persist();
53
+ }
54
+ }
55
+ export function sha256(value) {
56
+ return createHash("sha256").update(value).digest("hex");
57
+ }
@@ -0,0 +1,27 @@
1
+ import type { AppConfig, RouteName } from "../types.js";
2
+ /** Agent-filled args (not mcp.json forms). Delivery credentials come from MCP env → env.sh. */
3
+ export type PatrolSetupInput = {
4
+ name: string;
5
+ cron?: string;
6
+ probe_command?: string;
7
+ probe_script?: string;
8
+ route?: RouteName;
9
+ /** Soft mode: allow missing bind/bundle (tests / preview). Always stages only. */
10
+ dry_run?: boolean;
11
+ };
12
+ export declare function resolveBundleRoot(env?: NodeJS.ProcessEnv): string;
13
+ export declare function readBundleInfo(env?: NodeJS.ProcessEnv): {
14
+ available: boolean;
15
+ bundle_root: string;
16
+ fingerprint: string | null;
17
+ server_version: string | null;
18
+ includes_node: boolean;
19
+ includes_dws: boolean;
20
+ message: string;
21
+ };
22
+ /**
23
+ * Stage offline patrol materials only. Does not SSH/scp.
24
+ * Agent copies bundle_root + staging_dir to the host (host-execution or SSH), then runs install.sh there.
25
+ */
26
+ export declare function runPatrolSetup(config: AppConfig, input: PatrolSetupInput, env?: NodeJS.ProcessEnv): Promise<Record<string, unknown>>;
27
+ export declare function bundleFingerprintForDoctor(env?: NodeJS.ProcessEnv): Record<string, unknown>;
@@ -0,0 +1,213 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "agent-compose-notify-delivery";
5
+ import { DingtalkDwsError } from "../errors.js";
6
+ import { isRouteConfigured, readTargets } from "../notify/bind.js";
7
+ import { mcpPackageRoot, readPackageVersion } from "../version.js";
8
+ const DEFAULT_CRON = "*/15 * * * *";
9
+ const DEFAULT_PROBE = `#!/usr/bin/env bash
10
+ set -euo pipefail
11
+ printf '%s\\n' '{"status":"ok","summary":"【进度】示例探针正常\\n【监控】请替换为真实检查","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}'
12
+ `;
13
+ export function resolveBundleRoot(env = process.env) {
14
+ const override = env.DINGTALK_PATROL_BUNDLE_ROOT?.trim();
15
+ if (override)
16
+ return path.resolve(override);
17
+ return path.join(mcpPackageRoot(), "deploy", "linux-x64");
18
+ }
19
+ export function readBundleInfo(env = process.env) {
20
+ const bundle_root = resolveBundleRoot(env);
21
+ const manifestPath = path.join(bundle_root, "manifest.json");
22
+ const ready = fs.existsSync(manifestPath) && fs.existsSync(path.join(bundle_root, "runtime", "patrol"));
23
+ if (!ready) {
24
+ return {
25
+ available: false,
26
+ bundle_root,
27
+ fingerprint: null,
28
+ server_version: null,
29
+ includes_node: false,
30
+ includes_dws: false,
31
+ message: `Patrol bundle missing at ${bundle_root}. Run npm run pack:deploy.`,
32
+ };
33
+ }
34
+ try {
35
+ const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
36
+ return {
37
+ available: true,
38
+ bundle_root,
39
+ fingerprint: typeof m.fingerprint === "string" ? m.fingerprint : null,
40
+ server_version: typeof m.server_version === "string" ? m.server_version : null,
41
+ includes_node: Boolean(m.includes_node),
42
+ includes_dws: Boolean(m.includes_dws),
43
+ message: "bundle ready",
44
+ };
45
+ }
46
+ catch {
47
+ return {
48
+ available: false,
49
+ bundle_root,
50
+ fingerprint: null,
51
+ server_version: null,
52
+ includes_node: false,
53
+ includes_dws: false,
54
+ message: `Invalid manifest at ${manifestPath}`,
55
+ };
56
+ }
57
+ }
58
+ function shellQuote(value) {
59
+ return `'${value.replace(/'/g, `'\\''`)}'`;
60
+ }
61
+ function writeLf(filePath, content, mode) {
62
+ const body = content.replace(/\r\n/g, "\n");
63
+ if (mode === undefined)
64
+ fs.writeFileSync(filePath, body, "utf8");
65
+ else
66
+ fs.writeFileSync(filePath, body, { mode });
67
+ }
68
+ /** Sync local notify credentials (already verified by doctor) into target env.sh. Not user OAuth.
69
+ * Never copy laptop DINGTALK_DWS_COMMAND / config.dwsCommand — that path is Agent-host local
70
+ * (Windows .exe or Linux absolute) and is wrong on the patrol target. Target resolves `dws` via
71
+ * install runtime PATH (bundled linux ELF) or system dws. Optional target-only override:
72
+ * DINGTALK_PATROL_HOST_DWS (path that exists on the target machine).
73
+ */
74
+ function writeEnvFile(filePath, config, env) {
75
+ const lines = ["# Generated by dingtalk_patrol_setup — delivery credentials for target host"];
76
+ const put = (key, value) => {
77
+ if (value)
78
+ lines.push(`export ${key}=${shellQuote(value)}`);
79
+ };
80
+ put("DINGTALK_CLIENT_ID", config.clientId ?? env.DINGTALK_CLIENT_ID);
81
+ put("DINGTALK_CLIENT_SECRET", config.clientSecret ?? env.DINGTALK_CLIENT_SECRET);
82
+ put("DINGTALK_ROBOT_CODE", config.robotCode ?? env.DINGTALK_ROBOT_CODE);
83
+ put("DINGTALK_WEBHOOK_URL", config.webhookUrl ?? env.DINGTALK_WEBHOOK_URL);
84
+ put("DINGTALK_WEBHOOK_SECRET", config.webhookSecret ?? env.DINGTALK_WEBHOOK_SECRET);
85
+ const hostDws = env.DINGTALK_PATROL_HOST_DWS?.trim();
86
+ if (hostDws)
87
+ put("DINGTALK_DWS_COMMAND", hostDws);
88
+ writeLf(filePath, `${lines.join("\n")}\n`, 0o600);
89
+ }
90
+ function pickRoute(config, preferred) {
91
+ if (config.adapter === "webhook")
92
+ return "notify_group";
93
+ if (preferred) {
94
+ if (!isRouteConfigured(config, preferred)) {
95
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `Route ${preferred} is not bound`);
96
+ }
97
+ return preferred;
98
+ }
99
+ if (isRouteConfigured(config, "notify_group"))
100
+ return "notify_group";
101
+ if (isRouteConfigured(config, "notify_user"))
102
+ return "notify_user";
103
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "Bind a notify target first (or use webhook)");
104
+ }
105
+ function buildYaml(options) {
106
+ const delivery = [` type: ${options.deliveryType}`];
107
+ if (options.target)
108
+ delivery.push(` ${options.target.field}: ${JSON.stringify(options.target.id)}`);
109
+ const body = DEFAULT_PATROL_MESSAGE_BODY.split("\n").map((l) => ` ${l}`).join("\n");
110
+ return `name: ${options.name}
111
+ cron: ${JSON.stringify(options.cron)}
112
+ probe:
113
+ exec:
114
+ command: ${JSON.stringify(options.probeCommand)}
115
+ timeout_sec: 60
116
+ delivery:
117
+ ${delivery.join("\n")}
118
+ template:
119
+ title: ${JSON.stringify(DEFAULT_PATROL_MESSAGE_TITLE)}
120
+ body: |
121
+ ${body}
122
+ `;
123
+ }
124
+ /**
125
+ * Stage offline patrol materials only. Does not SSH/scp.
126
+ * Agent copies bundle_root + staging_dir to the host (host-execution or SSH), then runs install.sh there.
127
+ */
128
+ export async function runPatrolSetup(config, input, env = process.env) {
129
+ const name = input.name.trim();
130
+ if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
131
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "name must be a single path segment");
132
+ }
133
+ if (!config.adapter) {
134
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "Set DINGTALK_CLIENT_ID/SECRET or DINGTALK_WEBHOOK_URL in MCP env");
135
+ }
136
+ const cron = input.cron?.trim() || DEFAULT_CRON;
137
+ const bundle = readBundleInfo(env);
138
+ if (!bundle.available && !input.dry_run) {
139
+ throw new DingtalkDwsError("ADAPTER_UNAVAILABLE", bundle.message);
140
+ }
141
+ const route = pickRoute(config, input.route);
142
+ const deliveryType = config.adapter;
143
+ let target;
144
+ if (deliveryType === "dws") {
145
+ if (!isRouteConfigured(config, route) && !input.dry_run) {
146
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `Bind ${route} before setup`);
147
+ }
148
+ const id = readTargets(config)[route];
149
+ if (id) {
150
+ target = route === "notify_group"
151
+ ? { field: "open_conversation_id", id }
152
+ : { field: "user_id", id };
153
+ }
154
+ }
155
+ const stagingRoot = path.join(env.DINGTALK_PATROL_STAGING_DIR?.trim()
156
+ ? path.resolve(env.DINGTALK_PATROL_STAGING_DIR)
157
+ : path.join(os.tmpdir(), "dingtalk-patrol-staging"), name);
158
+ fs.mkdirSync(path.join(stagingRoot, "probes"), { recursive: true, mode: 0o700 });
159
+ const probeCommand = input.probe_command?.trim() || "./probes/example.sh";
160
+ const probeFile = path.join(stagingRoot, "probes", path.basename(probeCommand) === probeCommand ? "example.sh" : path.basename(probeCommand));
161
+ const script = input.probe_script ?? DEFAULT_PROBE;
162
+ writeLf(probeFile, script.endsWith("\n") ? script : `${script}\n`, 0o755);
163
+ const yamlPath = path.join(stagingRoot, "patrol.yaml");
164
+ const envPath = path.join(stagingRoot, "env.sh");
165
+ writeLf(yamlPath, buildYaml({
166
+ name,
167
+ cron,
168
+ probeCommand: probeCommand.includes("/") || probeCommand.includes("\\")
169
+ ? probeCommand
170
+ : `./probes/${path.basename(probeFile)}`,
171
+ deliveryType,
172
+ target,
173
+ }));
174
+ writeEnvFile(envPath, config, env);
175
+ const response = {
176
+ ok: true,
177
+ name,
178
+ cron,
179
+ route,
180
+ next_action: "install_on_host",
181
+ staging_dir: stagingRoot,
182
+ bundle_root: bundle.bundle_root,
183
+ bundle_fingerprint: bundle.fingerprint,
184
+ includes_node: bundle.includes_node,
185
+ includes_dws: bundle.includes_dws,
186
+ install_hint: "Copy bundle_root (runtime+install.sh) and staging_dir to the host, then run: install.sh <staging>",
187
+ server_version: readPackageVersion(),
188
+ paths: { yaml: yamlPath, env: envPath, probe: probeFile },
189
+ };
190
+ if (deliveryType === "dws" && bundle.available && !bundle.includes_dws) {
191
+ response.warning =
192
+ "bundle includes_dws=false: target needs system dws on PATH, or re-pack with linux ELF (DINGTALK_PATROL_DWS_PATH)";
193
+ }
194
+ const serialized = JSON.stringify(response);
195
+ for (const secret of [config.clientSecret, config.webhookSecret, env.DINGTALK_CLIENT_SECRET, env.DINGTALK_WEBHOOK_SECRET]) {
196
+ if (secret && serialized.includes(secret)) {
197
+ throw new DingtalkDwsError("INTERNAL_ERROR", "refusing to return secrets");
198
+ }
199
+ }
200
+ return response;
201
+ }
202
+ export function bundleFingerprintForDoctor(env = process.env) {
203
+ const info = readBundleInfo(env);
204
+ return {
205
+ available: info.available,
206
+ fingerprint: info.fingerprint,
207
+ server_version: info.server_version ?? readPackageVersion(),
208
+ includes_node: info.includes_node,
209
+ includes_dws: info.includes_dws,
210
+ path: info.bundle_root,
211
+ message: info.message,
212
+ };
213
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MCP prompts = user-controlled recipes (slash / menu).
3
+ * Do NOT put server-wide constraints here — those belong in initialize `instructions`.
4
+ * Do NOT put per-tool when/when-not here — that belongs in tool `description`.
5
+ */
6
+ import type { ToolSurface } from "./tools/registry.js";
7
+ export type PromptDefinition = {
8
+ name: string;
9
+ description: string;
10
+ };
11
+ export declare function listPromptDefinitions(_surface: ToolSurface): PromptDefinition[];
12
+ export declare function getPromptMessages(name: string): {
13
+ description: string;
14
+ messages: Array<{
15
+ role: "user";
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ };
20
+ }>;
21
+ };
@@ -0,0 +1,37 @@
1
+ const NOTIFY_RECIPE = `One-shot DingTalk notify (user-selected recipe):
2
+ 1. dingtalk_doctor → follow next_action
3
+ 2. Managed: search → bind → dingtalk_send (route, title, content)
4
+ 3. Webhook: dingtalk_send with route=notify_group only`;
5
+ const PATROL_RECIPE = `Host Patrol Runtime (user-selected recipe):
6
+ 1. dingtalk_doctor (credentials in MCP env only)
7
+ 2. Managed: bind notify target if needed
8
+ 3. From conversation: host + what to check + how often → call dingtalk_patrol_setup once (stages materials + delivery credentials)
9
+ 4. Copy bundle_root + staging_dir to the host (host-execution if available, else SSH with user-provided IP/user/password or key), run install.sh on the host
10
+ 5. Stop — do not download Node on the laptop`;
11
+ export function listPromptDefinitions(_surface) {
12
+ return [
13
+ {
14
+ name: "dingtalk_notify",
15
+ description: "User recipe: send one DingTalk notification with this server's tools.",
16
+ },
17
+ {
18
+ name: "dingtalk_patrol",
19
+ description: "User recipe: set up host-side Patrol Runtime (not a session loop).",
20
+ },
21
+ ];
22
+ }
23
+ export function getPromptMessages(name) {
24
+ if (name === "dingtalk_notify") {
25
+ return {
26
+ description: "One-shot notify recipe",
27
+ messages: [{ role: "user", content: { type: "text", text: NOTIFY_RECIPE } }],
28
+ };
29
+ }
30
+ if (name === "dingtalk_patrol") {
31
+ return {
32
+ description: "Host patrol setup recipe",
33
+ messages: [{ role: "user", content: { type: "text", text: PATROL_RECIPE } }],
34
+ };
35
+ }
36
+ throw new Error(`Unknown prompt: ${name}`);
37
+ }
@@ -0,0 +1,44 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { IdempotencyStore } from "./notify/idempotency-store.js";
3
+ import { DingtalkDwsService } from "./service.js";
4
+ export declare const MCP_SERVER_NAME = "dingtalk-dws";
5
+ export declare const MCP_SERVER_VERSION: string;
6
+ export declare function createMcpServer(service?: DingtalkDwsService, store?: IdempotencyStore): {
7
+ server: Server<{
8
+ method: string;
9
+ params?: {
10
+ [x: string]: unknown;
11
+ _meta?: {
12
+ [x: string]: unknown;
13
+ progressToken?: string | number | undefined;
14
+ "io.modelcontextprotocol/related-task"?: {
15
+ taskId: string;
16
+ } | undefined;
17
+ } | undefined;
18
+ } | undefined;
19
+ }, {
20
+ method: string;
21
+ params?: {
22
+ [x: string]: unknown;
23
+ _meta?: {
24
+ [x: string]: unknown;
25
+ progressToken?: string | number | undefined;
26
+ "io.modelcontextprotocol/related-task"?: {
27
+ taskId: string;
28
+ } | undefined;
29
+ } | undefined;
30
+ } | undefined;
31
+ }, {
32
+ [x: string]: unknown;
33
+ _meta?: {
34
+ [x: string]: unknown;
35
+ progressToken?: string | number | undefined;
36
+ "io.modelcontextprotocol/related-task"?: {
37
+ taskId: string;
38
+ } | undefined;
39
+ } | undefined;
40
+ }>;
41
+ service: DingtalkDwsService;
42
+ store: IdempotencyStore;
43
+ };
44
+ export declare function runStdioServer(): Promise<void>;