feishu-mcp
Version:
Model Context Protocol server for Feishu integration
100 lines (99 loc) • 4.27 kB
JavaScript
import { resolve } from 'path';
import { homedir } from 'os';
import { existsSync } from 'fs';
import { config as loadDotEnv } from 'dotenv';
import { Logger } from '../utils/logger.js';
import { dispatch, listTools } from './dispatcher.js';
import { handleConfigShow, handleConfigSet } from './commands/config.js';
import { handleAuthStatus, handleAuthLogout } from './commands/auth.js';
import { handleGuide } from './commands/guide.js';
import { handleHelp } from './commands/help.js';
// 按优先级查找 .env:CWD → ~/.cache/feishu-mcp(与 token 缓存同目录)
const envCandidates = [
resolve(process.cwd(), '.env'),
resolve(homedir(), '.cache', 'feishu-mcp', '.env'),
];
const envPath = envCandidates.find(p => existsSync(p));
if (envPath)
loadDotEnv({ path: envPath });
// 禁用所有日志输出,保持 stdout 纯 JSON
Logger.setEnabled(false);
// key 为空字符串表示默认子命令(即直接执行父命令时)
const COMMAND_REGISTRY = {
config: {
'': { description: '查看当前生效配置', handler: (_) => handleConfigShow(envPath) },
set: { description: '修改配置 <KEY> <VALUE>(不带参数可查看所有可用 KEY)', handler: (args) => handleConfigSet(args[0], args[1], envPath) },
},
auth: {
'': { description: '查看 token 授权状态', handler: (_) => handleAuthStatus() },
logout: { description: '清除已缓存的 token', handler: (_) => handleAuthLogout() },
},
guide: {
'': { description: '飞书应用配置指南(步骤说明)', handler: (_) => handleGuide() },
},
help: {
'': { description: '查看工具详细说明 <tool-name>(不带参数列出所有工具)', handler: (args) => handleHelp(args) },
},
};
async function main() {
const [, , cmd, sub, ...rest] = process.argv;
// 子命令路由:在注册表中查找并执行
if (cmd && cmd in COMMAND_REGISTRY) {
const subMap = COMMAND_REGISTRY[cmd];
// sub 命中具名子命令则执行,否则 fallback 到默认('')
const subKey = sub && sub in subMap ? sub : '';
const def = subMap[subKey];
if (def) {
const args = subKey ? rest : (sub ? [sub, ...rest] : []);
await def.handler(args);
process.exit(0);
}
}
const toolName = cmd;
const rawParams = sub ?? '{}';
// 无参数时打印帮助(按认证类型过滤工具列表,自动从注册表生成子命令说明)
if (!toolName || toolName === '--help' || toolName === '-h') {
const authType = (process.env.FEISHU_AUTH_TYPE ?? 'tenant');
const subcommands = {};
for (const [c, subMap] of Object.entries(COMMAND_REGISTRY)) {
for (const [s, def] of Object.entries(subMap)) {
subcommands[s ? `${c} ${s}` : c] = def.description;
}
}
process.stdout.write(JSON.stringify({
usage: "feishu-tool <tool-name> '<json-params>'",
subcommands,
authType,
toolsNote: authType === 'tenant'
? 'task/member 工具需 FEISHU_AUTH_TYPE=user 才可用'
: '所有工具均可用',
tools: listTools(authType),
}, null, 2) + '\n');
process.exit(0);
}
// 解析参数
let params;
try {
params = JSON.parse(rawParams);
}
catch {
process.stdout.write(JSON.stringify({ error: `参数解析失败,请提供合法的 JSON 字符串: ${rawParams}` }) + '\n');
process.exit(1);
}
// 调度工具
try {
const result = await dispatch(toolName, params);
// 若工具已返回序列化字符串(如 get_feishu_document_blocks),直接输出避免双重编码
process.stdout.write((typeof result === 'string' ? result : JSON.stringify(result)) + '\n');
process.exit(0);
}
catch (err) {
const message = err instanceof Error ? err.message
: typeof err === 'object' && err !== null ? JSON.stringify(err)
: String(err);
process.stdout.write(JSON.stringify({ error: message }) + '\n');
process.exit(1);
}
}
main().catch(() => process.exit(1));