├── .npmrc ├── src ├── mcp-tool │ ├── document-tool │ │ ├── index.ts │ │ └── recall │ │ │ ├── type.ts │ │ │ ├── request.ts │ │ │ └── index.ts │ ├── utils │ │ ├── index.ts │ │ ├── get-should-use-uat.ts │ │ ├── case-transf.ts │ │ ├── filter-tools.ts │ │ └── handler.ts │ ├── index.ts │ ├── tools │ │ ├── en │ │ │ ├── builtin-tools │ │ │ │ ├── index.ts │ │ │ │ └── im │ │ │ │ │ └── buildin.ts │ │ │ └── gen-tools │ │ │ │ └── zod │ │ │ │ ├── verification_v1.ts │ │ │ │ ├── authen_v1.ts │ │ │ │ ├── board_v1.ts │ │ │ │ ├── wiki_v1.ts │ │ │ │ ├── event_v1.ts │ │ │ │ ├── moments_v1.ts │ │ │ │ ├── optical_char_recognition_v1.ts │ │ │ │ ├── tenant_v2.ts │ │ │ │ ├── docs_v1.ts │ │ │ │ ├── human_authentication_v1.ts │ │ │ │ ├── security_and_compliance_v1.ts │ │ │ │ ├── translation_v1.ts │ │ │ │ ├── application_v5.ts │ │ │ │ ├── mdm_v1.ts │ │ │ │ ├── auth_v3.ts │ │ │ │ ├── passport_v1.ts │ │ │ │ ├── hire_v2.ts │ │ │ │ ├── report_v1.ts │ │ │ │ ├── minutes_v1.ts │ │ │ │ ├── speech_to_text_v1.ts │ │ │ │ ├── ehr_v1.ts │ │ │ │ ├── workplace_v1.ts │ │ │ │ └── mdm_v3.ts │ │ ├── zh │ │ │ ├── builtin-tools │ │ │ │ ├── index.ts │ │ │ │ └── im │ │ │ │ │ └── buildin.ts │ │ │ └── gen-tools │ │ │ │ └── zod │ │ │ │ ├── verification_v1.ts │ │ │ │ ├── authen_v1.ts │ │ │ │ ├── board_v1.ts │ │ │ │ ├── moments_v1.ts │ │ │ │ ├── optical_char_recognition_v1.ts │ │ │ │ ├── event_v1.ts │ │ │ │ ├── tenant_v2.ts │ │ │ │ ├── docs_v1.ts │ │ │ │ ├── human_authentication_v1.ts │ │ │ │ ├── wiki_v1.ts │ │ │ │ ├── security_and_compliance_v1.ts │ │ │ │ ├── translation_v1.ts │ │ │ │ ├── mdm_v1.ts │ │ │ │ ├── passport_v1.ts │ │ │ │ ├── ehr_v1.ts │ │ │ │ ├── application_v5.ts │ │ │ │ ├── speech_to_text_v1.ts │ │ │ │ ├── hire_v2.ts │ │ │ │ ├── minutes_v1.ts │ │ │ │ ├── report_v1.ts │ │ │ │ ├── auth_v3.ts │ │ │ │ ├── mdm_v3.ts │ │ │ │ └── workplace_v1.ts │ │ └── index.ts │ ├── types │ │ └── index.ts │ └── constants.ts ├── cli │ └── index.ts ├── mcp-server │ ├── index.ts │ ├── shared │ │ ├── index.ts │ │ ├── types.ts │ │ └── init.ts │ └── transport │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── stdio.ts │ │ ├── sse.ts │ │ └── streamable.ts ├── auth │ ├── handler │ │ └── index.ts │ ├── provider │ │ ├── index.ts │ │ └── types.ts │ ├── utils │ │ ├── index.ts │ │ ├── pkce.ts │ │ ├── is-token-valid.ts │ │ ├── encryption.ts │ │ └── storage-manager.ts │ ├── index.ts │ ├── types.ts │ └── config.ts ├── index.ts └── utils │ ├── noop.ts │ ├── safe-json-parse.ts │ ├── clean-env-args.ts │ ├── parser-string-array.ts │ ├── version.ts │ ├── http-instance.ts │ ├── constants.ts │ └── logger.ts ├── .prettierignore ├── .dockerignore ├── .prettierrc ├── tests ├── utils │ ├── noop.test.ts │ ├── http-instance.test.ts │ ├── parser-string-array.test.ts │ ├── clean-env-args.test.ts │ ├── constants.test.ts │ └── safe-json-parse.test.ts ├── setup.ts ├── mcp-tool │ ├── utils │ │ ├── case-transf.test.ts │ │ └── get-should-use-uat.test.ts │ ├── document-tool │ │ └── recall │ │ │ ├── index.test.ts │ │ │ └── request.test.ts │ └── tools │ │ └── additional-coverage.test.ts └── cli.test.ts ├── tsconfig.json ├── .gitignore ├── assets ├── trae.svg └── trae-cn.svg ├── .gitattributes ├── jest.config.js ├── LICENSE ├── docs ├── troubleshooting │ ├── faq-zh.md │ └── faq.md ├── usage │ └── docker │ │ ├── docker-zh.md │ │ └── docker.md ├── reference │ ├── cli │ │ └── cli-zh.md │ └── tool-presets │ │ └── presets-zh.md └── recall-mcp │ └── README_ZH.md ├── package.json ├── CHANGELOG.md └── Dockerfile /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /src/mcp-tool/document-tool/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recall'; 2 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | export { LoginHandler, LoginOptions } from './login-handler'; 2 | -------------------------------------------------------------------------------- /src/mcp-server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transport'; 2 | export * from './shared'; 3 | -------------------------------------------------------------------------------- /src/mcp-server/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './init'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/auth/handler/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handler-local'; 2 | export * from './handler'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cli'; 2 | export * from './mcp-tool'; 3 | export * from './mcp-server'; 4 | -------------------------------------------------------------------------------- /src/auth/provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oauth'; 2 | export * from './oidc'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/mcp-server/transport/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stdio'; 2 | export * from './sse'; 3 | export * from './streamable'; 4 | -------------------------------------------------------------------------------- /src/utils/noop.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-function 2 | export const noop = () => {}; 3 | -------------------------------------------------------------------------------- /src/auth/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-token-valid'; 2 | export * from './pkce'; 3 | export * from './storage-manager'; 4 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './store'; 3 | export * from './handler'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/mcp-tool/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter-tools'; 2 | export * from './handler'; 3 | export * from './case-transf'; 4 | export * from './get-should-use-uat'; 5 | -------------------------------------------------------------------------------- /src/mcp-tool/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mcp-tool'; 2 | export * from './tools'; 3 | export * from './types'; 4 | export * from './constants'; 5 | export * from './document-tool'; 6 | -------------------------------------------------------------------------------- /src/auth/provider/types.ts: -------------------------------------------------------------------------------- 1 | export interface LarkProxyOAuthServerProviderOptions { 2 | domain: string; 3 | host: string; 4 | port: number; 5 | appId: string; 6 | appSecret: string; 7 | callbackUrl: string; 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/CHANGELOG.* 2 | # 包管理文件 3 | **/pnpm-lock.yaml 4 | **/yarn.lock 5 | **/package-lock.json 6 | **/shrinkwrap.json 7 | 8 | # 构建产物 9 | **/dist 10 | **/lib 11 | 12 | # 在 Markdown 中,Prettier 将会对代码块进行格式化,这会影响输出 13 | *.md 14 | *.svg 15 | -------------------------------------------------------------------------------- /src/utils/safe-json-parse.ts: -------------------------------------------------------------------------------- 1 | export function safeJsonParse(str: string | undefined | null, fallback: T): T { 2 | if (!str) { 3 | return fallback; 4 | } 5 | try { 6 | return JSON.parse(str); 7 | } catch (e) { 8 | return fallback; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore dependencies and VCS 2 | node_modules 3 | .git 4 | .gitignore 5 | 6 | # Build artifacts and coverage 7 | dist 8 | coverage 9 | 10 | # Logs and caches 11 | npm-debug.log* 12 | yarn-error.log* 13 | **/.DS_Store 14 | **/.vscode 15 | **/.idea 16 | **/tmp 17 | **/.cache 18 | 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "endOfLine": "auto", 6 | "tabWidth": 2, 7 | "overrides": [ 8 | { 9 | "files": ".prettierrc", 10 | "options": { 11 | "parser": "json" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/utils/noop.test.ts: -------------------------------------------------------------------------------- 1 | import { noop } from '../../src/utils/noop'; 2 | 3 | describe('noop', () => { 4 | it('should be a function', () => { 5 | expect(typeof noop).toBe('function'); 6 | }); 7 | 8 | it('should return undefined', () => { 9 | expect(noop()).toBeUndefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/clean-env-args.ts: -------------------------------------------------------------------------------- 1 | export function cleanEnvArgs(args: Record) { 2 | const result = {} as Record; 3 | for (const [key, value] of Object.entries(args)) { 4 | if (value) { 5 | result[key as keyof typeof args] = value; 6 | } 7 | } 8 | return result; 9 | } 10 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/builtin-tools/index.ts: -------------------------------------------------------------------------------- 1 | import { docxBuiltinToolName, docxBuiltinTools } from './docx/builtin'; 2 | import { imBuiltinToolName, imBuiltinTools } from './im/buildin'; 3 | 4 | export const BuiltinTools = [...docxBuiltinTools, ...imBuiltinTools]; 5 | 6 | export type BuiltinToolName = docxBuiltinToolName | imBuiltinToolName; 7 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/builtin-tools/index.ts: -------------------------------------------------------------------------------- 1 | import { docxBuiltinToolName, docxBuiltinTools } from './docx/builtin'; 2 | import { imBuiltinToolName, imBuiltinTools } from './im/buildin'; 3 | 4 | export const BuiltinTools = [...docxBuiltinTools, ...imBuiltinTools]; 5 | 6 | export type BuiltinToolName = docxBuiltinToolName | imBuiltinToolName; 7 | -------------------------------------------------------------------------------- /src/utils/parser-string-array.ts: -------------------------------------------------------------------------------- 1 | export function parseStringArray(str?: string | string[]): string[] { 2 | if (!str) { 3 | return []; 4 | } 5 | if (typeof str === 'string') { 6 | // split by comma or space and trim space 7 | return str.split(/[,\s]+/).map((item) => item.trim()); 8 | } 9 | return str.map((item) => item.trim()); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } -------------------------------------------------------------------------------- /src/auth/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; 2 | import { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js'; 3 | 4 | export interface StorageData { 5 | localTokens?: { [appId: string]: string }; // encrypted local tokens by appId 6 | tokens: { [key: string]: AuthInfo }; // encrypted tokens 7 | clients: { [key: string]: OAuthClientInformationFull }; // encrypted clients 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/config.ts: -------------------------------------------------------------------------------- 1 | import { ENV_PATHS } from '../utils/constants'; 2 | 3 | export const AUTH_CONFIG = { 4 | SERVER_NAME: 'lark-mcp', 5 | AES_KEY_NAME: 'encryption-key', 6 | STORAGE_DIR: ENV_PATHS.data, 7 | STORAGE_FILE: 'storage.json', 8 | ENCRYPTION: { 9 | ALGORITHM: 'aes-256-cbc' as const, 10 | KEY_LENGTH: 32, // 256 bits 11 | IV_LENGTH: 16, // 128 bits 12 | }, 13 | } as const; 14 | 15 | export type AuthConfig = typeof AUTH_CONFIG; 16 | -------------------------------------------------------------------------------- /src/mcp-tool/utils/get-should-use-uat.ts: -------------------------------------------------------------------------------- 1 | import { TokenMode } from '../types'; 2 | 3 | export function getShouldUseUAT(tokenMode: TokenMode = TokenMode.AUTO, useUAT?: boolean) { 4 | switch (tokenMode) { 5 | case TokenMode.USER_ACCESS_TOKEN: { 6 | return true; 7 | } 8 | case TokenMode.TENANT_ACCESS_TOKEN: { 9 | return false; 10 | } 11 | case TokenMode.AUTO: 12 | default: { 13 | return useUAT; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build 5 | dist/ 6 | build/ 7 | lib/ 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Environment variables 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # IDE 24 | .idea/ 25 | .vscode/ 26 | *.swp 27 | *.swo 28 | 29 | # OS 30 | .DS_Store 31 | Thumbs.db 32 | 33 | # Test 34 | coverage/ 35 | .nyc_output/ 36 | 37 | api-json -------------------------------------------------------------------------------- /src/mcp-tool/utils/case-transf.ts: -------------------------------------------------------------------------------- 1 | import { ToolNameCase } from '../types'; 2 | 3 | export function caseTransf(toolName: string, caseType?: ToolNameCase) { 4 | if (caseType === 'snake') { 5 | return toolName.replace(/\./g, '_'); 6 | } 7 | if (caseType === 'camel') { 8 | return toolName.replace(/\./g, '_').replace(/_(\w)/g, (_, letter) => letter.toUpperCase()); 9 | } 10 | if (caseType === 'kebab') { 11 | return toolName.replace(/\./g, '-'); 12 | } 13 | return toolName; 14 | } 15 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@larksuiteoapi/node-sdk', () => { 2 | return { 3 | Client: jest.fn().mockImplementation(() => ({ 4 | request: jest.fn(), 5 | im: { 6 | message: { 7 | create: jest.fn(), 8 | }, 9 | chat: { 10 | create: jest.fn(), 11 | list: jest.fn(), 12 | }, 13 | }, 14 | })), 15 | withUserAccessToken: jest.fn((token) => ({ userAccessToken: token })), 16 | }; 17 | }); 18 | 19 | beforeEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { safeJsonParse } from './safe-json-parse'; 4 | 5 | function getPackageJsonVersion() { 6 | const filePath = path.join(__dirname, '../../package.json'); 7 | try { 8 | const packageJson = safeJsonParse(fs.readFileSync(filePath, 'utf8'), { 9 | version: '0.0.0', 10 | }); 11 | return packageJson.version || '0.0.0'; 12 | } catch (error) { 13 | return '0.0.0'; 14 | } 15 | } 16 | 17 | export const currentVersion = getPackageJsonVersion(); 18 | -------------------------------------------------------------------------------- /src/mcp-tool/document-tool/recall/type.ts: -------------------------------------------------------------------------------- 1 | import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; 2 | import type { z } from "zod"; 3 | 4 | export interface DocumentRecallToolOptions { 5 | domain?: string; 6 | smallToBig?: boolean; 7 | count?: number; 8 | multiQuery?: boolean; 9 | timeout?: number; 10 | } 11 | 12 | export interface DocumentRecallTool { 13 | name: string; 14 | description: string; 15 | schema: { query: z.ZodType }; 16 | handler: (params: { query: string }, options: DocumentRecallToolOptions) => Promise; 17 | } -------------------------------------------------------------------------------- /src/mcp-tool/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { BuiltinToolName, BuiltinTools } from './en/builtin-tools'; 2 | import { BuiltinTools as BuiltinToolsZh } from './zh/builtin-tools'; 3 | 4 | import { ToolName as GenToolName, GenTools as GenToolsEn, ProjectName as GenProjectName } from './en/gen-tools'; 5 | import { GenTools as GenToolsZh } from './zh/gen-tools'; 6 | 7 | export type ToolName = GenToolName | BuiltinToolName; 8 | export type ProjectName = GenProjectName; 9 | 10 | export const AllTools = [...GenToolsEn, ...BuiltinTools]; 11 | export const AllToolsZh = [...GenToolsZh, ...BuiltinToolsZh]; 12 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/verification_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type verificationV1ToolName = 'verification.v1.verification.get'; 3 | export const verificationV1VerificationGet = { 4 | project: 'verification', 5 | name: 'verification.v1.verification.get', 6 | sdkName: 'verification.v1.verification.get', 7 | path: '/open-apis/verification/v1/verification', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-认证信息-获取认证信息', 10 | accessTokens: ['tenant'], 11 | schema: {}, 12 | }; 13 | export const verificationV1Tools = [verificationV1VerificationGet]; 14 | -------------------------------------------------------------------------------- /src/auth/utils/pkce.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function generateCodeVerifier(): string { 4 | return crypto.randomBytes(32).toString('base64url'); 5 | } 6 | 7 | export function generateCodeChallenge(codeVerifier: string): string { 8 | return crypto.createHash('sha256').update(codeVerifier).digest('base64url'); 9 | } 10 | 11 | export function generatePKCEPair(): { codeVerifier: string; codeChallenge: string } { 12 | const codeVerifier = generateCodeVerifier(); 13 | const codeChallenge = generateCodeChallenge(codeVerifier); 14 | return { codeVerifier, codeChallenge }; 15 | } 16 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/authen_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type authenV1ToolName = 'authen.v1.userInfo.get'; 3 | export const authenV1UserInfoGet = { 4 | project: 'authen', 5 | name: 'authen.v1.userInfo.get', 6 | sdkName: 'authen.v1.userInfo.get', 7 | path: '/open-apis/authen/v1/user_info', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-认证及授权-登录态管理-获取用户信息-通过 `user_access_token` 获取相关用户信息', 10 | accessTokens: ['user'], 11 | schema: { 12 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 13 | }, 14 | }; 15 | export const authenV1Tools = [authenV1UserInfoGet]; 16 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/verification_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type verificationV1ToolName = 'verification.v1.verification.get'; 3 | export const verificationV1VerificationGet = { 4 | project: 'verification', 5 | name: 'verification.v1.verification.get', 6 | sdkName: 'verification.v1.verification.get', 7 | path: '/open-apis/verification/v1/verification', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-Verification Information-Obtain verification information', 10 | accessTokens: ['tenant'], 11 | schema: {}, 12 | }; 13 | export const verificationV1Tools = [verificationV1VerificationGet]; 14 | -------------------------------------------------------------------------------- /assets/trae.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Add to Trae 8 | -------------------------------------------------------------------------------- /assets/trae-cn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Add to Trae CN 8 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/board_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type boardV1ToolName = 'board.v1.whiteboardNode.list'; 3 | export const boardV1WhiteboardNodeList = { 4 | project: 'board', 5 | name: 'board.v1.whiteboardNode.list', 6 | sdkName: 'board.v1.whiteboardNode.list', 7 | path: '/open-apis/board/v1/whiteboards/:whiteboard_id/nodes', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-云文档-画板-节点-获取所有节点-获取画板内所有的节点', 10 | accessTokens: ['tenant', 'user'], 11 | schema: { 12 | path: z.object({ whiteboard_id: z.string().describe('画板唯一标识') }), 13 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 14 | }, 15 | }; 16 | export const boardV1Tools = [boardV1WhiteboardNodeList]; 17 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/authen_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type authenV1ToolName = 'authen.v1.userInfo.get'; 3 | export const authenV1UserInfoGet = { 4 | project: 'authen', 5 | name: 'authen.v1.userInfo.get', 6 | sdkName: 'authen.v1.userInfo.get', 7 | path: '/open-apis/authen/v1/user_info', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Authenticate and Authorize-Login state management-Get User Information-Get related user info via `user_access_token`', 11 | accessTokens: ['user'], 12 | schema: { 13 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 14 | }, 15 | }; 16 | export const authenV1Tools = [authenV1UserInfoGet]; 17 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/moments_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type momentsV1ToolName = 'moments.v1.post.get'; 3 | export const momentsV1PostGet = { 4 | project: 'moments', 5 | name: 'moments.v1.post.get', 6 | sdkName: 'moments.v1.post.get', 7 | path: '/open-apis/moments/v1/posts/:post_id', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-公司圈-帖子-查询帖子信息-通过 ID 查询帖子实体数据信息', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | params: z 13 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 14 | .optional(), 15 | path: z.object({ post_id: z.string().describe('帖子的ID,可从发布帖子接口返回数据或发布帖子事件中获取') }), 16 | }, 17 | }; 18 | export const momentsV1Tools = [momentsV1PostGet]; 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=text 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/tests/**/*.test.ts'], 5 | collectCoverage: true, 6 | collectCoverageFrom: [ 7 | 'src/**/*.ts', 8 | '!src/cli.ts', 9 | '!src/mcp-tool/tools/**/*.ts', 10 | '!src/**/*.d.ts', 11 | '!src/**/index.ts', 12 | ], 13 | coverageDirectory: 'coverage', 14 | coverageReporters: ['text', 'lcov'], 15 | moduleNameMapper: { 16 | '^@/(.*)$': '/src/$1', 17 | }, 18 | moduleFileExtensions: ['ts', 'js', 'json'], 19 | transform: { 20 | '^.+\\.ts$': [ 21 | 'ts-jest', 22 | { 23 | tsconfig: 'tsconfig.json', 24 | }, 25 | ], 26 | }, 27 | transformIgnorePatterns: ['/node_modules/(?!@modelcontextprotocol)'], 28 | setupFilesAfterEnv: ['./tests/setup.ts'], 29 | }; 30 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/board_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type boardV1ToolName = 'board.v1.whiteboardNode.list'; 3 | export const boardV1WhiteboardNodeList = { 4 | project: 'board', 5 | name: 'board.v1.whiteboardNode.list', 6 | sdkName: 'board.v1.whiteboardNode.list', 7 | path: '/open-apis/board/v1/whiteboards/:whiteboard_id/nodes', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-Docs-Board-nodes-list nodes-Obtain all nodes of a board', 10 | accessTokens: ['tenant', 'user'], 11 | schema: { 12 | path: z.object({ whiteboard_id: z.string().describe('The unique identification of the board') }), 13 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 14 | }, 15 | }; 16 | export const boardV1Tools = [boardV1WhiteboardNodeList]; 17 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/wiki_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type wikiV1ToolName = 'wiki.v1.node.search'; 3 | export const wikiV1NodeSearch = { 4 | project: 'wiki', 5 | name: 'wiki.v1.node.search', 6 | sdkName: 'wiki.v1.node.search', 7 | path: '/open-apis/wiki/v1/nodes/search', 8 | httpMethod: 'POST', 9 | description: '[Feishu/Lark]-Docs-Wiki-Search Wiki', 10 | accessTokens: ['user'], 11 | schema: { 12 | data: z.object({ query: z.string(), space_id: z.string().optional(), node_id: z.string().optional() }), 13 | params: z.object({ page_token: z.string().optional(), page_size: z.number().optional() }).optional(), 14 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 15 | }, 16 | }; 17 | export const wikiV1Tools = [wikiV1NodeSearch]; 18 | -------------------------------------------------------------------------------- /src/mcp-tool/document-tool/recall/request.ts: -------------------------------------------------------------------------------- 1 | import { DocumentRecallToolOptions } from './type'; 2 | import { commonHttpInstance } from '../../../utils/http-instance'; 3 | 4 | export const recallDeveloperDocument = async (query: string, options: DocumentRecallToolOptions) => { 5 | try { 6 | const { domain, count = 3 } = options; 7 | // Get Feishu search API endpoint 8 | const searchEndpoint = `${domain}/document_portal/v1/recall`; 9 | const payload = { 10 | question: query, 11 | }; 12 | // Send network request to Feishu search API 13 | const response = await commonHttpInstance.post(searchEndpoint, payload, { 14 | timeout: 10000, 15 | }); 16 | 17 | // Process search results 18 | let results = response.data.chunks || []; 19 | return results.slice(0, count); 20 | } catch (error) { 21 | throw error; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/optical_char_recognition_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type opticalCharRecognitionV1ToolName = 'optical_char_recognition.v1.image.basicRecognize'; 3 | export const opticalCharRecognitionV1ImageBasicRecognize = { 4 | project: 'optical_char_recognition', 5 | name: 'optical_char_recognition.v1.image.basicRecognize', 6 | sdkName: 'optical_char_recognition.v1.image.basicRecognize', 7 | path: '/open-apis/optical_char_recognition/v1/image/basic_recognize', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-AI 能力-光学字符识别-识别图片中的文字-可识别图片中的文字,按图片中的区域划分,分段返回文本列表。文件大小需小于5M', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ image: z.string().describe('base64 后的图片数据').optional() }).optional(), 14 | }, 15 | }; 16 | export const opticalCharRecognitionV1Tools = [opticalCharRecognitionV1ImageBasicRecognize]; 17 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/event_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type eventV1ToolName = 'event.v1.outboundIp.list'; 3 | export const eventV1OutboundIpList = { 4 | project: 'event', 5 | name: 'event.v1.outboundIp.list', 6 | sdkName: 'event.v1.outboundIp.list', 7 | path: '/open-apis/event/v1/outbound_ip', 8 | httpMethod: 'GET', 9 | description: 10 | "[Feishu/Lark]-Events and callbacks-Event subscriptions-Get event's outbound IP-When Feishu Open Platform pushes events to the callback address configured by the application, it is sent out through a specific IP, and the application can get all relevant IP addresses through this interface", 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z.object({ page_size: z.number().optional(), page_token: z.string().optional() }).optional(), 14 | }, 15 | }; 16 | export const eventV1Tools = [eventV1OutboundIpList]; 17 | -------------------------------------------------------------------------------- /src/utils/http-instance.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import { ProxyAgent } from 'proxy-agent'; 3 | import { USER_AGENT } from './constants'; 4 | 5 | const proxyAgent = new ProxyAgent(); 6 | 7 | const traceMiddleware = (request: T) => { 8 | if (request.headers) { 9 | request.headers['User-Agent'] = USER_AGENT; 10 | } 11 | return request; 12 | }; 13 | 14 | export const commonHttpInstance = axios.create({ proxy: false, httpAgent: proxyAgent, httpsAgent: proxyAgent }); 15 | commonHttpInstance.interceptors.request.use(traceMiddleware, undefined, { synchronous: true }); 16 | 17 | export const oapiHttpInstance = axios.create({ proxy: false, httpAgent: proxyAgent, httpsAgent: proxyAgent }); 18 | oapiHttpInstance.interceptors.request.use(traceMiddleware, undefined, { synchronous: true }); 19 | oapiHttpInstance.interceptors.response.use((response) => response.data); 20 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/event_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type eventV1ToolName = 'event.v1.outboundIp.list'; 3 | export const eventV1OutboundIpList = { 4 | project: 'event', 5 | name: 'event.v1.outboundIp.list', 6 | sdkName: 'event.v1.outboundIp.list', 7 | path: '/open-apis/event/v1/outbound_ip', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-事件与回调-事件订阅-获取事件出口 IP-飞书开放平台向应用配置的回调地址推送事件时,是通过特定的 IP 发送出去的,应用可以通过本接口获取所有相关的 IP 地址', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z 14 | .object({ 15 | page_size: z.number().describe('分页大小,默认10,取值范围 10-50').optional(), 16 | page_token: z 17 | .string() 18 | .describe( 19 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 20 | ) 21 | .optional(), 22 | }) 23 | .optional(), 24 | }, 25 | }; 26 | export const eventV1Tools = [eventV1OutboundIpList]; 27 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/moments_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type momentsV1ToolName = 'moments.v1.post.get'; 3 | export const momentsV1PostGet = { 4 | project: 'moments', 5 | name: 'moments.v1.post.get', 6 | sdkName: 'moments.v1.post.get', 7 | path: '/open-apis/moments/v1/posts/:post_id', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-Moments-Post-Query post information-Query post entity data information by post id', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | params: z 13 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 14 | .optional(), 15 | path: z.object({ 16 | post_id: z 17 | .string() 18 | .describe( 19 | 'Post ID, which can be got from the data returned by the "Publish moment" interface or the "Moment posted" event', 20 | ), 21 | }), 22 | }, 23 | }; 24 | export const momentsV1Tools = [momentsV1PostGet]; 25 | -------------------------------------------------------------------------------- /src/auth/utils/is-token-valid.ts: -------------------------------------------------------------------------------- 1 | import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; 2 | import { authStore } from '../store'; 3 | 4 | export async function isTokenValid( 5 | accessToken?: string, 6 | ): Promise<{ valid: boolean; isExpired: boolean; token: AuthInfo | undefined }> { 7 | if (!accessToken) { 8 | return { valid: false, isExpired: false, token: undefined }; 9 | } 10 | const token = await authStore.getToken(accessToken); 11 | if (!token) { 12 | return { valid: false, isExpired: false, token: undefined }; 13 | } 14 | const isExpired = isTokenExpired(token); 15 | if (isExpired) { 16 | return { valid: false, isExpired: true, token }; 17 | } 18 | return { valid: true, isExpired: false, token }; 19 | } 20 | 21 | export function isTokenExpired(token?: AuthInfo) { 22 | if (!token) { 23 | return false; 24 | } 25 | if (token.expiresAt && token.expiresAt < Date.now() / 1000) { 26 | return true; 27 | } 28 | return false; 29 | } 30 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/optical_char_recognition_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type opticalCharRecognitionV1ToolName = 'optical_char_recognition.v1.image.basicRecognize'; 3 | export const opticalCharRecognitionV1ImageBasicRecognize = { 4 | project: 'optical_char_recognition', 5 | name: 'optical_char_recognition.v1.image.basicRecognize', 6 | sdkName: 'optical_char_recognition.v1.image.basicRecognize', 7 | path: '/open-apis/optical_char_recognition/v1/image/basic_recognize', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-AI-Optical character recognition-Recognize text in pictures-Basic picture recognition interface, recognize the text in the picture, and return the text list by area.File size must be less than 5M', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ image: z.string().describe('Picture data after base64').optional() }).optional(), 14 | }, 15 | }; 16 | export const opticalCharRecognitionV1Tools = [opticalCharRecognitionV1ImageBasicRecognize]; 17 | -------------------------------------------------------------------------------- /src/mcp-tool/utils/filter-tools.ts: -------------------------------------------------------------------------------- 1 | import { ToolName, ProjectName } from '../tools'; 2 | import { McpTool, ToolsFilterOptions, TokenMode } from '../types'; 3 | 4 | export function filterTools(tools: McpTool[], options: ToolsFilterOptions) { 5 | let filteredTools = tools.filter( 6 | (tool) => 7 | options.allowTools?.includes(tool.name as ToolName) || 8 | options.allowProjects?.includes(tool.project as ProjectName), 9 | ); 10 | 11 | // Filter by token mode 12 | if (options.tokenMode && options.tokenMode !== TokenMode.AUTO) { 13 | filteredTools = filteredTools.filter((tool) => { 14 | if (!tool.accessTokens) { 15 | return false; 16 | } 17 | if (options.tokenMode === TokenMode.USER_ACCESS_TOKEN) { 18 | return tool.accessTokens.includes('user'); 19 | } 20 | if (options.tokenMode === TokenMode.TENANT_ACCESS_TOKEN) { 21 | return tool.accessTokens.includes('tenant'); 22 | } 23 | return true; 24 | }); 25 | } 26 | 27 | return filteredTools; 28 | } 29 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/tenant_v2.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type tenantV2ToolName = 'tenant.v2.tenantProductAssignInfo.query' | 'tenant.v2.tenant.query'; 3 | export const tenantV2TenantProductAssignInfoQuery = { 4 | project: 'tenant', 5 | name: 'tenant.v2.tenantProductAssignInfo.query', 6 | sdkName: 'tenant.v2.tenantProductAssignInfo.query', 7 | path: '/open-apis/tenant/v2/tenant/assign_info_list/query', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-企业信息-企业席位信息-获取企业席位信息接口-获取租户下待分配的席位列表,包含席位名称、席位ID、数量及对应有效期', 11 | accessTokens: ['tenant'], 12 | schema: {}, 13 | }; 14 | export const tenantV2TenantQuery = { 15 | project: 'tenant', 16 | name: 'tenant.v2.tenant.query', 17 | sdkName: 'tenant.v2.tenant.query', 18 | path: '/open-apis/tenant/v2/tenant/query', 19 | httpMethod: 'GET', 20 | description: '[Feishu/Lark]-企业信息-获取企业信息-获取企业名称、企业编号等企业信息', 21 | accessTokens: ['tenant'], 22 | schema: {}, 23 | }; 24 | export const tenantV2Tools = [tenantV2TenantProductAssignInfoQuery, tenantV2TenantQuery]; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lark Technologies Pte. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice, shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/docs_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type docsV1ToolName = 'docs.v1.content.get'; 3 | export const docsV1ContentGet = { 4 | project: 'docs', 5 | name: 'docs.v1.content.get', 6 | sdkName: 'docs.v1.content.get', 7 | path: '/open-apis/docs/v1/content', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-云文档-通用-获取云文档内容-可获取云文档内容,当前只支持获取新版文档 Markdown 格式的内容', 10 | accessTokens: ['tenant', 'user'], 11 | schema: { 12 | params: z.object({ 13 | doc_token: z.string().describe('云文档的唯一标识。点击[这里]了解如何获取文档的 `doc_token`'), 14 | doc_type: z.literal('docx').describe('云文档类型 Options:docx(新版文档)'), 15 | content_type: z.literal('markdown').describe('内容类型 Options:markdown(Markdown 格式)'), 16 | lang: z 17 | .enum(['zh', 'en', 'ja']) 18 | .describe( 19 | '云文档中存在 @用户 元素时,指定该用户名称的语言。默认 `zh`,即中文 Options:zh(中文),en(英文),ja(日文)', 20 | ) 21 | .optional(), 22 | }), 23 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 24 | }, 25 | }; 26 | export const docsV1Tools = [docsV1ContentGet]; 27 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnvArgs } from './clean-env-args'; 2 | import { currentVersion } from './version'; 3 | import envPaths from 'env-paths'; 4 | 5 | export const ENV_PATHS = envPaths('lark-mcp'); 6 | 7 | const [major] = process.versions.node.split('.').map(Number); 8 | 9 | export const USER_AGENT = `oapi-sdk-mcp/${currentVersion}`; 10 | export const NODE_VERSION_MAJOR = major; 11 | 12 | export const OAPI_MCP_DEFAULT_ARGS = { 13 | domain: 'https://open.feishu.cn', 14 | toolNameCase: 'snake', 15 | language: 'en', 16 | tokenMode: 'auto', 17 | mode: 'stdio', 18 | host: 'localhost', 19 | port: '3000', 20 | }; 21 | 22 | export const OAPI_MCP_ENV_ARGS = cleanEnvArgs({ 23 | appId: process.env.APP_ID, 24 | appSecret: process.env.APP_SECRET, 25 | userAccessToken: process.env.USER_ACCESS_TOKEN, 26 | tokenMode: process.env.LARK_TOKEN_MODE, 27 | tools: process.env.LARK_TOOLS, 28 | domain: process.env.LARK_DOMAIN, 29 | }); 30 | 31 | export enum OAPI_MCP_ERROR_CODE { 32 | USER_ACCESS_TOKEN_INVALID = 99991668, 33 | USER_ACCESS_TOKEN_UNAUTHORIZED = 99991679, 34 | } 35 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/human_authentication_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type humanAuthenticationV1ToolName = 'human_authentication.v1.identity.create'; 3 | export const humanAuthenticationV1IdentityCreate = { 4 | project: 'human_authentication', 5 | name: 'human_authentication.v1.identity.create', 6 | sdkName: 'human_authentication.v1.identity.create', 7 | path: '/open-apis/human_authentication/v1/identities', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-实名认证-录入身份信息-该接口用于录入实名认证的身份信息,在唤起有源活体认证前,需要使用该接口进行实名认证', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ 14 | identity_name: z.string().describe('姓名'), 15 | identity_code: z.string().describe('身份证号'), 16 | mobile: z.string().describe('手机号').optional(), 17 | }), 18 | params: z.object({ 19 | user_id: z 20 | .string() 21 | .describe( 22 | '用户的唯一标识(使用的ID类型见下一参数描述,不同ID类型的区别和获取,参考文档:[如何获得 User ID、Open ID 和 Union ID?])', 23 | ), 24 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional(), 25 | }), 26 | }, 27 | }; 28 | export const humanAuthenticationV1Tools = [humanAuthenticationV1IdentityCreate]; 29 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/wiki_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type wikiV1ToolName = 'wiki.v1.node.search'; 3 | export const wikiV1NodeSearch = { 4 | project: 'wiki', 5 | name: 'wiki.v1.node.search', 6 | sdkName: 'wiki.v1.node.search', 7 | path: '/open-apis/wiki/v1/nodes/search', 8 | httpMethod: 'POST', 9 | description: '[Feishu/Lark]-云文档-知识库-搜索 Wiki', 10 | accessTokens: ['user'], 11 | schema: { 12 | data: z.object({ 13 | query: z.string().describe('搜索关键词'), 14 | space_id: z.string().describe('文档所属的知识空间ID,为空搜索所有 wiki').optional(), 15 | node_id: z 16 | .string() 17 | .describe('wiki token,不为空搜索该节点及其所有子节点,为空搜索所有 wiki(根据 space_id 选择 space)') 18 | .optional(), 19 | }), 20 | params: z 21 | .object({ 22 | page_token: z 23 | .string() 24 | .describe( 25 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该page_token 获取查询结果', 26 | ) 27 | .optional(), 28 | page_size: z.number().describe('分页大小').optional(), 29 | }) 30 | .optional(), 31 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 32 | }, 33 | }; 34 | export const wikiV1Tools = [wikiV1NodeSearch]; 35 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/tenant_v2.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type tenantV2ToolName = 'tenant.v2.tenantProductAssignInfo.query' | 'tenant.v2.tenant.query'; 3 | export const tenantV2TenantProductAssignInfoQuery = { 4 | project: 'tenant', 5 | name: 'tenant.v2.tenantProductAssignInfo.query', 6 | sdkName: 'tenant.v2.tenantProductAssignInfo.query', 7 | path: '/open-apis/tenant/v2/tenant/assign_info_list/query', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Company Information-Tenant Product Assign Info-Obtain company assign information-Obtain the seat list to be allocated under the tenant, including seat name, subscription ID, quantity and validity period', 11 | accessTokens: ['tenant'], 12 | schema: {}, 13 | }; 14 | export const tenantV2TenantQuery = { 15 | project: 'tenant', 16 | name: 'tenant.v2.tenant.query', 17 | sdkName: 'tenant.v2.tenant.query', 18 | path: '/open-apis/tenant/v2/tenant/query', 19 | httpMethod: 'GET', 20 | description: 21 | '[Feishu/Lark]-Company Information-Obtain company information-Obtains company information such as the company name and the company ID', 22 | accessTokens: ['tenant'], 23 | schema: {}, 24 | }; 25 | export const tenantV2Tools = [tenantV2TenantProductAssignInfoQuery, tenantV2TenantQuery]; 26 | -------------------------------------------------------------------------------- /src/mcp-tool/document-tool/recall/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { DocumentRecallTool } from "./type"; 3 | import { recallDeveloperDocument } from "./request"; 4 | 5 | const searchParamsSchema = { 6 | query: z.string().describe("user input"), 7 | }; 8 | 9 | export const RecallTool: DocumentRecallTool = { 10 | name: "openplatform_developer_document_recall", 11 | description: "Recall for relevant documents in all of the Feishu/Lark Open Platform Developer Documents based on user input.", 12 | schema: searchParamsSchema, 13 | handler: async (params, options) => { 14 | const { query } = params; 15 | try { 16 | const results = await recallDeveloperDocument(query, options); 17 | // Return results 18 | return { 19 | content: [ 20 | { 21 | type: "text", 22 | text: results.length ? `Find ${results.length} results:\n${results.join("\n\n")}` : "No results found", 23 | } 24 | ], 25 | }; 26 | } catch (error: Error | unknown) { 27 | return { 28 | isError: true, 29 | content: [ 30 | { 31 | type: "text", 32 | text: `Search failed:${error instanceof Error ? error.message : 'Unknown error'}`, 33 | }, 34 | ], 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/mcp-server/transport/utils.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { McpServerOptions, mcpServerOptionSchema } from '../shared'; 3 | 4 | export enum JSONRPCErrorCodes { 5 | PARSE_ERROR = -32700, 6 | INVALID_REQUEST = -32600, 7 | METHOD_NOT_FOUND = -32601, 8 | INVALID_PARAMS = -32602, 9 | INTERNAL_ERROR = -32603, 10 | } 11 | 12 | export function parseMCPServerOptionsFromRequest(req: Request): { 13 | data: McpServerOptions; 14 | success: boolean; 15 | message?: string; 16 | } { 17 | const result = mcpServerOptionSchema.safeParse(req.query || {}); 18 | if (!result.success) { 19 | return { data: {}, success: false, message: result.error.message }; 20 | } 21 | return { data: result.data as McpServerOptions, success: true }; 22 | } 23 | 24 | export function sendJsonRpcError( 25 | res: Response, 26 | error: Error, 27 | httpCode = 500, 28 | code = JSONRPCErrorCodes.INTERNAL_ERROR, 29 | id: number | null = null, 30 | ) { 31 | console.error(error); 32 | if (!res.headersSent) { 33 | res.status(httpCode).json({ jsonrpc: '2.0', error: { code, message: error.message }, id }); 34 | } 35 | } 36 | 37 | export function sendResponseError(res: Response, error: Error, httpCode = 500): void { 38 | console.error(error); 39 | if (!res.headersSent) { 40 | res.status(httpCode).send(error.message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/mcp-tool/utils/case-transf.test.ts: -------------------------------------------------------------------------------- 1 | import { caseTransf } from '../../../src/mcp-tool/utils/case-transf'; 2 | 3 | describe('caseTransf', () => { 4 | const testToolName = 'im.v1.chat.create'; 5 | 6 | it('应该将点号转换为下划线 (snake case)', () => { 7 | const result = caseTransf(testToolName, 'snake'); 8 | expect(result).toBe('im_v1_chat_create'); 9 | }); 10 | 11 | it('应该将点号转换为驼峰命名 (camel case)', () => { 12 | const result = caseTransf(testToolName, 'camel'); 13 | expect(result).toBe('imV1ChatCreate'); 14 | }); 15 | 16 | it('应该将点号转换为短横线 (kebab case)', () => { 17 | const result = caseTransf(testToolName, 'kebab'); 18 | expect(result).toBe('im-v1-chat-create'); 19 | }); 20 | 21 | it('不指定时应该保持原样 (dot case)', () => { 22 | const result = caseTransf(testToolName); 23 | expect(result).toBe('im.v1.chat.create'); 24 | }); 25 | 26 | it('处理已经是目标格式的工具名', () => { 27 | const kebabCaseName = 'im-v1-chat-create'; 28 | const result = caseTransf(kebabCaseName, 'kebab'); 29 | expect(result).toBe('im-v1-chat-create'); 30 | }); 31 | 32 | it('处理空字符串', () => { 33 | const result = caseTransf('', 'camel'); 34 | expect(result).toBe(''); 35 | }); 36 | 37 | it('处理没有点号的工具名', () => { 38 | const noDotName = 'createchat'; 39 | const result = caseTransf(noDotName, 'snake'); 40 | expect(result).toBe('createchat'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/mcp-server/shared/types.ts: -------------------------------------------------------------------------------- 1 | import * as larkmcp from '../../mcp-tool'; 2 | import { z } from 'zod'; 3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 4 | import { LarkAuthHandler } from '../../auth'; 5 | 6 | export type McpServerType = 'oapi' | 'recall'; 7 | export type McpServerTransport = 'stdio' | 'sse' | 'streamable'; 8 | 9 | export const mcpServerOptionSchema = z.object({ 10 | tools: z.union([z.string(), z.array(z.string())]).optional(), 11 | language: z.enum(['zh', 'en']).optional(), 12 | toolNameCase: z.enum(['snake', 'camel']).optional(), 13 | tokenMode: z.enum(['auto', 'user_access_token', 'tenant_access_token']).optional(), 14 | }); 15 | 16 | export interface McpServerOptions { 17 | appId?: string; 18 | appSecret?: string; 19 | domain?: string; 20 | tools?: string[]; 21 | language?: 'zh' | 'en'; 22 | toolNameCase?: larkmcp.ToolNameCase; 23 | tokenMode?: larkmcp.TokenMode; 24 | userAccessToken?: string | larkmcp.SettableValue; 25 | oauth?: boolean; 26 | scope?: string[]; 27 | 28 | mode?: McpServerTransport; 29 | host?: string; 30 | port?: number; 31 | } 32 | 33 | export type InitTransportServerFunction = ( 34 | getNewServer: (options?: McpServerOptions, authHandler?: LarkAuthHandler) => McpServer, 35 | mcpServerOptions: McpServerOptions, 36 | authOptions?: { needAuthFlow: boolean }, 37 | ) => void | Promise; 38 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/docs_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type docsV1ToolName = 'docs.v1.content.get'; 3 | export const docsV1ContentGet = { 4 | project: 'docs', 5 | name: 'docs.v1.content.get', 6 | sdkName: 'docs.v1.content.get', 7 | path: '/open-apis/docs/v1/content', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Docs-Common-Get docs content-You can obtain the docs content. Currently, only upgraded document content in markdown format is supported', 11 | accessTokens: ['tenant', 'user'], 12 | schema: { 13 | params: z.object({ 14 | doc_token: z 15 | .string() 16 | .describe('The unique identification of the docs. Click [here] to learn how to get `doc_token`'), 17 | doc_type: z.literal('docx').describe('Docs type Options:docx(Upgraded Document)'), 18 | content_type: z.literal('markdown').describe('Content type Options:markdown(Markdown format)'), 19 | lang: z 20 | .enum(['zh', 'en', 'ja']) 21 | .describe( 22 | 'Specifies the language of the user name when the @user element exists in the docs. Default `zh` Options:zh(Chinese),en(English),ja(Japanese)', 23 | ) 24 | .optional(), 25 | }), 26 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 27 | }, 28 | }; 29 | export const docsV1Tools = [docsV1ContentGet]; 30 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/security_and_compliance_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type securityAndComplianceV1ToolName = 'security_and_compliance.v1.openapiLog.listData'; 3 | export const securityAndComplianceV1OpenapiLogListData = { 4 | project: 'security_and_compliance', 5 | name: 'security_and_compliance.v1.openapiLog.listData', 6 | sdkName: 'security_and_compliance.v1.openapiLog.listData', 7 | path: '/open-apis/security_and_compliance/v1/openapi_logs/list_data', 8 | httpMethod: 'POST', 9 | description: '[Feishu/Lark]-安全合规-OpenAPI审计日志-获取OpenAPI审计日志数据-该接口用于获取OpenAPI审计日志数据', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | data: z 13 | .object({ 14 | api_keys: z.array(z.string()).describe('飞书开放平台定义的API,参考:[API列表]').optional(), 15 | start_time: z.number().describe('以秒为单位的起始时间戳').optional(), 16 | end_time: z.number().describe('以秒为单位的终止时间戳').optional(), 17 | app_id: z 18 | .string() 19 | .describe('调用OpenAPI的应用唯一标识,可以前往 [开发者后台] > 应用详情页 > 凭证与基础信息中获取 app_id') 20 | .optional(), 21 | page_size: z.number().describe('分页大小').optional(), 22 | page_token: z 23 | .string() 24 | .describe( 25 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 26 | ) 27 | .optional(), 28 | }) 29 | .optional(), 30 | }, 31 | }; 32 | export const securityAndComplianceV1Tools = [securityAndComplianceV1OpenapiLogListData]; 33 | -------------------------------------------------------------------------------- /src/auth/utils/encryption.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { AUTH_CONFIG } from '../config'; 3 | import { logger } from '../../utils/logger'; 4 | 5 | export class EncryptionUtil { 6 | private aesKey: string; 7 | 8 | constructor(aesKey: string) { 9 | this.aesKey = aesKey; 10 | } 11 | 12 | encrypt(data: string): string { 13 | const iv = crypto.randomBytes(AUTH_CONFIG.ENCRYPTION.IV_LENGTH); 14 | const key = Buffer.from(this.aesKey, 'hex'); 15 | const cipher = crypto.createCipheriv(AUTH_CONFIG.ENCRYPTION.ALGORITHM, key, iv); 16 | let encrypted = cipher.update(data, 'utf8', 'hex'); 17 | encrypted += cipher.final('hex'); 18 | return iv.toString('hex') + ':' + encrypted; 19 | } 20 | 21 | decrypt(encryptedData: string): string { 22 | const parts = encryptedData.split(':'); 23 | if (parts.length !== 2) { 24 | logger.error(`[EncryptionUtil] decrypt: Invalid encrypted data format`); 25 | throw new Error('Invalid encrypted data format'); 26 | } 27 | const iv = Buffer.from(parts[0], 'hex'); 28 | const encrypted = parts[1]; 29 | const key = Buffer.from(this.aesKey, 'hex'); 30 | const decipher = crypto.createDecipheriv(AUTH_CONFIG.ENCRYPTION.ALGORITHM, key, iv); 31 | let decrypted = decipher.update(encrypted, 'hex', 'utf8'); 32 | decrypted += decipher.final('utf8'); 33 | return decrypted; 34 | } 35 | 36 | static generateKey(): string { 37 | return crypto.randomBytes(AUTH_CONFIG.ENCRYPTION.KEY_LENGTH).toString('hex'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/human_authentication_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type humanAuthenticationV1ToolName = 'human_authentication.v1.identity.create'; 3 | export const humanAuthenticationV1IdentityCreate = { 4 | project: 'human_authentication', 5 | name: 'human_authentication.v1.identity.create', 6 | sdkName: 'human_authentication.v1.identity.create', 7 | path: '/open-apis/human_authentication/v1/identities', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-Identity Authentication-Upload Identity Information-This interface is used to upload the identity information for real-name authentication. Before arousing active living body authentication, this interface needs to be used for real-name authentication', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ 14 | identity_name: z.string().describe('Name'), 15 | identity_code: z.string().describe('User identification number'), 16 | mobile: z.string().describe('Mobile phone').optional(), 17 | }), 18 | params: z.object({ 19 | user_id: z 20 | .string() 21 | .describe( 22 | 'The unique identifier of the user(For the ID type used, see the next parameter description, the difference and acquisition of different ID types, refer to the document: [How to obtain User ID, Open ID and Union ID?])', 23 | ), 24 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional(), 25 | }), 26 | }, 27 | }; 28 | export const humanAuthenticationV1Tools = [humanAuthenticationV1IdentityCreate]; 29 | -------------------------------------------------------------------------------- /src/mcp-server/transport/stdio.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { InitTransportServerFunction } from '../shared'; 4 | import { authStore } from '../../auth'; 5 | import { LarkAuthHandlerLocal } from '../../auth'; 6 | import { logger } from '../../utils/logger'; 7 | 8 | export const initStdioServer: InitTransportServerFunction = async ( 9 | getNewServer, 10 | options, 11 | { needAuthFlow } = { needAuthFlow: false }, 12 | ) => { 13 | const { userAccessToken, appId, oauth } = options; 14 | 15 | let authHandler: LarkAuthHandlerLocal | undefined; 16 | 17 | if (!userAccessToken && needAuthFlow) { 18 | const app = express(); 19 | app.use(express.json()); 20 | authHandler = new LarkAuthHandlerLocal(app, options); 21 | if (oauth) { 22 | authHandler.setupRoutes(); 23 | } 24 | } 25 | 26 | const transport = new StdioServerTransport(); 27 | 28 | const userAccessTokenValue = userAccessToken 29 | ? userAccessToken 30 | : appId 31 | ? { getter: async () => await authStore.getLocalAccessToken(appId) } 32 | : undefined; 33 | 34 | const mcpServer = getNewServer({ ...options, userAccessToken: userAccessTokenValue }, authHandler); 35 | 36 | logger.info( 37 | `[StdioServerTransport] Connecting to MCP Server, userAccessToken: ${Boolean(userAccessToken)}, appId: ${appId}`, 38 | ); 39 | mcpServer.connect(transport).catch((error) => { 40 | logger.error(`[StdioServerTransport] MCP Connect Error: ${error}`); 41 | process.exit(1); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /docs/troubleshooting/faq-zh.md: -------------------------------------------------------------------------------- 1 | ## 常见问题(FAQ) 2 | 3 | 以下为常见问题与解决方案,附加补充说明便于定位原因与快速处理。 4 | 5 | ### 无法连接到飞书/Lark API 6 | 7 | 解决方案: 8 | - 检查本地网络连接、代理设置。 9 | - 核对 `APP_ID`、`APP_SECRET` 是否填写正确。 10 | - 测试是否能正常访问开放平台 API 域名(如 `https://open.feishu.cn` 或 `https://open.larksuite.com`)。 11 | 12 | ### 使用 user_access_token 报错 13 | 14 | 解决方案: 15 | - 检查 token 是否过期(通常 2 小时有效)。 16 | - 建议优先使用 `login` 获取并保存用户令牌。 17 | - 若在服务器/CI 环境使用,确保安全管理令牌并妥善刷新。 18 | 19 | ### 启动 MCP 服务后调用某些 API 提示权限不足 20 | 21 | 解决方案: 22 | - 在开发者后台为应用开通对应 API 权限,并等待审批通过。 23 | - 以用户身份调用(需要 `user_access_token`)的场景,确认授权范围(`scope`)是否包含对应权限。如果授权范围不足,需要重新登录。 24 | 25 | ### 图片或文件上传/下载相关 API 失败 26 | 27 | 解决方案: 28 | - 当前版本暂不支持文件/图片上传下载,相关能力将于后续版本支持。 29 | 30 | ### Windows 终端显示乱码 31 | 32 | 解决方案: 33 | - 在命令提示符中执行 `chcp 65001` 切换为 UTF-8。 34 | - PowerShell 用户可调整字体或相关终端设置以提升兼容性。 35 | 36 | ### 安装时遇到权限错误 37 | 38 | 解决方案: 39 | - macOS/Linux:使用 `sudo npm install -g @larksuiteoapi/lark-mcp` 或调整 npm 全局路径权限。 40 | - Windows:尝试以管理员身份运行命令提示符。 41 | 42 | ### 启动 MCP 服务后提示 token 超过上限 43 | 44 | 解决方案: 45 | - 使用 `-t`(或在 MCP 配置中 `args` 的 `-t`)减少启用的 API 数量。 46 | - 使用支持更大上下文长度的模型。 47 | 48 | ### SSE/Streamable 模式下无法连接或接收消息 49 | 50 | 解决方案: 51 | - 检查端口占用情况,必要时更换端口。 52 | - 确保客户端正确连接到对应端点并能处理事件流。 53 | 54 | ### Linux 环境启动报错 [StorageManager] Failed to initialize: xxx 55 | 56 | 说明: 57 | - 不影响“手动传入 `user_access_token`”或“不使用 `user_access_token`”的场景。 58 | 59 | 原因:`StorageManager` 使用 keytar 对 `user_access_token` 做加密存储。 60 | 61 | 解决方案: 62 | - 安装 `libsecret` 依赖: 63 | - Debian/Ubuntu: `sudo apt-get install libsecret-1-dev` 64 | - Red Hat-based: `sudo yum install libsecret-devel` 65 | - Arch Linux: `sudo pacman -S libsecret` 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/translation_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type translationV1ToolName = 'translation.v1.text.detect' | 'translation.v1.text.translate'; 3 | export const translationV1TextDetect = { 4 | project: 'translation', 5 | name: 'translation.v1.text.detect', 6 | sdkName: 'translation.v1.text.detect', 7 | path: '/open-apis/translation/v1/text/detect', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-AI 能力-机器翻译-识别文本语种-机器翻译 (MT),支持 100 多种语言识别,返回符合 ISO 639-1 标准', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ text: z.string().describe('需要被识别语种的文本') }), 14 | }, 15 | }; 16 | export const translationV1TextTranslate = { 17 | project: 'translation', 18 | name: 'translation.v1.text.translate', 19 | sdkName: 'translation.v1.text.translate', 20 | path: '/open-apis/translation/v1/text/translate', 21 | httpMethod: 'POST', 22 | description: 23 | '[Feishu/Lark]-AI 能力-机器翻译-翻译文本-机器翻译 (MT),支持以下语种互译:"zh": 汉语;"zh-Hant": 繁体汉语;"en": 英语;"ja": 日语;"ru": 俄语;"de": 德语;"fr": 法语;"it": 意大利语;"pl": 波兰语;"th": 泰语;"hi": 印地语;"id": 印尼语;"es": 西班牙语;"pt": 葡萄牙语;"ko": 朝鲜语;"vi": 越南语;', 24 | accessTokens: ['tenant'], 25 | schema: { 26 | data: z.object({ 27 | source_language: z.string().describe('源语言'), 28 | text: z.string().describe('源文本,字符上限为 1,000'), 29 | target_language: z.string().describe('目标语言'), 30 | glossary: z 31 | .array(z.object({ from: z.string().describe('原文'), to: z.string().describe('译文') })) 32 | .describe('请求级术语表,携带术语,仅在本次翻译中生效(最多能携带 128个术语词)') 33 | .optional(), 34 | }), 35 | }, 36 | }; 37 | export const translationV1Tools = [translationV1TextDetect, translationV1TextTranslate]; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/lark-mcp", 3 | "version": "0.5.1", 4 | "description": "Feishu/Lark OpenAPI MCP", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "chenli.idevlab", 8 | "license": "MIT", 9 | "bin": { 10 | "lark-mcp": "dist/cli.js" 11 | }, 12 | "files": [ 13 | "dist", 14 | "docs", 15 | "CHANGELOG.md", 16 | "README.md", 17 | "README_ZH.md", 18 | "README_RECALL.md", 19 | "README_RECALL_ZH.md", 20 | "LICENSE" 21 | ], 22 | "scripts": { 23 | "build": "rm -rf dist && tsc", 24 | "dev": "ts-node src/index.ts", 25 | "dev:cli": "ts-node src/cli.ts", 26 | "format": "prettier --write \"src/**/*.ts\"", 27 | "prepare": "yarn build", 28 | "test": "jest", 29 | "test:watch": "jest --watch", 30 | "test:coverage": "jest --coverage" 31 | }, 32 | "keywords": [ 33 | "feishu", 34 | "lark", 35 | "mcp", 36 | "open-api", 37 | "ai" 38 | ], 39 | "homepage": "https://github.com/larksuite/lark-openapi-mcp", 40 | "engines": { 41 | "node": ">=20.0.0" 42 | }, 43 | "dependencies": { 44 | "@larksuiteoapi/node-sdk": "^1.50.0", 45 | "@modelcontextprotocol/sdk": "^1.12.1", 46 | "axios": "^1.8.4", 47 | "commander": "^13.1.0", 48 | "dotenv": "^16.4.7", 49 | "env-paths": "^2.2.1", 50 | "express": "^5.1.0", 51 | "keytar": "^7.9.0", 52 | "open": "^8.4.2", 53 | "proxy-agent": "^6.5.0" 54 | }, 55 | "devDependencies": { 56 | "@types/express": "^5.0.1", 57 | "@types/jest": "^29.5.14", 58 | "@types/node": "^20.4.5", 59 | "jest": "^29.7.0", 60 | "prettier": "^3.5.3", 61 | "ts-jest": "^29.3.2", 62 | "ts-node": "^10.9.1", 63 | "typescript": "^5.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/mcp-tool/utils/get-should-use-uat.test.ts: -------------------------------------------------------------------------------- 1 | import { getShouldUseUAT } from '../../../src/mcp-tool/utils/get-should-use-uat'; 2 | import { TokenMode } from '../../../src/mcp-tool/types'; 3 | 4 | describe('getShouldUseUAT', () => { 5 | describe('USER_ACCESS_TOKEN 模式', () => { 6 | it('当有 userAccessToken 时应返回 true', () => { 7 | expect(getShouldUseUAT(TokenMode.USER_ACCESS_TOKEN, false)).toBe(true); 8 | expect(getShouldUseUAT(TokenMode.USER_ACCESS_TOKEN, true)).toBe(true); 9 | }); 10 | 11 | it('当没有 userAccessToken 时应返回 true', () => { 12 | expect(getShouldUseUAT(TokenMode.USER_ACCESS_TOKEN, false)).toBe(true); 13 | expect(getShouldUseUAT(TokenMode.USER_ACCESS_TOKEN, true)).toBe(true); 14 | }); 15 | }); 16 | 17 | describe('TENANT_ACCESS_TOKEN 模式', () => { 18 | it('无论参数如何都应返回 false', () => { 19 | expect(getShouldUseUAT(TokenMode.TENANT_ACCESS_TOKEN, false)).toBe(false); 20 | expect(getShouldUseUAT(TokenMode.TENANT_ACCESS_TOKEN, true)).toBe(false); 21 | expect(getShouldUseUAT(TokenMode.TENANT_ACCESS_TOKEN, false)).toBe(false); 22 | expect(getShouldUseUAT(TokenMode.TENANT_ACCESS_TOKEN, true)).toBe(false); 23 | }); 24 | }); 25 | 26 | describe('AUTO 模式', () => { 27 | it('应该直接返回 useUAT 参数的值', () => { 28 | expect(getShouldUseUAT(TokenMode.AUTO, true)).toBe(true); 29 | expect(getShouldUseUAT(TokenMode.AUTO, true)).toBe(true); 30 | expect(getShouldUseUAT(TokenMode.AUTO, false)).toBe(false); 31 | expect(getShouldUseUAT(TokenMode.AUTO, false)).toBe(false); 32 | }); 33 | 34 | it('当 useUAT 为 undefined 时应返回 undefined', () => { 35 | expect(getShouldUseUAT(TokenMode.AUTO, undefined)).toBe(undefined); 36 | expect(getShouldUseUAT(TokenMode.AUTO, undefined)).toBe(undefined); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/security_and_compliance_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type securityAndComplianceV1ToolName = 'security_and_compliance.v1.openapiLog.listData'; 3 | export const securityAndComplianceV1OpenapiLogListData = { 4 | project: 'security_and_compliance', 5 | name: 'security_and_compliance.v1.openapiLog.listData', 6 | sdkName: 'security_and_compliance.v1.openapiLog.listData', 7 | path: '/open-apis/security_and_compliance/v1/openapi_logs/list_data', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-security_and_compliance-OpenAPI Audit Log-Obtain OpenAPI audit log-This api is used to obtain OpenAPI audit log', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z 14 | .object({ 15 | api_keys: z.array(z.string()).describe('Feishu OpenAPI definition, reference: [API list]').optional(), 16 | start_time: z.number().describe('Starting timestamp in seconds').optional(), 17 | end_time: z.number().describe('Termination timestamp in seconds').optional(), 18 | app_id: z 19 | .string() 20 | .describe( 21 | 'The unique identifier of the application calling OpenAPI, can be obtained by going to [Developer Console] > Application details page > Certificate and Basic Information', 22 | ) 23 | .optional(), 24 | page_size: z.number().describe('paging size').optional(), 25 | page_token: z 26 | .string() 27 | .describe( 28 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 29 | ) 30 | .optional(), 31 | }) 32 | .optional(), 33 | }, 34 | }; 35 | export const securityAndComplianceV1Tools = [securityAndComplianceV1OpenapiLogListData]; 36 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/mdm_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type mdmV1ToolName = 'mdm.v1.userAuthDataRelation.bind' | 'mdm.v1.userAuthDataRelation.unbind'; 3 | export const mdmV1UserAuthDataRelationBind = { 4 | project: 'mdm', 5 | name: 'mdm.v1.userAuthDataRelation.bind', 6 | sdkName: 'mdm.v1.userAuthDataRelation.bind', 7 | path: '/open-apis/mdm/v1/user_auth_data_relations/bind', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-飞书主数据-数据维度-用户数据维度绑定-通过该接口,可为指定应用下的用户绑定一类数据维度,支持批量给多个用户同时增量授权', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ 14 | root_dimension_type: z.string().describe('数据类型编码'), 15 | sub_dimension_types: z.array(z.string()).describe('数据编码列表'), 16 | authorized_user_ids: z.array(z.string()).describe('授权人的lark id'), 17 | uams_app_id: z.string().describe('uams系统中应用id'), 18 | }), 19 | params: z 20 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 21 | .optional(), 22 | }, 23 | }; 24 | export const mdmV1UserAuthDataRelationUnbind = { 25 | project: 'mdm', 26 | name: 'mdm.v1.userAuthDataRelation.unbind', 27 | sdkName: 'mdm.v1.userAuthDataRelation.unbind', 28 | path: '/open-apis/mdm/v1/user_auth_data_relations/unbind', 29 | httpMethod: 'POST', 30 | description: 31 | '[Feishu/Lark]-飞书主数据-数据维度-用户数据维度解绑-通过该接口,可为指定应用下的指定用户解除一类数据维度', 32 | accessTokens: ['tenant'], 33 | schema: { 34 | data: z.object({ 35 | root_dimension_type: z.string().describe('数据类型编码'), 36 | sub_dimension_types: z.array(z.string()).describe('数据编码列表'), 37 | authorized_user_ids: z.array(z.string()).describe('授权人的lark id'), 38 | uams_app_id: z.string().describe('uams系统中应用id'), 39 | }), 40 | params: z 41 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 42 | .optional(), 43 | }, 44 | }; 45 | export const mdmV1Tools = [mdmV1UserAuthDataRelationBind, mdmV1UserAuthDataRelationUnbind]; 46 | -------------------------------------------------------------------------------- /src/mcp-tool/utils/handler.ts: -------------------------------------------------------------------------------- 1 | import * as lark from '@larksuiteoapi/node-sdk'; 2 | import { McpHandler, McpHandlerOptions } from '../types'; 3 | import { logger } from '../../utils/logger'; 4 | 5 | const sdkFuncCall = async (client: lark.Client, params: any, options: McpHandlerOptions) => { 6 | const { tool, userAccessToken } = options || {}; 7 | const { sdkName, path, httpMethod } = tool || {}; 8 | 9 | if (!sdkName) { 10 | logger.error(`[larkOapiHandler] Invalid sdkName`); 11 | throw new Error('Invalid sdkName'); 12 | } 13 | 14 | const chain = sdkName.split('.'); 15 | let func: any = client; 16 | for (const element of chain) { 17 | func = func[element as keyof typeof func]; 18 | if (!func) { 19 | func = async (params: any, ...args: any) => 20 | await client.request({ method: httpMethod, url: path, ...params }, ...args); 21 | break; 22 | } 23 | } 24 | if (!(func instanceof Function)) { 25 | func = async (params: any, ...args: any) => 26 | await client.request({ method: httpMethod, url: path, ...params }, ...args); 27 | } 28 | 29 | if (params?.useUAT) { 30 | if (!userAccessToken) { 31 | logger.error(`[larkOapiHandler] UserAccessToken is invalid or expired`); 32 | throw new Error('UserAccessToken is invalid or expired'); 33 | } 34 | return await func(params, lark.withUserAccessToken(userAccessToken)); 35 | } 36 | return await func(params); 37 | }; 38 | 39 | export const larkOapiHandler: McpHandler = async (client, params, options) => { 40 | try { 41 | const response = await sdkFuncCall(client, params, options); 42 | return { 43 | content: [ 44 | { 45 | type: 'text' as const, 46 | text: JSON.stringify(response?.data ?? response), 47 | }, 48 | ], 49 | }; 50 | } catch (error) { 51 | return { 52 | isError: true, 53 | content: [ 54 | { 55 | type: 'text' as const, 56 | text: JSON.stringify((error as any)?.response?.data || (error as any)?.message || error), 57 | }, 58 | ], 59 | }; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/translation_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type translationV1ToolName = 'translation.v1.text.detect' | 'translation.v1.text.translate'; 3 | export const translationV1TextDetect = { 4 | project: 'translation', 5 | name: 'translation.v1.text.detect', 6 | sdkName: 'translation.v1.text.detect', 7 | path: '/open-apis/translation/v1/text/detect', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-AI-Machine Translation-Text language recognition-Machine translation (MT), supporting recognition of over 100 languages. The return value is ISO 639-1 compliant', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ text: z.string().describe('Text whose language is to be recognized') }), 14 | }, 15 | }; 16 | export const translationV1TextTranslate = { 17 | project: 'translation', 18 | name: 'translation.v1.text.translate', 19 | sdkName: 'translation.v1.text.translate', 20 | path: '/open-apis/translation/v1/text/translate', 21 | httpMethod: 'POST', 22 | description: 23 | '[Feishu/Lark]-AI-Machine Translation-Translate text-The following languages are supported for translation: "Zh": Chinese ; "Zh-Hant": Traditional Chinese ; "En": English; " Ja ": Japanese ; " Ru ": Russian ; " de ": German ; " Fr ": French ; "It": Italian ; " pl ": Polish ; " Th ": Thai ; "Hi": Hindi ; "Id": Indonesian ; " es ": Spanish ; " Pt ": Portuguese ; " Ko ": Korean ; " vi ": Vietnamese', 24 | accessTokens: ['tenant'], 25 | schema: { 26 | data: z.object({ 27 | source_language: z.string().describe('Source language'), 28 | text: z.string().describe('Source text, character limit is 1,000'), 29 | target_language: z.string().describe('Target language'), 30 | glossary: z 31 | .array(z.object({ from: z.string().describe('Associated text'), to: z.string().describe('Translation') })) 32 | .describe('Request level glossary with at most 128 terms, valid only in this translation') 33 | .optional(), 34 | }), 35 | }, 36 | }; 37 | export const translationV1Tools = [translationV1TextDetect, translationV1TextTranslate]; 38 | -------------------------------------------------------------------------------- /tests/utils/http-instance.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { USER_AGENT } from '../../src/utils/constants'; 3 | import { oapiHttpInstance } from '../../src/utils/http-instance'; 4 | 5 | // 我们需要在导入被测模块前先模拟axios 6 | jest.mock('axios', () => { 7 | return { 8 | create: jest.fn(() => ({ 9 | interceptors: { 10 | request: { 11 | use: jest.fn((successFn) => { 12 | // 保存拦截器函数以便测试 13 | (axios as any).mockRequestInterceptor = successFn; 14 | }), 15 | }, 16 | response: { 17 | use: jest.fn((successFn) => { 18 | // 保存拦截器函数以便测试 19 | (axios as any).mockResponseInterceptor = successFn; 20 | }), 21 | }, 22 | }, 23 | })), 24 | }; 25 | }); 26 | 27 | describe('http-instance', () => { 28 | it('应该创建axios实例', () => { 29 | const { oapiHttpInstance } = require('../../src/utils/http-instance'); 30 | expect(axios.create).toHaveBeenCalled(); 31 | expect(oapiHttpInstance).toBeDefined(); 32 | }); 33 | 34 | it('应该正确设置请求拦截器', () => { 35 | // 测试请求拦截器逻辑 36 | const mockRequest = { headers: {} }; 37 | const interceptor = (axios as any).mockRequestInterceptor; 38 | 39 | expect(typeof interceptor).toBe('function'); 40 | const result = interceptor(mockRequest); 41 | 42 | expect(result).toBe(mockRequest); 43 | expect(result.headers['User-Agent']).toBe(USER_AGENT); 44 | }); 45 | 46 | it('当请求没有headers属性时应该正确处理', () => { 47 | // 测试请求拦截器处理没有headers的情况 48 | const mockRequest = {}; 49 | const interceptor = (axios as any).mockRequestInterceptor; 50 | 51 | const result = interceptor(mockRequest); 52 | 53 | expect(result).toBe(mockRequest); 54 | // 不应该添加任何headers 55 | expect(mockRequest).not.toHaveProperty('headers'); 56 | }); 57 | 58 | it('应该正确设置响应拦截器', () => { 59 | // 测试响应拦截器逻辑 60 | const mockResponse = { data: { key: 'value' } }; 61 | const interceptor = (axios as any).mockResponseInterceptor; 62 | 63 | expect(typeof interceptor).toBe('function'); 64 | const result = interceptor(mockResponse); 65 | 66 | expect(result).toBe(mockResponse.data); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/cli.test.ts: -------------------------------------------------------------------------------- 1 | import { LoginHandler } from '../src/cli/login-handler'; 2 | import * as server from '../src/mcp-server'; 3 | 4 | // 模拟 LoginHandler 5 | jest.mock('../src/cli/login-handler', () => ({ 6 | LoginHandler: { 7 | handleLogin: jest.fn(), 8 | handleLogout: jest.fn(), 9 | }, 10 | })); 11 | 12 | // 模拟server模块 13 | jest.mock('../src/mcp-server', () => ({ 14 | initMcpServerWithTransport: jest.fn(), 15 | })); 16 | 17 | describe('CLI Commands', () => { 18 | beforeEach(() => { 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | describe('Login Command', () => { 23 | it('应该调用 LoginHandler.handleLogin', async () => { 24 | const options = { 25 | appId: 'test-app-id', 26 | appSecret: 'test-app-secret', 27 | domain: 'https://open.feishu.cn', 28 | scope: 'test-scope', 29 | }; 30 | 31 | // 模拟login命令的action回调 32 | const loginActionCallback = jest.fn(async (options) => { 33 | await LoginHandler.handleLogin(options); 34 | }); 35 | 36 | await loginActionCallback(options); 37 | 38 | expect(LoginHandler.handleLogin).toHaveBeenCalledWith(options); 39 | }); 40 | }); 41 | 42 | describe('Logout Command', () => { 43 | it('应该调用 LoginHandler.handleLogout', async () => { 44 | // 模拟logout命令的action回调 45 | const logoutActionCallback = jest.fn(async () => { 46 | await LoginHandler.handleLogout(); 47 | }); 48 | 49 | await logoutActionCallback(); 50 | 51 | expect(LoginHandler.handleLogout).toHaveBeenCalledWith(); 52 | }); 53 | }); 54 | 55 | describe('MCP Command', () => { 56 | it('应该调用 initMcpServerWithTransport', async () => { 57 | const options = { 58 | appId: 'test-app-id', 59 | appSecret: 'test-app-secret', 60 | mode: 'stdio', 61 | }; 62 | 63 | // 模拟mcp命令的action回调 64 | const mcpActionCallback = jest.fn(async (options) => { 65 | await server.initMcpServerWithTransport('oapi', options); 66 | }); 67 | 68 | await mcpActionCallback(options); 69 | 70 | expect(server.initMcpServerWithTransport).toHaveBeenCalledWith('oapi', options); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/mcp-tool/types/index.ts: -------------------------------------------------------------------------------- 1 | import * as lark from '@larksuiteoapi/node-sdk'; 2 | import { ProjectName, ToolName } from '../tools'; 3 | import { CallToolResult } from '@modelcontextprotocol/sdk/types'; 4 | 5 | export type ToolNameCase = 'snake' | 'camel' | 'kebab' | 'dot'; 6 | 7 | export enum TokenMode { 8 | AUTO = 'auto', 9 | USER_ACCESS_TOKEN = 'user_access_token', 10 | TENANT_ACCESS_TOKEN = 'tenant_access_token', 11 | } 12 | 13 | export interface McpHandlerOptions { 14 | userAccessToken?: string; 15 | tool?: McpTool; 16 | } 17 | 18 | export type McpHandler = ( 19 | client: lark.Client, 20 | params: any, 21 | options: McpHandlerOptions, 22 | ) => Promise | CallToolResult; 23 | 24 | /** 25 | * MCP Tool 26 | */ 27 | export interface McpTool { 28 | // Project 29 | project: string; 30 | // Tool Name 31 | name: string; 32 | // Tool Description 33 | description: string; 34 | // Tool Parameters 35 | schema: any; 36 | // Node SDK Call Name 37 | sdkName?: string; 38 | // API Path 39 | path?: string; 40 | // API HTTP Method 41 | httpMethod?: string; 42 | // Access Token Type 43 | accessTokens?: string[]; 44 | // Whether to support file upload 45 | supportFileUpload?: boolean; 46 | // Whether to support file download 47 | supportFileDownload?: boolean; 48 | // Custom Handler 49 | customHandler?: McpHandler; 50 | } 51 | 52 | /** 53 | * Tools Filter Options 54 | */ 55 | export interface ToolsFilterOptions { 56 | // Language 57 | language?: 'zh' | 'en'; 58 | // Allowed Tools 59 | allowTools?: ToolName[]; 60 | // Allowed Projects 61 | allowProjects?: ProjectName[]; 62 | // Access Token Type 63 | tokenMode?: TokenMode; 64 | } 65 | 66 | export type LarkClientOptions = Partial[0]>; 67 | 68 | export interface LarkMcpToolOptions extends LarkClientOptions { 69 | client?: lark.Client; 70 | appId?: string; 71 | appSecret?: string; 72 | toolsOptions?: ToolsFilterOptions; 73 | tokenMode?: TokenMode; 74 | oauth?: boolean; 75 | } 76 | 77 | export interface SettableValue { 78 | value?: string; 79 | getter?: () => Promise; 80 | setter?: (value?: string) => Promise; 81 | } 82 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/application_v5.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type applicationV5ToolName = 'application.v5.application.favourite' | 'application.v5.application.recommend'; 3 | export const applicationV5ApplicationFavourite = { 4 | project: 'application', 5 | name: 'application.v5.application.favourite', 6 | sdkName: 'application.v5.application.favourite', 7 | path: '/open-apis/application/v5/applications/favourite', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-Workplace-My favorite-获取用户常用的应用', 10 | accessTokens: ['user'], 11 | schema: { 12 | params: z 13 | .object({ 14 | language: z 15 | .enum(['zh_cn', 'en_us', 'ja_jp']) 16 | .describe('Options:zh_cn(Chinese 中文),en_us(English 英文),ja_jp(Japanese 日文)') 17 | .optional(), 18 | page_token: z.string().optional(), 19 | page_size: z.number().optional(), 20 | }) 21 | .optional(), 22 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 23 | }, 24 | }; 25 | export const applicationV5ApplicationRecommend = { 26 | project: 'application', 27 | name: 'application.v5.application.recommend', 28 | sdkName: 'application.v5.application.recommend', 29 | path: '/open-apis/application/v5/applications/recommend', 30 | httpMethod: 'GET', 31 | description: '[Feishu/Lark]-Workplace-My favorite-获取企业推荐的应用', 32 | accessTokens: ['user'], 33 | schema: { 34 | params: z 35 | .object({ 36 | language: z 37 | .enum(['zh_cn', 'en_us', 'ja_jp']) 38 | .describe('Options:zh_cn(Chinese 中文),en_us(English 英文),ja_jp(Japanese 日文)') 39 | .optional(), 40 | recommend_type: z 41 | .enum(['user_unremovable', 'user_removable']) 42 | .describe( 43 | 'Options:user_unremovable(UserUnremovable 用户不可移除的推荐应用列表),user_removable(UserRemovable 用户可移除的推荐应用列表)', 44 | ) 45 | .optional(), 46 | page_token: z.string().optional(), 47 | page_size: z.number().optional(), 48 | }) 49 | .optional(), 50 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 51 | }, 52 | }; 53 | export const applicationV5Tools = [applicationV5ApplicationFavourite, applicationV5ApplicationRecommend]; 54 | -------------------------------------------------------------------------------- /docs/usage/docker/docker-zh.md: -------------------------------------------------------------------------------- 1 | # 在 Docker 中使用 lark-mcp (Alpha) 2 | 3 | 本文档将介绍如何构建并运行官方 lark-mcp Docker 镜像,内置 keytar 安全存储,开箱即用。 4 | 5 | [English Version](./docker.md) 6 | 7 | ## 前置条件 8 | - 已安装 Docker Desktop(或任意 Docker 运行时) 9 | - 拥有你的飞书/Lark 应用 App ID 和 App Secret 10 | 11 | ## 1)构建镜像 12 | ```bash 13 | # 在项目根目录执行 14 | docker build -t lark-mcp:latest . 15 | ``` 16 | 17 | ## 2)快速检查 18 | - 查看帮助 19 | ```bash 20 | docker run --rm -it lark-mcp:latest --help 21 | ``` 22 | - 查看登录状态(会初始化存储) 23 | ```bash 24 | docker run --rm -it lark-mcp:latest whoami 25 | ``` 26 | 27 | 28 | ## 3)MCP 客户端配置 29 | 30 | 启动Docker容器后,还需要在MCP客户端中进行相应配置才能使用。 31 | 32 | ### stdio 模式配置 33 | 34 | 如果使用默认的stdio模式,在MCP客户端(如Cursor)配置文件中添加: 35 | 36 | ```json 37 | { 38 | "mcpServers": { 39 | "lark-mcp": { 40 | "command": "docker", 41 | "args": [ 42 | "run", "--rm", "-i", 43 | "-v", "lark_mcp_data:/home/node/.local/share", 44 | "lark-mcp:latest", "mcp", 45 | "-a", "your_app_id", 46 | "-s", "your_app_secret" 47 | ] 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ### streamable 模式配置 54 | 55 | 如果使用streamable(HTTP)模式,需要先启动容器: 56 | 57 | ```bash 58 | docker run --rm -it \ 59 | -p 3000:3000 \ 60 | -v lark_mcp_data:/home/node/.local/share \ 61 | lark-mcp:latest mcp -a -s -m streamable --host 0.0.0.0 -p 3000 62 | ``` 63 | 64 | 然后在MCP客户端配置文件中添加: 65 | 66 | ```json 67 | { 68 | "mcpServers": { 69 | "lark-mcp": { 70 | "url": "http://localhost:3000/mcp" 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ### 用户身份配置(user_access_token) 77 | 78 | > ⚠️ **重要提示**:在Docker环境中,暂不支持 OAuth,请手动传入 user_access_token 进行用户身份验证。 79 | 80 | ```json 81 | { 82 | "mcpServers": { 83 | "lark-mcp": { 84 | "command": "docker", 85 | "args": [ 86 | "run", "--rm", "-i", 87 | "-v", "lark_mcp_data:/home/node/.local/share", 88 | "lark-mcp:latest", "mcp", 89 | "-a", "your_app_id", 90 | "-s", "your_app_secret", 91 | "-u", "your_user_access_token", 92 | "--token-mode", "user_access_token" 93 | ] 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ## 提示 100 | - 无需输入密码:容器内已自动初始化 secrets 服务,keytar 无需交互即可安全存储令牌。 101 | - 持久化令牌:建议始终挂载 `lark_mcp_data` 数据卷。 102 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/mdm_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type mdmV1ToolName = 'mdm.v1.userAuthDataRelation.bind' | 'mdm.v1.userAuthDataRelation.unbind'; 3 | export const mdmV1UserAuthDataRelationBind = { 4 | project: 'mdm', 5 | name: 'mdm.v1.userAuthDataRelation.bind', 6 | sdkName: 'mdm.v1.userAuthDataRelation.bind', 7 | path: '/open-apis/mdm/v1/user_auth_data_relations/bind', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-Feishu Master Data Management-Data dimension-User data dimension binding-Through this interface, a type of data dimension can be bound to users under a specified application, and batch authorization can be granted to multiple users at the same time', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ 14 | root_dimension_type: z.string().describe('data type encoding'), 15 | sub_dimension_types: z.array(z.string()).describe('data encoding list'), 16 | authorized_user_ids: z.array(z.string()).describe("authorizer's lark id"), 17 | uams_app_id: z.string().describe('application id in uams system'), 18 | }), 19 | params: z 20 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 21 | .optional(), 22 | }, 23 | }; 24 | export const mdmV1UserAuthDataRelationUnbind = { 25 | project: 'mdm', 26 | name: 'mdm.v1.userAuthDataRelation.unbind', 27 | sdkName: 'mdm.v1.userAuthDataRelation.unbind', 28 | path: '/open-apis/mdm/v1/user_auth_data_relations/unbind', 29 | httpMethod: 'POST', 30 | description: 31 | '[Feishu/Lark]-Feishu Master Data Management-Data dimension-User data dimension unbinding-Through this interface, a type of data dimension can be released for a specified user under a specified application', 32 | accessTokens: ['tenant'], 33 | schema: { 34 | data: z.object({ 35 | root_dimension_type: z.string().describe('data type encoding'), 36 | sub_dimension_types: z.array(z.string()).describe('data encoding list'), 37 | authorized_user_ids: z.array(z.string()).describe("authorizer's lark id"), 38 | uams_app_id: z.string().describe('application id in uams system'), 39 | }), 40 | params: z 41 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 42 | .optional(), 43 | }, 44 | }; 45 | export const mdmV1Tools = [mdmV1UserAuthDataRelationBind, mdmV1UserAuthDataRelationUnbind]; 46 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/builtin-tools/im/buildin.ts: -------------------------------------------------------------------------------- 1 | import { McpTool } from '../../../../types'; 2 | import { z } from 'zod'; 3 | 4 | // 工具名称类型 5 | export type imBuiltinToolName = 'im.builtin.batchSend'; 6 | 7 | export const larkImBuiltinBatchSendTool: McpTool = { 8 | project: 'im', 9 | name: 'im.builtin.batchSend', 10 | accessTokens: ['tenant'], 11 | description: '[飞书/Lark] - 批量发送消息 - 支持给多个用户、部门批量发送消息,支持文本和卡片', 12 | schema: { 13 | data: z.object({ 14 | msg_type: z 15 | .enum(['text', 'post', 'image', 'interactive', 'share_chat']) 16 | .describe( 17 | '消息类型,如果 msg_type 取值为 text、image、post 或者 share_chat,则消息内容需要传入 content 参数内。如果 msg_type 取值为 interactive,则消息内容需要传入 card 参数内。富文本类型(post)的消息,不支持使用 md 标签。', 18 | ), 19 | content: z 20 | .any() 21 | .describe( 22 | '消息内容,JSON 结构。该参数的取值与 msg_type 对应,例如 msg_type 取值为 text,则该参数需要传入文本类型的内容。', 23 | ) 24 | .optional(), 25 | card: z 26 | .any() 27 | .describe( 28 | '卡片内容,JSON 结构。该参数的取值与 msg_type 对应,仅当 msg_type 取值为 interactive 时,需要将卡片内容传入当前参数。当 msg_type 取值不为 interactive 时,消息内容需要传入到 content 参数。', 29 | ) 30 | .optional(), 31 | open_ids: z.array(z.string()).describe('接收者open_id列表').optional(), 32 | user_ids: z.array(z.string()).describe('接收者user_id列表').optional(), 33 | union_ids: z.array(z.string()).describe('接收者union_id列表').optional(), 34 | department_ids: z 35 | .array(z.string()) 36 | .describe('部门 ID 列表。列表内支持传入部门 department_id 和 open_department_id') 37 | .optional(), 38 | }), 39 | }, 40 | customHandler: async (client, params): Promise => { 41 | try { 42 | const { data } = params; 43 | const response = await client.request({ 44 | method: 'POST', 45 | url: '/open-apis/message/v4/batch_send', 46 | data, 47 | }); 48 | return { 49 | content: [ 50 | { 51 | type: 'text' as const, 52 | text: JSON.stringify(response.data ?? response), 53 | }, 54 | ], 55 | }; 56 | } catch (error) { 57 | return { 58 | isError: true, 59 | content: [ 60 | { 61 | type: 'text' as const, 62 | text: JSON.stringify((error as any)?.response?.data || error), 63 | }, 64 | ], 65 | }; 66 | } 67 | }, 68 | }; 69 | 70 | export const imBuiltinTools = [larkImBuiltinBatchSendTool]; 71 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/passport_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type passportV1ToolName = 'passport.v1.session.logout' | 'passport.v1.session.query'; 3 | export const passportV1SessionLogout = { 4 | project: 'passport', 5 | name: 'passport.v1.session.logout', 6 | sdkName: 'passport.v1.session.logout', 7 | path: '/open-apis/passport/v1/sessions/logout', 8 | httpMethod: 'POST', 9 | description: '[Feishu/Lark]-认证及授权-登录态管理-退出登录-该接口用于退出用户的登录态', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | data: z.object({ 13 | idp_credential_id: z.string().describe('idp 侧的唯一标识,logout_type = 2 时必填').optional(), 14 | logout_type: z 15 | .number() 16 | .describe( 17 | '登出的方式 Options:1(UserID UserID,使用开放平台的维度登出),2(IdpCredentialID IdpCredentialID,使用 idp 侧的唯一标识登出),3(SessionUUID Session 标识符,基于session uuid 登出)', 18 | ), 19 | terminal_type: z 20 | .array(z.number()) 21 | .describe( 22 | '登出的客户端类型,默认全部登出。可选值:- 1:PC 端- 2:Web 端- 3:Android 端- 4:iOS 端- 5:服务端- 6:旧版小程序端- 8:其他移动端', 23 | ) 24 | .optional(), 25 | user_id: z 26 | .string() 27 | .describe('开放平台的数据标识,用户 ID 类型与查询参数 user_id_type 一致,logout_type = 1 时必填') 28 | .optional(), 29 | logout_reason: z 30 | .number() 31 | .describe( 32 | '登出提示语,非必填,不传时默认提示:你已在其他客户端上退出了当前设备,请重新登录。可选值:- 34:您已修改登录密码,请重新登录- 35:您的登录态已失效,请重新登录- 36:您的密码已过期,请在登录页面通过忘记密码功能修改密码后重新登录', 33 | ) 34 | .optional(), 35 | sid: z.string().describe('需要精确登出的 session 标识符,logout_type = 3 时必填').optional(), 36 | }), 37 | params: z 38 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 39 | .optional(), 40 | }, 41 | }; 42 | export const passportV1SessionQuery = { 43 | project: 'passport', 44 | name: 'passport.v1.session.query', 45 | sdkName: 'passport.v1.session.query', 46 | path: '/open-apis/passport/v1/sessions/query', 47 | httpMethod: 'POST', 48 | description: '[Feishu/Lark]-认证及授权-登录态管理-批量获取脱敏的用户登录信息-该接口用于查询用户的登录信息', 49 | accessTokens: ['tenant'], 50 | schema: { 51 | data: z.object({ user_ids: z.array(z.string()).describe('用户 ID').optional() }).optional(), 52 | params: z 53 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 54 | .optional(), 55 | }, 56 | }; 57 | export const passportV1Tools = [passportV1SessionLogout, passportV1SessionQuery]; 58 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/ehr_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type ehrV1ToolName = 'ehr.v1.employee.list'; 3 | export const ehrV1EmployeeList = { 4 | project: 'ehr', 5 | name: 'ehr.v1.employee.list', 6 | sdkName: 'ehr.v1.employee.list', 7 | path: '/open-apis/ehr/v1/employees', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-飞书人事(标准版)-批量获取员工花名册信息-根据员工飞书用户 ID / 员工状态 / 雇员类型等搜索条件 ,批量获取员工花名册字段信息。字段包括「系统标准字段 / system_fields」和「自定义字段 / custom_fields」', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z 14 | .object({ 15 | view: z 16 | .enum(['basic', 'full']) 17 | .describe( 18 | '返回数据类型,不传值默认为 basic。 Options:basic(概览,只返回 id、name 等基本信息),full(明细,返回系统标准字段和自定义字段集合)', 19 | ) 20 | .optional(), 21 | status: z 22 | .array( 23 | z 24 | .number() 25 | .describe( 26 | 'Options:1(to_be_onboarded 待入职),2(active 在职),3(onboarding_cancelled 已取消入职),4(offboarding 待离职),5(offboarded 已离职)', 27 | ), 28 | ) 29 | .describe('员工状态,不传代表查询所有员工状态实际在职 = 2&4可同时查询多个状态的记录,如 status=2&status=4') 30 | .optional(), 31 | type: z 32 | .array( 33 | z 34 | .number() 35 | .describe( 36 | 'Options:1(regular 全职),2(intern 实习),3(consultant 顾问),4(outsourcing 外包),5(contractor 劳务)', 37 | ), 38 | ) 39 | .describe( 40 | '人员类型,不传代表查询所有人员类型同时可使用自定义员工类型的 int 值进行查询,可通过下方接口获取到该租户的自定义员工类型的名称,参见 [获取人员类型]', 41 | ) 42 | .optional(), 43 | start_time: z.string().describe('查询开始时间(入职时间 >= 此时间)').optional(), 44 | end_time: z.string().describe('查询结束时间(入职时间 <= 此时间)').optional(), 45 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional(), 46 | user_ids: z 47 | .array(z.string()) 48 | .describe( 49 | 'user_id、open_id 或 union_id,默认为 open_id。如果传入的值不是 open_id,需要一并传入 user_id_type 参数。可一次查询多个 id 的用户,例如:user_ids=ou_8ebd4f35d7101ffdeb4771d7c8ec517e&user_ids=ou_7abc4f35d7101ffdeb4771dabcde[用户相关的 ID 概念]', 50 | ) 51 | .optional(), 52 | page_token: z 53 | .string() 54 | .describe( 55 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 56 | ) 57 | .optional(), 58 | page_size: z.number().describe('分页大小,取值范围 1~100,默认 10').optional(), 59 | }) 60 | .optional(), 61 | }, 62 | }; 63 | export const ehrV1Tools = [ehrV1EmployeeList]; 64 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/application_v5.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type applicationV5ToolName = 'application.v5.application.favourite' | 'application.v5.application.recommend'; 3 | export const applicationV5ApplicationFavourite = { 4 | project: 'application', 5 | name: 'application.v5.application.favourite', 6 | sdkName: 'application.v5.application.favourite', 7 | path: '/open-apis/application/v5/applications/favourite', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-工作台-我的常用-获取用户自定义常用的应用', 10 | accessTokens: ['user'], 11 | schema: { 12 | params: z 13 | .object({ 14 | language: z 15 | .enum(['zh_cn', 'en_us', 'ja_jp']) 16 | .describe('应用信息的语言版本 Options:zh_cn(Chinese 中文),en_us(English 英文),ja_jp(Japanese 日文)') 17 | .optional(), 18 | page_token: z 19 | .string() 20 | .describe( 21 | '分页标记,不填表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 22 | ) 23 | .optional(), 24 | page_size: z.number().describe('单页需求最大个数(最大 100),不传默认10个').optional(), 25 | }) 26 | .optional(), 27 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 28 | }, 29 | }; 30 | export const applicationV5ApplicationRecommend = { 31 | project: 'application', 32 | name: 'application.v5.application.recommend', 33 | sdkName: 'application.v5.application.recommend', 34 | path: '/open-apis/application/v5/applications/recommend', 35 | httpMethod: 'GET', 36 | description: '[Feishu/Lark]-工作台-我的常用-获取管理员推荐的应用', 37 | accessTokens: ['user'], 38 | schema: { 39 | params: z 40 | .object({ 41 | language: z 42 | .enum(['zh_cn', 'en_us', 'ja_jp']) 43 | .describe('应用信息的语言版本 Options:zh_cn(Chinese 中文),en_us(English 英文),ja_jp(Japanese 日文)') 44 | .optional(), 45 | recommend_type: z 46 | .enum(['user_unremovable', 'user_removable']) 47 | .describe( 48 | '推荐应用类型,默认为用户不可移除的推荐应用列表 Options:user_unremovable(UserUnremovable 用户不可移除的推荐应用列表),user_removable(UserRemovable 用户可移除的推荐应用列表)', 49 | ) 50 | .optional(), 51 | page_token: z 52 | .string() 53 | .describe( 54 | '分页标记,不填表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 55 | ) 56 | .optional(), 57 | page_size: z.number().describe('单页需求最大个数(最大 100),不传默认10个').optional(), 58 | }) 59 | .optional(), 60 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 61 | }, 62 | }; 63 | export const applicationV5Tools = [applicationV5ApplicationFavourite, applicationV5ApplicationRecommend]; 64 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/speech_to_text_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type speechToTextV1ToolName = 3 | | 'speech_to_text.v1.speech.fileRecognize' 4 | | 'speech_to_text.v1.speech.streamRecognize'; 5 | export const speechToTextV1SpeechFileRecognize = { 6 | project: 'speech_to_text', 7 | name: 'speech_to_text.v1.speech.fileRecognize', 8 | sdkName: 'speech_to_text.v1.speech.fileRecognize', 9 | path: '/open-apis/speech_to_text/v1/speech/file_recognize', 10 | httpMethod: 'POST', 11 | description: 12 | '[Feishu/Lark]-AI 能力-语音识别-识别语音文件-语音文件识别接口,上传整段语音文件进行一次性识别。接口适合 60 秒以内音频识别', 13 | accessTokens: ['tenant'], 14 | schema: { 15 | data: z.object({ 16 | speech: z 17 | .object({ 18 | speech: z 19 | .string() 20 | .describe('pcm格式音频文件(文件识别)或音频分片(流式识别)经base64编码后的内容') 21 | .optional(), 22 | }) 23 | .describe('语音资源'), 24 | config: z 25 | .object({ 26 | file_id: z.string().describe('仅包含字母数字和下划线的 16 位字符串作为文件的标识,用户生成'), 27 | format: z.string().describe('语音格式,目前仅支持:pcm'), 28 | engine_type: z.string().describe('引擎类型,目前仅支持:16k_auto 中英混合'), 29 | }) 30 | .describe('配置属性'), 31 | }), 32 | }, 33 | }; 34 | export const speechToTextV1SpeechStreamRecognize = { 35 | project: 'speech_to_text', 36 | name: 'speech_to_text.v1.speech.streamRecognize', 37 | sdkName: 'speech_to_text.v1.speech.streamRecognize', 38 | path: '/open-apis/speech_to_text/v1/speech/stream_recognize', 39 | httpMethod: 'POST', 40 | description: 41 | '[Feishu/Lark]-AI 能力-语音识别-识别流式语音-语音流式接口,将整个音频文件分片进行传入模型。能够实时返回数据。建议每个音频分片的大小为 100-200ms', 42 | accessTokens: ['tenant'], 43 | schema: { 44 | data: z.object({ 45 | speech: z 46 | .object({ 47 | speech: z 48 | .string() 49 | .describe('pcm格式音频文件(文件识别)或音频分片(流式识别)经base64编码后的内容') 50 | .optional(), 51 | }) 52 | .describe('语音资源'), 53 | config: z 54 | .object({ 55 | stream_id: z.string().describe('仅包含字母数字和下划线的 16 位字符串作为同一数据流的标识,用户生成'), 56 | sequence_id: z.number().describe('数据流分片的序号,序号从 0 开始,每次请求递增 1'), 57 | action: z 58 | .number() 59 | .describe( 60 | '数据流标记:1 首包,2 正常结束,等待结果返回,3 中断数据流不返回最终结果,0 传输语音中间的数据包', 61 | ), 62 | format: z.string().describe('语音格式,目前仅支持:pcm'), 63 | engine_type: z.string().describe('引擎类型,目前仅支持:16k_auto 中英混合'), 64 | }) 65 | .describe('配置属性'), 66 | }), 67 | }, 68 | }; 69 | export const speechToTextV1Tools = [speechToTextV1SpeechFileRecognize, speechToTextV1SpeechStreamRecognize]; 70 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/hire_v2.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type hireV2ToolName = 'hire.v2.interviewRecord.get' | 'hire.v2.interviewRecord.list' | 'hire.v2.talent.get'; 3 | export const hireV2InterviewRecordGet = { 4 | project: 'hire', 5 | name: 'hire.v2.interviewRecord.get', 6 | sdkName: 'hire.v2.interviewRecord.get', 7 | path: '/open-apis/hire/v2/interview_records/:interview_record_id', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-招聘-候选人管理-投递流程-面试-获取面试评价详细信息(新版)-获取面试评价详细信息,如面试结论、面试得分和面试官等信息', 11 | accessTokens: ['tenant', 'user'], 12 | schema: { 13 | params: z 14 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 15 | .optional(), 16 | path: z.object({ interview_record_id: z.string().describe('面试评价 ID,可通过[获取面试信息]接口获取') }), 17 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 18 | }, 19 | }; 20 | export const hireV2InterviewRecordList = { 21 | project: 'hire', 22 | name: 'hire.v2.interviewRecord.list', 23 | sdkName: 'hire.v2.interviewRecord.list', 24 | path: '/open-apis/hire/v2/interview_records', 25 | httpMethod: 'GET', 26 | description: 27 | '[Feishu/Lark]-招聘-候选人管理-投递流程-面试-批量获取面试评价详细信息(新版)-批量获取面试评价详细信息,如面试结论、面试得分和面试官等信息', 28 | accessTokens: ['tenant', 'user'], 29 | schema: { 30 | params: z 31 | .object({ 32 | ids: z 33 | .array(z.string()) 34 | .describe('面试评价 ID 列表,可通过[获取面试信息]接口获取,使用该筛选项时不会分页') 35 | .optional(), 36 | page_size: z.number().describe('分页大小**注意**:若不传该参数,则默认根据 `ids` 参数获取数据').optional(), 37 | page_token: z 38 | .string() 39 | .describe( 40 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 41 | ) 42 | .optional(), 43 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional(), 44 | }) 45 | .optional(), 46 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 47 | }, 48 | }; 49 | export const hireV2TalentGet = { 50 | project: 'hire', 51 | name: 'hire.v2.talent.get', 52 | sdkName: 'hire.v2.talent.get', 53 | path: '/open-apis/hire/v2/talents/:talent_id', 54 | httpMethod: 'GET', 55 | description: 56 | '[Feishu/Lark]-招聘-候选人管理-人才-获取人才详情-根据人才 ID 获取人才详情,包含人才加入文件夹列表、标签、人才库、备注以及屏蔽名单等信息', 57 | accessTokens: ['tenant'], 58 | schema: { 59 | params: z 60 | .object({ 61 | user_id_type: z.enum(['open_id', 'union_id', 'user_id', 'people_admin_id']).describe('用户ID类型').optional(), 62 | }) 63 | .optional(), 64 | path: z.object({ talent_id: z.string().describe('人才 ID,可通过[获取人才列表]接口获取') }), 65 | }, 66 | }; 67 | export const hireV2Tools = [hireV2InterviewRecordGet, hireV2InterviewRecordList, hireV2TalentGet]; 68 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/minutes_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type minutesV1ToolName = 3 | | 'minutes.v1.minute.get' 4 | | 'minutes.v1.minuteMedia.get' 5 | | 'minutes.v1.minuteStatistics.get'; 6 | export const minutesV1MinuteGet = { 7 | project: 'minutes', 8 | name: 'minutes.v1.minute.get', 9 | sdkName: 'minutes.v1.minute.get', 10 | path: '/open-apis/minutes/v1/minutes/:minute_token', 11 | httpMethod: 'GET', 12 | description: 13 | '[Feishu/Lark]-妙记-妙记信息-获取妙记信息-通过这个接口,可以得到一篇妙记的基础概述信息,包含 `owner_id`、`create_time`、标题、封面、时长和 URL', 14 | accessTokens: ['tenant', 'user'], 15 | schema: { 16 | params: z 17 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 18 | .optional(), 19 | path: z.object({ 20 | minute_token: z 21 | .string() 22 | .describe( 23 | '妙记唯一标识。可从妙记的 URL 链接中获取,一般为最后一串字符:https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 24 | ), 25 | }), 26 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 27 | }, 28 | }; 29 | export const minutesV1MinuteMediaGet = { 30 | project: 'minutes', 31 | name: 'minutes.v1.minuteMedia.get', 32 | sdkName: 'minutes.v1.minuteMedia.get', 33 | path: '/open-apis/minutes/v1/minutes/:minute_token/media', 34 | httpMethod: 'GET', 35 | description: '[Feishu/Lark]-妙记-妙记音视频文件-下载妙记音视频文件-获取妙记的音视频文件', 36 | accessTokens: ['tenant', 'user'], 37 | schema: { 38 | path: z.object({ 39 | minute_token: z 40 | .string() 41 | .describe( 42 | '妙记唯一标识。可从妙记的 URL 链接中获取,一般为最后一串字符:https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 43 | ), 44 | }), 45 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 46 | }, 47 | }; 48 | export const minutesV1MinuteStatisticsGet = { 49 | project: 'minutes', 50 | name: 'minutes.v1.minuteStatistics.get', 51 | sdkName: 'minutes.v1.minuteStatistics.get', 52 | path: '/open-apis/minutes/v1/minutes/:minute_token/statistics', 53 | httpMethod: 'GET', 54 | description: 55 | '[Feishu/Lark]-妙记-妙记统计数据-获取妙记统计数据-通过这个接口,可以获得妙记的访问情况统计,包含PV、UV、访问过的 user id、访问过的 user timestamp', 56 | accessTokens: ['tenant', 'user'], 57 | schema: { 58 | params: z 59 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 60 | .optional(), 61 | path: z.object({ 62 | minute_token: z 63 | .string() 64 | .describe( 65 | '妙记唯一标识。可从妙记的 URL 链接中获取,一般为最后一串字符:https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 66 | ), 67 | }), 68 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 69 | }, 70 | }; 71 | export const minutesV1Tools = [minutesV1MinuteGet, minutesV1MinuteMediaGet, minutesV1MinuteStatisticsGet]; 72 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/report_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type reportV1ToolName = 'report.v1.rule.query' | 'report.v1.ruleView.remove' | 'report.v1.task.query'; 3 | export const reportV1RuleQuery = { 4 | project: 'report', 5 | name: 'report.v1.rule.query', 6 | sdkName: 'report.v1.rule.query', 7 | path: '/open-apis/report/v1/rules/query', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-汇报-规则-查询规则-查询规则', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | params: z.object({ 13 | rule_name: z.string().describe('规则名称'), 14 | include_deleted: z 15 | .number() 16 | .describe('是否包括已删除,默认未删除 Options:0(Exclude 不包括已删除),1(Include 包括已删除)') 17 | .optional(), 18 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional(), 19 | }), 20 | }, 21 | }; 22 | export const reportV1RuleViewRemove = { 23 | project: 'report', 24 | name: 'report.v1.ruleView.remove', 25 | sdkName: 'report.v1.ruleView.remove', 26 | path: '/open-apis/report/v1/rules/:rule_id/views/remove', 27 | httpMethod: 'POST', 28 | description: '[Feishu/Lark]-汇报-规则看板-移除规则看板-移除规则看板', 29 | accessTokens: ['tenant'], 30 | schema: { 31 | data: z 32 | .object({ 33 | user_ids: z 34 | .array(z.string()) 35 | .describe('列表为空删除规则下全用户视图,列表不为空删除指定用户视图,大小限制200') 36 | .optional(), 37 | }) 38 | .optional(), 39 | params: z 40 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 41 | .optional(), 42 | path: z.object({ rule_id: z.string().describe('汇报规则ID') }), 43 | }, 44 | }; 45 | export const reportV1TaskQuery = { 46 | project: 'report', 47 | name: 'report.v1.task.query', 48 | sdkName: 'report.v1.task.query', 49 | path: '/open-apis/report/v1/tasks/query', 50 | httpMethod: 'POST', 51 | description: '[Feishu/Lark]-汇报-任务-查询任务-查询任务', 52 | accessTokens: ['tenant', 'user'], 53 | schema: { 54 | data: z.object({ 55 | commit_start_time: z.number().describe('提交开始时间时间戳'), 56 | commit_end_time: z.number().describe('提交结束时间时间戳'), 57 | rule_id: z.string().describe('汇报规则ID').optional(), 58 | user_id: z.string().describe('用户ID').optional(), 59 | page_token: z 60 | .string() 61 | .describe( 62 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 63 | ), 64 | page_size: z.number().describe('单次分页返回的条数'), 65 | }), 66 | params: z 67 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('用户ID类型').optional() }) 68 | .optional(), 69 | useUAT: z.boolean().describe('使用用户身份请求, 否则使用应用身份').optional(), 70 | }, 71 | }; 72 | export const reportV1Tools = [reportV1RuleQuery, reportV1RuleViewRemove, reportV1TaskQuery]; 73 | -------------------------------------------------------------------------------- /docs/troubleshooting/faq.md: -------------------------------------------------------------------------------- 1 | ## Frequently Asked Questions (FAQ) 2 | 3 | Below are common issues and solutions, with additional explanations to help identify causes and process quickly. 4 | 5 | ### Unable to connect to Feishu/Lark API 6 | 7 | Solutions: 8 | - Check local network connection and proxy settings. 9 | - Verify that `APP_ID` and `APP_SECRET` are filled in correctly. 10 | - Test if you can access the open platform API domain normally (such as `https://open.feishu.cn` or `https://open.larksuite.com`). 11 | 12 | ### Error when using user_access_token 13 | 14 | Solutions: 15 | - Check if the token has expired (usually valid for 2 hours). 16 | - It's recommended to use `login` to obtain and save user tokens first. 17 | - If using in server/CI environment, ensure secure token management and proper refresh. 18 | 19 | ### Permission denied when calling certain APIs after starting MCP service 20 | 21 | Solutions: 22 | - Enable corresponding API permissions for the application in the developer console and wait for approval. 23 | - For scenarios calling as a user (requiring `user_access_token`), ensure the authorization scope (`scope`) includes the corresponding permissions. If not, you need to login again. 24 | 25 | ### Image or file upload/download related API failures 26 | 27 | Solutions: 28 | - The current version does not support file/image upload and download yet. These capabilities will be supported in future versions. 29 | 30 | ### Garbled characters in Windows terminal 31 | 32 | Solutions: 33 | - Execute `chcp 65001` in Command Prompt to switch to UTF-8. 34 | - PowerShell users can adjust fonts or related terminal settings to improve compatibility. 35 | 36 | ### Permission errors during installation 37 | 38 | Solutions: 39 | - macOS/Linux: Use `sudo npm install -g @larksuiteoapi/lark-mcp` or adjust npm global path permissions. 40 | - Windows: Try running Command Prompt as administrator. 41 | 42 | ### Token limit exceeded prompt after starting MCP service 43 | 44 | Solutions: 45 | - Use `-t` (or `-t` in MCP configuration `args`) to reduce the number of enabled APIs. 46 | - Use models that support larger context lengths. 47 | 48 | ### Unable to connect or receive messages in SSE/Streamable mode 49 | 50 | Solutions: 51 | - Check port usage and change ports if necessary. 52 | - Ensure the client connects correctly to the corresponding endpoint and can handle event streams. 53 | 54 | ### Linux environment startup error [StorageManager] Failed to initialize: xxx 55 | 56 | Note: 57 | - Does not affect scenarios of "manually passing `user_access_token`" or "not using `user_access_token`". 58 | 59 | `StorageManager` uses keytar for encrypted storage of `user_access_token`. 60 | 61 | Solutions: 62 | - Install `libsecret` dependency: 63 | - Debian/Ubuntu: `sudo apt-get install libsecret-1-dev` 64 | - Red Hat-based: `sudo yum install libsecret-devel` 65 | - Arch Linux: `sudo pacman -S libsecret` 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/builtin-tools/im/buildin.ts: -------------------------------------------------------------------------------- 1 | import { McpTool } from '../../../../types'; 2 | import { z } from 'zod'; 3 | 4 | export type imBuiltinToolName = 'im.builtin.batchSend'; 5 | 6 | export const larkImBuiltinBatchSendTool: McpTool = { 7 | project: 'im', 8 | name: 'im.builtin.batchSend', 9 | accessTokens: ['tenant'], 10 | description: 11 | '[Feishu/Lark] - Batch send messages - Supports batch sending messages to multiple users and departments, supports text and card', 12 | schema: { 13 | data: z.object({ 14 | msg_type: z 15 | .enum(['text', 'post', 'image', 'interactive', 'share_chat']) 16 | .describe( 17 | 'Message type. If msg_type is text, image, post, or share_chat, the message content should be passed in the content parameter. If msg_type is interactive, the message content should be passed in the card parameter. Rich text type (post) messages do not support md tags.', 18 | ), 19 | content: z 20 | .any() 21 | .describe( 22 | 'Message content, JSON structure. The value of this parameter corresponds to msg_type. For example, if msg_type is text, this parameter should be the text content.', 23 | ) 24 | .optional(), 25 | card: z 26 | .any() 27 | .describe( 28 | 'Card content, JSON structure. The value of this parameter corresponds to msg_type. Only when msg_type is interactive, the card content should be passed in this parameter. When msg_type is not interactive, the message content should be passed in the content parameter.', 29 | ) 30 | .optional(), 31 | open_ids: z.array(z.string()).describe('List of recipient open_ids').optional(), 32 | user_ids: z.array(z.string()).describe('List of recipient user_ids').optional(), 33 | union_ids: z.array(z.string()).describe('List of recipient union_ids').optional(), 34 | department_ids: z 35 | .array(z.string()) 36 | .describe('List of department IDs. The list supports both department_id and open_department_id') 37 | .optional(), 38 | }), 39 | }, 40 | customHandler: async (client, params): Promise => { 41 | try { 42 | const { data } = params; 43 | const response = await client.request({ 44 | method: 'POST', 45 | url: '/open-apis/message/v4/batch_send', 46 | data, 47 | }); 48 | return { 49 | content: [ 50 | { 51 | type: 'text' as const, 52 | text: JSON.stringify(response.data ?? response), 53 | }, 54 | ], 55 | }; 56 | } catch (error) { 57 | return { 58 | isError: true, 59 | content: [ 60 | { 61 | type: 'text' as const, 62 | text: JSON.stringify((error as any)?.response?.data || error), 63 | }, 64 | ], 65 | }; 66 | } 67 | }, 68 | }; 69 | 70 | export const imBuiltinTools = [larkImBuiltinBatchSendTool]; 71 | -------------------------------------------------------------------------------- /docs/usage/docker/docker.md: -------------------------------------------------------------------------------- 1 | # Use lark-mcp in Docker (Alpha) 2 | 3 | This document describes how to build and run the official lark-mcp Docker image with built-in keytar secure storage, ready to use out of the box. 4 | 5 | [中文版本](./docker-zh.md) 6 | 7 | ## Prerequisites 8 | - Docker Desktop installed (or any Docker runtime) 9 | - Your Feishu/Lark application App ID and App Secret 10 | 11 | ## 1) Build the image 12 | ```bash 13 | # Execute from project root directory 14 | docker build -t lark-mcp:latest . 15 | ``` 16 | 17 | ## 2) Quick checks 18 | - View help 19 | ```bash 20 | docker run --rm -it lark-mcp:latest --help 21 | ``` 22 | - Check login status (will initialize storage) 23 | ```bash 24 | docker run --rm -it lark-mcp:latest whoami 25 | ``` 26 | 27 | 28 | ## 3) MCP Client Configuration 29 | 30 | After starting the Docker container, you need to configure your MCP client accordingly to use it. 31 | 32 | ### stdio Mode Configuration 33 | 34 | If using the default stdio mode, add the following to your MCP client (e.g., Cursor) configuration file: 35 | 36 | ```json 37 | { 38 | "mcpServers": { 39 | "lark-mcp": { 40 | "command": "docker", 41 | "args": [ 42 | "run", "--rm", "-i", 43 | "-v", "lark_mcp_data:/home/node/.local/share", 44 | "lark-mcp:latest", "mcp", 45 | "-a", "your_app_id", 46 | "-s", "your_app_secret" 47 | ] 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ### streamable Mode Configuration 54 | 55 | If using streamable (HTTP) mode, first start the container: 56 | 57 | ```bash 58 | docker run --rm -it \ 59 | -p 3000:3000 \ 60 | -v lark_mcp_data:/home/node/.local/share \ 61 | lark-mcp:latest mcp -a -s -m streamable --host 0.0.0.0 -p 3000 62 | ``` 63 | 64 | Then add the following to your MCP client configuration file: 65 | 66 | ```json 67 | { 68 | "mcpServers": { 69 | "lark-mcp": { 70 | "url": "http://localhost:3000/mcp" 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ### User Identity Configuration (user_access_token) 77 | 78 | 79 | > ⚠️ **Important Note**: In Docker environment, OAuth mode does not support yet. Please use -u for user authentication. 80 | 81 | 82 | 1. **MCP client configuration**: 83 | ```json 84 | { 85 | "mcpServers": { 86 | "lark-mcp": { 87 | "command": "docker", 88 | "args": [ 89 | "run", "--rm", "-i", 90 | "-v", "lark_mcp_data:/home/node/.local/share", 91 | "lark-mcp:latest", "mcp", 92 | "-a", "your_app_id", 93 | "-s", "your_app_secret", 94 | "-u", "your_user_access_token", 95 | "--token-mode", "user_access_token" 96 | ] 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | 103 | 104 | ## Tips 105 | - No password required: The container automatically initializes the secrets service, allowing keytar to securely store tokens without interaction. 106 | - Token persistence: Suggest always mount the `lark_mcp_data` volume. 107 | -------------------------------------------------------------------------------- /tests/utils/parser-string-array.test.ts: -------------------------------------------------------------------------------- 1 | import { parseStringArray } from '../../src/utils/parser-string-array'; 2 | 3 | describe('parseStringArray', () => { 4 | it('should return empty array for undefined input', () => { 5 | const result = parseStringArray(undefined); 6 | expect(result).toEqual([]); 7 | }); 8 | 9 | it('should return empty array for null input', () => { 10 | const result = parseStringArray(null as any); 11 | expect(result).toEqual([]); 12 | }); 13 | 14 | it('should return empty array for empty string', () => { 15 | const result = parseStringArray(''); 16 | expect(result).toEqual([]); 17 | }); 18 | 19 | it('should parse comma-separated string', () => { 20 | const result = parseStringArray('apple,banana,cherry'); 21 | expect(result).toEqual(['apple', 'banana', 'cherry']); 22 | }); 23 | 24 | it('should parse space-separated string', () => { 25 | const result = parseStringArray('apple banana cherry'); 26 | expect(result).toEqual(['apple', 'banana', 'cherry']); 27 | }); 28 | 29 | it('should parse mixed comma and space-separated string', () => { 30 | const result = parseStringArray('apple, banana cherry,orange'); 31 | expect(result).toEqual(['apple', 'banana', 'cherry', 'orange']); 32 | }); 33 | 34 | it('should trim whitespace from each item', () => { 35 | const result = parseStringArray(' apple , banana , cherry '); 36 | expect(result).toEqual(['', 'apple', 'banana', 'cherry', '']); 37 | }); 38 | 39 | it('should handle multiple consecutive separators', () => { 40 | const result = parseStringArray('apple,,, banana ,,, cherry'); 41 | expect(result).toEqual(['apple', 'banana', 'cherry']); 42 | }); 43 | 44 | it('should handle string array input and trim each item', () => { 45 | const result = parseStringArray([' apple ', ' banana ', ' cherry ']); 46 | expect(result).toEqual(['apple', 'banana', 'cherry']); 47 | }); 48 | 49 | it('should handle empty string array', () => { 50 | const result = parseStringArray([]); 51 | expect(result).toEqual([]); 52 | }); 53 | 54 | it('should handle array with empty strings', () => { 55 | const result = parseStringArray(['apple', '', 'banana', ' ', 'cherry']); 56 | expect(result).toEqual(['apple', '', 'banana', '', 'cherry']); 57 | }); 58 | 59 | it('should handle single item string', () => { 60 | const result = parseStringArray('apple'); 61 | expect(result).toEqual(['apple']); 62 | }); 63 | 64 | it('should handle single item array', () => { 65 | const result = parseStringArray(['apple']); 66 | expect(result).toEqual(['apple']); 67 | }); 68 | 69 | it('should handle string with only separators', () => { 70 | const result = parseStringArray(',,, '); 71 | expect(result).toEqual(['', '']); 72 | }); 73 | 74 | it('should handle complex mixed separators', () => { 75 | const result = parseStringArray('apple,banana orange\tpear,\n\rgrape'); 76 | expect(result).toEqual(['apple', 'banana', 'orange', 'pear', 'grape']); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/auth_v3.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type authV3ToolName = 3 | | 'auth.v3.auth.appAccessToken' 4 | | 'auth.v3.auth.appAccessTokenInternal' 5 | | 'auth.v3.auth.appTicketResend' 6 | | 'auth.v3.auth.tenantAccessToken' 7 | | 'auth.v3.auth.tenantAccessTokenInternal'; 8 | export const authV3AuthAppAccessToken = { 9 | project: 'auth', 10 | name: 'auth.v3.auth.appAccessToken', 11 | sdkName: 'auth.v3.auth.appAccessToken', 12 | path: '/open-apis/auth/v3/app_access_token', 13 | httpMethod: 'POST', 14 | description: '[Feishu/Lark]-Authenticate and Authorize-Get Access Tokens-Store applications get app_access_token', 15 | accessTokens: undefined, 16 | schema: { 17 | data: z.object({ app_id: z.string(), app_secret: z.string(), app_ticket: z.string() }), 18 | }, 19 | }; 20 | export const authV3AuthAppAccessTokenInternal = { 21 | project: 'auth', 22 | name: 'auth.v3.auth.appAccessTokenInternal', 23 | sdkName: 'auth.v3.auth.appAccessTokenInternal', 24 | path: '/open-apis/auth/v3/app_access_token/internal', 25 | httpMethod: 'POST', 26 | description: '[Feishu/Lark]-Authenticate and Authorize-Get Access Tokens-Get custom app app_access_token', 27 | accessTokens: undefined, 28 | schema: { 29 | data: z.object({ app_id: z.string(), app_secret: z.string() }), 30 | }, 31 | }; 32 | export const authV3AuthAppTicketResend = { 33 | project: 'auth', 34 | name: 'auth.v3.auth.appTicketResend', 35 | sdkName: 'auth.v3.auth.appTicketResend', 36 | path: '/open-apis/auth/v3/app_ticket/resend', 37 | httpMethod: 'POST', 38 | description: '[Feishu/Lark]-Authenticate and Authorize-Get Access Tokens-Retrieve app_ticket', 39 | accessTokens: undefined, 40 | schema: { 41 | data: z.object({ app_id: z.string(), app_secret: z.string() }), 42 | }, 43 | }; 44 | export const authV3AuthTenantAccessToken = { 45 | project: 'auth', 46 | name: 'auth.v3.auth.tenantAccessToken', 47 | sdkName: 'auth.v3.auth.tenantAccessToken', 48 | path: '/open-apis/auth/v3/tenant_access_token', 49 | httpMethod: 'POST', 50 | description: '[Feishu/Lark]-Authenticate and Authorize-Get Access Tokens-Store applications get tenant_access_token', 51 | accessTokens: undefined, 52 | schema: { 53 | data: z.object({ app_access_token: z.string(), tenant_key: z.string() }), 54 | }, 55 | }; 56 | export const authV3AuthTenantAccessTokenInternal = { 57 | project: 'auth', 58 | name: 'auth.v3.auth.tenantAccessTokenInternal', 59 | sdkName: 'auth.v3.auth.tenantAccessTokenInternal', 60 | path: '/open-apis/auth/v3/tenant_access_token/internal', 61 | httpMethod: 'POST', 62 | description: '[Feishu/Lark]-Authenticate and Authorize-Get Access Tokens-Get custom app tenant_access_token', 63 | accessTokens: undefined, 64 | schema: { 65 | data: z.object({ app_id: z.string(), app_secret: z.string() }), 66 | }, 67 | }; 68 | export const authV3Tools = [ 69 | authV3AuthAppAccessToken, 70 | authV3AuthAppAccessTokenInternal, 71 | authV3AuthAppTicketResend, 72 | authV3AuthTenantAccessToken, 73 | authV3AuthTenantAccessTokenInternal, 74 | ]; 75 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { ENV_PATHS } from './constants'; 4 | import { currentVersion } from './version'; 5 | 6 | export enum LogLevel { 7 | ERROR = 1, 8 | WARN = 2, 9 | INFO = 3, 10 | DEBUG = 4, 11 | } 12 | 13 | export class Logger { 14 | private level: LogLevel = LogLevel.WARN; 15 | 16 | static logFilesToKeep = 7; 17 | 18 | static logFilePrefix = 'lark-mcp-'; 19 | 20 | constructor() { 21 | this.initLogFile(); 22 | this.cleanHistoryLogFile(); 23 | } 24 | 25 | get logFileName() { 26 | return `${Logger.logFilePrefix}${new Date().toISOString().split('T')[0]}.log`; 27 | } 28 | 29 | initLogFile = () => { 30 | if (!fs.existsSync(ENV_PATHS.log)) { 31 | fs.mkdirSync(ENV_PATHS.log, { recursive: true }); 32 | } 33 | }; 34 | 35 | cleanHistoryLogFile = () => { 36 | try { 37 | // clean old log files, 7 days ago 38 | const logFiles = fs 39 | .readdirSync(ENV_PATHS.log) 40 | .filter((file) => file.startsWith(Logger.logFilePrefix) && file.endsWith('.log')); 41 | const logFilesToDelete = logFiles.filter((file) => { 42 | const fileDate = file.split('-')[1].split('.')[0]; 43 | const fileDateObj = new Date(fileDate); 44 | return fileDateObj < new Date(Date.now() - Logger.logFilesToKeep * 24 * 60 * 60 * 1000); 45 | }); 46 | for (const file of logFilesToDelete) { 47 | try { 48 | fs.unlinkSync(path.join(ENV_PATHS.log, file)); 49 | } catch (error) { 50 | console.error(`Failed to delete log file: ${error}`); 51 | } 52 | } 53 | } catch (error) { 54 | console.error(`Failed to clean history log file: ${error}`); 55 | } 56 | }; 57 | 58 | setLevel = (level: LogLevel) => { 59 | this.level = level; 60 | }; 61 | 62 | log = (message: string) => { 63 | try { 64 | fs.appendFileSync( 65 | path.join(ENV_PATHS.log, this.logFileName), 66 | `[${new Date().toISOString()}] [${currentVersion}] [${process.pid}] ${message}\n`, 67 | ); 68 | } catch (error) { 69 | console.error(`Failed to write log: ${error} ${message}`); 70 | } 71 | }; 72 | 73 | debug = (message: string) => { 74 | if (this.level < LogLevel.DEBUG) { 75 | return; 76 | } 77 | this.log(`[DEBUG] ${message}`); 78 | }; 79 | 80 | info = (message: string) => { 81 | if (this.level < LogLevel.INFO) { 82 | return; 83 | } 84 | this.log(`[INFO] ${message}`); 85 | }; 86 | 87 | warn = (message: string) => { 88 | if (this.level < LogLevel.WARN) { 89 | return; 90 | } 91 | console.error(`[WARN] ${message}`); 92 | this.log(`[WARN] ${message}`); 93 | }; 94 | 95 | error = (message: string) => { 96 | if (this.level < LogLevel.ERROR) { 97 | return; 98 | } 99 | console.error(`[ERROR] ${message}`); 100 | this.log(`[ERROR] ${message}`); 101 | }; 102 | } 103 | 104 | export const logger = new Logger(); 105 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/passport_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type passportV1ToolName = 'passport.v1.session.logout' | 'passport.v1.session.query'; 3 | export const passportV1SessionLogout = { 4 | project: 'passport', 5 | name: 'passport.v1.session.logout', 6 | sdkName: 'passport.v1.session.logout', 7 | path: '/open-apis/passport/v1/sessions/logout', 8 | httpMethod: 'POST', 9 | description: 10 | '[Feishu/Lark]-Authenticate and Authorize-Login state management-Log out-This interface is used to log out of the user login state', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | data: z.object({ 14 | idp_credential_id: z 15 | .string() 16 | .describe('The unique identifier of the idp, required when logout_type = 2') 17 | .optional(), 18 | logout_type: z 19 | .number() 20 | .describe( 21 | 'Used to identify the type of logout Options:1(UserID UserID),2(IdpCredentialID),3(SessionUUID Session uuid)', 22 | ), 23 | terminal_type: z 24 | .array(z.number()) 25 | .describe( 26 | 'Logout terminal_type, default all logout.- 1:pc- 2:web- 3:android- 4:iOS- 5:server- 6:old version mini program- 8:other mobile', 27 | ) 28 | .optional(), 29 | user_id: z 30 | .string() 31 | .describe( 32 | 'User ID categories, is consistent with the query parameter user_id_type.required when logout_type = 1', 33 | ) 34 | .optional(), 35 | logout_reason: z 36 | .number() 37 | .describe( 38 | 'Logout prompt, optional.- 34:You have changed your login password, please log in again.- 35:Your login status has expired, please log in again. - 36:Your password has expired. Please use the Forgot Password function on the login page to change your password and log in again', 39 | ) 40 | .optional(), 41 | sid: z 42 | .string() 43 | .describe('The session that needs to be logged out accurately, required when logout_type = 3') 44 | .optional(), 45 | }), 46 | params: z 47 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 48 | .optional(), 49 | }, 50 | }; 51 | export const passportV1SessionQuery = { 52 | project: 'passport', 53 | name: 'passport.v1.session.query', 54 | sdkName: 'passport.v1.session.query', 55 | path: '/open-apis/passport/v1/sessions/query', 56 | httpMethod: 'POST', 57 | description: 58 | "[Feishu/Lark]-Authenticate and Authorize-Login state management-Obtain desensitized user login information in batches-This interface is used to query the user's login information", 59 | accessTokens: ['tenant'], 60 | schema: { 61 | data: z.object({ user_ids: z.array(z.string()).describe('User ID').optional() }).optional(), 62 | params: z 63 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 64 | .optional(), 65 | }, 66 | }; 67 | export const passportV1Tools = [passportV1SessionLogout, passportV1SessionQuery]; 68 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/hire_v2.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type hireV2ToolName = 'hire.v2.interviewRecord.get' | 'hire.v2.interviewRecord.list' | 'hire.v2.talent.get'; 3 | export const hireV2InterviewRecordGet = { 4 | project: 'hire', 5 | name: 'hire.v2.interviewRecord.get', 6 | sdkName: 'hire.v2.interviewRecord.get', 7 | path: '/open-apis/hire/v2/interview_records/:interview_record_id', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Hire-Candidate management-Delivery process-Interview-Get Interview Feedback Detail(new version)-Get interview feedback details', 11 | accessTokens: ['tenant', 'user'], 12 | schema: { 13 | params: z 14 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 15 | .optional(), 16 | path: z.object({ interview_record_id: z.string().describe('Interview record ID') }), 17 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 18 | }, 19 | }; 20 | export const hireV2InterviewRecordList = { 21 | project: 'hire', 22 | name: 'hire.v2.interviewRecord.list', 23 | sdkName: 'hire.v2.interviewRecord.list', 24 | path: '/open-apis/hire/v2/interview_records', 25 | httpMethod: 'GET', 26 | description: 27 | '[Feishu/Lark]-Hire-Candidate management-Delivery process-Interview-Batch Get Interview Feedback Details(new version)-Batch get interview feedback', 28 | accessTokens: ['tenant', 'user'], 29 | schema: { 30 | params: z 31 | .object({ 32 | ids: z.array(z.string()).describe('List interview feedback by IDs, page param will not be used').optional(), 33 | page_size: z.number().describe('Paging size').optional(), 34 | page_token: z 35 | .string() 36 | .describe( 37 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 38 | ) 39 | .optional(), 40 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional(), 41 | }) 42 | .optional(), 43 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 44 | }, 45 | }; 46 | export const hireV2TalentGet = { 47 | project: 'hire', 48 | name: 'hire.v2.talent.get', 49 | sdkName: 'hire.v2.talent.get', 50 | path: '/open-apis/hire/v2/talents/:talent_id', 51 | httpMethod: 'GET', 52 | description: 53 | '[Feishu/Lark]-Hire-Candidate management-Talent-Get talent details-Obtain talent information according to talent ID', 54 | accessTokens: ['tenant'], 55 | schema: { 56 | params: z 57 | .object({ 58 | user_id_type: z.enum(['open_id', 'union_id', 'user_id', 'people_admin_id']).describe('User ID type').optional(), 59 | }) 60 | .optional(), 61 | path: z.object({ talent_id: z.string().describe('Talent ID') }), 62 | }, 63 | }; 64 | export const hireV2Tools = [hireV2InterviewRecordGet, hireV2InterviewRecordList, hireV2TalentGet]; 65 | -------------------------------------------------------------------------------- /tests/utils/clean-env-args.test.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnvArgs } from '../../src/utils/clean-env-args'; 2 | 3 | describe('cleanEnvArgs', () => { 4 | it('should remove undefined values from the object', () => { 5 | const input = { 6 | validKey: 'validValue', 7 | undefinedKey: undefined, 8 | anotherValidKey: 'anotherValue', 9 | }; 10 | 11 | const result = cleanEnvArgs(input); 12 | 13 | expect(result).toEqual({ 14 | validKey: 'validValue', 15 | anotherValidKey: 'anotherValue', 16 | }); 17 | }); 18 | 19 | it('should remove empty string values', () => { 20 | const input = { 21 | validKey: 'validValue', 22 | emptyKey: '', 23 | anotherValidKey: 'anotherValue', 24 | }; 25 | 26 | const result = cleanEnvArgs(input); 27 | 28 | expect(result).toEqual({ 29 | validKey: 'validValue', 30 | anotherValidKey: 'anotherValue', 31 | }); 32 | }); 33 | 34 | it('should handle an empty object', () => { 35 | const input = {}; 36 | 37 | const result = cleanEnvArgs(input); 38 | 39 | expect(result).toEqual({}); 40 | }); 41 | 42 | it('should handle object with all undefined values', () => { 43 | const input = { 44 | key1: undefined, 45 | key2: undefined, 46 | key3: undefined, 47 | }; 48 | 49 | const result = cleanEnvArgs(input); 50 | 51 | expect(result).toEqual({}); 52 | }); 53 | 54 | it('should handle object with all valid values', () => { 55 | const input = { 56 | key1: 'value1', 57 | key2: 'value2', 58 | key3: 'value3', 59 | }; 60 | 61 | const result = cleanEnvArgs(input); 62 | 63 | expect(result).toEqual({ 64 | key1: 'value1', 65 | key2: 'value2', 66 | key3: 'value3', 67 | }); 68 | }); 69 | 70 | it('should handle mixed undefined, empty strings, and valid values', () => { 71 | const input = { 72 | validKey: 'validValue', 73 | undefinedKey: undefined, 74 | emptyKey: '', 75 | anotherValidKey: 'anotherValue', 76 | nullishKey: undefined, 77 | }; 78 | 79 | const result = cleanEnvArgs(input); 80 | 81 | expect(result).toEqual({ 82 | validKey: 'validValue', 83 | anotherValidKey: 'anotherValue', 84 | }); 85 | }); 86 | 87 | it('should preserve whitespace-only strings as they are truthy', () => { 88 | const input = { 89 | validKey: 'validValue', 90 | whitespaceKey: ' ', 91 | tabKey: '\t', 92 | newlineKey: '\n', 93 | }; 94 | 95 | const result = cleanEnvArgs(input); 96 | 97 | expect(result).toEqual({ 98 | validKey: 'validValue', 99 | whitespaceKey: ' ', 100 | tabKey: '\t', 101 | newlineKey: '\n', 102 | }); 103 | }); 104 | 105 | it('should handle numeric strings', () => { 106 | const input = { 107 | zeroString: '0', 108 | numberString: '123', 109 | undefinedKey: undefined, 110 | }; 111 | 112 | const result = cleanEnvArgs(input); 113 | 114 | expect(result).toEqual({ 115 | zeroString: '0', 116 | numberString: '123', 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/mcp-server/transport/sse.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; 3 | import { InitTransportServerFunction } from '../shared'; 4 | import { LarkAuthHandler } from '../../auth'; 5 | import { parseMCPServerOptionsFromRequest } from './utils'; 6 | import { logger } from '../../utils/logger'; 7 | 8 | export const initSSEServer: InitTransportServerFunction = ( 9 | getNewServer, 10 | options, 11 | { needAuthFlow } = { needAuthFlow: false }, 12 | ) => { 13 | const { userAccessToken, port, host, oauth } = options; 14 | 15 | if (!port || !host) { 16 | throw new Error('[Lark MCP] Port and host are required'); 17 | } 18 | 19 | const app = express(); 20 | const transports: Map = new Map(); 21 | 22 | let authHandler: LarkAuthHandler | undefined; 23 | 24 | if (!userAccessToken && needAuthFlow) { 25 | authHandler = new LarkAuthHandler(app, options); 26 | if (oauth) { 27 | authHandler.setupRoutes(); 28 | } 29 | } 30 | 31 | const authMiddleware = (req: Request, res: Response, next: NextFunction) => { 32 | if (authHandler && oauth) { 33 | authHandler.authenticateRequest(req, res, next); 34 | } else { 35 | const authToken = req.headers.authorization?.split(' ')[1]; 36 | if (authToken) { 37 | req.auth = { token: authToken, clientId: 'client_id_for_local_auth', scopes: [] }; 38 | } 39 | next(); 40 | } 41 | }; 42 | 43 | app.get('/sse', authMiddleware, async (req: Request, res: Response) => { 44 | logger.info(`[SSEServerTransport] Received GET SSE request`); 45 | 46 | const token = req.auth?.token; 47 | const { data } = parseMCPServerOptionsFromRequest(req); 48 | const server = getNewServer({ ...options, ...data, userAccessToken: data.userAccessToken || token }, authHandler); 49 | const transport = new SSEServerTransport('/messages', res); 50 | transports.set(transport.sessionId, transport); 51 | 52 | res.on('close', () => { 53 | transport.close(); 54 | server.close(); 55 | transports.delete(transport.sessionId); 56 | }); 57 | 58 | await server.connect(transport); 59 | }); 60 | 61 | app.post('/messages', authMiddleware, async (req: Request, res: Response) => { 62 | console.log('Received POST messages request'); 63 | logger.info(`[SSEServerTransport] Received POST messages request`); 64 | 65 | const sessionId = req.query.sessionId as string; 66 | const transport = transports.get(sessionId); 67 | if (!transport) { 68 | res.status(400).send('No transport found for sessionId'); 69 | return; 70 | } 71 | await transport.handlePostMessage(req, res); 72 | }); 73 | 74 | console.log('⚠️ SSE Mode is deprecated and will be removed in a future version. Please use Streamable mode instead.'); 75 | 76 | app.listen(port, host, (error) => { 77 | if (error) { 78 | logger.error(`[SSEServerTransport] Server error: ${error}`); 79 | process.exit(1); 80 | } 81 | console.log(`📡 SSE endpoint: http://${host}:${port}/sse`); 82 | logger.info(`[SSEServerTransport] SSE endpoint: http://${host}:${port}/sse`); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /tests/utils/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { USER_AGENT, OAPI_MCP_DEFAULT_ARGS, OAPI_MCP_ENV_ARGS } from '../../src/utils/constants'; 2 | import { currentVersion } from '../../src/utils/version'; 3 | 4 | // Mock environment variables 5 | const originalEnv = process.env; 6 | 7 | describe('Constants', () => { 8 | beforeEach(() => { 9 | jest.resetModules(); 10 | process.env = { ...originalEnv }; 11 | }); 12 | 13 | afterAll(() => { 14 | process.env = originalEnv; 15 | }); 16 | 17 | describe('USER_AGENT', () => { 18 | it('should include the current version', () => { 19 | expect(USER_AGENT).toBe(`oapi-sdk-mcp/${currentVersion}`); 20 | }); 21 | }); 22 | 23 | describe('OAPI_MCP_DEFAULT_ARGS', () => { 24 | it('should have correct default values', () => { 25 | expect(OAPI_MCP_DEFAULT_ARGS).toEqual({ 26 | domain: 'https://open.feishu.cn', 27 | toolNameCase: 'snake', 28 | language: 'en', 29 | tokenMode: 'auto', 30 | mode: 'stdio', 31 | host: 'localhost', 32 | port: '3000', 33 | }); 34 | }); 35 | }); 36 | 37 | describe('OAPI_MCP_ENV_ARGS', () => { 38 | it('should be empty when no environment variables are set', () => { 39 | // Clear environment variables 40 | delete process.env.APP_ID; 41 | delete process.env.APP_SECRET; 42 | delete process.env.USER_ACCESS_TOKEN; 43 | delete process.env.LARK_TOKEN_MODE; 44 | delete process.env.LARK_TOOLS; 45 | delete process.env.LARK_DOMAIN; 46 | 47 | // Re-require the module to get fresh environment 48 | jest.resetModules(); 49 | const { OAPI_MCP_ENV_ARGS: freshEnvArgs } = require('../../src/utils/constants'); 50 | 51 | expect(freshEnvArgs).toEqual({}); 52 | }); 53 | 54 | it('should include environment variables when they are set', () => { 55 | process.env.APP_ID = 'test-app-id'; 56 | process.env.APP_SECRET = 'test-app-secret'; 57 | process.env.USER_ACCESS_TOKEN = 'test-user-token'; 58 | process.env.LARK_TOKEN_MODE = 'manual'; 59 | process.env.LARK_TOOLS = 'tool1,tool2'; 60 | process.env.LARK_DOMAIN = 'https://custom.domain.com'; 61 | 62 | jest.resetModules(); 63 | const { OAPI_MCP_ENV_ARGS: freshEnvArgs } = require('../../src/utils/constants'); 64 | 65 | expect(freshEnvArgs).toEqual({ 66 | appId: 'test-app-id', 67 | appSecret: 'test-app-secret', 68 | userAccessToken: 'test-user-token', 69 | tokenMode: 'manual', 70 | tools: 'tool1,tool2', 71 | domain: 'https://custom.domain.com', 72 | }); 73 | }); 74 | 75 | it('should filter out empty string environment variables', () => { 76 | process.env.APP_ID = 'valid-id'; 77 | process.env.APP_SECRET = ''; 78 | process.env.USER_ACCESS_TOKEN = undefined; 79 | 80 | jest.resetModules(); 81 | const { OAPI_MCP_ENV_ARGS: freshEnvArgs } = require('../../src/utils/constants'); 82 | 83 | // Should only include valid non-empty values 84 | expect(freshEnvArgs).toEqual({ 85 | appId: 'valid-id', 86 | }); 87 | expect(freshEnvArgs).not.toHaveProperty('appSecret'); 88 | expect(freshEnvArgs).not.toHaveProperty('userAccessToken'); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/auth_v3.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type authV3ToolName = 3 | | 'auth.v3.auth.appAccessToken' 4 | | 'auth.v3.auth.appAccessTokenInternal' 5 | | 'auth.v3.auth.appTicketResend' 6 | | 'auth.v3.auth.tenantAccessToken' 7 | | 'auth.v3.auth.tenantAccessTokenInternal'; 8 | export const authV3AuthAppAccessToken = { 9 | project: 'auth', 10 | name: 'auth.v3.auth.appAccessToken', 11 | sdkName: 'auth.v3.auth.appAccessToken', 12 | path: '/open-apis/auth/v3/app_access_token', 13 | httpMethod: 'POST', 14 | description: '[Feishu/Lark]-认证及授权-获取访问凭证-商店应用获取 app_access_token', 15 | accessTokens: undefined, 16 | schema: { 17 | data: z.object({ 18 | app_id: z.string().describe('应用唯一标识,创建应用后获得'), 19 | app_secret: z.string().describe('应用秘钥,创建应用后获得'), 20 | app_ticket: z.string().describe('平台定时推送给应用的临时凭证,通过事件监听机制获得'), 21 | }), 22 | }, 23 | }; 24 | export const authV3AuthAppAccessTokenInternal = { 25 | project: 'auth', 26 | name: 'auth.v3.auth.appAccessTokenInternal', 27 | sdkName: 'auth.v3.auth.appAccessTokenInternal', 28 | path: '/open-apis/auth/v3/app_access_token/internal', 29 | httpMethod: 'POST', 30 | description: '[Feishu/Lark]-认证及授权-获取访问凭证-自建应用获取 app_access_token', 31 | accessTokens: undefined, 32 | schema: { 33 | data: z.object({ 34 | app_id: z.string().describe('应用唯一标识,创建应用后获得'), 35 | app_secret: z.string().describe('应用秘钥,创建应用后获得'), 36 | }), 37 | }, 38 | }; 39 | export const authV3AuthAppTicketResend = { 40 | project: 'auth', 41 | name: 'auth.v3.auth.appTicketResend', 42 | sdkName: 'auth.v3.auth.appTicketResend', 43 | path: '/open-apis/auth/v3/app_ticket/resend', 44 | httpMethod: 'POST', 45 | description: '[Feishu/Lark]-认证及授权-获取访问凭证-重新获取 app_ticket', 46 | accessTokens: undefined, 47 | schema: { 48 | data: z.object({ 49 | app_id: z.string().describe('应用唯一标识,创建应用后获得'), 50 | app_secret: z.string().describe('应用秘钥,创建应用后获得'), 51 | }), 52 | }, 53 | }; 54 | export const authV3AuthTenantAccessToken = { 55 | project: 'auth', 56 | name: 'auth.v3.auth.tenantAccessToken', 57 | sdkName: 'auth.v3.auth.tenantAccessToken', 58 | path: '/open-apis/auth/v3/tenant_access_token', 59 | httpMethod: 'POST', 60 | description: '[Feishu/Lark]-认证及授权-获取访问凭证-商店应用获取 tenant_access_token', 61 | accessTokens: undefined, 62 | schema: { 63 | data: z.object({ 64 | app_access_token: z.string().describe('应用唯一标识,创建应用'), 65 | tenant_key: z.string().describe('应用秘钥,创建应用后获得'), 66 | }), 67 | }, 68 | }; 69 | export const authV3AuthTenantAccessTokenInternal = { 70 | project: 'auth', 71 | name: 'auth.v3.auth.tenantAccessTokenInternal', 72 | sdkName: 'auth.v3.auth.tenantAccessTokenInternal', 73 | path: '/open-apis/auth/v3/tenant_access_token/internal', 74 | httpMethod: 'POST', 75 | description: '[Feishu/Lark]-认证及授权-获取访问凭证-自建应用获取 tenant_access_token', 76 | accessTokens: undefined, 77 | schema: { 78 | data: z.object({ 79 | app_id: z.string().describe('应用唯一标识,创建应用后获得'), 80 | app_secret: z.string().describe('应用秘钥,创建应用后获得'), 81 | }), 82 | }, 83 | }; 84 | export const authV3Tools = [ 85 | authV3AuthAppAccessToken, 86 | authV3AuthAppAccessTokenInternal, 87 | authV3AuthAppTicketResend, 88 | authV3AuthTenantAccessToken, 89 | authV3AuthTenantAccessTokenInternal, 90 | ]; 91 | -------------------------------------------------------------------------------- /docs/reference/cli/cli-zh.md: -------------------------------------------------------------------------------- 1 | # 命令行参考 2 | 3 | 本文档提供了 lark-mcp 工具所有命令行参数的详细说明。 4 | 5 | ## 目录 6 | 7 | - [lark-mcp login](#lark-mcp-login) 8 | - [lark-mcp logout](#lark-mcp-logout) 9 | - [lark-mcp mcp](#lark-mcp-mcp) 10 | 11 | ## lark-mcp login 12 | 13 | `lark-mcp login` 命令用于进行用户身份认证,获取用户访问令牌以访问用户的个人数据。 14 | 15 | ### 参数说明 16 | 17 | | 参数 | 简写 | 描述 | 示例 | 18 | |------|------|------|------| 19 | | `--app-id` | `-a` | 飞书/Lark应用的App ID | `-a cli_xxxx` | 20 | | `--app-secret` | `-s` | 飞书/Lark应用的App Secret | `-s xxxx` | 21 | | `--domain` | `-d` | 飞书/Lark API域名,默认为https://open.feishu.cn | `-d https://open.larksuite.com` | 22 | | `--host` | | 监听主机,默认为localhost | `--host localhost` | 23 | | `--port` | `-p` | 监听端口,默认为3000 | `-p 3000` | 24 | | `--scope` | | 指定授权用户访问令牌的OAuth权限范围,默认为应用开通的全部权限,用空格或者逗号分割 | `--scope offline_access docx:document` | 25 | 26 | ### 使用示例 27 | 28 | ```bash 29 | # 基础登录 30 | npx -y @larksuiteoapi/lark-mcp login -a cli_xxxx -s your_secret 31 | 32 | # 指定特定的OAuth权限范围登录 33 | npx -y @larksuiteoapi/lark-mcp login -a cli_xxxx -s your_secret --scope offline_access docx:document 34 | 35 | # 使用自定义域名登录(适用于Lark国际版) 36 | npx -y @larksuiteoapi/lark-mcp login -a cli_xxxx -s your_secret -d https://open.larksuite.com 37 | ``` 38 | 39 | ## lark-mcp logout 40 | 41 | `lark-mcp logout` 命令用于清除本地存储的用户访问令牌。 42 | 43 | ### 参数说明 44 | 45 | | 参数 | 简写 | 描述 | 示例 | 46 | |------|------|------|------| 47 | | `--app-id` | `-a` | 飞书/Lark应用的App ID,可选。指定则只清除该应用的令牌,不指定则清除所有应用的令牌 | `-a cli_xxxx` | 48 | 49 | ### 功能说明 50 | 51 | 此命令用于清除本地存储的用户访问令牌。如果指定了 `--app-id` 参数,则只清除该应用的用户访问令牌;如果不指定,则清除所有应用的用户访问令牌。 52 | 53 | ### 使用示例 54 | 55 | ```bash 56 | # 清除特定应用的令牌 57 | npx -y @larksuiteoapi/lark-mcp logout -a cli_xxxx 58 | 59 | # 清除所有应用的令牌 60 | npx -y @larksuiteoapi/lark-mcp logout 61 | ``` 62 | 63 | ## lark-mcp mcp 64 | 65 | `lark-mcp mcp` 工具提供了多种命令行参数,以便您灵活配置MCP服务。 66 | 67 | ### 参数说明 68 | 69 | | 参数 | 简写 | 描述 | 示例 | 70 | |------|------|------|------| 71 | | `--app-id` | `-a` | 飞书/Lark应用的App ID | `-a cli_xxxx` | 72 | | `--app-secret` | `-s` | 飞书/Lark应用的App Secret | `-s xxxx` | 73 | | `--domain` | `-d` | 飞书/Lark API域名,默认为https://open.feishu.cn | `-d https://open.larksuite.com` | 74 | | `--tools` | `-t` | 需要启用的API工具列表,用空格或者逗号分割 | `-t im.v1.message.create,im.v1.chat.create` | 75 | | `--tool-name-case` | `-c` | 工具注册名称的命名格式,可选值为snake、camel、dot或kebab,默认为snake | `-c camel` | 76 | | `--language` | `-l` | 工具语言,可选值为zh或en,默认为en | `-l zh` | 77 | | `--user-access-token` | `-u` | 用户访问令牌,用于以用户身份调用API | `-u u-xxxx` | 78 | | `--token-mode` | | API令牌类型,可选值为auto、tenant_access_token或user_access_token,默认为auto | `--token-mode user_access_token` | 79 | | `--oauth` | | 开启 MCP Auth Server 获取user_access_token,且当Token失效时自动要求用户重新登录(Beta) | `--oauth` | 80 | | `--scope` | | 指定授权用户访问令牌的OAuth权限范围,默认为应用开通的全部权限,用空格或者逗号分割 | `--scope offline_access docx:document` | 81 | | `--mode` | `-m` | 传输模式,可选值为stdio、streamable或sse,默认为stdio | `-m streamable` | 82 | | `--host` | | SSE\Streamable模式下的监听主机,默认为localhost | `--host 0.0.0.0` | 83 | | `--port` | `-p` | SSE\Streamable模式下的监听端口,默认为3000 | `-p 3000` | 84 | | `--config` | | 配置文件路径,支持JSON格式 | `--config ./config.json` | 85 | | `--version` | `-V` | 显示版本号 | `-V` | 86 | | `--help` | `-h` | 显示帮助信息 | `-h` | 87 | 88 | ## 相关文档 89 | 90 | - [配置指南](../../usage/configuration/configuration-zh.md) 91 | - [工具参考](../tool-presets/tools-zh.md) 92 | - [故障排除](../../troubleshooting/faq-zh.md) 93 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/report_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type reportV1ToolName = 'report.v1.rule.query' | 'report.v1.ruleView.remove' | 'report.v1.task.query'; 3 | export const reportV1RuleQuery = { 4 | project: 'report', 5 | name: 'report.v1.rule.query', 6 | sdkName: 'report.v1.rule.query', 7 | path: '/open-apis/report/v1/rules/query', 8 | httpMethod: 'GET', 9 | description: '[Feishu/Lark]-Report-Rule-Query rules-Query rules', 10 | accessTokens: ['tenant'], 11 | schema: { 12 | params: z.object({ 13 | rule_name: z.string().describe('Rule name'), 14 | include_deleted: z 15 | .number() 16 | .describe( 17 | 'Whether to include deleted, not deleted by default Options:0(Exclude Does not include deleted),1(Include Include deleted)', 18 | ) 19 | .optional(), 20 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional(), 21 | }), 22 | }, 23 | }; 24 | export const reportV1RuleViewRemove = { 25 | project: 'report', 26 | name: 'report.v1.ruleView.remove', 27 | sdkName: 'report.v1.ruleView.remove', 28 | path: '/open-apis/report/v1/rules/:rule_id/views/remove', 29 | httpMethod: 'POST', 30 | description: '[Feishu/Lark]-Report-Rule view-Remove rule board-Remove rule board', 31 | accessTokens: ['tenant'], 32 | schema: { 33 | data: z 34 | .object({ 35 | user_ids: z 36 | .array(z.string()) 37 | .describe( 38 | 'If the list is empty and, deletes the full user view under the rule. If the list is not empty, then deletes the specified user view. The size limit is 200', 39 | ) 40 | .optional(), 41 | }) 42 | .optional(), 43 | params: z 44 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 45 | .optional(), 46 | path: z.object({ rule_id: z.string().describe('Reporting tule ID') }), 47 | }, 48 | }; 49 | export const reportV1TaskQuery = { 50 | project: 'report', 51 | name: 'report.v1.task.query', 52 | sdkName: 'report.v1.task.query', 53 | path: '/open-apis/report/v1/tasks/query', 54 | httpMethod: 'POST', 55 | description: '[Feishu/Lark]-Report-Task-Query tasks-Query tasks', 56 | accessTokens: ['tenant'], 57 | schema: { 58 | data: z.object({ 59 | commit_start_time: z.number().describe('Commit start time timestamp'), 60 | commit_end_time: z.number().describe('End of submission timestamp'), 61 | rule_id: z.string().describe('Reporting rule ID').optional(), 62 | user_id: z.string().describe('User ID').optional(), 63 | page_token: z 64 | .string() 65 | .describe( 66 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 67 | ), 68 | page_size: z.number().describe('Number of items returned by a single page'), 69 | }), 70 | params: z 71 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 72 | .optional(), 73 | }, 74 | }; 75 | export const reportV1Tools = [reportV1RuleQuery, reportV1RuleViewRemove, reportV1TaskQuery]; 76 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/minutes_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type minutesV1ToolName = 3 | | 'minutes.v1.minute.get' 4 | | 'minutes.v1.minuteMedia.get' 5 | | 'minutes.v1.minuteStatistics.get'; 6 | export const minutesV1MinuteGet = { 7 | project: 'minutes', 8 | name: 'minutes.v1.minute.get', 9 | sdkName: 'minutes.v1.minute.get', 10 | path: '/open-apis/minutes/v1/minutes/:minute_token', 11 | httpMethod: 'GET', 12 | description: 13 | '[Feishu/Lark]-Minutes-Minutes Meta-Get minutes meta-Through this api, you can get a basic overview of Lark Minutes, including `owner_id`, `create_time`, title, cover picture, duration and URL', 14 | accessTokens: ['tenant', 'user'], 15 | schema: { 16 | params: z 17 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 18 | .optional(), 19 | path: z.object({ 20 | minute_token: z 21 | .string() 22 | .describe( 23 | 'The unique identifier for Minutes. It can be obtained from the URL link of the Minutes, usually the last string of characters: https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 24 | ), 25 | }), 26 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 27 | }, 28 | }; 29 | export const minutesV1MinuteMediaGet = { 30 | project: 'minutes', 31 | name: 'minutes.v1.minuteMedia.get', 32 | sdkName: 'minutes.v1.minuteMedia.get', 33 | path: '/open-apis/minutes/v1/minutes/:minute_token/media', 34 | httpMethod: 'GET', 35 | description: 36 | '[Feishu/Lark]-Minutes-Minutes audio or video file-Download minutes audio or video file-Get the audio or video file of minutes', 37 | accessTokens: ['tenant', 'user'], 38 | schema: { 39 | path: z.object({ 40 | minute_token: z 41 | .string() 42 | .describe( 43 | 'The unique identifier for Minutes. It can be obtained from the URL link of the Minutes, usually the last string of characters: https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 44 | ), 45 | }), 46 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 47 | }, 48 | }; 49 | export const minutesV1MinuteStatisticsGet = { 50 | project: 'minutes', 51 | name: 'minutes.v1.minuteStatistics.get', 52 | sdkName: 'minutes.v1.minuteStatistics.get', 53 | path: '/open-apis/minutes/v1/minutes/:minute_token/statistics', 54 | httpMethod: 'GET', 55 | description: 56 | '[Feishu/Lark]-Minutes-Minutes statistics-Get minutes statistics-Through this API, you can get access statistics of Feishu Minutes, including PV, UV, visited user id, visited user timestamp', 57 | accessTokens: ['tenant', 'user'], 58 | schema: { 59 | params: z 60 | .object({ user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional() }) 61 | .optional(), 62 | path: z.object({ 63 | minute_token: z 64 | .string() 65 | .describe( 66 | 'The unique identifier for Minutes. It can be obtained from the URL link of the Minutes, usually the last string of characters: https://sample.feishu.cn/minutes/==obcnq3b9jl72l83w4f14xxxx==', 67 | ), 68 | }), 69 | useUAT: z.boolean().describe('Use user access token, otherwise use tenant access token').optional(), 70 | }, 71 | }; 72 | export const minutesV1Tools = [minutesV1MinuteGet, minutesV1MinuteMediaGet, minutesV1MinuteStatisticsGet]; 73 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/mdm_v3.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type mdmV3ToolName = 'mdm.v3.batchCountryRegion.get' | 'mdm.v3.countryRegion.list'; 3 | export const mdmV3BatchCountryRegionGet = { 4 | project: 'mdm', 5 | name: 'mdm.v3.batchCountryRegion.get', 6 | sdkName: 'mdm.v3.batchCountryRegion.get', 7 | path: '/open-apis/mdm/v3/batch_country_region', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-飞书主数据-基础数据-国家/地区-根据主数据编码批量查询国家/地区-通过mdmcode批量查询国家/地区信息。资源介绍请参考[概述]', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z.object({ 14 | fields: z.array(z.string()).describe('需要的查询字段集'), 15 | ids: z.array(z.string()).describe('主数据编码集'), 16 | languages: z 17 | .array(z.string().describe('语言')) 18 | .describe( 19 | '希望返回的语言种类,支持格式如下:- 中文:zh-CN- 英文:en-US- 日文:ja-JP对于多语文本字段,传入特定语言,将会返回对应语言文本', 20 | ), 21 | }), 22 | }, 23 | }; 24 | export const mdmV3CountryRegionList = { 25 | project: 'mdm', 26 | name: 'mdm.v3.countryRegion.list', 27 | sdkName: 'mdm.v3.countryRegion.list', 28 | path: '/open-apis/mdm/v3/country_regions', 29 | httpMethod: 'GET', 30 | description: 31 | '[Feishu/Lark]-飞书主数据-基础数据-国家/地区-分页批量查询国家/地区-分页批量查询国家/地区。资源介绍请参考[概述]', 32 | accessTokens: ['tenant'], 33 | schema: { 34 | data: z 35 | .object({ 36 | filter: z 37 | .object({ 38 | logic: z.string().describe('逻辑关系同一层级的多个expression由logic参数决定使用“与/或”条件0=and,1=or'), 39 | expressions: z 40 | .array( 41 | z.object({ 42 | field: z.string().describe('字段名'), 43 | operator: z 44 | .string() 45 | .describe( 46 | '运算符。0=等于,1=不等于,2=大于,3=大于等于,4=小于,5=小于等于,6=属于任意,7=不属于任意,8=匹配,9=前缀匹配,10=后缀匹配,11=为空,12=不为空', 47 | ), 48 | value: z 49 | .object({ 50 | string_value: z.string().describe('字符串值').optional(), 51 | bool_value: z.boolean().describe('布尔值').optional(), 52 | int_value: z.string().describe('整型值').optional(), 53 | string_list_value: z.array(z.string()).describe('字符串列表值').optional(), 54 | int_list_value: z.array(z.string()).describe('整型列表值').optional(), 55 | }) 56 | .describe('字段值'), 57 | }), 58 | ) 59 | .describe('过滤条件') 60 | .optional(), 61 | }) 62 | .describe('过滤参数') 63 | .optional(), 64 | common: z.object({}).describe('此参数可忽略').optional(), 65 | }) 66 | .optional(), 67 | params: z.object({ 68 | languages: z 69 | .array(z.string().describe('语言')) 70 | .describe( 71 | '希望返回的语言种类,支持格式如下:- 中文:zh-CN- 英文:en-US- 日文:ja-JP对于多语文本字段,传入特定语言,将会返回对应语言文本', 72 | ), 73 | fields: z.array(z.string()).describe('需要的查询字段集'), 74 | limit: z.number().describe('查询页大小').optional(), 75 | offset: z.number().describe('查询起始位置').optional(), 76 | return_count: z.boolean().describe('是否返回总数').optional(), 77 | page_token: z 78 | .string() 79 | .describe( 80 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 81 | ) 82 | .optional(), 83 | }), 84 | }, 85 | }; 86 | export const mdmV3Tools = [mdmV3BatchCountryRegionGet, mdmV3CountryRegionList]; 87 | -------------------------------------------------------------------------------- /src/mcp-server/transport/streamable.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; 3 | import { InitTransportServerFunction } from '../shared'; 4 | import { parseMCPServerOptionsFromRequest, sendJsonRpcError } from './utils'; 5 | import { LarkAuthHandler } from '../../auth'; 6 | import { logger } from '../../utils/logger'; 7 | 8 | export const initStreamableServer: InitTransportServerFunction = ( 9 | getNewServer, 10 | options, 11 | { needAuthFlow } = { needAuthFlow: false }, 12 | ) => { 13 | const { userAccessToken, oauth, port, host } = options; 14 | 15 | if (!port || !host) { 16 | throw new Error('[Lark MCP] Port and host are required'); 17 | } 18 | 19 | const app = express(); 20 | app.use(express.json()); 21 | 22 | let authHandler: LarkAuthHandler | undefined; 23 | 24 | if (!userAccessToken && needAuthFlow) { 25 | authHandler = new LarkAuthHandler(app, options); 26 | if (oauth) { 27 | authHandler.setupRoutes(); 28 | } 29 | } 30 | 31 | const authMiddleware = (req: Request, res: Response, next: NextFunction) => { 32 | if (authHandler && oauth) { 33 | authHandler.authenticateRequest(req, res, next); 34 | } else { 35 | const authToken = req.headers.authorization?.split(' ')[1]; 36 | if (authToken) { 37 | req.auth = { token: authToken, clientId: 'client_id_for_local_auth', scopes: [] }; 38 | } 39 | next(); 40 | } 41 | }; 42 | 43 | app.post('/mcp', authMiddleware, async (req: Request, res: Response) => { 44 | const token = req.auth?.token; 45 | const { data } = parseMCPServerOptionsFromRequest(req); 46 | const server = getNewServer({ ...options, ...data, userAccessToken: data.userAccessToken || token }, authHandler); 47 | const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); 48 | res.on('close', () => { 49 | transport.close(); 50 | server.close(); 51 | }); 52 | 53 | await server.connect(transport); 54 | await transport.handleRequest(req, res, req.body); 55 | }); 56 | 57 | const handleMethodNotAllowed = async (_req: Request, res: Response) => { 58 | res 59 | .writeHead(405) 60 | .end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null })); 61 | }; 62 | 63 | app.get('/mcp', async (req: Request, res: Response) => { 64 | try { 65 | console.log('Received GET MCP request'); 66 | logger.info(`[StreamableServerTransport] Received GET MCP request`); 67 | await handleMethodNotAllowed(req, res); 68 | } catch (error) { 69 | sendJsonRpcError(res, error as Error); 70 | } 71 | }); 72 | 73 | app.delete('/mcp', async (req: Request, res: Response) => { 74 | try { 75 | console.log('Received DELETE MCP request'); 76 | logger.info(`[StreamableServerTransport] Received DELETE MCP request`); 77 | await handleMethodNotAllowed(req, res); 78 | } catch (error) { 79 | sendJsonRpcError(res, error as Error); 80 | } 81 | }); 82 | 83 | app.listen(port, host, (error) => { 84 | if (error) { 85 | logger.error(`[StreamableServerTransport] Server error: ${error}`); 86 | process.exit(1); 87 | } 88 | console.log(`📡 Streamable endpoint: http://${host}:${port}/mcp`); 89 | logger.info(`[StreamableServerTransport] Streamable endpoint: http://${host}:${port}/mcp`); 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.1 2 | - Fix: 修复login再次登录的时候没有正确唤起授权 3 | 4 | - Fix: Resolved the issue where the authorization process was not properly triggered during re-login. 5 | 6 | # 0.5.0 7 | - Feat: 使用Login登录会自动用浏览器打开登录链接,且login不在会判断是否已经登录,再次登录会直接用新的token覆盖旧的token 8 | - Fix: SSE/Streamable 模式下未开启oauth错误需要鉴权流程的问题 9 | - Chore:优化鉴权失败的文案,和 keytar 不可用时的文案 10 | - Chore: 同步最新Open API, 移除了Helpdesk部分不可用的API 11 | 12 | - Feat: Using Login to log in will automatically open the login link in the browser, and if login is not present, it will check whether you have already logged in. Logging in again will directly overwrite the old token with a new one. 13 | - Fix: The issue where the unopened oauth error in SSE/Streamable mode requires an authentication process. 14 | - Chore: Optimize the copy for authentication failure and the copy when keytar is unavailable. 15 | - Chore: Sync the latest Open API. 16 | 17 | # 0.4.1 18 | Fix: 开放平台开发文档检索 MCP 错误需要鉴权流程的问题 19 | Fix: Fixed authentication process issue in Open Platform Development Documentation Retrieval MCP 20 | 21 | # 0.4.0 22 | Feat: 新增 StreamableHttp 的传输模式 23 | Feat: StreamableHttp/SSE 支持 [MCP Auth](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) 24 | Feat: Stdio(本地) 模式支持 login 和 logout 命令登录登出和自动使用 refresh_token 刷新 25 | Fix: 修复 TokenMode=Auto 模式下没有设置UserAccessToken且CallTool传递参数useUAT=true依然使用应用身份 26 | Bump: 升级 @modelcontextprotocol/sdk 到 1.12.1 27 | BREAK: 由于升级了 @modelcontextprotocol/sdk,最低兼容 Node 版本调整为 Node 20 28 | 29 | Feat: Added StreamableHttp transport mode 30 | Feat: StreamableHttp/SSE supports [MCP Auth](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) 31 | Feat: Stdio (local) mode supports login and logout commands for authentication and automatic refresh_token renewal 32 | Fix: Fixed issue where TokenMode=Auto would still use app identity when UserAccessToken is not set but CallTool parameter useUAT=true 33 | Bump: Upgraded @modelcontextprotocol/sdk to 1.12.1 34 | BREAK: Due to @modelcontextprotocol/sdk upgrade, minimum compatible Node version is now Node 20 35 | 36 | 37 | # 0.3.1 38 | Fix: 修复使用 configFile 配置 mode 参数不生效的问题 39 | Fix: 修复由于使用了z.record(z.any())类型的字段导致直接传给豆包模型无法使用的问题 40 | Feat: 新增 preset.light 预设 41 | 42 | Fix: Fix the problem that the mode parameter configured by configFile does not take effect 43 | Fix: Fix the problem that the z.record(z.any()) type field is passed directly to the doubao model and cannot be used 44 | Feat: Add preset.light preset 45 | 46 | # 0.3.0 47 | 48 | New: 开放平台开发文档检索 MCP,旨在帮助用户输入自身诉求后迅速检索到自己需要的开发文档,帮助开发者在AI IDE中编写与飞书集成的代码 49 | New: 新增--token-mode,现在可以在启动的时候指定调用API的token类型,支持auto/tenant_access_token/user_access_token 50 | New: -t 支持配置 preset.default preset.im.default preset.bitable.default preset.doc.default 等默认预设 51 | Bump: 升级 @modelcontextprotocol/sdk 到 1.11.0 52 | 53 | New:Retrieval of Open Platform Development Documents in MCP aims to enable users to quickly find the development documents they need after inputting their own requirements, and assist developers in writing code integrated with Feishu in the AI IDE. 54 | New: Added --token-mode, now you can specify the API token type when starting, supporting auto/tenant_access_token/user_access_token 55 | New: -t supports configuring preset.default preset.im.default preset.bitable.default preset.doc.default etc. 56 | Bump: Upgraded @modelcontextprotocol/sdk to 1.11.0 57 | 58 | # 0.2.0 59 | 60 | 飞书/Lark OpenAPI MCP 工具,可以帮助你快速开始使用MCP协议连接飞书/Lark,实现 Agent 与飞书/Lark平台的高效协作 61 | 62 | Feishu/Lark OpenAPI MCP tool helps you quickly start using the MCP protocol to connect with Feishu/Lark, enabling efficient collaboration between Agent and the Feishu/Lark platform -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/speech_to_text_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type speechToTextV1ToolName = 3 | | 'speech_to_text.v1.speech.fileRecognize' 4 | | 'speech_to_text.v1.speech.streamRecognize'; 5 | export const speechToTextV1SpeechFileRecognize = { 6 | project: 'speech_to_text', 7 | name: 'speech_to_text.v1.speech.fileRecognize', 8 | sdkName: 'speech_to_text.v1.speech.fileRecognize', 9 | path: '/open-apis/speech_to_text/v1/speech/file_recognize', 10 | httpMethod: 'POST', 11 | description: 12 | '[Feishu/Lark]-AI-Speech recognition-Audio file speech recognition-An audio speech recognition API is provided to recognize the entire audio file (less than 60s) at one time', 13 | accessTokens: ['tenant'], 14 | schema: { 15 | data: z.object({ 16 | speech: z 17 | .object({ speech: z.string().describe('Content of the base64-encoded audio file').optional() }) 18 | .describe('Audio resources'), 19 | config: z 20 | .object({ 21 | file_id: z 22 | .string() 23 | .describe( 24 | 'A 16-bit string generated by a user to identify a file. The string can only contain letters, numbers, and underscores', 25 | ), 26 | format: z.string().describe('Audio format. Only pcm supported'), 27 | engine_type: z 28 | .string() 29 | .describe('Engine type. Only 16k_auto that allows a mix of Chinese and English is supported'), 30 | }) 31 | .describe('Configures properties'), 32 | }), 33 | }, 34 | }; 35 | export const speechToTextV1SpeechStreamRecognize = { 36 | project: 'speech_to_text', 37 | name: 'speech_to_text.v1.speech.streamRecognize', 38 | sdkName: 'speech_to_text.v1.speech.streamRecognize', 39 | path: '/open-apis/speech_to_text/v1/speech/stream_recognize', 40 | httpMethod: 'POST', 41 | description: 42 | '[Feishu/Lark]-AI-Speech recognition-Streaming speech recognition-A streaming speech recognition API is provided to input an audio clip by clip and receive recognition results in real time. Each audio clip is recommended to be within 100 to 200 ms', 43 | accessTokens: ['tenant'], 44 | schema: { 45 | data: z.object({ 46 | speech: z 47 | .object({ speech: z.string().describe('Content of the base64-encoded audio file').optional() }) 48 | .describe('Audio resources'), 49 | config: z 50 | .object({ 51 | stream_id: z 52 | .string() 53 | .describe( 54 | 'A 16-bit string generated by a user to identify the same data stream. The string can only contain letters, numbers, and underscores', 55 | ), 56 | sequence_id: z 57 | .number() 58 | .describe('Sequence number of a data stream clip, starting from 0 and incremented by 1 for each request'), 59 | action: z 60 | .number() 61 | .describe( 62 | 'Indicates the data packet sent to the API in a request. 1: The first packet. 2: The last packet. A result will be returned. 3: The cancelled data packets,no final result is returned. 0: Transfer data packets in the middle of voice transmission', 63 | ), 64 | format: z.string().describe('Audio format. Only pcm supported'), 65 | engine_type: z 66 | .string() 67 | .describe('Engine type. Only 16k_auto that allows a mix of Chinese and English is supported'), 68 | }) 69 | .describe('Configures properties'), 70 | }), 71 | }, 72 | }; 73 | export const speechToTextV1Tools = [speechToTextV1SpeechFileRecognize, speechToTextV1SpeechStreamRecognize]; 74 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/zh/gen-tools/zod/workplace_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type workplaceV1ToolName = 3 | | 'workplace.v1.customWorkplaceAccessData.search' 4 | | 'workplace.v1.workplaceAccessData.search' 5 | | 'workplace.v1.workplaceBlockAccessData.search'; 6 | export const workplaceV1CustomWorkplaceAccessDataSearch = { 7 | project: 'workplace', 8 | name: 'workplace.v1.customWorkplaceAccessData.search', 9 | sdkName: 'workplace.v1.customWorkplaceAccessData.search', 10 | path: '/open-apis/workplace/v1/custom_workplace_access_data/search', 11 | httpMethod: 'POST', 12 | description: '[Feishu/Lark]-工作台-工作台访问数据-获取定制工作台访问数据-获取定制工作台访问数据', 13 | accessTokens: ['tenant'], 14 | schema: { 15 | params: z.object({ 16 | from_date: z.string().describe('数据检索开始时间,精确到日。格式yyyy-MM-dd'), 17 | to_date: z.string().describe('数据检索结束时间,精确到日。格式yyyy-MM-dd'), 18 | page_size: z.number().describe('分页大小,最小为 1,最大为 200,默认为 20'), 19 | page_token: z 20 | .string() 21 | .describe( 22 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 23 | ) 24 | .optional(), 25 | custom_workplace_id: z 26 | .string() 27 | .describe( 28 | '定制工作台id,非必填。不填时,返回所有定制工作台数据。如何获取定制工作台ID:可前往 飞书管理后台 > 工作台 > 定制工作台,点击指定工作台的 设置 进入设置页面;鼠标连续点击三次顶部的 设置 字样即可出现 ID,复制 ID 即可', 29 | ) 30 | .optional(), 31 | }), 32 | }, 33 | }; 34 | export const workplaceV1WorkplaceAccessDataSearch = { 35 | project: 'workplace', 36 | name: 'workplace.v1.workplaceAccessData.search', 37 | sdkName: 'workplace.v1.workplaceAccessData.search', 38 | path: '/open-apis/workplace/v1/workplace_access_data/search', 39 | httpMethod: 'POST', 40 | description: 41 | '[Feishu/Lark]-工作台-工作台访问数据-获取工作台访问数据-获取工作台访问数据(包含默认工作台与定制工作台)', 42 | accessTokens: ['tenant'], 43 | schema: { 44 | params: z.object({ 45 | from_date: z.string().describe('数据检索开始时间,精确到日。格式yyyy-MM-dd'), 46 | to_date: z.string().describe('数据检索结束时间,精确到日。格式yyyy-MM-dd'), 47 | page_size: z.number().describe('分页大小,最小为 1,最大为 200,默认为 20'), 48 | page_token: z 49 | .string() 50 | .describe( 51 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 52 | ) 53 | .optional(), 54 | }), 55 | }, 56 | }; 57 | export const workplaceV1WorkplaceBlockAccessDataSearch = { 58 | project: 'workplace', 59 | name: 'workplace.v1.workplaceBlockAccessData.search', 60 | sdkName: 'workplace.v1.workplaceBlockAccessData.search', 61 | path: '/open-apis/workplace/v1/workplace_block_access_data/search', 62 | httpMethod: 'POST', 63 | description: '[Feishu/Lark]-工作台-工作台访问数据-获取定制工作台小组件访问数据-获取定制工作台小组件访问数据', 64 | accessTokens: ['tenant'], 65 | schema: { 66 | params: z.object({ 67 | from_date: z.string().describe('数据检索开始时间,精确到日。格式yyyy-MM-dd'), 68 | to_date: z.string().describe('数据检索结束时间,精确到日。格式yyyy-MM-dd'), 69 | page_size: z.number().describe('分页大小,最小为 1,最大为 200,默认为 20'), 70 | page_token: z 71 | .string() 72 | .describe( 73 | '分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果', 74 | ) 75 | .optional(), 76 | block_id: z 77 | .string() 78 | .describe( 79 | '小组件id(BlockID)。可前往 飞书管理后台 > 工作台 > 定制工作台,选择指定的工作台并进入工作台编辑器,点击某个小组件,即可查看页面右侧面板中该小组件名称下方的“BlockID”', 80 | ) 81 | .optional(), 82 | }), 83 | }, 84 | }; 85 | export const workplaceV1Tools = [ 86 | workplaceV1CustomWorkplaceAccessDataSearch, 87 | workplaceV1WorkplaceAccessDataSearch, 88 | workplaceV1WorkplaceBlockAccessDataSearch, 89 | ]; 90 | -------------------------------------------------------------------------------- /tests/mcp-tool/document-tool/recall/index.test.ts: -------------------------------------------------------------------------------- 1 | import { RecallTool } from '../../../../src/mcp-tool/document-tool/recall'; 2 | import * as request from '../../../../src/mcp-tool/document-tool/recall/request'; 3 | import { DocumentRecallToolOptions } from '../../../../src/mcp-tool/document-tool/recall/type'; 4 | 5 | // 模拟请求模块 6 | jest.mock('../../../../src/mcp-tool/document-tool/recall/request'); 7 | 8 | describe('RecallTool', () => { 9 | // 获取模拟函数 10 | const mockedRecallDeveloperDocument = request.recallDeveloperDocument as jest.Mock; 11 | 12 | beforeEach(() => { 13 | // 重置所有模拟 14 | jest.clearAllMocks(); 15 | }); 16 | 17 | it('应该具有正确的名称和描述', () => { 18 | expect(RecallTool.name).toBe('openplatform_developer_document_recall'); 19 | expect(RecallTool.description).toMatch(/Recall for relevant documents/); 20 | expect(RecallTool.schema).toBeDefined(); 21 | }); 22 | 23 | it('应该处理正常结果', async () => { 24 | // 模拟搜索结果 25 | const mockResults = ['result1', 'result2', 'result3']; 26 | mockedRecallDeveloperDocument.mockResolvedValueOnce(mockResults); 27 | 28 | // 调用参数 29 | const params = { query: 'test query' }; 30 | const options: DocumentRecallToolOptions = { domain: 'https://example.com' }; 31 | 32 | // 执行处理程序 33 | const result = await RecallTool.handler(params, options); 34 | 35 | // 验证结果 36 | expect(mockedRecallDeveloperDocument).toHaveBeenCalledWith('test query', options); 37 | expect(result).toEqual({ 38 | content: [ 39 | { 40 | type: 'text', 41 | text: `Find 3 results:\n${mockResults.join('\n\n')}`, 42 | }, 43 | ], 44 | }); 45 | }); 46 | 47 | it('应该处理空结果', async () => { 48 | // 模拟空搜索结果 49 | mockedRecallDeveloperDocument.mockResolvedValueOnce([]); 50 | 51 | // 调用参数 52 | const params = { query: 'test query' }; 53 | const options: DocumentRecallToolOptions = { domain: 'https://example.com' }; 54 | 55 | // 执行处理程序 56 | const result = await RecallTool.handler(params, options); 57 | 58 | // 验证结果 59 | expect(result).toEqual({ 60 | content: [ 61 | { 62 | type: 'text', 63 | text: 'No results found', 64 | }, 65 | ], 66 | }); 67 | }); 68 | 69 | it('应该处理错误情况', async () => { 70 | // 模拟错误 71 | const mockError = new Error('API error'); 72 | mockedRecallDeveloperDocument.mockRejectedValueOnce(mockError); 73 | 74 | // 调用参数 75 | const params = { query: 'test query' }; 76 | const options: DocumentRecallToolOptions = { domain: 'https://example.com' }; 77 | 78 | // 执行处理程序 79 | const result = await RecallTool.handler(params, options); 80 | 81 | // 验证结果 82 | expect(result).toEqual({ 83 | isError: true, 84 | content: [ 85 | { 86 | type: 'text', 87 | text: 'Search failed:API error', 88 | }, 89 | ], 90 | }); 91 | }); 92 | 93 | it('应该处理未知错误类型', async () => { 94 | // 模拟非Error类型的错误 95 | mockedRecallDeveloperDocument.mockRejectedValueOnce('String error'); 96 | 97 | // 调用参数 98 | const params = { query: 'test query' }; 99 | const options: DocumentRecallToolOptions = { domain: 'https://example.com' }; 100 | 101 | // 执行处理程序 102 | const result = await RecallTool.handler(params, options); 103 | 104 | // 验证结果 105 | expect(result).toEqual({ 106 | isError: true, 107 | content: [ 108 | { 109 | type: 'text', 110 | text: 'Search failed:Unknown error', 111 | }, 112 | ], 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/ehr_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type ehrV1ToolName = 'ehr.v1.employee.list'; 3 | export const ehrV1EmployeeList = { 4 | project: 'ehr', 5 | name: 'ehr.v1.employee.list', 6 | sdkName: 'ehr.v1.employee.list', 7 | path: '/open-apis/ehr/v1/employees', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Feishu CoreHR - (Standard version)-Employee directory information batch retrieval-You can batch retrieve employee directory fields with filters, such as Feishu user ID, employee status, and employee type. Directory fields can be divided into system fields (system_fields) and custom fields (custom_fields)', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z 14 | .object({ 15 | view: z 16 | .enum(['basic', 'full']) 17 | .describe( 18 | 'Data range. If no value is passed, it defaults to basic. Options:basic(Overview. Only the ID, name, and other basic information will be returned.),full(Details. A combination of system fields and custom fields will be returned.)', 19 | ) 20 | .optional(), 21 | status: z 22 | .array( 23 | z 24 | .number() 25 | .describe( 26 | 'Options:1(to_be_onboarded To be onboarded),2(active),3(onboarding_cancelled Onboarding cancelle),4(offboarding),5(offboarded)', 27 | ), 28 | ) 29 | .describe( 30 | 'Employee statusAll employee statuses will be returned by default if this attribute is not specified.Currently active employees = 2&4Multiple status records can be queried at the same time, such as status=2&status=4', 31 | ) 32 | .optional(), 33 | type: z 34 | .array( 35 | z 36 | .number() 37 | .describe( 38 | 'Options:1(regular To be onboarded),2(intern Active),3(consultant Onboard canceled),4(outsourcing Offboarding),5(contractor Offboarded)', 39 | ), 40 | ) 41 | .describe( 42 | 'Employee typeAll employee types will be returned by default if this attribute is not specified.You can use the int value of the custom employee type for search, and the name of the custom employee type for the tenant can be obtained through the API: [Obtain workforce type]', 43 | ) 44 | .optional(), 45 | start_time: z.string().describe('Query start time(Hire time >= this time)').optional(), 46 | end_time: z.string().describe('Query end time(Hire time >= this time)').optional(), 47 | user_id_type: z.enum(['open_id', 'union_id', 'user_id']).describe('User ID type').optional(), 48 | user_ids: z 49 | .array(z.string()) 50 | .describe( 51 | 'user_id, open_id, or union_id. The "open_id" will be returned by default.If the passed value is not an "open_id" , you need to pass the "user_id_type" parameter together.You can query users with multiple ids at once, for example: user_ids=ou_8ebd4f35d7101ffdeb4771d7c8ec517e&user_ids=ou_7abc4f35d7101ffdeb4771dabcde[The ID concepts]', 52 | ) 53 | .optional(), 54 | page_token: z 55 | .string() 56 | .describe( 57 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 58 | ) 59 | .optional(), 60 | page_size: z.number().describe('Page size, value range 1~ 100, default 10').optional(), 61 | }) 62 | .optional(), 63 | }, 64 | }; 65 | export const ehrV1Tools = [ehrV1EmployeeList]; 66 | -------------------------------------------------------------------------------- /src/mcp-tool/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Commonly used tools in MCP 3 | */ 4 | 5 | import { ToolName } from './tools'; 6 | 7 | export enum PresetName { 8 | /** 9 | * Default preset including IM, Bitable, Doc and Contact tools 10 | */ 11 | LIGHT = 'preset.light', 12 | /** 13 | * Default preset including IM, Bitable, Doc and Contact tools 14 | */ 15 | DEFAULT = 'preset.default', 16 | /** 17 | * IM related tools for chat and message operations 18 | */ 19 | IM_DEFAULT = 'preset.im.default', 20 | /** 21 | * Base preset for base operations 22 | */ 23 | BASE_DEFAULT = 'preset.base.default', 24 | /** 25 | * Base tools with batch operations 26 | */ 27 | BASE_BATCH = 'preset.base.batch', 28 | /** 29 | * Document related tools for content and permission operations 30 | */ 31 | DOC_DEFAULT = 'preset.doc.default', 32 | /** 33 | * Task management related tools 34 | */ 35 | TASK_DEFAULT = 'preset.task.default', 36 | /** 37 | * Calendar event management tools 38 | */ 39 | CALENDAR_DEFAULT = 'preset.calendar.default', 40 | } 41 | 42 | export const presetLightToolNames: ToolName[] = [ 43 | 'im.v1.message.list', 44 | 'im.v1.message.create', 45 | 'im.v1.chat.search', 46 | 'contact.v3.user.batchGetId', 47 | 'docx.v1.document.rawContent', 48 | 'docx.builtin.import', 49 | 'docx.builtin.search', 50 | 'wiki.v2.space.getNode', 51 | 'bitable.v1.appTableRecord.search', 52 | 'bitable.v1.appTableRecord.batchCreate', 53 | ]; 54 | 55 | export const presetContactToolNames: ToolName[] = ['contact.v3.user.batchGetId']; 56 | 57 | export const presetImToolNames: ToolName[] = [ 58 | 'im.v1.chat.create', 59 | 'im.v1.chat.list', 60 | 'im.v1.chatMembers.get', 61 | 'im.v1.message.create', 62 | 'im.v1.message.list', 63 | ]; 64 | 65 | export const presetBaseCommonToolNames: ToolName[] = [ 66 | 'bitable.v1.app.create', 67 | 'bitable.v1.appTable.create', 68 | 'bitable.v1.appTable.list', 69 | 'bitable.v1.appTableField.list', 70 | 'bitable.v1.appTableRecord.search', 71 | ]; 72 | 73 | export const presetBaseToolNames: ToolName[] = [ 74 | ...presetBaseCommonToolNames, 75 | 'bitable.v1.appTableRecord.create', 76 | 'bitable.v1.appTableRecord.update', 77 | ]; 78 | 79 | export const presetBaseRecordBatchToolNames: ToolName[] = [ 80 | ...presetBaseCommonToolNames, 81 | 'bitable.v1.appTableRecord.batchCreate', 82 | 'bitable.v1.appTableRecord.batchUpdate', 83 | ]; 84 | 85 | export const presetDocToolNames: ToolName[] = [ 86 | 'docx.v1.document.rawContent', 87 | 'docx.builtin.import', 88 | 'docx.builtin.search', 89 | 'drive.v1.permissionMember.create', 90 | 'wiki.v2.space.getNode', 91 | 'wiki.v1.node.search', 92 | ]; 93 | 94 | export const presetTaskToolNames: ToolName[] = [ 95 | 'task.v2.task.create', 96 | 'task.v2.task.patch', 97 | 'task.v2.task.addMembers', 98 | 'task.v2.task.addReminders', 99 | ]; 100 | 101 | export const presetCalendarToolNames: ToolName[] = [ 102 | 'calendar.v4.calendarEvent.create', 103 | 'calendar.v4.calendarEvent.patch', 104 | 'calendar.v4.calendarEvent.get', 105 | 'calendar.v4.freebusy.list', 106 | 'calendar.v4.calendar.primary', 107 | ]; 108 | 109 | export const defaultToolNames: ToolName[] = [ 110 | ...presetImToolNames, 111 | ...presetBaseToolNames, 112 | ...presetDocToolNames, 113 | ...presetContactToolNames, 114 | ]; 115 | 116 | export const presetTools: Record = { 117 | [PresetName.LIGHT]: presetLightToolNames, 118 | [PresetName.DEFAULT]: defaultToolNames, 119 | [PresetName.IM_DEFAULT]: presetImToolNames, 120 | [PresetName.BASE_DEFAULT]: presetBaseToolNames, 121 | [PresetName.BASE_BATCH]: presetBaseRecordBatchToolNames, 122 | [PresetName.DOC_DEFAULT]: presetDocToolNames, 123 | [PresetName.TASK_DEFAULT]: presetTaskToolNames, 124 | [PresetName.CALENDAR_DEFAULT]: presetCalendarToolNames, 125 | }; 126 | -------------------------------------------------------------------------------- /tests/mcp-tool/tools/additional-coverage.test.ts: -------------------------------------------------------------------------------- 1 | import { getShouldUseUAT } from '../../../src/mcp-tool/utils/get-should-use-uat'; 2 | import { caseTransf } from '../../../src/mcp-tool/utils/case-transf'; 3 | import { cleanEnvArgs } from '../../../src/utils/clean-env-args'; 4 | import { TokenMode } from '../../../src/mcp-tool/types'; 5 | 6 | describe('Additional Coverage Tests', () => { 7 | describe('getShouldUseUAT', () => { 8 | it('should return true for user access token mode', () => { 9 | const result = getShouldUseUAT(TokenMode.USER_ACCESS_TOKEN); 10 | expect(result).toBe(true); 11 | }); 12 | 13 | it('should return false for tenant access token mode', () => { 14 | const result = getShouldUseUAT(TokenMode.TENANT_ACCESS_TOKEN); 15 | expect(result).toBe(false); 16 | }); 17 | 18 | it('should return true for auto mode with useUAT=true', () => { 19 | const result = getShouldUseUAT(TokenMode.AUTO, true); 20 | expect(result).toBe(true); 21 | }); 22 | 23 | it('should return false for auto mode with useUAT=false', () => { 24 | const result = getShouldUseUAT(TokenMode.AUTO, false); 25 | expect(result).toBe(false); 26 | }); 27 | 28 | it('should return undefined for auto mode with useUAT=undefined', () => { 29 | const result = getShouldUseUAT(TokenMode.AUTO, undefined); 30 | expect(result).toBe(undefined); 31 | }); 32 | 33 | it('should handle default auto mode', () => { 34 | const result = getShouldUseUAT(TokenMode.AUTO); 35 | expect(result).toBe(undefined); 36 | }); 37 | }); 38 | 39 | describe('caseTransf', () => { 40 | it('should transform to snake_case', () => { 41 | const result = caseTransf('test.tool.name', 'snake'); 42 | expect(result).toBe('test_tool_name'); 43 | }); 44 | 45 | it('should transform to camelCase', () => { 46 | const result = caseTransf('test.tool.name', 'camel'); 47 | expect(result).toBe('testToolName'); 48 | }); 49 | 50 | it('should transform to kebab-case', () => { 51 | const result = caseTransf('test.tool.name', 'kebab'); 52 | expect(result).toBe('test-tool-name'); 53 | }); 54 | 55 | it('should return original name when no case type provided', () => { 56 | const result = caseTransf('test.tool.name'); 57 | expect(result).toBe('test.tool.name'); 58 | }); 59 | 60 | it('should handle empty string', () => { 61 | const result = caseTransf('', 'snake'); 62 | expect(result).toBe(''); 63 | }); 64 | }); 65 | 66 | describe('cleanEnvArgs', () => { 67 | it('should remove undefined values and empty strings', () => { 68 | const args: Record = { 69 | defined: 'value', 70 | undefined: undefined, 71 | empty: '', 72 | anotherDefined: 'value2', 73 | }; 74 | const result = cleanEnvArgs(args); 75 | expect(result).toEqual({ 76 | defined: 'value', 77 | anotherDefined: 'value2', 78 | }); 79 | }); 80 | 81 | it('should handle empty object', () => { 82 | const result = cleanEnvArgs({}); 83 | expect(result).toEqual({}); 84 | }); 85 | 86 | it('should handle object with all undefined values', () => { 87 | const args: Record = { 88 | a: undefined, 89 | b: undefined, 90 | }; 91 | const result = cleanEnvArgs(args); 92 | expect(result).toEqual({}); 93 | }); 94 | 95 | it('should only keep truthy values', () => { 96 | const args: Record = { 97 | empty: '', 98 | defined: 'value', 99 | undefined: undefined, 100 | whitespace: ' ', 101 | }; 102 | const result = cleanEnvArgs(args); 103 | expect(result).toEqual({ 104 | defined: 'value', 105 | whitespace: ' ', 106 | }); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/mcp-server/shared/init.ts: -------------------------------------------------------------------------------- 1 | import * as larkmcp from '../../mcp-tool'; 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import { initStdioServer, initSSEServer, initStreamableServer } from '../transport'; 4 | import { McpServerOptions, McpServerType } from './types'; 5 | import { noop } from '../../utils/noop'; 6 | import { currentVersion } from '../../utils/version'; 7 | import { oapiHttpInstance } from '../../utils/http-instance'; 8 | import { LarkAuthHandler } from '../../auth'; 9 | import { logger } from '../../utils/logger'; 10 | 11 | export function initOAPIMcpServer(options: McpServerOptions, authHandler?: LarkAuthHandler) { 12 | const { appId, appSecret, userAccessToken, tokenMode, domain, oauth } = options; 13 | 14 | if (!appId || !appSecret) { 15 | console.error('Error: Missing App Credentials'); 16 | throw new Error('Missing App Credentials'); 17 | } 18 | 19 | let allowTools = options.tools || []; 20 | 21 | for (const [presetName, presetTools] of Object.entries(larkmcp.presetTools)) { 22 | if (allowTools.includes(presetName)) { 23 | allowTools = [...presetTools, ...allowTools]; 24 | } 25 | } 26 | 27 | // Unique 28 | allowTools = Array.from(new Set(allowTools)); 29 | 30 | // Create MCP Server 31 | const mcpServer = new McpServer({ id: 'lark-mcp-server', name: 'Feishu/Lark MCP Server', version: currentVersion }); 32 | 33 | const toolsOptions = allowTools.length 34 | ? { allowTools: allowTools as larkmcp.ToolName[], language: options.language } 35 | : { language: options.language }; 36 | 37 | const larkClient = new larkmcp.LarkMcpTool( 38 | { 39 | appId, 40 | appSecret, 41 | logger: { warn: noop, error: noop, debug: noop, info: noop, trace: noop }, 42 | httpInstance: oapiHttpInstance, 43 | domain, 44 | toolsOptions, 45 | tokenMode, 46 | oauth, 47 | }, 48 | authHandler, 49 | ); 50 | 51 | if (userAccessToken) { 52 | larkClient.updateUserAccessToken(userAccessToken); 53 | } 54 | 55 | larkClient.registerMcpServer(mcpServer, { toolNameCase: options.toolNameCase }); 56 | 57 | return { mcpServer, larkClient }; 58 | } 59 | 60 | export function initRecallMcpServer(options: McpServerOptions) { 61 | const server = new McpServer({ 62 | id: 'lark-recall-mcp-server', 63 | name: 'Lark Recall MCP Service', 64 | version: currentVersion, 65 | }); 66 | server.tool(larkmcp.RecallTool.name, larkmcp.RecallTool.description, larkmcp.RecallTool.schema, (params) => 67 | larkmcp.RecallTool.handler(params, options), 68 | ); 69 | return server; 70 | } 71 | 72 | export async function initMcpServerWithTransport(serverType: McpServerType, options: McpServerOptions) { 73 | const { mode, userAccessToken, oauth } = options; 74 | 75 | if (userAccessToken && oauth) { 76 | logger.error(`[initMcpServerWithTransport] userAccessToken and oauth cannot be used together`); 77 | throw new Error('userAccessToken and oauth cannot be used together'); 78 | } 79 | 80 | const getNewServer = (commonOptions?: McpServerOptions, authHandler?: LarkAuthHandler) => { 81 | if (serverType === 'oapi') { 82 | const { mcpServer } = initOAPIMcpServer({ ...options, ...commonOptions }, authHandler); 83 | return mcpServer; 84 | } else if (serverType === 'recall') { 85 | return initRecallMcpServer({ ...options, ...commonOptions }); 86 | } 87 | logger.error(`[initMcpServerWithTransport] Invalid server type: ${serverType}`); 88 | throw new Error('Invalid server type'); 89 | }; 90 | 91 | const needAuthFlow = serverType === 'oapi'; 92 | 93 | switch (mode) { 94 | case 'stdio': 95 | await initStdioServer(getNewServer, options, { needAuthFlow }); 96 | break; 97 | case 'sse': 98 | await initSSEServer(getNewServer, options, { needAuthFlow }); 99 | break; 100 | case 'streamable': 101 | await initStreamableServer(getNewServer, options, { needAuthFlow }); 102 | break; 103 | default: 104 | throw new Error('Invalid mode:' + mode); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /docs/reference/tool-presets/presets-zh.md: -------------------------------------------------------------------------------- 1 | # 预设工具集参考 2 | 3 | 本文档提供了 lark-mcp 中所有可用预设工具集的详细信息。预设是为特定用例预定义的工具集,可以一起启用。 4 | 5 | ## 概述 6 | 7 | 如无特殊需求,可保持默认 preset 即可使用常用功能。需要精细控制或了解完整列表时,可参考下面的预设表格。 8 | 9 | ## 如何使用预设 10 | 11 | 要使用预设,请在 `-t` 参数中指定: 12 | 13 | ```json 14 | { 15 | "mcpServers": { 16 | "lark-mcp": { 17 | "command": "npx", 18 | "args": [ 19 | "-y", 20 | "@larksuiteoapi/lark-mcp", 21 | "mcp", 22 | "-a", "", 23 | "-s", "", 24 | "-t", "preset.light" 25 | ] 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 您也可以将预设与单个工具组合: 32 | 33 | ```json 34 | { 35 | "mcpServers": { 36 | "lark-mcp": { 37 | "command": "npx", 38 | "args": [ 39 | "-y", 40 | "@larksuiteoapi/lark-mcp", 41 | "mcp", 42 | "-a", "", 43 | "-s", "", 44 | "-t", "preset.light,im.v1.message.create" 45 | ] 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## 预设工具集详表 52 | 53 | | 工具名称 | 功能描述 | preset.light | preset.default (默认) | preset.im.default | preset.base.default | preset.base.batch | preset.doc.default | preset.task.default | preset.calendar.default | 54 | | --- | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 55 | | im.v1.chat.create | 创建群 | | ✓ | ✓ | | | | | | 56 | | im.v1.chat.list | 获取群列表 | | ✓ | ✓ | | | | | | 57 | | im.v1.chat.search | 搜索群 | ✓ | | | | | | | | 58 | | im.v1.chatMembers.get | 获取群成员 | | ✓ | ✓ | | | | | | 59 | | im.v1.message.create | 发送消息 | ✓ | ✓ | ✓ | | | | | | 60 | | im.v1.message.list | 获取消息列表 | ✓ | ✓ | ✓ | | | | | | 61 | | bitable.v1.app.create | 创建多维表格 | | ✓ | | ✓ | ✓ | | | | 62 | | bitable.v1.appTable.create | 创建多维表格数据表 | | ✓ | | ✓ | ✓ | | | | 63 | | bitable.v1.appTable.list | 获取多维表格数据表列表 | | ✓ | | ✓ | ✓ | | | | 64 | | bitable.v1.appTableField.list | 获取多维表格数据表字段列表 | | ✓ | | ✓ | ✓ | | | | 65 | | bitable.v1.appTableRecord.search | 搜索多维表格数据表记录 | ✓ | ✓ | | ✓ | ✓ | | | | 66 | | bitable.v1.appTableRecord.create | 创建多维表格数据表记录 | | ✓ | | ✓ | | | | | 67 | | bitable.v1.appTableRecord.batchCreate | 批量创建多维表格数据表记录 | ✓ | | | | ✓ | | | | 68 | | bitable.v1.appTableRecord.update | 更新多维表格数据表记录 | | ✓ | | ✓ | | | | | 69 | | bitable.v1.appTableRecord.batchUpdate | 批量更新多维表格数据表记录 | | | | | ✓ | | | | 70 | | docx.v1.document.rawContent | 获取文档内容 | ✓ | ✓ | | | | ✓ | | | 71 | | docx.builtin.import | 导入文档 | ✓ | ✓ | | | | ✓ | | | 72 | | docx.builtin.search | 搜索文档 | ✓ | ✓ | | | | ✓ | | | 73 | | drive.v1.permissionMember.create | 添加协作者权限 | | ✓ | | | | ✓ | | | 74 | | wiki.v2.space.getNode | 获取知识库节点 | ✓ | ✓ | | | | ✓ | | | 75 | | wiki.v1.node.search | 搜索知识库节点 | | ✓ | | | | ✓ | | | 76 | | contact.v3.user.batchGetId | 批量获取用户ID | ✓ | ✓ | | | | | | | 77 | | task.v2.task.create | 创建任务 | | | | | | | ✓ | | 78 | | task.v2.task.patch | 修改任务 | | | | | | | ✓ | | 79 | | task.v2.task.addMembers | 添加任务成员 | | | | | | | ✓ | | 80 | | task.v2.task.addReminders | 添加任务提醒 | | | | | | | ✓ | | 81 | | calendar.v4.calendarEvent.create | 创建日历事件 | | | | | | | | ✓ | 82 | | calendar.v4.calendarEvent.patch | 修改日历事件 | | | | | | | | ✓ | 83 | | calendar.v4.calendarEvent.get | 获取日历事件 | | | | | | | | ✓ | 84 | | calendar.v4.freebusy.list | 查询忙闲状态 | | | | | | | | ✓ | 85 | | calendar.v4.calendar.primary | 获取主日历 | | | | | | | | ✓ | 86 | 87 | > **说明**:表格中"✓"表示该工具包含在对应的预设工具集中。使用`-t preset.xxx`参数时,会启用该列标有"✓"的工具。 88 | 89 | ## 预设说明 90 | 91 | ### preset.light 92 | 最小化预设,仅包含基本消息和文档操作的核心工具。适合轻量级集成。 93 | 94 | ### preset.default(默认) 95 | 默认预设,包含消息、文档、数据库和协作等常用工具。推荐大多数用户使用。 96 | 97 | ### preset.im.default 98 | 专注于即时消息功能,包括群聊创建、成员管理和消息处理。 99 | 100 | ### preset.base.default 101 | 包含多维表格的基础数据库操作,适用于数据管理场景。 102 | 103 | ### preset.base.batch 104 | 专门用于多维表格数据的批量操作,适合批量数据处理。 105 | 106 | ### preset.doc.default 107 | 以文档为中心的预设,包括文档读取、导入、搜索和协作功能。 108 | 109 | ### preset.task.default 110 | 任务管理预设,用于创建、修改和管理带有提醒和成员的任务。 111 | 112 | ### preset.calendar.default 113 | 日历管理预设,用于创建、修改事件和查询可用性。 114 | 115 | ## 相关文档 116 | 117 | - [主要文档](../../../README_ZH.md) 118 | - [工具参考](./tools-zh.md) 119 | - [配置指南](../../usage/configuration/configuration-zh.md) 120 | - [命令行参考](../cli/cli-zh.md) 121 | -------------------------------------------------------------------------------- /src/auth/utils/storage-manager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { EncryptionUtil } from './encryption'; 4 | import { AUTH_CONFIG } from '../config'; 5 | import { StorageData } from '../types'; 6 | import { logger } from '../../utils/logger'; 7 | 8 | export class StorageManager { 9 | private encryptionUtil: EncryptionUtil | undefined; 10 | private initializePromise: Promise | undefined; 11 | private isInitializedStorageSuccess = false; 12 | 13 | constructor() { 14 | this.initialize(); 15 | } 16 | 17 | get storageFile(): string { 18 | return path.join(AUTH_CONFIG.STORAGE_DIR, AUTH_CONFIG.STORAGE_FILE); 19 | } 20 | 21 | private async initialize(): Promise { 22 | if (this.initializePromise) { 23 | return this.initializePromise; 24 | } 25 | 26 | this.initializePromise = this.performInitialization(); 27 | 28 | await this.initializePromise; 29 | } 30 | 31 | private async performInitialization(): Promise { 32 | try { 33 | await this.initializeEncryption(); 34 | this.ensureStorageDir(); 35 | this.isInitializedStorageSuccess = true; 36 | } catch (error) { 37 | logger.warn(`[StorageManager] Failed to initialize: ${error}`); 38 | logger.warn( 39 | '[StorageManager] ⚠️ Builtin User Access Token Store will be disabled. but you can still use it with memory store', 40 | ); 41 | this.isInitializedStorageSuccess = false; 42 | } 43 | } 44 | 45 | private async initializeEncryption(): Promise { 46 | try { 47 | const keytar = await import('keytar'); 48 | let key = await keytar.getPassword(AUTH_CONFIG.SERVER_NAME, AUTH_CONFIG.AES_KEY_NAME); 49 | if (!key) { 50 | key = EncryptionUtil.generateKey(); 51 | await keytar.setPassword(AUTH_CONFIG.SERVER_NAME, AUTH_CONFIG.AES_KEY_NAME, key); 52 | } 53 | this.encryptionUtil = new EncryptionUtil(key); 54 | } catch (error) { 55 | logger.warn(`[StorageManager] Failed to initialize encryption: ${error}`); 56 | throw error; 57 | } 58 | } 59 | 60 | private ensureStorageDir(): void { 61 | if (!fs.existsSync(AUTH_CONFIG.STORAGE_DIR)) { 62 | fs.mkdirSync(AUTH_CONFIG.STORAGE_DIR, { recursive: true }); 63 | } 64 | } 65 | 66 | encrypt(data: string): string { 67 | if (!this.isInitializedStorageSuccess || !this.encryptionUtil) { 68 | throw new Error('StorageManager not initialized - call initialize() first'); 69 | } 70 | return this.encryptionUtil.encrypt(data); 71 | } 72 | 73 | decrypt(encryptedData: string): string { 74 | if (!this.isInitializedStorageSuccess || !this.encryptionUtil) { 75 | throw new Error('StorageManager not initialized - call initialize() first'); 76 | } 77 | return this.encryptionUtil.decrypt(encryptedData); 78 | } 79 | 80 | async loadStorageData(): Promise { 81 | await this.initialize(); 82 | if (!this.isInitializedStorageSuccess || !fs.existsSync(this.storageFile)) { 83 | return { tokens: {}, clients: {} }; 84 | } 85 | try { 86 | const data = fs.readFileSync(this.storageFile, 'utf8'); 87 | return data ? JSON.parse(this.decrypt(data)) : { tokens: {}, clients: {} }; 88 | } catch (error) { 89 | logger.error(`[StorageManager] Failed to load storage data: ${error}`); 90 | logger.error( 91 | '[StorageManager] ⚠️ Builtin User Access Token Store will be disabled. but you can still use it with memory store', 92 | ); 93 | return { tokens: {}, clients: {} }; 94 | } 95 | } 96 | 97 | async saveStorageData(data: StorageData): Promise { 98 | if (!this.isInitializedStorageSuccess) { 99 | return; 100 | } 101 | await this.initialize(); 102 | try { 103 | const encryptedData = this.encrypt(JSON.stringify(data, null, 2)); 104 | fs.writeFileSync(this.storageFile, encryptedData); 105 | } catch (error) { 106 | logger.error(`[StorageManager] Failed to save storage data: ${error}`); 107 | throw error; 108 | } 109 | } 110 | } 111 | 112 | export const storageManager = new StorageManager(); 113 | -------------------------------------------------------------------------------- /tests/utils/safe-json-parse.test.ts: -------------------------------------------------------------------------------- 1 | import { safeJsonParse } from '../../src/utils/safe-json-parse'; 2 | 3 | describe('safeJsonParse', () => { 4 | it('should return fallback for undefined input', () => { 5 | const fallback = { default: 'value' }; 6 | const result = safeJsonParse(undefined, fallback); 7 | expect(result).toBe(fallback); 8 | }); 9 | 10 | it('should return fallback for null input', () => { 11 | const fallback = { default: 'value' }; 12 | const result = safeJsonParse(null, fallback); 13 | expect(result).toBe(fallback); 14 | }); 15 | 16 | it('should return fallback for empty string input', () => { 17 | const fallback = { default: 'value' }; 18 | const result = safeJsonParse('', fallback); 19 | expect(result).toBe(fallback); 20 | }); 21 | 22 | it('should parse valid JSON string', () => { 23 | const jsonString = '{"name": "test", "value": 123}'; 24 | const fallback = { default: 'value' }; 25 | const result = safeJsonParse(jsonString, fallback); 26 | expect(result).toEqual({ name: 'test', value: 123 }); 27 | }); 28 | 29 | it('should parse valid JSON array', () => { 30 | const jsonString = '[1, 2, 3, "test"]'; 31 | const fallback: any[] = []; 32 | const result = safeJsonParse(jsonString, fallback); 33 | expect(result).toEqual([1, 2, 3, 'test']); 34 | }); 35 | 36 | it('should parse primitive JSON values', () => { 37 | expect(safeJsonParse('true', false)).toBe(true); 38 | expect(safeJsonParse('false', true)).toBe(false); 39 | expect(safeJsonParse('null', 'fallback')).toBe(null); 40 | expect(safeJsonParse('123', 0)).toBe(123); 41 | expect(safeJsonParse('"hello"', 'fallback')).toBe('hello'); 42 | }); 43 | 44 | it('should return fallback for invalid JSON', () => { 45 | const fallback = { error: 'invalid' }; 46 | const result = safeJsonParse('invalid json', fallback); 47 | expect(result).toBe(fallback); 48 | }); 49 | 50 | it('should return fallback for malformed JSON object', () => { 51 | const fallback = { error: 'malformed' }; 52 | const result = safeJsonParse('{"name": "test",}', fallback); 53 | expect(result).toBe(fallback); 54 | }); 55 | 56 | it('should return fallback for malformed JSON array', () => { 57 | const fallback: any[] = []; 58 | const result = safeJsonParse('[1, 2, 3,]', fallback); 59 | expect(result).toBe(fallback); 60 | }); 61 | 62 | it('should return fallback for unclosed JSON', () => { 63 | const fallback = { error: 'unclosed' }; 64 | const result = safeJsonParse('{"name": "test"', fallback); 65 | expect(result).toBe(fallback); 66 | }); 67 | 68 | it('should handle complex nested JSON', () => { 69 | const jsonString = '{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "total": 2}'; 70 | const fallback = {}; 71 | const result = safeJsonParse(jsonString, fallback); 72 | expect(result).toEqual({ 73 | users: [ 74 | { name: 'Alice', age: 30 }, 75 | { name: 'Bob', age: 25 }, 76 | ], 77 | total: 2, 78 | }); 79 | }); 80 | 81 | it('should work with different fallback types', () => { 82 | expect(safeJsonParse('invalid', 'string fallback')).toBe('string fallback'); 83 | expect(safeJsonParse('invalid', 42)).toBe(42); 84 | expect(safeJsonParse('invalid', true)).toBe(true); 85 | expect(safeJsonParse('invalid', null)).toBe(null); 86 | expect(safeJsonParse('invalid', undefined)).toBe(undefined); 87 | }); 88 | 89 | it('should handle whitespace-only strings', () => { 90 | const fallback = { whitespace: 'test' }; 91 | expect(safeJsonParse(' ', fallback)).toBe(fallback); 92 | expect(safeJsonParse('\t\n\r', fallback)).toBe(fallback); 93 | }); 94 | 95 | it('should preserve type information from parsed JSON', () => { 96 | interface TestType { 97 | id: number; 98 | name: string; 99 | active: boolean; 100 | } 101 | 102 | const jsonString = '{"id": 1, "name": "test", "active": true}'; 103 | const fallback: TestType = { id: 0, name: '', active: false }; 104 | const result = safeJsonParse(jsonString, fallback); 105 | 106 | expect(result.id).toBe(1); 107 | expect(result.name).toBe('test'); 108 | expect(result.active).toBe(true); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/workplace_v1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type workplaceV1ToolName = 3 | | 'workplace.v1.customWorkplaceAccessData.search' 4 | | 'workplace.v1.workplaceAccessData.search' 5 | | 'workplace.v1.workplaceBlockAccessData.search'; 6 | export const workplaceV1CustomWorkplaceAccessDataSearch = { 7 | project: 'workplace', 8 | name: 'workplace.v1.customWorkplaceAccessData.search', 9 | sdkName: 'workplace.v1.customWorkplaceAccessData.search', 10 | path: '/open-apis/workplace/v1/custom_workplace_access_data/search', 11 | httpMethod: 'POST', 12 | description: 13 | '[Feishu/Lark]-Workplace-workplace access data-Get Custom Workplace Access Data-Get Custom Workplace Access Data', 14 | accessTokens: ['tenant'], 15 | schema: { 16 | params: z.object({ 17 | from_date: z.string(), 18 | to_date: z.string(), 19 | page_size: z.number(), 20 | page_token: z 21 | .string() 22 | .describe( 23 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 24 | ) 25 | .optional(), 26 | custom_workplace_id: z 27 | .string() 28 | .describe( 29 | 'Custom workplace ID,which is not mandatory. When empty, all custom workplace data is responsed. How to obtain a custom workplace ID: You can go to Feishu Admin>Workplace>Custom Workplace, click on the settings of the specified workplace to enter the settings page; Click the "Settings" button at the top three times with the mouse to display the ID, and then copy the ID', 30 | ) 31 | .optional(), 32 | }), 33 | }, 34 | }; 35 | export const workplaceV1WorkplaceAccessDataSearch = { 36 | project: 'workplace', 37 | name: 'workplace.v1.workplaceAccessData.search', 38 | sdkName: 'workplace.v1.workplaceAccessData.search', 39 | path: '/open-apis/workplace/v1/workplace_access_data/search', 40 | httpMethod: 'POST', 41 | description: 42 | '[Feishu/Lark]-Workplace-workplace access data-search workplace access data-Get Workplace Access Data, including default workplace and custom workplace', 43 | accessTokens: ['tenant'], 44 | schema: { 45 | params: z.object({ 46 | from_date: z.string(), 47 | to_date: z.string(), 48 | page_size: z.number(), 49 | page_token: z 50 | .string() 51 | .describe( 52 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 53 | ) 54 | .optional(), 55 | }), 56 | }, 57 | }; 58 | export const workplaceV1WorkplaceBlockAccessDataSearch = { 59 | project: 'workplace', 60 | name: 'workplace.v1.workplaceBlockAccessData.search', 61 | sdkName: 'workplace.v1.workplaceBlockAccessData.search', 62 | path: '/open-apis/workplace/v1/workplace_block_access_data/search', 63 | httpMethod: 'POST', 64 | description: 65 | '[Feishu/Lark]-Workplace-workplace access data-Get Block Access Data-Get Custom Workplace Block Access Data', 66 | accessTokens: ['tenant'], 67 | schema: { 68 | params: z.object({ 69 | from_date: z.string(), 70 | to_date: z.string(), 71 | page_size: z.number(), 72 | page_token: z 73 | .string() 74 | .describe( 75 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 76 | ) 77 | .optional(), 78 | block_id: z 79 | .string() 80 | .describe( 81 | 'BlockID. You can go to Feishu Admin>Workplace>Custom Workplace, select the specified workplace and enter the Workplace Builder. Click on a block to view the "BlockID" below the block name in the right panel of the page', 82 | ) 83 | .optional(), 84 | }), 85 | }, 86 | }; 87 | export const workplaceV1Tools = [ 88 | workplaceV1CustomWorkplaceAccessDataSearch, 89 | workplaceV1WorkplaceAccessDataSearch, 90 | workplaceV1WorkplaceBlockAccessDataSearch, 91 | ]; 92 | -------------------------------------------------------------------------------- /docs/recall-mcp/README_ZH.md: -------------------------------------------------------------------------------- 1 | # 飞书/Lark 开放平台开发文档检索 MCP 2 | 3 | [![npm version](https://img.shields.io/npm/v/@larksuiteoapi/lark-mcp.svg)](https://www.npmjs.com/package/@larksuiteoapi/lark-mcp) 4 | [![npm downloads](https://img.shields.io/npm/dm/@larksuiteoapi/lark-mcp.svg)](https://www.npmjs.com/package/@larksuiteoapi/lark-mcp) 5 | [![Node.js Version](https://img.shields.io/node/v/@larksuiteoapi/lark-mcp.svg)](https://nodejs.org/) 6 | 7 | 中文 | [English](./README.md) 8 | 9 | > **⚠️ Beta版本提示**:当前工具处于Beta版本阶段,功能和API可能会有变更,请密切关注版本更新。 10 | 11 | 这是飞书/Lark官方 开放平台开发文档检索 MCP(Model Context Protocol)工具,旨在帮助用户输入自身诉求后迅速检索到自己需要的开发文档,帮助开发者在AI IDE中编写与飞书集成的代码。也可搭配 [飞书/Lark OpenAPI MCP](../../README_ZH.md) 来让 AI 助手运行自动化场景 12 | 13 | >**说明**: 开放平台开发文档检索,检索范围是 [开发文档](https://open.feishu.cn/document/home/index) 下所有的开发指南、开发教程、服务端 API、客户端 API,帮助用户迅速检索到对应的 OpenApi 或者其他开发文档,非「飞书云文档」的检索。 14 | 15 | ## 使用准备 16 | 17 | ### 安装Node.js 18 | 19 | 在使用lark-mcp工具之前,您需要先安装Node.js环境。如已安装过 Node.js,可以跳过本步骤 20 | 1. **使用Homebrew安装(推荐)**: 21 | 22 | ```bash 23 | brew install node 24 | ``` 25 | 26 | 2. **使用官方安装包**: 27 | - 访问[Node.js官网](https://nodejs.org/) 28 | - 下载并安装LTS版本 29 | - 安装完成后,打开终端验证: 30 | ```bash 31 | node -v 32 | npm -v 33 | ``` 34 | 35 | #### Windows安装Node.js 36 | 37 | 1. **使用官方安装包**: 38 | 39 | - 访问[Node.js官网](https://nodejs.org/) 40 | - 下载并运行Windows安装程序(.msi文件) 41 | - 按照安装向导操作,完成安装 42 | - 安装完成后,打开命令提示符验证: 43 | ```bash 44 | node -v 45 | npm -v 46 | ``` 47 | 48 | 2. **使用nvm-windows**: 49 | - 下载[nvm-windows](https://github.com/coreybutler/nvm-windows/releases) 50 | - 安装nvm-windows 51 | - 使用nvm安装Node.js: 52 | ```bash 53 | nvm install latest 54 | nvm use <版本号> 55 | ``` 56 | 57 | ## 安装 58 | 59 | 全局安装lark-mcp工具: 60 | 61 | ```bash 62 | npm install -g @larksuiteoapi/lark-mcp 63 | ``` 64 | 65 | ## 使用指南 66 | 67 | ### 在Trae/Cursor/Claude中使用 68 | 如需在Trae/Cursor或Claude等AI工具中集成飞书/Lark功能,你可以通过下方按钮安装到对应的工具: 69 | 70 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=lark_open_doc_search&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsYXJrc3VpdGVvYXBpL2xhcmstbWNwIiwicmVjYWxsLWRldmVsb3Blci1kb2N1bWVudHMiXX0=) 71 | 72 | [![Install MCP Server](../../assets/trae-cn.svg)](trae-cn://trae.ai-ide/mcp-import?source=lark&type=stdio&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsYXJrc3VpdGVvYXBpL2xhcmstbWNwIiwicmVjYWxsLWRldmVsb3Blci1kb2N1bWVudHMiXX0=) [![Install MCP Server](../../assets/trae.svg)](trae://trae.ai-ide/mcp-import?source=lark&type=stdio&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsYXJrc3VpdGVvYXBpL2xhcmstbWNwIiwicmVjYWxsLWRldmVsb3Blci1kb2N1bWVudHMiXX0=) 73 | 74 | 也可以在配置文件中添加以下内容: 75 | 76 | ```json 77 | { 78 | "mcpServers": { 79 | "lark-mcp": { 80 | "command": "npx", 81 | "args": [ 82 | "-y", 83 | "@larksuiteoapi/lark-mcp", 84 | "recall-developer-documents", 85 | ] 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ### 高级配置 92 | 93 | #### 命令行参数说明 94 | 95 | `lark-mcp recall-developer-documents`工具提供了多种命令行参数,以便您灵活配置MCP服务: 96 | 97 | | 参数 | 简写 | 描述 | 示例 | 98 | |------|------|------|------| 99 | | `--mode` | `-m` | 传输模式,可选值为stdio、streamable或sse,默认为stdio | `-m sse` | 100 | | `--host` | | SSE\Streamable模式下的监听主机,默认为localhost | `--host 0.0.0.0` | 101 | | `--port` | `-p` | SSE\Streamable模式下的监听端口,默认为3000 | `-p 3000` | 102 | | `--version` | `-V` | 显示版本号 | `-V` | 103 | | `--help` | `-h` | 显示帮助信息 | `-h` | 104 | 105 | #### 参数使用示例 106 | 107 | 1. **传输模式**: 108 | 109 | recall-developer-documents 支持两种传输模式: 110 | 111 | 1. **stdio模式(默认/推荐)**:适用于与Cursor或Claude等AI工具集成,通过标准输入输出流进行通信。 112 | ```bash 113 | lark-mcp recall-developer-documents -m stdio 114 | ``` 115 | 116 | 2. **SSE模式**:提供基于Server-Sent Events的HTTP接口,适用于Web应用或需要网络接口的场景。 117 | 118 | ```bash 119 | # 默认只监听localhost 120 | lark-mcp recall-developer-documents -m sse -p 3000 121 | 122 | # 监听所有网络接口(允许远程访问) 123 | lark-mcp recall-developer-documents -m sse --host 0.0.0.0 -p 3000 124 | ``` 125 | 126 | 启动后,SSE端点将可在 `http://:/sse` 访问。 127 | 128 | ## 相关链接 129 | 130 | - [飞书开放平台](https://open.feishu.cn/) 131 | - [Lark国际版开放平台](https://open.larksuite.com/) 132 | - [Node.js官网](https://nodejs.org/) 133 | - [npm文档](https://docs.npmjs.com/) 134 | 135 | ## 反馈 136 | 137 | 欢迎提交Issues来帮助改进这个工具。如有问题或建议,请在GitHub仓库中提出。 -------------------------------------------------------------------------------- /src/mcp-tool/tools/en/gen-tools/zod/mdm_v3.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export type mdmV3ToolName = 'mdm.v3.batchCountryRegion.get' | 'mdm.v3.countryRegion.list'; 3 | export const mdmV3BatchCountryRegionGet = { 4 | project: 'mdm', 5 | name: 'mdm.v3.batchCountryRegion.get', 6 | sdkName: 'mdm.v3.batchCountryRegion.get', 7 | path: '/open-apis/mdm/v3/batch_country_region', 8 | httpMethod: 'GET', 9 | description: 10 | '[Feishu/Lark]-Feishu Master Data Management-Common Data-country region-batch get major by id-Batch Get Country Region By ID', 11 | accessTokens: ['tenant'], 12 | schema: { 13 | params: z.object({ 14 | fields: z.array(z.string()).describe('Required query field set'), 15 | ids: z.array(z.string()).describe('Master Data CodeSet'), 16 | languages: z 17 | .array(z.string()) 18 | .describe( 19 | 'The language type you want to return, the supported format is as follows:-Chinese: zh-CN-English: en-US-Japanese: ja-JPFor multilingual text fields, if a specific language is passed in, the corresponding language text will be returned', 20 | ), 21 | }), 22 | }, 23 | }; 24 | export const mdmV3CountryRegionList = { 25 | project: 'mdm', 26 | name: 'mdm.v3.countryRegion.list', 27 | sdkName: 'mdm.v3.countryRegion.list', 28 | path: '/open-apis/mdm/v3/country_regions', 29 | httpMethod: 'GET', 30 | description: 31 | '[Feishu/Lark]-Feishu Master Data Management-Common Data-country region-Pagination Batch Query Country Region-Paging batch query country region', 32 | accessTokens: ['tenant'], 33 | schema: { 34 | data: z 35 | .object({ 36 | filter: z 37 | .object({ 38 | logic: z 39 | .string() 40 | .describe( 41 | 'LogicMultiple expressions at the same level are determined by the logic parameter using "and/or" conditions.0=and, 1=or', 42 | ), 43 | expressions: z 44 | .array( 45 | z.object({ 46 | field: z.string().describe('field name'), 47 | operator: z 48 | .string() 49 | .describe( 50 | 'Operator0=equal, 1=not equal, 2=greater than, 3=greater than or equal to, 4=less than, 5=less than or equal to, 6=any, 7=not any, 8=match, 9=prefix match, 10=suffix Match, 11=null, 12=not null', 51 | ), 52 | value: z 53 | .object({ 54 | string_value: z.string().describe('string value').optional(), 55 | bool_value: z.boolean().describe('Boolean').optional(), 56 | int_value: z.string().describe('shaping value').optional(), 57 | string_list_value: z.array(z.string()).describe('String list value').optional(), 58 | int_list_value: z.array(z.string()).describe('integer list value').optional(), 59 | }) 60 | .describe('field value'), 61 | }), 62 | ) 63 | .describe('filter condition') 64 | .optional(), 65 | }) 66 | .describe('Filter parameters') 67 | .optional(), 68 | common: z.object({}).describe('This parameter can be ignored').optional(), 69 | }) 70 | .optional(), 71 | params: z.object({ 72 | languages: z 73 | .array(z.string()) 74 | .describe( 75 | 'The language type you want to return, the supported format is as follows:-Chinese: zh-CN-English: en-US-Japanese: ja-JPFor multilingual text fields, if a specific language is passed in, the corresponding language text will be returned', 76 | ), 77 | fields: z.array(z.string()).describe('Required query field set'), 78 | limit: z.number().describe('query page size').optional(), 79 | offset: z.number().describe('query start location').optional(), 80 | return_count: z.boolean().describe('Whether to return the total').optional(), 81 | page_token: z 82 | .string() 83 | .describe( 84 | 'Page identifier. It is not filled in the first request, indicating traversal from the beginning; when there will be more groups, the new page_token will be returned at the same time, and the next traversal can use the page_token to get more groups', 85 | ) 86 | .optional(), 87 | }), 88 | }, 89 | }; 90 | export const mdmV3Tools = [mdmV3BatchCountryRegionGet, mdmV3CountryRegionList]; 91 | -------------------------------------------------------------------------------- /tests/mcp-tool/document-tool/recall/request.test.ts: -------------------------------------------------------------------------------- 1 | import { recallDeveloperDocument } from '../../../../src/mcp-tool/document-tool/recall/request'; 2 | import { DocumentRecallToolOptions } from '../../../../src/mcp-tool/document-tool/recall/type'; 3 | import { commonHttpInstance } from '../../../../src/utils/http-instance'; 4 | 5 | // 模拟http-instance 6 | jest.mock('../../../../src/utils/http-instance'); 7 | const mockedHttpInstance = commonHttpInstance as jest.Mocked; 8 | 9 | describe('recallDeveloperDocument', () => { 10 | // 每个测试前重置模拟 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('应该使用正确的URL和参数发送请求', async () => { 16 | // 模拟响应数据 17 | const mockResponse = { 18 | data: { 19 | chunks: ['result1', 'result2', 'result3', 'result4'], 20 | }, 21 | }; 22 | mockedHttpInstance.post.mockResolvedValueOnce(mockResponse); 23 | 24 | // 测试参数 25 | const query = 'test query'; 26 | const options: DocumentRecallToolOptions = { 27 | domain: 'https://example.com', 28 | count: 3, 29 | }; 30 | 31 | // 调用函数 32 | await recallDeveloperDocument(query, options); 33 | 34 | // 验证请求参数 35 | expect(mockedHttpInstance.post).toHaveBeenCalledWith( 36 | 'https://example.com/document_portal/v1/recall', 37 | { question: query }, 38 | { 39 | timeout: 10000, 40 | }, 41 | ); 42 | }); 43 | 44 | it('应该返回指定数量的结果', async () => { 45 | // 模拟响应数据 46 | const mockResponse = { 47 | data: { 48 | chunks: ['result1', 'result2', 'result3', 'result4'], 49 | }, 50 | }; 51 | mockedHttpInstance.post.mockResolvedValueOnce(mockResponse); 52 | 53 | // 测试参数 54 | const query = 'test query'; 55 | const options: DocumentRecallToolOptions = { 56 | domain: 'https://example.com', 57 | count: 2, 58 | }; 59 | 60 | // 调用函数并获取结果 61 | const results = await recallDeveloperDocument(query, options); 62 | 63 | // 验证返回结果数量 64 | expect(results).toHaveLength(2); 65 | expect(results).toEqual(['result1', 'result2']); 66 | }); 67 | 68 | it('应该使用默认数量(3)当未指定count时', async () => { 69 | // 模拟响应数据 70 | const mockResponse = { 71 | data: { 72 | chunks: ['result1', 'result2', 'result3', 'result4', 'result5'], 73 | }, 74 | }; 75 | mockedHttpInstance.post.mockResolvedValueOnce(mockResponse); 76 | 77 | // 测试参数,不包含count 78 | const query = 'test query'; 79 | const options: DocumentRecallToolOptions = { 80 | domain: 'https://example.com', 81 | }; 82 | 83 | // 调用函数并获取结果 84 | const results = await recallDeveloperDocument(query, options); 85 | 86 | // 验证返回结果数量为默认值3 87 | expect(results).toHaveLength(3); 88 | expect(results).toEqual(['result1', 'result2', 'result3']); 89 | }); 90 | 91 | it('应该返回空数组当chunks为空时', async () => { 92 | // 模拟响应数据 93 | const mockResponse = { 94 | data: { 95 | chunks: [], 96 | }, 97 | }; 98 | mockedHttpInstance.post.mockResolvedValueOnce(mockResponse); 99 | 100 | // 测试参数 101 | const query = 'test query'; 102 | const options: DocumentRecallToolOptions = { 103 | domain: 'https://example.com', 104 | count: 3, 105 | }; 106 | 107 | // 调用函数并获取结果 108 | const results = await recallDeveloperDocument(query, options); 109 | 110 | // 验证返回结果为空数组 111 | expect(results).toEqual([]); 112 | }); 113 | 114 | it('应该返回空数组当响应中没有chunks字段时', async () => { 115 | // 模拟响应数据 116 | const mockResponse = { 117 | data: {}, 118 | }; 119 | mockedHttpInstance.post.mockResolvedValueOnce(mockResponse); 120 | 121 | // 测试参数 122 | const query = 'test query'; 123 | const options: DocumentRecallToolOptions = { 124 | domain: 'https://example.com', 125 | count: 3, 126 | }; 127 | 128 | // 调用函数并获取结果 129 | const results = await recallDeveloperDocument(query, options); 130 | 131 | // 验证返回结果为空数组 132 | expect(results).toEqual([]); 133 | }); 134 | 135 | it('应该抛出错误当请求失败时', async () => { 136 | // 模拟网络错误 137 | const errorMessage = 'Network Error'; 138 | mockedHttpInstance.post.mockRejectedValueOnce(new Error(errorMessage)); 139 | 140 | // 测试参数 141 | const query = 'test query'; 142 | const options: DocumentRecallToolOptions = { 143 | domain: 'https://example.com', 144 | count: 3, 145 | }; 146 | 147 | // 验证函数抛出预期的错误 148 | await expect(recallDeveloperDocument(query, options)).rejects.toThrow(errorMessage); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:20-bookworm-slim 4 | 5 | ENV NODE_ENV=production \ 6 | # Runtime directory required by gnome-keyring/DBus 7 | XDG_RUNTIME_DIR=/home/node/.xdg/runtime 8 | 9 | # Runtime deps: libsecret + gnome-keyring + dbus (provides org.freedesktop.secrets) + build deps for keytar 10 | RUN apt-get update \ 11 | && apt-get install -y --no-install-recommends \ 12 | libsecret-1-0 \ 13 | libsecret-tools \ 14 | libglib2.0-bin \ 15 | gnome-keyring \ 16 | dbus \ 17 | dbus-x11 \ 18 | ca-certificates \ 19 | # build deps (in case keytar needs to compile) 20 | python3 \ 21 | make \ 22 | g++ \ 23 | pkg-config \ 24 | libsecret-1-dev \ 25 | && rm -rf /var/lib/apt/lists/* 26 | 27 | ## Install lark-mcp globally (will install keytar as dependency) 28 | RUN npm install -g @larksuiteoapi/lark-mcp@latest \ 29 | && npm cache clean --force 30 | 31 | ## Prepare XDG and user-writable dirs before dropping privileges 32 | RUN mkdir -p ${XDG_RUNTIME_DIR} \ 33 | && mkdir -p /home/node/.local/state /home/node/.local/share/lark-mcp \ 34 | && chown -R node:node ${XDG_RUNTIME_DIR} /home/node/.local 35 | 36 | ## Dev dependencies were pruned in the build stage; no need to prune again here 37 | 38 | # Initialize DBus and gnome-keyring (secrets) so keytar can operate 39 | RUN <<'EOF' 40 | cat >/usr/local/bin/docker-entrypoint.sh <<'SCRIPT' 41 | #!/usr/bin/env bash 42 | set -e 43 | 44 | # Prepare DBus session; address uses XDG_RUNTIME_DIR 45 | if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then 46 | mkdir -p "${XDG_RUNTIME_DIR}" 47 | dbus-daemon --session --address="unix:path=${XDG_RUNTIME_DIR}/bus" --fork 48 | export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" 49 | fi 50 | 51 | # Create required directories with correct permissions 52 | mkdir -p "${XDG_RUNTIME_DIR}/keyring" 53 | mkdir -p "${HOME}/.local/share/keyrings" 54 | chmod 0700 "${XDG_RUNTIME_DIR}/keyring" 55 | chmod 0700 "${HOME}/.local/share/keyrings" 56 | 57 | # Start gnome-keyring daemon properly 58 | if command -v gnome-keyring-daemon >/dev/null 2>&1; then 59 | # Kill any existing keyring processes 60 | pkill -f gnome-keyring 2>/dev/null || true 61 | sleep 0.5 62 | 63 | # Create an initial login keyring file 64 | current_ts="$(date +%s)" 65 | cat > "${HOME}/.local/share/keyrings/login.keyring" </dev/null) 77 | 78 | # Wait for the daemon to be available and control socket to exist 79 | for i in {1..50}; do 80 | if [[ -S "${XDG_RUNTIME_DIR}/keyring/control" ]] && gdbus introspect --session --dest org.freedesktop.secrets --object-path /org/freedesktop/secrets >/dev/null 2>&1; then 81 | break 82 | fi 83 | sleep 0.1 84 | done 85 | 86 | # Export control directory for the session 87 | export GNOME_KEYRING_CONTROL="${XDG_RUNTIME_DIR}/keyring" 88 | 89 | # Create login collection via D-Bus 90 | if gdbus introspect --session --dest org.freedesktop.secrets --object-path /org/freedesktop/secrets >/dev/null 2>&1; then 91 | # Try to create login collection 92 | printf "" | gdbus call --session \ 93 | --dest org.freedesktop.secrets \ 94 | --object-path /org/freedesktop/secrets \ 95 | --method org.freedesktop.secrets.Service.CreateCollection \ 96 | "{'org.freedesktop.Secret.Collection.Label': <'login'>}" \ 97 | "login" >/dev/null 2>&1 || true 98 | 99 | # Set login as the default alias 100 | gdbus call --session \ 101 | --dest org.freedesktop.secrets \ 102 | --object-path /org/freedesktop/secrets \ 103 | --method org.freedesktop.secrets.Service.SetAlias \ 104 | "login" "/org/freedesktop/secrets/collection/login" >/dev/null 2>&1 || true 105 | fi 106 | fi 107 | 108 | # Suppress gnome-keyring warnings by redirecting stderr for the main process 109 | exec "$@" 2> >(grep -v "couldn't access control socket\|discover_other_daemon" >&2) 110 | SCRIPT 111 | chmod +x /usr/local/bin/docker-entrypoint.sh 112 | EOF 113 | 114 | USER node 115 | 116 | EXPOSE 3000 117 | 118 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh", "lark-mcp"] 119 | 120 | # Show help by default; override CMD to pass CLI arguments 121 | CMD ["--help"] 122 | 123 | 124 | --------------------------------------------------------------------------------