add mcp/cf-search-mcp.js and update README & HTML
This commit is contained in:
32
.npmignore
Normal file
32
.npmignore
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Cloudflare Workers files (not needed for MCP server)
|
||||||
|
.wrangler/
|
||||||
|
wrangler.toml
|
||||||
|
worker.js
|
||||||
|
envs.js
|
||||||
|
utils/
|
||||||
|
index.d.ts
|
||||||
|
|
||||||
|
# Documentation assets
|
||||||
|
screenshot.png
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
55
README.md
55
README.md
@@ -274,6 +274,61 @@ const byEngine = data.results.reduce((acc, result) => {
|
|||||||
}, {});
|
}, {});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## MCP 集成
|
||||||
|
|
||||||
|
通过 MCP (Model Context Protocol) 让 AI 助手直接调用你的搜索服务,获取实时搜索结果。
|
||||||
|
|
||||||
|
### 安装配置
|
||||||
|
|
||||||
|
#### 1. 添加 MCP 服务器配置
|
||||||
|
|
||||||
|
编辑配置文件([配置指南](https://modelcontextprotocol.io/quickstart/user)):
|
||||||
|
|
||||||
|
**Claude Code**: `~/.claude/config.json` / `~/.claude.json`
|
||||||
|
**Claude Desktop macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
**Claude Desktop Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"cloudflare-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@yrobot/cf-search-mcp"],
|
||||||
|
"env": {
|
||||||
|
"CF_SEARCH_URL": "https://your-worker.workers.dev",
|
||||||
|
"CF_SEARCH_TOKEN": "your-token-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**环境变量说明**:
|
||||||
|
|
||||||
|
- `CF_SEARCH_URL`: Worker 部署地址(必填)
|
||||||
|
- `CF_SEARCH_TOKEN`: 鉴权 Token(如果 Worker 配置了 TOKEN 则必填)
|
||||||
|
|
||||||
|
#### 2. 重启应用
|
||||||
|
|
||||||
|
保存配置后重启 Claude Code 或 Claude Desktop。
|
||||||
|
|
||||||
|
#### 3. 验证安装
|
||||||
|
|
||||||
|
- 在 Claude Code 中运行 `/mcp` 命令,应该能看到 `cloudflare-search` 工具。
|
||||||
|
- 或 使用 `claude mcp list`, 看到 `cloudflare-search: npx -y @yrobot/cf-search-mcp@latest - ✓ Connected` 说明配置成功
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```
|
||||||
|
用 cloudflare-search 搜索 "Cloudflare Workers 最佳实践"
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
用 cloudflare-search 搜索 "Next.js 14 新特性"
|
||||||
|
```
|
||||||
|
|
||||||
|
AI 会返回来自多个搜索引擎的聚合结果,包括标题、描述和链接。
|
||||||
|
|
||||||
## 注意与提醒
|
## 注意与提醒
|
||||||
|
|
||||||
### 🚨 重要提示
|
### 🚨 重要提示
|
||||||
|
|||||||
189
mcp/cf-search-mcp.js
Normal file
189
mcp/cf-search-mcp.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloudflare Search MCP Server
|
||||||
|
*
|
||||||
|
* This MCP server provides access to the Cloudflare Search API,
|
||||||
|
* allowing AI assistants to search across multiple search engines.
|
||||||
|
*
|
||||||
|
* Environment Variables:
|
||||||
|
* - CF_SEARCH_URL: The URL of your Cloudflare Search Worker (required)
|
||||||
|
* - CF_SEARCH_TOKEN: Authentication token for the search API (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
CallToolRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
// Get configuration from environment variables
|
||||||
|
const CF_SEARCH_URL = process.env.CF_SEARCH_URL;
|
||||||
|
const CF_SEARCH_TOKEN = process.env.CF_SEARCH_TOKEN;
|
||||||
|
|
||||||
|
if (!CF_SEARCH_URL) {
|
||||||
|
console.error("Error: CF_SEARCH_URL environment variable is required");
|
||||||
|
console.error("Example: export CF_SEARCH_URL=https://your-worker.workers.dev");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the Cloudflare Search API
|
||||||
|
*/
|
||||||
|
async function searchAPI(query, engines = null) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ q: query });
|
||||||
|
|
||||||
|
if (engines && engines.length > 0) {
|
||||||
|
params.append("engines", engines.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CF_SEARCH_TOKEN) {
|
||||||
|
params.append("token", CF_SEARCH_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${CF_SEARCH_URL}/search?${params.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to search: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and configure the MCP server
|
||||||
|
*/
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "cloudflare-search",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for listing available tools
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "search",
|
||||||
|
description:
|
||||||
|
"Search across multiple search engines (Google, Brave, DuckDuckGo, Bing) and return aggregated results. " +
|
||||||
|
"This tool provides comprehensive search results from multiple sources, with source attribution for each result.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "The search query string",
|
||||||
|
},
|
||||||
|
engines: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["google", "brave", "duckduckgo", "bing"],
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Optional: Array of search engines to use. If not specified, uses default engines. " +
|
||||||
|
"Available engines: google, brave, duckduckgo, bing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for tool execution
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
if (request.params.name !== "search") {
|
||||||
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.params.arguments) {
|
||||||
|
throw new Error("Missing arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { query, engines } = request.params.arguments;
|
||||||
|
|
||||||
|
if (!query || typeof query !== "string") {
|
||||||
|
throw new Error("Query must be a non-empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchAPI(query, engines);
|
||||||
|
|
||||||
|
// Format the results for better readability
|
||||||
|
const formattedResults = result.results
|
||||||
|
.map((item, index) => {
|
||||||
|
return `${index + 1}. [${item.engine.toUpperCase()}] ${item.title}\n ${item.description}\n ${item.url}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const summary = [
|
||||||
|
`Search Query: "${result.query}"`,
|
||||||
|
`Total Results: ${result.number_of_results}`,
|
||||||
|
`Engines Used: ${result.enabled_engines.join(", ")}`,
|
||||||
|
result.unresponsive_engines.length > 0
|
||||||
|
? `Unresponsive Engines: ${result.unresponsive_engines.join(", ")}`
|
||||||
|
: null,
|
||||||
|
"",
|
||||||
|
"Results:",
|
||||||
|
formattedResults,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: summary,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Search failed: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
console.error("Cloudflare Search MCP Server running on stdio");
|
||||||
|
console.error(`Connected to: ${CF_SEARCH_URL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@yrobot/cf-search-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for Cloudflare Search API - Aggregated search across multiple engines",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "mcp/cf-search-mcp.js",
|
||||||
|
"bin": {
|
||||||
|
"cf-search-mcp": "./mcp/cf-search-mcp.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node mcp/cf-search-mcp.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"search",
|
||||||
|
"cloudflare",
|
||||||
|
"api",
|
||||||
|
"google",
|
||||||
|
"brave",
|
||||||
|
"duckduckgo",
|
||||||
|
"bing"
|
||||||
|
],
|
||||||
|
"author": "Yrobot",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Yrobot/cloudflare-search.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -445,6 +445,87 @@ export function getSearchHtml() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MCP 集成 -->
|
||||||
|
<div class="mt-8 rounded-2xl border border-purple-200 bg-purple-50 dark:border-purple-800/40 dark:bg-purple-900/10 p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-purple-900 dark:text-purple-100 mb-4">
|
||||||
|
🤖 MCP 集成
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-purple-800 dark:text-purple-200 mb-4">
|
||||||
|
通过 MCP (Model Context Protocol) 让 AI 助手 (如 Claude) 直接调用你的搜索服务,获取实时搜索结果。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- 步骤 1 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">1</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">添加 MCP 服务器配置</p>
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300 mb-3">
|
||||||
|
编辑配置文件 (<a href="https://modelcontextprotocol.io/quickstart/user" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">配置指南</a>):
|
||||||
|
</p>
|
||||||
|
<div class="space-y-1 text-xs text-purple-700 dark:text-purple-300 mb-3">
|
||||||
|
<p><strong>Claude Code:</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/.claude/config.json</code> 或 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/.claude.json</code></p>
|
||||||
|
<p><strong>Claude Desktop (macOS):</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">~/Library/Application Support/Claude/claude_desktop_config.json</code></p>
|
||||||
|
<p><strong>Claude Desktop (Windows):</strong> <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">%APPDATA%\\Claude\\claude_desktop_config.json</code></p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-3">
|
||||||
|
<pre class="text-xs overflow-x-auto text-purple-900 dark:text-purple-100"><code id='mcp-config-json'></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 2 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">2</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">重启应用</p>
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
保存配置后重启 Claude Code 或 Claude Desktop。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤 3 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-purple-200 dark:bg-purple-800 text-purple-900 dark:text-purple-100 rounded-full flex items-center justify-center text-xs font-semibold mr-3">3</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">验证安装</p>
|
||||||
|
<div class="text-xs text-purple-700 dark:text-purple-300 space-y-1">
|
||||||
|
<p>• 在 Claude Code 中运行 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">/mcp</code> 命令,应该能看到 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">cloudflare-search</code> 工具</p>
|
||||||
|
<p>• 或 使用 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">claude mcp list</code>, 看到 <code class="px-1 py-0.5 bg-purple-100 dark:bg-purple-900/30 rounded">cloudflare-search: ... - ✓ Connected</code> 说明配置成功</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用示例 -->
|
||||||
|
<div class="rounded-lg bg-white dark:bg-purple-900/20 p-4 border border-purple-200 dark:border-purple-800/40">
|
||||||
|
<p class="text-sm font-medium text-purple-900 dark:text-purple-100 mb-2">💬 使用示例</p>
|
||||||
|
<div class="space-y-2 text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-2">
|
||||||
|
<code>用 cloudflare-search 搜索 "Cloudflare Workers 最佳实践"</code>
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-purple-100 dark:bg-purple-900/30 p-2">
|
||||||
|
<code>用 cloudflare-search 搜索 "Next.js 14 新特性"</code>
|
||||||
|
</div>
|
||||||
|
<p class="pt-2">AI 会返回来自多个搜索引擎的聚合结果,包括标题、描述和链接。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-t border-purple-200 dark:border-purple-800/40">
|
||||||
|
<p class="text-xs text-purple-700 dark:text-purple-300">
|
||||||
|
📦 NPM 包: <a href="https://www.npmjs.com/package/@yrobot/cf-search-mcp" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">@yrobot/cf-search-mcp</a> |
|
||||||
|
📚 MCP 文档: <a href="https://modelcontextprotocol.io" target="_blank" class="underline hover:text-purple-900 dark:hover:text-purple-100">modelcontextprotocol.io</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 功能特性 -->
|
<!-- 功能特性 -->
|
||||||
<div class="mt-16 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
<div class="mt-16 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
<div class="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
<div class="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
@@ -525,6 +606,18 @@ export function getSearchHtml() {
|
|||||||
|
|
||||||
document.getElementById('apiExample1').textContent = currentOrigin + '/search?q=cloudflare' + tokenParam;
|
document.getElementById('apiExample1').textContent = currentOrigin + '/search?q=cloudflare' + tokenParam;
|
||||||
document.getElementById('apiExample2').textContent = 'curl -X POST "' + currentOrigin + '/search" -d "q=cloudflare&engines=google,brave' + tokenBodyParam + '"';
|
document.getElementById('apiExample2').textContent = 'curl -X POST "' + currentOrigin + '/search" -d "q=cloudflare&engines=google,brave' + tokenBodyParam + '"';
|
||||||
|
document.getElementById('mcp-config-json').innerHTML = \`{
|
||||||
|
"mcpServers": {
|
||||||
|
"cloudflare-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@yrobot/cf-search-mcp"],
|
||||||
|
"env": {
|
||||||
|
"CF_SEARCH_URL": "\${currentOrigin}",
|
||||||
|
"CF_SEARCH_TOKEN": "\${TOKEN_ENABLED ? TOKEN_ENABLED : ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}\`
|
||||||
|
|
||||||
// 搜索表单提交
|
// 搜索表单提交
|
||||||
document.getElementById('searchForm').addEventListener('submit', async function(event) {
|
document.getElementById('searchForm').addEventListener('submit', async function(event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user