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,275 @@
1
+ import { DingtalkDwsError, asDingtalkDwsError } from "../errors.js";
2
+ const emptySchema = { type: "object", properties: {}, additionalProperties: false };
3
+ export function resolveToolSurface(adapter) {
4
+ if (adapter === "webhook") {
5
+ return { adapter, authTools: false, directoryTools: false, listRoutes: false, send: true };
6
+ }
7
+ if (adapter === "dws") {
8
+ return { adapter, authTools: true, directoryTools: true, listRoutes: true, send: true };
9
+ }
10
+ // Unconfigured or config conflict: handshake only until delivery is chosen.
11
+ return { adapter, authTools: true, directoryTools: false, listRoutes: false, send: false };
12
+ }
13
+ export function buildInstructions(surface) {
14
+ // Cross-tool constraints only. Per-tool when/when-not → tool descriptions.
15
+ // User-selected recipes → prompts. Do not duplicate those here.
16
+ const lines = [
17
+ "DingTalk robot notify + dws readiness (Agent surface of the dws ecosystem). Office stays on local dws.",
18
+ "Constraint: recurring/host patrol must not use session loops or repeated dingtalk_send — Agent derives host/cron/probe from conversation, calls dingtalk_patrol_setup once (stages bundle paths + delivery credentials), copies to host via host-execution or SSH, runs install.sh on the host, then stops. MCP env holds credentials only. Do not download Node in-session.",
19
+ "Call dingtalk_doctor first; connected ≠ ready.",
20
+ ];
21
+ if (surface.adapter === null) {
22
+ lines.push("Delivery unset: configure Managed robot credentials (default) or optional webhook, then restart.");
23
+ }
24
+ else if (surface.adapter === "webhook") {
25
+ lines.push("Webhook tools: doctor + send (notify_group only).");
26
+ }
27
+ else {
28
+ lines.push("Managed tools: search → bind → send. Bind targets are for this server only.");
29
+ }
30
+ lines.push("Never put credentials or secrets in message content.");
31
+ if (surface.authTools) {
32
+ lines.push("Manual/headless login: run `dws auth login` in a local terminal when doctor asks.");
33
+ }
34
+ return lines.join(" ");
35
+ }
36
+ export function createToolDefinitions(surface) {
37
+ const tools = [];
38
+ tools.push({
39
+ name: "dingtalk_doctor",
40
+ description: "Check whether local dws and notify delivery are ready; returns next_action.",
41
+ inputSchema: emptySchema,
42
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
43
+ });
44
+ if (surface.authTools) {
45
+ tools.push({
46
+ name: "dingtalk_auth_status",
47
+ description: "Check local dws login (needed for directory search).",
48
+ inputSchema: emptySchema,
49
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
50
+ }, {
51
+ name: "dingtalk_auth_login",
52
+ description: "Start controlled dws login. Manual mode: user runs `dws auth login` locally.",
53
+ inputSchema: emptySchema,
54
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
55
+ });
56
+ }
57
+ if (surface.directoryTools) {
58
+ tools.push({
59
+ name: "dingtalk_search_targets",
60
+ description: "Find groups/users by display name; returns opaque candidate_ref for bind.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ query: { type: "string", minLength: 1 },
65
+ type: { type: "string", enum: ["all", "group", "user"] },
66
+ limit: { type: "integer", minimum: 1, maximum: 20 },
67
+ },
68
+ required: ["query"],
69
+ additionalProperties: false,
70
+ },
71
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
72
+ }, {
73
+ name: "dingtalk_configure_route_target",
74
+ description: "Bind one search result to notify_group or notify_user for later send.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ query: { type: "string", minLength: 1 },
79
+ route: { type: "string", enum: ["notify_group", "notify_user"] },
80
+ type: { type: "string", enum: ["group", "user"] },
81
+ candidate_ref: { type: "string", minLength: 1 },
82
+ },
83
+ required: ["query"],
84
+ additionalProperties: false,
85
+ },
86
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
87
+ });
88
+ }
89
+ if (surface.listRoutes) {
90
+ tools.push({
91
+ name: "dingtalk_list_routes",
92
+ description: "List whether notify_group / notify_user are bound.",
93
+ inputSchema: emptySchema,
94
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
95
+ });
96
+ }
97
+ if (surface.send) {
98
+ const routeEnum = surface.adapter === "webhook" ? ["notify_group"] : ["notify_group", "notify_user"];
99
+ tools.push({
100
+ name: "dingtalk_send",
101
+ description: surface.adapter === "webhook"
102
+ ? "Send one group message now (route=notify_group, title, content). Do not use for recurring, polling, or long-running host checks."
103
+ : "Send one message now (route, title, content). Do not use for recurring, polling, or long-running host checks.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ route: { type: "string", enum: routeEnum },
108
+ title: { type: "string", minLength: 1 },
109
+ content: { type: "string", minLength: 1 },
110
+ dedupe_key: { type: "string" },
111
+ },
112
+ required: ["route", "title", "content"],
113
+ additionalProperties: false,
114
+ },
115
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
116
+ });
117
+ }
118
+ // Always available: stage host patrol materials (Agent transfers + remote install).
119
+ tools.push({
120
+ name: "dingtalk_patrol_setup",
121
+ description: "Stage offline patrol materials only (patrol.yaml, env.sh with local delivery credentials, probe). Does not SSH. Agent fills name/cron/probe from conversation; prefer bound notify target (omit route). Then copy bundle_root + staging_dir to the host (host-execution or SSH) and run install.sh there. Call once for staging, finish install, then stop. Never returns secrets.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ name: { type: "string", minLength: 1 },
126
+ cron: { type: "string", description: "Crontab expression; Agent derives from user intent (default */15)" },
127
+ route: { type: "string", enum: ["notify_group", "notify_user"] },
128
+ probe_command: { type: "string" },
129
+ probe_script: { type: "string" },
130
+ dry_run: { type: "boolean", description: "Allow missing bind/bundle (preview/tests); still writes staging" },
131
+ },
132
+ required: ["name"],
133
+ additionalProperties: false,
134
+ },
135
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
136
+ });
137
+ return tools;
138
+ }
139
+ function asRecord(args) {
140
+ return args && typeof args === "object" && !Array.isArray(args) ? args : {};
141
+ }
142
+ function requireNonEmptyString(input, key) {
143
+ const value = input[key];
144
+ if (typeof value !== "string" || !value.trim()) {
145
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `${key} is required`);
146
+ }
147
+ return value.trim();
148
+ }
149
+ function optionalPositiveInt(value, key) {
150
+ if (value === undefined)
151
+ return undefined;
152
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
153
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `${key} must be a positive integer`);
154
+ }
155
+ return value;
156
+ }
157
+ function parseSearchType(value) {
158
+ if (value === undefined)
159
+ return undefined;
160
+ if (value === "all" || value === "group" || value === "user")
161
+ return value;
162
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "type must be all, group, or user");
163
+ }
164
+ function parseRouteName(value, allowed) {
165
+ if (typeof value !== "string" || !allowed.includes(value)) {
166
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `route must be one of: ${allowed.join(", ")}`);
167
+ }
168
+ return value;
169
+ }
170
+ function parseSendInput(input, allowedRoutes) {
171
+ const route = parseRouteName(input.route, allowedRoutes);
172
+ const title = requireNonEmptyString(input, "title");
173
+ const content = requireNonEmptyString(input, "content");
174
+ const dedupeRaw = input.dedupe_key;
175
+ let dedupe_key;
176
+ if (dedupeRaw !== undefined) {
177
+ if (typeof dedupeRaw !== "string")
178
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "dedupe_key must be a string");
179
+ dedupe_key = dedupeRaw;
180
+ }
181
+ return { route, title, content, dedupe_key };
182
+ }
183
+ function assertToolAllowed(surface, name) {
184
+ const allowed = new Set(createToolDefinitions(surface).map((tool) => tool.name));
185
+ if (!allowed.has(name)) {
186
+ throw new DingtalkDwsError("ADAPTER_UNSUPPORTED", `Tool ${name} is not available for this adapter`);
187
+ }
188
+ }
189
+ export async function callTool(service, name, args, surface) {
190
+ const resolvedSurface = surface ?? resolveToolSurface(service.adapterKind());
191
+ try {
192
+ assertToolAllowed(resolvedSurface, name);
193
+ let result;
194
+ const input = asRecord(args);
195
+ if (name === "dingtalk_doctor")
196
+ result = await service.doctor();
197
+ else if (name === "dingtalk_auth_status")
198
+ result = await service.authStatus();
199
+ else if (name === "dingtalk_auth_login")
200
+ result = await service.authLogin();
201
+ else if (name === "dingtalk_list_routes")
202
+ result = service.listRoutes();
203
+ else if (name === "dingtalk_search_targets") {
204
+ result = await service.searchTargets({
205
+ query: requireNonEmptyString(input, "query"),
206
+ type: parseSearchType(input.type),
207
+ limit: optionalPositiveInt(input.limit, "limit"),
208
+ });
209
+ }
210
+ else if (name === "dingtalk_configure_route_target") {
211
+ const route = input.route === undefined
212
+ ? undefined
213
+ : parseRouteName(input.route, ["notify_group", "notify_user"]);
214
+ let type;
215
+ if (input.type !== undefined) {
216
+ if (input.type !== "group" && input.type !== "user") {
217
+ throw new DingtalkDwsError("INVALID_ARGUMENT", "type must be group or user");
218
+ }
219
+ type = input.type;
220
+ }
221
+ result = await service.configureRouteTarget({
222
+ query: requireNonEmptyString(input, "query"),
223
+ route,
224
+ type,
225
+ candidate_ref: input.candidate_ref === undefined
226
+ ? undefined
227
+ : requireNonEmptyString(input, "candidate_ref"),
228
+ });
229
+ }
230
+ else if (name === "dingtalk_send") {
231
+ const allowed = resolvedSurface.adapter === "webhook"
232
+ ? ["notify_group"]
233
+ : ["notify_group", "notify_user"];
234
+ result = await service.send(parseSendInput(input, allowed));
235
+ }
236
+ else if (name === "dingtalk_patrol_setup") {
237
+ const optionalString = (key) => {
238
+ const value = input[key];
239
+ if (value === undefined)
240
+ return undefined;
241
+ if (typeof value !== "string")
242
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `${key} must be a string`);
243
+ return value;
244
+ };
245
+ const optionalBool = (key) => {
246
+ const value = input[key];
247
+ if (value === undefined)
248
+ return undefined;
249
+ if (typeof value !== "boolean")
250
+ throw new DingtalkDwsError("INVALID_ARGUMENT", `${key} must be a boolean`);
251
+ return value;
252
+ };
253
+ result = await service.patrolSetup({
254
+ name: requireNonEmptyString(input, "name"),
255
+ cron: optionalString("cron"),
256
+ route: input.route === undefined
257
+ ? undefined
258
+ : parseRouteName(input.route, ["notify_group", "notify_user"]),
259
+ probe_command: optionalString("probe_command"),
260
+ probe_script: optionalString("probe_script"),
261
+ dry_run: optionalBool("dry_run"),
262
+ });
263
+ }
264
+ else {
265
+ result = { code: "INVALID_ARGUMENT", message: `Unknown tool: ${name}` };
266
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
267
+ }
268
+ return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
269
+ }
270
+ catch (error) {
271
+ const err = asDingtalkDwsError(error);
272
+ const result = { code: err.code, message: err.message };
273
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
274
+ }
275
+ }
@@ -0,0 +1,40 @@
1
+ export type AdapterKind = "dws" | "webhook";
2
+ export type RouteName = "notify_group" | "notify_user";
3
+ export type RouteType = "group" | "user";
4
+ export interface AppConfig {
5
+ adapter: AdapterKind | null;
6
+ adapterError?: string;
7
+ dwsCommand: string;
8
+ clientId?: string;
9
+ clientSecret?: string;
10
+ robotCode?: string;
11
+ webhookUrl?: string;
12
+ webhookSecret?: string;
13
+ authLoginMode: "interactive" | "manual";
14
+ stateDir: string;
15
+ targetsPath: string;
16
+ idempotencyPath: string;
17
+ timeoutMs: number;
18
+ maxOutputBytes: number;
19
+ maxTitleLength: number;
20
+ maxContentLength: number;
21
+ suppressionWindowSeconds: number;
22
+ }
23
+ export interface DeliveryRequest {
24
+ routeType: RouteType;
25
+ targetId: string;
26
+ title: string;
27
+ content: string;
28
+ }
29
+ export interface TargetCandidate {
30
+ type: RouteType;
31
+ name: string;
32
+ target_id: string;
33
+ }
34
+ /** Minimal send: route + title + content; optional dedupe_key only. */
35
+ export interface SendInput {
36
+ route: RouteName;
37
+ title: string;
38
+ content: string;
39
+ dedupe_key?: string;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function mcpPackageRoot(): string;
2
+ export declare function readPackageVersion(): string;
@@ -0,0 +1,30 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function mcpPackageRoot() {
5
+ let dir = path.dirname(fileURLToPath(import.meta.url));
6
+ for (let i = 0; i < 6; i++) {
7
+ const pkgPath = path.join(dir, "package.json");
8
+ if (fs.existsSync(pkgPath)) {
9
+ try {
10
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
11
+ if (pkg.name === "dingtalk-dws-mcp")
12
+ return dir;
13
+ }
14
+ catch {
15
+ // continue
16
+ }
17
+ }
18
+ dir = path.dirname(dir);
19
+ }
20
+ throw new Error("cannot locate dingtalk-dws-mcp package root");
21
+ }
22
+ export function readPackageVersion() {
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync(path.join(mcpPackageRoot(), "package.json"), "utf8"));
25
+ return pkg.version ?? "0.0.0";
26
+ }
27
+ catch {
28
+ return "0.0.0";
29
+ }
30
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "dingtalk-dws-mcp",
3
+ "version": "1.0.0",
4
+ "description": "DingTalk robot notification MCP with dws readiness handshake",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.13.0"
9
+ },
10
+ "main": "dist/src/index.js",
11
+ "files": [
12
+ "dist/src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "bin": {
17
+ "dingtalk-dws-mcp": "dist/src/index.js"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "prepack": "npm run build",
22
+ "prepublishOnly": "npm run build && npm test",
23
+ "start": "node dist/src/index.js",
24
+ "pack:deploy": "node scripts/pack-deploy.mjs",
25
+ "test": "npm run build && vitest run",
26
+ "test:watch": "vitest",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit"
28
+ },
29
+ "keywords": [
30
+ "mcp",
31
+ "dingtalk",
32
+ "dws",
33
+ "notification"
34
+ ],
35
+ "dependencies": {
36
+ "agent-compose-notify-delivery": "^1.0.0",
37
+ "@modelcontextprotocol/sdk": "^1.29.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.0.0",
41
+ "typescript": "^5.9.3",
42
+ "vitest": "^3.2.4"
43
+ }
44
+ }