pse-mcp 0.1.1 → 0.1.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-search-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for Google search and webpage analysis",
5
5
  "type": "module",
6
6
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pse-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for Google search and webpage analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,235 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import { GoogleSearchService } from './services/google-search.service.js';
6
- class GoogleSearchServer {
7
- constructor() {
8
- this.searchService = new GoogleSearchService();
9
- this.server = new Server({
10
- name: 'google-search',
11
- version: '1.0.0'
12
- }, {
13
- capabilities: {
14
- tools: {
15
- google_search: {
16
- description: 'Search Google and return relevant results from the web. This tool finds web pages, articles, and information on specific topics using Google\'s search engine. Results include titles, snippets, and URLs that can be analyzed further using extract_webpage_content.',
17
- inputSchema: {
18
- type: 'object',
19
- properties: {
20
- query: {
21
- type: 'string',
22
- description: 'Search query - be specific and use quotes for exact matches. For best results, use clear keywords and avoid very long queries.'
23
- },
24
- num_results: {
25
- type: 'number',
26
- description: 'Number of results to return (default: 10, max: 10). Increase for broader coverage, decrease for faster response.'
27
- },
28
- site: {
29
- type: 'string',
30
- description: 'Limit search results to a specific website domain (e.g., "wikipedia.org" or "nytimes.com").'
31
- },
32
- language: {
33
- type: 'string',
34
- description: 'Filter results by language using ISO 639-1 codes (e.g., "en" for English, "es" for Spanish, "fr" for French).'
35
- },
36
- dateRestrict: {
37
- type: 'string',
38
- description: 'Filter results by date using Google\'s date restriction format: "d[number]" for past days, "w[number]" for past weeks, "m[number]" for past months, or "y[number]" for past years. Example: "m6" for results from the past 6 months.'
39
- },
40
- exactTerms: {
41
- type: 'string',
42
- description: 'Search for results that contain this exact phrase. This is equivalent to putting the terms in quotes in the search query.'
43
- },
44
- resultType: {
45
- type: 'string',
46
- description: 'Specify the type of results to return. Options include "image" (or "images"), "news", and "video" (or "videos"). Default is general web results.'
47
- },
48
- page: {
49
- type: 'number',
50
- description: 'Page number for paginated results (starts at 1). Use in combination with resultsPerPage to navigate through large result sets.'
51
- },
52
- resultsPerPage: {
53
- type: 'number',
54
- description: 'Number of results to show per page (default: 10, max: 10). Controls how many results are returned for each page.'
55
- },
56
- sort: {
57
- type: 'string',
58
- description: 'Sorting method for search results. Options: "relevance" (default) or "date" (most recent first).'
59
- }
60
- },
61
- required: ['query']
62
- }
63
- }
64
- }
65
- }
66
- });
67
- // Register tool list handler
68
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
69
- tools: [
70
- {
71
- name: 'google_search',
72
- description: 'Search Google and return relevant results from the web. This tool finds web pages, articles, and information on specific topics using Google\'s search engine. Results include titles, snippets, and URLs that can be analyzed further using extract_webpage_content.',
73
- inputSchema: {
74
- type: 'object',
75
- properties: {
76
- query: {
77
- type: 'string',
78
- description: 'Search query - be specific and use quotes for exact matches. For best results, use clear keywords and avoid very long queries.'
79
- },
80
- num_results: {
81
- type: 'number',
82
- description: 'Number of results to return (default: 10, max: 10). Increase for broader coverage, decrease for faster response.'
83
- },
84
- site: {
85
- type: 'string',
86
- description: 'Limit search results to a specific website domain (e.g., "wikipedia.org" or "nytimes.com").'
87
- },
88
- language: {
89
- type: 'string',
90
- description: 'Filter results by language using ISO 639-1 codes (e.g., "en" for English, "es" for Spanish, "fr" for French).'
91
- },
92
- dateRestrict: {
93
- type: 'string',
94
- description: 'Filter results by date using Google\'s date restriction format: "d[number]" for past days, "w[number]" for past weeks, "m[number]" for past months, or "y[number]" for past years. Example: "m6" for results from the past 6 months.'
95
- },
96
- exactTerms: {
97
- type: 'string',
98
- description: 'Search for results that contain this exact phrase. This is equivalent to putting the terms in quotes in the search query.'
99
- },
100
- resultType: {
101
- type: 'string',
102
- description: 'Specify the type of results to return. Options include "image" (or "images"), "news", and "video" (or "videos"). Default is general web results.'
103
- },
104
- page: {
105
- type: 'number',
106
- description: 'Page number for paginated results (starts at 1). Use in combination with resultsPerPage to navigate through large result sets.'
107
- },
108
- resultsPerPage: {
109
- type: 'number',
110
- description: 'Number of results to show per page (default: 10, max: 10). Controls how many results are returned for each page.'
111
- },
112
- sort: {
113
- type: 'string',
114
- description: 'Sorting method for search results. Options: "relevance" (default) or "date" (most recent first).'
115
- }
116
- },
117
- required: ['query']
118
- }
119
- }
120
- ]
121
- }));
122
- // Register tool call handler
123
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
124
- switch (request.params.name) {
125
- case 'google_search':
126
- if (typeof request.params.arguments === 'object' && request.params.arguments !== null && 'query' in request.params.arguments) {
127
- return this.handleSearch({
128
- query: String(request.params.arguments.query),
129
- num_results: typeof request.params.arguments.num_results === 'number' ? request.params.arguments.num_results : undefined,
130
- filters: {
131
- site: request.params.arguments.site ? String(request.params.arguments.site) : undefined,
132
- language: request.params.arguments.language ? String(request.params.arguments.language) : undefined,
133
- dateRestrict: request.params.arguments.dateRestrict ? String(request.params.arguments.dateRestrict) : undefined,
134
- exactTerms: request.params.arguments.exactTerms ? String(request.params.arguments.exactTerms) : undefined,
135
- resultType: request.params.arguments.resultType ? String(request.params.arguments.resultType) : undefined,
136
- page: typeof request.params.arguments.page === 'number' ? request.params.arguments.page : undefined,
137
- resultsPerPage: typeof request.params.arguments.resultsPerPage === 'number' ? request.params.arguments.resultsPerPage : undefined,
138
- sort: request.params.arguments.sort ? String(request.params.arguments.sort) : undefined
139
- }
140
- });
141
- }
142
- throw new Error('Invalid arguments for google_search tool');
143
- default:
144
- throw new Error(`Unknown tool: ${request.params.name}`);
145
- }
146
- });
147
- }
148
- async handleSearch(args) {
149
- try {
150
- const { results, pagination, categories } = await this.searchService.search(args.query, args.num_results, args.filters);
151
- if (results.length === 0) {
152
- return {
153
- content: [{
154
- type: 'text',
155
- text: 'No results found. Try:\n- Using different keywords\n- Removing quotes from non-exact phrases\n- Using more general terms'
156
- }],
157
- isError: true
158
- };
159
- }
160
- // Format results in a more concise, readable way
161
- const formattedResults = results.map(result => ({
162
- title: result.title,
163
- link: result.link,
164
- snippet: result.snippet,
165
- category: result.category
166
- }));
167
- // Format results in a more AI-friendly way
168
- let responseText = `Search results for "${args.query}":\n\n`;
169
- // Add category summary if available
170
- if (categories && categories.length > 0) {
171
- responseText += "Categories: " + categories.map(c => `${c.name} (${c.count})`).join(', ') + "\n\n";
172
- }
173
- // Add pagination info
174
- if (pagination) {
175
- responseText += `Showing page ${pagination.currentPage}${pagination.totalResults ? ` of approximately ${pagination.totalResults} results` : ''}\n\n`;
176
- }
177
- // Add each result in a readable format
178
- formattedResults.forEach((result, index) => {
179
- responseText += `${index + 1}. ${result.title}\n`;
180
- responseText += ` URL: ${result.link}\n`;
181
- responseText += ` ${result.snippet}\n\n`;
182
- });
183
- // Add navigation hints if pagination exists
184
- if (pagination && (pagination.hasNextPage || pagination.hasPreviousPage)) {
185
- responseText += "Navigation: ";
186
- if (pagination.hasPreviousPage) {
187
- responseText += "Use 'page: " + (pagination.currentPage - 1) + "' for previous results. ";
188
- }
189
- if (pagination.hasNextPage) {
190
- responseText += "Use 'page: " + (pagination.currentPage + 1) + "' for more results.";
191
- }
192
- responseText += "\n";
193
- }
194
- return {
195
- content: [
196
- {
197
- type: 'text',
198
- text: responseText,
199
- },
200
- ],
201
- };
202
- }
203
- catch (error) {
204
- const message = error instanceof Error ? error.message : 'Unknown error during search';
205
- return {
206
- content: [{ type: 'text', text: message }],
207
- isError: true
208
- };
209
- }
210
- }
211
- async start() {
212
- try {
213
- const transport = new StdioServerTransport();
214
- await this.server.connect(transport);
215
- console.error('Google Search MCP server running');
216
- // Keep the process running
217
- process.on('SIGINT', () => {
218
- this.server.close().catch(console.error);
219
- process.exit(0);
220
- });
221
- }
222
- catch (error) {
223
- if (error instanceof Error) {
224
- console.error('Failed to start MCP server:', error.message);
225
- }
226
- else {
227
- console.error('Failed to start MCP server: Unknown error');
228
- }
229
- process.exit(1);
230
- }
231
- }
232
- }
233
- // Start the server
234
- const server = new GoogleSearchServer();
235
- server.start().catch(console.error);
@@ -1,244 +0,0 @@
1
- import { google } from 'googleapis';
2
- import { URL } from 'url';
3
- export class GoogleSearchService {
4
- constructor() {
5
- // Cache for search results (key: query string + filters, value: results)
6
- this.searchCache = new Map();
7
- // Cache expiration time in milliseconds (5 minutes)
8
- this.cacheTTL = 5 * 60 * 1000;
9
- const apiKey = process.env.GOOGLE_API_KEY;
10
- const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE_ID;
11
- if (!apiKey || !searchEngineId) {
12
- throw new Error('Missing required environment variables: GOOGLE_API_KEY and GOOGLE_SEARCH_ENGINE_ID');
13
- }
14
- // Initialize Google Custom Search API
15
- this.customSearch = google.customsearch('v1').cse;
16
- this.searchEngineId = searchEngineId;
17
- // Set up the API client
18
- google.options({
19
- auth: apiKey
20
- });
21
- }
22
- /**
23
- * Generate a cache key from search parameters
24
- */
25
- generateCacheKey(query, numResults, filters) {
26
- return JSON.stringify({
27
- query,
28
- numResults,
29
- filters
30
- });
31
- }
32
- /**
33
- * Check if a cache entry is still valid
34
- */
35
- isCacheValid(entry) {
36
- const now = Date.now();
37
- return now - entry.timestamp < this.cacheTTL;
38
- }
39
- /**
40
- * Store search results in cache
41
- */
42
- cacheSearchResults(cacheKey, results, pagination, categories) {
43
- this.searchCache.set(cacheKey, {
44
- timestamp: Date.now(),
45
- data: { results, pagination, categories }
46
- });
47
- // Limit cache size to prevent memory issues (max 100 entries)
48
- if (this.searchCache.size > 100) {
49
- // Delete oldest entry
50
- const oldestKey = Array.from(this.searchCache.entries())
51
- .sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
52
- this.searchCache.delete(oldestKey);
53
- }
54
- }
55
- async search(query, numResults = 10, filters) {
56
- try {
57
- // Generate cache key
58
- const cacheKey = this.generateCacheKey(query, numResults, filters);
59
- // Check cache first
60
- const cachedResult = this.searchCache.get(cacheKey);
61
- if (cachedResult && this.isCacheValid(cachedResult)) {
62
- console.error('Using cached search results');
63
- return cachedResult.data;
64
- }
65
- let formattedQuery = query;
66
- // Apply site filter if provided
67
- if (filters?.site) {
68
- formattedQuery += ` site:${filters.site}`;
69
- }
70
- // Apply exact terms if provided
71
- if (filters?.exactTerms) {
72
- formattedQuery += ` "${filters.exactTerms}"`;
73
- }
74
- // Set default pagination values if not provided
75
- const page = filters?.page && filters.page > 0 ? filters.page : 1;
76
- const resultsPerPage = filters?.resultsPerPage ? Math.min(filters.resultsPerPage, 10) : Math.min(numResults, 10);
77
- // Calculate start index for pagination (Google uses 1-based indexing)
78
- const startIndex = (page - 1) * resultsPerPage + 1;
79
- const params = {
80
- cx: this.searchEngineId,
81
- q: formattedQuery,
82
- num: resultsPerPage,
83
- start: startIndex
84
- };
85
- // Apply language filter if provided
86
- if (filters?.language) {
87
- params.lr = `lang_${filters.language}`;
88
- }
89
- // Apply date restriction if provided
90
- if (filters?.dateRestrict) {
91
- params.dateRestrict = filters.dateRestrict;
92
- }
93
- // Apply result type filter if provided
94
- if (filters?.resultType) {
95
- switch (filters.resultType.toLowerCase()) {
96
- case 'image':
97
- case 'images':
98
- params.searchType = 'image';
99
- break;
100
- case 'news':
101
- // For news, we need to modify the query
102
- formattedQuery += ' source:news';
103
- params.q = formattedQuery;
104
- break;
105
- case 'video':
106
- case 'videos':
107
- // For videos, we can use a more specific filter
108
- formattedQuery += ' filetype:video OR inurl:video OR inurl:watch';
109
- params.q = formattedQuery;
110
- break;
111
- }
112
- }
113
- // Apply sorting if provided
114
- if (filters?.sort) {
115
- switch (filters.sort.toLowerCase()) {
116
- case 'date':
117
- // Sort by date (most recent first)
118
- params.sort = 'date';
119
- break;
120
- case 'relevance':
121
- default:
122
- // Google's default sort is by relevance, so we don't need to specify
123
- break;
124
- }
125
- }
126
- const response = await this.customSearch.list(params);
127
- // If no items are found, return empty results with pagination info
128
- if (!response.data.items) {
129
- return {
130
- results: [],
131
- pagination: {
132
- currentPage: page,
133
- resultsPerPage,
134
- totalResults: 0,
135
- totalPages: 0,
136
- hasNextPage: false,
137
- hasPreviousPage: page > 1
138
- },
139
- categories: []
140
- };
141
- }
142
- // Map the search results and categorize them
143
- const results = response.data.items.map(item => {
144
- const result = {
145
- title: item.title || '',
146
- link: item.link || '',
147
- snippet: item.snippet || '',
148
- pagemap: item.pagemap || {},
149
- datePublished: item.pagemap?.metatags?.[0]?.['article:published_time'] || '',
150
- source: 'google_search'
151
- };
152
- // Add category to the result
153
- result.category = this.categorizeResult(result);
154
- return result;
155
- });
156
- // Generate category statistics
157
- const categories = this.generateCategoryStats(results);
158
- // Create pagination information
159
- const totalResults = parseInt(response.data.searchInformation?.totalResults || '0', 10);
160
- const totalPages = Math.ceil(totalResults / resultsPerPage);
161
- const pagination = {
162
- currentPage: page,
163
- resultsPerPage,
164
- totalResults,
165
- totalPages,
166
- hasNextPage: page < totalPages,
167
- hasPreviousPage: page > 1
168
- };
169
- // Cache the results before returning
170
- this.cacheSearchResults(cacheKey, results, pagination, categories);
171
- return {
172
- results,
173
- pagination,
174
- categories
175
- };
176
- }
177
- catch (error) {
178
- if (error instanceof Error) {
179
- throw new Error(`Google Search API error: ${error.message}`);
180
- }
181
- throw new Error('Unknown error during Google search');
182
- }
183
- }
184
- /**
185
- * Categorizes a search result based on its content
186
- * @param result The search result to categorize
187
- * @returns The category name
188
- */
189
- categorizeResult(result) {
190
- try {
191
- // Extract the domain from the URL
192
- const url = new URL(result.link);
193
- const domain = url.hostname.replace(/^www\./, '');
194
- // Check if this is a social media site
195
- if (domain.match(/facebook\.com|twitter\.com|instagram\.com|linkedin\.com|pinterest\.com|tiktok\.com|reddit\.com/i)) {
196
- return 'Social Media';
197
- }
198
- // Check if this is a video site
199
- if (domain.match(/youtube\.com|vimeo\.com|dailymotion\.com|twitch\.tv/i)) {
200
- return 'Video';
201
- }
202
- // Check if this is a news site
203
- if (domain.match(/news|cnn\.com|bbc\.com|nytimes\.com|wsj\.com|reuters\.com|bloomberg\.com/i)) {
204
- return 'News';
205
- }
206
- // Check if this is an educational site
207
- if (domain.match(/\.edu$|wikipedia\.org|khan|course|learn|study|academic/i)) {
208
- return 'Educational';
209
- }
210
- // Check if this is a documentation site
211
- if (domain.match(/docs|documentation|developer|github\.com|gitlab\.com|bitbucket\.org|stackoverflow\.com/i) ||
212
- result.title.match(/docs|documentation|api|reference|manual/i)) {
213
- return 'Documentation';
214
- }
215
- // Check if this is a shopping site
216
- if (domain.match(/amazon\.com|ebay\.com|etsy\.com|walmart\.com|shop|store|buy/i)) {
217
- return 'Shopping';
218
- }
219
- // Default category based on domain
220
- return domain.split('.').slice(-2, -1)[0].charAt(0).toUpperCase() + domain.split('.').slice(-2, -1)[0].slice(1);
221
- }
222
- catch (error) {
223
- // If there's any error in categorization, return a default category
224
- return 'Other';
225
- }
226
- }
227
- /**
228
- * Generates category statistics from search results
229
- * @param results The search results to analyze
230
- * @returns An array of category information
231
- */
232
- generateCategoryStats(results) {
233
- // Count results by category
234
- const categoryCounts = {};
235
- results.forEach(result => {
236
- const category = result.category || 'Other';
237
- categoryCounts[category] = (categoryCounts[category] || 0) + 1;
238
- });
239
- // Convert to array of category info objects
240
- return Object.entries(categoryCounts)
241
- .map(([name, count]) => ({ name, count }))
242
- .sort((a, b) => b.count - a.count); // Sort by count in descending order
243
- }
244
- }
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};