diff --git a/package.json b/package.json index 72ad23f4..abb1ea90 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { + "@modelcontextprotocol/server-duckduckgo": "*", "@modelcontextprotocol/server-everything": "*", "@modelcontextprotocol/server-gdrive": "*", "@modelcontextprotocol/server-postgres": "*", diff --git a/src/duckduckgo/README.md b/src/duckduckgo/README.md new file mode 100644 index 00000000..fb6ae376 --- /dev/null +++ b/src/duckduckgo/README.md @@ -0,0 +1,39 @@ +# DuckDuckGo MCP Server +MCP server providing search functionality via DuckDuckGo's HTML interface. + +## Core Concepts +### Resources +Single resource endpoint for search results: +```duckduckgo://search``` + +### Tools +Search tool with configurable result count: +```json +{ + "name": "search", + "arguments": { + "query": "your search query", + "numResults": 5 // optional, defaults to 5 + } +} +``` + +## Implementation Details +- HTML scraping via JSDOM +- Clean result formatting with titles, snippets, and URLs +- Error handling for network/parsing issues +- Request rate limiting built-in via DuckDuckGo's interface + +## Usage Example +```typescript +// Search tool response format +{ + content: [{ + type: "text", + text: "Title: Example Result\nSnippet: Result description...\nURL: https://..." + }] +} +``` + +## Development +Requires Node.js and npm. Uses ES modules. diff --git a/src/duckduckgo/index.ts b/src/duckduckgo/index.ts new file mode 100644 index 00000000..e463142d --- /dev/null +++ b/src/duckduckgo/index.ts @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import fetch from "node-fetch"; +import { JSDOM } from "jsdom"; + +const server = new Server( + { + name: "example-servers/duckduckgo", + version: "0.1.0", + }, + { + capabilities: { + tools: {}, + resources: {}, // Required since we're using ListResourcesRequestSchema + }, + }, +); + +// Add Resources List Handler - Important for showing up in the tools list +server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [ + { + uri: "duckduckgo://search", + mimeType: "text/plain", + name: "DuckDuckGo Search Results", + }, + ], + }; +}); + +// Add Read Resource Handler +server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if (request.params.uri.toString() === "duckduckgo://search") { + return { + contents: [ + { + uri: "duckduckgo://search", + mimeType: "text/plain", + text: "DuckDuckGo search interface", + }, + ], + }; + } + throw new Error("Resource not found"); +}); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "search", + description: + "Performs a search using DuckDuckGo and returns the top search results. " + + "Returns titles, snippets, and URLs of the search results. " + + "Use this tool when you need to search for current information on the internet.", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "The search query to look up", + }, + numResults: { + type: "number", + description: "Number of results to return (default: 5)", + default: 5, + }, + }, + required: ["query"], + }, + }, + ], + }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "search") { + try { + const { query, numResults = 5 } = request.params.arguments as { + query: string; + numResults?: number; + }; + + const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; + const headers = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }; + + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const html = await response.text(); + const dom = new JSDOM(html); + const document = dom.window.document; + + const results = []; + const resultElements = document.querySelectorAll(".result"); + + for (let i = 0; i < Math.min(numResults, resultElements.length); i++) { + const result = resultElements[i]; + const titleElem = result.querySelector(".result__title"); + const snippetElem = result.querySelector(".result__snippet"); + const urlElem = result.querySelector(".result__url"); + + if (titleElem && snippetElem) { + results.push({ + title: titleElem.textContent?.trim() || "", + snippet: snippetElem.textContent?.trim() || "", + url: urlElem?.getAttribute("href") || "", + }); + } + } + + const formattedResults = results + .map( + (result) => + `Title: ${result.title}\nSnippet: ${result.snippet}\nURL: ${result.url}\n`, + ) + .join("\n"); + + return { + content: [ + { + type: "text", + text: formattedResults || "No results found.", + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error performing search: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + + throw new Error(`Unknown tool: ${request.params.name}`); +}); + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("DuckDuckGo MCP Server running on stdio"); +} + +runServer().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); diff --git a/src/duckduckgo/package.json b/src/duckduckgo/package.json new file mode 100644 index 00000000..89a30463 --- /dev/null +++ b/src/duckduckgo/package.json @@ -0,0 +1,30 @@ +{ + "name": "@modelcontextprotocol/server-duckduckgo", + "version": "0.1.0", + "description": "MCP server for DuckDuckGo search", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "type": "module", + "bin": { + "mcp-server-duckduckgo": "dist/index.js" + }, + "files": ["dist"], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.5.0", + "jsdom": "^24.0.0", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2", + "@types/jsdom": "^21.1.6", + "@types/node": "^20.10.0" + } +} diff --git a/src/duckduckgo/tsconfig.json b/src/duckduckgo/tsconfig.json new file mode 100644 index 00000000..ec5da158 --- /dev/null +++ b/src/duckduckgo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] +}