serper-search-mcp 2.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.
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Result Formatter for Serper MCP Server v2.0.0 - Enterprise Edition
4
+ * Author: SMJAHID from SMLabs01
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ResultFormatter = void 0;
8
+ class ResultFormatter {
9
+ /**
10
+ * Format search results for display
11
+ */
12
+ static formatResults(results, maxResults, searchType) {
13
+ const query = results.searchParameters?.q || 'Unknown query';
14
+ let output = `## ${this.capitalizeFirst(searchType)} Search Results for "${query}"\n\n`;
15
+ // Handle different result structures based on search type
16
+ let resultArray = [];
17
+ let resultKey = '';
18
+ switch (searchType) {
19
+ case 'web':
20
+ resultKey = 'organic';
21
+ break;
22
+ case 'images':
23
+ resultKey = 'images';
24
+ break;
25
+ case 'videos':
26
+ resultKey = 'videos';
27
+ break;
28
+ case 'news':
29
+ resultKey = 'news';
30
+ break;
31
+ case 'shopping':
32
+ resultKey = 'shopping';
33
+ break;
34
+ default:
35
+ resultKey = 'organic';
36
+ }
37
+ if (!results[resultKey] ||
38
+ results[resultKey].length === 0) {
39
+ return output + 'No search results found.';
40
+ }
41
+ resultArray = results[resultKey];
42
+ const limitedResults = resultArray.slice(0, maxResults);
43
+ limitedResults.forEach((result, index) => {
44
+ output += `### ${index + 1}. `;
45
+ switch (searchType) {
46
+ case 'web':
47
+ output += `${result.title}\n`;
48
+ output += `**URL:** ${result.link}\n`;
49
+ if (result.snippet) {
50
+ output += `**Snippet:** ${result.snippet}\n`;
51
+ }
52
+ break;
53
+ case 'images':
54
+ output += `${result.title}\n`;
55
+ output += `**Image URL:** ${result.imageUrl}\n`;
56
+ output += `**Source:** ${result.source}\n`;
57
+ if (result.link) {
58
+ output += `**Page:** ${result.link}\n`;
59
+ }
60
+ break;
61
+ case 'videos':
62
+ output += `${result.title}\n`;
63
+ output += `**Channel:** ${result.channel}\n`;
64
+ output += `**Duration:** ${result.duration || 'Unknown'}\n`;
65
+ if (result.link) {
66
+ output += `**URL:** ${result.link}\n`;
67
+ }
68
+ break;
69
+ case 'news':
70
+ output += `${result.title}\n`;
71
+ output += `**Source:** ${result.source}\n`;
72
+ output += `**Published:** ${result.date || 'Unknown'}\n`;
73
+ if (result.link) {
74
+ output += `**URL:** ${result.link}\n`;
75
+ }
76
+ if (result.snippet) {
77
+ output += `**Snippet:** ${result.snippet}\n`;
78
+ }
79
+ break;
80
+ case 'shopping':
81
+ output += `${result.title}\n`;
82
+ output += `**Price:** ${result.price || 'Price not available'}\n`;
83
+ output += `**Source:** ${result.source}\n`;
84
+ if (result.link) {
85
+ output += `**URL:** ${result.link}\n`;
86
+ }
87
+ if (result.rating) {
88
+ output += `**Rating:** ${result.rating}/5\n`;
89
+ }
90
+ break;
91
+ }
92
+ output += '\n';
93
+ });
94
+ if (resultArray.length > maxResults) {
95
+ output += `*Showing ${maxResults} of ${resultArray.length} total results.*\n`;
96
+ }
97
+ return output;
98
+ }
99
+ /**
100
+ * Format error message
101
+ */
102
+ static formatError(error) {
103
+ return `❌ **Error:** ${error.message}\n\nPlease check your query and try again.`;
104
+ }
105
+ /**
106
+ * Format validation error
107
+ */
108
+ static formatValidationError(message) {
109
+ return `⚠️ **Validation Error:** ${message}\n\nPlease check your input parameters.`;
110
+ }
111
+ /**
112
+ * Format server info
113
+ */
114
+ static formatServerInfo() {
115
+ return `🚀 **Serper MCP Server v2.0.0 - Enterprise Edition**
116
+
117
+ 👨‍💻 **Author:** SMJAHID from SMLabs01
118
+ 🔧 **Features:** Multi-Transport, Advanced Filtering, AI Summarization
119
+ 🌐 **Search Types:** Web, Images, Videos, News, Shopping
120
+
121
+ Ready to process search requests!`;
122
+ }
123
+ /**
124
+ * Capitalize first letter of a string
125
+ */
126
+ static capitalizeFirst(str) {
127
+ return str.charAt(0).toUpperCase() + str.slice(1);
128
+ }
129
+ }
130
+ exports.ResultFormatter = ResultFormatter;
131
+ //# sourceMappingURL=ResultFormatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResultFormatter.js","sourceRoot":"","sources":["../../src/utils/ResultFormatter.ts"],"names":[],"mappings":"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"}
package/index.js ADDED
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
4
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const { SSEServerTransport } = require("@modelcontextprotocol/sdk/server/sse.js");
6
+ const { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } = require("@modelcontextprotocol/sdk/types.js");
7
+
8
+ class SerperMCPServer {
9
+ constructor(options = {}) {
10
+ this.apiKey = options.apiKey || process.env.SERPER_API_KEY;
11
+ if (!this.apiKey) {
12
+ throw new Error("SERPER_API_KEY environment variable is required");
13
+ }
14
+
15
+ this.transport = options.transport || process.env.SERPER_MCP_TRANSPORT || 'stdio';
16
+ this.port = options.port || parseInt(process.env.SERPER_MCP_PORT) || 8080;
17
+ this.host = options.host || process.env.SERPER_MCP_HOST || '0.0.0.0';
18
+ this.logLevel = options.logLevel || process.env.SERPER_MCP_LOG_LEVEL || 'info';
19
+
20
+ this.server = new Server(
21
+ {
22
+ name: "serper-search-server",
23
+ version: "2.0.0"
24
+ },
25
+ {
26
+ capabilities: {
27
+ tools: {}
28
+ }
29
+ }
30
+ );
31
+
32
+ this.setupToolHandlers();
33
+ }
34
+
35
+ setupToolHandlers() {
36
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
37
+ return {
38
+ tools: [
39
+ {
40
+ name: "search_web",
41
+ description: "Search the web using Serper API (Google search results)",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ query: {
46
+ type: "string",
47
+ description: "The search query to execute (max 400 chars, 50 words)"
48
+ },
49
+ num_results: {
50
+ type: "number",
51
+ description: "Number of results to return (1-20, default: 10)",
52
+ default: 10,
53
+ minimum: 1,
54
+ maximum: 20
55
+ },
56
+ country: {
57
+ type: "string",
58
+ description: "Country code (default: 'US')",
59
+ default: "US"
60
+ },
61
+ search_lang: {
62
+ type: "string",
63
+ description: "Search language (default: 'en')",
64
+ default: "en"
65
+ },
66
+ ui_lang: {
67
+ type: "string",
68
+ description: "UI language (default: 'en-US')",
69
+ default: "en-US"
70
+ },
71
+ freshness: {
72
+ type: "string",
73
+ description: "Time filter: 'pd' (day), 'pw' (week), 'pm' (month), 'py' (year)",
74
+ enum: ["pd", "pw", "pm", "py"]
75
+ },
76
+ safesearch: {
77
+ type: "string",
78
+ description: "Content filtering",
79
+ enum: ["off", "moderate", "strict"],
80
+ default: "moderate"
81
+ },
82
+ summary: {
83
+ type: "boolean",
84
+ description: "Enable AI summarization (default: false)",
85
+ default: false
86
+ }
87
+ },
88
+ required: ["query"]
89
+ }
90
+ },
91
+ {
92
+ name: "search_images",
93
+ description: "Search for images using Serper API",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ query: {
98
+ type: "string",
99
+ description: "The image search query"
100
+ },
101
+ num_results: {
102
+ type: "number",
103
+ description: "Number of results to return (default: 10)",
104
+ default: 10
105
+ }
106
+ },
107
+ required: ["query"]
108
+ }
109
+ },
110
+ {
111
+ name: "search_videos",
112
+ description: "Search for videos using Serper API",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ query: {
117
+ type: "string",
118
+ description: "The video search query"
119
+ },
120
+ num_results: {
121
+ type: "number",
122
+ description: "Number of results to return (default: 10)",
123
+ default: 10
124
+ }
125
+ },
126
+ required: ["query"]
127
+ }
128
+ },
129
+ {
130
+ name: "search_news",
131
+ description: "Search for news articles using Serper API",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ query: {
136
+ type: "string",
137
+ description: "The news search query"
138
+ },
139
+ num_results: {
140
+ type: "number",
141
+ description: "Number of results to return (default: 10)",
142
+ default: 10
143
+ }
144
+ },
145
+ required: ["query"]
146
+ }
147
+ },
148
+ {
149
+ name: "search_shopping",
150
+ description: "Search for products and shopping results using Serper API",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ query: {
155
+ type: "string",
156
+ description: "The shopping search query"
157
+ },
158
+ num_results: {
159
+ type: "number",
160
+ description: "Number of results to return (default: 10)",
161
+ default: 10
162
+ }
163
+ },
164
+ required: ["query"]
165
+ }
166
+ }
167
+ ]
168
+ };
169
+ });
170
+
171
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
172
+ const { name, arguments: args } = request.params;
173
+
174
+ switch (name) {
175
+ case "search_web":
176
+ return await this.performSearch(args, "web");
177
+ case "search_images":
178
+ return await this.performSearch(args, "images");
179
+ case "search_videos":
180
+ return await this.performSearch(args, "videos");
181
+ case "search_news":
182
+ return await this.performSearch(args, "news");
183
+ case "search_shopping":
184
+ return await this.performSearch(args, "shopping");
185
+ default:
186
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
187
+ }
188
+ });
189
+ }
190
+
191
+ async performSearch(args, searchType = "web") {
192
+ const {
193
+ query,
194
+ num_results = 10,
195
+ country = "US",
196
+ search_lang = "en",
197
+ ui_lang = "en-US",
198
+ freshness,
199
+ safesearch = "moderate",
200
+ summary = false
201
+ } = args;
202
+
203
+ if (!query || typeof query !== "string" || query.trim() === "") {
204
+ throw new McpError(ErrorCode.InvalidParams, "Query parameter is required and must be a non-empty string");
205
+ }
206
+
207
+ // Validate query length (following Brave API limits)
208
+ if (query.length > 400 || query.split(' ').length > 50) {
209
+ throw new McpError(ErrorCode.InvalidParams, "Query too long (max 400 chars, 50 words)");
210
+ }
211
+
212
+ try {
213
+ const searchResults = await this.makeSerperRequest(query.trim(), searchType, {
214
+ num_results,
215
+ country,
216
+ search_lang,
217
+ ui_lang,
218
+ freshness,
219
+ safesearch,
220
+ summary
221
+ });
222
+
223
+ // Format results for better readability
224
+ const formattedResults = this.formatSearchResults(searchResults, num_results, searchType);
225
+
226
+ return {
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: formattedResults
231
+ }
232
+ ]
233
+ };
234
+ } catch (error) {
235
+ throw new McpError(
236
+ ErrorCode.InternalError,
237
+ `Search failed: ${error.message}`
238
+ );
239
+ }
240
+ }
241
+
242
+ async makeSerperRequest(query, searchType = "web", options = {}) {
243
+ const {
244
+ num_results = 10,
245
+ country = "US",
246
+ search_lang = "en",
247
+ ui_lang = "en-US",
248
+ freshness,
249
+ safesearch = "moderate",
250
+ summary = false
251
+ } = options;
252
+
253
+ const requestBody = {
254
+ q: query,
255
+ num: Math.min(num_results, 20), // Cap at 20 per API limits
256
+ gl: country, // Country code
257
+ hl: ui_lang, // UI language
258
+ lr: `lang_${search_lang}` // Search language
259
+ };
260
+
261
+ // Add search type specific parameters
262
+ switch (searchType) {
263
+ case "images":
264
+ requestBody.tbm = "isch"; // Images search
265
+ break;
266
+ case "videos":
267
+ requestBody.tbm = "vid"; // Videos search
268
+ break;
269
+ case "news":
270
+ requestBody.tbm = "nws"; // News search
271
+ break;
272
+ case "shopping":
273
+ requestBody.tbm = "shop"; // Shopping search
274
+ break;
275
+ // web search uses default parameters
276
+ }
277
+
278
+ // Add optional filters
279
+ if (freshness) {
280
+ requestBody.tbs = `qdr:${freshness}`; // Time-based search
281
+ }
282
+
283
+ if (safesearch && safesearch !== "moderate") {
284
+ requestBody.safe = safesearch === "strict" ? "active" : "off";
285
+ }
286
+
287
+ if (summary) {
288
+ requestBody.summary = true; // Enable AI summarization
289
+ }
290
+
291
+ const response = await fetch('https://google.serper.dev/search', {
292
+ method: 'POST',
293
+ headers: {
294
+ 'X-API-KEY': this.apiKey,
295
+ 'Content-Type': 'application/json'
296
+ },
297
+ body: JSON.stringify(requestBody)
298
+ });
299
+
300
+ if (!response.ok) {
301
+ const errorText = await response.text();
302
+ throw new Error(`Serper API error (${response.status}): ${errorText}`);
303
+ }
304
+
305
+ return await response.json();
306
+ }
307
+
308
+ formatSearchResults(results, maxResults, searchType = "web") {
309
+ const query = results.searchParameters?.q || 'Unknown query';
310
+ let output = `## ${this.capitalizeFirst(searchType)} Search Results for "${query}"\n\n`;
311
+
312
+ // Handle different result structures based on search type
313
+ let resultArray = [];
314
+ let resultKey = "";
315
+
316
+ switch (searchType) {
317
+ case "web":
318
+ resultKey = "organic";
319
+ break;
320
+ case "images":
321
+ resultKey = "images";
322
+ break;
323
+ case "videos":
324
+ resultKey = "videos";
325
+ break;
326
+ case "news":
327
+ resultKey = "news";
328
+ break;
329
+ case "shopping":
330
+ resultKey = "shopping";
331
+ break;
332
+ default:
333
+ resultKey = "organic";
334
+ }
335
+
336
+ if (!results[resultKey] || results[resultKey].length === 0) {
337
+ return output + "No search results found.";
338
+ }
339
+
340
+ resultArray = results[resultKey];
341
+ const limitedResults = resultArray.slice(0, maxResults);
342
+
343
+ limitedResults.forEach((result, index) => {
344
+ output += `### ${index + 1}. `;
345
+
346
+ switch (searchType) {
347
+ case "web":
348
+ output += `${result.title}\n`;
349
+ output += `**URL:** ${result.link}\n`;
350
+ if (result.snippet) {
351
+ output += `**Snippet:** ${result.snippet}\n`;
352
+ }
353
+ break;
354
+
355
+ case "images":
356
+ output += `${result.title}\n`;
357
+ output += `**Image URL:** ${result.imageUrl}\n`;
358
+ output += `**Source:** ${result.source}\n`;
359
+ if (result.link) {
360
+ output += `**Page:** ${result.link}\n`;
361
+ }
362
+ break;
363
+
364
+ case "videos":
365
+ output += `${result.title}\n`;
366
+ output += `**Channel:** ${result.channel}\n`;
367
+ output += `**Duration:** ${result.duration || 'Unknown'}\n`;
368
+ if (result.link) {
369
+ output += `**URL:** ${result.link}\n`;
370
+ }
371
+ break;
372
+
373
+ case "news":
374
+ output += `${result.title}\n`;
375
+ output += `**Source:** ${result.source}\n`;
376
+ output += `**Published:** ${result.date || 'Unknown'}\n`;
377
+ if (result.link) {
378
+ output += `**URL:** ${result.link}\n`;
379
+ }
380
+ if (result.snippet) {
381
+ output += `**Snippet:** ${result.snippet}\n`;
382
+ }
383
+ break;
384
+
385
+ case "shopping":
386
+ output += `${result.title}\n`;
387
+ output += `**Price:** ${result.price || 'Price not available'}\n`;
388
+ output += `**Source:** ${result.source}\n`;
389
+ if (result.link) {
390
+ output += `**URL:** ${result.link}\n`;
391
+ }
392
+ if (result.rating) {
393
+ output += `**Rating:** ${result.rating}/5\n`;
394
+ }
395
+ break;
396
+ }
397
+
398
+ output += `\n`;
399
+ });
400
+
401
+ if (resultArray.length > maxResults) {
402
+ output += `*Showing ${maxResults} of ${resultArray.length} total results.*\n`;
403
+ }
404
+
405
+ return output;
406
+ }
407
+
408
+ capitalizeFirst(str) {
409
+ return str.charAt(0).toUpperCase() + str.slice(1);
410
+ }
411
+
412
+ async run() {
413
+ if (this.transport === 'http') {
414
+ // HTTP transport using SSE
415
+ const transport = new SSEServerTransport(this.host, this.port);
416
+ await this.server.connect(transport);
417
+ console.error(`Serper MCP server running on HTTP at http://${this.host}:${this.port}`);
418
+ } else {
419
+ // Default STDIO transport
420
+ const transport = new StdioServerTransport();
421
+ await this.server.connect(transport);
422
+ console.error("Serper MCP server running on stdio");
423
+ }
424
+ }
425
+ }
426
+
427
+ // Parse command line arguments
428
+ function parseArgs() {
429
+ const args = process.argv.slice(2);
430
+ const options = {};
431
+
432
+ for (let i = 0; i < args.length; i++) {
433
+ const arg = args[i];
434
+ switch (arg) {
435
+ case '--transport':
436
+ options.transport = args[++i];
437
+ break;
438
+ case '--port':
439
+ options.port = parseInt(args[++i]);
440
+ break;
441
+ case '--host':
442
+ options.host = args[++i];
443
+ break;
444
+ case '--log-level':
445
+ options.logLevel = args[++i];
446
+ break;
447
+ case '--api-key':
448
+ options.apiKey = args[++i];
449
+ break;
450
+ case '--help':
451
+ console.log(`
452
+ Serper MCP Server v2.0.0
453
+
454
+ Usage: node index.js [options]
455
+
456
+ Options:
457
+ --transport <stdio|http> Transport mode (default: stdio)
458
+ --port <number> HTTP server port (default: 8080)
459
+ --host <string> HTTP server host (default: "0.0.0.0")
460
+ --log-level <string> Logging level (default: "info")
461
+ --api-key <string> Serper API key
462
+ --help Show this help message
463
+
464
+ Environment Variables:
465
+ SERPER_API_KEY Your Serper API key (required)
466
+ SERPER_MCP_TRANSPORT Transport mode ("stdio" or "http")
467
+ SERPER_MCP_PORT HTTP server port
468
+ SERPER_MCP_HOST HTTP server host
469
+ SERPER_MCP_LOG_LEVEL Logging level
470
+ `);
471
+ process.exit(0);
472
+ break;
473
+ }
474
+ }
475
+
476
+ return options;
477
+ }
478
+
479
+ // Start server if this file is run directly
480
+ if (require.main === module) {
481
+ try {
482
+ const options = parseArgs();
483
+ const server = new SerperMCPServer(options);
484
+ server.run().catch((error) => {
485
+ console.error("Failed to start server:", error);
486
+ process.exit(1);
487
+ });
488
+ } catch (error) {
489
+ console.error("Error:", error.message);
490
+ process.exit(1);
491
+ }
492
+ }
493
+
494
+ module.exports = { SerperMCPServer };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "serper-search-mcp",
3
+ "version": "2.0.0",
4
+ "description": "MCP server for Serper API (Google search results) with multi-transport support",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "serper-search-mcp": "index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "SERPER_API_KEY=test npm run build && node dist/index.js",
13
+ "http": "SERPER_MCP_TRANSPORT=http npm run build && node dist/index.js",
14
+ "stdio": "SERPER_MCP_TRANSPORT=stdio npm run build && node dist/index.js",
15
+ "docker:build": "docker build -t smlabs01/server-serper-search .",
16
+ "docker:run": "docker run -i --rm -e SERPER_API_KEY smlabs01/server-serper-search",
17
+ "compose:up": "docker-compose up",
18
+ "compose:dev": "docker-compose --profile dev up",
19
+ "help": "node dist/index.js --help",
20
+ "clean": "rm -rf dist",
21
+ "rebuild": "npm run clean && npm run build"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "modelcontextprotocol",
26
+ "serper",
27
+ "search",
28
+ "google",
29
+ "api"
30
+ ],
31
+ "author": "SMJAHID from SMLabs01",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^0.5.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.19.22",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/smjahid012/serper-search-mcp-server.git"
46
+ },
47
+ "homepage": "https://github.com/smjahid012/serper-search-mcp-server#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/smjahid012/serper-search-mcp-server/issues"
50
+ },
51
+ "files": [
52
+ "dist/",
53
+ "index.js",
54
+ "README.md",
55
+ "LICENSE"
56
+ ],
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }