web3-research-mcp
Version:
Deep Research for crypto - free & fully local
548 lines • 22.5 kB
JavaScript
import { z } from "zod";
import { performSearch, fetchContent, searchSource, sleep, } from "../utils/searchUtils.js";
export async function getResourceContent(url, storage) {
if (url.startsWith("research://resource/")) {
const resourceId = url.replace("research://resource/", "");
const resource = storage.getResource(resourceId);
if (!resource) {
throw new Error(`Resource not found: ${resourceId}`);
}
return resource.content;
}
return fetchContent(url, "markdown");
}
export function registerResearchTools(server, storage) {
server.tool("search", {
query: z.string().describe("Search query"),
searchType: z
.enum(["web", "news", "images", "videos"])
.default("web")
.describe("Type of search"),
}, async ({ query, searchType }) => {
storage.addLogEntry(`Performing ${searchType} search for: "${query}"`);
try {
const results = await performSearch(query, searchType);
storage.addToSection("searchResults", {
[searchType]: {
[query]: results,
},
});
return {
content: [
{
type: "text",
text: `Search results for "${query}" (${searchType}):\n\n${JSON.stringify(results.results ? results.results.slice(0, 5) : [], null, 2)}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error searching for "${query}": ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error performing search: ${error}`,
},
],
};
}
});
server.tool("create-research-plan", {
tokenName: z.string().describe("Token name"),
tokenTicker: z.string().describe("Token ticker symbol"),
}, async ({ tokenName, tokenTicker, }) => {
storage.addLogEntry(`Creating research plan for ${tokenName} (${tokenTicker})`);
const researchPlan = {
projectInfo: {
description: "Gather basic information about the project",
sources: ["Project website", "Documentation", "CoinMarketCap"],
status: "planned",
},
technicalFundamentals: {
description: "Analyze the token's technical aspects",
sources: ["Documentation", "GitHub", "IQ Wiki", "Token contract"],
status: "planned",
},
marketStatus: {
description: "Evaluate current market performance",
sources: ["CoinMarketCap", "TradingView", "GeckoTerminal"],
status: "planned",
},
listings: {
description: "Find where the token is traded",
sources: ["GeckoTerminal", "CoinMarketCap"],
status: "planned",
},
news: {
description: "Gather recent news about the token",
sources: ["Crypto news sites", "Twitter", "Medium"],
status: "planned",
},
community: {
description: "Analyze the project's community",
sources: ["Twitter", "Discord", "Telegram", "Reddit"],
status: "planned",
},
predictions: {
description: "Collect price predictions and forecasts",
sources: ["Analysis sites", "Expert opinions"],
status: "planned",
},
teamInfo: {
description: "Research the team behind the project",
sources: ["Project website", "LinkedIn", "Twitter"],
status: "planned",
},
relatedCoins: {
description: "Identify tokens in the same category",
sources: ["GeckoTerminal", "CoinMarketCap"],
status: "planned",
},
socialSentiment: {
description: "Gauge social media sentiment",
sources: ["Twitter", "Reddit", "Trading forums"],
status: "planned",
},
};
storage.updateSection("researchPlan", researchPlan);
return {
content: [
{
type: "text",
text: `Created research plan for ${tokenName} (${tokenTicker}):\n\n${JSON.stringify(researchPlan, null, 2)}`,
},
],
};
});
server.tool("research-with-keywords", {
tokenName: z.string().describe("Name of the token"),
tokenTicker: z.string().describe("Ticker symbol of the token"),
keywords: z.array(z.string()).describe("Keywords to search for"),
}, async ({ tokenName, tokenTicker, keywords, }) => {
storage.addLogEntry(`Researching ${tokenName} with keywords: ${keywords.join(", ")}`);
try {
const results = {};
for (const keyword of keywords) {
const query = `${tokenName} ${tokenTicker} ${keyword}`;
storage.addLogEntry(`Searching for: ${query}`);
await sleep(2000);
const searchResults = await performSearch(query, "web");
if (!searchResults.results || searchResults.results.length === 0) {
results[keyword] = { error: "No results found" };
continue;
}
const topResults = searchResults.results.slice(0, 3);
results[keyword] = topResults;
storage.addToSection("searchResults", { [keyword]: topResults });
}
const resourceId = `combined_search_${tokenName.toLowerCase()}_${new Date().getTime()}`;
storage.addToSection("resources", {
[resourceId]: {
format: "json",
content: JSON.stringify(results, null, 2),
title: `Combined search results for ${tokenName}`,
fetchedAt: new Date().toISOString(),
},
});
return {
content: [
{
type: "text",
text: `Completed searches for ${tokenName} with keywords: ${keywords.join(", ")}\n\nResults saved as resource: research://resource/${resourceId}\n\nHighlights:\n${Object.entries(results)
.map(([keyword, data]) => {
if (Array.isArray(data) && data.length > 0) {
return `- ${keyword}: ${data[0].title} (${data[0].url})`;
}
return `- ${keyword}: No results`;
})
.join("\n")}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error in keyword research: ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error performing keyword research: ${error}`,
},
],
};
}
});
server.tool("update-status", {
section: z
.string()
.describe("Section name to update (e.g., 'projectInfo', 'technicalFundamentals')"),
status: z
.enum(["planned", "in_progress", "completed"])
.describe("New status for the section"),
}, async ({ section, status, }) => {
try {
const researchPlan = storage.getSection("researchPlan");
if (!researchPlan || !researchPlan[section]) {
return {
isError: true,
content: [
{
type: "text",
text: `Section '${section}' not found in research plan`,
},
],
};
}
const updatedSection = {
...researchPlan[section],
status,
};
const updatedPlan = {
...researchPlan,
[section]: updatedSection,
};
storage.updateSection("researchPlan", updatedPlan);
storage.addLogEntry(`Updated status of ${section} to ${status}`);
return {
content: [
{
type: "text",
text: `Updated status of '${section}' to '${status}'`,
},
],
};
}
catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Error updating status: ${error}`,
},
],
};
}
});
server.tool("fetch-content", {
url: z
.string()
.describe("URL to fetch content from (can be a resource:// URL)"),
format: z
.enum(["text", "html", "markdown", "json"])
.default("markdown")
.describe("Output format"),
}, async ({ url, format, }) => {
storage.addLogEntry(`Fetching content from: ${url} (format: ${format})`);
try {
let content;
if (url.startsWith("research://resource/")) {
content = await getResourceContent(url, storage);
}
else {
content = await fetchContent(url, format);
}
const resourceId = url.startsWith("research://resource/")
? `derived_${url.replace("research://resource/", "")}_${new Date().getTime()}`
: url
.replace(/https?:\/\//, "")
.replace(/[^\w]/g, "_")
.substring(0, 30);
storage.addToSection("resources", {
[resourceId]: {
url,
format,
content,
fetchedAt: new Date().toISOString(),
},
});
return {
content: [
{
type: "text",
text: `Fetched content from ${url} (${format}):\n\n${content.substring(0, 1000)}${content.length > 1000
? "...\n\n[Content truncated, full version saved as resource]"
: ""}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error fetching content from ${url}: ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error fetching content: ${error}`,
},
],
};
}
});
server.tool("search-source", {
tokenName: z.string().describe("Name of the token"),
tokenTicker: z.string().describe("Ticker symbol of the token"),
source: z
.string()
.describe("Source to search (e.g., 'Dune', 'IQ Wiki', 'News')"),
}, async ({ tokenName, tokenTicker, source, }) => {
storage.addLogEntry(`Searching ${source} for ${tokenName} (${tokenTicker})`);
try {
const results = await searchSource(tokenName, tokenTicker, source);
storage.addToSection("searchResults", {
[source]: results,
});
let responseText = `Search results for ${source} about ${tokenName} (${tokenTicker}):\n\n`;
if (results.results && results.results.length > 0) {
const topResults = results.results.slice(0, 5);
responseText += JSON.stringify(topResults, null, 2);
if (topResults[0] && topResults[0].url) {
const url = topResults[0].url;
responseText += `\n\nFetching content from top result: ${url}`;
try {
await sleep(3000);
const content = await fetchContent(url, "text");
const resourceId = `${source.toLowerCase()}_${tokenName.toLowerCase()}_${new Date().getTime()}`;
storage.addToSection("resources", {
[resourceId]: {
url,
format: "text",
content,
source,
fetchedAt: new Date().toISOString(),
},
});
responseText += `\n\nContent has been saved as a resource. Use 'research://resource/${resourceId}' to access it.`;
}
catch (fetchError) {
responseText += `\n\nCould not fetch content from URL: ${fetchError}`;
}
}
}
else {
responseText += `No results found.`;
}
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error searching ${source}: ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error searching ${source}: ${error}`,
},
],
};
}
});
server.tool("list-resources", {}, async () => {
try {
const resources = storage.getAllResources();
const resourceList = Object.keys(resources).map((id) => ({
id,
url: resources[id].url,
title: resources[id].title || "No title",
source: resources[id].source || "Unknown",
contentLength: resources[id].content?.length || 0,
fetchedAt: resources[id].fetchedAt,
}));
return {
content: [
{
type: "text",
text: `Available resources:\n\n${JSON.stringify(resourceList, null, 2)}`,
},
],
};
}
catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Error listing resources: ${error}`,
},
],
};
}
});
server.tool("research-source", {
tokenName: z.string().describe("Name of the token"),
tokenTicker: z.string().describe("Ticker symbol of the token"),
source: z.string().describe("Single source to research"),
}, async ({ tokenName, tokenTicker, source, }) => {
storage.addLogEntry(`Researching source: ${source} for ${tokenName} (${tokenTicker})`);
try {
const query = `${tokenName} ${tokenTicker} ${source}`;
const results = await performSearch(query, "web");
if (!results.results || results.results.length === 0) {
storage.addLogEntry(`No results found for ${source}`);
return {
content: [
{
type: "text",
text: `No results found for ${source}`,
},
],
};
}
const topResults = results.results.slice(0, 3);
storage.addToSection("searchResults", { [source]: topResults });
if (topResults[0] && topResults[0].url) {
try {
const url = topResults[0].url;
storage.addLogEntry(`Fetching content from ${url}`);
const content = await fetchContent(url, "markdown");
const resourceId = `${source.toLowerCase()}_${tokenName.toLowerCase()}_${new Date().getTime()}`;
storage.addToSection("resources", {
[resourceId]: {
url,
format: "markdown",
content,
title: topResults[0].title,
source,
fetchedAt: new Date().toISOString(),
},
});
return {
content: [
{
type: "text",
text: `Researched ${source} for ${tokenName} (${tokenTicker}).\n\nTop result: ${topResults[0].title}\n\nContent saved as resource: research://resource/${resourceId}\n\nAll search results:\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error fetching content from ${topResults[0].url}: ${error}`);
return {
content: [
{
type: "text",
text: `Found search results for ${source}, but couldn't fetch content: ${error}\n\nSearch results:\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
}
return {
content: [
{
type: "text",
text: `Search results for ${source}:\n\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error researching ${source}: ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error researching ${source}: ${error}`,
},
],
};
}
});
server.tool("research-token", {
tokenName: z.string().describe("Name of the token"),
tokenTicker: z.string().describe("Ticker symbol of the token"),
source: z
.string()
.describe("Source to research (e.g., 'IQ Wiki', 'CoinMarketCap')"),
}, async ({ tokenName, tokenTicker, source, }) => {
storage.addLogEntry(`Researching source: ${source} for ${tokenName} (${tokenTicker})`);
try {
const query = `${tokenName} ${tokenTicker} ${source}`;
const results = await performSearch(query, "web");
if (!results.results || results.results.length === 0) {
storage.addLogEntry(`No results found for ${source}`);
return {
content: [
{
type: "text",
text: `No results found for ${source}`,
},
],
};
}
const topResults = results.results.slice(0, 3);
storage.addToSection("searchResults", { [source]: topResults });
if (topResults[0] && topResults[0].url) {
try {
const url = topResults[0].url;
storage.addLogEntry(`Fetching content from ${url}`);
const content = await fetchContent(url, "markdown");
const resourceId = `${source.toLowerCase()}_${tokenName.toLowerCase()}_${new Date().getTime()}`;
storage.addToSection("resources", {
[resourceId]: {
url,
format: "markdown",
content,
title: topResults[0].title,
source,
fetchedAt: new Date().toISOString(),
},
});
return {
content: [
{
type: "text",
text: `Researched ${source} for ${tokenName} (${tokenTicker}).\n\nTop result: ${topResults[0].title}\n\nContent saved as resource: research://resource/${resourceId}\n\nAll search results:\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error fetching content from ${topResults[0].url}: ${error}`);
return {
content: [
{
type: "text",
text: `Found search results for ${source}, but couldn't fetch content: ${error}\n\nSearch results:\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
}
return {
content: [
{
type: "text",
text: `Search results for ${source}:\n\n${JSON.stringify(topResults, null, 2)}`,
},
],
};
}
catch (error) {
storage.addLogEntry(`Error researching ${source}: ${error}`);
return {
isError: true,
content: [
{
type: "text",
text: `Error researching ${source}: ${error}`,
},
],
};
}
});
}
//# sourceMappingURL=researchTools.js.map