orbitx 0.0.1 → 0.0.2
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/.env.example +1 -0
- package/Readme.md +81 -0
- package/orbitx.png +0 -0
- package/package.json +17 -2
- package/src/core/ai-provider.ts +25 -0
- package/src/core/base-agent.ts +297 -0
- package/src/core/deepseek-provider.ts +71 -0
- package/src/core/mcp-client.ts +113 -0
- package/src/core/mcp-computer.ts +5 -0
- package/src/core/mcp-connection.ts +12 -0
- package/src/core/mcp-ipc-connection.ts +153 -0
- package/src/core/mcp-server.ts +49 -0
- package/src/core/mcp.ts +23 -0
- package/src/core/ollama-provider.ts +70 -0
- package/src/core/skill.ts +16 -0
- package/src/index.ts +17 -0
- package/src/templates/researcher.ts +73 -0
- package/src/templates/simple.ts +64 -0
- package/src/tests/mcp.test.ts +73 -0
- package/src/tools/fs/index.ts +2 -0
- package/src/tools/fs/read-file.ts +54 -0
- package/src/tools/fs/write-file.ts +73 -0
- package/src/tools/utils/get-current-time.ts +45 -0
- package/src/tools/utils/index.ts +1 -0
- package/src/tools/www/fetch.ts +161 -0
- package/src/tools/www/index.ts +3 -0
- package/src/tools/www/read-web-page.ts +350 -0
- package/src/tools/www/web-search.ts +102 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import MCPTool from "../../core/mcp";
|
|
2
|
+
|
|
3
|
+
export const GetCurrentTimeTool = new MCPTool({
|
|
4
|
+
name: "get-current-time",
|
|
5
|
+
description: "Returns current time (24h) and today's date in Gregorian, Persian, and Islamic calendars.",
|
|
6
|
+
inputs: [],
|
|
7
|
+
execute: async (_: string, __: Record<string, any>): Promise<any> => {
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const time = now.toLocaleTimeString("en-US", {
|
|
10
|
+
hour: "2-digit",
|
|
11
|
+
minute: "2-digit",
|
|
12
|
+
hour12: false
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const gregorian = now.toLocaleDateString("en-US", {
|
|
16
|
+
weekday: "long",
|
|
17
|
+
year: "numeric",
|
|
18
|
+
month: "long",
|
|
19
|
+
day: "numeric"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const persian = now.toLocaleDateString("fa-IR-u-ca-persian", {
|
|
23
|
+
weekday: "long",
|
|
24
|
+
year: "numeric",
|
|
25
|
+
month: "long",
|
|
26
|
+
day: "numeric"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const hijri = now.toLocaleDateString("ar-SA-u-ca-islamic", {
|
|
30
|
+
weekday: "long",
|
|
31
|
+
year: "numeric",
|
|
32
|
+
month: "long",
|
|
33
|
+
day: "numeric"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
time,
|
|
38
|
+
gregorian,
|
|
39
|
+
persian,
|
|
40
|
+
hijri
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default GetCurrentTimeTool;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GetCurrentTimeTool } from "./get-current-time"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import MCPTool from "../../core/mcp";
|
|
2
|
+
|
|
3
|
+
export const FetchTool = new MCPTool({
|
|
4
|
+
name: "fetch",
|
|
5
|
+
description: "Makes HTTP requests to APIs or endpoints. For testing APIs, fetching data, or making web requests. Not intended for reading web pages (HTML).",
|
|
6
|
+
inputs: [{
|
|
7
|
+
name: "url",
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "The URL to fetch (e.g., 'https://api.example.com/data')",
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "method",
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)",
|
|
16
|
+
required: false,
|
|
17
|
+
default: "GET"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "headers",
|
|
21
|
+
type: "object",
|
|
22
|
+
description: "HTTP headers as key-value pairs (e.g., {'Authorization': 'Bearer token', 'Content-Type': 'application/json'})",
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "body",
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Request body (required for POST, PUT, PATCH methods). Can be JSON string or form data.",
|
|
29
|
+
required: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "timeout",
|
|
33
|
+
type: "number",
|
|
34
|
+
description: "Request timeout in milliseconds (default: 30000)",
|
|
35
|
+
required: false,
|
|
36
|
+
default: 30000
|
|
37
|
+
}],
|
|
38
|
+
execute: async (_: string, inputs: Record<string, any>): Promise<any> => {
|
|
39
|
+
try {
|
|
40
|
+
const { url, method = "GET", headers = {}, body, timeout = 30000 } = inputs;
|
|
41
|
+
|
|
42
|
+
if (!url || typeof url !== "string") {
|
|
43
|
+
throw new Error("url must be a non-empty string");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
new URL(url);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const validMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
53
|
+
const normalizedMethod = method.toUpperCase();
|
|
54
|
+
if (!validMethods.includes(normalizedMethod)) {
|
|
55
|
+
throw new Error(`Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (body && ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod)) {
|
|
59
|
+
throw new Error(`${normalizedMethod} requests cannot have a body`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let parsedHeaders = headers;
|
|
63
|
+
if (typeof headers === "string") {
|
|
64
|
+
try {
|
|
65
|
+
parsedHeaders = JSON.parse(headers);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error("headers must be a valid JSON object");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (parsedHeaders && typeof parsedHeaders !== "object") {
|
|
72
|
+
throw new Error("headers must be an object or JSON string");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const fetchOptions: RequestInit = {
|
|
80
|
+
method: normalizedMethod,
|
|
81
|
+
headers: parsedHeaders || {},
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (body) {
|
|
86
|
+
const isJson = typeof body === "string" && (
|
|
87
|
+
body.trim().startsWith("{") ||
|
|
88
|
+
body.trim().startsWith("[")
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (isJson && !parsedHeaders?.["Content-Type"] && !parsedHeaders?.["content-type"]) {
|
|
92
|
+
fetchOptions.headers = {
|
|
93
|
+
...parsedHeaders,
|
|
94
|
+
"Content-Type": "application/json"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
fetchOptions.body = body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const response = await fetch(url, fetchOptions);
|
|
101
|
+
|
|
102
|
+
clearTimeout(timeoutId);
|
|
103
|
+
|
|
104
|
+
const contentType = response.headers.get("content-type") || "";
|
|
105
|
+
let data;
|
|
106
|
+
|
|
107
|
+
if (contentType.includes("application/json")) {
|
|
108
|
+
try {
|
|
109
|
+
data = await response.json();
|
|
110
|
+
} catch {
|
|
111
|
+
data = await response.text();
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
data = await response.text();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = {
|
|
118
|
+
status: response.status,
|
|
119
|
+
statusText: response.statusText,
|
|
120
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
121
|
+
data: data,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timeoutId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
let errorMessage = error instanceof Error ? error.message : String(error);
|
|
139
|
+
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
if (error.name === "AbortError") {
|
|
142
|
+
errorMessage = `Request timed out after ${inputs.timeout || 30000}ms`;
|
|
143
|
+
} else if (error.message.includes("ENOTFOUND")) {
|
|
144
|
+
errorMessage = `DNS resolution failed: ${inputs.url}`;
|
|
145
|
+
} else if (error.message.includes("ECONNREFUSED")) {
|
|
146
|
+
errorMessage = `Connection refused: ${inputs.url}`;
|
|
147
|
+
} else if (error.message.includes("ECONNRESET")) {
|
|
148
|
+
errorMessage = `Connection reset by peer: ${inputs.url}`;
|
|
149
|
+
} else if (error.message.includes("ETIMEDOUT")) {
|
|
150
|
+
errorMessage = `Connection timed out: ${inputs.url}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
error: errorMessage,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export default FetchTool;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import puppeteer, { Browser } from "puppeteer-core";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import MCPTool from "../../core/mcp";
|
|
4
|
+
|
|
5
|
+
export const ReadWebPageTool = new MCPTool({
|
|
6
|
+
name: "read-web-page",
|
|
7
|
+
description: "Retrieve and extract content from a specific URL. Use this when you need to read a webpage's HTML or convert it to clean markdown text. Ideal for reading articles, documentation, API responses, or any web content. Returns structured text without navigation menus or scripts.",
|
|
8
|
+
inputs: [{
|
|
9
|
+
name: "url",
|
|
10
|
+
type: "string",
|
|
11
|
+
description: `Complete URL including protocol (http:// or https://). Example: "https://example.com/page"`,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "outputType",
|
|
16
|
+
type: "string",
|
|
17
|
+
description: `Output format: "text" returns clean markdown-formatted content without HTML tags (best for reading articles and text), "html" returns raw HTML source (use for debugging or when structure matters)`,
|
|
18
|
+
required: true,
|
|
19
|
+
}],
|
|
20
|
+
execute: async (_: string, inputs: Record<string, any>): Promise<any> => {
|
|
21
|
+
let browser: Browser | undefined;
|
|
22
|
+
try {
|
|
23
|
+
const url = inputs.url;
|
|
24
|
+
const outputType = inputs.outputType;
|
|
25
|
+
const maxWords = 5000;
|
|
26
|
+
|
|
27
|
+
if (!url || typeof url !== "string") {
|
|
28
|
+
throw new Error("Invalid parameter: 'url' must be a non-empty string like 'https://example.com'");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!url.match(/^https?:\/\//i)) {
|
|
32
|
+
throw new Error("Invalid URL format: URL must start with http:// or https://");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!outputType || typeof outputType !== "string") {
|
|
36
|
+
throw new Error("Invalid parameter: 'outputType' must be specified as either 'text' or 'html'");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (outputType !== "text" && outputType !== "html") {
|
|
40
|
+
throw new Error(`Invalid outputType '${outputType}': must be 'text' or 'html'`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
browser = await puppeteer.launch({
|
|
44
|
+
executablePath: process.env.CHROME_PATH || (os.platform() == "win32" ?
|
|
45
|
+
"C:/Program Files/Google/Chrome/Application/chrome.exe" :
|
|
46
|
+
"/usr/bin/google-chrome-stable"),
|
|
47
|
+
headless: true,
|
|
48
|
+
args: ["--no-sandbox"],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const page = await browser.newPage();
|
|
52
|
+
|
|
53
|
+
await page.evaluateOnNewDocument(() => {
|
|
54
|
+
(window as any).__name = (fn: Function, _name: string) => fn;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await page.setUserAgent(
|
|
58
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
59
|
+
);
|
|
60
|
+
await page.goto(url, { waitUntil: "networkidle0", timeout: 50000 });
|
|
61
|
+
|
|
62
|
+
if (outputType === "html") {
|
|
63
|
+
return await page.content();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const extracted = await page.evaluate(() => {
|
|
67
|
+
const getInnerText = (el: Element): string => {
|
|
68
|
+
if (el instanceof HTMLElement) return el.innerText.trim();
|
|
69
|
+
return el.textContent?.trim() || "";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const garbageSelectors = [
|
|
73
|
+
"script", "style", "noscript", "iframe", "svg", "canvas",
|
|
74
|
+
"nav", "footer", "header", "aside",
|
|
75
|
+
".sidebar", ".toc", ".menu", ".navigation", ".breadcrumb",
|
|
76
|
+
".advertisement", ".ads", ".popup", ".modal",
|
|
77
|
+
".feedback", ".rating", ".comment", ".social-share", ".share",
|
|
78
|
+
".social", ".related", ".recommended", ".popular", ".trending",
|
|
79
|
+
".footer-links", ".copyright", ".privacy", ".terms", ".cookie",
|
|
80
|
+
".consent", ".newsletter", ".subscribe", ".email-signup",
|
|
81
|
+
".download", ".app-promo", ".widget", ".right-rail", ".left-rail",
|
|
82
|
+
".sticky", ".floating", ".popover", ".tooltip", ".dropdown-menu",
|
|
83
|
+
".search", ".filter", ".sort", ".pagination", ".author-bio",
|
|
84
|
+
".meta", ".timestamp", ".category", ".tags", ".breadcrumb",
|
|
85
|
+
".video-player", ".audio-player", ".slider", ".carousel", ".tabs",
|
|
86
|
+
".accordion", ".collapse", ".toggle", ".share-buttons", ".print",
|
|
87
|
+
".pdf", ".download-link", ".external-link", ".like", ".vote",
|
|
88
|
+
".rating-stars", ".review", ".testimonial", ".footnote",
|
|
89
|
+
".reference", ".citation", ".table-of-contents", ".summary",
|
|
90
|
+
".abstract", ".excerpt", ".teaser", ".preview", ".thumbnail",
|
|
91
|
+
".avatar", ".profile", ".user", ".comment-author", ".comment-content",
|
|
92
|
+
".reply", ".thread", ".discussion", ".forum", ".chat-message",
|
|
93
|
+
".notification", ".alert", ".warning", ".error", ".success", ".info",
|
|
94
|
+
".banner", ".hero", ".jumbotron", ".cta", ".call-to-action",
|
|
95
|
+
".button-wrapper", ".btn-group", ".toolbar", ".controls",
|
|
96
|
+
".settings", ".preferences", ".language-selector", ".back-to-top",
|
|
97
|
+
".scroll-to-top", ".share-this", ".bookmark", ".favorite",
|
|
98
|
+
".poll", ".survey", ".quiz", ".load-more", ".infinite-scroll",
|
|
99
|
+
".spinner", ".loader", ".progress", ".visually-hidden", ".sr-only",
|
|
100
|
+
"[role='navigation']", "[role='banner']", "[role='complementary']",
|
|
101
|
+
"[aria-hidden='true']", ".ask-bloom", ".chat-widget", ".support-widget"
|
|
102
|
+
];
|
|
103
|
+
garbageSelectors.forEach(sel => {
|
|
104
|
+
document.querySelectorAll(sel).forEach(el => el.remove());
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
document.querySelectorAll("div, section, article").forEach(el => {
|
|
108
|
+
if (!(el instanceof HTMLElement)) return;
|
|
109
|
+
const text = el.innerText.trim();
|
|
110
|
+
if (!text.length) return;
|
|
111
|
+
const links = el.querySelectorAll("a");
|
|
112
|
+
let linkTextLength = 0;
|
|
113
|
+
links.forEach(link => {
|
|
114
|
+
if (link instanceof HTMLElement) linkTextLength += link.innerText.length;
|
|
115
|
+
});
|
|
116
|
+
if (linkTextLength / text.length > 0.7) {
|
|
117
|
+
el.remove();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let mainContent: Element | null = null;
|
|
122
|
+
|
|
123
|
+
const candidates = [
|
|
124
|
+
document.querySelector("main"),
|
|
125
|
+
document.querySelector("article"),
|
|
126
|
+
document.querySelector("[role='main']"),
|
|
127
|
+
document.querySelector(".main-content"),
|
|
128
|
+
document.querySelector(".content"),
|
|
129
|
+
document.querySelector(".post-content"),
|
|
130
|
+
document.querySelector(".prose"),
|
|
131
|
+
document.querySelector("#content"),
|
|
132
|
+
document.querySelector(".post"),
|
|
133
|
+
document.querySelector(".entry"),
|
|
134
|
+
document.querySelector(".article-content"),
|
|
135
|
+
document.querySelector(".story"),
|
|
136
|
+
document.querySelector(".body"),
|
|
137
|
+
document.querySelector(".primary"),
|
|
138
|
+
document.querySelector(".main"),
|
|
139
|
+
document.querySelector(".column-main"),
|
|
140
|
+
document.querySelector(".region-main"),
|
|
141
|
+
document.querySelector(".node-content"),
|
|
142
|
+
document.querySelector(".field-name-body"),
|
|
143
|
+
document.querySelector(".body-field"),
|
|
144
|
+
document.querySelector(".page-content"),
|
|
145
|
+
document.querySelector(".post-body"),
|
|
146
|
+
document.querySelector(".entry-content"),
|
|
147
|
+
document.querySelector(".article-body"),
|
|
148
|
+
document.querySelector(".blog-post"),
|
|
149
|
+
document.querySelector(".single-post"),
|
|
150
|
+
document.querySelector(".main-text"),
|
|
151
|
+
document.querySelector(".primary-content"),
|
|
152
|
+
document.querySelector(".content-area"),
|
|
153
|
+
document.querySelector(".site-content"),
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const candidate of candidates) {
|
|
157
|
+
if (candidate && getInnerText(candidate).length > 300) {
|
|
158
|
+
mainContent = candidate;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!mainContent) {
|
|
164
|
+
let maxTextLen = 0;
|
|
165
|
+
let bestEl: Element | null = null;
|
|
166
|
+
const walker = document.createTreeWalker(
|
|
167
|
+
document.body,
|
|
168
|
+
NodeFilter.SHOW_ELEMENT,
|
|
169
|
+
{
|
|
170
|
+
acceptNode: (node) => {
|
|
171
|
+
const el = node as Element;
|
|
172
|
+
const tag = el.tagName.toLowerCase();
|
|
173
|
+
if (["div", "section", "article", "main"].includes(tag)) {
|
|
174
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
175
|
+
}
|
|
176
|
+
return NodeFilter.FILTER_SKIP;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
while (walker.nextNode()) {
|
|
181
|
+
const el = walker.currentNode as Element;
|
|
182
|
+
const textLen = getInnerText(el).length;
|
|
183
|
+
if (textLen > maxTextLen && textLen > 500) {
|
|
184
|
+
maxTextLen = textLen;
|
|
185
|
+
bestEl = el;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
mainContent = bestEl;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!mainContent) {
|
|
192
|
+
mainContent = document.body;
|
|
193
|
+
document.querySelectorAll("aside, .sidebar, nav, footer, header").forEach(el => el.remove());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function nodeToMd(node: Node, depth = 0): string {
|
|
197
|
+
if (node.nodeType === 3) {
|
|
198
|
+
return (node.textContent || "").replace(/\s+/g, " ");
|
|
199
|
+
}
|
|
200
|
+
if (node.nodeType !== 1) return "";
|
|
201
|
+
|
|
202
|
+
const el = node as Element;
|
|
203
|
+
const tag = el.tagName.toLowerCase();
|
|
204
|
+
|
|
205
|
+
if (el instanceof HTMLElement) {
|
|
206
|
+
const style = window.getComputedStyle(el);
|
|
207
|
+
if (style.display === "none" || style.visibility === "hidden") return "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const children = () => Array.from(el.childNodes).map(n => nodeToMd(n, depth)).join("");
|
|
211
|
+
|
|
212
|
+
if (tag === "h1") return `\n\n# ${children().trim()}\n\n`;
|
|
213
|
+
if (tag === "h2") return `\n\n## ${children().trim()}\n\n`;
|
|
214
|
+
if (tag === "h3") return `\n\n### ${children().trim()}\n\n`;
|
|
215
|
+
if (tag === "h4") return `\n\n#### ${children().trim()}\n\n`;
|
|
216
|
+
if (tag === "h5") return `\n\n##### ${children().trim()}\n\n`;
|
|
217
|
+
if (tag === "h6") return `\n\n###### ${children().trim()}\n\n`;
|
|
218
|
+
|
|
219
|
+
if (tag === "br") return "\n";
|
|
220
|
+
if (tag === "hr") return "\n\n---\n\n";
|
|
221
|
+
|
|
222
|
+
if (["p", "div", "section", "article"].includes(tag)) {
|
|
223
|
+
const inner = children().trim();
|
|
224
|
+
return inner ? `\n\n${inner}\n\n` : "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (tag === "a") {
|
|
228
|
+
const anchor = el as HTMLAnchorElement;
|
|
229
|
+
const href = anchor.href;
|
|
230
|
+
const text = children().trim();
|
|
231
|
+
if (!text) return "";
|
|
232
|
+
if (href.startsWith("#")) return text;
|
|
233
|
+
if (href.startsWith("javascript:")) return text;
|
|
234
|
+
if (href && href !== text && text.length > 0) return `[${text}](${href})`;
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (tag === "img") {
|
|
239
|
+
const img = el as HTMLImageElement;
|
|
240
|
+
const alt = img.alt?.trim() || "";
|
|
241
|
+
const src = img.src;
|
|
242
|
+
if (!src || src.startsWith("data:")) return alt || "";
|
|
243
|
+
return alt ? `` : ``;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (["strong", "b"].includes(tag)) {
|
|
247
|
+
const inner = children().trim();
|
|
248
|
+
return inner ? `**${inner}**` : "";
|
|
249
|
+
}
|
|
250
|
+
if (["em", "i"].includes(tag)) {
|
|
251
|
+
const inner = children().trim();
|
|
252
|
+
return inner ? `*${inner}*` : "";
|
|
253
|
+
}
|
|
254
|
+
if (tag === "code") {
|
|
255
|
+
const inner = children().trim();
|
|
256
|
+
return inner ? `\`${inner}\`` : "";
|
|
257
|
+
}
|
|
258
|
+
if (tag === "pre") {
|
|
259
|
+
const codeEl = el.querySelector("code");
|
|
260
|
+
const lang = codeEl?.className.match(/language-(\w+)/)?.[1] || "";
|
|
261
|
+
const text = (codeEl || el).textContent?.trim() || "";
|
|
262
|
+
return text ? `\n\n\`\`\`${lang}\n${text}\n\`\`\`\n\n` : "";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (tag === "blockquote") {
|
|
266
|
+
const inner = children().trim();
|
|
267
|
+
if (!inner) return "";
|
|
268
|
+
return "\n\n" + inner.split("\n").map(l => `> ${l}`).join("\n") + "\n\n";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (tag === "ul") {
|
|
272
|
+
const items = Array.from(el.children)
|
|
273
|
+
.filter(c => c.tagName.toLowerCase() === "li")
|
|
274
|
+
.map(li => {
|
|
275
|
+
const text = nodeToMd(li, depth + 1).trim();
|
|
276
|
+
const indent = " ".repeat(depth);
|
|
277
|
+
return `${indent}- ${text}`;
|
|
278
|
+
});
|
|
279
|
+
return items.length ? `\n\n${items.join("\n")}\n\n` : "";
|
|
280
|
+
}
|
|
281
|
+
if (tag === "ol") {
|
|
282
|
+
const items = Array.from(el.children)
|
|
283
|
+
.filter(c => c.tagName.toLowerCase() === "li")
|
|
284
|
+
.map((li, i) => {
|
|
285
|
+
const text = nodeToMd(li, depth + 1).trim();
|
|
286
|
+
const indent = " ".repeat(depth);
|
|
287
|
+
return `${indent}${i + 1}. ${text}`;
|
|
288
|
+
});
|
|
289
|
+
return items.length ? `\n\n${items.join("\n")}\n\n` : "";
|
|
290
|
+
}
|
|
291
|
+
if (tag === "li") return children();
|
|
292
|
+
|
|
293
|
+
if (tag === "table") {
|
|
294
|
+
const rows = Array.from(el.querySelectorAll("tr"));
|
|
295
|
+
if (rows.length === 0) return "";
|
|
296
|
+
const toRow = (row: Element) =>
|
|
297
|
+
Array.from(row.querySelectorAll("th,td"))
|
|
298
|
+
.map(cell => cell.textContent?.replace(/\s+/g, " ").trim() || "")
|
|
299
|
+
.join(" | ");
|
|
300
|
+
const firstRow = rows[0];
|
|
301
|
+
if (!firstRow) return "";
|
|
302
|
+
const isHeaderRow = firstRow.querySelectorAll("th").length > 0;
|
|
303
|
+
const headerText = toRow(firstRow);
|
|
304
|
+
const cols = firstRow.querySelectorAll("th,td").length;
|
|
305
|
+
if (cols === 0) return "";
|
|
306
|
+
const separator = Array(cols).fill("---").join(" | ");
|
|
307
|
+
const dataRows = isHeaderRow ? rows.slice(1) : rows;
|
|
308
|
+
const bodyLines = dataRows.map(r => `| ${toRow(r)} |`).join("\n");
|
|
309
|
+
return `\n\n| ${headerText} |\n| ${separator} |\n${bodyLines}\n\n`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return children();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let md = nodeToMd(mainContent);
|
|
316
|
+
|
|
317
|
+
md = md.replace(/\n{3,}/g, "\n\n");
|
|
318
|
+
md = md.replace(/[ \t]+$/gm, "");
|
|
319
|
+
md = md.trim();
|
|
320
|
+
|
|
321
|
+
return md;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
let output = `# Source: ${url}\n\n${extracted}`;
|
|
325
|
+
|
|
326
|
+
const words = output.split(/\s+/);
|
|
327
|
+
if (words.length > maxWords) {
|
|
328
|
+
return words.slice(0, maxWords).join(" ") +
|
|
329
|
+
`\n\n---\n*Truncated: ${words.length} words → ${maxWords} words*`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!output) {
|
|
333
|
+
throw new Error(`Failed to fetch content from ${url} - received empty response`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
content: output,
|
|
338
|
+
size: output.length
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return {
|
|
342
|
+
error: error instanceof Error ? error.message : String(error),
|
|
343
|
+
};
|
|
344
|
+
} finally {
|
|
345
|
+
await browser?.close();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
export default ReadWebPageTool;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import puppeteer from "puppeteer-core";
|
|
3
|
+
import MCPTool from "../../core/mcp";
|
|
4
|
+
|
|
5
|
+
export const WebSearchTool = new MCPTool({
|
|
6
|
+
name: "web-search",
|
|
7
|
+
description: "Search the web for information, news, or resources. Returns titles, snippets, and URLs.",
|
|
8
|
+
inputs: [{
|
|
9
|
+
name: "query",
|
|
10
|
+
type: "string",
|
|
11
|
+
description: `Search term or phrase (e.g., "climate change effects", "Python tutorials")`,
|
|
12
|
+
required: true,
|
|
13
|
+
}],
|
|
14
|
+
execute: async (_: string, inputs: Record<string, any>): Promise<any> => {
|
|
15
|
+
try {
|
|
16
|
+
const query = inputs.query;
|
|
17
|
+
|
|
18
|
+
if (!query || typeof query !== "string") {
|
|
19
|
+
throw new Error(`Invalid parameter: "query" must be a non-empty search query string`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cleanedQuery = query.trim();
|
|
23
|
+
|
|
24
|
+
if (cleanedQuery.length === 0) {
|
|
25
|
+
throw new Error(`Invalid parameter: "query" cannot be empty or just whitespace`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (cleanedQuery.length > 500) {
|
|
29
|
+
throw new Error(`Search query is quite long (${cleanedQuery.length} chars). Consider shortening for better results.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const browser = await puppeteer.launch({
|
|
33
|
+
executablePath: process.env.CHROME_PATH || (os.platform() == "win32" ?
|
|
34
|
+
"C:/Program Files/Google/Chrome/Application/chrome.exe" :
|
|
35
|
+
"/usr/bin/google-chrome-stable"),
|
|
36
|
+
headless: true,
|
|
37
|
+
args: ["--no-sandbox"],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const page = await browser.newPage();
|
|
41
|
+
|
|
42
|
+
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
|
43
|
+
|
|
44
|
+
await page.goto(
|
|
45
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
|
|
46
|
+
{ waitUntil: "networkidle0", timeout: 30000 }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await page.waitForSelector(".result", { timeout: 10000 });
|
|
50
|
+
|
|
51
|
+
const results = await page.evaluate(() => {
|
|
52
|
+
const resultElements = document.querySelectorAll(".result");
|
|
53
|
+
|
|
54
|
+
return Array.from(resultElements).map((el, i) => {
|
|
55
|
+
const titleElement = el.querySelector(".result__a, .result-title, a.result__url");
|
|
56
|
+
const title = titleElement?.textContent?.trim() || "";
|
|
57
|
+
|
|
58
|
+
let url = "";
|
|
59
|
+
const linkElement = titleElement as HTMLAnchorElement;
|
|
60
|
+
if (linkElement?.href) {
|
|
61
|
+
url = linkElement.href;
|
|
62
|
+
try {
|
|
63
|
+
const urlParams = new URLSearchParams(linkElement.href.split("?")[1]);
|
|
64
|
+
const uddg = urlParams.get("uddg");
|
|
65
|
+
if (uddg) {
|
|
66
|
+
url = decodeURIComponent(uddg);
|
|
67
|
+
}
|
|
68
|
+
} catch (e) { }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const snippetElement = el.querySelector(".result__snippet, .result-snippet, .snippet");
|
|
72
|
+
const snippet = snippetElement?.textContent?.trim() || "";
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
title,
|
|
76
|
+
url,
|
|
77
|
+
snippet,
|
|
78
|
+
};
|
|
79
|
+
}).filter((r) => r.title && r.url && r.url.startsWith("http"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await browser.close();
|
|
83
|
+
|
|
84
|
+
if (results.length === 0) {
|
|
85
|
+
console.log("No results found for:", query);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!results || (Array.isArray(results) && results.length === 0)) {
|
|
90
|
+
throw new Error(`No results found for your search query`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export default WebSearchTool;
|