@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 +193 -0
- package/bin/kagi-search +3 -0
- package/bin/kagi-search-mcp +3 -0
- package/dist/package.json +71 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +57 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +2 -0
- package/dist/src/logger.js +53 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/mcp-server.d.ts +2 -0
- package/dist/src/mcp-server.js +74 -0
- package/dist/src/mcp-server.js.map +1 -0
- package/dist/src/search.d.ts +19 -0
- package/dist/src/search.js +183 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/types.d.ts +33 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +71 -0
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.
|
package/bin/kagi-search
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
|
+
}
|
|
@@ -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,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,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 @@
|
|
|
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
|
+
}
|