@pashvc/mcp-server-serper
Version:
Serper MCP Server supporting search and webpage scraping
238 lines (237 loc) • 10.1 kB
JavaScript
#!/usr/bin/env node
/**
* MCP server implementation that provides web search capabilities via Serper API.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { SerperClient } from "./services/serper-client.js";
import { SerperSearchTools } from "./tools/search-tool.js";
import { SerperPrompts } from "./prompts/index.js";
// Initialize Serper client with API key from environment
const serperApiKey = process.env.SERPER_API_KEY;
if (!serperApiKey) {
throw new Error("SERPER_API_KEY environment variable is required");
}
// Create Serper client, search tool, and prompts
const serperClient = new SerperClient(serperApiKey);
const searchTools = new SerperSearchTools(serperClient);
const prompts = new SerperPrompts(searchTools);
// Create MCP server
const server = new Server({
name: "Serper MCP Server",
version: "0.1.0",
}, {
capabilities: {
tools: {},
prompts: {}
},
});
/**
* Handler that lists available tools.
* Exposes a single "webSearch" tool for performing web searches.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
// Define input schema for search tool
const searchInputSchema = {
type: "object",
properties: {
q: {
type: "string",
description: "Search query with optional Google operators. Examples: 'machine learning', 'site:github.com react hooks', 'filetype:pdf deep learning', 'site:linkedin.com/in/ \"software engineer\" San Francisco', '\"exact phrase\" -exclude site:specific.com'"
},
gl: {
type: "string",
description: "Region code for search results in ISO 3166-1 alpha-2 format (e.g., 'us' for United States, 'gb' for United Kingdom, 'de' for Germany). Default: 'us'"
},
hl: {
type: "string",
description: "Language code for search results in ISO 639-1 format (e.g., 'en' for English, 'es' for Spanish, 'fr' for French). Default: 'en'"
},
location: {
type: "string",
description: "Optional location for search results (e.g., 'SoHo, New York, United States', 'California, United States')"
},
num: {
type: "number",
description: "Number of results to return (default: 10)"
},
tbs: {
type: "string",
description: "Time-based search filter ('qdr:h' for past hour, 'qdr:d' for past day, 'qdr:w' for past week, 'qdr:m' for past month, 'qdr:y' for past year)"
},
page: {
type: "number",
description: "Page number of results to return (default: 1)"
},
autocorrect: {
type: "boolean",
description: "Whether to autocorrect spelling in query"
},
// Advanced search operators
site: {
type: "string",
description: "Limit results to specific domain. Common uses: 'linkedin.com/in/' for people, 'linkedin.com/company/' for companies, 'github.com' for code, 'stackoverflow.com' for technical Q&A, 'arxiv.org' for research papers"
},
filetype: {
type: "string",
description: "Limit to specific file types (e.g., 'pdf', 'doc', 'xls')"
},
inurl: {
type: "string",
description: "Search for pages with word in URL (e.g., 'download', 'tutorial')"
},
intitle: {
type: "string",
description: "Search for pages with word in title (e.g., 'review', 'how to')"
},
related: {
type: "string",
description: "Find similar websites (e.g., 'github.com', 'stackoverflow.com')"
},
cache: {
type: "string",
description: "View Google's cached version of a specific URL (e.g., 'example.com/page')"
},
before: {
type: "string",
description: "Date before in YYYY-MM-DD format (e.g., '2024-01-01')"
},
after: {
type: "string",
description: "Date after in YYYY-MM-DD format (e.g., '2023-01-01')"
},
exact: {
type: "string",
description: "Exact phrase match. Note: You can also use quotes directly in the query (e.g., '\"machine learning\"' in the q parameter)"
},
exclude: {
type: "string",
description: "Terms to exclude from results as comma-separated string. Note: You can also use minus operator directly in query (e.g., '-spam -ads' in the q parameter)"
},
or: {
type: "string",
description: "Alternative terms as comma-separated string. Note: You can also use OR operator directly in query (e.g., 'React OR Vue OR Angular' in the q parameter)"
}
},
required: ["q"],
};
// Return list of tools with input schemas
return {
tools: [
{
name: "google_search",
description: "Advanced Google search tool that supports complex queries with operators like site:, filetype:, intitle:, before:/after:, and more. Perfect for finding LinkedIn profiles (site:linkedin.com/in/), companies (site:linkedin.com/company/), technical documentation, research papers, and specific content. Can combine multiple operators for precise results. Examples: 'site:linkedin.com/in/ \"data scientist\" Python', 'site:github.com machine learning stars:>100', 'filetype:pdf AI research after:2023-01-01'.",
inputSchema: searchInputSchema,
},
{
name: "scrape",
description: "Tool to scrape a webpage and retrieve the text and, optionally, the markdown content. It will retrieve also the JSON-LD metadata and the head metadata.",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "The URL of the webpage to scrape.",
},
includeMarkdown: {
type: "boolean",
description: "Whether to include markdown content.",
default: false,
},
},
required: ["url"],
},
},
],
};
});
/**
* Handler for the webSearch tool.
* Performs a web search using Serper API and returns results.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "google_search": {
const { q, gl, hl, location, num, tbs, page, autocorrect,
// Advanced search parameters
site, filetype, inurl, intitle, related, cache, before, after, exact, exclude, or } = request.params.arguments || {};
if (!q) {
throw new Error("Search query (q) is required");
}
// Provide defaults for region and language if not specified
const region = gl || 'us';
const language = hl || 'en';
try {
const result = await searchTools.search({
q: String(q),
gl: String(region),
hl: String(language),
location: location,
num: num,
tbs: tbs,
page: page,
autocorrect: autocorrect,
// Advanced search parameters
site: site,
filetype: filetype,
inurl: inurl,
intitle: intitle,
related: related,
cache: cache,
before: before,
after: after,
exact: exact,
exclude: exclude,
or: or
});
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
throw new Error(`Search failed: ${error}`);
}
}
case "scrape": {
const url = request.params.arguments?.url;
const includeMarkdown = request.params.arguments
?.includeMarkdown;
const result = await searchTools.scrape({ url, includeMarkdown });
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
default:
throw new Error("Unknown tool");
}
});
// Handle prompts/list requests
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return prompts.listPrompts();
});
// Handle prompts/get requests
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
return prompts.getPrompt(request.params.name, request.params.arguments || {});
});
/**
* Start the server using stdio transport.
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});