@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.
- package/LICENSE +7 -0
- package/README.md +339 -0
- package/bin/google-search +3 -0
- package/bin/google-search-mcp +3 -0
- package/bin/google-search-mcp.cmd +2 -0
- package/bin/google-search.cmd +2 -0
- package/dist/browser-config.d.ts +41 -0
- package/dist/browser-config.js +96 -0
- package/dist/browser-config.js.map +1 -0
- package/dist/browser-pool.d.ts +13 -0
- package/dist/browser-pool.js +37 -0
- package/dist/browser-pool.js.map +1 -0
- package/dist/cache.d.ts +48 -0
- package/dist/cache.js +111 -0
- package/dist/cache.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -0
- package/dist/filters.d.ts +48 -0
- package/dist/filters.js +192 -0
- package/dist/filters.js.map +1 -0
- package/dist/html-cleaner.d.ts +62 -0
- package/dist/html-cleaner.js +236 -0
- package/dist/html-cleaner.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +41 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +822 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.js +1080 -0
- package/dist/search.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.js +23 -0
- package/dist/validation.js.map +1 -0
- package/dist/web-fetcher.d.ts +10 -0
- package/dist/web-fetcher.js +179 -0
- package/dist/web-fetcher.js.map +1 -0
- package/package.json +67 -0
- 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
|