feat: API Tester 支持 MCP 模式 + 修复多个 bug

MCP 模式:
- 新增浏览器端 MCP 客户端 (SSE + JSON-RPC + initialize 握手)
- API Tester 页面双模式: REST API tab + MCP Tools tab
- 连接后自动发现 13 个 tools,支持参数编辑和调用
- 与 AI 客户端走完全相同的协议路径

后端修复:
- 每个 SSE 连接创建独立 McpServer 实例,支持多客户端并发
- express.json() 移到 SSE 路由之后,避免消费 MCP 请求体
- 新增 GET /api/xhs/login/cookie-check 轻量接口(不打开浏览器)

前端修复:
- QR 轮询改用 cookie-check 接口,不再重复打开浏览器窗口
- useLoginStatus 默认不自动请求,避免页面加载触发浏览器
- Header 添加 Token 未配置警告横幅
- tsup clean:false 保护 dist/web 不被清除
This commit is contained in:
2026-03-01 16:33:33 +08:00
parent f464333a53
commit 785ec84e56
8 changed files with 665 additions and 146 deletions
+18
View File
@@ -7,6 +7,7 @@ import { logger } from '../../utils/logger.js';
import { classifyError, sanitizeErrorMessage } from '../../utils/errors.js';
import { validateMediaPath } from '../../utils/downloader.js';
import { rateLimiter } from '../../server/middleware.js';
import { cookieStore } from '../../cookie/store.js';
import { checkLoginStatus, getLoginQRCode, deleteCookies } from './login.js';
import { listFeeds } from './feeds.js';
@@ -217,6 +218,23 @@ export function createXhsRoutes(browser: BrowserManager): Router {
})();
});
// -----------------------------------------------------------------------
// GET /login/cookie-check
// Lightweight check: does a cookie file exist on disk?
// Does NOT open a browser — safe for frequent polling.
// -----------------------------------------------------------------------
router.get('/login/cookie-check', readRateLimiter, (_req, res) => {
void (async () => {
try {
const state = await cookieStore.load(PLATFORM);
const hasCookies = state !== null && state.cookies.length > 0;
res.json(successResponse({ hasCookies }) as ApiResponse<{ hasCookies: boolean }>);
} catch (err) {
handleError(res, err);
}
})();
});
// =========================================================================
// Content browsing
// =========================================================================
+17 -8
View File
@@ -85,9 +85,8 @@ export class AppServer {
// -- Constructor ----------------------------------------------------------
constructor() {
// 1. Express app + body parsing
// 1. Express app
this.app = express();
this.app.use(express.json());
// 2. Security & availability middleware
this.app.use(shutdownGuard(() => this.shuttingDown));
@@ -97,13 +96,16 @@ export class AppServer {
{ name: 'social-mcp', version: PACKAGE_VERSION },
);
// 4. SSE transport endpoints
// 4. SSE transport endpoints (BEFORE body parsing — MCP SDK reads raw body)
this.setupSseEndpoints();
// 5. Health endpoint
// 5. Body parsing for non-MCP routes
this.app.use(express.json());
// 6. Health endpoint
this.setupHealthEndpoint();
// 6. Bearer token auth for /api/* routes
// 7. Bearer token auth for /api/* routes
initBearerToken();
this.app.use('/api', bearerAuth);
@@ -245,9 +247,16 @@ export class AppServer {
this.transports.delete(sessionId);
});
// Connect the transport to the MCP server. This starts the SSE
// stream and sends the initial endpoint event to the client.
void this.mcpServer.connect(transport).catch((err: unknown) => {
// Each SSE connection needs its own McpServer instance because the
// MCP SDK only allows one transport per server at a time.
const perSessionMcp = new McpServer(
{ name: 'social-mcp', version: PACKAGE_VERSION },
);
for (const plugin of this.plugins) {
plugin.registerTools(perSessionMcp, browserManager);
}
void perSessionMcp.connect(transport).catch((err: unknown) => {
logger.error({ err, sessionId }, 'Failed to connect SSE transport to MCP server');
this.transports.delete(sessionId);
});