@finchagentic/mcp 4.6.0 → 4.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/package.json +5 -6
- package/dist/_http-cache.js +0 -96
- package/dist/agent-loop.js +0 -301
- package/dist/annotations.js +0 -122
- package/dist/cli.js +0 -1391
- package/dist/clink-input.js +0 -15
- package/dist/config.js +0 -132
- package/dist/convex.js +0 -175
- package/dist/dex-pair.js +0 -54
- package/dist/enrichment-router.js +0 -315
- package/dist/index.js +0 -258
- package/dist/llm.js +0 -298
- package/dist/local-memory-file.js +0 -147
- package/dist/local-memory.js +0 -135
- package/dist/local-vault.js +0 -454
- package/dist/output-schemas.js +0 -605
- package/dist/project.js +0 -36
- package/dist/prompts.js +0 -111
- package/dist/public-url.js +0 -107
- package/dist/resources.js +0 -111
- package/dist/server.js +0 -322
- package/dist/signal-gate.js +0 -57
- package/dist/token-decimals.js +0 -26
- package/dist/token-gate.js +0 -88
- package/dist/tool-filter.js +0 -53
- package/dist/tools/_solidity-scan.js +0 -313
- package/dist/tools/agents.js +0 -441
- package/dist/tools/automation.js +0 -354
- package/dist/tools/base-mcp.js +0 -466
- package/dist/tools/base.js +0 -283
- package/dist/tools/chronicle.js +0 -268
- package/dist/tools/coder.js +0 -94
- package/dist/tools/deep-research.js +0 -1421
- package/dist/tools/defi.js +0 -292
- package/dist/tools/equity.js +0 -372
- package/dist/tools/events.js +0 -182
- package/dist/tools/github.js +0 -564
- package/dist/tools/insider.js +0 -264
- package/dist/tools/insight.js +0 -630
- package/dist/tools/market.js +0 -555
- package/dist/tools/memory.js +0 -1044
- package/dist/tools/miroshark.js +0 -350
- package/dist/tools/monitor.js +0 -319
- package/dist/tools/os.js +0 -236
- package/dist/tools/packets.js +0 -296
- package/dist/tools/research-chain.js +0 -226
- package/dist/tools/research-compare.js +0 -280
- package/dist/tools/research.js +0 -188
- package/dist/tools/rh-bridge.js +0 -148
- package/dist/tools/rh-mcp.js +0 -1448
- package/dist/tools/rh-orders.js +0 -556
- package/dist/tools/scanner.js +0 -564
- package/dist/tools/stake.js +0 -369
- package/dist/tools/vault.js +0 -1020
- package/dist/tools/wallet.js +0 -200
- package/dist/types.js +0 -2
- package/dist/wallet.js +0 -372
package/dist/tools/defi.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFI_TOOLS = void 0;
|
|
4
|
-
exports.buildDefiYields = buildDefiYields;
|
|
5
|
-
exports.handleDefiTool = handleDefiTool;
|
|
6
|
-
const zod_1 = require("zod");
|
|
7
|
-
const convex_js_1 = require("../convex.js");
|
|
8
|
-
const wallet_js_1 = require("../wallet.js");
|
|
9
|
-
const token_decimals_js_1 = require("../token-decimals.js");
|
|
10
|
-
exports.DEFI_TOOLS = [
|
|
11
|
-
{
|
|
12
|
-
name: "get_defi_yields",
|
|
13
|
-
description: "Fetch top DeFi yield opportunities on Base - Morpho, Moonwell, Aerodrome, Uniswap, and more. " +
|
|
14
|
-
"Returns APY, TVL, and pool info aggregated from DeFiLlama (no API key required) - broadest " +
|
|
15
|
-
"protocol coverage, but DeFiLlama's numbers can lag the protocol's own API by hours. Good first " +
|
|
16
|
-
"stop for comparing across protocols. For the freshest Morpho-vault-specific numbers use " +
|
|
17
|
-
"base_mcp_yield_vaults instead; for the freshest Moonwell supply/borrow rates use " +
|
|
18
|
-
"base_mcp_lending_rates instead - both query the protocol directly. " +
|
|
19
|
-
"Filter by token or minimum APY. Use before depositing to find the best rates.",
|
|
20
|
-
inputSchema: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: {
|
|
23
|
-
token: { type: "string", description: "Optional: filter by token symbol, e.g. 'USDC', 'ETH', 'WETH'" },
|
|
24
|
-
minApy: { type: "number", description: "Optional: minimum APY % to show (default 1)" },
|
|
25
|
-
limit: { type: "number", description: "Max results to return (default 20)" },
|
|
26
|
-
},
|
|
27
|
-
required: [],
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
const SwapSchema = zod_1.z.object({
|
|
32
|
-
fromToken: zod_1.z.string().min(1),
|
|
33
|
-
toToken: zod_1.z.string().min(1),
|
|
34
|
-
amount: zod_1.z.string().min(1),
|
|
35
|
-
maxSlippagePct: zod_1.z.number().positive().max(50).optional(),
|
|
36
|
-
maxPriceImpactPct: zod_1.z.number().positive().max(50).optional(),
|
|
37
|
-
});
|
|
38
|
-
const DEFAULT_MAX_SLIPPAGE_PCT = 1.0;
|
|
39
|
-
const DEFAULT_MAX_PRICE_IMPACT_PCT = 3.0;
|
|
40
|
-
const SendSchema = zod_1.z.object({ token: zod_1.z.string().min(1), toAddress: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), amount: zod_1.z.string().min(1) });
|
|
41
|
-
const DefiYieldsSchema = zod_1.z.object({
|
|
42
|
-
token: zod_1.z.string().optional(),
|
|
43
|
-
minApy: zod_1.z.number().optional(),
|
|
44
|
-
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
45
|
-
}).default({});
|
|
46
|
-
function formatTokenAmount(raw, token) {
|
|
47
|
-
const dec = (0, token_decimals_js_1.decimalsFor)(token);
|
|
48
|
-
// Unknown decimals: show raw units rather than a confidently wrong number.
|
|
49
|
-
if (dec === undefined)
|
|
50
|
-
return `${raw} (raw units — decimals unknown)`;
|
|
51
|
-
// BigInt math: parseInt on an 18-digit wei string loses precision past
|
|
52
|
-
// MAX_SAFE_INTEGER.
|
|
53
|
-
try {
|
|
54
|
-
const places = dec === 6 ? 2 : 6;
|
|
55
|
-
const scale = 10n ** BigInt(dec);
|
|
56
|
-
const value = BigInt(raw);
|
|
57
|
-
const whole = value / scale;
|
|
58
|
-
const frac = (value % scale).toString().padStart(dec, "0").slice(0, places);
|
|
59
|
-
return places > 0 ? `${whole}.${frac}` : `${whole}`;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return raw;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Structured output builder for get_defi_yields (schema in output-schemas.ts).
|
|
66
|
-
function buildDefiYields(token, minApy, pools) {
|
|
67
|
-
return {
|
|
68
|
-
token: token ? token.toUpperCase() : null,
|
|
69
|
-
minApy,
|
|
70
|
-
count: pools.length,
|
|
71
|
-
pools: pools.map((p) => ({
|
|
72
|
-
symbol: p.symbol ?? p.pool ?? null,
|
|
73
|
-
project: p.project ?? null,
|
|
74
|
-
apyPct: p.apy ?? null,
|
|
75
|
-
tvlUsd: p.tvlUsd ?? null,
|
|
76
|
-
chain: p.chain ?? null,
|
|
77
|
-
})),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
async function handleDefiTool(name, args) {
|
|
81
|
-
switch (name) {
|
|
82
|
-
case "get_portfolio": {
|
|
83
|
-
const result = await (0, convex_js_1.callConvex)("/mcp/defi/portfolio", "GET", undefined, "get_portfolio");
|
|
84
|
-
if (result.error)
|
|
85
|
-
return { content: [{ type: "text", text: `Portfolio fetch failed: ${result.error}` }], isError: true };
|
|
86
|
-
const balances = result.balances ?? [];
|
|
87
|
-
const totalUsd = result.totalUsd ?? result.totalValueUsd ?? balances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
|
|
88
|
-
if (!balances.length) {
|
|
89
|
-
return { content: [{ type: "text", text: "Your wallet has no tokens yet. Send ETH or USDC on Base to get started." }] };
|
|
90
|
-
}
|
|
91
|
-
const lines = [`**Portfolio** - Total: $${totalUsd.toFixed(2)}`, ""];
|
|
92
|
-
for (const b of balances) {
|
|
93
|
-
const value = b.valueUsd != null ? ` ($${Number(b.valueUsd).toFixed(2)})` : "";
|
|
94
|
-
lines.push(`• **${b.token ?? b.symbol}**: ${Number(b.balance ?? b.amount).toLocaleString(undefined, { maximumFractionDigits: 6 })}${value}`);
|
|
95
|
-
}
|
|
96
|
-
lines.push("", `Wallet: \`${result.address ?? "unknown"}\``);
|
|
97
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
98
|
-
}
|
|
99
|
-
case "estimate_swap": {
|
|
100
|
-
const parsed = SwapSchema.safeParse(args);
|
|
101
|
-
if (!parsed.success)
|
|
102
|
-
return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
|
|
103
|
-
const { fromToken, toToken, amount, maxSlippagePct, maxPriceImpactPct } = parsed.data;
|
|
104
|
-
const slippageLimit = maxSlippagePct ?? DEFAULT_MAX_SLIPPAGE_PCT;
|
|
105
|
-
const impactLimit = maxPriceImpactPct ?? DEFAULT_MAX_PRICE_IMPACT_PCT;
|
|
106
|
-
// Pass the MCP local wallet as the swap taker so 0x routes output back
|
|
107
|
-
// to the wallet that's signing - not the backend's custodial wallet.
|
|
108
|
-
const localWallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
109
|
-
const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "swap_tokens");
|
|
110
|
-
if (!result.success)
|
|
111
|
-
return { content: [{ type: "text", text: `Estimate failed: ${result.error}` }], isError: true };
|
|
112
|
-
const q = result.quote;
|
|
113
|
-
const buyHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
|
|
114
|
-
const sellHuman = formatTokenAmount(q.sellAmount ?? "0", q.sellToken ?? fromToken);
|
|
115
|
-
const impactPct = q.estimatedPriceImpact != null ? Number(q.estimatedPriceImpact) : 0;
|
|
116
|
-
const priceImpact = q.estimatedPriceImpact != null ? `${impactPct.toFixed(3)}%` : "< 0.01%";
|
|
117
|
-
const impactWarning = impactPct > impactLimit
|
|
118
|
-
? `\n⚠️ **Price impact ${impactPct.toFixed(2)}% exceeds limit ${impactLimit}%** - \`swap_tokens\` will refuse execution. Increase \`maxPriceImpactPct\` to override.`
|
|
119
|
-
: "";
|
|
120
|
-
return {
|
|
121
|
-
content: [{
|
|
122
|
-
type: "text",
|
|
123
|
-
text: [
|
|
124
|
-
`**Swap Estimate** (not executed)`,
|
|
125
|
-
``,
|
|
126
|
-
`You sell: **${sellHuman} ${(q.sellToken ?? fromToken).toUpperCase()}**`,
|
|
127
|
-
`You get: **~${buyHuman} ${(q.buyToken ?? toToken).toUpperCase()}**`,
|
|
128
|
-
`Price impact: ${priceImpact}`,
|
|
129
|
-
`Slippage cap: ${slippageLimit}% · Price-impact cap: ${impactLimit}%`,
|
|
130
|
-
impactWarning,
|
|
131
|
-
``,
|
|
132
|
-
`Run \`swap_tokens\` with the same params to execute.`,
|
|
133
|
-
].join("\n"),
|
|
134
|
-
}],
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
case "swap_tokens": {
|
|
138
|
-
const parsed = SwapSchema.safeParse(args);
|
|
139
|
-
if (!parsed.success)
|
|
140
|
-
return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
|
|
141
|
-
const { fromToken, toToken, amount, maxSlippagePct, maxPriceImpactPct } = parsed.data;
|
|
142
|
-
const slippageLimit = maxSlippagePct ?? DEFAULT_MAX_SLIPPAGE_PCT;
|
|
143
|
-
const impactLimit = maxPriceImpactPct ?? DEFAULT_MAX_PRICE_IMPACT_PCT;
|
|
144
|
-
const wallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
145
|
-
const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: wallet.address, slippagePercentage: slippageLimit / 100 }, "swap_tokens");
|
|
146
|
-
if (!result.success)
|
|
147
|
-
return { content: [{ type: "text", text: `Swap failed: ${result.error}` }], isError: true };
|
|
148
|
-
const q = result.quote;
|
|
149
|
-
const impactPct = q.estimatedPriceImpact != null ? Number(q.estimatedPriceImpact) : 0;
|
|
150
|
-
if (impactPct > impactLimit) {
|
|
151
|
-
return {
|
|
152
|
-
content: [{
|
|
153
|
-
type: "text",
|
|
154
|
-
text: [
|
|
155
|
-
`🛑 **Swap refused - price impact too high.**`,
|
|
156
|
-
``,
|
|
157
|
-
`Quoted price impact: **${impactPct.toFixed(3)}%**`,
|
|
158
|
-
`Configured cap: **${impactLimit}%**`,
|
|
159
|
-
``,
|
|
160
|
-
`Override by passing \`maxPriceImpactPct: ${Math.ceil(impactPct) + 1}\` if you understand the risk,`,
|
|
161
|
-
`or reduce \`amount\` to lower the impact.`,
|
|
162
|
-
].join("\n"),
|
|
163
|
-
}],
|
|
164
|
-
isError: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, q);
|
|
168
|
-
const buyAmountHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
|
|
169
|
-
const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
|
|
170
|
-
const header = !receipt.mined
|
|
171
|
-
? `⏳ Broadcast, not yet confirmed within the wait window.`
|
|
172
|
-
: receipt.ok
|
|
173
|
-
? `✅ Swap confirmed on-chain (block ${receipt.blockNumber}).`
|
|
174
|
-
: `❌ Transaction REVERTED - no swap occurred (gas was still spent).`;
|
|
175
|
-
return {
|
|
176
|
-
content: [{
|
|
177
|
-
type: "text",
|
|
178
|
-
text: [
|
|
179
|
-
header,
|
|
180
|
-
`${amount} ${fromToken.toUpperCase()} → ${receipt.mined && receipt.ok ? buyAmountHuman : "(quoted, not received)"} ${q.buyToken}`,
|
|
181
|
-
`Slippage cap: ${slippageLimit}% · Price impact: ${impactPct.toFixed(3)}%`,
|
|
182
|
-
`Tx Hash: \`${txHash}\``,
|
|
183
|
-
`https://basescan.org/tx/${txHash}`,
|
|
184
|
-
!receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
|
|
185
|
-
].filter(Boolean).join("\n"),
|
|
186
|
-
}],
|
|
187
|
-
isError: receipt.mined && !receipt.ok,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
case "send_token": {
|
|
191
|
-
const parsed = SendSchema.safeParse(args);
|
|
192
|
-
if (!parsed.success)
|
|
193
|
-
return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
|
|
194
|
-
const { token, toAddress, amount } = parsed.data;
|
|
195
|
-
const wallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
196
|
-
const result = await (0, convex_js_1.callConvex)("/mcp/defi/send", "POST", parsed.data, "send_token");
|
|
197
|
-
if (!result.success)
|
|
198
|
-
return { content: [{ type: "text", text: `Send failed: ${result.error}` }], isError: true };
|
|
199
|
-
const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, result.txData);
|
|
200
|
-
const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
|
|
201
|
-
const header = !receipt.mined
|
|
202
|
-
? `⏳ Broadcast, not yet confirmed within the wait window.`
|
|
203
|
-
: receipt.ok
|
|
204
|
-
? `✅ Sent - confirmed on-chain (block ${receipt.blockNumber}).`
|
|
205
|
-
: `❌ Transaction REVERTED - funds were NOT sent (gas was still spent).`;
|
|
206
|
-
return {
|
|
207
|
-
content: [{
|
|
208
|
-
type: "text",
|
|
209
|
-
text: [
|
|
210
|
-
header,
|
|
211
|
-
`${amount} ${token.toUpperCase()} → \`${toAddress}\``,
|
|
212
|
-
`Tx Hash: \`${txHash}\``,
|
|
213
|
-
`https://basescan.org/tx/${txHash}`,
|
|
214
|
-
!receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
|
|
215
|
-
].filter(Boolean).join("\n"),
|
|
216
|
-
}],
|
|
217
|
-
isError: receipt.mined && !receipt.ok,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
// analyze_wallet was removed - it called POST /wallet/analyze, which was
|
|
221
|
-
// never registered in app/convex/http.ts (only a dangling section-header
|
|
222
|
-
// comment exists there). The tool was never in the exported DEFI_TOOLS
|
|
223
|
-
// list either, so this case was already unreachable dead code - no Tool
|
|
224
|
-
// registers the name "analyze_wallet" for the MCP dispatcher to route to.
|
|
225
|
-
case "get_defi_yields": {
|
|
226
|
-
const parsed = DefiYieldsSchema.safeParse(args ?? {});
|
|
227
|
-
if (!parsed.success)
|
|
228
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
229
|
-
const { token, minApy = 1, limit = 20 } = parsed.data;
|
|
230
|
-
let pools;
|
|
231
|
-
try {
|
|
232
|
-
const res = await fetch("https://yields.llama.fi/pools", { signal: AbortSignal.timeout(15000) });
|
|
233
|
-
if (!res.ok)
|
|
234
|
-
throw new Error(`HTTP ${res.status}`);
|
|
235
|
-
const data = await res.json();
|
|
236
|
-
pools = data.data ?? [];
|
|
237
|
-
}
|
|
238
|
-
catch (e) {
|
|
239
|
-
return { content: [{ type: "text", text: `DeFiLlama fetch failed: ${e.message}` }], isError: true };
|
|
240
|
-
}
|
|
241
|
-
// Filter to Base chain
|
|
242
|
-
let filtered = pools.filter((p) => p.chain === "Base");
|
|
243
|
-
// Filter by token if specified
|
|
244
|
-
if (token) {
|
|
245
|
-
const upper = token.toUpperCase();
|
|
246
|
-
filtered = filtered.filter((p) => (p.symbol ?? "").toUpperCase().includes(upper) ||
|
|
247
|
-
(p.underlyingTokens ?? []).some((t) => t.toUpperCase().includes(upper)));
|
|
248
|
-
}
|
|
249
|
-
// Filter by minimum APY and remove outliers (>10000% are usually broken)
|
|
250
|
-
filtered = filtered
|
|
251
|
-
.filter((p) => (p.apy ?? 0) >= minApy && (p.apy ?? 0) <= 10000)
|
|
252
|
-
.sort((a, b) => (b.apy ?? 0) - (a.apy ?? 0))
|
|
253
|
-
.slice(0, limit);
|
|
254
|
-
if (!filtered.length) {
|
|
255
|
-
return {
|
|
256
|
-
content: [{
|
|
257
|
-
type: "text",
|
|
258
|
-
text: [
|
|
259
|
-
`## DeFi Yields on Base`,
|
|
260
|
-
`No pools found${token ? ` for ${token.toUpperCase()}` : ""} with APY ≥ ${minApy}%.`,
|
|
261
|
-
``,
|
|
262
|
-
`Try lowering \`minApy\` or removing the token filter.`,
|
|
263
|
-
].join("\n"),
|
|
264
|
-
}],
|
|
265
|
-
structuredContent: buildDefiYields(token, minApy, []),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
const fmt = (n) => n >= 1000000000 ? `$${(n / 1000000000).toFixed(1)}B`
|
|
269
|
-
: n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M`
|
|
270
|
-
: n >= 1000 ? `$${(n / 1000).toFixed(0)}K`
|
|
271
|
-
: `$${n.toFixed(0)}`;
|
|
272
|
-
const lines = [
|
|
273
|
-
`## DeFi Yields on Base${token ? ` - ${token.toUpperCase()}` : ""}`,
|
|
274
|
-
`Top ${filtered.length} pools · APY ≥ ${minApy}% · Source: DeFiLlama`,
|
|
275
|
-
``,
|
|
276
|
-
`| # | Pool | Protocol | APY | TVL |`,
|
|
277
|
-
`|---|------|----------|-----|-----|`,
|
|
278
|
-
];
|
|
279
|
-
filtered.forEach((p, i) => {
|
|
280
|
-
const apy = (p.apy ?? 0).toFixed(1);
|
|
281
|
-
const tvl = fmt(p.tvlUsd ?? 0);
|
|
282
|
-
const name = (p.symbol ?? p.pool ?? "-").replace(/-/g, " ");
|
|
283
|
-
const proj = p.project ?? "-";
|
|
284
|
-
lines.push(`| ${i + 1} | ${name} | ${proj} | **${apy}%** | ${tvl} |`);
|
|
285
|
-
});
|
|
286
|
-
lines.push(``, `Use \`swap_tokens\` to position, then deposit via the protocol's UI. Always check smart contract risk before depositing.`);
|
|
287
|
-
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildDefiYields(token, minApy, filtered) };
|
|
288
|
-
}
|
|
289
|
-
default:
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
package/dist/tools/equity.js
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Public-company fundamentals from primary sources.
|
|
3
|
-
//
|
|
4
|
-
// Financials come from SEC EDGAR XBRL — the numbers as filed in 10-Q/10-K, not
|
|
5
|
-
// a vendor's re-typing of them. No API key, no rate-limit tier, and if a figure
|
|
6
|
-
// here is wrong it is wrong in the filing itself.
|
|
7
|
-
//
|
|
8
|
-
// The tool computes what is mechanical (margins, YoY growth, quarter vs YTD)
|
|
9
|
-
// and hands the result to the caller's model to interpret. It does not write
|
|
10
|
-
// the analysis: the caller has the user's thesis, risk tolerance and context,
|
|
11
|
-
// and is a stronger analyst than anything this module could embed.
|
|
12
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.EQUITY_TOOLS = void 0;
|
|
14
|
-
exports.buildStockFundamentals = buildStockFundamentals;
|
|
15
|
-
exports.handleEquityTool = handleEquityTool;
|
|
16
|
-
const zod_1 = require("zod");
|
|
17
|
-
// ── Structured output builder (schema in output-schemas.ts) ─────────────────
|
|
18
|
-
function buildStockFundamentals(ticker, companyName, cik, quote, quarterly, annual) {
|
|
19
|
-
return { ticker, companyName, cik, quote, quarterly, annual };
|
|
20
|
-
}
|
|
21
|
-
const SEC_UA = "Finch MCP research (contact: support@finchagentic.com)";
|
|
22
|
-
const SEC_TICKERS = "https://www.sec.gov/files/company_tickers.json";
|
|
23
|
-
const SEC_CONCEPT = (cik, tag) => `https://data.sec.gov/api/xbrl/companyconcept/CIK${cik}/us-gaap/${tag}.json`;
|
|
24
|
-
/**
|
|
25
|
-
* Concepts pulled per company. Order drives the report layout.
|
|
26
|
-
*
|
|
27
|
-
* Each concept lists several candidate XBRL tags because issuers do not agree
|
|
28
|
-
* on which one to use, and a company can stop using one mid-history. Apple
|
|
29
|
-
* abandoned `Revenues` at the ASC 606 transition and reports under
|
|
30
|
-
* `RevenueFromContractWithCustomer…`; querying only `Revenues` returns its
|
|
31
|
-
* pre-2019 figures, which then render as the current picture — stale by years,
|
|
32
|
-
* and indistinguishable from correct output. The tag with the most recent data
|
|
33
|
-
* wins, so a legacy series can never outrank a live one.
|
|
34
|
-
*/
|
|
35
|
-
const CONCEPTS = [
|
|
36
|
-
{
|
|
37
|
-
key: "revenue",
|
|
38
|
-
label: "Revenue",
|
|
39
|
-
kind: "flow",
|
|
40
|
-
tags: [
|
|
41
|
-
"Revenues",
|
|
42
|
-
"RevenueFromContractWithCustomerExcludingAssessedTax",
|
|
43
|
-
"RevenueFromContractWithCustomerIncludingAssessedTax",
|
|
44
|
-
"SalesRevenueNet",
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
{ key: "netIncome", label: "Net income", kind: "flow", tags: ["NetIncomeLoss"] },
|
|
48
|
-
{ key: "opIncome", label: "Operating income", kind: "flow", tags: ["OperatingIncomeLoss"] },
|
|
49
|
-
{ key: "eps", label: "EPS (diluted)", kind: "pershare", tags: ["EarningsPerShareDiluted"] },
|
|
50
|
-
{ key: "assets", label: "Total assets", kind: "stock", tags: ["Assets"] },
|
|
51
|
-
{ key: "liabilities", label: "Total liabilities", kind: "stock", tags: ["Liabilities"] },
|
|
52
|
-
{
|
|
53
|
-
key: "equity",
|
|
54
|
-
label: "Shareholders' equity",
|
|
55
|
-
kind: "stock",
|
|
56
|
-
tags: ["StockholdersEquity", "StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest"],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
key: "cash",
|
|
60
|
-
label: "Cash & equivalents",
|
|
61
|
-
kind: "stock",
|
|
62
|
-
tags: [
|
|
63
|
-
"CashAndCashEquivalentsAtCarryingValue",
|
|
64
|
-
"CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents",
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
exports.EQUITY_TOOLS = [
|
|
69
|
-
{
|
|
70
|
-
name: "stock_fundamentals",
|
|
71
|
-
description: "Fetch a public company's financials straight from SEC EDGAR XBRL (as filed in 10-Q/10-K) " +
|
|
72
|
-
"plus a live quote. Returns revenue, operating and net income, diluted EPS, balance sheet, " +
|
|
73
|
-
"computed margins and YoY growth — with quarterly figures correctly separated from " +
|
|
74
|
-
"year-to-date ones. US-listed issuers only. No API key needed. " +
|
|
75
|
-
"You do the analysis: the numbers and a review rubric come back, not a written opinion.",
|
|
76
|
-
inputSchema: {
|
|
77
|
-
type: "object",
|
|
78
|
-
properties: {
|
|
79
|
-
ticker: { type: "string", description: "US ticker symbol, e.g. 'HOOD', 'COIN', 'AAPL'" },
|
|
80
|
-
periods: { type: "number", description: "How many recent periods per metric (default 6, max 12)" },
|
|
81
|
-
},
|
|
82
|
-
required: ["ticker"],
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
|
-
const Schema = zod_1.z.object({
|
|
87
|
-
ticker: zod_1.z.string().min(1).max(10),
|
|
88
|
-
periods: zod_1.z.number().int().min(1).max(12).optional(),
|
|
89
|
-
});
|
|
90
|
-
function fmtMoney(v) {
|
|
91
|
-
const a = Math.abs(v);
|
|
92
|
-
const sign = v < 0 ? "-" : "";
|
|
93
|
-
if (a >= 1e9)
|
|
94
|
-
return `${sign}$${(a / 1e9).toFixed(2)}B`;
|
|
95
|
-
if (a >= 1e6)
|
|
96
|
-
return `${sign}$${(a / 1e6).toFixed(0)}M`;
|
|
97
|
-
if (a >= 1e3)
|
|
98
|
-
return `${sign}$${(a / 1e3).toFixed(0)}K`;
|
|
99
|
-
return `${sign}$${a.toFixed(2)}`;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* XBRL reports the same tag as both a quarterly and a cumulative figure for the
|
|
103
|
-
* same period end. Treating them as one series silently triples a quarter, so
|
|
104
|
-
* classify by the reported span before anything else touches the numbers.
|
|
105
|
-
*/
|
|
106
|
-
function spanKind(f) {
|
|
107
|
-
if (!f.start)
|
|
108
|
-
return "point";
|
|
109
|
-
const days = Math.round((Date.parse(f.end) - Date.parse(f.start)) / 86400000);
|
|
110
|
-
if (days <= 110)
|
|
111
|
-
return "quarter";
|
|
112
|
-
if (days <= 300)
|
|
113
|
-
return "ytd";
|
|
114
|
-
return "annual";
|
|
115
|
-
}
|
|
116
|
-
async function secJson(url) {
|
|
117
|
-
try {
|
|
118
|
-
const res = await fetch(url, {
|
|
119
|
-
headers: { "User-Agent": SEC_UA, Accept: "application/json" },
|
|
120
|
-
signal: AbortSignal.timeout(20000),
|
|
121
|
-
});
|
|
122
|
-
if (!res.ok)
|
|
123
|
-
return null;
|
|
124
|
-
return await res.json();
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
async function resolveCik(ticker) {
|
|
131
|
-
const data = await secJson(SEC_TICKERS);
|
|
132
|
-
if (!data)
|
|
133
|
-
return null;
|
|
134
|
-
const up = ticker.trim().toUpperCase();
|
|
135
|
-
const hit = Object.values(data).find((x) => String(x.ticker).toUpperCase() === up);
|
|
136
|
-
if (!hit)
|
|
137
|
-
return null;
|
|
138
|
-
return { cik: String(hit.cik_str).padStart(10, "0"), name: hit.title };
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Live quote. Best-effort — fundamentals stand on their own without it.
|
|
142
|
-
*
|
|
143
|
-
* `meta.chartPreviousClose` is the close BEFORE THE REQUESTED RANGE BEGINS, not
|
|
144
|
-
* the previous session. Reading it off a multi-day range reports a multi-day
|
|
145
|
-
* move as a one-day move: on 2026-07-22 HOOD sat at $106.71 against a genuine
|
|
146
|
-
* prior close of $106.36 (+0.33%), but a `range=5d` request returns $115.54 —
|
|
147
|
-
* the close six sessions back — for a headline of -7.6% that never happened.
|
|
148
|
-
*
|
|
149
|
-
* `regularMarketPreviousClose` is absent from this endpoint, so the previous
|
|
150
|
-
* close is taken from the daily bar series instead: walk back past today's
|
|
151
|
-
* still-forming bar and use the last completed session. That is derived from
|
|
152
|
-
* the data rather than from a field whose meaning shifts with the query.
|
|
153
|
-
*/
|
|
154
|
-
async function liveQuote(ticker) {
|
|
155
|
-
try {
|
|
156
|
-
const res = await fetch(`https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?interval=1d&range=10d`, { headers: { "User-Agent": "Mozilla/5.0" }, signal: AbortSignal.timeout(12000) });
|
|
157
|
-
if (!res.ok)
|
|
158
|
-
return null;
|
|
159
|
-
const j = await res.json();
|
|
160
|
-
const r = j?.chart?.result?.[0];
|
|
161
|
-
const m = r?.meta;
|
|
162
|
-
if (!m?.regularMarketPrice)
|
|
163
|
-
return null;
|
|
164
|
-
const stamps = r?.timestamp ?? [];
|
|
165
|
-
const closes = r?.indicators?.quote?.[0]?.close ?? [];
|
|
166
|
-
const bars = stamps
|
|
167
|
-
.map((t, i) => ({ day: new Date(t * 1000).toISOString().slice(0, 10), close: closes[i] }))
|
|
168
|
-
.filter((b) => typeof b.close === "number");
|
|
169
|
-
// The bar covering the quote's own timestamp is today's, still forming.
|
|
170
|
-
const today = new Date((m.regularMarketTime ?? Date.now() / 1000) * 1000).toISOString().slice(0, 10);
|
|
171
|
-
const completed = bars.filter((b) => b.day !== today);
|
|
172
|
-
const prev = completed[completed.length - 1];
|
|
173
|
-
if (!prev)
|
|
174
|
-
return null;
|
|
175
|
-
return { price: m.regularMarketPrice, prevClose: prev.close, prevDate: prev.day, currency: m.currency ?? "USD" };
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
async function handleEquityTool(name, args) {
|
|
182
|
-
if (name !== "stock_fundamentals")
|
|
183
|
-
return null;
|
|
184
|
-
const parsed = Schema.safeParse(args);
|
|
185
|
-
if (!parsed.success) {
|
|
186
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
187
|
-
}
|
|
188
|
-
const ticker = parsed.data.ticker.trim().toUpperCase();
|
|
189
|
-
const periods = parsed.data.periods ?? 6;
|
|
190
|
-
const company = await resolveCik(ticker);
|
|
191
|
-
if (!company) {
|
|
192
|
-
return {
|
|
193
|
-
content: [{
|
|
194
|
-
type: "text",
|
|
195
|
-
text: `No SEC filer found for **${ticker}**. This covers US-listed issuers that file with the SEC — ` +
|
|
196
|
-
`foreign private issuers, private companies and ETFs may be absent. Check the symbol.`,
|
|
197
|
-
}],
|
|
198
|
-
isError: true,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
// SEC asks for no more than 10 requests a second, and the candidate tags push
|
|
202
|
-
// the count past that if they all go out at once.
|
|
203
|
-
const jobs = CONCEPTS.flatMap((c) => c.tags.map((tag) => ({ key: c.key, tag })));
|
|
204
|
-
const raw = [];
|
|
205
|
-
for (let i = 0; i < jobs.length; i += 6) {
|
|
206
|
-
raw.push(...(await Promise.all(jobs.slice(i, i + 6).map((j) => secJson(SEC_CONCEPT(company.cik, j.tag))))));
|
|
207
|
-
}
|
|
208
|
-
const quote = await liveQuote(ticker);
|
|
209
|
-
// Per concept, keep the candidate tag whose reported periods run latest.
|
|
210
|
-
const chosen = new Map();
|
|
211
|
-
jobs.forEach((j, i) => {
|
|
212
|
-
const data = raw[i];
|
|
213
|
-
if (!data?.units)
|
|
214
|
-
return;
|
|
215
|
-
const unitKey = Object.keys(data.units)[0];
|
|
216
|
-
const facts = (data.units[unitKey] ?? []).filter((f) => f.form === "10-Q" || f.form === "10-K");
|
|
217
|
-
if (!facts.length)
|
|
218
|
-
return;
|
|
219
|
-
const latest = facts.reduce((m, f) => (f.end > m ? f.end : m), "");
|
|
220
|
-
const prev = chosen.get(j.key);
|
|
221
|
-
if (!prev || latest > prev.latest)
|
|
222
|
-
chosen.set(j.key, { tag: j.tag, facts, latest });
|
|
223
|
-
});
|
|
224
|
-
const lines = [
|
|
225
|
-
`# ${company.name} (${ticker}) — as filed with the SEC`,
|
|
226
|
-
``,
|
|
227
|
-
`CIK ${company.cik} · source: SEC EDGAR XBRL (10-Q / 10-K)`,
|
|
228
|
-
];
|
|
229
|
-
if (quote) {
|
|
230
|
-
const chg = ((quote.price - quote.prevClose) / quote.prevClose) * 100;
|
|
231
|
-
lines.push(``,
|
|
232
|
-
// The prior close is dated, so a stale or holiday-shifted comparison is
|
|
233
|
-
// visible in the output rather than silently read as a one-day move.
|
|
234
|
-
`**Live quote:** ${quote.currency} ${quote.price.toFixed(2)} ` +
|
|
235
|
-
`(${chg >= 0 ? "+" : ""}${chg.toFixed(2)}% vs ${quote.prevDate} close ${quote.prevClose.toFixed(2)})`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
lines.push(``, `_Live quote unavailable — fundamentals below are unaffected._`);
|
|
239
|
-
}
|
|
240
|
-
const quarterly = {};
|
|
241
|
-
const annual = {};
|
|
242
|
-
const usedTags = [];
|
|
243
|
-
CONCEPTS.forEach((c) => {
|
|
244
|
-
const pick = chosen.get(c.key);
|
|
245
|
-
if (!pick)
|
|
246
|
-
return;
|
|
247
|
-
usedTags.push(`${c.label} → \`${pick.tag}\``);
|
|
248
|
-
const facts = pick.facts;
|
|
249
|
-
// A period appears in more than one filing — the current 10-Q also carries
|
|
250
|
-
// last year's comparative. Without deduping by period end the same quarter
|
|
251
|
-
// is listed twice and the YoY lookup lands on a duplicate instead of the
|
|
252
|
-
// year-ago quarter, silently reporting ~0% growth.
|
|
253
|
-
const dedupe = (rows) => {
|
|
254
|
-
const byEnd = new Map();
|
|
255
|
-
for (const f of rows) {
|
|
256
|
-
const prev = byEnd.get(f.end);
|
|
257
|
-
// Prefer the original filing (earliest fy) so the label matches the period.
|
|
258
|
-
if (!prev || (f.fy ?? 0) < (prev.fy ?? 0))
|
|
259
|
-
byEnd.set(f.end, f);
|
|
260
|
-
}
|
|
261
|
-
return [...byEnd.values()].sort((a, b) => a.end.localeCompare(b.end));
|
|
262
|
-
};
|
|
263
|
-
if (c.kind === "stock") {
|
|
264
|
-
annual[c.key] = dedupe(facts).slice(-periods);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
quarterly[c.key] = dedupe(facts.filter((f) => spanKind(f) === "quarter")).slice(-periods);
|
|
268
|
-
annual[c.key] = dedupe(facts.filter((f) => spanKind(f) === "annual")).slice(-periods);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
const render = (tag, label, kind, rows) => {
|
|
272
|
-
if (!rows?.length)
|
|
273
|
-
return;
|
|
274
|
-
lines.push(``, `### ${label}`);
|
|
275
|
-
for (const f of rows) {
|
|
276
|
-
const v = kind === "pershare" ? `$${f.val.toFixed(2)}` : fmtMoney(f.val);
|
|
277
|
-
lines.push(`- ${f.end} — **${v}**${f.fy ? ` _(${f.form} FY${f.fy}${f.fp && f.fp !== "FY" ? " " + f.fp : ""})_` : ""}`);
|
|
278
|
-
}
|
|
279
|
-
// YoY against the same quarter a year earlier, matched by DATE rather than
|
|
280
|
-
// by position — periods are often missing, so counting back N rows can
|
|
281
|
-
// silently compare against the wrong quarter.
|
|
282
|
-
if (kind === "flow" && rows.length >= 2) {
|
|
283
|
-
const latest = rows[rows.length - 1];
|
|
284
|
-
const target = new Date(latest.end);
|
|
285
|
-
target.setFullYear(target.getFullYear() - 1);
|
|
286
|
-
const yearAgo = rows.find((r) => Math.abs(Date.parse(r.end) - target.getTime()) < 20 * 86400000);
|
|
287
|
-
if (yearAgo?.val && yearAgo.end !== latest.end) {
|
|
288
|
-
const g = ((latest.val - yearAgo.val) / Math.abs(yearAgo.val)) * 100;
|
|
289
|
-
lines.push(`- **YoY (${yearAgo.end} → ${latest.end}): ${g >= 0 ? "+" : ""}${g.toFixed(1)}%**`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
lines.push(``, `---`, ``, `## Quarterly (each figure is a single quarter, not year-to-date)`);
|
|
294
|
-
for (const c of CONCEPTS) {
|
|
295
|
-
if (c.kind === "stock")
|
|
296
|
-
continue;
|
|
297
|
-
render(c.key, c.label, c.kind, quarterly[c.key]);
|
|
298
|
-
}
|
|
299
|
-
lines.push(``, `---`, ``, `## Annual & balance sheet`);
|
|
300
|
-
for (const c of CONCEPTS)
|
|
301
|
-
render(c.key, c.label + (c.kind === "stock" ? " (period end)" : " — annual"), c.kind, annual[c.key]);
|
|
302
|
-
// Per-share figures are filed as-reported for their period and are NOT
|
|
303
|
-
// restated for later splits. NVDA's annual EPS runs $11.93 → $2.94 across the
|
|
304
|
-
// 10-for-1 of June 2024, which reads as a 75% earnings collapse in the same
|
|
305
|
-
// year revenue grew 114%. The split is discoverable — it is an 8-K item 5.03 —
|
|
306
|
-
// but only if the reader thinks to look, so the discontinuity is named here
|
|
307
|
-
// instead of left as a trap. Detected by direction: a per-share series moving
|
|
308
|
-
// hard against net income over the same periods is a share-count event, not
|
|
309
|
-
// an earnings event.
|
|
310
|
-
const splitWarnings = [];
|
|
311
|
-
for (const [label, eps, profit] of [
|
|
312
|
-
["quarterly", quarterly["eps"], quarterly["netIncome"]],
|
|
313
|
-
["annual", annual["eps"], annual["netIncome"]],
|
|
314
|
-
]) {
|
|
315
|
-
for (let i = 1; i < (eps?.length ?? 0); i++) {
|
|
316
|
-
const [prev, cur] = [eps[i - 1], eps[i]];
|
|
317
|
-
if (!prev.val || !cur.val || prev.val <= 0 || cur.val <= 0)
|
|
318
|
-
continue;
|
|
319
|
-
const epsChange = (cur.val - prev.val) / prev.val;
|
|
320
|
-
const ni = profit?.find((f) => f.end === cur.end);
|
|
321
|
-
const niPrev = profit?.find((f) => f.end === prev.end);
|
|
322
|
-
if (!ni?.val || !niPrev?.val || niPrev.val <= 0)
|
|
323
|
-
continue;
|
|
324
|
-
const niChange = (ni.val - niPrev.val) / niPrev.val;
|
|
325
|
-
// EPS down hard while profit held or grew (or the mirror image).
|
|
326
|
-
if ((epsChange < -0.4 && niChange > -0.1) || (epsChange > 0.6 && niChange < 0.1)) {
|
|
327
|
-
// Shares = income / EPS, so the implied share-count change is
|
|
328
|
-
// (income ratio) x (EPS ratio inverted). The raw EPS ratio alone
|
|
329
|
-
// conflates the split with the earnings move and understates it —
|
|
330
|
-
// NVDA's 10-for-1 reads as 4:1 until the income growth is divided out.
|
|
331
|
-
const shareRatio = (ni.val / niPrev.val) * (prev.val / cur.val);
|
|
332
|
-
const asSplit = shareRatio >= 1.5
|
|
333
|
-
? `${shareRatio.toFixed(1)}-for-1`
|
|
334
|
-
: shareRatio <= 0.67
|
|
335
|
-
? `1-for-${(1 / shareRatio).toFixed(1)} reverse`
|
|
336
|
-
: null;
|
|
337
|
-
splitWarnings.push(`- **${label} EPS ${prev.end} → ${cur.end}**: $${prev.val.toFixed(2)} → $${cur.val.toFixed(2)} ` +
|
|
338
|
-
`(${(epsChange * 100).toFixed(0)}%) while net income moved ${(niChange * 100).toFixed(0)}%. ` +
|
|
339
|
-
`Implied share count ×${shareRatio.toFixed(2)}${asSplit ? ` — consistent with a ${asSplit} split` : ""}, ` +
|
|
340
|
-
`not an earnings change. Confirm with \`stock_events\` (8-K item 5.03) before comparing these two periods.`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (splitWarnings.length) {
|
|
345
|
-
lines.push(``, `---`, ``, `## ⚠️ Per-share figures are not split-adjusted`, ``, ...splitWarnings, ``, `Revenue, income and balance-sheet lines are unaffected — only per-share values are.`);
|
|
346
|
-
}
|
|
347
|
-
// Margins, computed rather than asserted.
|
|
348
|
-
const rev = quarterly["revenue"]?.slice(-1)[0];
|
|
349
|
-
const ni = quarterly["netIncome"]?.slice(-1)[0];
|
|
350
|
-
const oi = quarterly["opIncome"]?.slice(-1)[0];
|
|
351
|
-
if (rev?.val) {
|
|
352
|
-
lines.push(``, `---`, ``, `## Computed margins — latest quarter (${rev.end})`);
|
|
353
|
-
if (oi)
|
|
354
|
-
lines.push(`- Operating margin: **${((oi.val / rev.val) * 100).toFixed(1)}%**`);
|
|
355
|
-
if (ni)
|
|
356
|
-
lines.push(`- Net margin: **${((ni.val / rev.val) * 100).toFixed(1)}%**`);
|
|
357
|
-
}
|
|
358
|
-
lines.push(``, `---`, ``, `## How to analyse this`, ``, `**Trajectory** — is revenue growth accelerating or decelerating? Compare the last 4 quarters ` +
|
|
359
|
-
`sequentially, then against the same quarter a year earlier. Say which, and by how much.`, ``, `**Quality of earnings** — is net income tracking operating income, or driven by items below ` +
|
|
360
|
-
`the operating line? A widening gap deserves an explanation.`, ``, `**Balance sheet** — equity vs liabilities trend, and whether cash covers near-term obligations.`, ``, `**What these numbers cannot tell you** — guidance, competitive position, regulatory exposure, ` +
|
|
361
|
-
`insider activity, and anything after the last filing date above. Say so explicitly rather ` +
|
|
362
|
-
`than inferring it. For sentiment and post-filing developments, run \`deep_research\`.`, ``, `Cite the period end for every figure you use. Do not annualise a quarter without labelling it ` +
|
|
363
|
-
`as your own extrapolation. If a metric is missing above, the company did not file that tag — ` +
|
|
364
|
-
`do not substitute an estimate.`, ``,
|
|
365
|
-
// Issuers tag the same concept differently, so naming the tag behind each
|
|
366
|
-
// series makes the figures checkable against EDGAR rather than trusted.
|
|
367
|
-
`_XBRL concepts used: ${usedTags.join(" · ")}_`, ``, `_Figures are as filed with the SEC. Not investment advice._`);
|
|
368
|
-
return {
|
|
369
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
370
|
-
structuredContent: buildStockFundamentals(ticker, company.name ?? null, company.cik ?? null, quote, quarterly, annual),
|
|
371
|
-
};
|
|
372
|
-
}
|