@tensorfeed/mcp-server 1.38.0 → 2.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.
- package/CHANGELOG.md +109 -11
- package/README.md +21 -5
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +38 -0
- package/dist/index.js +707 -1556
- package/dist/meta-snapshot.json +1 -0
- package/package.json +7 -5
- package/server.json +29 -58
package/dist/index.js
CHANGED
|
@@ -14,6 +14,15 @@ const API_BASE = 'https://tensorfeed.ai/api';
|
|
|
14
14
|
const REQUEST_TIMEOUT_MS = 20000;
|
|
15
15
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
16
16
|
const SDK_VERSION = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
17
|
+
// Upsell footer appended to the free routing-precursor tools (get_ai_status,
|
|
18
|
+
// get_model_pricing). Each is a signal an agent gathers before choosing a
|
|
19
|
+
// model, so each points at the one premium tool that fuses them into a signed
|
|
20
|
+
// decision. Mirrors the hosted /api/mcp `next` field. Honest, zero em dashes.
|
|
21
|
+
const ROUTE_VERDICT_FOOTER = '\n\nNext: route_verdict with tier="full" (1 credit, $0.02) fuses live pricing, contamination-discounted ' +
|
|
22
|
+
'benchmarks, real production usage, measured p95 latency, incident state, and deprecation ' +
|
|
23
|
+
'flags into a signed best-fit model decision with ranked runners-up and an AFTA-signed ' +
|
|
24
|
+
'receipt. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits ' +
|
|
25
|
+
'for 25 free credits.';
|
|
17
26
|
async function fetchJSON(path, opts = {}) {
|
|
18
27
|
const headers = {
|
|
19
28
|
'User-Agent': `TensorFeed-MCP/${SDK_VERSION}`,
|
|
@@ -181,7 +190,23 @@ server.registerResource('latest_news', 'tensorfeed://news/latest', {
|
|
|
181
190
|
registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a ranked list with title, source, URL, snippet, and publish time, filterable by category (e.g. "anthropic", "openai", "research", "tools"). Aggregates 15+ sources (Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more) into one normalized feed, so an agent reads one schema instead of polling each outlet. Free, no auth.', {
|
|
182
191
|
category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
|
|
183
192
|
limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
|
|
184
|
-
|
|
193
|
+
digest: z.boolean().optional().describe('When true, return a headline-only digest (title, source, URL per story) instead of the full article set.'),
|
|
194
|
+
}, async ({ category, limit, digest }) => {
|
|
195
|
+
// digest mode is a headline-only companion view of the same feed. It honors
|
|
196
|
+
// the same inputs as the full view: the category filter and the schema limit
|
|
197
|
+
// (default 5, max 50). Earlier this branch dropped category and capped at 20,
|
|
198
|
+
// silently ignoring declared params.
|
|
199
|
+
if (digest) {
|
|
200
|
+
const dParams = new URLSearchParams();
|
|
201
|
+
if (category)
|
|
202
|
+
dParams.set('category', category);
|
|
203
|
+
dParams.set('limit', String(limit || 5));
|
|
204
|
+
const data = await fetchJSON(`/news?${dParams}`);
|
|
205
|
+
const text = data.articles
|
|
206
|
+
.map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
|
|
207
|
+
.join('\n\n');
|
|
208
|
+
return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
|
|
209
|
+
}
|
|
185
210
|
const params = new URLSearchParams();
|
|
186
211
|
if (category)
|
|
187
212
|
params.set('category', category);
|
|
@@ -192,6 +217,29 @@ registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a rank
|
|
|
192
217
|
.join('\n\n');
|
|
193
218
|
return { content: [{ type: 'text', text: text || 'No articles found.' }] };
|
|
194
219
|
});
|
|
220
|
+
// ── Tool: find_tensorfeed_data (discovery, long-tail surface) ───────
|
|
221
|
+
registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers a data need. Describe what you want in plain language (e.g. "trending AI papers", "is OpenAI down", "model price history") and this returns the 2 to 3 best-matching TensorFeed endpoints, each with its HTTP path, what it returns, and whether it is free or paid. TensorFeed exposes 100+ AI-ecosystem data and signed-verdict endpoints; the core ones are also dedicated tools, but the full catalog is reachable here and callable over HTTP (paid ones via x402 or a credits token). Free, no auth. Use this first when no dedicated tool obviously fits.', {
|
|
222
|
+
query: z.string().describe('Plain-language description of the data or decision you need.'),
|
|
223
|
+
limit: z.number().int().min(1).max(5).optional().describe('Max endpoints to return (default 3).'),
|
|
224
|
+
}, async ({ query, limit }) => {
|
|
225
|
+
const { flattenCatalog, scoreEndpoints } = await import('./discovery.js');
|
|
226
|
+
let meta;
|
|
227
|
+
try {
|
|
228
|
+
meta = await fetchJSON('/meta');
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
const { readFileSync } = await import('node:fs');
|
|
232
|
+
const p = join(dirname(fileURLToPath(import.meta.url)), 'meta-snapshot.json');
|
|
233
|
+
meta = JSON.parse(readFileSync(p, 'utf-8'));
|
|
234
|
+
}
|
|
235
|
+
const rows = flattenCatalog(meta);
|
|
236
|
+
const top = scoreEndpoints(rows, query, limit ?? 3);
|
|
237
|
+
if (top.length === 0) {
|
|
238
|
+
return { content: [{ type: 'text', text: `No TensorFeed endpoint matched "${query}". Browse the full catalog at https://tensorfeed.ai/api/meta or https://tensorfeed.ai/developers.` }] };
|
|
239
|
+
}
|
|
240
|
+
const lines = top.map((r) => `- ${r.path}\n ${r.description}`).join('\n');
|
|
241
|
+
return { content: [{ type: 'text', text: `Top TensorFeed endpoints for "${query}":\n${lines}\n\nCall free endpoints directly over HTTP. Paid endpoints (marked with a credit cost) accept x402 payment or a TENSORFEED_TOKEN bearer; see https://tensorfeed.ai/developers/agent-payments.` }] };
|
|
242
|
+
});
|
|
195
243
|
// ── Tool: get_ai_status ─────────────────────────────────────────────
|
|
196
244
|
registerTool('get_ai_status', 'Get the real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face), including per-component breakdowns and an operational/degraded/down rollup per provider. One cross-provider status call instead of checking each vendor\'s status page, useful before an agent routes a request to a model that may be impaired. Free, no auth.', {}, async () => {
|
|
197
245
|
const data = await fetchJSON('/status');
|
|
@@ -203,7 +251,7 @@ registerTool('get_ai_status', 'Get the real-time operational status of major AI
|
|
|
203
251
|
return ` ${s.status === 'operational' ? 'OK' : s.status.toUpperCase()} ${s.name} (${s.provider})${components}`;
|
|
204
252
|
})
|
|
205
253
|
.join('\n');
|
|
206
|
-
return { content: [{ type: 'text', text: `AI Service Status:\n${text}` }] };
|
|
254
|
+
return { content: [{ type: 'text', text: `AI Service Status:\n${text}${ROUTE_VERDICT_FOOTER}` }] };
|
|
207
255
|
});
|
|
208
256
|
// ── Tool: is_service_down ───────────────────────────────────────────
|
|
209
257
|
registerTool('is_service_down', 'Check whether one named AI service (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate") is currently operational, degraded, or down, with its component-level breakdown. Matches on service or provider name and lists available services if there is no match, so an agent can gate a call on live status before sending traffic. Free, no auth.', {
|
|
@@ -222,65 +270,10 @@ registerTool('is_service_down', 'Check whether one named AI service (e.g. "claud
|
|
|
222
270
|
return {
|
|
223
271
|
content: [{
|
|
224
272
|
type: 'text',
|
|
225
|
-
text: `${statusEmoji} ${match.name} (${match.provider}) is ${match.status}${components}
|
|
273
|
+
text: `${statusEmoji} ${match.name} (${match.provider}) is ${match.status}${components}\n\nIf it is degraded, failover_verdict ranks the best operational alternative to fail over to right now.`
|
|
226
274
|
}]
|
|
227
275
|
};
|
|
228
276
|
});
|
|
229
|
-
// Tool: check_agent_ready
|
|
230
|
-
registerTool('check_agent_ready', 'Score how agent-ready a website is, 0 to 100, from its public surfaces: x402 payment manifest, /.well-known/agent.json, an OpenAPI spec, llms.txt, AI-bot crawlability, and ai.txt. Returns the score, the tier (closed, emerging, ready, or advanced), and exactly which surfaces the site exposes. Unlike single-site "is my site agent-ready" scanners, this reads from TensorFeed\'s daily-crawled cross-domain dataset, so the same call works for any tracked domain and the scoring rule is published and identical for every site. Free, no auth.', {
|
|
231
|
-
domain: z.string().describe('Bare domain to score, e.g. "stripe.com" (no scheme, no path)'),
|
|
232
|
-
}, async ({ domain }) => {
|
|
233
|
-
const data = await fetchJSON(`/agent-ready/site?domain=${encodeURIComponent(domain)}`);
|
|
234
|
-
if (!data.found || !data.readiness) {
|
|
235
|
-
return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet (no agent-readiness score). About 500 curated domains are tracked.` }] };
|
|
236
|
-
}
|
|
237
|
-
const r = data.readiness;
|
|
238
|
-
const present = Object.entries(r.surfaces).filter(([, v]) => v).map(([k]) => k);
|
|
239
|
-
const text = `${data.domain}: agent-readiness ${r.score}/100 (${r.tier})\n` +
|
|
240
|
-
`Surfaces present: ${present.length ? present.join(', ') : 'none'}\n` +
|
|
241
|
-
`x402=${r.surfaces.x402} agent.json=${r.surfaces.agentJson} openapi=${r.surfaces.openapi} llms.txt=${r.surfaces.llmsTxt} crawlable=${r.surfaces.crawlable} ai.txt=${r.surfaces.aiTxt}\n` +
|
|
242
|
-
`Last checked: ${data.captured_at ?? 'n/a'}`;
|
|
243
|
-
return { content: [{ type: 'text', text }] };
|
|
244
|
-
});
|
|
245
|
-
// Tool: agent_ready_summary
|
|
246
|
-
registerTool('agent_ready_summary', 'Get the state of the agentic web. Across about 500 curated domains, returns the share exposing each agent surface (x402, agent.json, OpenAPI, llms.txt, crawlable, ai.txt), the readiness-tier distribution, and a top-10 leaderboard of the most agent-ready sites. This aggregate, cross-domain view is exactly what a single-site scanner cannot give you. Free, no auth.', {}, async () => {
|
|
247
|
-
const d = await fetchJSON('/agent-ready/summary.json');
|
|
248
|
-
const a = d.adoption_pct;
|
|
249
|
-
const lb = (d.leaderboard || []).slice(0, 10).map((e, i) => `${i + 1}. ${e.domain} (${e.score}, ${e.tier})`).join('\n');
|
|
250
|
-
const text = `Agentic web readiness, ${d.profiled}/${d.domains_tracked} domains profiled (captured ${d.captured_at ?? 'n/a'}):\n` +
|
|
251
|
-
`Surface adoption: x402 ${a.x402}%, agent.json ${a.agentJson}%, openapi ${a.openapi}%, llms.txt ${a.llmsTxt}%, crawlable ${a.crawlable}%, ai.txt ${a.aiTxt}%\n` +
|
|
252
|
-
`Tiers: ${Object.entries(d.tier_distribution || {}).map(([t, n]) => `${t} ${n}`).join(', ')}\n` +
|
|
253
|
-
`Most agent-ready:\n${lb}`;
|
|
254
|
-
return { content: [{ type: 'text', text }] };
|
|
255
|
-
});
|
|
256
|
-
// Tool: check_crawler_access
|
|
257
|
-
registerTool('check_crawler_access', 'Check which AI bots a website allows or blocks in its robots.txt: GPTBot, ClaudeBot, PerplexityBot, CCBot, Google-Extended, Bytespider, and more, with a per-bot verdict (allowed, blocked, partial, or unknown), plus whether the site publishes llms.txt and ai.txt. Reports the site\'s stated policy, not enforcement (robots.txt compliance is voluntary). Free, no auth.', {
|
|
258
|
-
domain: z.string().describe('Bare domain to check, e.g. "nytimes.com" (no scheme, no path)'),
|
|
259
|
-
}, async ({ domain }) => {
|
|
260
|
-
const data = await fetchJSON(`/ai-crawler-access/site?domain=${encodeURIComponent(domain)}`);
|
|
261
|
-
if (!data.found || !data.record) {
|
|
262
|
-
return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet. About 500 curated domains are tracked for AI-crawler access.` }] };
|
|
263
|
-
}
|
|
264
|
-
const rec = data.record;
|
|
265
|
-
const bots = Object.entries(rec.bots).map(([b, v]) => ` ${b}: ${v}`).join('\n');
|
|
266
|
-
const text = `${data.domain} (robots.txt HTTP ${rec.robotsStatus ?? 'n/a'}) AI-bot access:\n${bots}\n` +
|
|
267
|
-
`llms.txt: ${rec.hasLlmsTxt ? 'yes' : 'no'}, ai.txt: ${rec.hasAiTxt ? 'yes' : 'no'}\n` +
|
|
268
|
-
`Stated policy only; compliance is voluntary. Last checked: ${data.captured_at ?? 'n/a'}`;
|
|
269
|
-
return { content: [{ type: 'text', text }] };
|
|
270
|
-
});
|
|
271
|
-
// Tool: crawler_access_summary
|
|
272
|
-
registerTool('crawler_access_summary', 'Get the aggregate AI-crawler access map across about 500 curated domains: the percentage of sites that block each AI bot (the publisher-versus-AI-crawler picture), plus llms.txt and ai.txt adoption rates. Unlike a single-site robots.txt checker, this is the cross-domain trend view of who is shutting AI crawlers out. Free, no auth.', {}, async () => {
|
|
273
|
-
const d = await fetchJSON('/ai-crawler-access/summary.json');
|
|
274
|
-
const blocked = Object.entries(d.bot_blocked_pct || {})
|
|
275
|
-
.sort((x, y) => y[1] - x[1])
|
|
276
|
-
.slice(0, 8)
|
|
277
|
-
.map(([b, p]) => ` ${b}: ${p}% blocked`)
|
|
278
|
-
.join('\n');
|
|
279
|
-
const text = `AI-crawler access across ${d.domains_with_data}/${d.domains_tracked} tracked domains (captured ${d.captured_at ?? 'n/a'}):\n` +
|
|
280
|
-
`Most-blocked AI bots:\n${blocked}\n` +
|
|
281
|
-
`llms.txt adoption: ${d.llms_txt_adoption_pct}%, ai.txt adoption: ${d.ai_txt_adoption_pct}%`;
|
|
282
|
-
return { content: [{ type: 'text', text }] };
|
|
283
|
-
});
|
|
284
277
|
// ── Tool: get_model_pricing ─────────────────────────────────────────
|
|
285
278
|
registerTool('get_model_pricing', 'Get AI model pricing across major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere) in one normalized table: input and output price per 1M tokens, context window, and release date per model. One cross-provider comparison instead of scraping six pricing pages, so an agent can pick the cheapest model that fits its context and budget. Free, no auth.', {}, async () => {
|
|
286
279
|
const data = await fetchJSON('/models');
|
|
@@ -299,472 +292,572 @@ registerTool('get_model_pricing', 'Get AI model pricing across major providers (
|
|
|
299
292
|
return ` ${p.name}:\n${models}`;
|
|
300
293
|
})
|
|
301
294
|
.join('\n\n');
|
|
302
|
-
return { content: [{ type: 'text', text: `AI Model Pricing (per 1M tokens):\n\n${text}` }] };
|
|
295
|
+
return { content: [{ type: 'text', text: `AI Model Pricing (per 1M tokens):\n\n${text}${ROUTE_VERDICT_FOOTER}` }] };
|
|
303
296
|
});
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
297
|
+
// ════════════════════════════════════════════════════════════════════
|
|
298
|
+
// PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
|
|
299
|
+
// ════════════════════════════════════════════════════════════════════
|
|
300
|
+
// ── Tool: account_status ────────────────────────────────────────────
|
|
301
|
+
// Merges the former get_account_balance and get_account_usage tools into
|
|
302
|
+
// one read. Both fetch calls and both output bodies are reused verbatim
|
|
303
|
+
// and concatenated into a single text response.
|
|
304
|
+
registerTool('account_status', 'Check the configured TensorFeed token: current credit balance plus recent per-endpoint usage (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
|
|
305
|
+
// Balance and usage are independent reads; fetch them in parallel so the
|
|
306
|
+
// merged tool costs one round-trip of wall-clock time, not two.
|
|
307
|
+
const [balance, usage] = (await Promise.all([
|
|
308
|
+
fetchJSON('/payment/balance', { auth: true }),
|
|
309
|
+
fetchJSON('/payment/usage', { auth: true }),
|
|
310
|
+
]));
|
|
311
|
+
const balanceText = `Balance: ${balance.balance} credits\nTotal purchased: ${balance.total_purchased}\nCreated: ${balance.created}\nLast used: ${balance.last_used}`;
|
|
312
|
+
let usageText;
|
|
313
|
+
if (usage.total_calls === 0) {
|
|
314
|
+
usageText = 'No premium API calls on this token yet.';
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const rows = Object.entries(usage.by_endpoint)
|
|
318
|
+
.sort(([, a], [, b]) => b.calls - a.calls)
|
|
319
|
+
.map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
|
|
320
|
+
.join('\n');
|
|
321
|
+
usageText = `Total: ${usage.total_calls} calls, ${usage.total_credits_spent} credits\n\n${rows}`;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: `${balanceText}\n\nUsage:\n${usageText}`,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
registerTool('route_verdict', "TensorFeed's signed model-routing decision: the single best model for a task or named model, fused from live pricing, contamination-discounted benchmarks, real production usage, measured p95 latency, incident state, and deprecation flags, with the reasoning. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
333
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds runners-up, filters, signed receipt)."),
|
|
334
|
+
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
|
|
335
|
+
model: z.string().optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
|
|
336
|
+
max_latency_p95_ms: z.number().optional().describe('Full tier only. Drop candidates whose measured p95 latency exceeds this value (ms).'),
|
|
337
|
+
budget: z.number().optional().describe('Full tier only. Max blended USD per 1M tokens.'),
|
|
338
|
+
min_quality: z.number().min(0).max(1).optional().describe('Full tier only. Minimum trust-discounted quality score in [0, 1].'),
|
|
339
|
+
require_operational: z.boolean().optional().describe('Full tier only. Default true. Set false to keep candidates known down or in failover.'),
|
|
340
|
+
exclude_deprecated: z.boolean().optional().describe('Full tier only. Default true. Set false to keep deprecated or sunsetted models.'),
|
|
341
|
+
}, async ({ tier, task, model, max_latency_p95_ms, budget, min_quality, require_operational, exclude_deprecated }) => {
|
|
342
|
+
if (!task && !model) {
|
|
324
343
|
return {
|
|
325
344
|
content: [
|
|
326
345
|
{
|
|
327
346
|
type: 'text',
|
|
328
|
-
text: '
|
|
347
|
+
text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a routing verdict.',
|
|
329
348
|
},
|
|
330
349
|
],
|
|
331
350
|
};
|
|
332
351
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
const params = new URLSearchParams();
|
|
353
|
+
if (task)
|
|
354
|
+
params.set('task', task);
|
|
355
|
+
if (model)
|
|
356
|
+
params.set('model', model);
|
|
357
|
+
if ((tier ?? 'preview') === 'full') {
|
|
358
|
+
if (typeof max_latency_p95_ms === 'number')
|
|
359
|
+
params.set('max_latency_p95_ms', String(max_latency_p95_ms));
|
|
360
|
+
if (typeof budget === 'number')
|
|
361
|
+
params.set('budget', String(budget));
|
|
362
|
+
if (typeof min_quality === 'number')
|
|
363
|
+
params.set('min_quality', String(min_quality));
|
|
364
|
+
if (typeof require_operational === 'boolean')
|
|
365
|
+
params.set('require_operational', String(require_operational));
|
|
366
|
+
if (typeof exclude_deprecated === 'boolean')
|
|
367
|
+
params.set('exclude_deprecated', String(exclude_deprecated));
|
|
368
|
+
const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
|
|
369
|
+
if (!data.verdict) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const v = data.verdict;
|
|
375
|
+
const t = data.trust;
|
|
376
|
+
const f = data.filters_applied;
|
|
377
|
+
const runners = (data.runners_up ?? [])
|
|
378
|
+
.map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
|
|
357
379
|
.join('\n');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
registerTool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Returns today\'s bot hit count, the most recent 50 hits with bot name + endpoint + timestamp, and a derived top-bots breakdown. Useful when an agent wants to see which crawlers are pulling AI ecosystem data and how that distribution shifts. Free, no auth.', {}, async () => {
|
|
376
|
-
const data = (await fetchJSON('/agents/activity'));
|
|
377
|
-
const tally = new Map();
|
|
378
|
-
for (const hit of data.recent) {
|
|
379
|
-
tally.set(hit.bot, (tally.get(hit.bot) || 0) + 1);
|
|
380
|
+
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
381
|
+
const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
|
|
382
|
+
const billing = data.billing
|
|
383
|
+
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
|
|
384
|
+
: '';
|
|
385
|
+
return {
|
|
386
|
+
content: [
|
|
387
|
+
{
|
|
388
|
+
type: 'text',
|
|
389
|
+
text: `Route Verdict: ${v.model.name} (${v.model.provider}) score ${v.composite_score}\n` +
|
|
390
|
+
`Why: ${v.why}\n` +
|
|
391
|
+
`Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n\n` +
|
|
392
|
+
`Runners-up:\n${runners || ' (none)'}\n\n` +
|
|
393
|
+
`${trustLine}\n${filterLine}\n\nClaim: ${data.claim}${billing}`,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
};
|
|
380
397
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
const data = (await fetchJSON(`/preview/route-verdict?${params}`));
|
|
399
|
+
if (!data.verdict) {
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: 'text', text: `No routing verdict available for that query.\n${data.claim ?? ''}` }],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const v = data.verdict;
|
|
405
|
+
const t = data.trust;
|
|
406
|
+
const fresh = [
|
|
407
|
+
data.data_freshness.pricing ? `pricing ${data.data_freshness.pricing}` : null,
|
|
408
|
+
data.data_freshness.probe ? `latency ${data.data_freshness.probe}` : null,
|
|
409
|
+
data.data_freshness.status ? `status ${data.data_freshness.status}` : null,
|
|
410
|
+
]
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.join(', ');
|
|
413
|
+
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
414
|
+
const rl = data.rate_limit ? `\nPreview: ${data.rate_limit.remaining} of ${data.rate_limit.limit} calls left today (${data.rate_limit.scope}).` : '';
|
|
389
415
|
return {
|
|
390
416
|
content: [
|
|
391
417
|
{
|
|
392
418
|
type: 'text',
|
|
393
|
-
text: `
|
|
394
|
-
`
|
|
395
|
-
`
|
|
396
|
-
|
|
397
|
-
`
|
|
398
|
-
`Public dashboard: https://tensorfeed.ai/agent-traffic`,
|
|
419
|
+
text: `Route Verdict: ${v.model.name} (${v.model.provider})\n` +
|
|
420
|
+
`Why: ${v.why}\n` +
|
|
421
|
+
`Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n` +
|
|
422
|
+
`${trustLine}${fresh ? `\nData freshness: ${fresh}` : ''}\n` +
|
|
423
|
+
`Upgrade: route_verdict tier='full' (1 credit, $0.02) adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`,
|
|
399
424
|
},
|
|
400
425
|
],
|
|
401
426
|
};
|
|
402
427
|
});
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
428
|
+
// ════════════════════════════════════════════════════════════════════
|
|
429
|
+
// Verdict family: 7 signed decisions, each a single tier-parameterized
|
|
430
|
+
// tool. tier='preview' (default) calls fetchJSON('/preview/<x>') (no
|
|
431
|
+
// auth, 10 calls per IP per day); tier='full' calls
|
|
432
|
+
// fetchJSON('/premium/<x>', { auth: true }) and burns 1 credit. The
|
|
433
|
+
// shapes below mirror the worker builder result interfaces exactly.
|
|
434
|
+
// ════════════════════════════════════════════════════════════════════
|
|
435
|
+
// Shared upsell tail for the preview tools. Mirrors ROUTE_VERDICT_FOOTER
|
|
436
|
+
// cadence. `tool` is the matching premium tool, `adds` names what the
|
|
437
|
+
// paid tier adds over the free preview, `rl` is the optional remaining
|
|
438
|
+
// calls suffix.
|
|
439
|
+
function verdictUpsell(tool, adds, rl) {
|
|
440
|
+
return (`Upgrade: call ${tool} again with tier="full" (1 credit, $0.02) for ${adds} and an AFTA-signed receipt. ` +
|
|
441
|
+
`No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`);
|
|
442
|
+
}
|
|
443
|
+
function verdictBilling(billing) {
|
|
444
|
+
return billing ? `\n\nCharged ${billing.credits_charged} credit. Remaining: ${billing.credits_remaining}.` : '';
|
|
445
|
+
}
|
|
446
|
+
function previewRateLine(rate) {
|
|
447
|
+
return rate ? ` Preview: ${rate.remaining} of ${rate.limit} calls left today.` : '';
|
|
448
|
+
}
|
|
449
|
+
registerTool('provider_reliability_verdict', "TensorFeed's signed dependability ruling over its OWN measured latency and availability probes of the frontier AI providers: the single most-dependable provider to build on and the riskiest, scoring availability and tail consistency (p50 over p95) equally because an agent retry loop feels the tail, not the median. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds the full per-provider ranking with measured availability and p50/p95/p99 and tail spread plus an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
450
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the full per-provider ranking and signed receipt)."),
|
|
451
|
+
}, async ({ tier }) => {
|
|
452
|
+
if ((tier ?? 'preview') === 'full') {
|
|
453
|
+
const data = (await fetchJSON('/premium/provider-reliability-verdict', { auth: true }));
|
|
454
|
+
const v = data.verdict;
|
|
455
|
+
if (!v || !v.most_dependable) {
|
|
456
|
+
return { content: [{ type: 'text', text: `No reliability verdict available right now.\n${data.claim ?? ''}` }] };
|
|
457
|
+
}
|
|
458
|
+
const ranking = (data.ranking ?? [])
|
|
459
|
+
.map((r) => ` #${r.rank} ${r.provider}: ${r.note}`)
|
|
460
|
+
.join('\n');
|
|
433
461
|
return {
|
|
434
462
|
content: [
|
|
435
463
|
{
|
|
436
464
|
type: 'text',
|
|
437
|
-
text: `
|
|
465
|
+
text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
|
|
466
|
+
(v.riskiest ? `, riskiest ${v.riskiest}` : '') +
|
|
467
|
+
'\n\n' +
|
|
468
|
+
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
469
|
+
`Claim: ${data.claim}` +
|
|
470
|
+
verdictBilling(data.billing),
|
|
438
471
|
},
|
|
439
472
|
],
|
|
440
473
|
};
|
|
441
474
|
}
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
475
|
+
const data = (await fetchJSON('/preview/provider-reliability-verdict'));
|
|
476
|
+
const v = data.verdict;
|
|
477
|
+
if (!v || !v.most_dependable) {
|
|
478
|
+
return { content: [{ type: 'text', text: `No reliability verdict available right now.\n${data.claim ?? ''}` }] };
|
|
479
|
+
}
|
|
480
|
+
const cov = data.coverage;
|
|
481
|
+
const rl = previewRateLine(data.rate_limit);
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: 'text',
|
|
486
|
+
text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
|
|
487
|
+
(v.riskiest ? `, riskiest ${v.riskiest}` : '') +
|
|
488
|
+
'\n' +
|
|
489
|
+
`Claim: ${data.claim}\n` +
|
|
490
|
+
`Coverage: ${cov.providers_ranked} providers ranked (${cov.fully_measured} fully measured, ${cov.availability_only} availability only)` +
|
|
491
|
+
(data.captured_at ? `, captured ${data.captured_at}` : '') +
|
|
492
|
+
'\n' +
|
|
493
|
+
verdictUpsell('provider_reliability_verdict', 'the full per-provider ranking with availability, tail spread, and measured p50/p95/p99', rl),
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
};
|
|
458
497
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
.
|
|
468
|
-
|
|
469
|
-
.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const filtered = data.iocs.filter((e) => !typeWanted || e.type.toLowerCase() === typeWanted);
|
|
475
|
-
const cap = Math.min(limit ?? 50, 500);
|
|
476
|
-
const rows = filtered.slice(0, cap);
|
|
477
|
-
if (rows.length === 0) {
|
|
498
|
+
registerTool('x402_settlement_verdict', "TensorFeed's signed ruling on the state of the x402 USDC settlement market on Base, computed over its OWN on-chain settlement index: market momentum versus the prior window of equal length, concentration, and the leading publisher. Covers the publishers TensorFeed indexes on Base, forward-only from launch. tier='preview' (default) is free (10 calls per day per IP), headline verdict only. tier='full' costs 1 credit ($0.02), adds the full per-publisher ranking with volume share, ecosystem totals, the Herfindahl concentration index, an optional window, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
499
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-publisher ranking, totals, window, signed receipt)."),
|
|
500
|
+
window: z.enum(['24h', '7d', '30d']).optional().describe('Full tier only. Settlement window to rule over (24h, 7d, 30d). Default 7d.'),
|
|
501
|
+
}, async ({ tier, window }) => {
|
|
502
|
+
if ((tier ?? 'preview') === 'full') {
|
|
503
|
+
const params = new URLSearchParams();
|
|
504
|
+
if (window)
|
|
505
|
+
params.set('window', window);
|
|
506
|
+
const qs = params.toString();
|
|
507
|
+
const data = (await fetchJSON(`/premium/x402-settlement-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
508
|
+
const v = data.verdict;
|
|
509
|
+
const e = data.ecosystem;
|
|
510
|
+
const ranking = (data.ranking ?? [])
|
|
511
|
+
.map((r) => ` #${r.rank} ${r.domain}: ${r.volume_usdc} USDC over ${r.count} settlements (${r.share_pct}% share)`)
|
|
512
|
+
.join('\n');
|
|
478
513
|
return {
|
|
479
514
|
content: [
|
|
480
515
|
{
|
|
481
516
|
type: 'text',
|
|
482
|
-
text: `
|
|
517
|
+
text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
|
|
518
|
+
(v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
|
|
519
|
+
'\n' +
|
|
520
|
+
`Ecosystem: ${e.volume_usdc} USDC over ${e.count} settlements across ${e.unique_publishers} publishers` +
|
|
521
|
+
(e.top_publisher_share_pct !== null ? `, top share ${e.top_publisher_share_pct}%` : '') +
|
|
522
|
+
(e.hhi !== null ? `, HHI ${e.hhi}` : '') +
|
|
523
|
+
'\n\n' +
|
|
524
|
+
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
525
|
+
`Claim: ${data.claim}` +
|
|
526
|
+
verdictBilling(data.billing),
|
|
483
527
|
},
|
|
484
528
|
],
|
|
485
529
|
};
|
|
486
530
|
}
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const header = `TensorFeed honeypot IOCs (${rows.length} of ${filtered.length} matched, ${data.total_iocs} in feed, ${data.window_hours}h window, generated ${data.generated_at}):`;
|
|
491
|
-
const footer = `\n\nLicense: ${data.policy.license}. ${data.policy.description}`;
|
|
492
|
-
return { content: [{ type: 'text', text: `${header}\n\n${body}${footer}` }] };
|
|
493
|
-
});
|
|
494
|
-
// ── Tool: check_afta_certification ──────────────────────────────────
|
|
495
|
-
registerTool('check_afta_certification', 'Check whether a domain is AFTA-certified (Agent Fair-Trade Agreement). AFTA is an open standard: cryptographically-signed receipts, transparent pricing, no-charge guarantees for failed responses, on-chain settlement, and federated trust between participating sites. The check runs 6 deterministic probes against the target domain (e.g. tensorfeed.ai, terminalfeed.io): well-known endpoint, receipts endpoint, x402 payment manifest, pricing transparency, free trial availability, federation membership. Returns score, verdict, and which checks passed/failed.', {
|
|
496
|
-
domain: z
|
|
497
|
-
.string()
|
|
498
|
-
.describe('Domain to certify (e.g. "tensorfeed.ai", "terminalfeed.io"). Bare host, no protocol.'),
|
|
499
|
-
}, async ({ domain }) => {
|
|
500
|
-
const data = (await fetchJSON(`/afta-certify/check?domain=${encodeURIComponent(domain)}`));
|
|
501
|
-
const lines = data.checks.map((c) => ` ${c.passed ? 'PASS' : 'FAIL'} ${c.name}${c.details ? ` (${c.details})` : ''}`);
|
|
502
|
-
const certified = data.afta_certified ? 'AFTA-CERTIFIED' : 'NOT certified';
|
|
503
|
-
const next = data.next_step ? `\nNext step: ${data.next_step}` : '';
|
|
531
|
+
const data = (await fetchJSON('/preview/x402-settlement-verdict'));
|
|
532
|
+
const v = data.verdict;
|
|
533
|
+
const rl = previewRateLine(data.rate_limit);
|
|
504
534
|
return {
|
|
505
535
|
content: [
|
|
506
536
|
{
|
|
507
537
|
type: 'text',
|
|
508
|
-
text: `
|
|
538
|
+
text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
|
|
539
|
+
(v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
|
|
540
|
+
'\n' +
|
|
541
|
+
`Claim: ${data.claim}` +
|
|
542
|
+
(data.captured_at ? `\nIndex captured ${data.captured_at}` : '') +
|
|
543
|
+
'\n' +
|
|
544
|
+
verdictUpsell('x402_settlement_verdict', 'the full per-publisher ranking, ecosystem volume and count totals, the Herfindahl concentration index, and an optional 24h/7d/30d window', rl),
|
|
509
545
|
},
|
|
510
546
|
],
|
|
511
547
|
};
|
|
512
548
|
});
|
|
513
|
-
// ── Tool:
|
|
514
|
-
registerTool('
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
549
|
+
// ── Tool: x402_publisher_verdict (preview free / full 1 credit) ──────
|
|
550
|
+
registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. Requires a domain. tier='preview' (default) is free (10 calls per day per IP), the trust verdict only. tier='full' costs 1 credit ($0.02), adds the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence (volume, count, last settled), and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
551
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds momentum, shared-wallet flag, evidence, signed receipt)."),
|
|
552
|
+
domain: z.string().describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
|
|
553
|
+
}, async ({ tier, domain }) => {
|
|
554
|
+
if (!domain || !domain.trim()) {
|
|
555
|
+
return { content: [{ type: 'text', text: 'Provide a publisher domain (e.g. domain "x402.tavily.com") to get a trust verdict.' }] };
|
|
556
|
+
}
|
|
557
|
+
const params = new URLSearchParams({ domain: domain.trim() });
|
|
558
|
+
if ((tier ?? 'preview') === 'full') {
|
|
559
|
+
const data = (await fetchJSON(`/premium/x402-publisher-verdict?${params}`, { auth: true }));
|
|
560
|
+
const t = data.trust;
|
|
561
|
+
const ev = data.evidence;
|
|
525
562
|
return {
|
|
526
563
|
content: [
|
|
527
564
|
{
|
|
528
565
|
type: 'text',
|
|
529
|
-
text:
|
|
566
|
+
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
567
|
+
`Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
|
|
568
|
+
(t.last_settled ? `. Last settled ${t.last_settled}` : '') +
|
|
569
|
+
'\n' +
|
|
570
|
+
`Evidence (${ev.window_days}d): ${ev.volume_usdc} USDC over ${ev.count} settlements\n` +
|
|
571
|
+
`Claim: ${data.claim}` +
|
|
572
|
+
verdictBilling(data.billing),
|
|
530
573
|
},
|
|
531
574
|
],
|
|
532
575
|
};
|
|
533
576
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
: `/agents/reputation/by-token/${encodeURIComponent(token_prefix)}`;
|
|
537
|
-
let data;
|
|
538
|
-
try {
|
|
539
|
-
data = await fetchJSON(path);
|
|
540
|
-
}
|
|
541
|
-
catch (e) {
|
|
542
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
543
|
-
if (msg.includes('404') || msg.toLowerCase().includes('not_found')) {
|
|
544
|
-
return {
|
|
545
|
-
content: [
|
|
546
|
-
{
|
|
547
|
-
type: 'text',
|
|
548
|
-
text: `No reputation card for ${wallet ?? token_prefix}. TF only builds cards for wallets observed in production; if this is a real agent that has paid TF before, it should appear in the next daily rebuild (04:50 UTC).`,
|
|
549
|
-
},
|
|
550
|
-
],
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
throw e;
|
|
554
|
-
}
|
|
555
|
-
// The card schema is rich; select the documented top-level fields and
|
|
556
|
-
// clamp any array so a large or inflated upstream card cannot blow the
|
|
557
|
-
// agent's context. Unknown fields still pass through (so the agent can
|
|
558
|
-
// reason over them) but every array is capped and the whole payload is
|
|
559
|
-
// bounded by the downstream sanitize char cap as a backstop.
|
|
560
|
-
const rawCard = data.card ?? data;
|
|
561
|
-
const MAX_CARD_ARRAY = 25;
|
|
562
|
-
const clamped = {};
|
|
563
|
-
for (const [k, v] of Object.entries(rawCard)) {
|
|
564
|
-
if (Array.isArray(v) && v.length > MAX_CARD_ARRAY) {
|
|
565
|
-
clamped[k] = [...v.slice(0, MAX_CARD_ARRAY), `...[${v.length - MAX_CARD_ARRAY} more omitted]`];
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
clamped[k] = v;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
577
|
+
const data = (await fetchJSON(`/preview/x402-publisher-verdict?${params}`));
|
|
578
|
+
const rl = previewRateLine(data.rate_limit);
|
|
571
579
|
return {
|
|
572
580
|
content: [
|
|
573
581
|
{
|
|
574
582
|
type: 'text',
|
|
575
|
-
text: `
|
|
583
|
+
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
584
|
+
`Claim: ${data.claim}` +
|
|
585
|
+
(data.captured_at ? `\nIndex captured ${data.captured_at}` : '') +
|
|
586
|
+
'\n' +
|
|
587
|
+
verdictUpsell('x402_publisher_verdict', 'the 30-day settlement momentum, the shared-wallet risk flag, and the settlement evidence (volume, count, daily series)', rl),
|
|
576
588
|
},
|
|
577
589
|
],
|
|
578
590
|
};
|
|
579
591
|
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
592
|
+
registerTool('stack_safety_verdict', "TensorFeed's deploy gate for an AI software stack: pass each package as comma-separated name@version and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. tier='preview' (default) is free (10 calls per day per IP), caps at 3 packages, gate plus worst offender. tier='full' costs 1 credit ($0.02), raises the cap to 10 packages, adds the matched-CVE evidence (ids, affected ranges, fixed versions, KEV status) and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
593
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free, caps at 3 packages) or 'full' (1 credit; up to 10 packages, matched-CVE evidence, signed receipt)."),
|
|
594
|
+
packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3, full up to 10.'),
|
|
595
|
+
}, async ({ tier, packages }) => {
|
|
596
|
+
if (!packages || !packages.trim()) {
|
|
597
|
+
return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
|
|
598
|
+
}
|
|
599
|
+
const params = new URLSearchParams({ packages: packages.trim() });
|
|
600
|
+
if ((tier ?? 'preview') === 'full') {
|
|
601
|
+
const data = (await fetchJSON(`/premium/stack-safety-verdict?${params}`, { auth: true }));
|
|
602
|
+
const c = data.counts;
|
|
603
|
+
const lines = (data.packages ?? [])
|
|
604
|
+
.map((p) => {
|
|
605
|
+
const cves = p.matched_cves
|
|
606
|
+
.map((m) => `${m.cve_id}${m.on_kev ? ' (KEV)' : ''} ${m.severity_label}`)
|
|
607
|
+
.join('; ');
|
|
608
|
+
return ` ${p.package}${p.version ? `@${p.version}` : ''}: ${p.verdict}. ${p.reason}` + (cves ? `\n CVEs: ${cves}` : '');
|
|
609
|
+
})
|
|
610
|
+
.join('\n');
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: 'text',
|
|
615
|
+
text: `Stack Safety Verdict: gate ${data.gate}\n` +
|
|
616
|
+
`Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n\n` +
|
|
617
|
+
`${lines || ' (no packages)'}\n\n` +
|
|
618
|
+
`Claim: ${data.claim}` +
|
|
619
|
+
verdictBilling(data.billing),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const data = (await fetchJSON(`/preview/stack-safety-verdict?${params}`));
|
|
625
|
+
const c = data.counts;
|
|
626
|
+
const worst = (data.packages ?? []).find((p) => p.verdict === 'BLOCK') ?? (data.packages ?? []).find((p) => p.verdict === 'HOLD') ?? null;
|
|
627
|
+
const worstLine = worst
|
|
628
|
+
? `Worst offender: ${worst.package}${worst.version ? `@${worst.version}` : ''} ${worst.verdict} (${worst.matched_cve_count} matched CVE${worst.matched_cve_count === 1 ? '' : 's'}). ${worst.reason}`
|
|
629
|
+
: 'No package hit a BLOCK or HOLD.';
|
|
630
|
+
const rl = previewRateLine(data.rate_limit);
|
|
586
631
|
return {
|
|
587
632
|
content: [
|
|
588
633
|
{
|
|
589
634
|
type: 'text',
|
|
590
|
-
text: `
|
|
635
|
+
text: `Stack Safety Verdict: gate ${data.gate}\n` +
|
|
636
|
+
`Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n` +
|
|
637
|
+
`${worstLine}\n` +
|
|
638
|
+
`Claim: ${data.claim}\n` +
|
|
639
|
+
verdictUpsell('stack_safety_verdict', 'the matched-CVE evidence (ids, affected ranges, fixed versions, KEV status) and up to 10 packages', rl),
|
|
591
640
|
},
|
|
592
641
|
],
|
|
593
642
|
};
|
|
594
643
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
644
|
+
registerTool('benchmark_trust_verdict', "TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted: a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. tier='preview' (default) is free (10 calls per day per IP), top verdict and bands only. tier='full' costs 1 credit ($0.02), adds the per-signal detail (ceiling proximity, frontier compression, contamination), a down-weight recommendation with an alternative benchmark, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
645
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-signal detail, recommendation, signed receipt)."),
|
|
646
|
+
benchmark: z.string().optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
|
|
647
|
+
category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
|
|
648
|
+
}, async ({ tier, benchmark, category }) => {
|
|
649
|
+
const params = new URLSearchParams();
|
|
650
|
+
if (benchmark)
|
|
651
|
+
params.set('benchmark', benchmark);
|
|
652
|
+
if (category)
|
|
653
|
+
params.set('category', category);
|
|
654
|
+
const qs = params.toString();
|
|
655
|
+
if ((tier ?? 'preview') === 'full') {
|
|
656
|
+
const data = (await fetchJSON(`/premium/benchmark-trust-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
657
|
+
const verdicts = data.verdicts ?? [];
|
|
658
|
+
if (verdicts.length === 0) {
|
|
659
|
+
return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
|
|
660
|
+
}
|
|
661
|
+
const lines = verdicts
|
|
662
|
+
.slice(0, 8)
|
|
663
|
+
.map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100. Ceiling ${v.signals.ceiling_proximity}, frontier ${v.signals.frontier_compression}.\n ${v.recommendation}`)
|
|
664
|
+
.join('\n');
|
|
665
|
+
return {
|
|
666
|
+
content: [
|
|
667
|
+
{
|
|
668
|
+
type: 'text',
|
|
669
|
+
text: `Benchmark Trust Verdict: ${data.count} benchmark${data.count === 1 ? '' : 's'} ruled on\n\n` +
|
|
670
|
+
`${lines}\n\n` +
|
|
671
|
+
`Claim: ${data.claim}` +
|
|
672
|
+
verdictBilling(data.billing),
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const data = (await fetchJSON(`/preview/benchmark-trust-verdict${qs ? `?${qs}` : ''}`));
|
|
678
|
+
const verdicts = data.verdicts ?? [];
|
|
679
|
+
if (verdicts.length === 0) {
|
|
680
|
+
return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
|
|
600
681
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
.
|
|
682
|
+
const top = verdicts[0];
|
|
683
|
+
const list = verdicts
|
|
684
|
+
.slice(0, 3)
|
|
685
|
+
.map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100`)
|
|
604
686
|
.join('\n');
|
|
687
|
+
const rl = previewRateLine(data.rate_limit);
|
|
605
688
|
return {
|
|
606
689
|
content: [
|
|
607
690
|
{
|
|
608
691
|
type: 'text',
|
|
609
|
-
text: `
|
|
692
|
+
text: `Benchmark Trust Verdict: ${top.name} is ${top.trust_band} (trust score ${top.trust_score}/100)\n` +
|
|
693
|
+
(data.count > 1 ? `${data.count} benchmarks ruled on. Top:\n${list}\n` : `${list}\n`) +
|
|
694
|
+
`Claim: ${data.claim}\n` +
|
|
695
|
+
verdictUpsell('benchmark_trust_verdict', 'the per-signal detail (ceiling proximity, frontier compression, contamination, status) and the down-weight recommendation with an alternative benchmark', rl),
|
|
610
696
|
},
|
|
611
697
|
],
|
|
612
698
|
};
|
|
613
699
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
700
|
+
registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. Requires from. tier='preview' (default) is free (10 calls per day per IP), the failover target only. tier='full' costs 1 credit ($0.02), adds the full candidate (pricing, measured p95 latency, quality), the ranked alternatives, the confirmed incident on A, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
701
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds candidate detail, ranked alternatives, incident, signed receipt)."),
|
|
702
|
+
from: z.string().describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
|
|
703
|
+
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to optimize the failover target for. Optional.'),
|
|
704
|
+
}, async ({ tier, from, task }) => {
|
|
705
|
+
if (!from || !from.trim()) {
|
|
706
|
+
return { content: [{ type: 'text', text: 'Provide a from provider (the degraded provider to fail over from, e.g. from "anthropic") to get a failover verdict.' }] };
|
|
707
|
+
}
|
|
708
|
+
const params = new URLSearchParams({ from: from.trim() });
|
|
622
709
|
if (task)
|
|
623
710
|
params.set('task', task);
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (!task && !model) {
|
|
711
|
+
if ((tier ?? 'preview') === 'full') {
|
|
712
|
+
const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
|
|
713
|
+
const dest = data.failover_to;
|
|
714
|
+
const incidentLine = data.from.incident
|
|
715
|
+
? `Incident on ${data.from.provider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
|
|
716
|
+
: '';
|
|
717
|
+
if (!dest) {
|
|
718
|
+
return {
|
|
719
|
+
content: [
|
|
720
|
+
{
|
|
721
|
+
type: 'text',
|
|
722
|
+
text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
|
|
723
|
+
incidentLine +
|
|
724
|
+
`Why: ${data.why}\nClaim: ${data.claim}` +
|
|
725
|
+
verdictBilling(data.billing),
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const alts = (data.alternatives ?? [])
|
|
731
|
+
.map((a) => ` #${a.rank} ${a.model.name} (${a.model.provider}) blended $${a.pricing.blended}/1M, p95 ${a.latency.measured_p95_ms ?? 'n/a'}ms, status ${a.operational.status}`)
|
|
732
|
+
.join('\n');
|
|
647
733
|
return {
|
|
648
734
|
content: [
|
|
649
735
|
{
|
|
650
736
|
type: 'text',
|
|
651
|
-
text:
|
|
737
|
+
text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
738
|
+
`Why: ${dest.why}\n` +
|
|
739
|
+
`Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
|
|
740
|
+
incidentLine +
|
|
741
|
+
`Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n\n` +
|
|
742
|
+
`Alternatives:\n${alts || ' (none)'}\n\n` +
|
|
743
|
+
`Claim: ${data.claim}` +
|
|
744
|
+
verdictBilling(data.billing),
|
|
652
745
|
},
|
|
653
746
|
],
|
|
654
747
|
};
|
|
655
748
|
}
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (
|
|
660
|
-
params.set('model', model);
|
|
661
|
-
const data = (await fetchJSON(`/preview/route-verdict?${params}`));
|
|
662
|
-
if (!data.verdict) {
|
|
749
|
+
const data = (await fetchJSON(`/preview/failover-verdict?${params}`));
|
|
750
|
+
const dest = data.failover_to;
|
|
751
|
+
const rl = previewRateLine(data.rate_limit);
|
|
752
|
+
if (!dest) {
|
|
663
753
|
return {
|
|
664
|
-
content: [
|
|
754
|
+
content: [
|
|
755
|
+
{
|
|
756
|
+
type: 'text',
|
|
757
|
+
text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
|
|
758
|
+
`Why: ${data.why}\n${data.claim}\n` +
|
|
759
|
+
verdictUpsell('failover_verdict', 'the full failover candidate (pricing, measured latency, quality) and the ranked alternatives', rl),
|
|
760
|
+
},
|
|
761
|
+
],
|
|
665
762
|
};
|
|
666
763
|
}
|
|
667
|
-
const v = data.verdict;
|
|
668
|
-
const t = data.trust;
|
|
669
|
-
const fresh = [
|
|
670
|
-
data.data_freshness.pricing ? `pricing ${data.data_freshness.pricing}` : null,
|
|
671
|
-
data.data_freshness.probe ? `latency ${data.data_freshness.probe}` : null,
|
|
672
|
-
data.data_freshness.status ? `status ${data.data_freshness.status}` : null,
|
|
673
|
-
]
|
|
674
|
-
.filter(Boolean)
|
|
675
|
-
.join(', ');
|
|
676
|
-
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
677
|
-
const rl = data.rate_limit ? `\nPreview: ${data.rate_limit.remaining} of ${data.rate_limit.limit} calls left today (${data.rate_limit.scope}).` : '';
|
|
678
764
|
return {
|
|
679
765
|
content: [
|
|
680
766
|
{
|
|
681
767
|
type: 'text',
|
|
682
|
-
text: `
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
`
|
|
768
|
+
text: `Failover Verdict from ${data.from.provider}` +
|
|
769
|
+
(data.from.in_incident ? ' (confirmed in incident)' : '') +
|
|
770
|
+
`: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
771
|
+
`Why: ${data.why}\n` +
|
|
772
|
+
`Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n` +
|
|
773
|
+
`Claim: ${data.claim}\n` +
|
|
774
|
+
verdictUpsell('failover_verdict', 'the full failover candidate (pricing, measured latency, quality) and the ranked alternatives', rl),
|
|
687
775
|
},
|
|
688
776
|
],
|
|
689
777
|
};
|
|
690
778
|
});
|
|
691
|
-
// ── Tool:
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
779
|
+
// ── Tool: ssvc_verdict (preview free / full 1 credit) ────────────────
|
|
780
|
+
registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision for one CVE, applying the CISA SSVC Coordinator decision tree to the recorded Vulnrichment decision points (exploitation, automatable, technical impact). Requires a CVE id. tier='preview' (default) is free (10 calls per day per IP): the three decision points and the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision. tier='full' costs 1 credit ($0.02), adds the computed decision across the full low/medium/high Mission and Well-being envelope, the per-level reasoning, a live CISA KEV cross-check, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
781
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the computed decision, envelope, reasoning, KEV cross-check, signed receipt)."),
|
|
782
|
+
cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
|
|
783
|
+
}, async ({ tier, cve }) => {
|
|
784
|
+
if (!cve || !cve.trim()) {
|
|
785
|
+
return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
|
|
786
|
+
}
|
|
787
|
+
const params = new URLSearchParams({ cve: cve.trim() });
|
|
788
|
+
if ((tier ?? 'preview') === 'full') {
|
|
789
|
+
const data = (await fetchJSON(`/premium/security/ssvc-verdict?${params}`, { auth: true }));
|
|
790
|
+
const dp = data.decision_points;
|
|
791
|
+
const env = data.decision_envelope;
|
|
792
|
+
const kev = data.kev_cross_check;
|
|
793
|
+
const kevLine = kev && kev.checked
|
|
794
|
+
? `KEV cross-check: ${kev.kev_listed ? 'listed on CISA KEV' : 'not on KEV'}${kev.flag && kev.flag !== 'none' ? ` (${kev.flag})` : ''}`
|
|
795
|
+
: 'KEV cross-check: unavailable';
|
|
706
796
|
return {
|
|
707
797
|
content: [
|
|
708
798
|
{
|
|
709
799
|
type: 'text',
|
|
710
|
-
text:
|
|
800
|
+
text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
|
|
801
|
+
`Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
|
|
802
|
+
`Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
|
|
803
|
+
`${kevLine}\n` +
|
|
804
|
+
`Tree: ${data.tree.name} ${data.tree.version}` +
|
|
805
|
+
(data.scored_at ? `, scored ${data.scored_at}` : '') +
|
|
806
|
+
verdictBilling(data.billing),
|
|
711
807
|
},
|
|
712
808
|
],
|
|
713
809
|
};
|
|
714
810
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (model)
|
|
719
|
-
params.set('model', model);
|
|
720
|
-
if (typeof max_latency_p95_ms === 'number')
|
|
721
|
-
params.set('max_latency_p95_ms', String(max_latency_p95_ms));
|
|
722
|
-
if (typeof budget === 'number')
|
|
723
|
-
params.set('budget', String(budget));
|
|
724
|
-
if (typeof min_quality === 'number')
|
|
725
|
-
params.set('min_quality', String(min_quality));
|
|
726
|
-
if (typeof require_operational === 'boolean')
|
|
727
|
-
params.set('require_operational', String(require_operational));
|
|
728
|
-
if (typeof exclude_deprecated === 'boolean')
|
|
729
|
-
params.set('exclude_deprecated', String(exclude_deprecated));
|
|
730
|
-
const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
|
|
731
|
-
if (!data.verdict) {
|
|
732
|
-
return {
|
|
733
|
-
content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
const v = data.verdict;
|
|
737
|
-
const t = data.trust;
|
|
738
|
-
const f = data.filters_applied;
|
|
739
|
-
const runners = (data.runners_up ?? [])
|
|
740
|
-
.map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
|
|
741
|
-
.join('\n');
|
|
742
|
-
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
743
|
-
const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
|
|
744
|
-
const billing = data.billing
|
|
745
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
|
|
746
|
-
: '';
|
|
811
|
+
const data = (await fetchJSON(`/preview/security/ssvc-verdict?${params}`));
|
|
812
|
+
const dp = data.decision_points;
|
|
813
|
+
const rl = previewRateLine(data.rate_limit);
|
|
747
814
|
return {
|
|
748
815
|
content: [
|
|
749
816
|
{
|
|
750
817
|
type: 'text',
|
|
751
|
-
text: `
|
|
752
|
-
`
|
|
753
|
-
`
|
|
754
|
-
`
|
|
755
|
-
|
|
818
|
+
text: `SSVC decision points for ${data.cve}:\n` +
|
|
819
|
+
` exploitation: ${dp.exploitation}\n` +
|
|
820
|
+
` automatable: ${dp.automatable}\n` +
|
|
821
|
+
` technical impact: ${dp.technical_impact}\n` +
|
|
822
|
+
`Tree: ${data.tree.name} ${data.tree.version}` +
|
|
823
|
+
(data.scored_at ? `, scored ${data.scored_at}` : '') +
|
|
824
|
+
'\n' +
|
|
825
|
+
verdictUpsell('ssvc_verdict', 'the computed SSVC decision (Act, Attend, Track, or Track*) across the low/medium/high Mission and Well-being envelope, the per-level reasoning, and the live KEV cross-check', rl),
|
|
756
826
|
},
|
|
757
827
|
],
|
|
758
828
|
};
|
|
759
829
|
});
|
|
760
|
-
//
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
830
|
+
// Series window helper: convert a requested day count into a `from` date
|
|
831
|
+
// (today minus days-1, UTC) for the premium date-range endpoints, which
|
|
832
|
+
// take from/to rather than a days shorthand.
|
|
833
|
+
function seriesFromDate(days) {
|
|
834
|
+
const d = new Date();
|
|
835
|
+
d.setUTCDate(d.getUTCDate() - (days - 1));
|
|
836
|
+
return d.toISOString().slice(0, 10);
|
|
837
|
+
}
|
|
838
|
+
// ── Tool: pricing_series (free 1 to 7 days, paid 8 to 90 days) ──────
|
|
839
|
+
registerTool('pricing_series', 'Daily price points for one AI model over a window. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding the min/max/delta summary over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
840
|
+
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7").'),
|
|
841
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
764
842
|
}, async ({ model, days }) => {
|
|
765
|
-
const
|
|
766
|
-
if (
|
|
767
|
-
params
|
|
843
|
+
const d = days ?? 7;
|
|
844
|
+
if (d > 7) {
|
|
845
|
+
const params = new URLSearchParams({ model, from: seriesFromDate(d) });
|
|
846
|
+
const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
|
|
847
|
+
const s = data.summary;
|
|
848
|
+
const summary = s.first && s.latest
|
|
849
|
+
? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
|
|
850
|
+
: 'no data points in range';
|
|
851
|
+
return {
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: 'text',
|
|
855
|
+
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const params = new URLSearchParams({ model, days: String(d) });
|
|
768
861
|
const data = (await fetchJSON(`/history/pricing/series?${params}`));
|
|
769
862
|
const s = data.summary;
|
|
770
863
|
const summary = s.first && s.latest
|
|
@@ -774,20 +867,35 @@ registerTool('pricing_series_free', 'Daily price points for one AI model over th
|
|
|
774
867
|
content: [
|
|
775
868
|
{
|
|
776
869
|
type: 'text',
|
|
777
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days,
|
|
870
|
+
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, pass days 8 to 90 (1 credit).`,
|
|
778
871
|
},
|
|
779
872
|
],
|
|
780
873
|
};
|
|
781
874
|
});
|
|
782
|
-
// ── Tool:
|
|
783
|
-
registerTool('
|
|
784
|
-
model: z.string().describe('Model id or display name'),
|
|
785
|
-
benchmark: z.string().describe('Benchmark key'),
|
|
786
|
-
days: z.number().min(1).max(
|
|
875
|
+
// ── Tool: benchmark_series (free 1 to 7 days, paid 8 to 90 days) ────
|
|
876
|
+
registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark over a window. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, tracking score evolution over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
877
|
+
model: z.string().describe('Model id or display name.'),
|
|
878
|
+
benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval).'),
|
|
879
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
787
880
|
}, async ({ model, benchmark, days }) => {
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
790
|
-
params
|
|
881
|
+
const d = days ?? 7;
|
|
882
|
+
if (d > 7) {
|
|
883
|
+
const params = new URLSearchParams({ model, benchmark, from: seriesFromDate(d) });
|
|
884
|
+
const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
|
|
885
|
+
const s = data.summary;
|
|
886
|
+
const summary = s.first && s.latest
|
|
887
|
+
? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
|
|
888
|
+
: 'no data in range';
|
|
889
|
+
return {
|
|
890
|
+
content: [
|
|
891
|
+
{
|
|
892
|
+
type: 'text',
|
|
893
|
+
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const params = new URLSearchParams({ model, benchmark, days: String(d) });
|
|
791
899
|
const data = (await fetchJSON(`/history/benchmarks/series?${params}`));
|
|
792
900
|
const s = data.summary;
|
|
793
901
|
const summary = s.first && s.latest
|
|
@@ -802,189 +910,85 @@ registerTool('benchmark_series_free', 'Daily benchmark scores for one model+benc
|
|
|
802
910
|
],
|
|
803
911
|
};
|
|
804
912
|
});
|
|
805
|
-
// ── Tool:
|
|
806
|
-
registerTool('
|
|
807
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
|
|
808
|
-
days: z.number().min(1).max(
|
|
913
|
+
// ── Tool: status_uptime (free 1 to 7 days, paid 8 to 90 days) ───────
|
|
914
|
+
registerTool('status_uptime', 'Daily uptime rollup for one provider over a window with operational/degraded/down day counts and uptime % (degraded counts as half-credit). days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding per-incident-day detail over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
915
|
+
provider: z.string().describe('Provider name (e.g. anthropic, openai, google).'),
|
|
916
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
809
917
|
}, async ({ provider, days }) => {
|
|
810
|
-
const
|
|
811
|
-
if (
|
|
812
|
-
params
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
type: 'text',
|
|
818
|
-
text: `${data.provider} over ${data.days_total} days: ${data.uptime_pct ?? '?'}% uptime (${data.days_operational} operational, ${data.days_degraded} degraded, ${data.days_down} down)`,
|
|
819
|
-
},
|
|
820
|
-
],
|
|
821
|
-
};
|
|
822
|
-
});
|
|
823
|
-
// ── Tool: status_leaderboard_free (free, 7-day cap) ─────────────────
|
|
824
|
-
registerTool('status_leaderboard_free', 'Cross-provider uptime leaderboard, ranked by uptime % DESC. Free, 7-day cap. Computed from minute-resolution counters (~720 samples per provider per day). For 90-day windows plus incident_count and mttr_minutes per provider use status_leaderboard (1 credit).', {
|
|
825
|
-
days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
|
|
826
|
-
}, async ({ days }) => {
|
|
827
|
-
const params = new URLSearchParams();
|
|
828
|
-
if (typeof days === 'number')
|
|
829
|
-
params.set('days', String(days));
|
|
830
|
-
const qs = params.toString();
|
|
831
|
-
const data = (await fetchJSON(`/status/leaderboard${qs ? `?${qs}` : ''}`));
|
|
832
|
-
if (!data.ok) {
|
|
918
|
+
const d = days ?? 7;
|
|
919
|
+
if (d > 7) {
|
|
920
|
+
const params = new URLSearchParams({ provider, from: seriesFromDate(d) });
|
|
921
|
+
const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
|
|
922
|
+
const incidents = data.incident_days.length
|
|
923
|
+
? '\n\nIncident days:\n' + data.incident_days.map(d2 => ` ${d2.date}: ${d2.status}`).join('\n')
|
|
924
|
+
: '';
|
|
833
925
|
return {
|
|
834
|
-
content: [
|
|
926
|
+
content: [
|
|
927
|
+
{
|
|
928
|
+
type: 'text',
|
|
929
|
+
text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
|
|
930
|
+
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
931
|
+
incidents +
|
|
932
|
+
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
933
|
+
},
|
|
934
|
+
],
|
|
835
935
|
};
|
|
836
936
|
}
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
.map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% (${e.downtime_minutes}m downtime, ${e.hard_down_minutes}m hard-down)`);
|
|
840
|
-
return {
|
|
841
|
-
content: [
|
|
842
|
-
{
|
|
843
|
-
type: 'text',
|
|
844
|
-
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}`,
|
|
845
|
-
},
|
|
846
|
-
],
|
|
847
|
-
};
|
|
848
|
-
});
|
|
849
|
-
// ── Tool: pricing_series (1 credit) ─────────────────────────────────
|
|
850
|
-
registerTool('pricing_series', 'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling pricing_series_free is the discovery option.', {
|
|
851
|
-
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
|
|
852
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
853
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
854
|
-
}, async ({ model, from, to }) => {
|
|
855
|
-
const params = new URLSearchParams({ model });
|
|
856
|
-
if (from)
|
|
857
|
-
params.set('from', from);
|
|
858
|
-
if (to)
|
|
859
|
-
params.set('to', to);
|
|
860
|
-
const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
|
|
861
|
-
const s = data.summary;
|
|
862
|
-
const summary = s.first && s.latest
|
|
863
|
-
? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
|
|
864
|
-
: 'no data points in range';
|
|
865
|
-
return {
|
|
866
|
-
content: [
|
|
867
|
-
{
|
|
868
|
-
type: 'text',
|
|
869
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
870
|
-
},
|
|
871
|
-
],
|
|
872
|
-
};
|
|
873
|
-
});
|
|
874
|
-
// ── Tool: benchmark_series (1 credit) ───────────────────────────────
|
|
875
|
-
registerTool('benchmark_series', 'Score evolution for a single benchmark on one AI model. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling benchmark_series_free is the discovery option. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.', {
|
|
876
|
-
model: z.string().describe('Model id or display name'),
|
|
877
|
-
benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval)'),
|
|
878
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
879
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
880
|
-
}, async ({ model, benchmark, from, to }) => {
|
|
881
|
-
const params = new URLSearchParams({ model, benchmark });
|
|
882
|
-
if (from)
|
|
883
|
-
params.set('from', from);
|
|
884
|
-
if (to)
|
|
885
|
-
params.set('to', to);
|
|
886
|
-
const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
|
|
887
|
-
const s = data.summary;
|
|
888
|
-
const summary = s.first && s.latest
|
|
889
|
-
? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
|
|
890
|
-
: 'no data in range';
|
|
891
|
-
return {
|
|
892
|
-
content: [
|
|
893
|
-
{
|
|
894
|
-
type: 'text',
|
|
895
|
-
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
896
|
-
},
|
|
897
|
-
],
|
|
898
|
-
};
|
|
899
|
-
});
|
|
900
|
-
// ── Tool: status_uptime (1 credit) ──────────────────────────────────
|
|
901
|
-
registerTool('status_uptime', 'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling status_uptime_free is the discovery option.', {
|
|
902
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
|
|
903
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
904
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
905
|
-
}, async ({ provider, from, to }) => {
|
|
906
|
-
const params = new URLSearchParams({ provider });
|
|
907
|
-
if (from)
|
|
908
|
-
params.set('from', from);
|
|
909
|
-
if (to)
|
|
910
|
-
params.set('to', to);
|
|
911
|
-
const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
|
|
912
|
-
const incidents = data.incident_days.length
|
|
913
|
-
? '\n\nIncident days:\n' + data.incident_days.map(d => ` ${d.date}: ${d.status}`).join('\n')
|
|
914
|
-
: '';
|
|
937
|
+
const params = new URLSearchParams({ provider, days: String(d) });
|
|
938
|
+
const data = (await fetchJSON(`/history/status/uptime?${params}`));
|
|
915
939
|
return {
|
|
916
940
|
content: [
|
|
917
941
|
{
|
|
918
942
|
type: 'text',
|
|
919
|
-
text: `${data.provider}
|
|
920
|
-
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
921
|
-
incidents +
|
|
922
|
-
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
943
|
+
text: `${data.provider} over ${data.days_total} days: ${data.uptime_pct ?? '?'}% uptime (${data.days_operational} operational, ${data.days_degraded} degraded, ${data.days_down} down)`,
|
|
923
944
|
},
|
|
924
945
|
],
|
|
925
946
|
};
|
|
926
947
|
});
|
|
927
|
-
// ── Tool: status_leaderboard (1
|
|
928
|
-
registerTool('status_leaderboard', 'Cross-provider uptime leaderboard
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
948
|
+
// ── Tool: status_leaderboard (free 1 to 7 days, paid 8 to 90 days) ──
|
|
949
|
+
registerTool('status_leaderboard', 'Cross-provider uptime leaderboard ranked by uptime % DESC, computed from minute-resolution counters (~720 samples per provider per day). days 1 to 7 is free; days 8 to 90 costs 3 credits ($0.06) and needs a TENSORFEED_TOKEN, adding incident_count and mttr_minutes (mean time to recover) per provider over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
950
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 3 credits.'),
|
|
951
|
+
}, async ({ days }) => {
|
|
952
|
+
const d = days ?? 7;
|
|
953
|
+
if (d > 7) {
|
|
954
|
+
const params = new URLSearchParams({ from: seriesFromDate(d) });
|
|
955
|
+
const data = (await fetchJSON(`/premium/status/leaderboard?${params}`, { auth: true }));
|
|
956
|
+
if (!data.ok) {
|
|
957
|
+
return {
|
|
958
|
+
content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const lines = (data.entries ?? []).map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% uptime, ${e.downtime_minutes}m downtime (${e.hard_down_minutes}m hard-down), ${e.incident_count ?? 0} incidents, MTTR ${e.mttr_minutes ?? 'n/a'}m`);
|
|
962
|
+
return {
|
|
963
|
+
content: [
|
|
964
|
+
{
|
|
965
|
+
type: 'text',
|
|
966
|
+
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
967
|
+
},
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const params = new URLSearchParams({ days: String(d) });
|
|
972
|
+
const data = (await fetchJSON(`/status/leaderboard?${params}`));
|
|
938
973
|
if (!data.ok) {
|
|
939
974
|
return {
|
|
940
975
|
content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
|
|
941
976
|
};
|
|
942
977
|
}
|
|
943
|
-
const lines = (data.entries ?? [])
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
{
|
|
947
|
-
type: 'text',
|
|
948
|
-
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
};
|
|
952
|
-
});
|
|
953
|
-
// ── Tool: premium_agents_directory (1 credit) ───────────────────────
|
|
954
|
-
registerTool('premium_agents_directory', 'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit ($0.02).', {
|
|
955
|
-
category: z.string().optional().describe('coding, research, general, creative, frameworks'),
|
|
956
|
-
status: z.enum(['operational', 'degraded', 'down', 'unknown']).optional(),
|
|
957
|
-
open_source: z.boolean().optional(),
|
|
958
|
-
sort: z.enum(['trending', 'alphabetical', 'status', 'price_low', 'price_high', 'news_count']).optional(),
|
|
959
|
-
limit: z.number().min(1).max(100).optional(),
|
|
960
|
-
}, async ({ category, status, open_source, sort, limit }) => {
|
|
961
|
-
const params = new URLSearchParams();
|
|
962
|
-
if (category)
|
|
963
|
-
params.set('category', category);
|
|
964
|
-
if (status)
|
|
965
|
-
params.set('status', status);
|
|
966
|
-
if (typeof open_source === 'boolean')
|
|
967
|
-
params.set('open_source', String(open_source));
|
|
968
|
-
if (sort)
|
|
969
|
-
params.set('sort', sort);
|
|
970
|
-
if (typeof limit === 'number')
|
|
971
|
-
params.set('limit', String(limit));
|
|
972
|
-
const data = (await fetchJSON(`/premium/agents/directory?${params}`, { auth: true }));
|
|
973
|
-
const list = data.agents
|
|
974
|
-
.map(a => ` ${a.name} (${a.provider}) [${a.category}] status=${a.live_status} score=${a.trending_score} news=${a.recent_news_count}` +
|
|
975
|
-
(a.flagship_pricing ? ` flagship=${a.flagship_pricing.model} $${a.flagship_pricing.blended}/1M` : ''))
|
|
976
|
-
.join('\n');
|
|
978
|
+
const lines = (data.entries ?? [])
|
|
979
|
+
.slice(0, 10)
|
|
980
|
+
.map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% (${e.downtime_minutes}m downtime, ${e.hard_down_minutes}m hard-down)`);
|
|
977
981
|
return {
|
|
978
982
|
content: [
|
|
979
983
|
{
|
|
980
984
|
type: 'text',
|
|
981
|
-
text: `
|
|
985
|
+
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nprovider_reliability_verdict ranks providers by dependability, not just current state.`,
|
|
982
986
|
},
|
|
983
987
|
],
|
|
984
988
|
};
|
|
985
989
|
});
|
|
986
990
|
// ── Tool: whats_new (1 credit) ──────────────────────────────────────
|
|
987
|
-
registerTool('whats_new', '
|
|
991
|
+
registerTool('whats_new', 'Catch up on everything that changed in AI in one call: pricing moves, new and removed models, status incidents, and top news from the last 1 to 7 days, so your agent boots with current context instead of stale assumptions. Costs 1 credit ($0.02).', {
|
|
988
992
|
days: z.number().min(1).max(7).optional().describe('Window length in days (default 1)'),
|
|
989
993
|
news_limit: z.number().min(1).max(25).optional().describe('Max news headlines (default 10)'),
|
|
990
994
|
}, async ({ days, news_limit }) => {
|
|
@@ -1020,7 +1024,7 @@ registerTool('whats_new', 'Agent morning brief: pricing changes, new/removed mod
|
|
|
1020
1024
|
};
|
|
1021
1025
|
});
|
|
1022
1026
|
// ── Tool: compare_models (1 credit) ─────────────────────────────────
|
|
1023
|
-
registerTool('compare_models', '
|
|
1027
|
+
registerTool('compare_models', 'Pick between models in one call: pricing, benchmarks, status, and recent news for 2 to 5 models side by side, with cheapest-blended and per-benchmark rankings, so you choose without scraping each provider. Costs 1 credit ($0.02).', {
|
|
1024
1028
|
ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.7,GPT-5.5,Gemini 3" or "opus-4-7,gpt-5-5"'),
|
|
1025
1029
|
}, async ({ ids }) => {
|
|
1026
1030
|
const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
|
|
@@ -1047,7 +1051,7 @@ registerTool('compare_models', 'Side-by-side comparison of 2-5 AI models. Return
|
|
|
1047
1051
|
};
|
|
1048
1052
|
});
|
|
1049
1053
|
// ── Tool: provider_deepdive (1 credit) ──────────────────────────────
|
|
1050
|
-
registerTool('provider_deepdive', 'Everything about
|
|
1054
|
+
registerTool('provider_deepdive', 'Everything about one AI provider in a single call: live status, every model with pricing, tier, and benchmarks joined in, recent news, and agent traffic, replacing about four separate lookups. Costs 3 credits ($0.06). Strict premium, no free trial.', {
|
|
1051
1055
|
provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
|
|
1052
1056
|
}, async ({ provider }) => {
|
|
1053
1057
|
const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
|
|
@@ -1076,79 +1080,6 @@ registerTool('provider_deepdive', 'Everything about an AI provider in one call:
|
|
|
1076
1080
|
],
|
|
1077
1081
|
};
|
|
1078
1082
|
});
|
|
1079
|
-
// ── Tool: cost_projection (1 credit) ────────────────────────────────
|
|
1080
|
-
registerTool('cost_projection', 'Project the cost of a token-usage workload across 1-10 AI models. Returns daily/weekly/monthly/yearly totals per model and a ranking by cheapest monthly. Costs 1 credit ($0.02).', {
|
|
1081
|
-
models: z.string().describe('One model or comma-separated list of up to 10 (e.g. "Claude Opus 4.7,GPT-5.5,Gemini 3"). Names or ids both work.'),
|
|
1082
|
-
input_tokens_per_day: z.number().min(0).describe('Expected daily input token volume'),
|
|
1083
|
-
output_tokens_per_day: z.number().min(0).describe('Expected daily output token volume'),
|
|
1084
|
-
horizon: z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().describe('Primary horizon to highlight (default monthly). All four are always computed.'),
|
|
1085
|
-
}, async ({ models, input_tokens_per_day, output_tokens_per_day, horizon }) => {
|
|
1086
|
-
const params = new URLSearchParams({
|
|
1087
|
-
model: models,
|
|
1088
|
-
input_tokens_per_day: String(input_tokens_per_day),
|
|
1089
|
-
output_tokens_per_day: String(output_tokens_per_day),
|
|
1090
|
-
});
|
|
1091
|
-
if (horizon)
|
|
1092
|
-
params.set('horizon', horizon);
|
|
1093
|
-
const data = (await fetchJSON(`/premium/cost/projection?${params}`, { auth: true }));
|
|
1094
|
-
const lines = data.projections.map(p => {
|
|
1095
|
-
if (!p.matched)
|
|
1096
|
-
return ` ${p.model}: not found in pricing catalog`;
|
|
1097
|
-
return ` ${p.model} (${p.provider}): $${p.daily.total}/day, $${p.weekly_total}/wk, $${p.monthly_total}/mo, $${p.yearly_total}/yr (in $${p.rates.input_per_1m}/1M, out $${p.rates.output_per_1m}/1M)`;
|
|
1098
|
-
}).join('\n');
|
|
1099
|
-
const ranked = data.ranked_cheapest_monthly.length
|
|
1100
|
-
? '\n\nCheapest monthly:\n' +
|
|
1101
|
-
data.ranked_cheapest_monthly
|
|
1102
|
-
.map((r, i) => ` #${i + 1} ${r.model} (${r.provider}): $${r.monthly_total}/mo`)
|
|
1103
|
-
.join('\n')
|
|
1104
|
-
: '';
|
|
1105
|
-
return {
|
|
1106
|
-
content: [
|
|
1107
|
-
{
|
|
1108
|
-
type: 'text',
|
|
1109
|
-
text: `Workload: ${data.workload.input_tokens_per_day} input + ${data.workload.output_tokens_per_day} output tokens/day\n\n${lines}${ranked}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1110
|
-
},
|
|
1111
|
-
],
|
|
1112
|
-
};
|
|
1113
|
-
});
|
|
1114
|
-
// ── Tool: news_search (1 credit) ────────────────────────────────────
|
|
1115
|
-
registerTool('news_search', 'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit ($0.02).', {
|
|
1116
|
-
q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
|
|
1117
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
|
|
1118
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
|
|
1119
|
-
provider: z.string().optional().describe('Substring match against source name and domain (e.g. "anthropic", "openai", "techcrunch")'),
|
|
1120
|
-
category: z.string().optional().describe('Substring match against article categories'),
|
|
1121
|
-
limit: z.number().min(1).max(100).optional().describe('Max results (default 25, max 100)'),
|
|
1122
|
-
}, async ({ q, from, to, provider, category, limit }) => {
|
|
1123
|
-
const params = new URLSearchParams();
|
|
1124
|
-
if (q)
|
|
1125
|
-
params.set('q', q);
|
|
1126
|
-
if (from)
|
|
1127
|
-
params.set('from', from);
|
|
1128
|
-
if (to)
|
|
1129
|
-
params.set('to', to);
|
|
1130
|
-
if (provider)
|
|
1131
|
-
params.set('provider', provider);
|
|
1132
|
-
if (category)
|
|
1133
|
-
params.set('category', category);
|
|
1134
|
-
if (typeof limit === 'number')
|
|
1135
|
-
params.set('limit', String(limit));
|
|
1136
|
-
const data = (await fetchJSON(`/premium/news/search?${params}`, { auth: true }));
|
|
1137
|
-
if (data.results.length === 0) {
|
|
1138
|
-
return { content: [{ type: 'text', text: `No articles matched. (matched: ${data.matched})` }] };
|
|
1139
|
-
}
|
|
1140
|
-
const list = data.results
|
|
1141
|
-
.map((r, i) => `${i + 1}. ${r.title} (${r.source})\n ${r.url}\n ${r.published_at} | relevance ${r.relevance}${r.matched_terms.length ? ` | terms: ${r.matched_terms.join(', ')}` : ''}\n ${r.snippet}`)
|
|
1142
|
-
.join('\n\n');
|
|
1143
|
-
return {
|
|
1144
|
-
content: [
|
|
1145
|
-
{
|
|
1146
|
-
type: 'text',
|
|
1147
|
-
text: `${data.returned} of ${data.matched} matches${data.query ? ` for "${data.query}"` : ''}:\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1148
|
-
},
|
|
1149
|
-
],
|
|
1150
|
-
};
|
|
1151
|
-
});
|
|
1152
1083
|
// ── Tool: list_watches ──────────────────────────────────────────────
|
|
1153
1084
|
registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
|
|
1154
1085
|
const data = (await fetchJSON('/premium/watches', { auth: true }));
|
|
@@ -1165,924 +1096,144 @@ registerTool('list_watches', 'List the active webhook watches owned by the confi
|
|
|
1165
1096
|
.join('\n\n');
|
|
1166
1097
|
return { content: [{ type: 'text', text: `${data.count} active watches:\n\n${rows}` }] };
|
|
1167
1098
|
});
|
|
1168
|
-
// ── Tool:
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const body = {
|
|
1202
|
-
spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
|
|
1203
|
-
callback_url,
|
|
1204
|
-
};
|
|
1205
|
-
if (secret !== undefined)
|
|
1206
|
-
body.secret = secret;
|
|
1207
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1208
|
-
return {
|
|
1209
|
-
content: [
|
|
1210
|
-
{
|
|
1211
|
-
type: 'text',
|
|
1212
|
-
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1213
|
-
},
|
|
1214
|
-
],
|
|
1215
|
-
};
|
|
1216
|
-
}, CREATE_TOOL);
|
|
1217
|
-
// ── Tool: create_digest_watch (1 credit) ────────────────────────────
|
|
1218
|
-
registerTool('create_digest_watch', 'Register a scheduled digest webhook that fires daily or weekly with a curated summary of pricing changes (regardless of whether anything dramatic happened). Costs 1 credit ($0.02). Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.', {
|
|
1219
|
-
cadence: z.enum(['daily', 'weekly']).describe('How often the digest fires'),
|
|
1220
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the digest fires'),
|
|
1221
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
1222
|
-
}, async ({ cadence, callback_url, secret }) => {
|
|
1223
|
-
const body = {
|
|
1224
|
-
spec: { type: 'digest', cadence },
|
|
1225
|
-
callback_url,
|
|
1226
|
-
};
|
|
1227
|
-
if (secret !== undefined)
|
|
1228
|
-
body.secret = secret;
|
|
1229
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1230
|
-
return {
|
|
1231
|
-
content: [
|
|
1232
|
-
{
|
|
1233
|
-
type: 'text',
|
|
1234
|
-
text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1235
|
-
},
|
|
1236
|
-
],
|
|
1237
|
-
};
|
|
1238
|
-
}, CREATE_TOOL);
|
|
1239
|
-
// ── Tool: create_leaderboard_rank_watch (1 credit) ──────────────────
|
|
1240
|
-
registerTool('create_leaderboard_rank_watch', 'Register a webhook that fires when a provider crosses a rank threshold on the cross-provider 7-day uptime leaderboard. Rank 1 = best (highest uptime). drops_below: was rank<=N, now rank>N (got worse). rises_above: was rank>=N, now rank<N (got better). changes: any rank movement. Costs 1 credit ($0.02) at registration. Watch lives 90 days.', {
|
|
1241
|
-
provider: z.string().describe('Provider name or slug (case-insensitive). e.g. claude, openai, gemini, bedrock, azure'),
|
|
1242
|
-
op: z.enum(['drops_below', 'rises_above', 'changes']).describe('Trigger condition'),
|
|
1243
|
-
threshold: z.number().int().min(1).optional().describe('Required for drops_below / rises_above. Integer rank position.'),
|
|
1244
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
|
|
1245
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
1246
|
-
}, async ({ provider, op, threshold, callback_url, secret }) => {
|
|
1247
|
-
const spec = { type: 'leaderboard_rank', provider, op };
|
|
1248
|
-
if (threshold !== undefined)
|
|
1249
|
-
spec.threshold = threshold;
|
|
1250
|
-
const body = { spec, callback_url };
|
|
1251
|
-
if (secret !== undefined)
|
|
1252
|
-
body.secret = secret;
|
|
1253
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1254
|
-
const desc = op === 'changes'
|
|
1255
|
-
? `${provider} rank changes`
|
|
1256
|
-
: `${provider} ${op.replace('_', ' ')} #${threshold}`;
|
|
1257
|
-
return {
|
|
1258
|
-
content: [
|
|
1259
|
-
{
|
|
1260
|
-
type: 'text',
|
|
1261
|
-
text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1262
|
-
},
|
|
1263
|
-
],
|
|
1264
|
-
};
|
|
1265
|
-
}, CREATE_TOOL);
|
|
1266
|
-
// ── Tool: delete_watch ──────────────────────────────────────────────
|
|
1267
|
-
registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
|
|
1268
|
-
watch_id: z
|
|
1269
|
-
.string()
|
|
1270
|
-
.regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
|
|
1271
|
-
.max(128)
|
|
1272
|
-
.describe('The wat_... id from create_price_watch / create_status_watch / list_watches'),
|
|
1273
|
-
}, async ({ watch_id }) => {
|
|
1274
|
-
await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
|
|
1275
|
-
return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
|
|
1276
|
-
}, DELETE_TOOL);
|
|
1277
|
-
// ── Tool: probe_latest (free) ───────────────────────────────────────
|
|
1278
|
-
registerTool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and availability per provider (Anthropic, OpenAI, Google, Mistral, Cohere). TensorFeed pings each provider\'s chat completion endpoint every 15 min and records time-to-first-byte, total response time, and HTTP status. Returns per-provider success rate and ttfb/total p50/p95/p99 latency. The data is unique because we measure it ourselves, not self-reported by the providers. Useful when an agent needs to pick a provider whose SLA you can verify, or to detect ongoing incidents before they hit a status page. Free, no auth.', {}, async () => {
|
|
1279
|
-
const data = (await fetchJSON('/probe/latest'));
|
|
1280
|
-
const s = data.summary;
|
|
1281
|
-
if (s.providers.length === 0) {
|
|
1282
|
-
return { content: [{ type: 'text', text: 'No probe data yet. Probing starts as soon as TensorFeed has at least one provider key configured.' }] };
|
|
1283
|
-
}
|
|
1284
|
-
const rows = s.providers
|
|
1285
|
-
.sort((a, b) => b.ok_pct - a.ok_pct)
|
|
1286
|
-
.map(p => {
|
|
1287
|
-
const okPct = (p.ok_pct * 100).toFixed(1);
|
|
1288
|
-
const ttfb = p.ttfb.p50 !== null ? `p50 ${p.ttfb.p50}ms / p95 ${p.ttfb.p95}ms / p99 ${p.ttfb.p99}ms` : 'no successful probes';
|
|
1289
|
-
const total = p.total.p50 !== null ? `p50 ${p.total.p50}ms / p95 ${p.total.p95}ms / p99 ${p.total.p99}ms` : 'no successful probes';
|
|
1290
|
-
const err = p.last_error ? `\n last error: ${p.last_error}` : '';
|
|
1291
|
-
return ` ${p.provider}: ${okPct}% ok over ${p.count} probes\n ttfb: ${ttfb}\n total: ${total}${err}`;
|
|
1292
|
-
})
|
|
1293
|
-
.join('\n\n');
|
|
1294
|
-
return {
|
|
1295
|
-
content: [
|
|
1296
|
-
{
|
|
1297
|
-
type: 'text',
|
|
1298
|
-
text: `LLM Endpoint SLA (${s.window_label}, computed ${s.computed_at})\n\n${rows}\n\nSource: TensorFeed measured probes, not self-reported.`,
|
|
1299
|
-
},
|
|
1300
|
-
],
|
|
1301
|
-
};
|
|
1302
|
-
});
|
|
1303
|
-
// ── Tool: probe_series (1 credit) ───────────────────────────────────
|
|
1304
|
-
registerTool('probe_series', 'Daily SLA series for one LLM provider, measured by TensorFeed. Returns per-day count, success rate, ttfb p50/p95/p99, total p50/p95/p99, and incident-hour count across the requested window. Provider status pages are politically managed; this is the measured truth. Pairs naturally with premium_routing for picking a model whose SLA you can verify. Costs 3 credits ($0.06). Strict premium, no free trial.', {
|
|
1305
|
-
provider: z.enum(['anthropic', 'openai', 'google', 'mistral', 'cohere']).describe('LLM provider key'),
|
|
1306
|
-
from: z.string().optional().describe('Inclusive start YYYY-MM-DD (default: 30 days before to)'),
|
|
1307
|
-
to: z.string().optional().describe('Inclusive end YYYY-MM-DD (default: today UTC)'),
|
|
1308
|
-
}, async ({ provider, from, to }) => {
|
|
1309
|
-
const params = new URLSearchParams();
|
|
1310
|
-
params.set('provider', provider);
|
|
1311
|
-
if (from)
|
|
1312
|
-
params.set('from', from);
|
|
1313
|
-
if (to)
|
|
1314
|
-
params.set('to', to);
|
|
1315
|
-
const data = (await fetchJSON(`/premium/probe/series?${params}`, { auth: true }));
|
|
1316
|
-
const lines = data.points
|
|
1317
|
-
.map(p => p.has_data
|
|
1318
|
-
? ` ${p.date} ${(p.ok_pct * 100).toFixed(1)}% ok ttfb p50/p95: ${p.ttfb_p50}/${p.ttfb_p95}ms total p50/p95: ${p.total_p50}/${p.total_p95}ms incidents: ${p.incident_hours}h`
|
|
1319
|
-
: ` ${p.date} (no data)`)
|
|
1320
|
-
.join('\n');
|
|
1321
|
-
const overall = data.summary.overall_uptime_pct !== null
|
|
1322
|
-
? `${(data.summary.overall_uptime_pct * 100).toFixed(2)}%`
|
|
1323
|
-
: 'insufficient data';
|
|
1324
|
-
const notes = data.notes.length ? '\nNotes: ' + data.notes.join('; ') : '';
|
|
1325
|
-
return {
|
|
1326
|
-
content: [
|
|
1327
|
-
{
|
|
1328
|
-
type: 'text',
|
|
1329
|
-
text: `${data.provider} measured SLA, ${data.from} -> ${data.to}\nOverall uptime: ${overall} (${data.summary.days_with_data} days with data, ${data.summary.days_with_incidents} with incidents)\n\n${lines}${notes}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1330
|
-
},
|
|
1331
|
-
],
|
|
1332
|
-
};
|
|
1333
|
-
});
|
|
1334
|
-
// ── Tool: mcp_registry_snapshot (free) ──────────────────────────────
|
|
1335
|
-
registerTool('mcp_registry_snapshot', 'Today\'s summary of the official Model Context Protocol server registry. Returns total servers, by-status breakdown, top namespaces, and 1-day deltas (newly added, reactivated, deprecated). Captured daily at 9:30 AM UTC from registry.modelcontextprotocol.io. Useful when an agent wants to see how the MCP ecosystem is growing or detect freshly-deprecated servers it may be using. Free, no auth.', {}, async () => {
|
|
1336
|
-
const data = (await fetchJSON('/mcp/registry/snapshot'));
|
|
1337
|
-
const s = data.summary;
|
|
1338
|
-
const statusLine = Object.entries(s.by_status)
|
|
1339
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
1340
|
-
.join(', ');
|
|
1341
|
-
const topNs = s.top_namespaces.slice(0, 10).map(n => ` ${n.namespace}: ${n.count}`).join('\n');
|
|
1342
|
-
const delta = s.delta_vs_yesterday
|
|
1343
|
-
? `Day over day: +${s.delta_vs_yesterday.added} new, -${s.delta_vs_yesterday.removed} removed, net ${s.delta_vs_yesterday.net >= 0 ? '+' : ''}${s.delta_vs_yesterday.net}`
|
|
1344
|
-
: 'Day over day: not available (first snapshot)';
|
|
1345
|
-
const newToday = s.new_today.count > 0
|
|
1346
|
-
? `\nNewly listed today (${s.new_today.count}):\n` + s.new_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
|
|
1347
|
-
: '';
|
|
1348
|
-
const deprecatedToday = s.deprecated_today.count > 0
|
|
1349
|
-
? `\nDeprecated today (${s.deprecated_today.count}):\n` + s.deprecated_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
|
|
1350
|
-
: '';
|
|
1351
|
-
return {
|
|
1352
|
-
content: [
|
|
1353
|
-
{
|
|
1354
|
-
type: 'text',
|
|
1355
|
-
text: `MCP Server Registry, ${s.date}\n` +
|
|
1356
|
-
`Total servers: ${s.total_servers} (across ${s.total_versions} versioned entries)\n` +
|
|
1357
|
-
`Status: ${statusLine}\n${delta}\n\n` +
|
|
1358
|
-
`Top namespaces:\n${topNs}` +
|
|
1359
|
-
newToday + deprecatedToday +
|
|
1360
|
-
`\n\nSource: registry.modelcontextprotocol.io`,
|
|
1361
|
-
},
|
|
1362
|
-
],
|
|
1363
|
-
};
|
|
1364
|
-
});
|
|
1365
|
-
// ── Tool: get_ai_papers_trending (free) ─────────────────────────────
|
|
1366
|
-
registerTool('get_ai_papers_trending', 'Daily curated AI/ML research papers ranked by citation count. Sourced from Semantic Scholar across five fan-out queries (large language model, transformer, RLHF, AI agents, diffusion model), deduped by paperId, top 30 returned. Each paper carries title, abstract, authors, year, venue, citation count, arxivId, doi, and fields of study. Refreshed daily at 11:00 UTC. Use this when an agent wants the citation-ranked top of the AI research literature. Free, no auth.', {}, async () => {
|
|
1367
|
-
const data = (await fetchJSON('/papers/ai-trending'));
|
|
1368
|
-
const s = data.snapshot;
|
|
1369
|
-
const lines = s.papers.map((p, i) => {
|
|
1370
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
1371
|
-
const year = p.year ? ` (${p.year})` : '';
|
|
1372
|
-
const venue = p.venue ? ` ${p.venue}.` : '';
|
|
1373
|
-
const arxiv = p.arxivId ? ` arXiv:${p.arxivId}` : '';
|
|
1374
|
-
const link = p.url ? `\n ${p.url}` : '';
|
|
1375
|
-
return `${i + 1}. ${p.title}${year}\n ${authors}.${venue} Citations: ${p.citationCount}.${arxiv}${link}`;
|
|
1376
|
-
}).join('\n\n');
|
|
1377
|
-
const venues = s.summary.top_venues.slice(0, 5).map(v => `${v.venue} (${v.count})`).join(', ');
|
|
1378
|
-
return {
|
|
1379
|
-
content: [
|
|
1380
|
-
{
|
|
1381
|
-
type: 'text',
|
|
1382
|
-
text: `AI Papers, citation-ranked. ${s.date}, ${s.total_papers} papers.\n` +
|
|
1383
|
-
(venues ? `Top venues: ${venues}\n\n` : '\n') +
|
|
1384
|
-
lines +
|
|
1385
|
-
`\n\nSource: Semantic Scholar Graph API. Captured ${s.capturedAt}.`,
|
|
1386
|
-
},
|
|
1387
|
-
],
|
|
1388
|
-
};
|
|
1389
|
-
});
|
|
1390
|
-
// ── Tool: get_arxiv_recent (free) ───────────────────────────────────
|
|
1391
|
-
registerTool('get_arxiv_recent', 'Most recent arXiv submissions in cs.AI / cs.LG / cs.CL / cs.CV. Single Atom API call, deduped by arxivId, top 50 by submission date. The firehose pair to get_ai_papers_trending: this shows what just dropped, ai_papers_trending ranks by citation count. Each paper carries arxivId, title, abstract, authors, primary category, publication date, html and pdf URLs. Refreshed daily at 11:30 UTC. Free, no auth.', {
|
|
1392
|
-
limit: z.number().min(1).max(50).optional().describe('Max papers to include in the rendered list (default 25, max 50)'),
|
|
1393
|
-
}, async ({ limit }) => {
|
|
1394
|
-
const data = (await fetchJSON('/papers/arxiv-recent'));
|
|
1395
|
-
const s = data.snapshot;
|
|
1396
|
-
const cap = Math.min(limit ?? 25, s.papers.length);
|
|
1397
|
-
const lines = s.papers.slice(0, cap).map((p, i) => {
|
|
1398
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
1399
|
-
const cat = p.primaryCategory ? ` [${p.primaryCategory}]` : '';
|
|
1400
|
-
const date = p.publishedAt ? ` ${p.publishedAt.slice(0, 10)}` : '';
|
|
1401
|
-
return `${i + 1}. ${p.title}${cat}${date}\n ${authors}\n ${p.htmlUrl}`;
|
|
1402
|
-
}).join('\n\n');
|
|
1403
|
-
return {
|
|
1404
|
-
content: [
|
|
1405
|
-
{
|
|
1406
|
-
type: 'text',
|
|
1407
|
-
text: `arXiv recent submissions. ${s.date}, showing ${cap} of ${s.total_papers}.\n` +
|
|
1408
|
-
`Categories: ${s.categories_queried.join(', ')}\n\n` +
|
|
1409
|
-
lines +
|
|
1410
|
-
`\n\nSource: arxiv.org. Captured ${s.capturedAt}.`,
|
|
1411
|
-
},
|
|
1412
|
-
],
|
|
1413
|
-
};
|
|
1414
|
-
});
|
|
1415
|
-
// ── Tool: get_hf_trending (free) ────────────────────────────────────
|
|
1416
|
-
registerTool('get_hf_trending', 'Top Hugging Face models, datasets, and Spaces. Models + datasets ranked by downloads (top 30 each); Spaces ranked by likes (top 30, since downloads is meaningless for hosted apps). Captured daily at 12:00 UTC against the public HF API (no auth). Each model entry carries id, downloads, likes, pipeline_tag, tags, lastModified, private, gated. Each space entry carries id, author, sdk (gradio/streamlit/static/docker), likes, tags, lastModified, private, runtime_stage, hardware. Use this when an agent wants the current shape of the open model + dataset + app ecosystem. Once enough daily snapshots accumulate, day-over-day deltas become a real trending signal. Free, no auth.', {
|
|
1417
|
-
section: z.enum(['models', 'datasets', 'spaces', 'all', 'both']).optional().describe('Which to include in the rendered output (default all). "both" is accepted as alias for "models+datasets" for backward compat.'),
|
|
1418
|
-
limit: z.number().min(1).max(30).optional().describe('Max items per section (default 15, max 30)'),
|
|
1419
|
-
}, async ({ section, limit }) => {
|
|
1420
|
-
const data = (await fetchJSON('/hf/trending'));
|
|
1421
|
-
const s = data.snapshot;
|
|
1422
|
-
const sec = section ?? 'all';
|
|
1423
|
-
const n = Math.min(limit ?? 15, 30);
|
|
1424
|
-
const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M` : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
|
|
1425
|
-
const modelLines = s.models.items.slice(0, n).map((m, i) => {
|
|
1426
|
-
const tag = m.pipeline_tag ? ` [${m.pipeline_tag}]` : '';
|
|
1427
|
-
return `${i + 1}. ${m.id}${tag} ${fmtCount(m.downloads)} downloads, ${fmtCount(m.likes)} likes`;
|
|
1428
|
-
}).join('\n');
|
|
1429
|
-
const datasetLines = s.datasets.items.slice(0, n).map((d, i) => `${i + 1}. ${d.id} ${fmtCount(d.downloads)} downloads, ${fmtCount(d.likes)} likes`).join('\n');
|
|
1430
|
-
const spaceLines = (s.spaces?.items ?? []).slice(0, n).map((sp, i) => {
|
|
1431
|
-
const sdk = sp.sdk ? ` [${sp.sdk}]` : '';
|
|
1432
|
-
const hw = sp.hardware ? `, hw: ${sp.hardware}` : '';
|
|
1433
|
-
const stage = sp.runtime_stage ? `, ${sp.runtime_stage.toLowerCase()}` : '';
|
|
1434
|
-
return `${i + 1}. ${sp.id}${sdk} ${fmtCount(sp.likes)} likes${stage}${hw}`;
|
|
1435
|
-
}).join('\n');
|
|
1436
|
-
const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
|
|
1437
|
-
const namespaces = s.summary.top_namespaces.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
|
|
1438
|
-
const sdks = (s.summary.top_space_sdks ?? []).slice(0, 5).map(t => `${t.sdk} (${t.count})`).join(', ');
|
|
1439
|
-
let text = `Hugging Face top assets. ${s.date}.\n`;
|
|
1440
|
-
if (pipelines)
|
|
1441
|
-
text += `Top pipeline tags (models): ${pipelines}\n`;
|
|
1442
|
-
if (sdks)
|
|
1443
|
-
text += `Top Space SDKs: ${sdks}\n`;
|
|
1444
|
-
if (namespaces)
|
|
1445
|
-
text += `Top namespaces: ${namespaces}\n`;
|
|
1446
|
-
const includeModels = sec === 'models' || sec === 'all' || sec === 'both';
|
|
1447
|
-
const includeDatasets = sec === 'datasets' || sec === 'all' || sec === 'both';
|
|
1448
|
-
const includeSpaces = sec === 'spaces' || sec === 'all';
|
|
1449
|
-
if (includeModels)
|
|
1450
|
-
text += `\nModels (by downloads):\n${modelLines}\n`;
|
|
1451
|
-
if (includeDatasets)
|
|
1452
|
-
text += `\nDatasets (by downloads):\n${datasetLines}\n`;
|
|
1453
|
-
if (includeSpaces)
|
|
1454
|
-
text += `\nSpaces (by likes):\n${spaceLines || ' (none returned)'}\n`;
|
|
1455
|
-
text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
|
|
1456
|
-
return { content: [{ type: 'text', text }] };
|
|
1457
|
-
});
|
|
1458
|
-
// ── Tool: get_hot_issues (free) ─────────────────────────────────────
|
|
1459
|
-
registerTool('get_hot_issues', 'Currently-hot GitHub issues across the AI ecosystem. Five fan-out keyword-phrase searches ("large language model", "AI agent", transformer, "machine learning", LLM) for is:issue is:open archived:false comments>=10 updated within the last 7 days. Deduped by URL, top 30 by comment count. Each issue carries url, repo, number, title, author, state, comments, reactions_total, labels, created_at, updated_at, and matched_topic (the keyword phrase that found it). Refreshed daily at 12:30 UTC. Companion to get_trending_repos: that shows which AI repos are gaining stars, this shows where the active conversations are. Free, no auth.', {
|
|
1460
|
-
limit: z.number().min(1).max(30).optional().describe('Max issues to render (default 15, max 30)'),
|
|
1461
|
-
}, async ({ limit }) => {
|
|
1462
|
-
const data = (await fetchJSON('/issues/hot'));
|
|
1463
|
-
const s = data.snapshot;
|
|
1464
|
-
const cap = Math.min(limit ?? 15, s.issues.length);
|
|
1465
|
-
const lines = s.issues.slice(0, cap).map((i, n) => {
|
|
1466
|
-
const meta = `${i.comments} comments, ${i.reactions_total} reactions [${i.matched_topic}]`;
|
|
1467
|
-
const author = i.author ? ` by ${i.author}` : '';
|
|
1468
|
-
const updated = i.updated_at ? ` updated ${i.updated_at.slice(0, 10)}` : '';
|
|
1469
|
-
return `${n + 1}. ${i.repo}: ${i.title}\n ${meta}${updated}${author}\n ${i.url}`;
|
|
1470
|
-
}).join('\n\n');
|
|
1471
|
-
const repos = s.summary.top_repos.slice(0, 5).map(r => `${r.repo} (${r.count})`).join(', ');
|
|
1472
|
-
return {
|
|
1473
|
-
content: [
|
|
1474
|
-
{
|
|
1475
|
-
type: 'text',
|
|
1476
|
-
text: `Hot AI GitHub issues. ${s.date}, showing ${cap} of ${s.total_issues}.\n` +
|
|
1477
|
-
`Window: last ${s.recent_window_days} days, comments threshold ${s.comments_threshold}.\n` +
|
|
1478
|
-
(repos ? `Most-active repos: ${repos}\n\n` : '\n') +
|
|
1479
|
-
lines +
|
|
1480
|
-
`\n\nSource: GitHub Search API. Captured ${s.capturedAt}.`,
|
|
1481
|
-
},
|
|
1482
|
-
],
|
|
1483
|
-
};
|
|
1484
|
-
});
|
|
1485
|
-
// ── Tool: get_reddit_trending (free) ────────────────────────────────
|
|
1486
|
-
registerTool('get_reddit_trending', 'Currently-hot threads in 7 AI-relevant subreddits (LocalLLaMA, MachineLearning, ClaudeAI, OpenAI, singularity, artificial, AI_Agents). Stickied and NSFW posts filtered, deduped by post id, ranked by score, top 30. Each post carries id, subreddit, title, author, score, upvote_ratio, num_comments, permalink, url, created_utc, flair, is_self, is_video. Refreshed daily at 13:00 UTC. Companion to get_hot_issues: that surfaces developer conversation on GitHub, this surfaces community conversation on Reddit. Titles are sanitized at capture time against prompt-injection tokens. Free, no auth.', {
|
|
1487
|
-
subreddit: z.string().optional().describe('Optional filter to a single subreddit name (e.g. "LocalLLaMA"). Case-insensitive. If absent, returns posts from all 7.'),
|
|
1488
|
-
limit: z.number().min(1).max(30).optional().describe('Max posts to render (default 15, max 30)'),
|
|
1489
|
-
}, async ({ subreddit, limit }) => {
|
|
1490
|
-
const data = (await fetchJSON('/reddit/trending'));
|
|
1491
|
-
const s = data.snapshot;
|
|
1492
|
-
const subFilter = subreddit?.trim().toLowerCase();
|
|
1493
|
-
const filtered = subFilter
|
|
1494
|
-
? s.posts.filter(p => p.subreddit.toLowerCase() === subFilter)
|
|
1495
|
-
: s.posts;
|
|
1496
|
-
const cap = Math.min(limit ?? 15, filtered.length);
|
|
1497
|
-
const fmtCount = (x) => x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
|
|
1498
|
-
const lines = filtered.slice(0, cap).map((p, i) => {
|
|
1499
|
-
const author = p.author ? ` by ${p.author}` : '';
|
|
1500
|
-
const flair = p.flair ? ` [${p.flair}]` : '';
|
|
1501
|
-
return `${i + 1}. r/${p.subreddit}: ${p.title}${flair}\n ${fmtCount(p.score)} pts, ${fmtCount(p.num_comments)} comments${author}\n ${p.permalink}`;
|
|
1502
|
-
}).join('\n\n');
|
|
1503
|
-
const subs = Object.entries(s.summary.by_subreddit)
|
|
1504
|
-
.sort((a, b) => b[1] - a[1])
|
|
1505
|
-
.slice(0, 5)
|
|
1506
|
-
.map(([sub, n]) => `r/${sub} (${n})`)
|
|
1507
|
-
.join(', ');
|
|
1508
|
-
let text = `Hot AI Reddit threads. ${s.date}, showing ${cap} of ${filtered.length}`;
|
|
1509
|
-
if (subFilter)
|
|
1510
|
-
text += ` (filtered to r/${subreddit})`;
|
|
1511
|
-
text += '.\n';
|
|
1512
|
-
if (subs && !subFilter)
|
|
1513
|
-
text += `Most-active subreddits: ${subs}\n`;
|
|
1514
|
-
text += '\n' + (lines || ' (no posts)');
|
|
1515
|
-
text += `\n\nSource: reddit.com. Captured ${s.capturedAt}.`;
|
|
1516
|
-
return { content: [{ type: 'text', text }] };
|
|
1517
|
-
});
|
|
1518
|
-
// ── Tool: get_openrouter_models (free) ──────────────────────────────
|
|
1519
|
-
registerTool('get_openrouter_models', 'OpenRouter cross-provider model catalog (200+ models normalized across 50+ inference providers). Each entry has per-token pricing (prompt + completion + image + request), context window, modality (e.g. text+image->text), instruct_type, tokenizer, top provider metadata (max_completion_tokens, moderation flag), and supported_parameters. Use this to find the long tail of OSS models on cloud inference, including the cheapest model for a given workload, the model with the largest context window, or the free-tier set. Pairs with get_model_pricing (curated frontier-lab catalog). Refreshed daily at 14:00 UTC. Free, no auth.', {
|
|
1520
|
-
namespace: z.string().optional().describe('Optional filter to a single provider namespace (e.g. "anthropic", "openai", "meta-llama"). Case-insensitive.'),
|
|
1521
|
-
free_only: z.boolean().optional().describe('If true, return only models with prompt+completion both 0 (free-tier).'),
|
|
1522
|
-
cheapest: z.boolean().optional().describe('If true, sort by cheapest input price ascending. Free-tier models excluded from this sort.'),
|
|
1523
|
-
limit: z.number().min(1).max(50).optional().describe('Max models to render (default 20, max 50)'),
|
|
1524
|
-
}, async ({ namespace, free_only, cheapest, limit }) => {
|
|
1525
|
-
const data = (await fetchJSON('/openrouter/models'));
|
|
1526
|
-
const s = data.snapshot;
|
|
1527
|
-
const ns = namespace?.trim().toLowerCase();
|
|
1528
|
-
let list = s.models;
|
|
1529
|
-
if (ns)
|
|
1530
|
-
list = list.filter(m => m.id.toLowerCase().startsWith(ns + '/'));
|
|
1531
|
-
if (free_only)
|
|
1532
|
-
list = list.filter(m => m.pricing.prompt === 0 && m.pricing.completion === 0);
|
|
1533
|
-
if (cheapest) {
|
|
1534
|
-
list = list
|
|
1535
|
-
.filter(m => typeof m.pricing.prompt === 'number' && m.pricing.prompt > 0)
|
|
1536
|
-
.sort((a, b) => (a.pricing.prompt ?? 0) - (b.pricing.prompt ?? 0));
|
|
1537
|
-
}
|
|
1538
|
-
const cap = Math.min(limit ?? 20, list.length);
|
|
1539
|
-
const fmtPrice = (p) => p === null ? '?' : p === 0 ? 'free' : `$${(p * 1_000_000).toFixed(2)}/Mtok`;
|
|
1540
|
-
const fmtCtx = (c) => c === null ? 'unknown' : c >= 1_000_000 ? `${(c / 1_000_000).toFixed(0)}M` : `${(c / 1_000).toFixed(0)}K`;
|
|
1541
|
-
const lines = list.slice(0, cap).map((m, i) => {
|
|
1542
|
-
const modality = m.modality ? ` ${m.modality}` : '';
|
|
1543
|
-
return `${i + 1}. ${m.id}${modality}\n in ${fmtPrice(m.pricing.prompt)}, out ${fmtPrice(m.pricing.completion)}, ctx ${fmtCtx(m.context_length)}`;
|
|
1544
|
-
}).join('\n\n');
|
|
1545
|
-
const namespaces = s.summary.by_namespace.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
|
|
1546
|
-
let header = `OpenRouter catalog. ${s.date}. ${s.total_models} models total, ${list.length} after filters, showing ${cap}.\n`;
|
|
1547
|
-
header += `Top provider namespaces: ${namespaces}\n`;
|
|
1548
|
-
if (s.summary.cheapest_input)
|
|
1549
|
-
header += `Cheapest input (paid): ${s.summary.cheapest_input.id} at $${s.summary.cheapest_input.usd_per_million.toFixed(2)}/Mtok\n`;
|
|
1550
|
-
if (s.summary.largest_context)
|
|
1551
|
-
header += `Largest context: ${s.summary.largest_context.id} at ${fmtCtx(s.summary.largest_context.tokens)} tokens\n`;
|
|
1552
|
-
header += `Free-tier models available: ${s.summary.free_tier_count}\n`;
|
|
1553
|
-
return {
|
|
1554
|
-
content: [
|
|
1555
|
-
{
|
|
1556
|
-
type: 'text',
|
|
1557
|
-
text: header + '\n' + (lines || ' (no models match the filters)') + `\n\nSource: openrouter.ai. Captured ${s.capturedAt}.`,
|
|
1558
|
-
},
|
|
1559
|
-
],
|
|
1560
|
-
};
|
|
1561
|
-
});
|
|
1562
|
-
// ── Tool: get_hf_daily_papers (free) ────────────────────────────────
|
|
1563
|
-
registerTool('get_hf_daily_papers', 'Hugging Face Daily Papers: editor-curated AI/ML papers from huggingface.co/papers, layered with community upvotes and discussion counts. Different signal from get_arxiv_recent (firehose) and get_ai_papers_trending (citation-ranked all-time): this is editor picks of-the-day with engagement signal. Each paper carries paperId, title, summary, authors, upvotes, num_comments, hf_url, arxiv_url (when applicable), github_repo, github_stars, ai_keywords. Refreshed daily at 14:15 UTC. Titles sanitized at capture time. Free, no auth.', {
|
|
1564
|
-
limit: z.number().min(1).max(30).optional().describe('Max papers to render (default 15, max 30)'),
|
|
1565
|
-
}, async ({ limit }) => {
|
|
1566
|
-
const data = (await fetchJSON('/papers/hf-daily'));
|
|
1567
|
-
const s = data.snapshot;
|
|
1568
|
-
const cap = Math.min(limit ?? 15, s.papers.length);
|
|
1569
|
-
const lines = s.papers.slice(0, cap).map((p, i) => {
|
|
1570
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
1571
|
-
const kw = p.ai_keywords.length > 0 ? `\n keywords: ${p.ai_keywords.slice(0, 5).join(', ')}` : '';
|
|
1572
|
-
const arxiv = p.arxiv_url ? `\n arxiv: ${p.arxiv_url}` : '';
|
|
1573
|
-
return `${i + 1}. ${p.title}\n ${authors}\n ${p.upvotes} upvotes, ${p.num_comments} comments\n ${p.hf_url}${arxiv}${kw}`;
|
|
1574
|
-
}).join('\n\n');
|
|
1575
|
-
const kw = s.summary.by_keyword.slice(0, 5).map(k => `${k.keyword} (${k.count})`).join(', ');
|
|
1576
|
-
let text = `HF Daily Papers (editor-curated). ${s.date}, showing ${cap} of ${s.total_papers}.\n`;
|
|
1577
|
-
if (kw)
|
|
1578
|
-
text += `Top keywords: ${kw}\n`;
|
|
1579
|
-
if (s.summary.most_upvoted)
|
|
1580
|
-
text += `Most upvoted: "${s.summary.most_upvoted.title}" (${s.summary.most_upvoted.upvotes})\n`;
|
|
1581
|
-
text += '\n' + lines + `\n\nSource: huggingface.co/papers. Captured ${s.capturedAt}.`;
|
|
1582
|
-
return { content: [{ type: 'text', text }] };
|
|
1583
|
-
});
|
|
1584
|
-
// ── Tool: get_ai_ecosystem_today (free, composite) ──────────────────
|
|
1585
|
-
// ── Tool: get_recent_earthquakes (free) ─────────────────────────────
|
|
1586
|
-
registerTool('get_recent_earthquakes', "Recent earthquakes from the USGS Earthquake Hazards Program pre-built summary feeds. Choose a magnitude bucket (significant | 4.5 | 2.5 | 1.0 | all) and a time window (hour | day | week | month). Returns id, magnitude, place, time (ISO 8601), depth_km, longitude, latitude, tsunami flag, USGS detail URL. Upstream feeds refresh every minute. License: US Government public domain (17 USC §105). Free, no auth.", {
|
|
1587
|
-
magnitude: z.enum(['significant', '4.5', '2.5', '1.0', 'all']).optional().describe('Magnitude bucket (default 4.5)'),
|
|
1588
|
-
period: z.enum(['hour', 'day', 'week', 'month']).optional().describe('Time window (default day)'),
|
|
1589
|
-
limit: z.number().min(1).max(50).optional().describe('Max earthquakes to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
|
|
1590
|
-
}, async ({ magnitude, period, limit }) => {
|
|
1591
|
-
const params = new URLSearchParams();
|
|
1592
|
-
params.set('magnitude', magnitude ?? '4.5');
|
|
1593
|
-
params.set('period', period ?? 'day');
|
|
1594
|
-
params.set('limit', String(Math.min(limit ?? 15, 50)));
|
|
1595
|
-
const data = (await fetchJSON(`/climate/earthquakes?${params}`));
|
|
1596
|
-
if (!data.ok || !data.earthquakes) {
|
|
1597
|
-
return { content: [{ type: 'text', text: `USGS earthquakes unavailable: ${data.error ?? 'unknown error'}` }] };
|
|
1598
|
-
}
|
|
1599
|
-
const eqs = data.earthquakes;
|
|
1600
|
-
const lines = eqs.map((q, i) => {
|
|
1601
|
-
const mag = q.magnitude !== null ? `M${q.magnitude.toFixed(1)}` : 'M?';
|
|
1602
|
-
const tsu = q.tsunami ? ' [tsunami flag]' : '';
|
|
1603
|
-
const time = q.time ? q.time.slice(0, 19).replace('T', ' ') + ' UTC' : 'unknown time';
|
|
1604
|
-
const depth = q.depth_km !== null ? `${q.depth_km.toFixed(0)}km deep` : 'depth unknown';
|
|
1605
|
-
const coords = q.latitude !== null && q.longitude !== null ? ` (${q.latitude.toFixed(2)}, ${q.longitude.toFixed(2)})` : '';
|
|
1606
|
-
return `${i + 1}. ${mag} ${q.place ?? 'location unknown'}${coords}\n ${time}, ${depth}${tsu}\n ${q.url ?? ''}`;
|
|
1607
|
-
}).join('\n\n');
|
|
1608
|
-
const meta = data.feed_metadata;
|
|
1609
|
-
const header = `USGS earthquakes (${data.query?.magnitude ?? '?'} / ${data.query?.period ?? '?'}). ${eqs.length} of ${meta?.upstream_count ?? '?'} returned.`;
|
|
1610
|
-
return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no events in this window)'}\n\nSource: USGS Earthquake Hazards Program. Public domain (17 USC §105).` }] };
|
|
1611
|
-
});
|
|
1612
|
-
// ── Tool: get_weather_alerts (free, US only) ────────────────────────
|
|
1613
|
-
registerTool('get_weather_alerts', "Active US weather alerts from the National Weather Service. US-only. Filter by 2-letter state code (area), exact NWS event name, severity, urgency, status. Returns id, event, severity, urgency, headline, description, areaDesc, sent/effective/expires/ends, sender_name, web URL. Active alerts only. Expired alerts fall off the upstream feed automatically. License: US Government public domain. Free, no auth.", {
|
|
1614
|
-
area: z.string().regex(/^[A-Za-z]{2}$/).optional().describe('2-letter US state or territory code (CA, TX, PR, etc)'),
|
|
1615
|
-
event: z.string().optional().describe('Exact NWS event name (e.g. "Tornado Warning", "Heat Advisory")'),
|
|
1616
|
-
severity: z.enum(['Extreme', 'Severe', 'Moderate', 'Minor', 'Unknown']).optional().describe('Severity filter'),
|
|
1617
|
-
urgency: z.enum(['Immediate', 'Expected', 'Future', 'Past', 'Unknown']).optional().describe('Urgency filter'),
|
|
1618
|
-
status: z.enum(['actual', 'exercise', 'system', 'test', 'draft']).optional().describe('Status filter (default actual unfiltered)'),
|
|
1619
|
-
limit: z.number().min(1).max(50).optional().describe('Max alerts to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
|
|
1620
|
-
}, async ({ area, event, severity, urgency, status, limit }) => {
|
|
1621
|
-
const params = new URLSearchParams();
|
|
1622
|
-
if (area)
|
|
1623
|
-
params.set('area', area.toUpperCase());
|
|
1624
|
-
if (event)
|
|
1625
|
-
params.set('event', event);
|
|
1626
|
-
if (severity)
|
|
1627
|
-
params.set('severity', severity);
|
|
1628
|
-
if (urgency)
|
|
1629
|
-
params.set('urgency', urgency);
|
|
1630
|
-
if (status)
|
|
1631
|
-
params.set('status', status);
|
|
1632
|
-
params.set('limit', String(Math.min(limit ?? 15, 50)));
|
|
1633
|
-
const data = (await fetchJSON(`/climate/weather-alerts?${params}`));
|
|
1634
|
-
if (!data.ok || !data.alerts) {
|
|
1635
|
-
return { content: [{ type: 'text', text: `NWS weather alerts unavailable: ${data.error ?? 'unknown error'}` }] };
|
|
1636
|
-
}
|
|
1637
|
-
const alerts = data.alerts;
|
|
1638
|
-
const lines = alerts.map((a, i) => {
|
|
1639
|
-
const ev = a.event ?? 'unknown event';
|
|
1640
|
-
const sev = a.severity ? `[${a.severity}]` : '';
|
|
1641
|
-
const eff = a.effective ? a.effective.slice(0, 19).replace('T', ' ') + ' UTC' : '';
|
|
1642
|
-
const exp = a.expires ? a.expires.slice(0, 19).replace('T', ' ') + ' UTC' : 'no expiry';
|
|
1643
|
-
return `${i + 1}. ${sev} ${ev}\n Where: ${a.area_desc ?? 'unspecified'}\n ${a.headline ?? ''}\n Effective: ${eff} → ${exp}\n ${a.web ?? ''}`;
|
|
1644
|
-
}).join('\n\n');
|
|
1645
|
-
const filters = [];
|
|
1646
|
-
if (area)
|
|
1647
|
-
filters.push(`area=${area.toUpperCase()}`);
|
|
1648
|
-
if (event)
|
|
1649
|
-
filters.push(`event=${event}`);
|
|
1650
|
-
if (severity)
|
|
1651
|
-
filters.push(`severity=${severity}`);
|
|
1652
|
-
const filterText = filters.length > 0 ? ` (${filters.join(', ')})` : '';
|
|
1653
|
-
const header = `NWS active US weather alerts${filterText}. ${alerts.length} of ${data.feed_metadata?.upstream_count ?? '?'} returned.`;
|
|
1654
|
-
return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no active alerts matching filters)'}\n\nSource: api.weather.gov. Public domain (17 USC §105). US-only coverage.` }] };
|
|
1655
|
-
});
|
|
1656
|
-
// ── Tool: get_agent_opportunities (free) ────────────────────────────
|
|
1657
|
-
registerTool('get_agent_opportunities', "TensorFeed's daily scan of new repositories across the AI agent ecosystem (Anthropic, OpenAI, Microsoft, ModelContextProtocol, HuggingFace, LangChain, frontier-lab orgs) plus recent MCP, x402, and skill keyword sweeps. Eleven signals total, deduped + composite-scored (signal_weight × log10(stars+1) × recency decay) with per-signal MIN/MAX caps so smaller signals are never starved. Refreshed daily at 13:30 UTC. Useful for surfacing distribution targets, integration ideas, or a daily brief on what's launching across the agent space. Free, no auth.", {
|
|
1658
|
-
signal: z.enum([
|
|
1659
|
-
'anthropic-org', 'openai-org', 'microsoft-org', 'mcp-org', 'huggingface-org',
|
|
1660
|
-
'langchain-org', 'frontier-labs', 'mcp-keyword', 'x402-keyword', 'skill-keyword',
|
|
1661
|
-
'vertical-pattern',
|
|
1662
|
-
]).optional().describe('Filter to one signal source. Omit to get the cross-signal ranked top-N.'),
|
|
1663
|
-
limit: z.number().min(1).max(25).optional().describe('Max opportunities to render (default 10, max 25)'),
|
|
1664
|
-
}, async ({ signal, limit }) => {
|
|
1665
|
-
const data = (await fetchJSON('/agents/opportunities'));
|
|
1666
|
-
if (data.error) {
|
|
1667
|
-
return { content: [{ type: 'text', text: `Agent opportunities unavailable: ${data.error}` }] };
|
|
1668
|
-
}
|
|
1669
|
-
const all = data.opportunities ?? [];
|
|
1670
|
-
const filtered = signal ? all.filter(o => o.signal === signal) : all;
|
|
1671
|
-
const cap = Math.min(limit ?? 10, 25);
|
|
1672
|
-
const top = filtered.slice(0, cap);
|
|
1673
|
-
const lines = top.map((o, i) => {
|
|
1674
|
-
const updated = o.updated_at ? o.updated_at.slice(0, 10) : '';
|
|
1675
|
-
const desc = o.description ? `\n ${o.description.length > 140 ? o.description.slice(0, 140) + '...' : o.description}` : '';
|
|
1676
|
-
return `${i + 1}. [${o.signal}] ${o.full_name} ${o.stars.toLocaleString()}★, score ${o.composite_score.toFixed(1)}, updated ${updated}${desc}\n ${o.html_url}`;
|
|
1677
|
-
}).join('\n\n');
|
|
1678
|
-
const filt = signal ? ` filtered to ${signal}` : '';
|
|
1679
|
-
const header = `Agent ecosystem opportunities for ${data.date ?? 'today'}${filt}. ${top.length} of ${filtered.length} (total snapshot: ${data.total_opportunities ?? all.length}).`;
|
|
1680
|
-
const bySignal = data.summary?.by_signal ? Object.entries(data.summary.by_signal).map(([k, v]) => `${k}=${v}`).join(', ') : '';
|
|
1681
|
-
const sig = bySignal ? `\nBy signal: ${bySignal}` : '';
|
|
1682
|
-
return { content: [{ type: 'text', text: `${header}${sig}\n\n${lines || ' (no opportunities returned)'}\n\nSource: TensorFeed daily GitHub scan via the public Search API.` }] };
|
|
1683
|
-
});
|
|
1684
|
-
// Single fetch to /api/today which fans out worker-side. The previous
|
|
1685
|
-
// implementation made 9 separate fetches per agent invocation; now
|
|
1686
|
-
// the worker's edge cache absorbs the load and serves one cached
|
|
1687
|
-
// response to all concurrent agents. Output stays text-rendered
|
|
1688
|
-
// (MCP returns text content) but the wire shape is the structured
|
|
1689
|
-
// JSON the worker produces.
|
|
1690
|
-
registerTool('get_ai_ecosystem_today', 'One-shot composite AI morning brief. Calls /api/today which fans out across all daily TensorFeed feeds (news, 3 paper feeds, HF models/datasets/Spaces, hot GitHub issues, Reddit threads, OpenRouter catalog summary, provider status) and returns a single synthesized text response. Optional sections filter (any subset of news, papers, hf, community, inference, status) and limit_per_section (1-10, default 3). Captured-at timestamps from each underlying snapshot are surfaced so the agent knows recency. Free, no auth.', {
|
|
1691
|
-
sections: z.array(z.enum([
|
|
1692
|
-
'news', 'papers', 'hf', 'community', 'inference', 'status',
|
|
1693
|
-
])).optional().describe('Which subsections to include (default: all six). "papers" covers all 3 paper feeds; "hf" covers models + datasets + spaces; "community" covers hot GitHub issues + Reddit threads; "inference" is the OpenRouter catalog summary.'),
|
|
1694
|
-
limit_per_section: z.number().min(1).max(10).optional().describe('Max items per subsection (default 3, max 10). The output is intentionally terse so an agent can call this without burning context.'),
|
|
1695
|
-
}, async ({ sections, limit_per_section }) => {
|
|
1696
|
-
const params = new URLSearchParams();
|
|
1697
|
-
if (sections && sections.length > 0)
|
|
1698
|
-
params.set('sections', sections.join(','));
|
|
1699
|
-
if (typeof limit_per_section === 'number')
|
|
1700
|
-
params.set('limit', String(limit_per_section));
|
|
1701
|
-
const path = `/today${params.toString() ? `?${params.toString()}` : ''}`;
|
|
1702
|
-
let brief;
|
|
1703
|
-
try {
|
|
1704
|
-
brief = (await fetchJSON(path));
|
|
1099
|
+
// ── Tool: create_watch (1 credit) ───────────────────────────────────
|
|
1100
|
+
// Merges the four former create_*_watch tools (price, status, digest,
|
|
1101
|
+
// leaderboard_rank) into one type-selected tool. Each case below builds
|
|
1102
|
+
// the EXACT body and uses the EXACT output formatting the matching old
|
|
1103
|
+
// create_*_watch tool used, against the same /premium/watches endpoint.
|
|
1104
|
+
registerTool('create_watch', 'Register a webhook watch. type selects what to watch: "price" (a model price change), "status" (a service status transition), "digest" (a scheduled daily or weekly pricing summary), or "leaderboard_rank" (a provider crossing an uptime-rank threshold). Costs 1 credit ($0.02) at registration; the watch lives 90 days and each fire is an HMAC-signed POST to callback_url. Needs a TENSORFEED_TOKEN.', {
|
|
1105
|
+
type: z.enum(['price', 'status', 'digest', 'leaderboard_rank']).describe('What to watch.'),
|
|
1106
|
+
callback_url: z.string().describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
|
|
1107
|
+
secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
|
|
1108
|
+
model: z.string().optional().describe('type=price only. Model name (e.g. "Claude Opus 4.7").'),
|
|
1109
|
+
field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('type=price only. Which price field to watch.'),
|
|
1110
|
+
cadence: z.enum(['daily', 'weekly']).optional().describe('type=digest only. How often the digest fires.'),
|
|
1111
|
+
provider: z.string().optional().describe('type=status or type=leaderboard_rank only. Provider name or slug (case-insensitive). e.g. anthropic, openai, gemini, bedrock, azure.'),
|
|
1112
|
+
op: z
|
|
1113
|
+
.enum(['lt', 'gt', 'changes', 'becomes', 'drops_below', 'rises_above'])
|
|
1114
|
+
.optional()
|
|
1115
|
+
.describe('type=price uses lt/gt/changes (lt = below threshold, gt = above, changes = any change). type=status uses becomes/changes (becomes = transitions to a specific value; changes = any transition). type=leaderboard_rank uses drops_below/rises_above/changes (rank 1 = best; drops_below: was rank<=N now rank>N; rises_above: was rank>=N now rank<N; changes: any rank movement).'),
|
|
1116
|
+
threshold: z.number().optional().describe('type=price (when op is lt or gt) or type=leaderboard_rank (when op is drops_below or rises_above). Integer rank position for leaderboard_rank.'),
|
|
1117
|
+
value: z.enum(['operational', 'degraded', 'down']).optional().describe('type=status only. Required when op is becomes.'),
|
|
1118
|
+
}, async (args) => {
|
|
1119
|
+
const { type, callback_url, secret, model, field, op, threshold, cadence, provider, value } = args;
|
|
1120
|
+
// The merged schema makes every type-specific field optional (each is
|
|
1121
|
+
// required for only one type). Guard the required fields per type here so a
|
|
1122
|
+
// missing one fails with a clear message instead of an opaque 4xx from the
|
|
1123
|
+
// paid /premium/watches endpoint (which would otherwise burn a request).
|
|
1124
|
+
const missingFields = [];
|
|
1125
|
+
if (type === 'price') {
|
|
1126
|
+
if (!model)
|
|
1127
|
+
missingFields.push('model');
|
|
1128
|
+
if (!field)
|
|
1129
|
+
missingFields.push('field');
|
|
1130
|
+
if (!op)
|
|
1131
|
+
missingFields.push('op');
|
|
1705
1132
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1133
|
+
else if (type === 'status') {
|
|
1134
|
+
if (!provider)
|
|
1135
|
+
missingFields.push('provider');
|
|
1136
|
+
if (!op)
|
|
1137
|
+
missingFields.push('op');
|
|
1710
1138
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
const sectionTexts = [];
|
|
1715
|
-
if (brief.news.available && brief.news.data && brief.news.data.items.length > 0) {
|
|
1716
|
-
const lines = brief.news.data.items.map((a, i) => ` ${i + 1}. ${a.title} (${a.source})\n ${a.url}`).join('\n');
|
|
1717
|
-
sectionTexts.push(`📰 NEWS\n${lines}`);
|
|
1139
|
+
else if (type === 'digest') {
|
|
1140
|
+
if (!cadence)
|
|
1141
|
+
missingFields.push('cadence');
|
|
1718
1142
|
}
|
|
1719
|
-
if (
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
if (
|
|
1723
|
-
|
|
1724
|
-
const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
|
|
1725
|
-
const venue = p.venue ? ` ${p.venue}.` : '';
|
|
1726
|
-
return ` ${i + 1}. ${p.title}${venue} ${authors}. ${fmtCount(p.citationCount)} citations`;
|
|
1727
|
-
}).join('\n');
|
|
1728
|
-
bits.push(` By citation count (Semantic Scholar):\n${lines}`);
|
|
1729
|
-
}
|
|
1730
|
-
const ax = brief.papers.data.arxiv_recent;
|
|
1731
|
-
if (ax.available && ax.data && ax.data.items.length > 0) {
|
|
1732
|
-
const lines = ax.data.items.map((p, i) => {
|
|
1733
|
-
const date = p.publishedAt ? p.publishedAt.slice(0, 10) : '';
|
|
1734
|
-
const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
|
|
1735
|
-
return ` ${i + 1}. ${p.title} (arXiv:${p.arxivId}, ${date}). ${authors}`;
|
|
1736
|
-
}).join('\n');
|
|
1737
|
-
bits.push(` Just submitted (arXiv recent):\n${lines}`);
|
|
1738
|
-
}
|
|
1739
|
-
const hd = brief.papers.data.hf_daily;
|
|
1740
|
-
if (hd.available && hd.data && hd.data.items.length > 0) {
|
|
1741
|
-
const lines = hd.data.items.map((p, i) => ` ${i + 1}. ${p.title} (${p.upvotes} upvotes, ${p.num_comments} comments)`).join('\n');
|
|
1742
|
-
bits.push(` HF editor picks today:\n${lines}`);
|
|
1743
|
-
}
|
|
1744
|
-
if (bits.length > 0)
|
|
1745
|
-
sectionTexts.push(`📄 PAPERS\n${bits.join('\n')}`);
|
|
1143
|
+
else if (type === 'leaderboard_rank') {
|
|
1144
|
+
if (!provider)
|
|
1145
|
+
missingFields.push('provider');
|
|
1146
|
+
if (!op)
|
|
1147
|
+
missingFields.push('op');
|
|
1746
1148
|
}
|
|
1747
|
-
if (
|
|
1748
|
-
|
|
1749
|
-
const { models, datasets, spaces } = brief.hf.data;
|
|
1750
|
-
if (models.length > 0) {
|
|
1751
|
-
bits.push(` Models (top by downloads):\n` + models.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
|
|
1752
|
-
}
|
|
1753
|
-
if (datasets.length > 0) {
|
|
1754
|
-
bits.push(` Datasets (top by downloads):\n` + datasets.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
|
|
1755
|
-
}
|
|
1756
|
-
if (spaces.length > 0) {
|
|
1757
|
-
bits.push(` Spaces (top by likes):\n` + spaces.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.likes)} likes)`).join('\n'));
|
|
1758
|
-
}
|
|
1759
|
-
if (bits.length > 0)
|
|
1760
|
-
sectionTexts.push(`🤗 HUGGING FACE\n${bits.join('\n')}`);
|
|
1149
|
+
if (missingFields.length > 0) {
|
|
1150
|
+
return { content: [{ type: 'text', text: `type=${type} requires: ${missingFields.join(', ')}.` }] };
|
|
1761
1151
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1152
|
+
switch (type) {
|
|
1153
|
+
case 'price': {
|
|
1154
|
+
const body = {
|
|
1155
|
+
spec: { type: 'price', model, field, op, ...(typeof threshold === 'number' ? { threshold } : {}) },
|
|
1156
|
+
callback_url,
|
|
1157
|
+
};
|
|
1158
|
+
if (secret !== undefined)
|
|
1159
|
+
body.secret = secret;
|
|
1160
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1161
|
+
return {
|
|
1162
|
+
content: [
|
|
1163
|
+
{
|
|
1164
|
+
type: 'text',
|
|
1165
|
+
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
};
|
|
1772
1169
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1170
|
+
case 'status': {
|
|
1171
|
+
const body = {
|
|
1172
|
+
spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
|
|
1173
|
+
callback_url,
|
|
1174
|
+
};
|
|
1175
|
+
if (secret !== undefined)
|
|
1176
|
+
body.secret = secret;
|
|
1177
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1178
|
+
return {
|
|
1179
|
+
content: [
|
|
1180
|
+
{
|
|
1181
|
+
type: 'text',
|
|
1182
|
+
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
};
|
|
1787
1186
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1187
|
+
case 'digest': {
|
|
1188
|
+
const body = {
|
|
1189
|
+
spec: { type: 'digest', cadence },
|
|
1190
|
+
callback_url,
|
|
1191
|
+
};
|
|
1192
|
+
if (secret !== undefined)
|
|
1193
|
+
body.secret = secret;
|
|
1194
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1195
|
+
return {
|
|
1196
|
+
content: [
|
|
1197
|
+
{
|
|
1198
|
+
type: 'text',
|
|
1199
|
+
text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
};
|
|
1794
1203
|
}
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1204
|
+
case 'leaderboard_rank': {
|
|
1205
|
+
const spec = { type: 'leaderboard_rank', provider, op };
|
|
1206
|
+
if (threshold !== undefined)
|
|
1207
|
+
spec.threshold = threshold;
|
|
1208
|
+
const body = { spec, callback_url };
|
|
1209
|
+
if (secret !== undefined)
|
|
1210
|
+
body.secret = secret;
|
|
1211
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1212
|
+
const desc = op === 'changes'
|
|
1213
|
+
? `${provider} rank changes`
|
|
1214
|
+
: `${provider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
|
|
1215
|
+
return {
|
|
1216
|
+
content: [
|
|
1217
|
+
{
|
|
1218
|
+
type: 'text',
|
|
1219
|
+
text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
};
|
|
1798
1223
|
}
|
|
1799
1224
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
type: 'text',
|
|
1805
|
-
text: 'No data available yet. The daily snapshots have not run, or all upstream feeds errored. Try again in a few minutes.',
|
|
1806
|
-
},
|
|
1807
|
-
],
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
const header = `TensorFeed AI ecosystem brief, generated ${brief.generated_at}`;
|
|
1811
|
-
const body = sectionTexts.join('\n\n');
|
|
1812
|
-
return {
|
|
1813
|
-
content: [
|
|
1814
|
-
{
|
|
1815
|
-
type: 'text',
|
|
1816
|
-
text: `${header}\n\n${body}\n\nFor any subsection, call the underlying tool directly: get_ai_news, get_ai_papers_trending, get_arxiv_recent, get_hf_daily_papers, get_hf_trending, get_hot_issues, get_reddit_trending, get_openrouter_models, get_ai_status.`,
|
|
1817
|
-
},
|
|
1818
|
-
],
|
|
1819
|
-
};
|
|
1820
|
-
});
|
|
1821
|
-
// ── Tool: get_ai_cves_latest ────────────────────────────────────────
|
|
1822
|
-
registerTool('get_ai_cves_latest', 'Get metadata for the most-recent AI-stack CVE batch plus the first 25 papers. Source: TensorFeed AI-CVE intelligence feed, derived from GitHub Security Advisories (CC BY 4.0). Each paper carries cve_ids, affected_products, affected_version_ranges, fixed_versions, exploited_in_wild, severity_label, and source_url. For the full set use get_ai_cves_feed with offset; for AI-stack-filtered + categorized + sorted papers use the paid /api/premium/ai-cves/* endpoints. Free, no auth.', {}, async () => {
|
|
1823
|
-
const data = (await fetchJSON('/ai-cves/latest'));
|
|
1824
|
-
if (!data.batch_id) {
|
|
1825
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet. Check back once TensorFeed CC has POSTed the next clean batch from the extraction pipeline.' }] };
|
|
1826
|
-
}
|
|
1827
|
-
const head = `AI-CVE batch ${data.batch_id} extracted ${data.extracted_at}. Total ${data.total_papers} papers, ${data.ai_flagged_count} AI-flagged. License ${data.source_license} (${data.source_attribution}).`;
|
|
1828
|
-
const lines = data.papers
|
|
1829
|
-
.map((p, i) => {
|
|
1830
|
-
const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
|
|
1831
|
-
const products = p.affected_products.slice(0, 3).join(', ');
|
|
1832
|
-
return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
|
|
1833
|
-
})
|
|
1834
|
-
.join('\n');
|
|
1835
|
-
return {
|
|
1836
|
-
content: [
|
|
1837
|
-
{
|
|
1838
|
-
type: 'text',
|
|
1839
|
-
text: `${head}\n\nFirst 25 papers:\n${lines}\n\nFor AI-stack-filtered + categorized + sorted papers: /api/premium/ai-cves/ai-stack-cves (1 credit, x402-paid). For only papers actively exploited in the wild: /api/premium/ai-cves/exploited-in-wild (1 credit). For single-CVE lookup: /api/premium/ai-cves/cve?id=CVE-YYYY-NNNNN (1 credit).`,
|
|
1840
|
-
},
|
|
1841
|
-
],
|
|
1842
|
-
};
|
|
1843
|
-
});
|
|
1844
|
-
// ── Tool: get_ai_cves_feed ──────────────────────────────────────────
|
|
1845
|
-
registerTool('get_ai_cves_feed', 'Get a paginated slice of the most-recent AI-stack CVE batch. limit is capped at 50 (the premium tier handles bulk). Source: TensorFeed AI-CVE feed, derived from GitHub Security Advisories (CC BY 4.0). Free, no auth.', {
|
|
1846
|
-
limit: z.number().min(1).max(50).optional().describe('Number of papers to return (default 25, max 50).'),
|
|
1847
|
-
offset: z.number().min(0).optional().describe('Pagination offset (default 0).'),
|
|
1848
|
-
}, async ({ limit, offset }) => {
|
|
1849
|
-
const params = new URLSearchParams();
|
|
1850
|
-
if (typeof limit === 'number')
|
|
1851
|
-
params.set('limit', String(limit));
|
|
1852
|
-
if (typeof offset === 'number')
|
|
1853
|
-
params.set('offset', String(offset));
|
|
1854
|
-
const path = `/ai-cves/feed${params.toString() ? `?${params.toString()}` : ''}`;
|
|
1855
|
-
const data = (await fetchJSON(path));
|
|
1856
|
-
if (!data.batch_id) {
|
|
1857
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
|
|
1858
|
-
}
|
|
1859
|
-
const head = `AI-CVE feed batch ${data.batch_id}: showing offset ${data.offset} to ${data.offset + data.papers.length} of ${data.total} total.`;
|
|
1860
|
-
const lines = data.papers
|
|
1861
|
-
.map((p, i) => {
|
|
1862
|
-
const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
|
|
1863
|
-
const products = p.affected_products.slice(0, 3).join(', ');
|
|
1864
|
-
return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
|
|
1865
|
-
})
|
|
1866
|
-
.join('\n');
|
|
1867
|
-
return { content: [{ type: 'text', text: `${head}\n\n${lines}` }] };
|
|
1868
|
-
});
|
|
1869
|
-
// ── Tool: get_ai_cves_stats ─────────────────────────────────────────
|
|
1870
|
-
registerTool('get_ai_cves_stats', 'Get aggregate counts across the most-recent AI-stack CVE batch: distribution by severity (critical/high/medium/low/unstated), distribution by exploitation status (stated_yes/stated_no/unstated), and top 10 vendors by frequency. Source: TensorFeed AI-CVE feed (GitHub Security Advisories, CC BY 4.0). Free, no auth.', {}, async () => {
|
|
1871
|
-
const data = (await fetchJSON('/ai-cves/stats'));
|
|
1872
|
-
if (!data.batch_id) {
|
|
1873
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
|
|
1874
|
-
}
|
|
1875
|
-
const sev = Object.entries(data.by_severity)
|
|
1876
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
1877
|
-
.join('\n');
|
|
1878
|
-
const exp = Object.entries(data.by_exploitation)
|
|
1879
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
1880
|
-
.join('\n');
|
|
1881
|
-
const vendors = data.top_vendors.map((v, i) => ` ${i + 1}. ${v.vendor}: ${v.count}`).join('\n');
|
|
1882
|
-
return {
|
|
1883
|
-
content: [
|
|
1884
|
-
{
|
|
1885
|
-
type: 'text',
|
|
1886
|
-
text: `AI-CVE batch ${data.batch_id}: ${data.total_papers} total papers.\n\nBy severity:\n${sev}\n\nBy exploited-in-wild:\n${exp}\n\nTop vendors:\n${vendors}`,
|
|
1887
|
-
},
|
|
1888
|
-
],
|
|
1889
|
-
};
|
|
1890
|
-
});
|
|
1891
|
-
// ── Tool: get_ai_company_filings (free) ─────────────────────────────
|
|
1892
|
-
registerTool('get_ai_company_filings', 'Get recent SEC filings (10-K, 10-Q, 8-K, etc.) for one AI bellwether ticker. Cohort: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA. Source: data.sec.gov, public domain. Refreshed every 6 hours. Free, no auth.', {
|
|
1893
|
-
ticker: z.string().describe('AI bellwether ticker (case-insensitive). One of NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.'),
|
|
1894
|
-
limit: z.number().min(1).max(50).optional().describe('Max filings to return (default 10, max 50).'),
|
|
1895
|
-
}, async ({ ticker, limit }) => {
|
|
1896
|
-
const params = new URLSearchParams({ ticker: ticker.toUpperCase() });
|
|
1897
|
-
params.set('limit', String(limit ?? 10));
|
|
1898
|
-
const data = (await fetchJSON(`/sec/filings/recent?${params}`));
|
|
1899
|
-
if (!data.ok || !data.filings || data.filings.length === 0) {
|
|
1900
|
-
return {
|
|
1901
|
-
content: [
|
|
1902
|
-
{
|
|
1903
|
-
type: 'text',
|
|
1904
|
-
text: `No filings returned for ${ticker.toUpperCase()}. The ticker may not be in the AI bellwether cohort; supported tickers: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.`,
|
|
1905
|
-
},
|
|
1906
|
-
],
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
const lines = data.filings
|
|
1910
|
-
.map((f, i) => `${i + 1}. ${f.form} on ${f.filing_date}\n ${f.primary_doc_description}\n ${f.primary_doc_url}`)
|
|
1911
|
-
.join('\n\n');
|
|
1912
|
-
return {
|
|
1913
|
-
content: [
|
|
1914
|
-
{
|
|
1915
|
-
type: 'text',
|
|
1916
|
-
text: `${ticker.toUpperCase()} recent SEC filings (captured ${data.capturedAt ?? 'unknown'}, ${data.filings_count} returned):\n\n${lines}`,
|
|
1917
|
-
},
|
|
1918
|
-
],
|
|
1919
|
-
};
|
|
1920
|
-
});
|
|
1921
|
-
// ── Tool: premium_ai_company (1 credit) ─────────────────────────────
|
|
1922
|
-
registerTool('premium_ai_company', 'Get the per-ticker AI intelligence envelope for one AI bellwether: latest 10 SEC filings, latest 10 AI-tagged news mentions filtered by curated aliases, strategic and equity funding rounds where the company is a lead or notable investor, and cohort metadata. One call replaces four free siblings (sec/filings, news, funding, cohort) plus the agent-side alias filter. Costs 1 credit ($0.02). Strict-premium; ticker must be in the cohort.', {
|
|
1923
|
-
ticker: z
|
|
1225
|
+
}, CREATE_TOOL);
|
|
1226
|
+
// ── Tool: delete_watch ──────────────────────────────────────────────
|
|
1227
|
+
registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
|
|
1228
|
+
watch_id: z
|
|
1924
1229
|
.string()
|
|
1925
|
-
.regex(/^[A-Za-
|
|
1926
|
-
.
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
.map((f, i) => ` ${i + 1}. ${f.form} on ${f.filing_date}: ${f.primary_doc_description}`)
|
|
1933
|
-
.join('\n');
|
|
1934
|
-
const newsList = data.news.items
|
|
1935
|
-
.slice(0, 10)
|
|
1936
|
-
.map((n, i) => ` ${i + 1}. ${n.title}\n ${n.source} (${n.publishedAt})`)
|
|
1937
|
-
.join('\n\n');
|
|
1938
|
-
const fundingList = data.funding_as_investor.items
|
|
1939
|
-
.map((r, i) => ` ${i + 1}. ${r.company} ${r.stage}, $${r.amountM}M, ${r.announcedDate}`)
|
|
1940
|
-
.join('\n');
|
|
1941
|
-
const billing = data.billing
|
|
1942
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
1943
|
-
: '';
|
|
1944
|
-
return {
|
|
1945
|
-
content: [
|
|
1946
|
-
{
|
|
1947
|
-
type: 'text',
|
|
1948
|
-
text: `${data.ticker} (${data.company.display_name}, ${data.company.exchange}, ${data.company.category})\nAI angle: ${data.company.ai_angle}\nCaptured: ${data.capturedAt}\n\nRecent SEC filings (${data.filings.count}):\n${filingsList || ' none'}\n\nAI news mentions (${data.news.count}, aliases: ${data.news.aliases_used.join(', ')}):\n${newsList || ' none'}\n\nFunding rounds as lead or notable investor (${data.funding_as_investor.count}):\n${fundingList || ' none'}${billing}`,
|
|
1949
|
-
},
|
|
1950
|
-
],
|
|
1951
|
-
};
|
|
1952
|
-
});
|
|
1953
|
-
// ── Tool: get_x402_summary (free) ───────────────────────────────────
|
|
1954
|
-
registerTool('get_x402_summary', 'Get ecosystem-level x402 USDC settlement rollup on Base mainnet. Returns volume_usdc, count, unique_publishers, and change_vs_prior_window across a 24h, 7d, or 30d window. TensorFeed indexes Base USDC Transfer events filtered to wallets self-published by x402-compliant publishers in their /.well-known/x402.json manifests. Forward-only from 2026-05-28; ~6 min worst-case freshness (10 min SLA). Free, capped to one window per call.', {
|
|
1955
|
-
window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h if omitted.'),
|
|
1956
|
-
}, async ({ window }) => {
|
|
1957
|
-
const w = window ?? '24h';
|
|
1958
|
-
const data = (await fetchJSON(`/x402-index/summary?window=${w}`));
|
|
1959
|
-
return {
|
|
1960
|
-
content: [
|
|
1961
|
-
{
|
|
1962
|
-
type: 'text',
|
|
1963
|
-
text: `x402 settlement rollup (window ${data.window}, captured ${data.captured_at}):\n volume_usdc: ${data.volume_usdc}\n count: ${data.count}\n unique_publishers: ${data.unique_publishers}\n change vs prior window: volume ${data.change_vs_prior_window.volume_pct}%, count ${data.change_vs_prior_window.count_pct}%`,
|
|
1964
|
-
},
|
|
1965
|
-
],
|
|
1966
|
-
};
|
|
1967
|
-
});
|
|
1968
|
-
// ── Tool: get_x402_publishers (free) ────────────────────────────────
|
|
1969
|
-
registerTool('get_x402_publishers', 'List x402-compliant publishers TensorFeed currently indexes. Each entry: domain, pay_to_wallets, first_seen, last_crawled, last_event_at, last_crawl_error. Publishers are auto-discovered daily via /.well-known/x402.json crawls. Use this to enumerate which publishers can then be queried in depth via premium_x402_publisher_receipts. Free, no input.', {}, async () => {
|
|
1970
|
-
const data = (await fetchJSON('/x402-index/publishers'));
|
|
1971
|
-
if (data.count === 0) {
|
|
1972
|
-
return { content: [{ type: 'text', text: 'No x402 publishers indexed yet.' }] };
|
|
1973
|
-
}
|
|
1974
|
-
// Cap the rendered list so a large index cannot flood the agent's
|
|
1975
|
-
// context. Sanitize layer also enforces a hard char cap downstream.
|
|
1976
|
-
const MAX_PUBLISHERS = 50;
|
|
1977
|
-
const shown = data.publishers.slice(0, MAX_PUBLISHERS);
|
|
1978
|
-
const lines = shown
|
|
1979
|
-
.map((p, i) => `${i + 1}. ${p.domain}\n wallets: ${p.pay_to_wallets.join(', ')}\n first_seen: ${p.first_seen}${p.last_event_at ? `\n last_event_at: ${p.last_event_at}` : ''}${p.last_crawled ? `\n last_crawled: ${p.last_crawled}` : ''}${p.last_crawl_error ? `\n last_crawl_error: ${p.last_crawl_error}` : ''}`)
|
|
1980
|
-
.join('\n\n');
|
|
1981
|
-
const more = data.count > shown.length ? `\n\n...and ${data.count - shown.length} more (showing first ${shown.length}).` : '';
|
|
1982
|
-
return {
|
|
1983
|
-
content: [
|
|
1984
|
-
{
|
|
1985
|
-
type: 'text',
|
|
1986
|
-
text: `x402 publishers (${data.count}, captured ${data.captured_at}):\n\n${lines}${more}`,
|
|
1987
|
-
},
|
|
1988
|
-
],
|
|
1989
|
-
};
|
|
1990
|
-
});
|
|
1991
|
-
// ── Tool: get_x402_leaderboard (free) ───────────────────────────────
|
|
1992
|
-
registerTool('get_x402_leaderboard', 'Top publishers by x402 USDC settlement volume across a 24h, 7d, or 30d window. Each leader: rank, domain, volume_usdc, count, share_pct of the sliced total. Limit clamped 1 to 25. Useful for surfacing which x402 services are receiving the most agent traffic. Free.', {
|
|
1993
|
-
window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h.'),
|
|
1994
|
-
limit: z.number().min(1).max(25).optional().describe('Top N publishers to return. Default 10, max 25.'),
|
|
1995
|
-
}, async ({ window, limit }) => {
|
|
1996
|
-
const w = window ?? '24h';
|
|
1997
|
-
const l = limit ?? 10;
|
|
1998
|
-
const data = (await fetchJSON(`/x402-index/leaderboard?window=${w}&limit=${l}`));
|
|
1999
|
-
if (data.leaders.length === 0) {
|
|
2000
|
-
return { content: [{ type: 'text', text: `No x402 leaders for window ${data.window}.` }] };
|
|
2001
|
-
}
|
|
2002
|
-
const lines = data.leaders
|
|
2003
|
-
.map((e) => ` ${e.rank}. ${e.domain}: $${e.volume_usdc} across ${e.count} settlements (${e.share_pct}% share)`)
|
|
2004
|
-
.join('\n');
|
|
2005
|
-
return {
|
|
2006
|
-
content: [
|
|
2007
|
-
{
|
|
2008
|
-
type: 'text',
|
|
2009
|
-
text: `x402 leaderboard (window ${data.window}, captured ${data.captured_at}):\n${lines}`,
|
|
2010
|
-
},
|
|
2011
|
-
],
|
|
2012
|
-
};
|
|
2013
|
-
});
|
|
2014
|
-
// ── Tool: get_x402_recent (free) ────────────────────────────────────
|
|
2015
|
-
registerTool('get_x402_recent', 'Most recent x402 USDC settlement events newest-first. Each event: tx_hash, block, ts, from_address, to_address, amount_usdc, publisher_domain, base_explorer_url (basescan.org link). Backed by a 100-event ring buffer updated on every indexer cron tick. Limit clamped 1 to 50, default 20. Free.', {
|
|
2016
|
-
limit: z.number().min(1).max(50).optional().describe('Number of most-recent events. Default 20, max 50.'),
|
|
2017
|
-
}, async ({ limit }) => {
|
|
2018
|
-
const l = limit ?? 20;
|
|
2019
|
-
const data = (await fetchJSON(`/x402-index/recent?limit=${l}`));
|
|
2020
|
-
if (data.count === 0) {
|
|
2021
|
-
return { content: [{ type: 'text', text: 'No recent x402 settlement events.' }] };
|
|
2022
|
-
}
|
|
2023
|
-
const lines = data.events
|
|
2024
|
-
.map((e, i) => `${i + 1}. ${e.ts} block ${e.block}\n ${e.publisher_domain}: $${e.amount_usdc} USDC\n from ${e.from_address} to ${e.to_address}\n ${e.base_explorer_url}`)
|
|
2025
|
-
.join('\n\n');
|
|
2026
|
-
return {
|
|
2027
|
-
content: [
|
|
2028
|
-
{
|
|
2029
|
-
type: 'text',
|
|
2030
|
-
text: `Recent x402 settlements (${data.count}, captured ${data.captured_at}):\n\n${lines}`,
|
|
2031
|
-
},
|
|
2032
|
-
],
|
|
2033
|
-
};
|
|
2034
|
-
});
|
|
2035
|
-
// ── Tool: premium_x402_publisher_receipts (1 credit) ────────────────
|
|
2036
|
-
registerTool('premium_x402_publisher_receipts', 'Per-publisher receipt feed for one x402-compliant domain across a from/to date range. Returns publisher meta (domain, pay_to_wallets, first_seen), window rollup (volume_usdc, count, avg_amount, daily_series), and attribution. \n\nVS FREE: get_x402_publishers tells you WHICH publishers exist but only with rolled-up scalar counters. This endpoint composes the per-day rollup across a date range with avg_amount, daily series for charts, and structured attribution license metadata in one paid call. An agent building a per-publisher analytics dashboard would otherwise need to call get_x402_publishers + reconstruct daily series from get_x402_summary per-day (impossible because /summary returns ecosystem totals not per-publisher) or wait for the next free-tier expansion. The premium endpoint saves the integration overhead and serves a forensic / compliance-grade payload for $0.02. \n\nForward-only index. Costs 1 credit ($0.02). Strict-premium; domain must exist in get_x402_publishers, otherwise 404.', {
|
|
2037
|
-
domain: z.string().describe('Publisher domain to query, e.g. tensorfeed.ai. Must exist in get_x402_publishers result.'),
|
|
2038
|
-
from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
|
|
2039
|
-
to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
|
|
2040
|
-
}, async ({ domain, from, to }) => {
|
|
2041
|
-
const params = new URLSearchParams({ from, to });
|
|
2042
|
-
const data = (await fetchJSON(`/premium/x402-index/publisher/${encodeURIComponent(domain)}?${params}`, { auth: true }));
|
|
2043
|
-
const series = data.rollup.daily_series
|
|
2044
|
-
.map((d) => ` ${d.date}: $${d.volume_usdc} across ${d.count}`)
|
|
2045
|
-
.join('\n');
|
|
2046
|
-
const billing = data.billing
|
|
2047
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
2048
|
-
: '';
|
|
2049
|
-
return {
|
|
2050
|
-
content: [
|
|
2051
|
-
{
|
|
2052
|
-
type: 'text',
|
|
2053
|
-
text: `${data.publisher.domain} (first_seen ${data.publisher.first_seen})\nwallets: ${data.publisher.pay_to_wallets.join(', ')}\nwindow: ${data.window.from} to ${data.window.to} (${data.window.days} days)\n\nRollup:\n volume_usdc: ${data.rollup.volume_usdc}\n count: ${data.rollup.count}\n avg_amount: ${data.rollup.avg_amount}\n\nDaily series:\n${series || ' none'}\n\nAttribution: ${data.attribution}\nLicense: ${data.license}${billing}`,
|
|
2054
|
-
},
|
|
2055
|
-
],
|
|
2056
|
-
};
|
|
2057
|
-
});
|
|
2058
|
-
// ── Tool: premium_x402_series (1 credit) ────────────────────────────
|
|
2059
|
-
registerTool('premium_x402_series', 'Time-series of ecosystem or per-publisher x402 settlement volume or count across a from/to date range, chart-feeding. Required: metric (volume or count), granularity (day or hour), from + to (YYYY-MM-DD). Optional: domain filter (omit for ecosystem). \n\nVS FREE: get_x402_summary returns one rolled-up scalar per call for a fixed window length (24h / 7d / 30d). For ANY chart or dashboard, the caller needs a series, not a scalar. The premium endpoint returns the full series in one call instead of N calls + N rate-limit budget burns. For a publisher-scoped series, it is the ONLY way to get per-publisher daily data (get_x402_summary is ecosystem-only). \n\nMVP supports granularity=day; hour returns an empty series with an attribution note (hourly rollups land in a future indexer version). Costs 1 credit ($0.02).', {
|
|
2060
|
-
metric: z.enum(['volume', 'count']).describe('Which series to return.'),
|
|
2061
|
-
granularity: z.enum(['day', 'hour']).describe('Bucket size. MVP: day only; hour returns empty series.'),
|
|
2062
|
-
from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
|
|
2063
|
-
to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
|
|
2064
|
-
domain: z.string().optional().describe('Optional publisher domain filter. Omit for ecosystem-wide series.'),
|
|
2065
|
-
}, async ({ metric, granularity, from, to, domain }) => {
|
|
2066
|
-
const params = new URLSearchParams({ metric, granularity, from, to });
|
|
2067
|
-
if (domain)
|
|
2068
|
-
params.set('domain', domain);
|
|
2069
|
-
const data = (await fetchJSON(`/premium/x402-index/series?${params}`, { auth: true }));
|
|
2070
|
-
const scope = domain ? `domain=${domain}` : 'ecosystem-wide';
|
|
2071
|
-
const seriesLines = data.series.length
|
|
2072
|
-
? data.series.map((p) => ` ${p.ts}: ${p.value}`).join('\n')
|
|
2073
|
-
: ' (empty)';
|
|
2074
|
-
const billing = data.billing
|
|
2075
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
2076
|
-
: '';
|
|
2077
|
-
return {
|
|
2078
|
-
content: [
|
|
2079
|
-
{
|
|
2080
|
-
type: 'text',
|
|
2081
|
-
text: `x402 ${data.metric} series (${data.granularity}, ${scope})\nwindow: ${data.window.from} to ${data.window.to}\n\nSeries:\n${seriesLines}\n\nAttribution: ${data.attribution}${billing}`,
|
|
2082
|
-
},
|
|
2083
|
-
],
|
|
2084
|
-
};
|
|
2085
|
-
});
|
|
1230
|
+
.regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
|
|
1231
|
+
.max(128)
|
|
1232
|
+
.describe('The wat_... id from create_watch / list_watches'),
|
|
1233
|
+
}, async ({ watch_id }) => {
|
|
1234
|
+
await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
|
|
1235
|
+
return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
|
|
1236
|
+
}, DELETE_TOOL);
|
|
2086
1237
|
// ── Start ───────────────────────────────────────────────────────────
|
|
2087
1238
|
async function main() {
|
|
2088
1239
|
const transport = new StdioServerTransport();
|