Skip to main content
Glama
jina-ai

Jina AI Remote MCP Server

Official
by jina-ai

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
NameRequiredDescriptionDefault
queryYesSearch 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.
numNoMaximum number of blog posts to return, between 1-100
tbsNoTime-based search parameter, e.g., 'qdr:h' for past hour, can be qdr:h, qdr:d, qdr:w, qdr:m, qdr:y

Implementation Reference

  • 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)}`);
    			}
    		},
    	);
    }
  • 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;
    }
  • 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;
  • 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")
    },

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jina-ai/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server