webs / build /tools /setupTools.js
Spooker's picture
Upload 106 files
c92aa92 verified
import { fetchLinuxDoArticle } from '../engines/linuxdo/fetchLinuxDoArticle.js';
import { searchBaidu } from '../engines/baidu/baidu.js';
import { searchBing } from '../engines/bing/bing.js';
import { searchLinuxDo } from "../engines/linuxdo/linuxdo.js";
import { searchCsdn } from "../engines/csdn/csdn.js";
import { fetchCsdnArticle } from "../engines/csdn/fetchCsdnArticle.js";
import { z } from 'zod';
import { searchDuckDuckGo } from "../engines/duckduckgo/index.js";
import { config } from "../config.js";
import { searchExa } from "../engines/exa/index.js";
import { searchBrave } from "../engines/brave/index.js";
import { fetchGithubReadme } from "../engines/github/index.js";
import { fetchJuejinArticle } from "../engines/juejin/fetchJuejinArticle.js";
import { searchJuejin } from "../engines/juejin/index.js";
import { fetchWebContent } from "../engines/web/index.js";
import { isPublicHttpUrl } from "../utils/urlSafety.js";
// 支持的搜索引擎
const SUPPORTED_ENGINES = ['baidu', 'bing', 'linuxdo', 'csdn', 'duckduckgo', 'exa', 'brave', 'juejin'];
// 搜索引擎调用函数映射
const engineMap = {
baidu: searchBaidu,
bing: searchBing,
linuxdo: searchLinuxDo,
csdn: searchCsdn,
duckduckgo: searchDuckDuckGo,
exa: searchExa,
brave: searchBrave,
juejin: searchJuejin,
};
// Normalize engine names from different client representations (e.g. "Bing", "DuckDuckGo", "linux.do")
export function normalizeEngineName(engine) {
const cleaned = engine.trim().toLowerCase();
const compact = cleaned.replace(/[\s._-]+/g, '');
switch (compact) {
case 'baidu':
return 'baidu';
case 'bing':
return 'bing';
case 'linuxdo':
return 'linuxdo';
case 'csdn':
return 'csdn';
case 'duckduckgo':
return 'duckduckgo';
case 'exa':
return 'exa';
case 'brave':
return 'brave';
case 'juejin':
return 'juejin';
default:
return cleaned;
}
}
// 分配搜索结果数量
const distributeLimit = (totalLimit, engineCount) => {
const base = Math.floor(totalLimit / engineCount);
const remainder = totalLimit % engineCount;
return Array.from({ length: engineCount }, (_, i) => base + (i < remainder ? 1 : 0));
};
// 执行搜索
const executeSearch = async (query, engines, limit) => {
// Clean up the query string to ensure it won't cause issues due to spaces or special characters
const cleanQuery = query.trim();
console.error(`[DEBUG] Executing search, query: "${cleanQuery}", engines: ${engines.join(', ')}, limit: ${limit}`);
if (!cleanQuery) {
console.error('Query string is empty');
throw new Error('Query string cannot be empty');
}
const limits = distributeLimit(limit, engines.length);
const searchTasks = engines.map((engine, index) => {
const engineLimit = limits[index];
const searchFn = engineMap[engine];
if (!searchFn) {
console.warn(`Unsupported search engine: ${engine}`);
return Promise.resolve([]);
}
return searchFn(query, engineLimit).catch(error => {
console.error(`Search failed for engine ${engine}:`, error);
return [];
});
});
try {
const results = await Promise.all(searchTasks);
return results.flat().slice(0, limit);
}
catch (error) {
console.error('Search execution failed:', error);
throw error;
}
};
// 验证文章 URL
const validateArticleUrl = (url, type) => {
try {
const urlObj = new URL(url);
switch (type) {
case 'linuxdo':
return urlObj.hostname === 'linux.do' && url.includes('.json');
case 'csdn':
return urlObj.hostname === 'blog.csdn.net' && url.includes('/article/details/');
case 'juejin':
return urlObj.hostname === 'juejin.cn' && url.includes('/post/');
default:
return false;
}
}
catch {
return false;
}
};
// 验证 GitHub URL
const validateGithubUrl = (url) => {
try {
const isSshGithub = /^git@github\.com:/.test(url);
if (isSshGithub) {
// SSH 格式: git@github.com:owner/repo.git
return /^git@github\.com:[^\/]+\/[^\/]+/.test(url);
}
const urlObj = new URL(url);
// 支持多种 GitHub URL 格式
const isHttpsGithub = urlObj.hostname === 'github.com' || urlObj.hostname === 'www.github.com';
if (isHttpsGithub) {
// 检查路径格式: /owner/repo
const pathParts = urlObj.pathname.split('/').filter(part => part.length > 0);
return pathParts.length >= 2;
}
return false;
}
catch {
return false;
}
};
// 验证通用网页 URL
const validateWebUrl = (url) => {
return isPublicHttpUrl(url);
};
// 获取工具名称,优先使用环境变量,否则使用默认值
function getToolName(envVarName, defaultName) {
const configuredName = process.env[envVarName];
if (configuredName) {
// Validate tool name to ensure it follows MCP naming conventions
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(configuredName)) {
console.warn(`Invalid tool name "${configuredName}" from environment variable ${envVarName}. Using default: "${defaultName}"`);
return defaultName;
}
console.error(`Using custom tool name "${configuredName}" for ${envVarName}`);
return configuredName;
}
return defaultName;
}
export const setupTools = (server) => {
// Get configurable tool names from environment variables
const searchToolName = getToolName('MCP_TOOL_SEARCH_NAME', 'search');
const fetchLinuxDoToolName = getToolName('MCP_TOOL_FETCH_LINUXDO_NAME', 'fetchLinuxDoArticle');
const fetchCsdnToolName = getToolName('MCP_TOOL_FETCH_CSDN_NAME', 'fetchCsdnArticle');
const fetchGithubToolName = getToolName('MCP_TOOL_FETCH_GITHUB_NAME', 'fetchGithubReadme');
const fetchJuejinToolName = getToolName('MCP_TOOL_FETCH_JUEJIN_NAME', 'fetchJuejinArticle');
const fetchWebToolName = getToolName('MCP_TOOL_FETCH_WEB_NAME', 'fetchWebContent');
// 搜索工具
// 生成搜索工具的动态描述
const getSearchDescription = () => {
if (config.allowedSearchEngines.length === 0) {
return "Search the web using multiple engines (e.g., Baidu, Bing, DuckDuckGo, CSDN, Exa, Brave, Juejin(掘金)) with no API key required";
}
else {
const enginesText = config.allowedSearchEngines.map(e => {
switch (e) {
case 'juejin':
return 'Juejin(掘金)';
default:
return e.charAt(0).toUpperCase() + e.slice(1);
}
}).join(', ');
return `Search the web using these engines: ${enginesText} (no API key required)`;
}
};
// 生成搜索引擎选项的枚举
const getEnginesEnum = () => {
// 如果没有限制,使用所有支持的引擎
const allowedEngines = config.allowedSearchEngines.length > 0
? config.allowedSearchEngines
: [...SUPPORTED_ENGINES];
return z.enum(allowedEngines);
};
const getEngineInputSchema = () => {
const enginesEnum = getEnginesEnum();
return z.string()
.min(1, "Engine value must not be empty")
.transform((engine) => normalizeEngineName(engine))
.pipe(enginesEnum);
};
server.tool(searchToolName, getSearchDescription(), {
query: z.string().min(1, "Search query must not be empty"),
limit: z.number().min(1).max(50).default(10),
engines: z.array(getEngineInputSchema()).min(1).default([config.defaultSearchEngine])
.transform(requestedEngines => {
// 如果有配置允许的搜索引擎,过滤请求的引擎
if (config.allowedSearchEngines.length > 0) {
const filteredEngines = requestedEngines.filter(engine => config.allowedSearchEngines.includes(engine));
// 如果所有请求的引擎都被过滤掉,使用默认引擎
return filteredEngines.length > 0 ?
filteredEngines :
[config.defaultSearchEngine];
}
return requestedEngines;
})
}, async ({ query, limit = 10, engines = ['bing'] }) => {
try {
console.error(`Searching for "${query}" using engines: ${engines.join(', ')}`);
const results = await executeSearch(query.trim(), engines, limit);
return {
content: [{
type: 'text',
text: JSON.stringify({
query: query.trim(),
engines: engines,
totalResults: results.length,
results: results
}, null, 2)
}]
};
}
catch (error) {
console.error('Search tool execution failed:', error);
return {
content: [{
type: 'text',
text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
// 获取 Linux.do 文章工具
server.tool(fetchLinuxDoToolName, "Fetch full article content from a linux.do post URL", {
url: z.string().url().refine((url) => validateArticleUrl(url, 'linuxdo'), "URL must be from linux.do and end with .json")
}, async ({ url }) => {
try {
console.error(`Fetching Linux.do article: ${url}`);
const result = await fetchLinuxDoArticle(url);
return {
content: [{
type: 'text',
text: result.content
}]
};
}
catch (error) {
console.error('Failed to fetch Linux.do article:', error);
return {
content: [{
type: 'text',
text: `Failed to fetch article: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
// 获取 CSDN 文章工具
server.tool(fetchCsdnToolName, "Fetch full article content from a csdn post URL", {
url: z.string().url().refine((url) => validateArticleUrl(url, 'csdn'), "URL must be from blog.csdn.net contains /article/details/ path")
}, async ({ url }) => {
try {
console.error(`Fetching CSDN article: ${url}`);
const result = await fetchCsdnArticle(url);
return {
content: [{
type: 'text',
text: result.content
}]
};
}
catch (error) {
console.error('Failed to fetch CSDN article:', error);
return {
content: [{
type: 'text',
text: `Failed to fetch article: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
// 获取 GitHub README 工具
server.tool(fetchGithubToolName, "Fetch README content from a GitHub repository URL", {
url: z.string().min(1).refine((url) => validateGithubUrl(url), "URL must be a valid GitHub repository URL (supports HTTPS, SSH formats)")
}, async ({ url }) => {
try {
console.error(`Fetching GitHub README: ${url}`);
const result = await fetchGithubReadme(url);
if (result) {
return {
content: [{
type: 'text',
text: result
}]
};
}
else {
return {
content: [{
type: 'text',
text: 'README not found or repository does not exist'
}],
isError: true
};
}
}
catch (error) {
console.error('Failed to fetch GitHub README:', error);
return {
content: [{
type: 'text',
text: `Failed to fetch README: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
// 获取通用网页/Markdown 内容工具
server.tool(fetchWebToolName, "Fetch content from a public HTTP(S) URL (supports Markdown files and normal web pages)", {
url: z.string().url().refine((url) => validateWebUrl(url), "URL must be a public HTTP(S) address (private/local network targets are blocked)"),
maxChars: z.number().int().min(1000).max(200000).default(30000)
}, async ({ url, maxChars = 30000 }) => {
try {
console.error(`Fetching web content: ${url}`);
const result = await fetchWebContent(url, maxChars);
return {
content: [{
type: 'text',
text: JSON.stringify(result, null, 2)
}]
};
}
catch (error) {
console.error('Failed to fetch web content:', error);
return {
content: [{
type: 'text',
text: `Failed to fetch web content: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
// 获取掘金文章工具
server.tool(fetchJuejinToolName, "Fetch full article content from a Juejin(掘金) post URL", {
url: z.string().url().refine((url) => validateArticleUrl(url, 'juejin'), "URL must be from juejin.cn and contain /post/ path")
}, async ({ url }) => {
try {
console.error(`Fetching Juejin article: ${url}`);
const result = await fetchJuejinArticle(url);
return {
content: [{
type: 'text',
text: result.content
}]
};
}
catch (error) {
console.error('Failed to fetch Juejin article:', error);
return {
content: [{
type: 'text',
text: `Failed to fetch article: ${error instanceof Error ? error.message : 'Unknown error'}`
}],
isError: true
};
}
});
};