feishu-mcp 0.0.11 → 0.0.12

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/dist/config.js CHANGED
@@ -1,26 +1,104 @@
1
- import { Config, ConfigSource } from './utils/config.js';
2
- /**
3
- * 为了向后兼容,保留getServerConfig函数
4
- * 但内部使用Config类
5
- * @param isStdioMode 是否在stdio模式下
6
- * @returns 服务器配置
7
- */
1
+ import { config } from "dotenv";
2
+ import yargs from "yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+ // 确保在任何配置读取前加载.env文件
5
+ config();
6
+ function maskApiKey(key) {
7
+ if (key.length <= 4)
8
+ return "****";
9
+ return `****${key.slice(-4)}`;
10
+ }
8
11
  export function getServerConfig(isStdioMode) {
9
- const config = Config.getInstance();
10
- if (!isStdioMode) {
11
- config.printConfig(isStdioMode);
12
- }
13
- // 为了向后兼容,返回旧格式的配置对象
14
- return {
15
- port: config.server.port,
16
- feishuAppId: config.feishu.appId,
17
- feishuAppSecret: config.feishu.appSecret,
12
+ // Parse command line arguments
13
+ const argv = yargs(hideBin(process.argv))
14
+ .options({
15
+ port: {
16
+ type: "number",
17
+ description: "Port to run the server on",
18
+ },
19
+ "feishu-app-id": {
20
+ type: "string",
21
+ description: "Feishu App ID",
22
+ },
23
+ "feishu-app-secret": {
24
+ type: "string",
25
+ description: "Feishu App Secret",
26
+ },
27
+ })
28
+ .help()
29
+ .parseSync();
30
+ const config = {
31
+ port: 3333,
18
32
  configSources: {
19
- port: config.configSources['server.port'].toLowerCase(),
20
- feishuAppId: config.configSources['feishu.appId']?.toLowerCase(),
21
- feishuAppSecret: config.configSources['feishu.appSecret']?.toLowerCase()
22
- }
33
+ port: "default",
34
+ },
23
35
  };
36
+ // Handle PORT
37
+ if (argv.port) {
38
+ config.port = argv.port;
39
+ config.configSources.port = "cli";
40
+ }
41
+ else if (process.env.PORT) {
42
+ config.port = parseInt(process.env.PORT, 10);
43
+ config.configSources.port = "env";
44
+ }
45
+ // 在加载环境变量之前添加日志
46
+ console.log('开始加载环境变量配置...');
47
+ console.log('当前环境变量 FEISHU_APP_ID:', process.env.FEISHU_APP_ID);
48
+ console.log('当前环境变量 FEISHU_APP_SECRET:', process.env.FEISHU_APP_SECRET);
49
+ // Handle Feishu configuration
50
+ if (argv["feishu-app-id"]) {
51
+ config.feishuAppId = argv["feishu-app-id"];
52
+ config.configSources.feishuAppId = "cli";
53
+ console.log(`飞书应用 ID 来自命令行参数: ${maskApiKey(config.feishuAppId)}`);
54
+ }
55
+ else if (process.env.FEISHU_APP_ID) {
56
+ config.feishuAppId = process.env.FEISHU_APP_ID;
57
+ config.configSources.feishuAppId = "env";
58
+ console.log(`飞书应用 ID 来自环境变量: ${maskApiKey(config.feishuAppId)}`);
59
+ }
60
+ else {
61
+ console.log('未提供飞书应用 ID');
62
+ }
63
+ if (argv["feishu-app-secret"]) {
64
+ config.feishuAppSecret = argv["feishu-app-secret"];
65
+ config.configSources.feishuAppSecret = "cli";
66
+ console.log(`飞书应用密钥来自命令行参数: ${maskApiKey(config.feishuAppSecret)}`);
67
+ }
68
+ else if (process.env.FEISHU_APP_SECRET) {
69
+ config.feishuAppSecret = process.env.FEISHU_APP_SECRET;
70
+ config.configSources.feishuAppSecret = "env";
71
+ console.log(`飞书应用密钥来自环境变量: ${maskApiKey(config.feishuAppSecret)}`);
72
+ }
73
+ else {
74
+ console.log('未提供飞书应用密钥');
75
+ }
76
+ // 输出飞书配置状态总结
77
+ if (config.feishuAppId && config.feishuAppSecret) {
78
+ console.log('飞书配置已完整提供,服务将被初始化');
79
+ }
80
+ else if (config.feishuAppId || config.feishuAppSecret) {
81
+ console.log('飞书配置不完整,服务将不会初始化');
82
+ }
83
+ else {
84
+ console.log('未提供飞书配置,服务将不会初始化');
85
+ }
86
+ // 验证配置
87
+ if (!config.feishuAppId || !config.feishuAppSecret) {
88
+ console.error("FEISHU_APP_ID 和 FEISHU_APP_SECRET 是必需的(通过命令行参数 --feishu-app-id 和 --feishu-app-secret 或 .env 文件)");
89
+ process.exit(1);
90
+ }
91
+ // Log configuration sources
92
+ if (!isStdioMode) {
93
+ console.log("\n配置信息:");
94
+ console.log(`- PORT: ${config.port} (来源: ${config.configSources.port})`);
95
+ if (config.feishuAppId) {
96
+ console.log(`- FEISHU_APP_ID: ${maskApiKey(config.feishuAppId)} (来源: ${config.configSources.feishuAppId})`);
97
+ }
98
+ if (config.feishuAppSecret) {
99
+ console.log(`- FEISHU_APP_SECRET: ${maskApiKey(config.feishuAppSecret)} (来源: ${config.configSources.feishuAppSecret})`);
100
+ }
101
+ console.log(); // 空行,提高可读性
102
+ }
103
+ return config;
24
104
  }
25
- // 导出Config类
26
- export { Config, ConfigSource };
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { formatErrorMessage } from '../../utils/error.js';
3
3
  import { Logger } from '../../utils/logger.js';
4
- import { DocumentIdSchema, BlockIdSchema, } from '../../types/feishuSchema.js';
4
+ import { DocumentIdSchema, BlockIdSchema, SearchKeySchema, } from '../../types/feishuSchema.js';
5
5
  /**
6
6
  * 注册飞书相关的MCP工具
7
7
  * @param server MCP服务器实例
@@ -134,4 +134,33 @@ export function registerFeishuTools(server, feishuService) {
134
134
  };
135
135
  }
136
136
  });
137
+ // 添加搜索文档工具
138
+ server.tool('search_feishu_documents', 'Searches for documents in Feishu. Supports keyword-based search and returns document information including title, type, and owner. Use this tool to find specific content or related documents in your document library.', {
139
+ searchKey: SearchKeySchema,
140
+ }, async ({ searchKey }) => {
141
+ try {
142
+ if (!feishuService) {
143
+ return {
144
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration.' }],
145
+ };
146
+ }
147
+ Logger.info(`开始搜索飞书文档,关键字: ${searchKey},`);
148
+ const searchResult = await feishuService.searchDocuments(searchKey);
149
+ Logger.info(`文档搜索完成,找到 ${searchResult.size} 个结果`);
150
+ return {
151
+ content: [
152
+ { type: 'text', text: JSON.stringify(searchResult, null, 2) },
153
+ ],
154
+ };
155
+ }
156
+ catch (error) {
157
+ Logger.error(`搜索飞书文档失败:`, error);
158
+ const errorMessage = formatErrorMessage(error);
159
+ return {
160
+ content: [
161
+ { type: 'text', text: `搜索飞书文档失败: ${errorMessage}` },
162
+ ],
163
+ };
164
+ }
165
+ });
137
166
  }
@@ -70,6 +70,7 @@ export class FeishuService {
70
70
  throw {
71
71
  status: response.status,
72
72
  err: response.data.msg || "Unknown error",
73
+ apiError: response.data
73
74
  };
74
75
  }
75
76
  this.accessToken = response.data.tenant_access_token;
@@ -86,6 +87,7 @@ export class FeishuService {
86
87
  throw {
87
88
  status: error.response.status,
88
89
  err: error.response.data?.msg || "Unknown error",
90
+ apiError: error.response.data
89
91
  };
90
92
  }
91
93
  Logger.error('获取访问令牌时发生未知错误:', error);
@@ -128,6 +130,7 @@ export class FeishuService {
128
130
  throw {
129
131
  status: error.response.status,
130
132
  err: error.response.data?.msg || "Unknown error",
133
+ apiError: error.response.data
131
134
  };
132
135
  }
133
136
  Logger.error('发送请求时发生未知错误:', error);
@@ -714,4 +714,50 @@ export class FeishuApiService extends BaseApiService {
714
714
  this.handleApiError(error, '创建文件夹失败');
715
715
  }
716
716
  }
717
+ /**
718
+ * 搜索飞书文档
719
+ * @param searchKey 搜索关键字
720
+ * @param count 每页数量,默认50
721
+ * @returns 搜索结果,包含所有页的数据
722
+ */
723
+ async searchDocuments(searchKey, count = 50) {
724
+ try {
725
+ Logger.info(`开始搜索文档,关键字: ${searchKey}`);
726
+ const endpoint = `//suite/docs-api/search/object`;
727
+ let offset = 0;
728
+ let allResults = [];
729
+ let hasMore = true;
730
+ // 循环获取所有页的数据
731
+ while (hasMore && offset + count < 200) {
732
+ const payload = {
733
+ search_key: searchKey,
734
+ docs_types: ["doc"],
735
+ count: count,
736
+ offset: offset
737
+ };
738
+ Logger.debug(`请求搜索,offset: ${offset}, count: ${count}`);
739
+ const response = await this.post(endpoint, payload);
740
+ Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
741
+ if (response && response.docs_entities) {
742
+ const newDocs = response.docs_entities;
743
+ allResults = [...allResults, ...newDocs];
744
+ hasMore = response.has_more || false;
745
+ offset += count;
746
+ Logger.debug(`当前页获取到 ${newDocs.length} 条数据,累计 ${allResults.length} 条,总计 ${response.total} 条,hasMore: ${hasMore}`);
747
+ }
748
+ else {
749
+ hasMore = false;
750
+ Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
751
+ }
752
+ }
753
+ const resultCount = allResults.length;
754
+ Logger.info(`文档搜索完成,找到 ${resultCount} 个结果`);
755
+ return {
756
+ data: allResults
757
+ };
758
+ }
759
+ catch (error) {
760
+ this.handleApiError(error, '搜索文档失败');
761
+ }
762
+ }
717
763
  }
@@ -118,3 +118,5 @@ export const OrderBySchema = z.string().optional().default('EditedTime').describ
118
118
  // 排序方向参数定义
119
119
  export const DirectionSchema = z.string().optional().default('DESC').describe('Sort direction (optional). Specifies the sort order. Available values: ' +
120
120
  '"DESC" (default) for descending order, "ASC" for ascending order. Case sensitive.');
121
+ // 搜索关键字参数定义
122
+ export const SearchKeySchema = z.string().describe('Search keyword (required). The keyword to search for in documents.');
@@ -265,9 +265,9 @@ Object.defineProperty(Logger, "config", {
265
265
  showTimestamp: true,
266
266
  showLevel: true,
267
267
  timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS',
268
- logToFile: true,
268
+ logToFile: false,
269
269
  logFilePath: 'log/log.txt',
270
- maxObjectDepth: 4, // 限制对象序列化深度
270
+ maxObjectDepth: 2, // 限制对象序列化深度
271
271
  maxObjectStringLength: 5000000 // 限制序列化后字符串长度
272
272
  }
273
273
  });
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "feishu-mcp",
3
- "version": "0.0.11",
4
- "description": "Model Context Protocol server for Feishu integration",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "feishu-mcp": "./dist/cli.js"
9
- },
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "build": "tsc && tsc-alias",
16
- "type-check": "tsc --noEmit",
17
- "start": "node dist/index.js",
18
- "start:cli": "cross-env NODE_ENV=cli node dist/index.js",
19
- "start:http": "node dist/index.js",
20
- "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
21
- "dev:cli": "cross-env NODE_ENV=development tsx watch src/index.ts --stdio",
22
- "lint": "eslint . --ext .ts",
23
- "format": "prettier --write \"src/**/*.ts\"",
24
- "inspect": "pnpx @modelcontextprotocol/inspector",
25
- "prepare": "pnpm run build",
26
- "pub:release": "pnpm build && npm publish"
27
- },
28
- "engines": {
29
- "node": "^20.17.0"
30
- },
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/cso1z/Feishu-MCP.git"
34
- },
35
- "keywords": [
36
- "feishu",
37
- "lark",
38
- "mcp",
39
- "typescript"
40
- ],
41
- "author": "cso1z",
42
- "license": "MIT",
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.6.1",
45
- "@types/yargs": "^17.0.33",
46
- "axios": "^1.7.9",
47
- "cross-env": "^7.0.3",
48
- "dotenv": "^16.4.7",
49
- "express": "^4.21.2",
50
- "remeda": "^2.20.1",
51
- "yargs": "^17.7.2",
52
- "zod": "^3.24.2"
53
- },
54
- "devDependencies": {
55
- "@types/express": "^5.0.0",
56
- "@types/jest": "^29.5.11",
57
- "@types/node": "^20.17.0",
58
- "@typescript-eslint/eslint-plugin": "^8.24.0",
59
- "@typescript-eslint/parser": "^8.24.0",
60
- "eslint": "^9.20.1",
61
- "eslint-config-prettier": "^10.0.1",
62
- "jest": "^29.7.0",
63
- "prettier": "^3.5.0",
64
- "ts-jest": "^29.2.5",
65
- "tsc-alias": "^1.8.10",
66
- "tsx": "^4.19.2",
67
- "typescript": "^5.7.3"
68
- },
69
- "pnpm": {
70
- "overrides": {
71
- "feishu-mcp": "link:"
72
- }
73
- }
74
- }
1
+ {
2
+ "name": "feishu-mcp",
3
+ "version": "0.0.12",
4
+ "description": "Model Context Protocol server for Feishu integration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "feishu-mcp": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && tsc-alias",
16
+ "type-check": "tsc --noEmit",
17
+ "start": "node dist/index.js",
18
+ "start:cli": "cross-env NODE_ENV=cli node dist/index.js",
19
+ "start:http": "node dist/index.js",
20
+ "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
21
+ "dev:cli": "cross-env NODE_ENV=development tsx watch src/index.ts --stdio",
22
+ "lint": "eslint . --ext .ts",
23
+ "format": "prettier --write \"src/**/*.ts\"",
24
+ "inspect": "pnpx @modelcontextprotocol/inspector",
25
+ "prepare": "pnpm run build",
26
+ "pub:release": "pnpm build && npm publish"
27
+ },
28
+ "engines": {
29
+ "node": "^20.17.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/cso1z/Feishu-MCP.git"
34
+ },
35
+ "keywords": [
36
+ "feishu",
37
+ "lark",
38
+ "mcp",
39
+ "typescript"
40
+ ],
41
+ "author": "cso1z",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.6.1",
45
+ "@types/yargs": "^17.0.33",
46
+ "axios": "^1.7.9",
47
+ "cross-env": "^7.0.3",
48
+ "dotenv": "^16.4.7",
49
+ "express": "^4.21.2",
50
+ "remeda": "^2.20.1",
51
+ "yargs": "^17.7.2",
52
+ "zod": "^3.24.2"
53
+ },
54
+ "devDependencies": {
55
+ "@types/express": "^5.0.0",
56
+ "@types/jest": "^29.5.11",
57
+ "@types/node": "^20.17.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.24.0",
59
+ "@typescript-eslint/parser": "^8.24.0",
60
+ "eslint": "^9.20.1",
61
+ "eslint-config-prettier": "^10.0.1",
62
+ "jest": "^29.7.0",
63
+ "prettier": "^3.5.0",
64
+ "ts-jest": "^29.2.5",
65
+ "tsc-alias": "^1.8.10",
66
+ "tsx": "^4.19.2",
67
+ "typescript": "^5.7.3"
68
+ },
69
+ "pnpm": {
70
+ "overrides": {
71
+ "feishu-mcp": "link:"
72
+ }
73
+ }
74
+ }
@@ -1,179 +0,0 @@
1
- /**
2
- * @deprecated 这个文件已被弃用,所有功能已迁移到BlockFactory类。
3
- * 请使用BlockFactory创建各种块内容。
4
- * 所有接口定义已迁移到blockFactory.ts文件。
5
- * 此文件仅保留为历史兼容性目的,将在后续版本中移除。
6
- */
7
- /**
8
- * 构建批量创建块的请求数据
9
- * @param blocks 块内容数组
10
- * @param index 插入位置索引
11
- * @returns 请求数据对象
12
- * @deprecated 请使用BlockFactory.buildCreateBlocksRequest
13
- */
14
- export function buildCreateBlocksRequest(blocks, index = 0) {
15
- return {
16
- children: blocks,
17
- index
18
- };
19
- }
20
- /**
21
- * 创建文本块内容
22
- * @param align 对齐方式:1左对齐,2居中,3右对齐
23
- * @returns 文本块内容对象
24
- * @deprecated 请使用BlockFactory.createTextBlock
25
- */
26
- export function createTextBlockContent(textContents, align = 1) {
27
- return {
28
- block_type: 2, // 2表示文本块
29
- text: {
30
- elements: textContents.map(content => ({
31
- text_run: {
32
- content: content.text,
33
- text_element_style: content.style || {}
34
- }
35
- })),
36
- style: {
37
- align: align // 1 居左,2 居中,3 居右
38
- }
39
- }
40
- };
41
- }
42
- /**
43
- * 创建代码块内容
44
- * @param code 代码内容
45
- * @param language 语言类型代码
46
- * @param wrap 是否自动换行
47
- * @returns 代码块内容对象
48
- * @deprecated 请使用BlockFactory.createCodeBlock
49
- */
50
- export function createCodeBlockContent(code, language = 0, wrap = false) {
51
- return {
52
- block_type: 14, // 14表示代码块
53
- code: {
54
- elements: [
55
- {
56
- text_run: {
57
- content: code,
58
- text_element_style: {
59
- bold: false,
60
- inline_code: false,
61
- italic: false,
62
- strikethrough: false,
63
- underline: false
64
- }
65
- }
66
- }
67
- ],
68
- style: {
69
- language: language,
70
- wrap: wrap
71
- }
72
- }
73
- };
74
- }
75
- /**
76
- * 创建标题块内容
77
- * @param text 标题文本
78
- * @param level 标题级别(1-9)
79
- * @param align 对齐方式:1左对齐,2居中,3右对齐
80
- * @returns 标题块内容对象
81
- * @deprecated 请使用BlockFactory.createHeadingBlock
82
- */
83
- export function createHeadingBlockContent(text, level = 1, align = 1) {
84
- // 确保标题级别在有效范围内(1-9)
85
- const safeLevel = Math.max(1, Math.min(9, level));
86
- // 根据标题级别设置block_type和对应的属性名
87
- // 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
88
- const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
89
- const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
90
- // 构建块内容
91
- const blockContent = {
92
- block_type: blockType
93
- };
94
- // 设置对应级别的标题属性
95
- blockContent[headingKey] = {
96
- elements: [
97
- {
98
- text_run: {
99
- content: text,
100
- text_element_style: {}
101
- }
102
- }
103
- ],
104
- style: {
105
- align: align,
106
- folded: false
107
- }
108
- };
109
- return blockContent;
110
- }
111
- /**
112
- * 创建列表块内容(有序或无序)
113
- * @param text 列表项文本
114
- * @param isOrdered 是否为有序列表
115
- * @param align 对齐方式:1左对齐,2居中,3右对齐
116
- * @returns 列表块内容对象
117
- * @deprecated 请使用BlockFactory.createListBlock
118
- */
119
- export function createListBlockContent(text, isOrdered = false, align = 1) {
120
- // 确保 align 值在合法范围内(1-3)
121
- const safeAlign = (align === 1 || align === 2 || align === 3) ? align : 1;
122
- // 有序列表是 block_type: 13,无序列表是 block_type: 12
123
- const blockType = isOrdered ? 13 : 12;
124
- const propertyKey = isOrdered ? "ordered" : "bullet";
125
- // 构建块内容
126
- const blockContent = {
127
- block_type: blockType
128
- };
129
- // 设置列表属性
130
- blockContent[propertyKey] = {
131
- elements: [
132
- {
133
- text_run: {
134
- content: text,
135
- text_element_style: {}
136
- }
137
- }
138
- ],
139
- style: {
140
- align: safeAlign,
141
- folded: false
142
- }
143
- };
144
- return blockContent;
145
- }
146
- /**
147
- * 处理Markdown语法转换
148
- * @param textContents 文本内容数组
149
- * @returns 处理后的文本内容数组
150
- * @deprecated 应当避免使用Markdown语法,请直接使用TextElementStyle设置样式
151
- */
152
- export function processMarkdownSyntax(textContents) {
153
- return textContents.map(content => {
154
- let { text, style = {} } = content;
155
- // 创建一个新的style对象,避免修改原始对象
156
- const newStyle = { ...style };
157
- // 处理粗体 **text**
158
- if (text.match(/\*\*([^*]+)\*\*/g)) {
159
- text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
160
- newStyle.bold = true;
161
- }
162
- // 处理斜体 *text*
163
- if (text.match(/(?<!\*)\*([^*]+)\*(?!\*)/g)) {
164
- text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
165
- newStyle.italic = true;
166
- }
167
- // 处理删除线 ~~text~~
168
- if (text.match(/~~([^~]+)~~/g)) {
169
- text = text.replace(/~~([^~]+)~~/g, "$1");
170
- newStyle.strikethrough = true;
171
- }
172
- // 处理行内代码 `code`
173
- if (text.match(/`([^`]+)`/g)) {
174
- text = text.replace(/`([^`]+)`/g, "$1");
175
- newStyle.inline_code = true;
176
- }
177
- return { text, style: newStyle };
178
- });
179
- }