@dinerburger/kagi-search-mcp 1.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 ADDED
@@ -0,0 +1,193 @@
1
+ # Kagi Search MCP
2
+
3
+ A Node.js tool that executes Kagi searches via Kagi's HTML-only endpoint — no official API access and no headless browser required. Suitable for people with a paid Kagi plan for a better web search experience.
4
+
5
+ Kagi is a privacy-focused, ad-free search engine that delivers high-quality search results. This tool can be used directly as a command-line tool or as a Model Context Protocol (MCP) server to provide real-time search capabilities to AI assistants like Claude.
6
+
7
+
8
+ <img width="1852" height="1738" alt="CleanShot 2025-08-06 at 16 05 16@2x" src="https://github.com/user-attachments/assets/35c7300f-6e5f-4d08-a298-f3ad2482fc5e" />
9
+
10
+ ## Prerequisites
11
+
12
+ NOTE: This tool assumes you have a paid plan from kagi.com!
13
+
14
+ Before using this tool, you need to obtain a Kagi search token:
15
+
16
+ 1. Visit [Kagi.com](https://kagi.com) and log in to your account
17
+ 2. On the main page, click on the hamburger icon on the top right corner, navigate to "Session Link" below the pop-up menu, click "Copy".
18
+ 3. Paste your Session link to a safe place. It may look like https://kagi.com/search?token=xxxx, where "xxxx" refers to your unique token. Copy this token for later use. Please follow Kagi's rules and keep this token secure as it provides access to your Kagi search quota.
19
+
20
+ ## Installation
21
+
22
+ ### From npm
23
+
24
+ ```bash
25
+ # Run the CLI directly with npx (no install)
26
+ npx -p @dinerburger/kagi-search-mcp kagi-search "search keywords"
27
+
28
+ # Or install globally
29
+ npm install -g @dinerburger/kagi-search-mcp
30
+ kagi-search "search keywords"
31
+ ```
32
+
33
+ The Kagi token is read from the `KAGI_TOKEN` environment variable (or a `.env` file in the working directory).
34
+
35
+ ### From source
36
+
37
+ ```bash
38
+ # Install from source
39
+ git clone https://github.com/dinerburger/kagi-search-mcp.git
40
+ cd kagi-search-mcp
41
+
42
+ # Create a .env file and fill in your kagi token.
43
+ echo "KAGI_TOKEN={YOUR_KAGI_TOKEN_HERE}" > .env
44
+ # Change YOUR_KAGI_TOKEN_HERE to your token
45
+
46
+ # Install dependencies
47
+ npm install
48
+ # Or using yarn
49
+ yarn
50
+ # Or using pnpm
51
+ pnpm install
52
+
53
+ # Compile TypeScript code
54
+ npm run build
55
+ # Or using yarn
56
+ yarn build
57
+ # Or using pnpm
58
+ pnpm build
59
+
60
+ # Link package globally (required for MCP functionality)
61
+ npm link
62
+ # Or using yarn
63
+ yarn link
64
+ # Or using pnpm
65
+ pnpm link
66
+ ```
67
+
68
+ ## ⚠️ Security Considerations
69
+
70
+ **Before uploading to GitHub or sharing this project:**
71
+
72
+ 1. **Clean all state files**: Run `npm run clean:all` to remove cached data
73
+ 2. **Verify .gitignore**: Ensure sensitive files are properly excluded
74
+ 3. **Check for sensitive data**: Never commit `.env` files
75
+
76
+ ## Usage
77
+
78
+ ### Command Line Tool
79
+
80
+ ```bash
81
+ # Direct command line usage
82
+ kagi-search "search keywords"
83
+
84
+ # Using command line options
85
+ kagi-search --limit 5 --timeout 60000 "search keywords"
86
+
87
+ # Or using npx
88
+ npx -p @dinerburger/kagi-search-mcp kagi-search --limit 5 "search keywords"
89
+ ```
90
+
91
+ #### Output Example
92
+
93
+ ```json
94
+ {
95
+ "query": "youtube",
96
+ "results": [
97
+ {
98
+ "title": "YouTube",
99
+ "link": "https://www.youtube.com/",
100
+ "snippet": ""
101
+ },
102
+ {
103
+ "title": "💭 Copyparty - YouTube",
104
+ "link": "https://waylonwalker.com//thoughts-767",
105
+ "snippet": ""
106
+ },
107
+ {
108
+ "title": "YouTube - Wikipedia",
109
+ "link": "https://en.wikipedia.org/wiki/YouTube",
110
+ "snippet": ""
111
+ },
112
+ {
113
+ "title": "YouTube Kids",
114
+ "link": "https://www.youtubekids.com/?hl=en-GB",
115
+ "snippet": ""
116
+ },
117
+ // more results...
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ### MCP Server
123
+
124
+ This project provides Model Context Protocol (MCP) server functionality, allowing AI assistants like Claude to directly use Kagi search capabilities. MCP is an open protocol that enables AI assistants to safely access external tools and data.
125
+
126
+ ```bash
127
+ # Build the project
128
+ pnpm build
129
+ ```
130
+
131
+ ### Docker
132
+
133
+ A multi-stage `Dockerfile` is included for running the MCP server without installing Node.js locally. The container runs the MCP server over stdio, so it is meant to be launched as a subprocess by an MCP client or a proxy like [mcpo](https://github.com/open-webui/mcpo).
134
+
135
+ ```bash
136
+ # Build the image
137
+ docker build -t kagi-search-mcp .
138
+
139
+ # Run it directly (stdio MCP server; requires -i so the client can talk to stdin)
140
+ docker run -i --rm --env-file .env kagi-search-mcp
141
+ ```
142
+
143
+ The Kagi session token is read from the `KAGI_TOKEN` environment variable — pass it with `--env-file .env` or `-e KAGI_TOKEN=...`.
144
+
145
+ #### Hosting behind mcpo (OpenAPI/REST access)
146
+
147
+ [mcpo](https://github.com/open-webui/mcpo) exposes any stdio MCP server as an OpenAPI-compatible HTTP server. Run mcpo on the host and point it at the container:
148
+
149
+ ```bash
150
+ uvx mcpo --port 8000 --api-key "top-secret" -- \
151
+ docker run -i --rm --env-file .env kagi-search-mcp
152
+ ```
153
+
154
+ The tool is then available at `http://localhost:8000` with auto-generated docs at `http://localhost:8000/docs`. The tool name is `kagi-search`, so it is exposed at the `/kagi-search` route.
155
+
156
+ #### Integration
157
+
158
+ I've only tested this with Cherry studio, so any update on typical mcp clients like Claude Desktop / VScode / Cursor would be appreciated.
159
+
160
+ ```JSON
161
+ {
162
+ "mcpServers": {
163
+ "kagi-search": {
164
+ "command": "npx",
165
+ "args": ["-y", "@dinerburger/kagi-search-mcp"],
166
+ "env": {
167
+ "KAGI_TOKEN": "your_kagi_token_here"
168
+ }
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ Or with Docker:
175
+
176
+ ```JSON
177
+ {
178
+ "mcpServers": {
179
+ "kagi-search": {
180
+ "command": "docker",
181
+ "args": ["run", "-i", "--rm", "-e", "KAGI_TOKEN=your_kagi_token_here", "dinerburger/kagi-search-mcp"]
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### Final Note
188
+
189
+ This project is a fork of google-search, and I have remixed it mainly for personal use as a passionate Kagi fan. I am not a professional programmer, and this is still a work in progress, so any contributions would be appreciated.
190
+
191
+ If this project violates any rights of Kagi, please inform me, and I will take it down if I receive such a message. That said, due to the nature of the project, it cannot tolerate heavy usage, and I have not tested it beyond my regular personal searching purposes. As long as you use it in a regular way like me, it should not be an issue.
192
+
193
+ The next step is to suppport quick answer and !bang to allow the tool search more freely & accurately without posting too much cost on Kagi.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/src/index.js";
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/src/mcp-server.js";
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@dinerburger/kagi-search-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Kagi search CLI and MCP tool",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "types": "dist/src/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "bin": {
14
+ "kagi-search-mcp": "./bin/kagi-search-mcp",
15
+ "kagi-search": "./bin/kagi-search"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
23
+ "dev": "ts-node src/index.ts",
24
+ "start": "node dist/src/index.js",
25
+ "test": "ts-node src/index.ts \"typescript\"",
26
+ "test:build": "npm run build && node dist/src/index.js \"typescript\"",
27
+ "debug": "ts-node src/index.ts \"typescript\"",
28
+ "debug:build": "npm run build && node dist/src/index.js \"typescript\"",
29
+ "link": "npm link",
30
+ "clean": "node -e \"const fs = require('fs'); const path = require('path'); if (fs.existsSync('dist')) fs.rmSync('dist', { recursive: true, force: true });\"",
31
+ "clean:all": "bash scripts/clean.sh",
32
+ "clean:full": "bash scripts/clean.sh --full",
33
+ "mcp": "ts-node src/mcp-server.ts",
34
+ "mcp:build": "npm run build && node dist/src/mcp-server.js"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/dinerburger/kagi-search-mcp.git"
39
+ },
40
+ "homepage": "https://github.com/dinerburger/kagi-search-mcp#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/dinerburger/kagi-search-mcp/issues"
43
+ },
44
+ "keywords": [
45
+ "kagi",
46
+ "search",
47
+ "cli",
48
+ "mcp",
49
+ "claude"
50
+ ],
51
+ "author": "dinerburger",
52
+ "license": "ISC",
53
+ "devDependencies": {
54
+ "@types/node": "^22.13.9",
55
+ "jsdom": "^26.0.0",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.8.2"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.6.1",
61
+ "commander": "^13.1.0",
62
+ "dotenv": "^17.2.1",
63
+ "node-html-parser": "^9.0.2",
64
+ "pino": "^9.6.0",
65
+ "pino-pretty": "^13.0.0",
66
+ "zod": "^3.22.4"
67
+ },
68
+ "engines": {
69
+ "node": ">=18.0.0"
70
+ }
71
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { config } from "dotenv";
3
+ import { Command } from "commander";
4
+ import { kagiSearch, getKagiSearchPageHtml } from "./search.js";
5
+ // Load environment variables
6
+ config();
7
+ // Get package info
8
+ import packageJson from "../package.json" with { type: "json" };
9
+ // Create the command-line program
10
+ const program = new Command();
11
+ // Configure command-line options
12
+ program
13
+ .name("kagi-search")
14
+ .description("Kagi search CLI tool using Kagi's HTML search endpoint")
15
+ .version(packageJson.version)
16
+ .argument("<query>", "Search query")
17
+ .option("-l, --limit <number>", "Maximum number of results", parseInt, 10)
18
+ .option("-t, --timeout <number>", "Timeout (milliseconds)", parseInt, 30000)
19
+ .option("--get-html", "Get the raw HTML of the search results page instead of parsed results")
20
+ .option("--save-html", "Save the HTML to a file")
21
+ .option("--html-output <path>", "HTML output file path")
22
+ .action(async (query, options) => {
23
+ try {
24
+ if (options.getHtml) {
25
+ // Get the HTML
26
+ const htmlResult = await getKagiSearchPageHtml(query, options, options.saveHtml || false, options.htmlOutput);
27
+ // If HTML was saved to a file, include the file path in the output
28
+ if (options.saveHtml && htmlResult.savedPath) {
29
+ console.log(`HTML saved to file: ${htmlResult.savedPath}`);
30
+ }
31
+ // Output the result (without the full HTML, to avoid excessive console output)
32
+ const outputResult = {
33
+ query: htmlResult.query,
34
+ url: htmlResult.url,
35
+ originalHtmlLength: htmlResult.originalHtmlLength, // Original HTML length (including CSS and JavaScript)
36
+ cleanedHtmlLength: htmlResult.html.length, // Cleaned HTML length (without CSS and JavaScript)
37
+ savedPath: htmlResult.savedPath,
38
+ // Only output the first 500 characters of HTML as a preview
39
+ htmlPreview: htmlResult.html.substring(0, 500) + (htmlResult.html.length > 500 ? '...' : '')
40
+ };
41
+ console.log(JSON.stringify(outputResult, null, 2));
42
+ }
43
+ else {
44
+ // Perform a regular search
45
+ const results = await kagiSearch(query, options);
46
+ // Output the results
47
+ console.log(JSON.stringify(results, null, 2));
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error("Error:", error);
52
+ process.exit(1);
53
+ }
54
+ });
55
+ // Parse command-line arguments
56
+ program.parse(process.argv);
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGhE,6BAA6B;AAC7B,MAAM,EAAE,CAAC;AAET,mBAAmB;AACnB,OAAO,WAAW,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEhE,kCAAkC;AAClC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,iCAAiC;AACjC,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,wDAAwD,CAAC;KACrE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC;KAC5B,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;KACnC,MAAM,CAAC,sBAAsB,EAAE,2BAA2B,EAAE,QAAQ,EAAE,EAAE,CAAC;KACzE,MAAM,CAAC,wBAAwB,EAAE,wBAAwB,EAAE,QAAQ,EAAE,KAAK,CAAC;KAC3E,MAAM,CAAC,YAAY,EAAE,uEAAuE,CAAC;KAC7F,MAAM,CAAC,aAAa,EAAE,yBAAyB,CAAC;KAChD,MAAM,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;KACvD,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,OAAwF,EAAE,EAAE;IACxH,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,eAAe;YACf,MAAM,UAAU,GAAG,MAAM,qBAAqB,CAC5C,KAAK,EACL,OAAO,EACP,OAAO,CAAC,QAAQ,IAAI,KAAK,EACzB,OAAO,CAAC,UAAU,CACnB,CAAC;YAEF,mEAAmE;YACnE,IAAI,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;gBAC7C,OAAO,CAAC,GAAG,CAAC,uBAAuB,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;YAC7D,CAAC;YAED,+EAA+E;YAC/E,MAAM,YAAY,GAAG;gBACnB,KAAK,EAAE,UAAU,CAAC,KAAK;gBACvB,GAAG,EAAE,UAAU,CAAC,GAAG;gBACnB,kBAAkB,EAAE,UAAU,CAAC,kBAAkB,EAAE,sDAAsD;gBACzG,iBAAiB,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,mDAAmD;gBAC9F,SAAS,EAAE,UAAU,CAAC,SAAS;gBAC/B,4DAA4D;gBAC5D,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7F,CAAC;YAEF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,2BAA2B;YAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAEjD,qBAAqB;YACrB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,+BAA+B;AAC/B,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ declare const logger: import("pino").Logger<never, boolean>;
2
+ export default logger;
@@ -0,0 +1,53 @@
1
+ import { pino } from "pino";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ // Use the system temp directory to ensure cross-platform compatibility
6
+ const logDir = path.join(os.tmpdir(), "google-search-logs");
7
+ if (!fs.existsSync(logDir)) {
8
+ fs.mkdirSync(logDir, { recursive: true });
9
+ }
10
+ // Create the log file path
11
+ const logFilePath = path.join(logDir, "google-search.log");
12
+ // Create the pino logger instance
13
+ const logger = pino({
14
+ level: process.env.LOG_LEVEL || "info", // Log level can be set via environment variable
15
+ transport: {
16
+ targets: [
17
+ // Output to console, prettified with pino-pretty
18
+ {
19
+ target: "pino-pretty",
20
+ level: "info",
21
+ options: {
22
+ colorize: true,
23
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss",
24
+ ignore: "pid,hostname",
25
+ },
26
+ },
27
+ // Output to file - use trace level to capture all logs
28
+ {
29
+ target: "pino/file",
30
+ level: "trace", // Use the lowest level to capture all logs
31
+ options: { destination: logFilePath },
32
+ },
33
+ ],
34
+ },
35
+ });
36
+ // Handle process exit
37
+ process.on("exit", () => {
38
+ logger.info("Process exiting, closing logs");
39
+ });
40
+ process.on("SIGINT", () => {
41
+ logger.info("Received SIGINT signal, closing logs");
42
+ process.exit(0);
43
+ });
44
+ process.on("SIGTERM", () => {
45
+ logger.info("Received SIGTERM signal, closing logs");
46
+ process.exit(0);
47
+ });
48
+ process.on("uncaughtException", (error) => {
49
+ logger.error({ err: error }, "Uncaught exception");
50
+ process.exit(1);
51
+ });
52
+ export default logger;
53
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,uEAAuE;AACvE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC;AAC5D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3B,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,2BAA2B;AAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE3D,kCAAkC;AAClC,MAAM,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,EAAE,gDAAgD;IACxF,SAAS,EAAE;QACT,OAAO,EAAE;YACP,iDAAiD;YACjD;gBACE,MAAM,EAAE,aAAa;gBACrB,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE;oBACP,QAAQ,EAAE,IAAI;oBACd,aAAa,EAAE,yBAAyB;oBACxC,MAAM,EAAE,cAAc;iBACvB;aACF;YACD,uDAAuD;YACvD;gBACE,MAAM,EAAE,WAAW;gBACnB,KAAK,EAAE,OAAO,EAAE,2CAA2C;gBAC3D,OAAO,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE;aACtC;SACF;KACF;CACF,CAAC,CAAC;AAEH,sBAAsB;AACtB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;IACtB,MAAM,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;IACxB,MAAM,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,MAAM,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;IACxC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { config } from "dotenv";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import { kagiSearch } from "./search.js";
7
+ import logger from "./logger.js";
8
+ // Load environment variables
9
+ config();
10
+ // Create the MCP server instance
11
+ const server = new McpServer({
12
+ name: "kagi-search",
13
+ version: "0.1.0",
14
+ });
15
+ // Register the Kagi search tool
16
+ server.tool("kagi-search", "Search the web in real time using the Kagi search engine, returning search results with titles, links, and snippets. Kagi is a privacy-focused search engine that provides high-quality, ad-free results. Suitable for scenarios such as getting the latest information, finding material on a specific topic, researching current events, or verifying facts. Results are returned as JSON, containing the query and a list of matching results.", {
17
+ query: z
18
+ .string()
19
+ .describe("The search query string. For best results: 1) use specific keywords rather than vague phrases; 2) use quotes around an \"exact phrase\" to force a match; 3) use site:domain to restrict results to a specific site; 4) use - to exclude words from the results; 5) use OR to connect alternative terms; 6) prefer technical terminology; 7) keep it to 2-5 keywords for balanced results. For example: 'climate change report 2024 site:gov -opinion' or '\"machine learning algorithms\" tutorial (Python OR Julia)'"),
20
+ limit: z
21
+ .number()
22
+ .optional()
23
+ .describe("Number of search results to return (default: 10, recommended range: 1-20)"),
24
+ timeout: z
25
+ .number()
26
+ .optional()
27
+ .describe("Timeout for the search operation in milliseconds (default: 30000)"),
28
+ }, async (params) => {
29
+ try {
30
+ const { query, limit, timeout } = params;
31
+ logger.info({ query }, "Performing Kagi search");
32
+ const results = await kagiSearch(query, { limit, timeout });
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: JSON.stringify(results, null, 2),
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ catch (error) {
43
+ logger.error({ error }, "Search tool execution error");
44
+ return {
45
+ isError: true,
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
50
+ },
51
+ ],
52
+ };
53
+ }
54
+ });
55
+ // Start the server
56
+ async function main() {
57
+ try {
58
+ logger.info("Starting Kagi search MCP server...");
59
+ // Check environment variables
60
+ if (!process.env.KAGI_TOKEN && !process.env.KAGI_SESSION_TOKEN) {
61
+ logger.error("KAGI_TOKEN environment variable is not set. Please set your Kagi token in the .env file.");
62
+ process.exit(1);
63
+ }
64
+ const transport = new StdioServerTransport();
65
+ await server.connect(transport);
66
+ logger.info("Kagi search MCP server started, waiting for connections...");
67
+ }
68
+ catch (error) {
69
+ logger.error({ error }, "Failed to start the server");
70
+ process.exit(1);
71
+ }
72
+ }
73
+ main();
74
+ //# sourceMappingURL=mcp-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../../src/mcp-server.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,6BAA6B;AAC7B,MAAM,EAAE,CAAC;AAET,iCAAiC;AACjC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,gCAAgC;AAChC,MAAM,CAAC,IAAI,CACT,aAAa,EACb,mbAAmb,EACnb;IACE,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CACP,wfAAwf,CACzf;IACH,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,2EAA2E,CAAC;IACxF,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,mEAAmE,CAAC;CACjF,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;IACf,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,wBAAwB,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAE5D,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;iBACvC;aACF;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,6BAA6B,CAAC,CAAC;QAEvD,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,kBACJ,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE;iBACH;aACF;SACF,CAAC;IACJ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,mBAAmB;AACnB,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;QAElD,8BAA8B;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;YAC/D,MAAM,CAAC,KAAK,CAAC,0FAA0F,CAAC,CAAC;YACzG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAEhC,MAAM,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,4BAA4B,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { SearchResponse, CommandOptions, HtmlResponse } from "./types.js";
2
+ /**
3
+ * Perform a Kagi search and return the results
4
+ * @param query Search keywords
5
+ * @param options Search options
6
+ * @returns Search results
7
+ */
8
+ export declare function kagiSearch(query: string, options?: CommandOptions): Promise<SearchResponse>;
9
+ /**
10
+ * Get the raw HTML of the Kagi search results page
11
+ * @param query Search keywords
12
+ * @param options Search options
13
+ * @param saveToFile Whether to save the HTML to a file
14
+ * @param outputPath Output file path
15
+ * @returns HTML response
16
+ */
17
+ export declare function getKagiSearchPageHtml(query: string, options?: CommandOptions, saveToFile?: boolean, outputPath?: string): Promise<HtmlResponse>;
18
+ export declare const googleSearch: typeof kagiSearch;
19
+ export declare const getGoogleSearchPageHtml: typeof getKagiSearchPageHtml;
@@ -0,0 +1,183 @@
1
+ import { parse } from "node-html-parser";
2
+ import * as fs from "fs";
3
+ import logger from "./logger.js";
4
+ // Kagi serves the fully server-rendered "HTML-only" search UI at /html/search,
5
+ // which is designed for browsers without JavaScript. A plain HTTP request with
6
+ // a browser-like User-Agent and the session cookie works fine — no headless
7
+ // browser or bot-evasion machinery required.
8
+ const KAGI_HTML_SEARCH_URL = "https://kagi.com/html/search";
9
+ const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15";
10
+ /**
11
+ * Get the Kagi session token from environment variables.
12
+ * Accepts KAGI_SESSION_TOKEN or the legacy KAGI_TOKEN variable name.
13
+ */
14
+ function getKagiToken() {
15
+ const token = process.env.KAGI_SESSION_TOKEN || process.env.KAGI_TOKEN;
16
+ if (!token) {
17
+ throw new Error("KAGI_TOKEN environment variable is not set. Please set your Kagi session token in the .env file.");
18
+ }
19
+ return token;
20
+ }
21
+ /**
22
+ * Perform a GET request against Kagi's HTML search endpoint.
23
+ * @param query Search keywords
24
+ * @returns Raw HTML of the search results page
25
+ */
26
+ async function fetchKagiSearchHtml(query) {
27
+ const token = getKagiToken();
28
+ const response = await fetch(`${KAGI_HTML_SEARCH_URL}?q=${encodeURIComponent(query)}`, {
29
+ headers: {
30
+ "User-Agent": USER_AGENT,
31
+ Cookie: `kagi_session=${token}`,
32
+ },
33
+ });
34
+ if (!response.ok) {
35
+ if (response.status === 401 || response.status === 403) {
36
+ throw new Error("Invalid or expired Kagi session token");
37
+ }
38
+ throw new Error(`Kagi search request failed: HTTP ${response.status} ${response.statusText}`);
39
+ }
40
+ return response.text();
41
+ }
42
+ /**
43
+ * Extract a single search result element.
44
+ * @param element A `.search-result` container element
45
+ * @returns A search result, or null if the element could not be parsed
46
+ */
47
+ function extractSearchResult(element) {
48
+ try {
49
+ const titleLink = element.querySelector(".__sri_title_link");
50
+ if (!titleLink)
51
+ return null;
52
+ const title = titleLink.textContent.trim();
53
+ const link = titleLink.getAttribute("href") || "";
54
+ const snippetElement = element.querySelector(".__sri-desc");
55
+ const snippet = snippetElement ? snippetElement.textContent.trim() : "";
56
+ if (!title || !link)
57
+ return null;
58
+ return { title, link, snippet };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Extract a single result from a grouped result element.
66
+ * @param element A `.__srgi` grouped result container element
67
+ * @returns A search result, or null if the element could not be parsed
68
+ */
69
+ function extractGroupedResult(element) {
70
+ try {
71
+ const titleLink = element.querySelector(".__srgi-title a");
72
+ if (!titleLink)
73
+ return null;
74
+ const title = titleLink.textContent.trim();
75
+ const link = titleLink.getAttribute("href") || "";
76
+ const snippetElement = element.querySelector(".__sri-desc");
77
+ const snippet = snippetElement ? snippetElement.textContent.trim() : "";
78
+ if (!title || !link)
79
+ return null;
80
+ return { title, link, snippet };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Parse search results from the Kagi HTML search page.
88
+ * @param html Raw HTML of the search results page
89
+ * @param limit Maximum number of results to return
90
+ * @returns Parsed search results
91
+ */
92
+ function parseSearchResults(html, limit) {
93
+ const root = parse(html);
94
+ const results = [];
95
+ // Top-level results
96
+ for (const element of root.querySelectorAll(".search-result")) {
97
+ if (results.length >= limit)
98
+ break;
99
+ const result = extractSearchResult(element);
100
+ if (result) {
101
+ results.push(result);
102
+ }
103
+ }
104
+ // Grouped results (news, images, etc.), only if we still need more
105
+ if (results.length < limit) {
106
+ for (const element of root.querySelectorAll(".sr-group .__srgi")) {
107
+ if (results.length >= limit)
108
+ break;
109
+ const result = extractGroupedResult(element);
110
+ if (result) {
111
+ results.push(result);
112
+ }
113
+ }
114
+ }
115
+ return results;
116
+ }
117
+ /**
118
+ * Perform a Kagi search and return the results
119
+ * @param query Search keywords
120
+ * @param options Search options
121
+ * @returns Search results
122
+ */
123
+ export async function kagiSearch(query, options = {}) {
124
+ const { limit = 10 } = options;
125
+ logger.info({ query }, "Performing Kagi search");
126
+ const html = await fetchKagiSearchHtml(query);
127
+ const results = parseSearchResults(html, limit);
128
+ logger.info({ count: results.length }, "Successfully extracted search results");
129
+ return {
130
+ query,
131
+ results,
132
+ };
133
+ }
134
+ /**
135
+ * Clean raw page HTML by removing script tags, style tags, and comments.
136
+ * @param html Raw page HTML
137
+ * @returns Cleaned HTML
138
+ */
139
+ function cleanHtml(html) {
140
+ return html
141
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
142
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
143
+ .replace(/<!--[\s\S]*?-->/g, "");
144
+ }
145
+ /**
146
+ * Get the raw HTML of the Kagi search results page
147
+ * @param query Search keywords
148
+ * @param options Search options
149
+ * @param saveToFile Whether to save the HTML to a file
150
+ * @param outputPath Output file path
151
+ * @returns HTML response
152
+ */
153
+ export async function getKagiSearchPageHtml(query, options = {}, saveToFile = false, outputPath) {
154
+ logger.info({ query }, "Fetching Kagi search page HTML...");
155
+ const originalHtml = await fetchKagiSearchHtml(query);
156
+ const cleanedHtml = cleanHtml(originalHtml);
157
+ let savedPath;
158
+ // Save the HTML to a file (if requested)
159
+ if (saveToFile) {
160
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
161
+ const defaultPath = `kagi-search-${timestamp}.html`;
162
+ savedPath = outputPath || defaultPath;
163
+ try {
164
+ fs.writeFileSync(savedPath, originalHtml, "utf8");
165
+ logger.info({ path: savedPath }, "HTML saved to file");
166
+ }
167
+ catch (error) {
168
+ logger.error({ error, path: savedPath }, "Failed to save the HTML file");
169
+ savedPath = undefined;
170
+ }
171
+ }
172
+ return {
173
+ query,
174
+ html: cleanedHtml,
175
+ url: `${KAGI_HTML_SEARCH_URL}?q=${encodeURIComponent(query)}`,
176
+ savedPath,
177
+ originalHtmlLength: originalHtml.length,
178
+ };
179
+ }
180
+ // For backward compatibility, export the old function names (but using the new implementation)
181
+ export const googleSearch = kagiSearch;
182
+ export const getGoogleSearchPageHtml = getKagiSearchPageHtml;
183
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Search result interface
3
+ */
4
+ export interface SearchResult {
5
+ title: string;
6
+ link: string;
7
+ snippet: string;
8
+ }
9
+ /**
10
+ * Search response interface
11
+ */
12
+ export interface SearchResponse {
13
+ query: string;
14
+ results: SearchResult[];
15
+ }
16
+ /**
17
+ * Command-line options interface
18
+ */
19
+ export interface CommandOptions {
20
+ limit?: number;
21
+ timeout?: number;
22
+ locale?: string;
23
+ }
24
+ /**
25
+ * HTML response interface - used for fetching the raw search page HTML
26
+ */
27
+ export interface HtmlResponse {
28
+ query: string;
29
+ html: string;
30
+ url: string;
31
+ savedPath?: string;
32
+ originalHtmlLength?: number;
33
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@dinerburger/kagi-search-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Kagi search CLI and MCP tool",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "types": "dist/src/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "bin": {
14
+ "kagi-search-mcp": "./bin/kagi-search-mcp",
15
+ "kagi-search": "./bin/kagi-search"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
23
+ "dev": "ts-node src/index.ts",
24
+ "start": "node dist/src/index.js",
25
+ "test": "ts-node src/index.ts \"typescript\"",
26
+ "test:build": "npm run build && node dist/src/index.js \"typescript\"",
27
+ "debug": "ts-node src/index.ts \"typescript\"",
28
+ "debug:build": "npm run build && node dist/src/index.js \"typescript\"",
29
+ "link": "npm link",
30
+ "clean": "node -e \"const fs = require('fs'); const path = require('path'); if (fs.existsSync('dist')) fs.rmSync('dist', { recursive: true, force: true });\"",
31
+ "clean:all": "bash scripts/clean.sh",
32
+ "clean:full": "bash scripts/clean.sh --full",
33
+ "mcp": "ts-node src/mcp-server.ts",
34
+ "mcp:build": "npm run build && node dist/src/mcp-server.js"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/dinerburger/kagi-search-mcp.git"
39
+ },
40
+ "homepage": "https://github.com/dinerburger/kagi-search-mcp#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/dinerburger/kagi-search-mcp/issues"
43
+ },
44
+ "keywords": [
45
+ "kagi",
46
+ "search",
47
+ "cli",
48
+ "mcp",
49
+ "claude"
50
+ ],
51
+ "author": "dinerburger",
52
+ "license": "ISC",
53
+ "devDependencies": {
54
+ "@types/node": "^22.13.9",
55
+ "jsdom": "^26.0.0",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.8.2"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.6.1",
61
+ "commander": "^13.1.0",
62
+ "dotenv": "^17.2.1",
63
+ "node-html-parser": "^9.0.2",
64
+ "pino": "^9.6.0",
65
+ "pino-pretty": "^13.0.0",
66
+ "zod": "^3.22.4"
67
+ },
68
+ "engines": {
69
+ "node": ">=18.0.0"
70
+ }
71
+ }