data-aggregator-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.
Potentially problematic release.
This version of data-aggregator-mcp might be problematic. Click here for more details.
- package/README.md +336 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/exchange.d.ts +15 -0
- package/dist/tools/exchange.d.ts.map +1 -0
- package/dist/tools/exchange.js +195 -0
- package/dist/tools/exchange.js.map +1 -0
- package/dist/tools/news.d.ts +20 -0
- package/dist/tools/news.d.ts.map +1 -0
- package/dist/tools/news.js +175 -0
- package/dist/tools/news.js.map +1 -0
- package/dist/tools/public-data.d.ts +24 -0
- package/dist/tools/public-data.d.ts.map +1 -0
- package/dist/tools/public-data.js +262 -0
- package/dist/tools/public-data.js.map +1 -0
- package/dist/tools/scraper.d.ts +19 -0
- package/dist/tools/scraper.d.ts.map +1 -0
- package/dist/tools/scraper.js +185 -0
- package/dist/tools/scraper.js.map +1 -0
- package/dist/tools/stocks.d.ts +14 -0
- package/dist/tools/stocks.d.ts.map +1 -0
- package/dist/tools/stocks.js +172 -0
- package/dist/tools/stocks.js.map +1 -0
- package/dist/tools/weather.d.ts +15 -0
- package/dist/tools/weather.d.ts.map +1 -0
- package/dist/tools/weather.js +172 -0
- package/dist/tools/weather.js.map +1 -0
- package/dist/types.d.ts +160 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cache.d.ts +45 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +88 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/http.d.ts +39 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +103 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +34 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +95 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/package.json +42 -0
- package/src/index.ts +461 -0
- package/src/tools/exchange.ts +241 -0
- package/src/tools/news.ts +238 -0
- package/src/tools/public-data.ts +325 -0
- package/src/tools/scraper.ts +217 -0
- package/src/tools/stocks.ts +205 -0
- package/src/tools/weather.ts +216 -0
- package/src/types.ts +184 -0
- package/src/utils/cache.ts +103 -0
- package/src/utils/http.ts +156 -0
- package/src/utils/rate-limiter.ts +114 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_news - News aggregation from multiple sources.
|
|
3
|
+
*
|
|
4
|
+
* Data sources:
|
|
5
|
+
* - NewsAPI.org (optional key via NEWS_API_KEY env)
|
|
6
|
+
* - Google News RSS (free, no key required)
|
|
7
|
+
*
|
|
8
|
+
* Falls back to Google News RSS when no NewsAPI key is configured.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { cache, TTL } from "../utils/cache.js";
|
|
12
|
+
import { rateLimiter } from "../utils/rate-limiter.js";
|
|
13
|
+
import { httpGetJson, httpGetText, formatError } from "../utils/http.js";
|
|
14
|
+
import type { NewsArticle, ToolResult } from "../types.js";
|
|
15
|
+
|
|
16
|
+
// ─── Google News RSS parsing ───────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function parseRssItems(xml: string): NewsArticle[] {
|
|
19
|
+
const articles: NewsArticle[] = [];
|
|
20
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/gi;
|
|
21
|
+
let itemMatch;
|
|
22
|
+
|
|
23
|
+
while ((itemMatch = itemRegex.exec(xml)) !== null) {
|
|
24
|
+
const item = itemMatch[1]!;
|
|
25
|
+
|
|
26
|
+
const title = extractXmlTag(item, "title");
|
|
27
|
+
const link = extractXmlTag(item, "link");
|
|
28
|
+
const pubDate = extractXmlTag(item, "pubDate");
|
|
29
|
+
const description = stripCdata(extractXmlTag(item, "description"));
|
|
30
|
+
const source = extractXmlTag(item, "source");
|
|
31
|
+
|
|
32
|
+
if (title && link) {
|
|
33
|
+
articles.push({
|
|
34
|
+
title: decodeHtmlEntities(title),
|
|
35
|
+
description: decodeHtmlEntities(stripHtmlTags(description)).slice(0, 500),
|
|
36
|
+
url: link,
|
|
37
|
+
source: source || "Google News",
|
|
38
|
+
publishedAt: pubDate ? new Date(pubDate).toISOString() : new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return articles;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractXmlTag(xml: string, tag: string): string {
|
|
47
|
+
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
|
|
48
|
+
const m = xml.match(re);
|
|
49
|
+
return m ? m[1]!.trim() : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stripCdata(text: string): string {
|
|
53
|
+
return text.replace(/<!\[CDATA\[/g, "").replace(/\]\]>/g, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stripHtmlTags(text: string): string {
|
|
57
|
+
return text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function decodeHtmlEntities(text: string): string {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, '"')
|
|
66
|
+
.replace(/'/g, "'")
|
|
67
|
+
.replace(/'/g, "'");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Google News RSS fetch ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
async function fetchGoogleNews(args: {
|
|
73
|
+
query?: string;
|
|
74
|
+
language?: string;
|
|
75
|
+
maxResults?: number;
|
|
76
|
+
}): Promise<NewsArticle[]> {
|
|
77
|
+
const { query, language = "en", maxResults = 10 } = args;
|
|
78
|
+
|
|
79
|
+
let url: string;
|
|
80
|
+
if (query) {
|
|
81
|
+
url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=${language}&gl=US&ceid=US:${language}`;
|
|
82
|
+
} else {
|
|
83
|
+
url = `https://news.google.com/rss?hl=${language}&gl=US&ceid=US:${language}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await rateLimiter.acquire("googlenews");
|
|
87
|
+
const xml = await httpGetText(url, { rotateUserAgent: true });
|
|
88
|
+
const articles = parseRssItems(xml);
|
|
89
|
+
|
|
90
|
+
return articles.slice(0, maxResults);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── NewsAPI.org fetch ─────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function fetchNewsApi(args: {
|
|
96
|
+
query?: string;
|
|
97
|
+
category?: string;
|
|
98
|
+
source?: string;
|
|
99
|
+
language?: string;
|
|
100
|
+
from?: string;
|
|
101
|
+
to?: string;
|
|
102
|
+
maxResults?: number;
|
|
103
|
+
apiKey: string;
|
|
104
|
+
}): Promise<NewsArticle[]> {
|
|
105
|
+
const {
|
|
106
|
+
query,
|
|
107
|
+
category,
|
|
108
|
+
source,
|
|
109
|
+
language = "en",
|
|
110
|
+
from,
|
|
111
|
+
to,
|
|
112
|
+
maxResults = 10,
|
|
113
|
+
apiKey,
|
|
114
|
+
} = args;
|
|
115
|
+
|
|
116
|
+
await rateLimiter.acquire("newsapi");
|
|
117
|
+
|
|
118
|
+
// Use /everything for queries, /top-headlines for category/source browsing
|
|
119
|
+
let url: string;
|
|
120
|
+
const params = new URLSearchParams();
|
|
121
|
+
params.set("apiKey", apiKey);
|
|
122
|
+
params.set("pageSize", String(Math.min(maxResults, 100)));
|
|
123
|
+
params.set("language", language);
|
|
124
|
+
|
|
125
|
+
if (query) {
|
|
126
|
+
url = "https://newsapi.org/v2/everything";
|
|
127
|
+
params.set("q", query);
|
|
128
|
+
params.set("sortBy", "publishedAt");
|
|
129
|
+
if (from) params.set("from", from);
|
|
130
|
+
if (to) params.set("to", to);
|
|
131
|
+
if (source) params.set("sources", source);
|
|
132
|
+
} else {
|
|
133
|
+
url = "https://newsapi.org/v2/top-headlines";
|
|
134
|
+
if (category) params.set("category", category);
|
|
135
|
+
if (source) params.set("sources", source);
|
|
136
|
+
// top-headlines needs country if no source
|
|
137
|
+
if (!source) params.set("country", "us");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fullUrl = `${url}?${params.toString()}`;
|
|
141
|
+
const data: any = await httpGetJson(fullUrl);
|
|
142
|
+
|
|
143
|
+
if (data.status !== "ok") {
|
|
144
|
+
throw new Error(data.message || "NewsAPI returned an error");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (data.articles ?? []).map((a: any) => ({
|
|
148
|
+
title: a.title ?? "",
|
|
149
|
+
description: a.description ?? "",
|
|
150
|
+
url: a.url ?? "",
|
|
151
|
+
source: a.source?.name ?? "Unknown",
|
|
152
|
+
publishedAt: a.publishedAt ?? new Date().toISOString(),
|
|
153
|
+
author: a.author ?? undefined,
|
|
154
|
+
imageUrl: a.urlToImage ?? undefined,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Public handler ────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export async function searchNews(args: {
|
|
161
|
+
query?: string;
|
|
162
|
+
category?: string;
|
|
163
|
+
source?: string;
|
|
164
|
+
language?: string;
|
|
165
|
+
from?: string;
|
|
166
|
+
to?: string;
|
|
167
|
+
maxResults?: number;
|
|
168
|
+
}): Promise<ToolResult> {
|
|
169
|
+
try {
|
|
170
|
+
const {
|
|
171
|
+
query,
|
|
172
|
+
category,
|
|
173
|
+
source,
|
|
174
|
+
language = "en",
|
|
175
|
+
from,
|
|
176
|
+
to,
|
|
177
|
+
maxResults = 10,
|
|
178
|
+
} = args;
|
|
179
|
+
|
|
180
|
+
const cacheKey = `news:${query ?? ""}:${category ?? ""}:${source ?? ""}:${language}:${from ?? ""}:${to ?? ""}`;
|
|
181
|
+
const cached = cache.get<NewsArticle[]>(cacheKey);
|
|
182
|
+
if (cached) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: JSON.stringify({ articles: cached, count: cached.length, cached: true }, null, 2),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let articles: NewsArticle[];
|
|
194
|
+
const newsApiKey = process.env.NEWS_API_KEY;
|
|
195
|
+
|
|
196
|
+
if (newsApiKey) {
|
|
197
|
+
articles = await fetchNewsApi({
|
|
198
|
+
query,
|
|
199
|
+
category,
|
|
200
|
+
source,
|
|
201
|
+
language,
|
|
202
|
+
from,
|
|
203
|
+
to,
|
|
204
|
+
maxResults,
|
|
205
|
+
apiKey: newsApiKey,
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
// Fall back to Google News RSS (category/source filtering not supported)
|
|
209
|
+
articles = await fetchGoogleNews({ query, language, maxResults });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
cache.set(cacheKey, articles, TTL.NEWS);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: JSON.stringify(
|
|
219
|
+
{
|
|
220
|
+
articles,
|
|
221
|
+
count: articles.length,
|
|
222
|
+
source: newsApiKey ? "NewsAPI.org" : "Google News RSS",
|
|
223
|
+
},
|
|
224
|
+
null,
|
|
225
|
+
2
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{ type: "text", text: `Error fetching news: ${formatError(err)}` },
|
|
234
|
+
],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query_public_data - Miscellaneous public API queries.
|
|
3
|
+
*
|
|
4
|
+
* Sub-commands:
|
|
5
|
+
* - wikipedia : Get a Wikipedia article summary
|
|
6
|
+
* - ip_geolocation : Look up geographic info for an IP address
|
|
7
|
+
* - dns_lookup : DNS record lookup for a domain
|
|
8
|
+
* - expand_url : Expand a shortened URL to its final destination
|
|
9
|
+
*
|
|
10
|
+
* All APIs used are free and require no API keys.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { cache, TTL } from "../utils/cache.js";
|
|
14
|
+
import { rateLimiter } from "../utils/rate-limiter.js";
|
|
15
|
+
import { httpGetJson, httpFetch, formatError } from "../utils/http.js";
|
|
16
|
+
import type {
|
|
17
|
+
WikipediaSummary,
|
|
18
|
+
IpGeolocation,
|
|
19
|
+
DnsRecord,
|
|
20
|
+
UrlInfo,
|
|
21
|
+
ToolResult,
|
|
22
|
+
} from "../types.js";
|
|
23
|
+
import { resolve } from "node:dns/promises";
|
|
24
|
+
|
|
25
|
+
// ─── Wikipedia Summary ─────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
async function fetchWikipediaSummary(
|
|
28
|
+
query: string,
|
|
29
|
+
language = "en"
|
|
30
|
+
): Promise<WikipediaSummary> {
|
|
31
|
+
// Use the Wikipedia REST API summary endpoint
|
|
32
|
+
const encoded = encodeURIComponent(query.replace(/\s+/g, "_"));
|
|
33
|
+
const url = `https://${language}.wikipedia.org/api/rest_v1/page/summary/${encoded}`;
|
|
34
|
+
|
|
35
|
+
await rateLimiter.acquire("wikipedia");
|
|
36
|
+
const data: any = await httpGetJson(url);
|
|
37
|
+
|
|
38
|
+
if (data.type === "disambiguation") {
|
|
39
|
+
return {
|
|
40
|
+
title: data.title ?? query,
|
|
41
|
+
extract: `This is a disambiguation page. ${data.extract ?? "Multiple meanings exist for this term."}`,
|
|
42
|
+
url: data.content_urls?.desktop?.page ?? `https://${language}.wikipedia.org/wiki/${encoded}`,
|
|
43
|
+
thumbnail: data.thumbnail?.source,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!data.extract) {
|
|
49
|
+
throw new Error(`No Wikipedia article found for "${query}". Try a different search term.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
title: data.title ?? query,
|
|
54
|
+
extract: data.extract,
|
|
55
|
+
url: data.content_urls?.desktop?.page ?? `https://${language}.wikipedia.org/wiki/${encoded}`,
|
|
56
|
+
thumbnail: data.thumbnail?.source,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── IP Geolocation ────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async function fetchIpGeolocation(ip?: string): Promise<IpGeolocation> {
|
|
64
|
+
// ip-api.com is free for non-commercial use, no key required
|
|
65
|
+
const target = ip ?? ""; // empty string = caller's IP
|
|
66
|
+
const url = `http://ip-api.com/json/${target}?fields=status,message,country,countryCode,regionName,city,lat,lon,timezone,isp,query`;
|
|
67
|
+
|
|
68
|
+
await rateLimiter.acquire("ipgeo");
|
|
69
|
+
const data: any = await httpGetJson(url);
|
|
70
|
+
|
|
71
|
+
if (data.status !== "success") {
|
|
72
|
+
throw new Error(data.message ?? `Geolocation failed for IP "${ip ?? "self"}"`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ip: data.query,
|
|
77
|
+
country: data.country,
|
|
78
|
+
countryCode: data.countryCode,
|
|
79
|
+
region: data.regionName,
|
|
80
|
+
city: data.city,
|
|
81
|
+
latitude: data.lat,
|
|
82
|
+
longitude: data.lon,
|
|
83
|
+
timezone: data.timezone,
|
|
84
|
+
isp: data.isp,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── DNS Lookup ────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
async function fetchDnsRecords(
|
|
91
|
+
domain: string,
|
|
92
|
+
recordType: string
|
|
93
|
+
): Promise<DnsRecord> {
|
|
94
|
+
await rateLimiter.acquire("dns");
|
|
95
|
+
|
|
96
|
+
const type = recordType.toUpperCase();
|
|
97
|
+
let records: string[] = [];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
switch (type) {
|
|
101
|
+
case "A": {
|
|
102
|
+
const result = await resolve(domain, "A");
|
|
103
|
+
records = result;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "AAAA": {
|
|
107
|
+
const result = await resolve(domain, "AAAA");
|
|
108
|
+
records = result;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "MX": {
|
|
112
|
+
const result = await resolve(domain, "MX");
|
|
113
|
+
records = (result as any[]).map(
|
|
114
|
+
(r: any) => `${r.priority} ${r.exchange}`
|
|
115
|
+
);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "TXT": {
|
|
119
|
+
const result = await resolve(domain, "TXT");
|
|
120
|
+
records = (result as string[][]).map((r) => r.join(""));
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "NS": {
|
|
124
|
+
const result = await resolve(domain, "NS");
|
|
125
|
+
records = result;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "CNAME": {
|
|
129
|
+
const result = await resolve(domain, "CNAME");
|
|
130
|
+
records = result;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "SOA": {
|
|
134
|
+
const result = await resolve(domain, "SOA");
|
|
135
|
+
const soa = result as any;
|
|
136
|
+
records = [
|
|
137
|
+
`nsname=${soa.nsname} hostmaster=${soa.hostmaster} serial=${soa.serial}`,
|
|
138
|
+
];
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Unsupported record type: ${type}. Supported: A, AAAA, MX, TXT, NS, CNAME, SOA`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
} catch (err: any) {
|
|
147
|
+
if (err.code === "ENODATA" || err.code === "ENOTFOUND") {
|
|
148
|
+
records = [];
|
|
149
|
+
} else {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
domain,
|
|
156
|
+
type,
|
|
157
|
+
records,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── URL Expander ──────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async function expandUrl(shortUrl: string): Promise<UrlInfo> {
|
|
164
|
+
await rateLimiter.acquire("generic");
|
|
165
|
+
|
|
166
|
+
// Follow redirects by making a HEAD request
|
|
167
|
+
const response = await httpFetch(shortUrl, {
|
|
168
|
+
method: "GET",
|
|
169
|
+
maxRetries: 1,
|
|
170
|
+
timeoutMs: 10_000,
|
|
171
|
+
rotateUserAgent: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
originalUrl: shortUrl,
|
|
176
|
+
expandedUrl: response.url,
|
|
177
|
+
statusCode: response.status,
|
|
178
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Public handler ────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
type SubCommand = "wikipedia" | "ip_geolocation" | "dns_lookup" | "expand_url";
|
|
185
|
+
|
|
186
|
+
export async function queryPublicData(args: {
|
|
187
|
+
command: SubCommand;
|
|
188
|
+
query?: string;
|
|
189
|
+
ip?: string;
|
|
190
|
+
domain?: string;
|
|
191
|
+
recordType?: string;
|
|
192
|
+
url?: string;
|
|
193
|
+
language?: string;
|
|
194
|
+
}): Promise<ToolResult> {
|
|
195
|
+
try {
|
|
196
|
+
const { command } = args;
|
|
197
|
+
|
|
198
|
+
switch (command) {
|
|
199
|
+
case "wikipedia": {
|
|
200
|
+
const query = args.query;
|
|
201
|
+
if (!query) {
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{ type: "text", text: 'The "query" parameter is required for Wikipedia lookups.' },
|
|
205
|
+
],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const cacheKey = `wiki:${query.toLowerCase()}:${args.language ?? "en"}`;
|
|
211
|
+
const cached = cache.get<WikipediaSummary>(cacheKey);
|
|
212
|
+
if (cached) {
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{ type: "text", text: JSON.stringify({ ...cached, cached: true }, null, 2) },
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const summary = await fetchWikipediaSummary(query, args.language);
|
|
221
|
+
cache.set(cacheKey, summary, TTL.PUBLIC);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "ip_geolocation": {
|
|
229
|
+
const cacheKey = `ipgeo:${args.ip ?? "self"}`;
|
|
230
|
+
const cached = cache.get<IpGeolocation>(cacheKey);
|
|
231
|
+
if (cached) {
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: "text", text: JSON.stringify({ ...cached, cached: true }, null, 2) },
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const geo = await fetchIpGeolocation(args.ip);
|
|
240
|
+
cache.set(cacheKey, geo, TTL.PUBLIC);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: "text", text: JSON.stringify(geo, null, 2) }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "dns_lookup": {
|
|
248
|
+
const domain = args.domain;
|
|
249
|
+
if (!domain) {
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{ type: "text", text: 'The "domain" parameter is required for DNS lookups.' },
|
|
253
|
+
],
|
|
254
|
+
isError: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const recordType = args.recordType ?? "A";
|
|
259
|
+
const cacheKey = `dns:${domain.toLowerCase()}:${recordType}`;
|
|
260
|
+
const cached = cache.get<DnsRecord>(cacheKey);
|
|
261
|
+
if (cached) {
|
|
262
|
+
return {
|
|
263
|
+
content: [
|
|
264
|
+
{ type: "text", text: JSON.stringify({ ...cached, cached: true }, null, 2) },
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const records = await fetchDnsRecords(domain, recordType);
|
|
270
|
+
cache.set(cacheKey, records, TTL.PUBLIC);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "expand_url": {
|
|
278
|
+
const url = args.url;
|
|
279
|
+
if (!url) {
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{ type: "text", text: 'The "url" parameter is required for URL expansion.' },
|
|
283
|
+
],
|
|
284
|
+
isError: true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const cacheKey = `url:${url}`;
|
|
289
|
+
const cached = cache.get<UrlInfo>(cacheKey);
|
|
290
|
+
if (cached) {
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{ type: "text", text: JSON.stringify({ ...cached, cached: true }, null, 2) },
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const info = await expandUrl(url);
|
|
299
|
+
cache.set(cacheKey, info, TTL.PUBLIC);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: "text", text: JSON.stringify(info, null, 2) }],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
return {
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: `Unknown sub-command: "${command}". Available commands: wikipedia, ip_geolocation, dns_lookup, expand_url`,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
isError: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{ type: "text", text: `Error in public data query: ${formatError(err)}` },
|
|
321
|
+
],
|
|
322
|
+
isError: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|