dingtalk-dws-mcp 1.0.0 → 1.0.2

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  源码包。用户能力与接入见 [`mcp/dingtalk-dws-mcp/GUIDE.md`](../../../mcp/dingtalk-dws-mcp/GUIDE.md)。
4
4
 
5
- Agent:`instructions` / tool `description` / `prompts`。长期巡检:`dingtalk_patrol_setup` 备料 → 传装到主机 → 停。
5
+ Agent:`instructions` / tool `description` / `prompts` / **Resources(`guides/*.md`)**。长期巡检:`dingtalk_patrol_setup` 备料 → 传装到主机 → 停。
6
6
 
7
7
  ## 构建
8
8
 
@@ -16,6 +16,6 @@ npm run pack:deploy
16
16
  可选:`DINGTALK_PATROL_INCLUDE_NODE=0`、`DINGTALK_PATROL_NODE_URL`、`DINGTALK_PATROL_INCLUDE_DWS=0`(默认尝试打入 linux ELF dws;Windows 打包机需 `DINGTALK_PATROL_DWS_PATH` 指向 ELF)。
17
17
  `dingtalk_patrol_setup` 只写 staging(含本机已验证投递凭据);传装用 host-execution 或 SSH,目标机跑 `install.sh`。可选 `deploy.sh` 为直连 SSH helper。
18
18
 
19
- 配置示例:[`mcp/dingtalk-dws-mcp/mcp.example.json`](../../../mcp/dingtalk-dws-mcp/mcp.example.json)。
19
+ 配置示例(npx):[`mcp/dingtalk-dws-mcp/mcp.example.json`](../../../mcp/dingtalk-dws-mcp/mcp.example.json)。npm:`dingtalk-dws-mcp@1.0.2`。
20
20
 
21
21
  规格(开发者):[RFC-0007](../../../docs/rfc/RFC-0007-dingtalk-dws-mcp.md)、[RFC-0008](../../../docs/rfc/RFC-0008-dingtalk-notify-patrol.md)。
@@ -1,4 +1,4 @@
1
- import { type DeliveryResult } from "agent-compose-notify-delivery";
1
+ import { type DeliveryResult } from "../notify-delivery/index.js";
2
2
  import type { AppConfig, DeliveryRequest } from "../types.js";
3
3
  export declare function validateWebhookConfig(config: AppConfig): {
4
4
  valid: boolean;
@@ -1,4 +1,4 @@
1
- import { sendWebhook as libSendWebhook, validateWebhookUrl, } from "agent-compose-notify-delivery";
1
+ import { sendWebhook as libSendWebhook, validateWebhookUrl, } from "../notify-delivery/index.js";
2
2
  export function validateWebhookConfig(config) {
3
3
  return validateWebhookUrl(config.webhookUrl);
4
4
  }
package/dist/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { redact } from "agent-compose-notify-delivery";
2
+ import { redact } from "./notify-delivery/index.js";
3
3
  import { runStdioServer } from "./server.js";
4
4
  runStdioServer().catch((error) => {
5
5
  const message = error instanceof Error ? error.message : String(error);
@@ -1,4 +1,4 @@
1
- import { type CommandRunner, type DeliveryResult } from "agent-compose-notify-delivery";
1
+ import { type CommandRunner, type DeliveryResult } from "../notify-delivery/index.js";
2
2
  import type { AppConfig, DeliveryRequest } from "../types.js";
3
3
  export type { CommandRunner };
4
4
  export declare function sendRobotMessage(config: AppConfig, request: DeliveryRequest, runner?: CommandRunner, env?: NodeJS.ProcessEnv): Promise<DeliveryResult>;
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { sendRobotMessage as libSendRobotMessage, } from "agent-compose-notify-delivery";
2
+ import { sendRobotMessage as libSendRobotMessage, } from "../notify-delivery/index.js";
3
3
  export async function sendRobotMessage(config, request, runner, env = process.env) {
4
4
  if (!config.clientId || !config.clientSecret || !config.robotCode) {
5
5
  return {
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
- import { redact } from "agent-compose-notify-delivery";
8
+ import { redact } from "../notify-delivery/index.js";
9
9
  const execFileAsync = promisify(execFile);
10
10
  const USER_DWS_ENV = [
11
11
  "HOME", "USERPROFILE", "PATH", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
@@ -0,0 +1,24 @@
1
+ import type { DeliveryMessage, DeliveryResult } from "./result.js";
2
+ export type CommandRunner = (file: string, args: readonly string[], options: {
3
+ timeout: number;
4
+ maxBuffer: number;
5
+ env: NodeJS.ProcessEnv;
6
+ windowsHide: boolean;
7
+ }) => Promise<{
8
+ stdout: string;
9
+ stderr: string;
10
+ }>;
11
+ export declare function robotDwsEnv(configDir: string, clientId: string, clientSecret: string, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
12
+ export declare function normalizeRobotResult(value: unknown): DeliveryResult;
13
+ export interface RobotSendOptions {
14
+ dwsCommand: string;
15
+ clientId: string;
16
+ clientSecret: string;
17
+ robotCode: string;
18
+ configDir: string;
19
+ timeoutMs: number;
20
+ maxOutputBytes: number;
21
+ runner?: CommandRunner;
22
+ env?: NodeJS.ProcessEnv;
23
+ }
24
+ export declare function sendRobotMessage(options: RobotSendOptions, message: DeliveryMessage): Promise<DeliveryResult>;
@@ -0,0 +1,129 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { redact } from "./redact.js";
4
+ const execFileAsync = promisify(execFile);
5
+ const GROUP_PATH = "/v1.0/robot/groupMessages/send";
6
+ const USER_PATH = "/v1.0/robot/oToMessages/batchSend";
7
+ const USER_DWS_ENV = [
8
+ "HOME", "USERPROFILE", "PATH", "USER", "LOGNAME", "TMPDIR", "TEMP", "TMP",
9
+ "XDG_CONFIG_HOME", "XDG_DATA_HOME", "BROWSER", "DISPLAY", "WAYLAND_DISPLAY",
10
+ "NODE_EXTRA_CA_CERTS", "DWS_CONFIG_DIR",
11
+ ];
12
+ function pickEnv(names, env) {
13
+ const result = {};
14
+ for (const name of names) {
15
+ if (env[name] !== undefined)
16
+ result[name] = env[name];
17
+ }
18
+ return result;
19
+ }
20
+ export function robotDwsEnv(configDir, clientId, clientSecret, env = process.env) {
21
+ return {
22
+ ...pickEnv(USER_DWS_ENV, env),
23
+ DWS_CONFIG_DIR: configDir,
24
+ DWS_CLIENT_ID: clientId,
25
+ DWS_CLIENT_SECRET: clientSecret,
26
+ };
27
+ }
28
+ function nested(root) {
29
+ const values = [root];
30
+ for (const key of ["result", "body", "payload", "response", "data"]) {
31
+ const candidate = root[key];
32
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
33
+ values.push(candidate);
34
+ }
35
+ }
36
+ return values;
37
+ }
38
+ function stringField(records, keys) {
39
+ for (const record of records) {
40
+ for (const key of keys) {
41
+ if (typeof record[key] === "string" && record[key])
42
+ return record[key];
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ function parseJsonObject(stdout) {
48
+ const trimmed = stdout.trim();
49
+ if (!trimmed)
50
+ throw new Error("Command produced no JSON output");
51
+ const value = JSON.parse(trimmed);
52
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
53
+ throw new Error("Command returned non-object JSON");
54
+ }
55
+ return value;
56
+ }
57
+ export function normalizeRobotResult(value) {
58
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
59
+ return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Non-object response" };
60
+ }
61
+ const root = value;
62
+ const records = nested(root);
63
+ const errorRecord = root.error && typeof root.error === "object" ? root.error : undefined;
64
+ const error = typeof errorRecord?.message === "string" ? errorRecord.message : undefined;
65
+ const explicitSuccess = records.find((item) => typeof item.success === "boolean")?.success;
66
+ if (explicitSuccess === false || error) {
67
+ return { status: "failed", errorCode: "ADAPTER_REJECTED", error: error ?? "Request rejected" };
68
+ }
69
+ const messageId = stringField(records, ["processQueryKey", "messageId", "taskId", "id"]);
70
+ if (explicitSuccess === true || messageId) {
71
+ return { status: "sent", messageId };
72
+ }
73
+ return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Delivery was not confirmed" };
74
+ }
75
+ export async function sendRobotMessage(options, message) {
76
+ if (!options.clientId || !options.clientSecret || !options.robotCode) {
77
+ return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Robot application credentials are required" };
78
+ }
79
+ const apiPath = message.routeType === "group" ? GROUP_PATH : USER_PATH;
80
+ const payload = {
81
+ robotCode: options.robotCode,
82
+ msgKey: "sampleMarkdown",
83
+ msgParam: JSON.stringify({ title: message.title, text: message.content }),
84
+ };
85
+ if (message.routeType === "group")
86
+ payload.openConversationId = message.targetId;
87
+ else
88
+ payload.userIds = [message.targetId];
89
+ const runner = options.runner ?? execFileAsync;
90
+ const env = robotDwsEnv(options.configDir, options.clientId, options.clientSecret, options.env);
91
+ try {
92
+ const { stdout } = await runner(options.dwsCommand, ["api", "POST", apiPath, "--data", JSON.stringify(payload), "--format", "json", "--yes"], {
93
+ timeout: options.timeoutMs,
94
+ maxBuffer: options.maxOutputBytes,
95
+ env,
96
+ windowsHide: true,
97
+ });
98
+ return normalizeRobotResult(parseJsonObject(stdout));
99
+ }
100
+ catch (error) {
101
+ const text = error instanceof Error ? error.message : String(error);
102
+ if (/missing app credentials|APP_CREDENTIALS|client.?secret/i.test(text)) {
103
+ return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Application credentials are required" };
104
+ }
105
+ if (/timed out|ETIMEDOUT|abort/i.test(text)) {
106
+ return {
107
+ status: "unknown",
108
+ errorCode: "DELIVERY_UNKNOWN",
109
+ reason: "Robot delivery timed out; delivery status is unknown",
110
+ };
111
+ }
112
+ if (/ENOENT|ECONNREFUSED/i.test(text)) {
113
+ return { status: "failed", errorCode: "ADAPTER_UNAVAILABLE", error: "dws is unavailable" };
114
+ }
115
+ try {
116
+ const stdout = error.stdout;
117
+ if (stdout)
118
+ return normalizeRobotResult(parseJsonObject(stdout));
119
+ }
120
+ catch {
121
+ // fall through
122
+ }
123
+ return {
124
+ status: "failed",
125
+ errorCode: "DELIVERY_FAILED",
126
+ error: redact(text) || "Robot delivery failed",
127
+ };
128
+ }
129
+ }
@@ -0,0 +1,5 @@
1
+ export type { DeliveryResult, DeliveryMessage, RouteType } from "./result.js";
2
+ export { normalizeWebhookResult, resolveWebhookUrl, sendWebhook, validateWebhookUrl, type WebhookSendOptions, } from "./webhook.js";
3
+ export { normalizeRobotResult, robotDwsEnv, sendRobotMessage, type CommandRunner, type RobotSendOptions, } from "./dws-robot.js";
4
+ export { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "./patrol-message.js";
5
+ export { redact } from "./redact.js";
@@ -0,0 +1,4 @@
1
+ export { normalizeWebhookResult, resolveWebhookUrl, sendWebhook, validateWebhookUrl, } from "./webhook.js";
2
+ export { normalizeRobotResult, robotDwsEnv, sendRobotMessage, } from "./dws-robot.js";
3
+ export { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "./patrol-message.js";
4
+ export { redact } from "./redact.js";
@@ -0,0 +1,3 @@
1
+ /** Shared default DingTalk markdown shell for patrol (CLI + MCP setup). */
2
+ export declare const DEFAULT_PATROL_MESSAGE_TITLE = "[{{status}}] {{name}}";
3
+ export declare const DEFAULT_PATROL_MESSAGE_BODY = "### {{name}} \u00B7 {{status}}\n\n{{summary}}\n\n---\n\u4E3B\u673A\uFF1A{{hostname}}\n\u65F6\u95F4\uFF1A{{timestamp}}\n";
@@ -0,0 +1,10 @@
1
+ /** Shared default DingTalk markdown shell for patrol (CLI + MCP setup). */
2
+ export const DEFAULT_PATROL_MESSAGE_TITLE = "[{{status}}] {{name}}";
3
+ export const DEFAULT_PATROL_MESSAGE_BODY = `### {{name}} · {{status}}
4
+
5
+ {{summary}}
6
+
7
+ ---
8
+ 主机:{{hostname}}
9
+ 时间:{{timestamp}}
10
+ `;
@@ -0,0 +1 @@
1
+ export declare function redact(value: string): string;
@@ -0,0 +1,12 @@
1
+ const SECRET_PATTERNS = [
2
+ /Bearer\s+[A-Za-z0-9._~+/-]+=*/gi,
3
+ /access_token=[^&\s]+/gi,
4
+ /client[_-]?secret["'\s:=]+[^\s"',}]+/gi,
5
+ ];
6
+ export function redact(value) {
7
+ let result = value;
8
+ for (const pattern of SECRET_PATTERNS) {
9
+ result = result.replace(pattern, "[REDACTED]");
10
+ }
11
+ return result;
12
+ }
@@ -0,0 +1,20 @@
1
+ /** Discriminated delivery outcome. failed = safe to retry; unknown = do not auto-retry. */
2
+ export type DeliveryResult = {
3
+ status: "sent";
4
+ messageId?: string;
5
+ } | {
6
+ status: "failed";
7
+ error: string;
8
+ errorCode?: string;
9
+ } | {
10
+ status: "unknown";
11
+ reason: string;
12
+ errorCode?: string;
13
+ };
14
+ export type RouteType = "group" | "user";
15
+ export interface DeliveryMessage {
16
+ routeType: RouteType;
17
+ targetId: string;
18
+ title: string;
19
+ content: string;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { DeliveryMessage, DeliveryResult } from "./result.js";
2
+ export declare function resolveWebhookUrl(value: string): URL | undefined;
3
+ export declare function validateWebhookUrl(webhookUrl: string | undefined): {
4
+ valid: boolean;
5
+ message: string;
6
+ };
7
+ export interface WebhookSendOptions {
8
+ webhookUrl: string;
9
+ webhookSecret?: string;
10
+ timeoutMs: number;
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export declare function sendWebhook(options: WebhookSendOptions, message: DeliveryMessage): Promise<DeliveryResult>;
14
+ export declare function normalizeWebhookResult(httpStatus: number, value: unknown): DeliveryResult;
@@ -0,0 +1,126 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { redact } from "./redact.js";
3
+ const WEBHOOK_HOST = "oapi.dingtalk.com";
4
+ const WEBHOOK_PATH = "/robot/send";
5
+ export function resolveWebhookUrl(value) {
6
+ try {
7
+ const url = new URL(value);
8
+ if (url.protocol !== "https:"
9
+ || url.hostname !== WEBHOOK_HOST
10
+ || url.pathname !== WEBHOOK_PATH
11
+ || !url.searchParams.get("access_token")
12
+ || url.username
13
+ || url.password
14
+ || url.hash) {
15
+ return undefined;
16
+ }
17
+ return url;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function signedUrl(url, secret, timestamp) {
24
+ const result = new URL(url);
25
+ const timestampValue = String(timestamp);
26
+ const signature = createHmac("sha256", secret)
27
+ .update(`${timestampValue}\n${secret}`)
28
+ .digest("base64");
29
+ result.searchParams.set("timestamp", timestampValue);
30
+ result.searchParams.set("sign", signature);
31
+ return result;
32
+ }
33
+ export function validateWebhookUrl(webhookUrl) {
34
+ if (!webhookUrl)
35
+ return { valid: false, message: "DINGTALK_WEBHOOK_URL is not set" };
36
+ if (!resolveWebhookUrl(webhookUrl)) {
37
+ return { valid: false, message: "DINGTALK_WEBHOOK_URL must be an https://oapi.dingtalk.com/robot/send URL" };
38
+ }
39
+ return { valid: true, message: "Webhook URL is valid" };
40
+ }
41
+ export async function sendWebhook(options, message) {
42
+ if (message.routeType !== "group") {
43
+ return { status: "failed", errorCode: "ADAPTER_UNSUPPORTED", error: "Webhook only supports group routes" };
44
+ }
45
+ const base = resolveWebhookUrl(options.webhookUrl);
46
+ if (!base) {
47
+ return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook URL is invalid" };
48
+ }
49
+ const url = options.webhookSecret
50
+ ? signedUrl(base, options.webhookSecret, Date.now())
51
+ : base;
52
+ const fetchImpl = options.fetchImpl ?? fetch;
53
+ try {
54
+ const response = await fetchImpl(url, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({
58
+ msgtype: "markdown",
59
+ markdown: { title: message.title, text: message.content },
60
+ }),
61
+ signal: AbortSignal.timeout(options.timeoutMs),
62
+ });
63
+ const text = await response.text();
64
+ let payload;
65
+ try {
66
+ payload = JSON.parse(text);
67
+ }
68
+ catch {
69
+ if (response.status === 401 || response.status === 403) {
70
+ return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook authorization failed" };
71
+ }
72
+ return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook returned non-JSON" };
73
+ }
74
+ return normalizeWebhookResult(response.status, payload);
75
+ }
76
+ catch (error) {
77
+ if (isTimeout(error)) {
78
+ return {
79
+ status: "unknown",
80
+ errorCode: "DELIVERY_UNKNOWN",
81
+ reason: "Webhook delivery timed out; delivery status is unknown",
82
+ };
83
+ }
84
+ return {
85
+ status: "failed",
86
+ errorCode: "ADAPTER_UNAVAILABLE",
87
+ error: error instanceof Error ? redact(error.message) : "Webhook request failed",
88
+ };
89
+ }
90
+ }
91
+ function isTimeout(error) {
92
+ const name = error && typeof error === "object" && "name" in error ? String(error.name) : "";
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ return name === "TimeoutError" || name === "AbortError" || /aborted due to timeout|The operation was aborted/i.test(message);
95
+ }
96
+ function isRecord(value) {
97
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
98
+ }
99
+ export function normalizeWebhookResult(httpStatus, value) {
100
+ if (httpStatus === 401 || httpStatus === 403) {
101
+ return { status: "failed", errorCode: "ADAPTER_UNAUTHORIZED", error: "Webhook authorization failed" };
102
+ }
103
+ if (!isRecord(value)) {
104
+ return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook returned a non-object response" };
105
+ }
106
+ const errcode = typeof value.errcode === "number" ? value.errcode : undefined;
107
+ const errmsg = typeof value.errmsg === "string" ? redact(value.errmsg) : undefined;
108
+ if (errcode === 310000 || /sign not match|check sign|invalid sign|缺少timestamp|timestamp is expired|加签/i.test(errmsg ?? "")) {
109
+ return {
110
+ status: "failed",
111
+ errorCode: "ADAPTER_UNAUTHORIZED",
112
+ error: "Webhook signing failed; set DINGTALK_WEBHOOK_SECRET for a signed robot",
113
+ };
114
+ }
115
+ if (httpStatus < 200 || httpStatus >= 300 || (errcode !== undefined && errcode !== 0)) {
116
+ return {
117
+ status: "failed",
118
+ errorCode: "ADAPTER_REJECTED",
119
+ error: errmsg ? `Webhook rejected: ${errmsg}` : "Webhook rejected the request",
120
+ };
121
+ }
122
+ if (errcode === 0) {
123
+ return { status: "sent" };
124
+ }
125
+ return { status: "unknown", errorCode: "DELIVERY_UNKNOWN", reason: "Webhook response did not confirm delivery" };
126
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "agent-compose-notify-delivery";
4
+ import { DEFAULT_PATROL_MESSAGE_BODY, DEFAULT_PATROL_MESSAGE_TITLE, } from "../notify-delivery/index.js";
5
5
  import { DingtalkDwsError } from "../errors.js";
6
6
  import { isRouteConfigured, readTargets } from "../notify/bind.js";
7
7
  import { mcpPackageRoot, readPackageVersion } from "../version.js";
@@ -0,0 +1,24 @@
1
+ export type GuideMeta = {
2
+ slug: string;
3
+ uri: string;
4
+ title: string;
5
+ description: string;
6
+ };
7
+ export declare function listMcpGuideResources(): Array<{
8
+ uri: string;
9
+ name: string;
10
+ title: string;
11
+ description: string;
12
+ mimeType: string;
13
+ annotations: {
14
+ audience: Array<"user" | "assistant">;
15
+ priority: number;
16
+ };
17
+ }>;
18
+ export declare function readMcpGuideResource(uri: string): {
19
+ contents: Array<{
20
+ uri: string;
21
+ mimeType: string;
22
+ text: string;
23
+ }>;
24
+ };
@@ -0,0 +1,36 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { mcpPackageRoot } from "../version.js";
4
+ const GUIDES = [
5
+ {
6
+ slug: "doc-search",
7
+ uri: "dingtalk-dws://guides/doc-search",
8
+ title: "DingTalk cloud doc search",
9
+ description: "Agent overlay: drive search short path and stop-rules; prefer over upstream doc search recipes when they lag.",
10
+ },
11
+ ];
12
+ function readMarkdown(slug) {
13
+ const filePath = path.join(mcpPackageRoot(), "guides", `${slug}.md`);
14
+ return fs.readFileSync(filePath, "utf8");
15
+ }
16
+ export function listMcpGuideResources() {
17
+ return GUIDES.map((g) => ({
18
+ uri: g.uri,
19
+ name: g.slug,
20
+ title: g.title,
21
+ description: g.description,
22
+ mimeType: "text/markdown",
23
+ annotations: { audience: ["assistant"], priority: 0.7 },
24
+ }));
25
+ }
26
+ export function readMcpGuideResource(uri) {
27
+ const guide = GUIDES.find((g) => g.uri === uri);
28
+ if (!guide) {
29
+ const err = new Error(`Resource not found: ${uri}`);
30
+ err.code = -32002;
31
+ throw err;
32
+ }
33
+ return {
34
+ contents: [{ uri, mimeType: "text/markdown", text: readMarkdown(guide.slug) }],
35
+ };
36
+ }
@@ -1,9 +1,10 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { IdempotencyStore } from "./notify/idempotency-store.js";
6
6
  import { getPromptMessages, listPromptDefinitions } from "./prompts.js";
7
+ import { listMcpGuideResources, readMcpGuideResource } from "./resources/guides.js";
7
8
  import { DingtalkDwsService } from "./service.js";
8
9
  import { buildInstructions, callTool, createToolDefinitions, resolveToolSurface } from "./tools/registry.js";
9
10
  import { readPackageVersion } from "./version.js";
@@ -22,7 +23,7 @@ export function createMcpServer(service, store) {
22
23
  if (!resolvedStore)
23
24
  throw new Error("store required");
24
25
  const surface = resolveToolSurface(adapter);
25
- const server = new Server({ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION }, { capabilities: { tools: {}, prompts: {} }, instructions: buildInstructions(surface) });
26
+ const server = new Server({ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION }, { capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: buildInstructions(surface) });
26
27
  const tools = createToolDefinitions(surface);
27
28
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...tools] }));
28
29
  server.setRequestHandler(CallToolRequestSchema, async (request) => (callTool(resolvedService, request.params.name, request.params.arguments ?? {}, surface)));
@@ -37,6 +38,10 @@ export function createMcpServer(service, store) {
37
38
  throw new Error(`Unknown prompt: ${request.params.name}`);
38
39
  }
39
40
  });
41
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
42
+ resources: listMcpGuideResources(),
43
+ }));
44
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => (readMcpGuideResource(request.params.uri)));
40
45
  return { server, service: resolvedService, store: resolvedStore };
41
46
  }
42
47
  export async function runStdioServer() {
@@ -15,6 +15,7 @@ export function buildInstructions(surface) {
15
15
  // User-selected recipes → prompts. Do not duplicate those here.
16
16
  const lines = [
17
17
  "DingTalk robot notify + dws readiness (Agent surface of the dws ecosystem). Office stays on local dws.",
18
+ "Office via local dws: before following upstream Skill recipes, read this server's MCP Resources overlays when they apply.",
18
19
  "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
20
  "Call dingtalk_doctor first; connected ≠ ready.",
20
21
  ];
@@ -0,0 +1,37 @@
1
+ # DingTalk cloud doc search (Agent overlay)
2
+
3
+ Use this when the user asks to find / list / summarize DingTalk cloud docs or knowledge-base pages.
4
+
5
+ Upstream dws Skill recipes may still say `doc search` / `doc list`. Prefer this overlay when they conflict.
6
+
7
+ ## Short path
8
+
9
+ 1. One primary search:
10
+ ```bash
11
+ dws drive search --query "<user keywords>" --format json
12
+ ```
13
+ 2. From `documents[]`, keep **5–10** hits with `name` / `nodeId` / `docUrl` (or `url`) / type.
14
+ 3. Deliver a **table** (name + link + one-line why relevant). Stop here if that answers the user.
15
+ 4. Only if the user wants depth, or top hits need a title skim: read **at most 2–3** `adoc` nodes:
16
+ ```bash
17
+ dws doc read --node <nodeId> --content-format markdown --format json
18
+ ```
19
+ Use the JSON field **`markdown`** (not `content`). Prefer headings / first ~20 lines — not the whole body in chat.
20
+
21
+ ## Do not
22
+
23
+ - Recurse `drive list` / wiki folder listing after search (folder child names are often opaque IDs).
24
+ - Fire many overlapping queries (`性能优化` + `训练性能` + …) by default — one main query, optional one supplement.
25
+ - Pull full document bodies by default.
26
+ - Treat dlink / non-adoc hits as readable via `doc read` — give the link only.
27
+ - Treat empty `doc read` as “try another format / write a helper script” — check the `markdown` field / `--help` once, then skip or try the next hit.
28
+
29
+ ## Knowledge-base scoped search
30
+
31
+ If the user already named a workspace/knowledge base:
32
+
33
+ ```bash
34
+ dws wiki node search --workspace <id> --query "<keywords>" --format json
35
+ ```
36
+
37
+ Otherwise stay on global `drive search`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-dws-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "DingTalk robot notification MCP with dws readiness handshake",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "main": "dist/src/index.js",
11
11
  "files": [
12
12
  "dist/src",
13
+ "guides",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
@@ -33,7 +34,6 @@
33
34
  "notification"
34
35
  ],
35
36
  "dependencies": {
36
- "agent-compose-notify-delivery": "^1.0.0",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0"
38
38
  },
39
39
  "devDependencies": {