@brave/brave-search-mcp-server 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import WebSearchTool from './web/index.js';
2
+ import LocalSearchTool from './local/index.js';
3
+ import VideoSearchTool from './videos/index.js';
4
+ import ImageSearchTool from './images/index.js';
5
+ import NewsSearchTool from './news/index.js';
6
+ import SummarizerTool from './summarizer/index.js';
7
+ export default {
8
+ WebSearchTool,
9
+ LocalSearchTool,
10
+ VideoSearchTool,
11
+ ImageSearchTool,
12
+ NewsSearchTool,
13
+ SummarizerTool,
14
+ };
@@ -0,0 +1,136 @@
1
+ import webParams from '../web/params.js';
2
+ import API from '../../BraveAPI/index.js';
3
+ import { formatWebResults } from '../web/index.js';
4
+ import { stringify } from '../../utils.js';
5
+ export const name = 'brave_local_search';
6
+ export const annotations = {
7
+ title: 'Brave Local Search',
8
+ openWorldHint: true,
9
+ };
10
+ export const description = `
11
+ Brave Local Search API provides enrichments for location search results. Access to this API is available only through the Brave Search API Pro plans; confirm the user's plan before using this tool (if the user does not have a Pro plan, use the brave_web_search tool). Searches for local businesses and places using Brave's Local Search API. Best for queries related to physical locations, businesses, restaurants, services, etc.
12
+
13
+ Returns detailed information including:
14
+ - Business names and addresses
15
+ - Ratings and review counts
16
+ - Phone numbers and opening hours
17
+
18
+ Use this when the query implies 'near me', 'in my area', or mentions specific locations (e.g., 'in San Francisco'). This tool automatically falls back to brave_web_search if no local results are found.
19
+ `;
20
+ // Access to Local API is available through the Pro plans.
21
+ export const execute = async (params) => {
22
+ // Make sure both 'web' and 'locations' are in the result_filter
23
+ params = { ...params, result_filter: [...(params.result_filter || []), 'web', 'locations'] };
24
+ // Starts with a web search to retrieve potential location IDs
25
+ const { locations, web: web_fallback } = await API.issueRequest('web', params);
26
+ // We can send up to 20 location IDs at a time to the Local API
27
+ // TODO (Sampson): Add support for multiple requests
28
+ const locationIDs = (locations?.results || []).map((poi) => poi.id).slice(0, 20);
29
+ // No locations were found - user's plan may not include access to the Local API
30
+ if (!locations || locationIDs.length === 0) {
31
+ // If we have web results, but no locations, we'll fall back to the web results
32
+ if (web_fallback && web_fallback.results.length > 0) {
33
+ return buildFallbackWebResponse(web_fallback);
34
+ }
35
+ // If we have no web results, we'll send a message to the user
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text: "No location data was returned. User's plan does not support local search, or the query may be unclear.",
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ // Fetch AI-generated descriptions
46
+ const descriptions = await API.issueRequest('localDescriptions', {
47
+ ids: locationIDs,
48
+ });
49
+ return {
50
+ content: formatLocalResults(locations.results, descriptions.results).map((formattedPOI) => ({
51
+ type: 'text',
52
+ text: formattedPOI,
53
+ })),
54
+ };
55
+ };
56
+ const buildFallbackWebResponse = (web_fallback) => {
57
+ if (!web_fallback || web_fallback.results.length === 0)
58
+ throw new Error('No web results found');
59
+ const fallback = {
60
+ content: [
61
+ {
62
+ type: 'text',
63
+ text: "No location data was returned. Either the user's plan does not support local search, or the API was unable to find locations for the provided query. Falling back to general web search.",
64
+ },
65
+ ],
66
+ };
67
+ for (const web_result of formatWebResults(web_fallback)) {
68
+ fallback.content.push({
69
+ type: 'text',
70
+ text: stringify(web_result),
71
+ });
72
+ }
73
+ return fallback;
74
+ };
75
+ const formatLocalResults = (poisData, descData = []) => {
76
+ return poisData.map((poi) => {
77
+ return stringify({
78
+ name: poi.title,
79
+ price_range: poi.price_range,
80
+ phone: poi.contact?.telephone,
81
+ rating: poi.rating?.ratingValue,
82
+ hours: formatOpeningHours(poi.opening_hours),
83
+ rating_count: poi.rating?.reviewCount,
84
+ description: descData.find(({ id }) => id === poi.id)?.description,
85
+ address: poi.postal_address?.displayAddress,
86
+ });
87
+ });
88
+ };
89
+ const formatOpeningHours = (openingHours) => {
90
+ if (!openingHours)
91
+ return undefined;
92
+ /**
93
+ * Response will be something like {
94
+ * 'sunday': '10:00-18:00',
95
+ * 'monday': '10:00-18:00',
96
+ * 'tuesday': '10:00-18:00',
97
+ * 'wednesday': '10:00-18:00, 19:00-22:00',
98
+ * 'thursday': '10:00-18:00',
99
+ * 'friday': '10:00-18:00',
100
+ * 'saturday': '12:00-18:00',
101
+ * }
102
+ */
103
+ const today = openingHours.current_day || [];
104
+ const response = {};
105
+ const dayHours = [
106
+ [
107
+ `today (${today[0].full_name.toLowerCase()})`,
108
+ today.map(({ opens, closes }) => `${opens}-${closes}`),
109
+ ],
110
+ ];
111
+ // Add the rest of the days to the response
112
+ for (let parts of openingHours.days || []) {
113
+ // Not all days have arrays of hours, so normalize to an array
114
+ if (!Array.isArray(parts))
115
+ parts = [parts];
116
+ // Add the hours for each day to the response
117
+ for (const { full_name, opens, closes } of parts) {
118
+ const dayName = full_name.toLowerCase();
119
+ const existingEntry = dayHours.find(([name]) => name === dayName);
120
+ existingEntry
121
+ ? existingEntry[1].push(`${opens}-${closes}`)
122
+ : dayHours.push([dayName, [`${opens}-${closes}`]]);
123
+ }
124
+ }
125
+ for (const [name, hours] of dayHours) {
126
+ response[name] = hours.join(', ');
127
+ }
128
+ return response;
129
+ };
130
+ export default {
131
+ name,
132
+ description,
133
+ annotations,
134
+ inputSchema: webParams.shape,
135
+ execute,
136
+ };
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export const LocalPoisParams = z.object({
3
+ ids: z.array(z.string()).describe('List of location IDs for which to fetch POIs'),
4
+ });
5
+ export const LocalDescriptionsParams = z.object({
6
+ ids: z.array(z.string()).describe('List of location IDs for which to fetch descriptions'),
7
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import params from './params.js';
2
+ import API from '../../BraveAPI/index.js';
3
+ import { stringify } from '../../utils.js';
4
+ export const name = 'brave_news_search';
5
+ export const annotations = {
6
+ title: 'Brave News Search',
7
+ openWorldHint: true,
8
+ };
9
+ export const description = `
10
+ This tool searches for news articles using Brave's News Search API based on the user's query. Use it when you need current news information, breaking news updates, or articles about specific topics, events, or entities.
11
+
12
+ When to use:
13
+ - Finding recent news articles on specific topics
14
+ - Getting breaking news updates
15
+ - Researching current events or trending stories
16
+ - Gathering news sources and headlines for analysis
17
+
18
+ Returns a JSON list of news-related results with title, url, and description. Some results may contain snippets of text from the article.
19
+
20
+ When relaying results in markdown-supporting environments, always cite sources with hyperlinks.
21
+
22
+ Examples:
23
+ - "According to [Reuters](https://www.reuters.com/technology/china-bans/), China bans uncertified and recalled power banks on planes".
24
+ - "The [New York Times](https://www.nytimes.com/2025/06/27/us/technology/ev-sales.html) reports that Tesla's EV sales have increased by 20%".
25
+ - "According to [BBC News](https://www.bbc.com/news/world-europe-65910000), the UK government has announced a new policy to support renewable energy".
26
+ `;
27
+ export const execute = async (params) => {
28
+ const response = await API.issueRequest('news', params);
29
+ return {
30
+ content: response.results.map((newsResult) => {
31
+ return {
32
+ type: 'text',
33
+ text: stringify({
34
+ url: newsResult.url,
35
+ title: newsResult.title,
36
+ age: newsResult.age,
37
+ page_age: newsResult.page_age,
38
+ breaking: newsResult.breaking ?? false,
39
+ description: newsResult.description,
40
+ extra_snippets: newsResult.extra_snippets,
41
+ thumbnail: newsResult.thumbnail?.src,
42
+ }),
43
+ };
44
+ }),
45
+ };
46
+ };
47
+ export default {
48
+ name,
49
+ description,
50
+ annotations,
51
+ inputSchema: params.shape,
52
+ execute,
53
+ };
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ export const params = z.object({
3
+ query: z
4
+ .string()
5
+ .max(400)
6
+ .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words')
7
+ .describe('Search query (max 400 chars, 50 words)'),
8
+ country: z
9
+ .string()
10
+ .default('US')
11
+ .describe('Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.')
12
+ .optional(),
13
+ search_lang: z
14
+ .string()
15
+ .default('en')
16
+ .describe('Search language preference. The 2 or more character language code for which the search results are provided.')
17
+ .optional(),
18
+ ui_lang: z
19
+ .string()
20
+ .default('en-US')
21
+ .describe('User interface language preferred in response. Usually of the format <language_code>-<country_code>. For more, see RFC 9110.')
22
+ .optional(),
23
+ count: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .max(50)
28
+ .default(20)
29
+ .describe('Number of results (1-50, default 20)')
30
+ .optional(),
31
+ offset: z
32
+ .number()
33
+ .int()
34
+ .min(0)
35
+ .max(9)
36
+ .default(0)
37
+ .describe('Pagination offset (max 9, default 0)')
38
+ .optional(),
39
+ spellcheck: z
40
+ .boolean()
41
+ .default(true)
42
+ .describe('Whether to spellcheck provided query.')
43
+ .optional(),
44
+ safesearch: z
45
+ .union([z.literal('off'), z.literal('moderate'), z.literal('strict')])
46
+ .default('moderate')
47
+ .describe("Filters search results for adult content. The following values are supported: 'off' - No filtering. 'moderate' - Filter out explicit content. 'strict' - Filter out explicit and suggestive content. The default value is 'moderate'.")
48
+ .optional(),
49
+ freshness: z
50
+ .union([z.literal('pd'), z.literal('pw'), z.literal('pm'), z.literal('py'), z.string()])
51
+ .default('pd')
52
+ .describe("Filters search results by when they were discovered. The following values are supported: 'pd' - Discovered within the last 24 hours. 'pw' - Discovered within the last 7 Days. 'pm' - Discovered within the last 31 Days. 'py' - Discovered within the last 365 Days. 'YYYY-MM-DDtoYYYY-MM-DD' - Timeframe is also supported by specifying the date range e.g. 2022-04-01to2022-07-30.")
53
+ .optional(),
54
+ extra_snippets: z
55
+ .boolean()
56
+ .default(false)
57
+ .describe('A snippet is an excerpt from a page you get as a result of the query, and extra_snippets allow you to get up to 5 additional, alternative excerpts. Only available under Free AI, Base AI, Pro AI, Base Data, Pro Data and Custom plans.')
58
+ .optional(),
59
+ goggles: z
60
+ .array(z.string())
61
+ .describe("Goggles act as a custom re-ranking on top of Brave's search index. The parameter supports both a url where the Goggle is hosted or the definition of the Goggle. For more details, refer to the Goggles repository (i.e., https://github.com/brave/goggles-quickstart).")
62
+ .optional(),
63
+ });
64
+ export default params;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { summarizerQueryParams } from './params.js';
2
+ import API from '../../BraveAPI/index.js';
3
+ import { log } from '../../utils.js';
4
+ export const name = 'brave_summarizer';
5
+ export const annotations = {
6
+ title: 'Brave Summarizer',
7
+ openWorldHint: true,
8
+ };
9
+ export const description = `
10
+ Retrieves AI-generated summaries of web search results using Brave's Summarizer API. This tool processes search results to create concise, coherent summaries of information gathered from multiple sources.
11
+
12
+ When to use:
13
+
14
+ - When you need a concise overview of complex topics from multiple sources
15
+ - For quick fact-checking or getting key points without reading full articles
16
+ - When providing users with summarized information that synthesizes various perspectives
17
+ - For research tasks requiring distilled information from web searches
18
+
19
+ Returns a text summary that consolidates information from the search results. Optional features include inline references to source URLs and additional entity information.
20
+
21
+ Requirements: Must first perform a web search using brave_web_search with summary=true parameter. Requires a Pro AI subscription to access the summarizer functionality.
22
+ `;
23
+ export const execute = async (params) => {
24
+ const response = { content: [], isError: false };
25
+ try {
26
+ const { summary } = await pollForSummary(params);
27
+ if (!summary || summary.length === 0) {
28
+ response.isError = true;
29
+ response.content.push({
30
+ type: 'text',
31
+ text: 'Unable to retrieve a Summarizer summary.',
32
+ });
33
+ }
34
+ else {
35
+ const summaryText = summary
36
+ .map((summary_part) => {
37
+ if (summary_part.type === 'token') {
38
+ return summary_part.data;
39
+ }
40
+ else if (summary_part.type === 'inline_reference') {
41
+ return ` (${summary_part.data?.url})`;
42
+ }
43
+ else {
44
+ return '';
45
+ }
46
+ })
47
+ .join('');
48
+ response.content.push({
49
+ type: 'text',
50
+ text: summaryText,
51
+ });
52
+ }
53
+ }
54
+ catch (error) {
55
+ response.isError = true;
56
+ response.content.push({
57
+ type: 'text',
58
+ text: 'Unable to retrieve a Summarizer summary.',
59
+ });
60
+ }
61
+ return response;
62
+ };
63
+ const pollForSummary = async (params, pollInterval = 50, attempts = 20) => {
64
+ let result = null;
65
+ while (!result && attempts > 0) {
66
+ try {
67
+ const response = await API.issueRequest('summarizer', params);
68
+ if (response.status === 'complete') {
69
+ result = response;
70
+ }
71
+ }
72
+ catch (error) {
73
+ await log('error', `Error polling for summarizer results: ${error}`);
74
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
75
+ }
76
+ attempts--;
77
+ }
78
+ if (!result) {
79
+ throw new Error('Summarizer summary could not be retrieved after multiple attempts.');
80
+ }
81
+ return result;
82
+ };
83
+ export default {
84
+ name,
85
+ description,
86
+ annotations,
87
+ inputSchema: summarizerQueryParams.shape,
88
+ execute,
89
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ export const summarizerQueryParams = z.object({
3
+ key: z
4
+ .string()
5
+ .describe('The key is equal to value of field key as part of the Summarizer response model.'),
6
+ entity_info: z
7
+ .boolean()
8
+ .default(false)
9
+ .describe('Returns extra entities info with the summary response.')
10
+ .optional(),
11
+ inline_references: z
12
+ .boolean()
13
+ .default(false)
14
+ .describe('Adds inline references to the summary response.')
15
+ .optional(),
16
+ });
17
+ export const chatCompletionsMessage = z.object({
18
+ role: z
19
+ .enum(['user'])
20
+ .default('user')
21
+ .describe('The role of the message. Only "user" is supported for now.'),
22
+ content: z
23
+ .string()
24
+ .describe('The content of the message. The value is the question to be answered.'),
25
+ });
26
+ export const chatCompletionParams = z.object({
27
+ messages: z
28
+ .array(chatCompletionsMessage)
29
+ .describe('The messages to use for the chat completion. The value is a list of ChatCompletionsMessage response models.'),
30
+ model: z
31
+ .enum(['brave-pro', 'brave'])
32
+ .default('brave-pro')
33
+ .optional()
34
+ .describe('The model to use for the chat completion. The value can be "brave-pro" (default) or "brave".'),
35
+ stream: z
36
+ .boolean()
37
+ .default(true)
38
+ .optional()
39
+ .describe('Whether to stream the response. The value is `true` by default. When using the OpenAI CLI, use `openai.AsyncOpenAI` for streaming and `openai.OpenAI` for blocking mode.'),
40
+ country: z
41
+ .string()
42
+ .default('us')
43
+ .optional()
44
+ .describe('The country backend to use for the chat completion. The value is "us" by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.'),
45
+ language: z
46
+ .string()
47
+ .default('en')
48
+ .optional()
49
+ .describe('The language to use for the chat completion. The value is "en" by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.'),
50
+ enable_entities: z
51
+ .boolean()
52
+ .default(false)
53
+ .optional()
54
+ .describe('Whether to enable entities in the chat completion. The value is `false` by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.'),
55
+ enable_citations: z
56
+ .boolean()
57
+ .default(false)
58
+ .optional()
59
+ .describe('Whether to enable citations in the chat completion. The value is `false` by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.'),
60
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import params from './params.js';
2
+ import API from '../../BraveAPI/index.js';
3
+ import { stringify } from '../../utils.js';
4
+ export const name = 'brave_video_search';
5
+ export const annotations = {
6
+ title: 'Brave Video Search',
7
+ openWorldHint: true,
8
+ };
9
+ export const description = `
10
+ Searches for videos using Brave's Video Search API and returns structured video results with metadata.
11
+
12
+ When to use:
13
+ - When you need to find videos related to a specific topic, keyword, or query.
14
+ - Useful for discovering video content, getting video metadata, or finding videos from specific creators/publishers.
15
+
16
+ Returns a JSON list of video-related results with title, url, description, duration, and thumbnail_url.
17
+ `;
18
+ export const execute = async (params) => {
19
+ const response = await API.issueRequest('videos', params);
20
+ return {
21
+ content: response.results.map(({ url, title, description, video, thumbnail }) => {
22
+ const duration = video?.duration;
23
+ const thumbnail_url = thumbnail?.src;
24
+ return {
25
+ type: 'text',
26
+ text: stringify({ url, title, description, duration, thumbnail_url }),
27
+ };
28
+ }),
29
+ };
30
+ };
31
+ export default {
32
+ name,
33
+ description,
34
+ annotations,
35
+ inputSchema: params.shape,
36
+ execute,
37
+ };
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ export const params = z.object({
3
+ query: z
4
+ .string()
5
+ .min(1)
6
+ .max(400)
7
+ .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words')
8
+ .describe("The user's search query. Query cannot be empty. Limited to 400 characters and 50 words."),
9
+ country: z
10
+ .string()
11
+ .default('US')
12
+ .describe('Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.')
13
+ .optional(),
14
+ search_lang: z
15
+ .string()
16
+ .default('en')
17
+ .describe('Search language preference. The 2 or more character language code for which the search results are provided.')
18
+ .optional(),
19
+ ui_lang: z
20
+ .string()
21
+ .default('en-US')
22
+ .describe('User interface language preferred in response. Usually of the format <language_code>-<country_code>. For more, see RFC 9110.')
23
+ .optional(),
24
+ count: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .max(50)
29
+ .default(20)
30
+ .describe('Number of results (1-50, default 20). Combine this parameter with `offset` to paginate search results.')
31
+ .optional(),
32
+ offset: z
33
+ .number()
34
+ .int()
35
+ .min(0)
36
+ .max(9)
37
+ .default(0)
38
+ .describe('Pagination offset (max 9, default 0). Combine this parameter with `count` to paginate search results.')
39
+ .optional(),
40
+ spellcheck: z
41
+ .boolean()
42
+ .describe('Whether to spellcheck provided query.')
43
+ .default(true)
44
+ .optional(),
45
+ safesearch: z
46
+ .union([z.literal('off'), z.literal('moderate'), z.literal('strict')])
47
+ .default('moderate')
48
+ .describe("Filters search results for adult content. The following values are supported: 'off' - No filtering. 'moderate' - Filter out explicit content. 'strict' - Filter out explicit and suggestive content. The default value is 'moderate'.")
49
+ .optional(),
50
+ freshness: z
51
+ .union([z.literal('pd'), z.literal('pw'), z.literal('pm'), z.literal('py'), z.string()])
52
+ .describe("Filters search results by when they were discovered. The following values are supported: 'pd' - Discovered within the last 24 hours. 'pw' - Discovered within the last 7 days. 'pm' - Discovered within the last 31 days. 'py' - Discovered within the last 365 days. 'YYYY-MM-DDtoYYYY-MM-DD' - timeframe is also supported by specifying the date range (e.g. '2022-04-01to2022-07-30').")
53
+ .optional(),
54
+ });
55
+ export default params;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,140 @@
1
+ import params from './params.js';
2
+ import API from '../../BraveAPI/index.js';
3
+ import { stringify } from '../../utils.js';
4
+ export const name = 'brave_web_search';
5
+ export const annotations = {
6
+ title: 'Brave Web Search',
7
+ openWorldHint: true,
8
+ };
9
+ export const description = `
10
+ Performs web searches using the Brave Search API and returns comprehensive search results with rich metadata.
11
+
12
+ When to use:
13
+ - General web searches for information, facts, or current topics
14
+ - Location-based queries (restaurants, businesses, points of interest)
15
+ - News searches for recent events or breaking stories
16
+ - Finding videos, discussions, or FAQ content
17
+ - Research requiring diverse result types (web pages, images, reviews, etc.)
18
+
19
+ Returns a JSON list of web results with title, description, and URL.
20
+
21
+ When the "results_filter" parameter is empty, JSON results may also contain FAQ, Discussions, News, and Video results.
22
+ `;
23
+ export const execute = async (params) => {
24
+ const response = { content: [], isError: false };
25
+ const { web, faq, discussions, news, videos, summarizer } = await API.issueRequest('web', params);
26
+ if (summarizer) {
27
+ response.content.push({
28
+ type: 'text',
29
+ text: `Summarizer key: ${summarizer.key}`,
30
+ });
31
+ }
32
+ if (!web || !Array.isArray(web.results) || web.results.length < 1) {
33
+ response.isError = true;
34
+ response.content.push({
35
+ type: 'text',
36
+ text: 'No web results found',
37
+ });
38
+ return response;
39
+ }
40
+ // TODO (Sampson): The following is unnecessarily repetitive.
41
+ if (web && web.results?.length > 0) {
42
+ for (const entry of formatWebResults(web)) {
43
+ response.content.push({
44
+ type: 'text',
45
+ text: stringify(entry),
46
+ });
47
+ }
48
+ }
49
+ if (faq && faq.results?.length > 0) {
50
+ for (const entry of formatFAQResults(faq)) {
51
+ response.content.push({
52
+ type: 'text',
53
+ text: stringify(entry),
54
+ });
55
+ }
56
+ }
57
+ if (discussions && discussions.results?.length > 0) {
58
+ for (const entry of formatDiscussionsResults(discussions)) {
59
+ response.content.push({
60
+ type: 'text',
61
+ text: stringify(entry),
62
+ });
63
+ }
64
+ }
65
+ if (news && news.results?.length > 0) {
66
+ for (const entry of formatNewsResults(news)) {
67
+ response.content.push({
68
+ type: 'text',
69
+ text: stringify(entry),
70
+ });
71
+ }
72
+ }
73
+ if (videos && videos.results?.length > 0) {
74
+ for (const entry of formatVideoResults(videos)) {
75
+ response.content.push({
76
+ type: 'text',
77
+ text: stringify(entry),
78
+ });
79
+ }
80
+ }
81
+ return response;
82
+ };
83
+ export const formatWebResults = (web) => {
84
+ return (web.results || []).map(({ url, title, description, extra_snippets }) => ({
85
+ url,
86
+ title,
87
+ description,
88
+ extra_snippets,
89
+ }));
90
+ };
91
+ const formatFAQResults = (faq) => {
92
+ return (faq.results || []).map(({ question, answer, title, url }) => ({
93
+ question,
94
+ answer,
95
+ title,
96
+ url,
97
+ }));
98
+ };
99
+ const formatDiscussionsResults = (discussions) => {
100
+ return (discussions.results || []).map(({ url, data }) => ({
101
+ mutated_by_goggles: discussions.mutated_by_goggles,
102
+ url,
103
+ data,
104
+ }));
105
+ };
106
+ const formatNewsResults = (news) => {
107
+ return (news.results || []).map(({ source, breaking, is_live, age, url, title, description, extra_snippets }) => ({
108
+ mutated_by_goggles: news.mutated_by_goggles,
109
+ source,
110
+ breaking,
111
+ is_live,
112
+ age,
113
+ url,
114
+ title,
115
+ description,
116
+ extra_snippets,
117
+ }));
118
+ };
119
+ const formatVideoResults = (videos) => {
120
+ return (videos.results || []).map(({ url, age, title, description, video, thumbnail }) => ({
121
+ mutated_by_goggles: videos.mutated_by_goggles,
122
+ url,
123
+ title,
124
+ description,
125
+ age,
126
+ thumbnail_url: thumbnail?.src,
127
+ duration: video.duration,
128
+ view_count: video.views,
129
+ creator: video.creator,
130
+ publisher: video.publisher,
131
+ tags: video.tags,
132
+ }));
133
+ };
134
+ export default {
135
+ name,
136
+ description,
137
+ annotations,
138
+ inputSchema: params.shape,
139
+ execute,
140
+ };