@humbletoes/google-search 1.0.0

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.
Files changed (47) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +339 -0
  3. package/bin/google-search +3 -0
  4. package/bin/google-search-mcp +3 -0
  5. package/bin/google-search-mcp.cmd +2 -0
  6. package/bin/google-search.cmd +2 -0
  7. package/dist/browser-config.d.ts +41 -0
  8. package/dist/browser-config.js +96 -0
  9. package/dist/browser-config.js.map +1 -0
  10. package/dist/browser-pool.d.ts +13 -0
  11. package/dist/browser-pool.js +37 -0
  12. package/dist/browser-pool.js.map +1 -0
  13. package/dist/cache.d.ts +48 -0
  14. package/dist/cache.js +111 -0
  15. package/dist/cache.js.map +1 -0
  16. package/dist/errors.d.ts +26 -0
  17. package/dist/errors.js +48 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/filters.d.ts +48 -0
  20. package/dist/filters.js +192 -0
  21. package/dist/filters.js.map +1 -0
  22. package/dist/html-cleaner.d.ts +62 -0
  23. package/dist/html-cleaner.js +236 -0
  24. package/dist/html-cleaner.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +59 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logger.d.ts +2 -0
  29. package/dist/logger.js +41 -0
  30. package/dist/logger.js.map +1 -0
  31. package/dist/mcp-server.d.ts +9 -0
  32. package/dist/mcp-server.js +822 -0
  33. package/dist/mcp-server.js.map +1 -0
  34. package/dist/search.d.ts +18 -0
  35. package/dist/search.js +1080 -0
  36. package/dist/search.js.map +1 -0
  37. package/dist/types.d.ts +67 -0
  38. package/dist/types.js +2 -0
  39. package/dist/types.js.map +1 -0
  40. package/dist/validation.d.ts +6 -0
  41. package/dist/validation.js +23 -0
  42. package/dist/validation.js.map +1 -0
  43. package/dist/web-fetcher.d.ts +10 -0
  44. package/dist/web-fetcher.js +179 -0
  45. package/dist/web-fetcher.js.map +1 -0
  46. package/package.json +67 -0
  47. package/scripts/setup.js +53 -0
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Google Search MCP Server v2.0
4
+ *
5
+ * Provides two tools:
6
+ * - google-search: Smart web search and content fetcher
7
+ * - get_code_context: Programming documentation and code examples search
8
+ */
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { z } from "zod";
12
+ import { googleSearch } from "./search.js";
13
+ import { fetchWebContent } from "./web-fetcher.js";
14
+ import * as os from "os";
15
+ import * as path from "path";
16
+ import logger from "./logger.js";
17
+ import { browserPool } from "./browser-pool.js";
18
+ import { SearchCache } from "./cache.js";
19
+ import { InputValidator } from "./validation.js";
20
+ import { RetryManager } from "./errors.js";
21
+ import { spawn } from "child_process";
22
+ // =============================================================================
23
+ // CONSTANTS & CONFIGURATION
24
+ // =============================================================================
25
+ const SERVER_VERSION = "1.0.0";
26
+ const CONTEXT7_API_BASE = "https://context7.com/api";
27
+ const CONTEXT7_TIMEOUT = 5000;
28
+ const DEFAULT_SEARCH_LIMIT = 20;
29
+ const DEFAULT_FETCH_TIMEOUT = 30000;
30
+ const DEFAULT_SEARCH_TIMEOUT = 60000;
31
+ const DEFAULT_CODE_CONTEXT_RESULTS = 5;
32
+ const DEFAULT_CODE_CONTEXT_TOKENS = 3000;
33
+ const CHARS_PER_TOKEN = 4;
34
+ // Browser state file path
35
+ const STATE_FILE_PATH = path.join(os.homedir(), ".google-search-browser-state.json");
36
+ // Global cache instance (singleton)
37
+ const searchCache = new SearchCache();
38
+ // =============================================================================
39
+ // LIBRARY CONFIGURATION
40
+ // =============================================================================
41
+ /** Known libraries for Context7 lookup and doc site targeting */
42
+ const KNOWN_LIBRARIES = {
43
+ // Frontend Frameworks
44
+ 'react': { canonical: 'react', docSites: 'site:react.dev OR site:reactjs.org' },
45
+ 'next': { canonical: 'next.js', docSites: 'site:nextjs.org' },
46
+ 'nextjs': { canonical: 'next.js', docSites: 'site:nextjs.org' },
47
+ 'vue': { canonical: 'vue', docSites: 'site:vuejs.org' },
48
+ 'angular': { canonical: 'angular', docSites: 'site:angular.io' },
49
+ 'svelte': { canonical: 'svelte', docSites: 'site:svelte.dev' },
50
+ 'solid': { canonical: 'solid', docSites: 'site:solidjs.com' },
51
+ // Backend Frameworks
52
+ 'express': { canonical: 'express', docSites: 'site:expressjs.com' },
53
+ 'fastify': { canonical: 'fastify', docSites: 'site:fastify.io' },
54
+ 'koa': { canonical: 'koa' },
55
+ 'hono': { canonical: 'hono', docSites: 'site:hono.dev' },
56
+ 'nest': { canonical: 'nestjs', docSites: 'site:docs.nestjs.com' },
57
+ 'nestjs': { canonical: 'nestjs', docSites: 'site:docs.nestjs.com' },
58
+ // Python
59
+ 'python': { canonical: 'python', docSites: 'site:docs.python.org' },
60
+ 'django': { canonical: 'django', docSites: 'site:docs.djangoproject.com' },
61
+ 'flask': { canonical: 'flask', docSites: 'site:flask.palletsprojects.com' },
62
+ 'fastapi': { canonical: 'fastapi', docSites: 'site:fastapi.tiangolo.com' },
63
+ 'pandas': { canonical: 'pandas', docSites: 'site:pandas.pydata.org' },
64
+ 'numpy': { canonical: 'numpy', docSites: 'site:numpy.org' },
65
+ // Rust
66
+ 'rust': { canonical: 'rust', docSites: 'site:doc.rust-lang.org OR site:docs.rs' },
67
+ 'tokio': { canonical: 'tokio', docSites: 'site:tokio.rs' },
68
+ 'actix': { canonical: 'actix', docSites: 'site:actix.rs' },
69
+ 'axum': { canonical: 'axum' },
70
+ // Go
71
+ 'go': { canonical: 'go', docSites: 'site:go.dev OR site:golang.org' },
72
+ 'golang': { canonical: 'go', docSites: 'site:go.dev OR site:golang.org' },
73
+ 'gin': { canonical: 'gin' },
74
+ 'echo': { canonical: 'echo' },
75
+ 'fiber': { canonical: 'fiber' },
76
+ // Languages & Typing
77
+ 'typescript': { canonical: 'typescript', docSites: 'site:typescriptlang.org' },
78
+ 'node': { canonical: 'node.js', docSites: 'site:nodejs.org' },
79
+ 'nodejs': { canonical: 'node.js', docSites: 'site:nodejs.org' },
80
+ 'deno': { canonical: 'deno', docSites: 'site:deno.land' },
81
+ 'bun': { canonical: 'bun', docSites: 'site:bun.sh' },
82
+ // Validation & Schema
83
+ 'zod': { canonical: 'zod' },
84
+ 'trpc': { canonical: 'trpc', docSites: 'site:trpc.io' },
85
+ 'graphql': { canonical: 'graphql', docSites: 'site:graphql.org' },
86
+ 'apollo': { canonical: 'apollo', docSites: 'site:apollographql.com' },
87
+ // CSS & Styling
88
+ 'tailwind': { canonical: 'tailwindcss', docSites: 'site:tailwindcss.com' },
89
+ 'tailwindcss': { canonical: 'tailwindcss', docSites: 'site:tailwindcss.com' },
90
+ // Database & ORM
91
+ 'prisma': { canonical: 'prisma', docSites: 'site:prisma.io' },
92
+ 'drizzle': { canonical: 'drizzle', docSites: 'site:orm.drizzle.team' },
93
+ 'typeorm': { canonical: 'typeorm' },
94
+ 'sequelize': { canonical: 'sequelize' },
95
+ 'mongodb': { canonical: 'mongodb', docSites: 'site:mongodb.com/docs' },
96
+ 'mongoose': { canonical: 'mongoose', docSites: 'site:mongoosejs.com' },
97
+ 'postgres': { canonical: 'postgresql', docSites: 'site:postgresql.org/docs' },
98
+ 'postgresql': { canonical: 'postgresql', docSites: 'site:postgresql.org/docs' },
99
+ 'redis': { canonical: 'redis', docSites: 'site:redis.io' },
100
+ 'sqlite': { canonical: 'sqlite', docSites: 'site:sqlite.org' },
101
+ // Cloud & Infrastructure
102
+ 'docker': { canonical: 'docker', docSites: 'site:docs.docker.com' },
103
+ 'kubernetes': { canonical: 'kubernetes', docSites: 'site:kubernetes.io/docs' },
104
+ 'k8s': { canonical: 'kubernetes', docSites: 'site:kubernetes.io/docs' },
105
+ 'aws': { canonical: 'aws', docSites: 'site:docs.aws.amazon.com' },
106
+ 'azure': { canonical: 'azure', docSites: 'site:docs.microsoft.com/azure' },
107
+ 'gcp': { canonical: 'gcp', docSites: 'site:cloud.google.com/docs' },
108
+ 'firebase': { canonical: 'firebase', docSites: 'site:firebase.google.com/docs' },
109
+ 'supabase': { canonical: 'supabase', docSites: 'site:supabase.com/docs' },
110
+ 'vercel': { canonical: 'vercel', docSites: 'site:vercel.com/docs' },
111
+ 'cloudflare': { canonical: 'cloudflare', docSites: 'site:developers.cloudflare.com' },
112
+ // APIs & Services
113
+ 'stripe': { canonical: 'stripe', docSites: 'site:stripe.com/docs' },
114
+ 'openai': { canonical: 'openai', docSites: 'site:platform.openai.com/docs' },
115
+ 'anthropic': { canonical: 'anthropic', docSites: 'site:docs.anthropic.com' },
116
+ 'langchain': { canonical: 'langchain', docSites: 'site:langchain.com' },
117
+ // Testing
118
+ 'playwright': { canonical: 'playwright', docSites: 'site:playwright.dev' },
119
+ 'puppeteer': { canonical: 'puppeteer', docSites: 'site:pptr.dev' },
120
+ 'cypress': { canonical: 'cypress', docSites: 'site:docs.cypress.io' },
121
+ 'vitest': { canonical: 'vitest', docSites: 'site:vitest.dev' },
122
+ 'jest': { canonical: 'jest', docSites: 'site:jestjs.io' },
123
+ // Build Tools
124
+ 'webpack': { canonical: 'webpack', docSites: 'site:webpack.js.org' },
125
+ 'vite': { canonical: 'vite', docSites: 'site:vitejs.dev' },
126
+ 'esbuild': { canonical: 'esbuild', docSites: 'site:esbuild.github.io' },
127
+ 'rollup': { canonical: 'rollup', docSites: 'site:rollupjs.org' },
128
+ 'turbopack': { canonical: 'turbopack' },
129
+ // State Management
130
+ 'redux': { canonical: 'redux', docSites: 'site:redux.js.org' },
131
+ 'zustand': { canonical: 'zustand' },
132
+ 'jotai': { canonical: 'jotai' },
133
+ 'recoil': { canonical: 'recoil' },
134
+ 'tanstack': { canonical: 'tanstack', docSites: 'site:tanstack.com' },
135
+ 'react-query': { canonical: 'tanstack-query', docSites: 'site:tanstack.com' },
136
+ 'swr': { canonical: 'swr', docSites: 'site:swr.vercel.app' },
137
+ // Animation
138
+ 'framer': { canonical: 'framer-motion', docSites: 'site:framer.com/motion' },
139
+ 'motion': { canonical: 'framer-motion', docSites: 'site:framer.com/motion' },
140
+ 'gsap': { canonical: 'gsap', docSites: 'site:gsap.com' },
141
+ // Graphics & Visualization
142
+ 'three': { canonical: 'three.js', docSites: 'site:threejs.org' },
143
+ 'threejs': { canonical: 'three.js', docSites: 'site:threejs.org' },
144
+ 'd3': { canonical: 'd3', docSites: 'site:d3js.org' },
145
+ 'chart': { canonical: 'chart.js', docSites: 'site:chartjs.org' },
146
+ };
147
+ /** Priority scores for code-relevant domains */
148
+ const DOMAIN_PRIORITIES = {
149
+ 'github.com': 10,
150
+ 'stackoverflow.com': 9,
151
+ 'docs.': 8,
152
+ 'documentation': 7,
153
+ 'developer.': 7,
154
+ 'dev.to': 6,
155
+ 'medium.com': 5,
156
+ 'npmjs.com': 5,
157
+ 'pypi.org': 5,
158
+ 'crates.io': 5,
159
+ 'pkg.go.dev': 5,
160
+ };
161
+ // =============================================================================
162
+ // UTILITY FUNCTIONS
163
+ // =============================================================================
164
+ /**
165
+ * Detect if input is a URL or a search query
166
+ */
167
+ function isUrl(input) {
168
+ const trimmed = input.trim();
169
+ // Check for explicit protocol
170
+ try {
171
+ const url = new URL(trimmed);
172
+ return url.protocol === 'http:' || url.protocol === 'https:';
173
+ }
174
+ catch {
175
+ // Check for common URL patterns without protocol
176
+ return /^(www\.)?[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+([\/\?#].*)?$/.test(trimmed);
177
+ }
178
+ }
179
+ /**
180
+ * Normalize URL (add https:// if missing)
181
+ */
182
+ function normalizeUrl(input) {
183
+ const trimmed = input.trim();
184
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
185
+ return trimmed;
186
+ }
187
+ return `https://${trimmed}`;
188
+ }
189
+ /**
190
+ * Extract library name from query for enhanced search
191
+ */
192
+ function extractLibraryInfo(query) {
193
+ const lowerQuery = query.toLowerCase();
194
+ for (const [key, info] of Object.entries(KNOWN_LIBRARIES)) {
195
+ // Match whole word or with common separators
196
+ const pattern = new RegExp(`\\b${key}\\b|${key}[\\s.-]|[\\s.-]${key}`, 'i');
197
+ if (pattern.test(lowerQuery) || lowerQuery.includes(key)) {
198
+ return info;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ /**
204
+ * Deduplicate results by URL
205
+ */
206
+ function deduplicateResults(results) {
207
+ const seen = new Set();
208
+ return results.filter(r => {
209
+ const url = r.link?.toLowerCase();
210
+ if (!url || seen.has(url))
211
+ return false;
212
+ seen.add(url);
213
+ return true;
214
+ });
215
+ }
216
+ /**
217
+ * Prioritize results likely to contain code/documentation
218
+ */
219
+ function prioritizeCodeResults(results) {
220
+ return [...results].sort((a, b) => {
221
+ const getScore = (link) => {
222
+ const lower = link.toLowerCase();
223
+ return Object.entries(DOMAIN_PRIORITIES).reduce((score, [domain, priority]) => lower.includes(domain) ? score + priority : score, 0);
224
+ };
225
+ return getScore(b.link) - getScore(a.link);
226
+ });
227
+ }
228
+ /**
229
+ * Fetch with timeout wrapper
230
+ */
231
+ async function fetchWithTimeout(url, options, timeout) {
232
+ const controller = new AbortController();
233
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
234
+ try {
235
+ const response = await fetch(url, { ...options, signal: controller.signal });
236
+ return response;
237
+ }
238
+ finally {
239
+ clearTimeout(timeoutId);
240
+ }
241
+ }
242
+ // =============================================================================
243
+ // CONTEXT7 INTEGRATION
244
+ // =============================================================================
245
+ /**
246
+ * Resolve library name to Context7 library ID
247
+ */
248
+ async function resolveContext7Library(libraryName, query) {
249
+ try {
250
+ const response = await fetchWithTimeout(`${CONTEXT7_API_BASE}/v1/resolve`, {
251
+ method: "POST",
252
+ headers: { "Content-Type": "application/json" },
253
+ body: JSON.stringify({ libraryName, query }),
254
+ }, CONTEXT7_TIMEOUT);
255
+ if (!response.ok)
256
+ return null;
257
+ const data = await response.json();
258
+ if (data.libraries?.length > 0) {
259
+ const lib = data.libraries[0];
260
+ return {
261
+ id: lib.id,
262
+ title: lib.title,
263
+ description: lib.description,
264
+ codeSnippets: lib.codeSnippets,
265
+ reputation: lib.reputation,
266
+ benchmarkScore: lib.benchmarkScore,
267
+ };
268
+ }
269
+ return null;
270
+ }
271
+ catch (error) {
272
+ logger.debug({ error, libraryName }, "Context7 resolve failed");
273
+ return null;
274
+ }
275
+ }
276
+ /**
277
+ * Query Context7 documentation
278
+ */
279
+ async function queryContext7Docs(libraryId, query) {
280
+ try {
281
+ const response = await fetchWithTimeout(`${CONTEXT7_API_BASE}/v1/query`, {
282
+ method: "POST",
283
+ headers: { "Content-Type": "application/json" },
284
+ body: JSON.stringify({ libraryId, query }),
285
+ }, CONTEXT7_TIMEOUT);
286
+ if (!response.ok)
287
+ return [];
288
+ const data = await response.json();
289
+ return (data.results || []).map((r) => ({
290
+ title: r.title || "Documentation",
291
+ source: r.source || r.url || "",
292
+ content: r.content || r.text || "",
293
+ code: r.code || r.snippet || "",
294
+ }));
295
+ }
296
+ catch (error) {
297
+ logger.debug({ error, libraryId }, "Context7 query failed");
298
+ return [];
299
+ }
300
+ }
301
+ // =============================================================================
302
+ // FORMATTING FUNCTIONS
303
+ // =============================================================================
304
+ /**
305
+ * Format search results in a condensed, token-efficient format
306
+ */
307
+ function formatSearchResults(results, condensed) {
308
+ if (!results.results?.length) {
309
+ return `No results for "${results.query}"`;
310
+ }
311
+ const lines = [
312
+ `# ${results.query}`,
313
+ `${results.results.length} results • ${results.searchTime || 0}ms${results.fromCache ? ' (cached)' : ''}`,
314
+ '',
315
+ ];
316
+ results.results.forEach((r, idx) => {
317
+ const num = idx + 1;
318
+ if (condensed) {
319
+ lines.push(`${num}. [${r.title}](${r.link})`);
320
+ }
321
+ else {
322
+ lines.push(`**${num}. [${r.title}](${r.link})**`);
323
+ if (r.snippet) {
324
+ const cleanSnippet = r.snippet
325
+ .replace(/\d{4}年\d{1,2}月\d{1,2}日\s*—\s*/g, '')
326
+ .replace(/\s+/g, ' ')
327
+ .trim();
328
+ if (cleanSnippet) {
329
+ lines.push(` ${cleanSnippet}`);
330
+ }
331
+ }
332
+ lines.push('');
333
+ }
334
+ });
335
+ return lines.join('\n');
336
+ }
337
+ /**
338
+ * Format web fetch results in a condensed format
339
+ */
340
+ function formatWebContent(result, maxLength) {
341
+ if (result.error) {
342
+ return `Error: ${result.url} - ${result.error}`;
343
+ }
344
+ const lines = [
345
+ `# ${result.title || 'Untitled'}`,
346
+ `${result.url} • ${result.wordCount || 0} words • ${result.fetchTime || 0}ms`,
347
+ '---',
348
+ '',
349
+ ];
350
+ let content = result.content || '';
351
+ if (maxLength && content.length > maxLength) {
352
+ content = content.substring(0, maxLength) + '\n\n[Content truncated...]';
353
+ }
354
+ lines.push(content);
355
+ return lines.join('\n');
356
+ }
357
+ /**
358
+ * Extract code-relevant content from page text
359
+ */
360
+ function extractCodeRelevantContent(content, maxLength) {
361
+ const lines = content.split('\n');
362
+ const relevantLines = [];
363
+ let charCount = 0;
364
+ const codePatterns = [
365
+ /^(import|export|const|let|var|function|class|def|async|await|return|if|for|while)\b/i,
366
+ /^(npm|yarn|pnpm|pip|cargo|go\s+get|brew|apt)\s+/i,
367
+ /[{}()[\]];$/,
368
+ /=>/,
369
+ /\$\{.*\}/,
370
+ /<[A-Z][a-zA-Z0-9]*[\s/>]/,
371
+ /^\s*@\w+/,
372
+ ];
373
+ const skipPatterns = [
374
+ /^(cookie|privacy|terms|copyright|©|all rights reserved)/i,
375
+ /^(sign up|log in|subscribe|newsletter|follow us)/i,
376
+ /^(advertisement|sponsored|ad\s)/i,
377
+ /^(share|tweet|facebook|linkedin|email this)/i,
378
+ ];
379
+ for (const line of lines) {
380
+ const trimmed = line.trim();
381
+ if (!trimmed || trimmed.length < 3)
382
+ continue;
383
+ if (skipPatterns.some(p => p.test(trimmed)))
384
+ continue;
385
+ const hasCode = codePatterns.some(p => p.test(trimmed));
386
+ if (charCount + trimmed.length > maxLength && relevantLines.length > 0)
387
+ break;
388
+ // Include line if it has code patterns, is early in the extraction, or is substantial
389
+ if (hasCode || relevantLines.length < 15 || trimmed.length > 30) {
390
+ relevantLines.push(trimmed);
391
+ charCount += trimmed.length + 1;
392
+ }
393
+ if (charCount >= maxLength)
394
+ break;
395
+ }
396
+ return relevantLines.join('\n');
397
+ }
398
+ // =============================================================================
399
+ // MCP SERVER SETUP
400
+ // =============================================================================
401
+ const server = new McpServer({
402
+ name: "google-search-server",
403
+ version: SERVER_VERSION,
404
+ });
405
+ // =============================================================================
406
+ // TOOL: google-search
407
+ // =============================================================================
408
+ server.tool("google-search", `Smart web search and content fetcher. Auto-detects URLs vs search queries.
409
+
410
+ SEARCH MODE (query string): Returns structured results with clickable links.
411
+ • Batch queries supported (array) for concurrent search
412
+ • Default ${DEFAULT_SEARCH_LIMIT} results (max 100) with title, URL, snippet
413
+ • condensed=true for minimal output
414
+
415
+ FETCH MODE (URL input): Extracts clean text from webpage.
416
+ • Removes HTML/scripts/ads/navigation
417
+ • maxContentLength limits output size
418
+
419
+ Examples: "react hooks" → search | "https://docs.python.org" → fetch`, {
420
+ query: z
421
+ .union([z.string(), z.array(z.string())])
422
+ .describe("Search query, URL to fetch, or array for batch"),
423
+ limit: z
424
+ .number()
425
+ .min(1)
426
+ .max(100)
427
+ .optional()
428
+ .describe(`Results per query (default: ${DEFAULT_SEARCH_LIMIT}, max: 100)`),
429
+ timeout: z
430
+ .number()
431
+ .min(1000)
432
+ .max(120000)
433
+ .optional()
434
+ .describe(`Timeout ms (default: ${DEFAULT_SEARCH_TIMEOUT} search, ${DEFAULT_FETCH_TIMEOUT} fetch)`),
435
+ useCache: z
436
+ .boolean()
437
+ .optional()
438
+ .describe("Use cache (default: true)"),
439
+ condensed: z
440
+ .boolean()
441
+ .optional()
442
+ .describe("Minimal output: title+URL only (default: false)"),
443
+ maxContentLength: z
444
+ .number()
445
+ .min(100)
446
+ .optional()
447
+ .describe("Max chars for URL fetch content"),
448
+ }, async (params) => {
449
+ const { query, limit = DEFAULT_SEARCH_LIMIT, timeout, useCache = true, condensed = false, maxContentLength } = params;
450
+ const queries = Array.isArray(query) ? query : [query];
451
+ // Validate queries
452
+ if (queries.length === 0) {
453
+ return {
454
+ isError: true,
455
+ content: [{ type: "text", text: "No query provided." }],
456
+ };
457
+ }
458
+ if (queries.length > 10) {
459
+ return {
460
+ isError: true,
461
+ content: [{ type: "text", text: "Maximum 10 queries per request." }],
462
+ };
463
+ }
464
+ const allUrls = queries.every(isUrl);
465
+ const allQueries = queries.every(q => !isUrl(q));
466
+ if (!allUrls && !allQueries) {
467
+ return {
468
+ isError: true,
469
+ content: [{ type: "text", text: "Cannot mix URLs and search queries in one request." }],
470
+ };
471
+ }
472
+ const mode = allUrls ? 'fetch' : 'search';
473
+ logger.info({ mode, count: queries.length, limit, condensed }, "google-search");
474
+ try {
475
+ if (allUrls) {
476
+ // URL FETCH MODE
477
+ const results = await Promise.all(queries.map(async (urlInput) => {
478
+ let browser;
479
+ try {
480
+ browser = await browserPool.acquire();
481
+ const url = normalizeUrl(urlInput);
482
+ const retryManager = new RetryManager();
483
+ return await retryManager.executeWithRetry(() => fetchWebContent(url, browser, timeout || DEFAULT_FETCH_TIMEOUT));
484
+ }
485
+ catch (error) {
486
+ return {
487
+ url: urlInput,
488
+ content: '',
489
+ wordCount: 0,
490
+ fetchTime: 0,
491
+ timestamp: new Date().toISOString(),
492
+ error: error instanceof Error ? error.message : String(error),
493
+ };
494
+ }
495
+ finally {
496
+ if (browser)
497
+ await browserPool.release(browser);
498
+ }
499
+ }));
500
+ const responseText = queries.length === 1
501
+ ? formatWebContent(results[0], maxContentLength)
502
+ : results.map((r, i) => `## Page ${i + 1}\n${formatWebContent(r, maxContentLength)}`).join('\n\n');
503
+ return { content: [{ type: "text", text: responseText }] };
504
+ }
505
+ else {
506
+ // SEARCH MODE
507
+ const results = await Promise.all(queries.map(async (q) => {
508
+ let browser;
509
+ try {
510
+ const sanitizedQuery = InputValidator.validateQuery(q);
511
+ if (useCache) {
512
+ const cached = searchCache.get(sanitizedQuery, limit);
513
+ if (cached) {
514
+ return { ...cached, query: sanitizedQuery, fromCache: true };
515
+ }
516
+ }
517
+ browser = await browserPool.acquire();
518
+ const retryManager = new RetryManager();
519
+ const searchResults = await retryManager.executeWithRetry(() => googleSearch(sanitizedQuery, { limit, timeout, stateFile: STATE_FILE_PATH }, browser));
520
+ if (useCache) {
521
+ searchCache.set(sanitizedQuery, searchResults, limit);
522
+ }
523
+ return { ...searchResults, query: sanitizedQuery, fromCache: false };
524
+ }
525
+ catch (error) {
526
+ return {
527
+ query: q,
528
+ results: [],
529
+ searchTime: 0,
530
+ fromCache: false,
531
+ error: error instanceof Error ? error.message : String(error),
532
+ };
533
+ }
534
+ finally {
535
+ if (browser)
536
+ await browserPool.release(browser);
537
+ }
538
+ }));
539
+ const responseText = queries.length === 1
540
+ ? formatSearchResults(results[0], condensed)
541
+ : results.map((r, i) => `---\n## Query ${i + 1}/${results.length}\n${formatSearchResults(r, condensed)}`).join('\n\n');
542
+ return { content: [{ type: "text", text: responseText }] };
543
+ }
544
+ }
545
+ catch (error) {
546
+ logger.error({ error }, "google-search error");
547
+ return {
548
+ isError: true,
549
+ content: [{ type: "text", text: `Failed: ${error instanceof Error ? error.message : String(error)}` }],
550
+ };
551
+ }
552
+ });
553
+ // =============================================================================
554
+ // TOOL: get_code_context
555
+ // =============================================================================
556
+ server.tool("get_code_context", `Search for programming documentation, code examples, and API references.
557
+
558
+ Uses Context7 + Google Search for high-quality library docs.
559
+ Optimized for: libraries, frameworks, SDKs, APIs, code patterns.
560
+ Returns condensed code snippets from authoritative sources.
561
+
562
+ Examples:
563
+ • "React useState hook examples"
564
+ • "Python pandas dataframe filtering"
565
+ • "Next.js app router server actions"
566
+ • "Express middleware authentication"`, {
567
+ query: z
568
+ .string()
569
+ .min(2)
570
+ .max(500)
571
+ .describe("Programming topic, library, API, or code pattern"),
572
+ maxResults: z
573
+ .number()
574
+ .min(1)
575
+ .max(10)
576
+ .optional()
577
+ .describe(`Sources to search (default: ${DEFAULT_CODE_CONTEXT_RESULTS}, max: 10)`),
578
+ maxTokens: z
579
+ .number()
580
+ .min(500)
581
+ .max(10000)
582
+ .optional()
583
+ .describe(`Approx max output tokens (default: ${DEFAULT_CODE_CONTEXT_TOKENS})`),
584
+ }, async (params) => {
585
+ const { query, maxResults = DEFAULT_CODE_CONTEXT_RESULTS, maxTokens = DEFAULT_CODE_CONTEXT_TOKENS } = params;
586
+ logger.info({ query, maxResults, maxTokens }, "get_code_context");
587
+ try {
588
+ const maxChars = maxTokens * CHARS_PER_TOKEN;
589
+ let currentChars = 0;
590
+ const outputLines = [];
591
+ outputLines.push(`## Code Context: ${query}`);
592
+ outputLines.push('');
593
+ currentChars += query.length + 25;
594
+ // Try Context7 first for known libraries
595
+ const libraryInfo = extractLibraryInfo(query);
596
+ let context7Results = [];
597
+ if (libraryInfo) {
598
+ logger.info({ library: libraryInfo.canonical }, "Attempting Context7 lookup");
599
+ const library = await resolveContext7Library(libraryInfo.canonical, query);
600
+ if (library) {
601
+ logger.info({ libraryId: library.id, title: library.title }, "Found Context7 library");
602
+ context7Results = await queryContext7Docs(library.id, query);
603
+ if (context7Results.length > 0) {
604
+ outputLines.push(`### 📚 ${library.title} Documentation`);
605
+ outputLines.push('');
606
+ for (const doc of context7Results.slice(0, 3)) {
607
+ if (currentChars >= maxChars * 0.7)
608
+ break;
609
+ const section = [`#### ${doc.title}`];
610
+ if (doc.source)
611
+ section.push(`Source: ${doc.source}`);
612
+ section.push('');
613
+ if (doc.code) {
614
+ section.push('```');
615
+ section.push(doc.code.substring(0, 1500));
616
+ section.push('```');
617
+ }
618
+ else if (doc.content) {
619
+ section.push(doc.content.substring(0, 800));
620
+ }
621
+ section.push('');
622
+ const sectionText = section.join('\n');
623
+ if (currentChars + sectionText.length < maxChars * 0.7) {
624
+ outputLines.push(sectionText);
625
+ currentChars += sectionText.length;
626
+ }
627
+ }
628
+ }
629
+ }
630
+ }
631
+ // Supplement with Google search
632
+ if (currentChars < maxChars * 0.5) {
633
+ const searchQuery = libraryInfo?.docSites
634
+ ? `${query} ${libraryInfo.docSites}`
635
+ : `${query} site:github.com OR site:stackoverflow.com OR site:dev.to`;
636
+ let browser;
637
+ let searchResults = [];
638
+ try {
639
+ browser = await browserPool.acquire();
640
+ const retryManager = new RetryManager();
641
+ const search = await retryManager.executeWithRetry(() => googleSearch(searchQuery, {
642
+ limit: Math.min(maxResults * 2, 15),
643
+ stateFile: STATE_FILE_PATH
644
+ }, browser));
645
+ searchResults = search.results || [];
646
+ }
647
+ catch (error) {
648
+ logger.debug({ error }, "Google search fallback failed");
649
+ }
650
+ finally {
651
+ if (browser)
652
+ await browserPool.release(browser);
653
+ }
654
+ const uniqueResults = deduplicateResults(searchResults);
655
+ const prioritizedResults = prioritizeCodeResults(uniqueResults).slice(0, maxResults);
656
+ if (prioritizedResults.length > 0) {
657
+ // Fetch content from top results
658
+ const fetchCount = context7Results.length > 0 ? 2 : 3;
659
+ const enrichedResults = await Promise.all(prioritizedResults.slice(0, fetchCount).map(async (result) => {
660
+ let browser;
661
+ try {
662
+ browser = await browserPool.acquire();
663
+ const content = await fetchWebContent(result.link, browser, 12000);
664
+ return { ...result, fetchedContent: content };
665
+ }
666
+ catch {
667
+ return { ...result, fetchedContent: null };
668
+ }
669
+ finally {
670
+ if (browser)
671
+ await browserPool.release(browser);
672
+ }
673
+ }));
674
+ if (context7Results.length > 0) {
675
+ outputLines.push('### 🌐 Additional Web Resources');
676
+ outputLines.push('');
677
+ }
678
+ for (const result of enrichedResults) {
679
+ if (currentChars >= maxChars)
680
+ break;
681
+ if (result.fetchedContent?.content) {
682
+ const header = `#### ${result.title}\n${result.link}\n`;
683
+ currentChars += header.length;
684
+ const content = extractCodeRelevantContent(result.fetchedContent.content, Math.min(1500, maxChars - currentChars));
685
+ if (content) {
686
+ outputLines.push(header);
687
+ outputLines.push('```');
688
+ outputLines.push(content);
689
+ outputLines.push('```');
690
+ outputLines.push('');
691
+ currentChars += content.length + 10;
692
+ }
693
+ }
694
+ }
695
+ // Add links to remaining results
696
+ const remainingResults = prioritizedResults
697
+ .filter(r => !enrichedResults.some(e => e.link === r.link))
698
+ .slice(0, 5);
699
+ if (remainingResults.length > 0 && currentChars < maxChars - 300) {
700
+ outputLines.push('### 🔗 More Resources');
701
+ for (const r of remainingResults) {
702
+ outputLines.push(`- [${r.title}](${r.link})`);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ if (outputLines.length <= 2) {
708
+ return {
709
+ content: [{
710
+ type: "text",
711
+ text: `No code documentation found for: "${query}"\n\nTry more specific library/framework names.`,
712
+ }],
713
+ };
714
+ }
715
+ return { content: [{ type: "text", text: outputLines.join('\n') }] };
716
+ }
717
+ catch (error) {
718
+ logger.error({ error }, "get_code_context error");
719
+ return {
720
+ isError: true,
721
+ content: [{ type: "text", text: `Code context failed: ${error instanceof Error ? error.message : String(error)}` }],
722
+ };
723
+ }
724
+ });
725
+ // =============================================================================
726
+ // SERVER LIFECYCLE
727
+ // =============================================================================
728
+ /**
729
+ * Check if Playwright is available and install it if needed
730
+ */
731
+ async function ensurePlaywrightInstalled() {
732
+ try {
733
+ // Try to import Playwright to check if it's available
734
+ const { chromium } = await import("playwright");
735
+ // Try to launch a browser briefly to ensure the browser is installed
736
+ const browser = await chromium.launch({
737
+ headless: true,
738
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
739
+ });
740
+ await browser.close();
741
+ logger.info("Playwright is available and ready");
742
+ return;
743
+ }
744
+ catch (error) {
745
+ logger.warn({ error }, "Playwright not available, installing...");
746
+ try {
747
+ // Install Playwright using npx with proper argument escaping
748
+ await new Promise((resolve, reject) => {
749
+ const installProcess = spawn("npx", ["playwright", "install", "chromium"], {
750
+ stdio: "inherit",
751
+ });
752
+ installProcess.on("close", (code) => {
753
+ if (code === 0) {
754
+ logger.info("Playwright installed successfully");
755
+ resolve();
756
+ }
757
+ else {
758
+ reject(new Error(`Playwright installation failed with code ${code}`));
759
+ }
760
+ });
761
+ installProcess.on("error", (error) => {
762
+ reject(new Error(`Failed to start Playwright installation: ${error.message}`));
763
+ });
764
+ });
765
+ // Verify installation worked
766
+ const { chromium } = await import("playwright");
767
+ const browser = await chromium.launch({
768
+ headless: true,
769
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
770
+ });
771
+ await browser.close();
772
+ logger.info("Playwright installation verified");
773
+ }
774
+ catch (installError) {
775
+ logger.error({ error: installError }, "Failed to install Playwright");
776
+ throw new Error(`Playwright installation failed: ${installError instanceof Error ? installError.message : String(installError)}`);
777
+ }
778
+ }
779
+ }
780
+ async function cleanup() {
781
+ logger.info("Cleaning up resources...");
782
+ try {
783
+ await browserPool.cleanup();
784
+ }
785
+ catch (error) {
786
+ logger.error({ error }, "Cleanup error");
787
+ }
788
+ }
789
+ async function main() {
790
+ try {
791
+ logger.info({ version: SERVER_VERSION }, "Starting Google Search MCP server");
792
+ // Ensure Playwright is installed and available
793
+ await ensurePlaywrightInstalled();
794
+ const transport = new StdioServerTransport();
795
+ await server.connect(transport);
796
+ logger.info("Google Search MCP server ready");
797
+ // Graceful shutdown handlers
798
+ const shutdown = async (signal) => {
799
+ logger.info({ signal }, "Received shutdown signal");
800
+ await cleanup();
801
+ process.exit(0);
802
+ };
803
+ process.on("SIGINT", () => shutdown("SIGINT"));
804
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
805
+ process.on("exit", () => {
806
+ logger.info("Process exiting");
807
+ });
808
+ // Windows-specific handling
809
+ if (process.platform === "win32") {
810
+ const readline = await import("readline");
811
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
812
+ rl.on("SIGINT", () => shutdown("SIGINT"));
813
+ }
814
+ }
815
+ catch (error) {
816
+ logger.error({ error }, "Server start failed");
817
+ await cleanup();
818
+ process.exit(1);
819
+ }
820
+ }
821
+ main();
822
+ //# sourceMappingURL=mcp-server.js.map