search_jina_blog
Search Jina AI's official blog for articles about AI, machine learning, neural search, embeddings, and Jina products to find documentation, tutorials, announcements, and technical deep-dives.
Instructions
Search Jina AI news and blog posts at jina.ai/news for articles about AI, machine learning, neural search, embeddings, and Jina products. Use this to find official Jina documentation, tutorials, product announcements, and technical deep-dives.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search terms to find relevant Jina blog posts (e.g., 'embeddings', 'reranker', 'ColBERT'). Can be a single query string or an array of queries for parallel search. | |
| num | No | Maximum number of blog posts to return, between 1-100 | |
| tbs | No | Time-based search parameter, e.g., 'qdr:h' for past hour, can be qdr:h, qdr:d, qdr:w, qdr:m, qdr:y |
Implementation Reference
- src/tools/jina-tools.ts:493-547 (handler)Full MCP tool registration block containing the handler function, Zod schema validation, and execution logic for 'search_jina_blog'. Handles single and parallel queries by calling the executeJinaBlogSearch helper.if (isToolEnabled("search_jina_blog")) { server.tool( "search_jina_blog", "Search Jina AI news and blog posts at jina.ai/news for articles about AI, machine learning, neural search, embeddings, and Jina products. Use this to find official Jina documentation, tutorials, product announcements, and technical deep-dives.", { query: z.union([z.string(), z.array(z.string())]).describe("Search terms to find relevant Jina blog posts (e.g., 'embeddings', 'reranker', 'ColBERT'). Can be a single query string or an array of queries for parallel search."), num: z.number().default(30).describe("Maximum number of blog posts to return, between 1-100"), tbs: z.string().optional().describe("Time-based search parameter, e.g., 'qdr:h' for past hour, can be qdr:h, qdr:d, qdr:w, qdr:m, qdr:y") }, async ({ query, num, tbs }: { query: string | string[]; num: number; tbs?: string }) => { try { const props = getProps(); // Get Ghost API key from props (set in index.ts from env) const ghostApiKey = props.ghostApiKey; if (!ghostApiKey) { return createErrorResponse("Ghost API key not configured"); } // Handle single query or single-element array if (typeof query === 'string' || (Array.isArray(query) && query.length === 1)) { const singleQuery = typeof query === 'string' ? query : query[0]; const searchResult = await executeJinaBlogSearch({ query: singleQuery, num, tbs }, ghostApiKey); return { content: formatSingleSearchResultToContentItems(searchResult), }; } // Handle multiple queries with parallel search if (Array.isArray(query) && query.length > 1) { const searches = query.map(q => ({ query: q, num, tbs })); const uniqueSearches = searches.filter((search, index, self) => index === self.findIndex(s => s.query === search.query) ); const jinaBlogSearchFunction = async (searchArgs: SearchJinaBlogArgs) => { return executeJinaBlogSearch(searchArgs, ghostApiKey); }; const results = await executeParallelSearches(uniqueSearches, jinaBlogSearchFunction, { timeout: 30000 }); return { content: formatParallelSearchResultsToContentItems(results), }; } return createErrorResponse("Invalid query format"); } catch (error) { return createErrorResponse(`Error: ${error instanceof Error ? error.message : String(error)}`); } }, ); }
- src/utils/search.ts:28-32 (schema)TypeScript interface defining the input arguments for the search_jina_blog tool, imported and used in the handler.export interface SearchJinaBlogArgs { query: string; num?: number; tbs?: string; }
- src/utils/search.ts:171-264 (helper)Core helper function that implements the actual blog search logic using Ghost.io API, including query filtering, time-based constraints, and result transformation.export async function executeJinaBlogSearch( searchArgs: SearchJinaBlogArgs, ghostApiKey: string ): Promise<SearchResultOrError> { try { const limit = searchArgs.num || 30; // Build filter for Ghost NQL // Ghost Content API only supports filtering on specific fields (title, tag, author, etc.) // Full-text search on content/excerpt is not supported - only substring matching on title const filters: string[] = []; // Search in title using contains operator if (searchArgs.query) { // Escape single quotes in query const escapedQuery = searchArgs.query.replace(/'/g, "\\'"); filters.push(`title:~'${escapedQuery}'`); } // Map tbs (time-based search) to Ghost's published_at filter if (searchArgs.tbs) { const now = new Date(); let dateFilter: Date | null = null; switch (searchArgs.tbs) { case 'qdr:h': // past hour dateFilter = new Date(now.getTime() - 60 * 60 * 1000); break; case 'qdr:d': // past day dateFilter = new Date(now.getTime() - 24 * 60 * 60 * 1000); break; case 'qdr:w': // past week dateFilter = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'qdr:m': // past month dateFilter = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; case 'qdr:y': // past year dateFilter = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); break; } if (dateFilter) { filters.push(`published_at:>'${dateFilter.toISOString()}'`); } } // Build URL with query parameters const params = new URLSearchParams({ key: ghostApiKey, limit: limit.toString(), fields: 'id,title,slug,excerpt,published_at,url,reading_time', order: 'published_at desc' }); if (filters.length > 0) { params.set('filter', filters.join('+')); } const response = await fetch(`https://jina-ai-gmbh.ghost.io/ghost/api/content/posts/?${params.toString()}`, { method: 'GET', headers: { 'Accept': 'application/json', }, }); if (!response.ok) { return { error: `Jina blog search failed for query "${searchArgs.query}": ${response.statusText}` }; } const data = await response.json() as any; // Transform Ghost posts to search result format const results = (data.posts || []).map((post: any) => { // Transform ghost.io URL to jina.ai/news URL let url = post.url || `https://jina.ai/news/${post.slug}`; if (url.includes('jina-ai-gmbh.ghost.io')) { url = url.replace('https://jina-ai-gmbh.ghost.io/podcast/', 'https://jina.ai/news/'); url = url.replace('https://jina-ai-gmbh.ghost.io/', 'https://jina.ai/news/'); } return { title: post.title, url, snippet: post.excerpt, date: post.published_at, reading_time: post.reading_time }; }); return { query: searchArgs.query, results }; } catch (error) { return { error: `Jina blog search failed for query "${searchArgs.query}": ${error instanceof Error ? error.message : String(error)}` }; } }
- src/index.ts:100-102 (registration)Top-level call to registerJinaTools which includes the search_jina_blog tool registration based on enabledTools filter.registerJinaTools(server, () => currentProps, enabledTools); return server;
- src/tools/jina-tools.ts:497-501 (schema)Zod schema for input validation in the MCP tool definition.{ query: z.union([z.string(), z.array(z.string())]).describe("Search terms to find relevant Jina blog posts (e.g., 'embeddings', 'reranker', 'ColBERT'). Can be a single query string or an array of queries for parallel search."), num: z.number().default(30).describe("Maximum number of blog posts to return, between 1-100"), tbs: z.string().optional().describe("Time-based search parameter, e.g., 'qdr:h' for past hour, can be qdr:h, qdr:d, qdr:w, qdr:m, qdr:y") },