/**
* search_stories MCP Tool
* Search Hacker News stories by relevance
*/
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { HNError } from '../lib/errors.js';
import type { HNClient } from '../lib/hn-client.js';
import { createChildLogger } from '../lib/logger.js';
export function registerSearchStories(server: McpServer, hnClient: HNClient): void {
server.registerTool(
'search_stories',
{
title: 'Search Stories',
description:
'Search Hacker News stories by relevance. Returns stories matching the query, sorted by relevance score, points, and comment count.',
inputSchema: {
query: z.string().describe('Search query text. Can be empty to get all stories matching tags/filters.'),
tags: z
.string()
.optional()
.describe(
"Optional filter tags. Comma-separated for AND logic, use parentheses for OR: 'story', 'show_hn', 'ask_hn', 'front_page', 'author_USERNAME'. Example: 'author_pg,(story,poll)'"
),
numericFilters: z
.string()
.optional()
.describe(
"Optional numeric filters: 'created_at_i>X', 'points>=Y', 'num_comments>=Z'. Comma-separated for AND."
),
page: z.number().int().min(0).optional().default(0).describe('Page number (0-indexed)'),
hitsPerPage: z.number().int().min(1).max(100).optional().default(20).describe('Results per page'),
},
outputSchema: {
hits: z.array(z.any()),
nbHits: z.number(),
nbPages: z.number(),
page: z.number(),
hitsPerPage: z.number(),
processingTimeMS: z.number(),
},
},
async ({ query, tags, numericFilters, page = 0, hitsPerPage = 20 }) => {
const correlationId = crypto.randomUUID();
const toolLogger = createChildLogger({ correlationId, tool: 'search_stories' });
try {
toolLogger.info({ query, tags, numericFilters, page, hitsPerPage }, 'Searching stories');
const result = await hnClient.search({
query,
...(tags && { tags }),
...(numericFilters && { numericFilters }),
page,
hitsPerPage,
});
toolLogger.info({ nbHits: result.nbHits, nbPages: result.nbPages }, 'Search completed');
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
structuredContent: result as unknown as Record<string, unknown>,
};
} catch (error) {
toolLogger.error({ error }, 'Search failed');
if (error instanceof HNError) {
const errorContent = { error: error.message };
return {
content: [{ type: 'text', text: JSON.stringify(errorContent) }],
isError: true,
};
}
throw error;
}
}
);
}