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.
- package/README.md +428 -0
- package/dist/api/SerperAPI.d.ts +30 -0
- package/dist/api/SerperAPI.d.ts.map +1 -0
- package/dist/api/SerperAPI.js +106 -0
- package/dist/api/SerperAPI.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/server/SerperMCPServer.d.ts +37 -0
- package/dist/server/SerperMCPServer.d.ts.map +1 -0
- package/dist/server/SerperMCPServer.js +166 -0
- package/dist/server/SerperMCPServer.js.map +1 -0
- package/dist/tools/SearchTools.d.ts +36 -0
- package/dist/tools/SearchTools.d.ts.map +1 -0
- package/dist/tools/SearchTools.js +257 -0
- package/dist/tools/SearchTools.js.map +1 -0
- package/dist/types/index.d.ts +59 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ResultFormatter.d.ts +28 -0
- package/dist/utils/ResultFormatter.d.ts.map +1 -0
- package/dist/utils/ResultFormatter.js +131 -0
- package/dist/utils/ResultFormatter.js.map +1 -0
- package/index.js +494 -0
- package/package.json +60 -0
|
@@ -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
|
+
}
|