@tensorfeed/mcp-server 1.9.0 → 1.9.1
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/README.md +1 -1
- package/package.json +8 -1
- package/server.json +3 -3
- package/src/index.ts +0 -1043
- package/tsconfig.json +0 -15
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Premium tier (with `TENSORFEED_TOKEN` set):
|
|
|
104
104
|
|
|
105
105
|
Premium tools cost 1 credit per call. Credits are 50 per $1 USDC at base rate, with volume discounts at $5 (10%), $30 (25%), and $200 (40%). See [tensorfeed.ai/developers/agent-payments](https://tensorfeed.ai/developers/agent-payments) for the full payment flow and trust attestation.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
No refunds: all credit purchases are final per Section 17.5 of the [Terms](https://tensorfeed.ai/terms#premium). Credits do not expire and are jointly redeemable on tensorfeed.ai and terminalfeed.io, so the safe pattern is to buy small ($1 USDC for 50 credits) and top up as call volume is calibrated. Premium access is unavailable in OFAC-sanctioned jurisdictions; see Section 17.9.
|
|
108
108
|
|
|
109
109
|
## Data Source
|
|
110
110
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tensorfeed/mcp-server",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
4
|
+
"mcpName": "ai.tensorfeed/mcp-server",
|
|
4
5
|
"description": "MCP server for TensorFeed.ai - AI news, status, model data, and premium endpoints (routing, cost projection, forecast, history series, news search, webhook watches with digest tier, enriched directory) for AI agents",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -8,6 +9,12 @@
|
|
|
8
9
|
"bin": {
|
|
9
10
|
"tensorfeed-mcp": "dist/index.js"
|
|
10
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"server.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
11
18
|
"scripts": {
|
|
12
19
|
"build": "tsc",
|
|
13
20
|
"start": "node dist/index.js"
|
package/server.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "ai.tensorfeed/mcp-server",
|
|
4
4
|
"title": "TensorFeed",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "1.9.
|
|
5
|
+
"description": "Real-time AI news, status, model pricing, and pay-per-call premium tools (USDC on Base).",
|
|
6
|
+
"version": "1.9.1",
|
|
7
7
|
"websiteUrl": "https://tensorfeed.ai/developers/agent-payments",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/RipperMercs/tensorfeed",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "@tensorfeed/mcp-server",
|
|
18
|
-
"version": "1.9.
|
|
18
|
+
"version": "1.9.1",
|
|
19
19
|
"runtimeHint": "npx",
|
|
20
20
|
"transport": {
|
|
21
21
|
"type": "stdio"
|
package/src/index.ts
DELETED
|
@@ -1,1043 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
const API_BASE = 'https://tensorfeed.ai/api';
|
|
8
|
-
const SDK_VERSION = '1.9.0';
|
|
9
|
-
|
|
10
|
-
// ── API helpers ─────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
interface FetchOptions {
|
|
13
|
-
method?: 'GET' | 'POST' | 'DELETE';
|
|
14
|
-
body?: unknown;
|
|
15
|
-
/** When true, attach the TENSORFEED_TOKEN env var as a Bearer token. */
|
|
16
|
-
auth?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function fetchJSON(path: string, opts: FetchOptions = {}): Promise<unknown> {
|
|
20
|
-
const headers: Record<string, string> = {
|
|
21
|
-
'User-Agent': `TensorFeed-MCP/${SDK_VERSION}`,
|
|
22
|
-
};
|
|
23
|
-
if (opts.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
24
|
-
if (opts.auth) {
|
|
25
|
-
const token = process.env.TENSORFEED_TOKEN;
|
|
26
|
-
if (!token) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
'TENSORFEED_TOKEN env var is not set. Premium MCP tools require a bearer token. ' +
|
|
29
|
-
'Buy credits at https://tensorfeed.ai/developers/agent-payments and pass the returned tf_live_... token via the TENSORFEED_TOKEN env var in your MCP client config.',
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
33
|
-
}
|
|
34
|
-
const res = await fetch(`${API_BASE}${path}`, {
|
|
35
|
-
method: opts.method ?? 'GET',
|
|
36
|
-
headers,
|
|
37
|
-
...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
|
|
38
|
-
});
|
|
39
|
-
if (!res.ok) {
|
|
40
|
-
let errPayload: unknown;
|
|
41
|
-
try {
|
|
42
|
-
errPayload = await res.json();
|
|
43
|
-
} catch {
|
|
44
|
-
errPayload = await res.text().catch(() => '');
|
|
45
|
-
}
|
|
46
|
-
if (res.status === 402) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`Payment required (402). Your token may be out of credits. Top up at https://tensorfeed.ai/developers/agent-payments. Detail: ${JSON.stringify(errPayload)}`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (res.status === 401) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Token rejected (401). Check that TENSORFEED_TOKEN is set to a valid tf_live_... token. Detail: ${JSON.stringify(errPayload)}`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
throw new Error(`API error ${res.status}: ${JSON.stringify(errPayload)}`);
|
|
57
|
-
}
|
|
58
|
-
return res.json();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Server setup ────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
const server = new McpServer({
|
|
64
|
-
name: 'tensorfeed',
|
|
65
|
-
version: SDK_VERSION,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// ── Tool: get_ai_news ───────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
server.tool(
|
|
71
|
-
'get_ai_news',
|
|
72
|
-
'Get the latest AI news articles from TensorFeed.ai. Aggregates from 15+ sources including Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more.',
|
|
73
|
-
{
|
|
74
|
-
category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
|
|
75
|
-
limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
|
|
76
|
-
},
|
|
77
|
-
async ({ category, limit }) => {
|
|
78
|
-
const params = new URLSearchParams();
|
|
79
|
-
if (category) params.set('category', category);
|
|
80
|
-
params.set('limit', String(limit || 10));
|
|
81
|
-
|
|
82
|
-
const data = await fetchJSON(`/news?${params}`) as {
|
|
83
|
-
articles: { title: string; url: string; source: string; snippet: string; categories: string[]; publishedAt: string }[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const text = data.articles
|
|
87
|
-
.map((a, i) => `${i + 1}. ${a.title}\n Source: ${a.source}\n URL: ${a.url}\n ${a.snippet ? a.snippet + '\n ' : ''}Published: ${a.publishedAt}`)
|
|
88
|
-
.join('\n\n');
|
|
89
|
-
|
|
90
|
-
return { content: [{ type: 'text' as const, text: text || 'No articles found.' }] };
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// ── Tool: get_ai_status ─────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
server.tool(
|
|
97
|
-
'get_ai_status',
|
|
98
|
-
'Get real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face).',
|
|
99
|
-
{},
|
|
100
|
-
async () => {
|
|
101
|
-
const data = await fetchJSON('/status') as {
|
|
102
|
-
services: { name: string; provider: string; status: string; components: { name: string; status: string }[] }[];
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const text = data.services
|
|
106
|
-
.map(s => {
|
|
107
|
-
const components = s.components.length > 0
|
|
108
|
-
? '\n' + s.components.map(c => ` ${c.name}: ${c.status}`).join('\n')
|
|
109
|
-
: '';
|
|
110
|
-
return ` ${s.status === 'operational' ? 'OK' : s.status.toUpperCase()} ${s.name} (${s.provider})${components}`;
|
|
111
|
-
})
|
|
112
|
-
.join('\n');
|
|
113
|
-
|
|
114
|
-
return { content: [{ type: 'text' as const, text: `AI Service Status:\n${text}` }] };
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// ── Tool: is_service_down ───────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
server.tool(
|
|
121
|
-
'is_service_down',
|
|
122
|
-
'Check if a specific AI service is currently down or experiencing issues.',
|
|
123
|
-
{
|
|
124
|
-
service: z.string().describe('Service name to check (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate")'),
|
|
125
|
-
},
|
|
126
|
-
async ({ service }) => {
|
|
127
|
-
const data = await fetchJSON('/status') as {
|
|
128
|
-
services: { name: string; provider: string; status: string; components: { name: string; status: string }[] }[];
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const match = data.services.find(s =>
|
|
132
|
-
s.name.toLowerCase().includes(service.toLowerCase()) ||
|
|
133
|
-
s.provider.toLowerCase().includes(service.toLowerCase())
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
if (!match) {
|
|
137
|
-
return { content: [{ type: 'text' as const, text: `Service "${service}" not found. Available services: ${data.services.map(s => s.name).join(', ')}` }] };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const statusEmoji = match.status === 'operational' ? 'OK' : match.status === 'degraded' ? 'DEGRADED' : 'DOWN';
|
|
141
|
-
const components = match.components.length > 0
|
|
142
|
-
? '\nComponents:\n' + match.components.map(c => ` ${c.name}: ${c.status}`).join('\n')
|
|
143
|
-
: '';
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
content: [{
|
|
147
|
-
type: 'text' as const,
|
|
148
|
-
text: `${statusEmoji} ${match.name} (${match.provider}) is ${match.status}${components}`
|
|
149
|
-
}]
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// ── Tool: get_model_pricing ─────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
server.tool(
|
|
157
|
-
'get_model_pricing',
|
|
158
|
-
'Get AI model pricing comparison across all major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere). Prices per 1M tokens.',
|
|
159
|
-
{},
|
|
160
|
-
async () => {
|
|
161
|
-
const data = await fetchJSON('/models') as {
|
|
162
|
-
providers: {
|
|
163
|
-
name: string;
|
|
164
|
-
models: { name: string; inputPrice: number; outputPrice: number; contextWindow: number; released: string; capabilities: string[] }[];
|
|
165
|
-
}[];
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const text = data.providers
|
|
169
|
-
.map(p => {
|
|
170
|
-
const models = p.models
|
|
171
|
-
.map(m => {
|
|
172
|
-
const input = m.inputPrice === 0 ? 'Free' : `$${m.inputPrice.toFixed(2)}`;
|
|
173
|
-
const output = m.outputPrice === 0 ? 'Free' : `$${m.outputPrice.toFixed(2)}`;
|
|
174
|
-
const ctx = m.contextWindow >= 1000000
|
|
175
|
-
? `${(m.contextWindow / 1000000).toFixed(0)}M`
|
|
176
|
-
: `${(m.contextWindow / 1000).toFixed(0)}K`;
|
|
177
|
-
return ` ${m.name}: Input ${input}, Output ${output}, Context ${ctx}, Released ${m.released}`;
|
|
178
|
-
})
|
|
179
|
-
.join('\n');
|
|
180
|
-
return ` ${p.name}:\n${models}`;
|
|
181
|
-
})
|
|
182
|
-
.join('\n\n');
|
|
183
|
-
|
|
184
|
-
return { content: [{ type: 'text' as const, text: `AI Model Pricing (per 1M tokens):\n\n${text}` }] };
|
|
185
|
-
}
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// ── Tool: get_ai_today ──────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
server.tool(
|
|
191
|
-
'get_ai_today',
|
|
192
|
-
'Get a summary of what happened in AI today. Returns the top stories from the last 24 hours.',
|
|
193
|
-
{
|
|
194
|
-
limit: z.number().min(1).max(20).optional().describe('Number of stories (default 5)'),
|
|
195
|
-
},
|
|
196
|
-
async ({ limit }) => {
|
|
197
|
-
const data = await fetchJSON(`/news?limit=${limit || 5}`) as {
|
|
198
|
-
articles: { title: string; url: string; source: string; publishedAt: string }[];
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const text = data.articles
|
|
202
|
-
.map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
|
|
203
|
-
.join('\n\n');
|
|
204
|
-
|
|
205
|
-
return { content: [{ type: 'text' as const, text: `Today in AI:\n\n${text}` }] };
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// ── Tool: get_agent_activity ────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
server.tool(
|
|
212
|
-
'get_agent_activity',
|
|
213
|
-
'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.',
|
|
214
|
-
{},
|
|
215
|
-
async () => {
|
|
216
|
-
const data = (await fetchJSON('/agents/activity')) as {
|
|
217
|
-
today_count: number;
|
|
218
|
-
last_updated: string;
|
|
219
|
-
recent: { bot: string; endpoint: string; timestamp: string }[];
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const tally = new Map<string, number>();
|
|
223
|
-
for (const hit of data.recent) {
|
|
224
|
-
tally.set(hit.bot, (tally.get(hit.bot) || 0) + 1);
|
|
225
|
-
}
|
|
226
|
-
const breakdown = Array.from(tally.entries())
|
|
227
|
-
.map(([bot, count]) => ` ${bot}: ${count}`)
|
|
228
|
-
.sort()
|
|
229
|
-
.join('\n');
|
|
230
|
-
|
|
231
|
-
const tail = data.recent
|
|
232
|
-
.slice(0, 10)
|
|
233
|
-
.map(h => ` ${h.bot} ${h.endpoint} (${h.timestamp})`)
|
|
234
|
-
.join('\n');
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
content: [
|
|
238
|
-
{
|
|
239
|
-
type: 'text' as const,
|
|
240
|
-
text:
|
|
241
|
-
`AI bot traffic on TensorFeed.ai\n\n` +
|
|
242
|
-
`Today's bot hits: ${data.today_count}\n` +
|
|
243
|
-
`Last updated: ${data.last_updated}\n\n` +
|
|
244
|
-
`Bot breakdown (recent ${data.recent.length} hits):\n${breakdown || ' (none)'}\n\n` +
|
|
245
|
-
`Recent tail (10 most recent):\n${tail || ' (none)'}\n\n` +
|
|
246
|
-
`Public dashboard: https://tensorfeed.ai/agent-traffic`,
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
};
|
|
250
|
-
},
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// ════════════════════════════════════════════════════════════════════
|
|
254
|
-
// PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
|
|
255
|
-
// ════════════════════════════════════════════════════════════════════
|
|
256
|
-
|
|
257
|
-
// ── Tool: get_account_balance ───────────────────────────────────────
|
|
258
|
-
|
|
259
|
-
server.tool(
|
|
260
|
-
'get_account_balance',
|
|
261
|
-
'Check the credit balance for the configured TensorFeed bearer token. Free, but requires TENSORFEED_TOKEN to be set.',
|
|
262
|
-
{},
|
|
263
|
-
async () => {
|
|
264
|
-
const data = (await fetchJSON('/payment/balance', { auth: true })) as {
|
|
265
|
-
balance: number;
|
|
266
|
-
created: string;
|
|
267
|
-
last_used: string;
|
|
268
|
-
total_purchased: number;
|
|
269
|
-
};
|
|
270
|
-
return {
|
|
271
|
-
content: [
|
|
272
|
-
{
|
|
273
|
-
type: 'text' as const,
|
|
274
|
-
text: `Balance: ${data.balance} credits\nTotal purchased: ${data.total_purchased}\nCreated: ${data.created}\nLast used: ${data.last_used}`,
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
};
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// ── Tool: get_account_usage ─────────────────────────────────────────
|
|
282
|
-
|
|
283
|
-
server.tool(
|
|
284
|
-
'get_account_usage',
|
|
285
|
-
'Show per-endpoint usage for the configured TensorFeed token (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.',
|
|
286
|
-
{},
|
|
287
|
-
async () => {
|
|
288
|
-
const data = (await fetchJSON('/payment/usage', { auth: true })) as {
|
|
289
|
-
total_calls: number;
|
|
290
|
-
total_credits_spent: number;
|
|
291
|
-
by_endpoint: Record<string, { calls: number; credits: number; last_seen: string }>;
|
|
292
|
-
};
|
|
293
|
-
if (data.total_calls === 0) {
|
|
294
|
-
return { content: [{ type: 'text' as const, text: 'No premium API calls on this token yet.' }] };
|
|
295
|
-
}
|
|
296
|
-
const rows = Object.entries(data.by_endpoint)
|
|
297
|
-
.sort(([, a], [, b]) => b.calls - a.calls)
|
|
298
|
-
.map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
|
|
299
|
-
.join('\n');
|
|
300
|
-
return {
|
|
301
|
-
content: [
|
|
302
|
-
{
|
|
303
|
-
type: 'text' as const,
|
|
304
|
-
text: `Total: ${data.total_calls} calls, ${data.total_credits_spent} credits\n\n${rows}`,
|
|
305
|
-
},
|
|
306
|
-
],
|
|
307
|
-
};
|
|
308
|
-
},
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
// ── Tool: premium_routing (1 credit) ────────────────────────────────
|
|
312
|
-
|
|
313
|
-
server.tool(
|
|
314
|
-
'premium_routing',
|
|
315
|
-
'Get a ranked list of recommended AI models for a task with full score breakdown (quality, availability, cost, latency). Costs 1 credit.',
|
|
316
|
-
{
|
|
317
|
-
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type the model needs to be good at (default: general)'),
|
|
318
|
-
budget: z.number().optional().describe('Max blended USD per 1M tokens'),
|
|
319
|
-
min_quality: z.number().min(0).max(1).optional().describe('Minimum quality score in [0, 1]'),
|
|
320
|
-
top_n: z.number().min(1).max(10).optional().describe('How many models to return (default 5)'),
|
|
321
|
-
},
|
|
322
|
-
async ({ task, budget, min_quality, top_n }) => {
|
|
323
|
-
const params = new URLSearchParams();
|
|
324
|
-
if (task) params.set('task', task);
|
|
325
|
-
if (typeof budget === 'number') params.set('budget', String(budget));
|
|
326
|
-
if (typeof min_quality === 'number') params.set('min_quality', String(min_quality));
|
|
327
|
-
if (typeof top_n === 'number') params.set('top_n', String(top_n));
|
|
328
|
-
const data = (await fetchJSON(`/premium/routing?${params}`, { auth: true })) as {
|
|
329
|
-
task: string;
|
|
330
|
-
recommendations: {
|
|
331
|
-
rank: number;
|
|
332
|
-
model: { name: string; provider: string };
|
|
333
|
-
pricing: { input: number; output: number };
|
|
334
|
-
composite_score: number;
|
|
335
|
-
components: { quality: number; availability: number; cost: number; latency: number };
|
|
336
|
-
}[];
|
|
337
|
-
billing?: { credits_charged: number; credits_remaining?: number };
|
|
338
|
-
};
|
|
339
|
-
const list = data.recommendations
|
|
340
|
-
.map(r => {
|
|
341
|
-
const c = r.components;
|
|
342
|
-
return ` #${r.rank} ${r.model.name} (${r.model.provider}) score=${r.composite_score.toFixed(3)}\n in $${r.pricing.input}/1M, out $${r.pricing.output}/1M\n quality=${c.quality.toFixed(2)} avail=${c.availability.toFixed(2)} cost=${c.cost.toFixed(2)} latency=${c.latency.toFixed(2)}`;
|
|
343
|
-
})
|
|
344
|
-
.join('\n\n');
|
|
345
|
-
const billing = data.billing
|
|
346
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
|
|
347
|
-
: '';
|
|
348
|
-
return { content: [{ type: 'text' as const, text: `Routing for "${data.task}":\n\n${list}${billing}` }] };
|
|
349
|
-
},
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// ── Tool: pricing_series (1 credit) ─────────────────────────────────
|
|
353
|
-
|
|
354
|
-
server.tool(
|
|
355
|
-
'pricing_series',
|
|
356
|
-
'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit.',
|
|
357
|
-
{
|
|
358
|
-
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
|
|
359
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
360
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
361
|
-
},
|
|
362
|
-
async ({ model, from, to }) => {
|
|
363
|
-
const params = new URLSearchParams({ model });
|
|
364
|
-
if (from) params.set('from', from);
|
|
365
|
-
if (to) params.set('to', to);
|
|
366
|
-
const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true })) as {
|
|
367
|
-
model: string;
|
|
368
|
-
provider: string | null;
|
|
369
|
-
points: { date: string; input: number; output: number; blended: number }[];
|
|
370
|
-
summary: {
|
|
371
|
-
first: { date: string; blended: number } | null;
|
|
372
|
-
latest: { date: string; blended: number } | null;
|
|
373
|
-
min_blended: number | null;
|
|
374
|
-
max_blended: number | null;
|
|
375
|
-
delta_pct_blended: number | null;
|
|
376
|
-
changes_detected: number;
|
|
377
|
-
days_with_data: number;
|
|
378
|
-
};
|
|
379
|
-
billing?: { credits_remaining?: number };
|
|
380
|
-
};
|
|
381
|
-
const s = data.summary;
|
|
382
|
-
const summary = s.first && s.latest
|
|
383
|
-
? `${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})`
|
|
384
|
-
: 'no data points in range';
|
|
385
|
-
return {
|
|
386
|
-
content: [
|
|
387
|
-
{
|
|
388
|
-
type: 'text' as const,
|
|
389
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
};
|
|
393
|
-
},
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
// ── Tool: benchmark_series (1 credit) ───────────────────────────────
|
|
397
|
-
|
|
398
|
-
server.tool(
|
|
399
|
-
'benchmark_series',
|
|
400
|
-
'Score evolution for a single benchmark on one AI model. Costs 1 credit. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.',
|
|
401
|
-
{
|
|
402
|
-
model: z.string().describe('Model id or display name'),
|
|
403
|
-
benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval)'),
|
|
404
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
405
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
406
|
-
},
|
|
407
|
-
async ({ model, benchmark, from, to }) => {
|
|
408
|
-
const params = new URLSearchParams({ model, benchmark });
|
|
409
|
-
if (from) params.set('from', from);
|
|
410
|
-
if (to) params.set('to', to);
|
|
411
|
-
const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true })) as {
|
|
412
|
-
model: string;
|
|
413
|
-
benchmark: string;
|
|
414
|
-
points: { date: string; score: number }[];
|
|
415
|
-
summary: { first: { date: string; score: number } | null; latest: { date: string; score: number } | null; delta_pp: number | null };
|
|
416
|
-
billing?: { credits_remaining?: number };
|
|
417
|
-
};
|
|
418
|
-
const s = data.summary;
|
|
419
|
-
const summary = s.first && s.latest
|
|
420
|
-
? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
|
|
421
|
-
: 'no data in range';
|
|
422
|
-
return {
|
|
423
|
-
content: [
|
|
424
|
-
{
|
|
425
|
-
type: 'text' as const,
|
|
426
|
-
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
};
|
|
430
|
-
},
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
// ── Tool: status_uptime (1 credit) ──────────────────────────────────
|
|
434
|
-
|
|
435
|
-
server.tool(
|
|
436
|
-
'status_uptime',
|
|
437
|
-
'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit.',
|
|
438
|
-
{
|
|
439
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
|
|
440
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
441
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
442
|
-
},
|
|
443
|
-
async ({ provider, from, to }) => {
|
|
444
|
-
const params = new URLSearchParams({ provider });
|
|
445
|
-
if (from) params.set('from', from);
|
|
446
|
-
if (to) params.set('to', to);
|
|
447
|
-
const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true })) as {
|
|
448
|
-
provider: string;
|
|
449
|
-
days_total: number;
|
|
450
|
-
days_with_data: number;
|
|
451
|
-
days_operational: number;
|
|
452
|
-
days_degraded: number;
|
|
453
|
-
days_down: number;
|
|
454
|
-
uptime_pct: number | null;
|
|
455
|
-
incident_days: { date: string; status: string }[];
|
|
456
|
-
billing?: { credits_remaining?: number };
|
|
457
|
-
};
|
|
458
|
-
const incidents = data.incident_days.length
|
|
459
|
-
? '\n\nIncident days:\n' + data.incident_days.map(d => ` ${d.date}: ${d.status}`).join('\n')
|
|
460
|
-
: '';
|
|
461
|
-
return {
|
|
462
|
-
content: [
|
|
463
|
-
{
|
|
464
|
-
type: 'text' as const,
|
|
465
|
-
text:
|
|
466
|
-
`${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
|
|
467
|
-
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
468
|
-
incidents +
|
|
469
|
-
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
470
|
-
},
|
|
471
|
-
],
|
|
472
|
-
};
|
|
473
|
-
},
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
// ── Tool: history_compare (1 credit) ────────────────────────────────
|
|
477
|
-
|
|
478
|
-
server.tool(
|
|
479
|
-
'history_compare',
|
|
480
|
-
'Diff two daily snapshots: returns added, removed, and changed entries with deltas. Costs 1 credit.',
|
|
481
|
-
{
|
|
482
|
-
from: z.string().describe('Earlier snapshot date YYYY-MM-DD'),
|
|
483
|
-
to: z.string().describe('Later snapshot date YYYY-MM-DD'),
|
|
484
|
-
type: z.enum(['pricing', 'benchmarks']).optional().describe('Snapshot type (default: pricing)'),
|
|
485
|
-
},
|
|
486
|
-
async ({ from, to, type }) => {
|
|
487
|
-
const params = new URLSearchParams({ from, to, type: type ?? 'pricing' });
|
|
488
|
-
const data = (await fetchJSON(`/premium/history/compare?${params}`, { auth: true })) as
|
|
489
|
-
| {
|
|
490
|
-
ok: true;
|
|
491
|
-
type: 'pricing';
|
|
492
|
-
added: { model: string; provider: string }[];
|
|
493
|
-
removed: { model: string; provider: string }[];
|
|
494
|
-
changed: { model: string; field: string; from: number; to: number; delta_pct: number | null }[];
|
|
495
|
-
unchanged_count: number;
|
|
496
|
-
billing?: { credits_remaining?: number };
|
|
497
|
-
}
|
|
498
|
-
| {
|
|
499
|
-
ok: true;
|
|
500
|
-
type: 'benchmarks';
|
|
501
|
-
added_models: string[];
|
|
502
|
-
removed_models: string[];
|
|
503
|
-
changed: { model: string; benchmark: string; from: number; to: number; delta_pp: number }[];
|
|
504
|
-
billing?: { credits_remaining?: number };
|
|
505
|
-
};
|
|
506
|
-
if (data.type === 'pricing') {
|
|
507
|
-
const changes = data.changed
|
|
508
|
-
.map(c => ` ${c.model} ${c.field}: ${c.from} -> ${c.to} (${c.delta_pct ?? 'n/a'}%)`)
|
|
509
|
-
.join('\n');
|
|
510
|
-
return {
|
|
511
|
-
content: [
|
|
512
|
-
{
|
|
513
|
-
type: 'text' as const,
|
|
514
|
-
text:
|
|
515
|
-
`Pricing diff ${from} -> ${to}\n` +
|
|
516
|
-
` added: ${data.added.length} (${data.added.map(a => a.model).join(', ') || 'none'})\n` +
|
|
517
|
-
` removed: ${data.removed.length} (${data.removed.map(r => r.model).join(', ') || 'none'})\n` +
|
|
518
|
-
` unchanged: ${data.unchanged_count}\n\n` +
|
|
519
|
-
(changes || ' no field changes') +
|
|
520
|
-
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
const changes = data.changed
|
|
526
|
-
.map(c => ` ${c.model} ${c.benchmark}: ${c.from} -> ${c.to} (delta ${c.delta_pp} pp)`)
|
|
527
|
-
.join('\n');
|
|
528
|
-
return {
|
|
529
|
-
content: [
|
|
530
|
-
{
|
|
531
|
-
type: 'text' as const,
|
|
532
|
-
text:
|
|
533
|
-
`Benchmark diff ${from} -> ${to}\n` +
|
|
534
|
-
` added: ${data.added_models.length} (${data.added_models.join(', ') || 'none'})\n` +
|
|
535
|
-
` removed: ${data.removed_models.length} (${data.removed_models.join(', ') || 'none'})\n\n` +
|
|
536
|
-
(changes || ' no score changes') +
|
|
537
|
-
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
538
|
-
},
|
|
539
|
-
],
|
|
540
|
-
};
|
|
541
|
-
},
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
// ── Tool: premium_agents_directory (1 credit) ───────────────────────
|
|
545
|
-
|
|
546
|
-
server.tool(
|
|
547
|
-
'premium_agents_directory',
|
|
548
|
-
'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit.',
|
|
549
|
-
{
|
|
550
|
-
category: z.string().optional().describe('coding, research, general, creative, frameworks'),
|
|
551
|
-
status: z.enum(['operational', 'degraded', 'down', 'unknown']).optional(),
|
|
552
|
-
open_source: z.boolean().optional(),
|
|
553
|
-
sort: z.enum(['trending', 'alphabetical', 'status', 'price_low', 'price_high', 'news_count']).optional(),
|
|
554
|
-
limit: z.number().min(1).max(100).optional(),
|
|
555
|
-
},
|
|
556
|
-
async ({ category, status, open_source, sort, limit }) => {
|
|
557
|
-
const params = new URLSearchParams();
|
|
558
|
-
if (category) params.set('category', category);
|
|
559
|
-
if (status) params.set('status', status);
|
|
560
|
-
if (typeof open_source === 'boolean') params.set('open_source', String(open_source));
|
|
561
|
-
if (sort) params.set('sort', sort);
|
|
562
|
-
if (typeof limit === 'number') params.set('limit', String(limit));
|
|
563
|
-
const data = (await fetchJSON(`/premium/agents/directory?${params}`, { auth: true })) as {
|
|
564
|
-
total: number;
|
|
565
|
-
returned: number;
|
|
566
|
-
sort: string;
|
|
567
|
-
agents: {
|
|
568
|
-
id: string;
|
|
569
|
-
name: string;
|
|
570
|
-
provider: string;
|
|
571
|
-
category: string;
|
|
572
|
-
live_status: string;
|
|
573
|
-
recent_news_count: number;
|
|
574
|
-
flagship_pricing: { model: string; blended: number } | null;
|
|
575
|
-
trending_score: number;
|
|
576
|
-
}[];
|
|
577
|
-
billing?: { credits_remaining?: number };
|
|
578
|
-
};
|
|
579
|
-
const list = data.agents
|
|
580
|
-
.map(
|
|
581
|
-
a =>
|
|
582
|
-
` ${a.name} (${a.provider}) [${a.category}] status=${a.live_status} score=${a.trending_score} news=${a.recent_news_count}` +
|
|
583
|
-
(a.flagship_pricing ? ` flagship=${a.flagship_pricing.model} $${a.flagship_pricing.blended}/1M` : ''),
|
|
584
|
-
)
|
|
585
|
-
.join('\n');
|
|
586
|
-
return {
|
|
587
|
-
content: [
|
|
588
|
-
{
|
|
589
|
-
type: 'text' as const,
|
|
590
|
-
text: `Agents (sort: ${data.sort}, ${data.returned} of ${data.total}):\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
};
|
|
594
|
-
},
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
// ── Tool: whats_new (1 credit) ──────────────────────────────────────
|
|
598
|
-
|
|
599
|
-
server.tool(
|
|
600
|
-
'whats_new',
|
|
601
|
-
'Agent morning brief: pricing changes, new/removed models, status incidents, and top news from the last 1-7 days. The tool to call when your agent boots up. Costs 1 credit.',
|
|
602
|
-
{
|
|
603
|
-
days: z.number().min(1).max(7).optional().describe('Window length in days (default 1)'),
|
|
604
|
-
news_limit: z.number().min(1).max(25).optional().describe('Max news headlines (default 10)'),
|
|
605
|
-
},
|
|
606
|
-
async ({ days, news_limit }) => {
|
|
607
|
-
const params = new URLSearchParams();
|
|
608
|
-
if (typeof days === 'number') params.set('days', String(days));
|
|
609
|
-
if (typeof news_limit === 'number') params.set('news_limit', String(news_limit));
|
|
610
|
-
const data = (await fetchJSON(`/premium/whats-new?${params}`, { auth: true })) as {
|
|
611
|
-
window: { days: number };
|
|
612
|
-
summary: { total_pricing_changes: number; new_models: number; removed_models: number; incidents: number; news_articles: number };
|
|
613
|
-
pricing: {
|
|
614
|
-
changes: { model: string; provider: string; field: string; from: number; to: number; delta_pct: number | null }[];
|
|
615
|
-
new_models: { model: string; provider: string; input_per_1m: number; output_per_1m: number }[];
|
|
616
|
-
removed_models: { model: string; provider: string }[];
|
|
617
|
-
};
|
|
618
|
-
status: { incidents: { service: string; severity: string; title: string; started_at: string }[]; currently_operational: number; currently_degraded: number; currently_down: number };
|
|
619
|
-
news: { title: string; url: string; source: string; published_at: string }[];
|
|
620
|
-
billing?: { credits_remaining?: number };
|
|
621
|
-
};
|
|
622
|
-
const sum = data.summary;
|
|
623
|
-
const priceLines = data.pricing.changes.length
|
|
624
|
-
? '\nPricing changes:\n' + data.pricing.changes.map(c => ` ${c.model} ${c.field}: ${c.from} -> ${c.to} (${c.delta_pct ?? 'n/a'}%)`).join('\n')
|
|
625
|
-
: '';
|
|
626
|
-
const newLines = data.pricing.new_models.length
|
|
627
|
-
? '\nNew models:\n' + data.pricing.new_models.map(m => ` ${m.model} (${m.provider}) in $${m.input_per_1m}/1M, out $${m.output_per_1m}/1M`).join('\n')
|
|
628
|
-
: '';
|
|
629
|
-
const incidentLines = data.status.incidents.length
|
|
630
|
-
? '\nIncidents:\n' + data.status.incidents.map(i => ` ${i.service} (${i.severity}): ${i.title} @ ${i.started_at}`).join('\n')
|
|
631
|
-
: '';
|
|
632
|
-
const newsLines = data.news.length
|
|
633
|
-
? '\nTop news:\n' + data.news.map(n => ` ${n.title} (${n.source})\n ${n.url}`).join('\n')
|
|
634
|
-
: '';
|
|
635
|
-
return {
|
|
636
|
-
content: [
|
|
637
|
-
{
|
|
638
|
-
type: 'text' as const,
|
|
639
|
-
text:
|
|
640
|
-
`What's new (last ${data.window.days}d): ${sum.total_pricing_changes} price changes, ${sum.new_models} new models, ${sum.removed_models} removed, ${sum.incidents} incidents, ${sum.news_articles} headlines\n` +
|
|
641
|
-
`Live: ${data.status.currently_operational} operational, ${data.status.currently_degraded} degraded, ${data.status.currently_down} down` +
|
|
642
|
-
priceLines + newLines + incidentLines + newsLines +
|
|
643
|
-
`\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
644
|
-
},
|
|
645
|
-
],
|
|
646
|
-
};
|
|
647
|
-
},
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
// ── Tool: compare_models (1 credit) ─────────────────────────────────
|
|
651
|
-
|
|
652
|
-
server.tool(
|
|
653
|
-
'compare_models',
|
|
654
|
-
'Side-by-side comparison of 2-5 AI models. Returns pricing, benchmarks (normalized to a union of keys with null for missing scores), provider status, and recent news per model, plus rankings (cheapest blended, most context, per-benchmark leaderboard). Costs 1 credit.',
|
|
655
|
-
{
|
|
656
|
-
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"'),
|
|
657
|
-
},
|
|
658
|
-
async ({ ids }) => {
|
|
659
|
-
const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true })) as {
|
|
660
|
-
benchmark_keys: string[];
|
|
661
|
-
models: (
|
|
662
|
-
| { matched: true; name: string; provider: string; pricing: { input: number; output: number; blended: number }; context_window: number | null; status: string; benchmarks: Record<string, number | null> }
|
|
663
|
-
| { matched: false; query: string; reason: string }
|
|
664
|
-
)[];
|
|
665
|
-
rankings: {
|
|
666
|
-
cheapest_blended: { name: string; blended: number }[];
|
|
667
|
-
most_context: { name: string; context_window: number }[];
|
|
668
|
-
by_benchmark: Record<string, { name: string; score: number }[]>;
|
|
669
|
-
};
|
|
670
|
-
billing?: { credits_remaining?: number };
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
const lines = data.models.map(m => {
|
|
674
|
-
if (!m.matched) return ` ${m.query}: not found`;
|
|
675
|
-
const benches = data.benchmark_keys
|
|
676
|
-
.map(k => `${k}=${m.benchmarks[k] ?? '-'}`)
|
|
677
|
-
.join(', ');
|
|
678
|
-
return ` ${m.name} (${m.provider}) [${m.status}]\n in $${m.pricing.input}/1M, out $${m.pricing.output}/1M, ctx ${m.context_window ?? '-'}\n ${benches}`;
|
|
679
|
-
}).join('\n\n');
|
|
680
|
-
|
|
681
|
-
const cheapest = data.rankings.cheapest_blended.map((r, i) => ` #${i + 1} ${r.name} ($${r.blended}/1M blended)`).join('\n');
|
|
682
|
-
const ctxLine = data.rankings.most_context.map((r, i) => ` #${i + 1} ${r.name} (${r.context_window})`).join('\n');
|
|
683
|
-
const benchSections = Object.entries(data.rankings.by_benchmark)
|
|
684
|
-
.map(([k, v]) => ` ${k}: ${v.map(r => `${r.name}=${r.score}`).join(' > ')}`)
|
|
685
|
-
.join('\n');
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
content: [
|
|
689
|
-
{
|
|
690
|
-
type: 'text' as const,
|
|
691
|
-
text: `Models compared:\n${lines}\n\nCheapest blended:\n${cheapest}\n\nMost context:\n${ctxLine || ' (no context data)'}\n\nBenchmark leaders:\n${benchSections || ' (no benchmark data)'}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
692
|
-
},
|
|
693
|
-
],
|
|
694
|
-
};
|
|
695
|
-
},
|
|
696
|
-
);
|
|
697
|
-
|
|
698
|
-
// ── Tool: provider_deepdive (1 credit) ──────────────────────────────
|
|
699
|
-
|
|
700
|
-
server.tool(
|
|
701
|
-
'provider_deepdive',
|
|
702
|
-
'Everything about an AI provider in one call: live status, all models with pricing + tier + benchmark scores joined in, recent news mentions, and agent traffic. Costs 1 credit. Aggregation IS the value; doing this from free endpoints would take 4 round-trips.',
|
|
703
|
-
{
|
|
704
|
-
provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
|
|
705
|
-
},
|
|
706
|
-
async ({ provider }) => {
|
|
707
|
-
const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true })) as {
|
|
708
|
-
provider: { name: string; url: string | null };
|
|
709
|
-
status: { state: string; status_page_url: string | null; last_checked: string | null };
|
|
710
|
-
models: { name: string; tier: string | null; pricing: { input: number; output: number }; benchmark_scores: Record<string, number> }[];
|
|
711
|
-
recent_news: { title: string; url: string; published_at: string }[];
|
|
712
|
-
recent_news_count: number;
|
|
713
|
-
agent_traffic_24h: number;
|
|
714
|
-
billing?: { credits_remaining?: number };
|
|
715
|
-
};
|
|
716
|
-
const modelLines = data.models
|
|
717
|
-
.map(m => {
|
|
718
|
-
const benchKeys = Object.keys(m.benchmark_scores);
|
|
719
|
-
const benches = benchKeys.length ? ` | ${benchKeys.map(k => `${k}=${m.benchmark_scores[k]}`).join(', ')}` : '';
|
|
720
|
-
return ` ${m.name} [${m.tier ?? '-'}] in $${m.pricing.input}/1M, out $${m.pricing.output}/1M${benches}`;
|
|
721
|
-
})
|
|
722
|
-
.join('\n');
|
|
723
|
-
const newsLines = data.recent_news
|
|
724
|
-
.slice(0, 5)
|
|
725
|
-
.map(n => ` - ${n.title} (${n.published_at})\n ${n.url}`)
|
|
726
|
-
.join('\n');
|
|
727
|
-
return {
|
|
728
|
-
content: [
|
|
729
|
-
{
|
|
730
|
-
type: 'text' as const,
|
|
731
|
-
text:
|
|
732
|
-
`${data.provider.name} (${data.provider.url ?? 'no url'})\n` +
|
|
733
|
-
`Status: ${data.status.state}${data.status.status_page_url ? ` (${data.status.status_page_url})` : ''}, last checked ${data.status.last_checked ?? 'unknown'}\n` +
|
|
734
|
-
`Agent traffic (24h): ${data.agent_traffic_24h}\n\n` +
|
|
735
|
-
`Models (${data.models.length}):\n${modelLines}\n\n` +
|
|
736
|
-
`Recent news (${data.recent_news_count} matched, top 5 shown):\n${newsLines || ' none'}\n\n` +
|
|
737
|
-
`Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
};
|
|
741
|
-
},
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
// ── Tool: forecast (1 credit) ───────────────────────────────────────
|
|
745
|
-
|
|
746
|
-
server.tool(
|
|
747
|
-
'forecast',
|
|
748
|
-
'Conservative statistical forecast for an AI model price or benchmark series. Linear least-squares fit on 7-90 days of history projected forward 1-30 days with a 95% prediction interval and confidence label. Costs 1 credit.',
|
|
749
|
-
{
|
|
750
|
-
target: z.enum(['price', 'benchmark']).describe('Forecast a price field or a benchmark score'),
|
|
751
|
-
model: z.string().describe('Model id or display name'),
|
|
752
|
-
field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('Required when target=price'),
|
|
753
|
-
benchmark: z.string().optional().describe('Required when target=benchmark (e.g. swe_bench, mmlu_pro)'),
|
|
754
|
-
lookback: z.number().min(7).max(90).optional().describe('Days of history to fit on (default 30)'),
|
|
755
|
-
horizon: z.number().min(1).max(30).optional().describe('Days into the future to project (default 7)'),
|
|
756
|
-
},
|
|
757
|
-
async ({ target, model, field, benchmark, lookback, horizon }) => {
|
|
758
|
-
const params = new URLSearchParams({ target, model });
|
|
759
|
-
if (field) params.set('field', field);
|
|
760
|
-
if (benchmark) params.set('benchmark', benchmark);
|
|
761
|
-
if (typeof lookback === 'number') params.set('lookback', String(lookback));
|
|
762
|
-
if (typeof horizon === 'number') params.set('horizon', String(horizon));
|
|
763
|
-
const data = (await fetchJSON(`/premium/forecast?${params}`, { auth: true })) as {
|
|
764
|
-
target: string;
|
|
765
|
-
model: string;
|
|
766
|
-
field?: string;
|
|
767
|
-
benchmark?: string;
|
|
768
|
-
current_value: number;
|
|
769
|
-
trend: { slope_per_day: number; r_squared: number };
|
|
770
|
-
confidence: { score: number; label: string };
|
|
771
|
-
forecast: { date: string; predicted: number; lower: number; upper: number }[];
|
|
772
|
-
notes: string[];
|
|
773
|
-
billing?: { credits_remaining?: number };
|
|
774
|
-
};
|
|
775
|
-
const what = target === 'price' ? `${data.model} ${data.field}` : `${data.model} ${data.benchmark}`;
|
|
776
|
-
const trendLine = `current=${data.current_value}, slope=${data.trend.slope_per_day}/day, r2=${data.trend.r_squared}, confidence=${data.confidence.label} (${data.confidence.score})`;
|
|
777
|
-
const forecastLines = data.forecast
|
|
778
|
-
.map(p => ` ${p.date}: ${p.predicted} (95% CI [${p.lower}, ${p.upper}])`)
|
|
779
|
-
.join('\n');
|
|
780
|
-
return {
|
|
781
|
-
content: [
|
|
782
|
-
{
|
|
783
|
-
type: 'text' as const,
|
|
784
|
-
text: `Forecast: ${what}\n${trendLine}\n\n${forecastLines}\n\nNotes: ${data.notes.join(' ')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
};
|
|
788
|
-
},
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
// ── Tool: cost_projection (1 credit) ────────────────────────────────
|
|
792
|
-
|
|
793
|
-
server.tool(
|
|
794
|
-
'cost_projection',
|
|
795
|
-
'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.',
|
|
796
|
-
{
|
|
797
|
-
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.'),
|
|
798
|
-
input_tokens_per_day: z.number().min(0).describe('Expected daily input token volume'),
|
|
799
|
-
output_tokens_per_day: z.number().min(0).describe('Expected daily output token volume'),
|
|
800
|
-
horizon: z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().describe('Primary horizon to highlight (default monthly). All four are always computed.'),
|
|
801
|
-
},
|
|
802
|
-
async ({ models, input_tokens_per_day, output_tokens_per_day, horizon }) => {
|
|
803
|
-
const params = new URLSearchParams({
|
|
804
|
-
model: models,
|
|
805
|
-
input_tokens_per_day: String(input_tokens_per_day),
|
|
806
|
-
output_tokens_per_day: String(output_tokens_per_day),
|
|
807
|
-
});
|
|
808
|
-
if (horizon) params.set('horizon', horizon);
|
|
809
|
-
const data = (await fetchJSON(`/premium/cost/projection?${params}`, { auth: true })) as {
|
|
810
|
-
workload: { input_tokens_per_day: number; output_tokens_per_day: number };
|
|
811
|
-
projections: (
|
|
812
|
-
| { model: string; provider: string; matched: true; daily: { total: number }; weekly_total: number; monthly_total: number; yearly_total: number; rates: { input_per_1m: number; output_per_1m: number } }
|
|
813
|
-
| { model: string; matched: false; reason: string }
|
|
814
|
-
)[];
|
|
815
|
-
ranked_cheapest_monthly: { model: string; provider: string; monthly_total: number }[];
|
|
816
|
-
billing?: { credits_remaining?: number };
|
|
817
|
-
};
|
|
818
|
-
const lines = data.projections.map(p => {
|
|
819
|
-
if (!p.matched) return ` ${p.model}: not found in pricing catalog`;
|
|
820
|
-
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)`;
|
|
821
|
-
}).join('\n');
|
|
822
|
-
const ranked = data.ranked_cheapest_monthly.length
|
|
823
|
-
? '\n\nCheapest monthly:\n' +
|
|
824
|
-
data.ranked_cheapest_monthly
|
|
825
|
-
.map((r, i) => ` #${i + 1} ${r.model} (${r.provider}): $${r.monthly_total}/mo`)
|
|
826
|
-
.join('\n')
|
|
827
|
-
: '';
|
|
828
|
-
return {
|
|
829
|
-
content: [
|
|
830
|
-
{
|
|
831
|
-
type: 'text' as const,
|
|
832
|
-
text:
|
|
833
|
-
`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 ?? '?'}`,
|
|
834
|
-
},
|
|
835
|
-
],
|
|
836
|
-
};
|
|
837
|
-
},
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
// ── Tool: news_search (1 credit) ────────────────────────────────────
|
|
841
|
-
|
|
842
|
-
server.tool(
|
|
843
|
-
'news_search',
|
|
844
|
-
'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.',
|
|
845
|
-
{
|
|
846
|
-
q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
|
|
847
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
|
|
848
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
|
|
849
|
-
provider: z.string().optional().describe('Substring match against source name and domain (e.g. "anthropic", "openai", "techcrunch")'),
|
|
850
|
-
category: z.string().optional().describe('Substring match against article categories'),
|
|
851
|
-
limit: z.number().min(1).max(100).optional().describe('Max results (default 25, max 100)'),
|
|
852
|
-
},
|
|
853
|
-
async ({ q, from, to, provider, category, limit }) => {
|
|
854
|
-
const params = new URLSearchParams();
|
|
855
|
-
if (q) params.set('q', q);
|
|
856
|
-
if (from) params.set('from', from);
|
|
857
|
-
if (to) params.set('to', to);
|
|
858
|
-
if (provider) params.set('provider', provider);
|
|
859
|
-
if (category) params.set('category', category);
|
|
860
|
-
if (typeof limit === 'number') params.set('limit', String(limit));
|
|
861
|
-
const data = (await fetchJSON(`/premium/news/search?${params}`, { auth: true })) as {
|
|
862
|
-
query: string | null;
|
|
863
|
-
matched: number;
|
|
864
|
-
returned: number;
|
|
865
|
-
results: { title: string; url: string; source: string; published_at: string; relevance: number; matched_terms: string[]; snippet: string }[];
|
|
866
|
-
billing?: { credits_remaining?: number };
|
|
867
|
-
};
|
|
868
|
-
if (data.results.length === 0) {
|
|
869
|
-
return { content: [{ type: 'text' as const, text: `No articles matched. (matched: ${data.matched})` }] };
|
|
870
|
-
}
|
|
871
|
-
const list = data.results
|
|
872
|
-
.map(
|
|
873
|
-
(r, i) =>
|
|
874
|
-
`${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}`,
|
|
875
|
-
)
|
|
876
|
-
.join('\n\n');
|
|
877
|
-
return {
|
|
878
|
-
content: [
|
|
879
|
-
{
|
|
880
|
-
type: 'text' as const,
|
|
881
|
-
text: `${data.returned} of ${data.matched} matches${data.query ? ` for "${data.query}"` : ''}:\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
882
|
-
},
|
|
883
|
-
],
|
|
884
|
-
};
|
|
885
|
-
},
|
|
886
|
-
);
|
|
887
|
-
|
|
888
|
-
// ── Tool: list_watches ──────────────────────────────────────────────
|
|
889
|
-
|
|
890
|
-
server.tool(
|
|
891
|
-
'list_watches',
|
|
892
|
-
'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.',
|
|
893
|
-
{},
|
|
894
|
-
async () => {
|
|
895
|
-
const data = (await fetchJSON('/premium/watches', { auth: true })) as {
|
|
896
|
-
count: number;
|
|
897
|
-
watches: {
|
|
898
|
-
id: string;
|
|
899
|
-
spec: { type: string; model?: string; field?: string; op?: string; threshold?: number; provider?: string; value?: string };
|
|
900
|
-
callback_url: string;
|
|
901
|
-
fire_count: number;
|
|
902
|
-
fire_cap: number;
|
|
903
|
-
expires_at: string;
|
|
904
|
-
status: string;
|
|
905
|
-
}[];
|
|
906
|
-
};
|
|
907
|
-
if (data.count === 0) {
|
|
908
|
-
return { content: [{ type: 'text' as const, text: 'No active watches.' }] };
|
|
909
|
-
}
|
|
910
|
-
const rows = data.watches
|
|
911
|
-
.map(w => {
|
|
912
|
-
const desc =
|
|
913
|
-
w.spec.type === 'price'
|
|
914
|
-
? `${w.spec.model} ${w.spec.field} ${w.spec.op}${w.spec.threshold !== undefined ? ' ' + w.spec.threshold : ''}`
|
|
915
|
-
: `${w.spec.provider} ${w.spec.op}${w.spec.value ? ' ' + w.spec.value : ''}`;
|
|
916
|
-
return ` ${w.id} (${w.status}) [${w.spec.type}] ${desc}\n -> ${w.callback_url}\n fired ${w.fire_count}/${w.fire_cap}, expires ${w.expires_at}`;
|
|
917
|
-
})
|
|
918
|
-
.join('\n\n');
|
|
919
|
-
return { content: [{ type: 'text' as const, text: `${data.count} active watches:\n\n${rows}` }] };
|
|
920
|
-
},
|
|
921
|
-
);
|
|
922
|
-
|
|
923
|
-
// ── Tool: create_price_watch (1 credit) ─────────────────────────────
|
|
924
|
-
|
|
925
|
-
server.tool(
|
|
926
|
-
'create_price_watch',
|
|
927
|
-
'Register a webhook watch on a model price change. Costs 1 credit. Watch lives 90 days. Each fire is an HMAC-signed POST to callback_url.',
|
|
928
|
-
{
|
|
929
|
-
model: z.string().describe('Model name (e.g. "Claude Opus 4.7")'),
|
|
930
|
-
field: z.enum(['inputPrice', 'outputPrice', 'blended']).describe('Which price field to watch'),
|
|
931
|
-
op: z.enum(['lt', 'gt', 'changes']).describe('Trigger: lt = below threshold, gt = above, changes = any change'),
|
|
932
|
-
threshold: z.number().optional().describe('Required when op is lt or gt; ignored for changes'),
|
|
933
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
|
|
934
|
-
secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies'),
|
|
935
|
-
},
|
|
936
|
-
async ({ model, field, op, threshold, callback_url, secret }) => {
|
|
937
|
-
const body: Record<string, unknown> = {
|
|
938
|
-
spec: { type: 'price', model, field, op, ...(typeof threshold === 'number' ? { threshold } : {}) },
|
|
939
|
-
callback_url,
|
|
940
|
-
};
|
|
941
|
-
if (secret !== undefined) body.secret = secret;
|
|
942
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true })) as {
|
|
943
|
-
watch: { id: string; expires_at: string };
|
|
944
|
-
billing?: { credits_remaining?: number };
|
|
945
|
-
};
|
|
946
|
-
return {
|
|
947
|
-
content: [
|
|
948
|
-
{
|
|
949
|
-
type: 'text' as const,
|
|
950
|
-
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
951
|
-
},
|
|
952
|
-
],
|
|
953
|
-
};
|
|
954
|
-
},
|
|
955
|
-
);
|
|
956
|
-
|
|
957
|
-
// ── Tool: create_status_watch (1 credit) ────────────────────────────
|
|
958
|
-
|
|
959
|
-
server.tool(
|
|
960
|
-
'create_status_watch',
|
|
961
|
-
'Register a webhook watch on a service status transition (e.g. anthropic becomes down). Costs 1 credit. Watch lives 90 days.',
|
|
962
|
-
{
|
|
963
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai)'),
|
|
964
|
-
op: z.enum(['becomes', 'changes']).describe('becomes = transitions to a specific value; changes = any transition'),
|
|
965
|
-
value: z.enum(['operational', 'degraded', 'down']).optional().describe('Required when op is becomes'),
|
|
966
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
|
|
967
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
968
|
-
},
|
|
969
|
-
async ({ provider, op, value, callback_url, secret }) => {
|
|
970
|
-
const body: Record<string, unknown> = {
|
|
971
|
-
spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
|
|
972
|
-
callback_url,
|
|
973
|
-
};
|
|
974
|
-
if (secret !== undefined) body.secret = secret;
|
|
975
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true })) as {
|
|
976
|
-
watch: { id: string; expires_at: string };
|
|
977
|
-
billing?: { credits_remaining?: number };
|
|
978
|
-
};
|
|
979
|
-
return {
|
|
980
|
-
content: [
|
|
981
|
-
{
|
|
982
|
-
type: 'text' as const,
|
|
983
|
-
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
984
|
-
},
|
|
985
|
-
],
|
|
986
|
-
};
|
|
987
|
-
},
|
|
988
|
-
);
|
|
989
|
-
|
|
990
|
-
// ── Tool: create_digest_watch (1 credit) ────────────────────────────
|
|
991
|
-
|
|
992
|
-
server.tool(
|
|
993
|
-
'create_digest_watch',
|
|
994
|
-
'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. Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.',
|
|
995
|
-
{
|
|
996
|
-
cadence: z.enum(['daily', 'weekly']).describe('How often the digest fires'),
|
|
997
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the digest fires'),
|
|
998
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
999
|
-
},
|
|
1000
|
-
async ({ cadence, callback_url, secret }) => {
|
|
1001
|
-
const body: Record<string, unknown> = {
|
|
1002
|
-
spec: { type: 'digest', cadence },
|
|
1003
|
-
callback_url,
|
|
1004
|
-
};
|
|
1005
|
-
if (secret !== undefined) body.secret = secret;
|
|
1006
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true })) as {
|
|
1007
|
-
watch: { id: string; expires_at: string };
|
|
1008
|
-
billing?: { credits_remaining?: number };
|
|
1009
|
-
};
|
|
1010
|
-
return {
|
|
1011
|
-
content: [
|
|
1012
|
-
{
|
|
1013
|
-
type: 'text' as const,
|
|
1014
|
-
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 ?? '?'}`,
|
|
1015
|
-
},
|
|
1016
|
-
],
|
|
1017
|
-
};
|
|
1018
|
-
},
|
|
1019
|
-
);
|
|
1020
|
-
|
|
1021
|
-
// ── Tool: delete_watch ──────────────────────────────────────────────
|
|
1022
|
-
|
|
1023
|
-
server.tool(
|
|
1024
|
-
'delete_watch',
|
|
1025
|
-
'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.',
|
|
1026
|
-
{
|
|
1027
|
-
watch_id: z.string().describe('The wat_... id from create_price_watch / create_status_watch / list_watches'),
|
|
1028
|
-
},
|
|
1029
|
-
async ({ watch_id }) => {
|
|
1030
|
-
await fetchJSON(`/premium/watches/${watch_id}`, { method: 'DELETE', auth: true });
|
|
1031
|
-
return { content: [{ type: 'text' as const, text: `Deleted watch ${watch_id}.` }] };
|
|
1032
|
-
},
|
|
1033
|
-
);
|
|
1034
|
-
|
|
1035
|
-
// ── Start ───────────────────────────────────────────────────────────
|
|
1036
|
-
|
|
1037
|
-
async function main() {
|
|
1038
|
-
const transport = new StdioServerTransport();
|
|
1039
|
-
await server.connect(transport);
|
|
1040
|
-
console.error('TensorFeed MCP server running on stdio');
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
main().catch(console.error);
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"types": ["node"]
|
|
13
|
-
},
|
|
14
|
-
"include": ["src"]
|
|
15
|
-
}
|