/**
* Tool request handlers for XHS MCP Server
*/
import { AuthService } from '../../core/auth/auth.service';
import { FeedService } from '../../core/feeds/feed.service';
import { PublishService } from '../../core/publishing/publish.service';
import { NoteService } from '../../core/notes/note.service';
import { getConfig } from '../../shared/config';
import { XHSError } from '../../shared/errors';
import {
validateRequiredParams,
validatePublishNoteParams,
safeErrorHandler,
createMcpToolResponse,
createMcpErrorResponse,
} from '../../shared/utils';
import { logger } from '../../shared/logger';
import { assertTitleWidthValid } from '../../shared/title-validator';
/**
* Tool request arguments interface
*/
export interface ToolRequestArgs {
browser_path?: string;
keyword?: string;
feed_id?: string;
xsec_token?: string;
note?: string;
type?: string;
title?: string;
content?: string;
media_paths?: string[];
tags?: string;
limit?: number;
cursor?: string;
note_id?: string;
last_published?: boolean;
}
export class ToolHandlers {
private authService: AuthService;
private feedService: FeedService;
private publishService: PublishService;
private noteService: NoteService;
constructor() {
const config = getConfig();
this.authService = new AuthService(config);
this.feedService = new FeedService(config);
this.publishService = new PublishService(config);
this.noteService = new NoteService(config);
}
async handleAuthLogin(
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
// Start login process in background and return immediately
// This follows the "instant response" pattern described in README
this.authService.login(browserPath).catch((error) => {
safeErrorHandler(error, 'Background login error', logger);
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message:
'Login process started. A browser window will open for you to complete the login.',
status: 'login_started',
action: 'browser_opened',
instructions: [
'1. Complete the login process in the opened browser window',
'2. Scan QR code or enter your credentials',
'3. Login will be automatically verified and cookies saved',
'4. Use xhs_auth_status to check if login completed',
],
note: 'The login process runs in the background. You can continue using other tools while login completes.',
},
null,
2
),
},
],
};
}
async handleAuthLogout(): Promise<{ content: Array<{ type: string; text: string }> }> {
const result = await this.authService.logout();
return createMcpToolResponse(result);
}
async handleAuthStatus(
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
const result = await this.authService.checkStatus(browserPath);
return createMcpToolResponse(result);
}
async handleDiscoverFeeds(
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
const result = await this.feedService.getFeedList(browserPath);
return createMcpToolResponse(result);
}
async handleSearchNote(
keyword?: string,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
validateRequiredParams({ keyword }, ['keyword']);
const result = await this.feedService.searchFeeds(keyword!, browserPath);
return createMcpToolResponse(result);
}
async handleGetNoteDetail(
feedId?: string,
xsecToken?: string,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
validateRequiredParams({ feedId, xsecToken }, ['feedId', 'xsecToken']);
const result = await this.feedService.getFeedDetail(feedId!, xsecToken!, browserPath);
return createMcpToolResponse(result);
}
async handleCommentOnNote(
feedId?: string,
xsecToken?: string,
note?: string,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
validateRequiredParams({ feedId, xsecToken, note }, ['feedId', 'xsecToken', 'note']);
const result = await this.feedService.commentOnFeed(feedId!, xsecToken!, note!, browserPath);
return createMcpToolResponse(result);
}
async handlePublishContent(
type?: string,
title?: string,
content?: string,
mediaPaths?: string[],
tags?: string,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
validateRequiredParams({ type, title, content, mediaPaths }, [
'type',
'title',
'content',
'mediaPaths',
]);
// Validate content type
if (type !== 'image' && type !== 'video') {
throw new Error('Content type must be "image" or "video"');
}
// Validate parameter constraints using width-aware title validation
assertTitleWidthValid(title!);
if (content!.length > 1000) {
throw new Error('Content must be 1000 characters or less');
}
// Execute unified publishing process
const result = await this.publishService.publishContent(
type as 'image' | 'video',
title!,
content!,
mediaPaths!,
tags,
browserPath
);
return createMcpToolResponse(result);
}
async handleGetUserNotes(
limit?: number,
cursor?: string,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
const result = await this.noteService.getUserNotes(limit, cursor, browserPath);
return createMcpToolResponse(result);
}
async handleDeleteNote(
noteId?: string,
lastPublished?: boolean,
browserPath?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
if (lastPublished) {
const result = await this.noteService.deleteLastPublishedNote(browserPath);
return createMcpToolResponse(result);
} else if (noteId) {
const result = await this.noteService.deleteNote(noteId, browserPath);
return createMcpToolResponse(result);
} else {
throw new Error('Either note_id or last_published must be specified');
}
}
async handleToolRequest(
name: string,
args: ToolRequestArgs = {}
): Promise<{ content: Array<{ type: string; text: string }> }> {
try {
switch (name) {
case 'xhs_auth_login':
return await this.handleAuthLogin(args?.browser_path as string);
case 'xhs_auth_logout':
return await this.handleAuthLogout();
case 'xhs_auth_status':
return await this.handleAuthStatus(args?.browser_path as string);
case 'xhs_discover_feeds':
return await this.handleDiscoverFeeds(args?.browser_path as string);
case 'xhs_search_note':
return await this.handleSearchNote(args?.keyword as string, args?.browser_path as string);
case 'xhs_get_note_detail':
return await this.handleGetNoteDetail(
args?.feed_id as string,
args?.xsec_token as string,
args?.browser_path as string
);
case 'xhs_comment_on_note':
return await this.handleCommentOnNote(
args?.feed_id as string,
args?.xsec_token as string,
args?.note as string,
args?.browser_path as string
);
case 'xhs_publish_content':
return await this.handlePublishContent(
args?.type as string,
args?.title as string,
args?.content as string,
args?.media_paths as string[],
args?.tags as string,
args?.browser_path as string
);
case 'xhs_get_user_notes':
return await this.handleGetUserNotes(
args?.limit as number,
args?.cursor as string,
args?.browser_path as string
);
case 'xhs_delete_note':
return await this.handleDeleteNote(
args?.note_id as string,
args?.last_published as boolean,
args?.browser_path as string
);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof XHSError) {
return createMcpToolResponse(error.toJSON());
}
return createMcpErrorResponse(error);
}
}
}