@finchagentic/mcp 4.6.2 โ 4.6.3
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 +39 -74
- package/dist/_http-cache.js +96 -0
- package/dist/_text-search.js +39 -0
- package/dist/agent-loop.js +301 -0
- package/dist/annotations.js +122 -0
- package/dist/cli.js +1391 -0
- package/dist/clink-input.js +15 -0
- package/dist/config.js +132 -0
- package/dist/convex.js +175 -0
- package/dist/dex-pair.js +54 -0
- package/dist/enrichment-router.js +315 -0
- package/dist/index.js +258 -0
- package/dist/llm.js +298 -0
- package/dist/local-memory-file.js +150 -0
- package/dist/local-memory.js +135 -0
- package/dist/local-vault.js +456 -0
- package/dist/output-schemas.js +605 -0
- package/dist/project.js +36 -0
- package/dist/prompts.js +111 -0
- package/dist/public-url.js +107 -0
- package/dist/resources.js +111 -0
- package/dist/server.js +322 -0
- package/dist/signal-gate.js +57 -0
- package/dist/token-decimals.js +26 -0
- package/dist/token-gate.js +88 -0
- package/dist/tool-filter.js +53 -0
- package/dist/tools/_solidity-scan.js +313 -0
- package/dist/tools/agents.js +441 -0
- package/dist/tools/automation.js +354 -0
- package/dist/tools/base-mcp.js +466 -0
- package/dist/tools/base.js +283 -0
- package/dist/tools/chronicle.js +268 -0
- package/dist/tools/coder.js +94 -0
- package/dist/tools/deep-research.js +1421 -0
- package/dist/tools/defi.js +292 -0
- package/dist/tools/equity.js +372 -0
- package/dist/tools/events.js +182 -0
- package/dist/tools/github.js +564 -0
- package/dist/tools/insider.js +264 -0
- package/dist/tools/insight.js +630 -0
- package/dist/tools/market.js +555 -0
- package/dist/tools/memory.js +1059 -0
- package/dist/tools/miroshark.js +350 -0
- package/dist/tools/monitor.js +319 -0
- package/dist/tools/os.js +236 -0
- package/dist/tools/packets.js +296 -0
- package/dist/tools/research-chain.js +226 -0
- package/dist/tools/research-compare.js +280 -0
- package/dist/tools/research.js +188 -0
- package/dist/tools/rh-bridge.js +148 -0
- package/dist/tools/rh-mcp.js +1448 -0
- package/dist/tools/rh-orders.js +556 -0
- package/dist/tools/scanner.js +564 -0
- package/dist/tools/stake.js +369 -0
- package/dist/tools/vault.js +1020 -0
- package/dist/tools/wallet.js +200 -0
- package/dist/types.js +2 -0
- package/dist/wallet.js +372 -0
- package/package.json +4 -7
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BASE_TOOLS = void 0;
|
|
4
|
+
exports.handleBaseTool = handleBaseTool;
|
|
5
|
+
const MORPHO_API = "https://blue-api.morpho.org/graphql";
|
|
6
|
+
const MOONWELL_API = "https://api.moonwell.fi/v1/markets";
|
|
7
|
+
/** Morpho's public API occasionally 403s transiently (rate-limit/WAF blip,
|
|
8
|
+
* confirmed by hand: identical requests succeed moments later) - one retry
|
|
9
|
+
* after a short delay clears most of these without adding real latency to
|
|
10
|
+
* the common case. */
|
|
11
|
+
async function fetchMorphoGql(query) {
|
|
12
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13
|
+
if (attempt > 0)
|
|
14
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
15
|
+
const res = await fetch(MORPHO_API, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ query }),
|
|
19
|
+
signal: AbortSignal.timeout(15000),
|
|
20
|
+
});
|
|
21
|
+
if (res.ok)
|
|
22
|
+
return res.json();
|
|
23
|
+
if (attempt === 1)
|
|
24
|
+
throw new Error(`Morpho API error: ${res.status} (after retry)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.BASE_TOOLS = [
|
|
28
|
+
{
|
|
29
|
+
name: "base_mcp_yield_vaults",
|
|
30
|
+
description: "Find the best yield/earning opportunities on Base chain using Morpho vaults - queries Morpho's own " +
|
|
31
|
+
"API directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Morpho figures, " +
|
|
32
|
+
"but this tool only covers Morpho (not Aerodrome/Uniswap/other protocols - use get_defi_yields for " +
|
|
33
|
+
"cross-protocol comparison). Returns all vaults ranked by APY โ use this when the user asks about " +
|
|
34
|
+
"yield farming, APY, best rates to earn, where to put USDC, or passive income on Base. For yield on " +
|
|
35
|
+
"a specific token, also see base_mcp_lend.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
asset: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Filter by asset symbol (e.g. USDC, WETH, cbBTC). Leave empty for all.",
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: "number",
|
|
45
|
+
description: "Max vaults to return (default 10)",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: [],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "base_mcp_lending_rates",
|
|
53
|
+
description: "Get lending and borrowing rates across all Moonwell markets on Base - queries Moonwell's own API " +
|
|
54
|
+
"directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Moonwell figures, but " +
|
|
55
|
+
"this tool only covers Moonwell (use get_defi_yields for cross-protocol comparison, or " +
|
|
56
|
+
"base_mcp_yield_vaults for Morpho specifically). Returns supply APY, borrow APY, liquidity, and " +
|
|
57
|
+
"utilization per asset. Use when the user asks about borrow rates, lending rates, interest rates, " +
|
|
58
|
+
"or supply APY on Base.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
asset: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Filter by asset symbol (e.g. USDC, ETH, cbBTC). Leave empty for all.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: [],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "base_mcp_deposit_guide",
|
|
72
|
+
description: "Get step-by-step deposit instructions for a Morpho vault โ shows the vault address, expected APY, and manual deposit steps. Does NOT execute the transaction. Call base_mcp_yield_vaults first to find the vault, then call this to get instructions.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
vaultName: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Name or partial name of the vault (e.g. 'Gauntlet USDC', 'steakUSDC')",
|
|
79
|
+
},
|
|
80
|
+
amount: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Amount to deposit (e.g. '100', '1000')",
|
|
83
|
+
},
|
|
84
|
+
asset: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Asset to deposit (e.g. USDC, WETH)",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["asset", "amount"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "base_mcp_network",
|
|
94
|
+
description: "Get real-time Base network stats: ETH price in USD, gas price in gwei, and latest block number. Use when the user asks about gas fees, ETH price, Base network status, or current block. Does not require wallet auth.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {},
|
|
98
|
+
required: [],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
// โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
103
|
+
function fmt(n, decimals = 2) {
|
|
104
|
+
if (n >= 1000000000)
|
|
105
|
+
return `$${(n / 1000000000).toFixed(1)}B`;
|
|
106
|
+
if (n >= 1000000)
|
|
107
|
+
return `$${(n / 1000000).toFixed(1)}M`;
|
|
108
|
+
if (n >= 1000)
|
|
109
|
+
return `$${(n / 1000).toFixed(1)}K`;
|
|
110
|
+
return `$${n.toFixed(decimals)}`;
|
|
111
|
+
}
|
|
112
|
+
function pct(n) {
|
|
113
|
+
return `${(n * 100).toFixed(2)}%`;
|
|
114
|
+
}
|
|
115
|
+
// โโ Morpho vaults โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
116
|
+
async function fetchMorphoVaults(asset, limit = 10) {
|
|
117
|
+
const gql = `{
|
|
118
|
+
vaults(
|
|
119
|
+
where: { chainId_in: [8453] }
|
|
120
|
+
orderBy: NetApy
|
|
121
|
+
orderDirection: Desc
|
|
122
|
+
first: 50
|
|
123
|
+
) {
|
|
124
|
+
items {
|
|
125
|
+
name
|
|
126
|
+
address
|
|
127
|
+
asset { symbol name }
|
|
128
|
+
state { apy netApy totalAssetsUsd }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}`;
|
|
132
|
+
const data = await fetchMorphoGql(gql);
|
|
133
|
+
let vaults = data?.data?.vaults?.items ?? [];
|
|
134
|
+
// Filter out test/spam vaults: min $10k TVL, max 500% APY
|
|
135
|
+
vaults = vaults.filter((v) => {
|
|
136
|
+
const tvl = v.state?.totalAssetsUsd ?? 0;
|
|
137
|
+
const apy = v.state?.netApy ?? v.state?.apy ?? 0;
|
|
138
|
+
return tvl >= 10000 && apy <= 5;
|
|
139
|
+
});
|
|
140
|
+
if (asset) {
|
|
141
|
+
vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase().includes(asset.toLowerCase()));
|
|
142
|
+
}
|
|
143
|
+
vaults = vaults.slice(0, limit);
|
|
144
|
+
if (!vaults.length)
|
|
145
|
+
return "No vaults found for that asset.";
|
|
146
|
+
const lines = vaults.map((v, i) => {
|
|
147
|
+
const apy = pct(v.state?.netApy ?? v.state?.apy ?? 0);
|
|
148
|
+
const tvl = fmt(v.state?.totalAssetsUsd ?? 0);
|
|
149
|
+
const addr = `${v.address?.slice(0, 6)}...${v.address?.slice(-4)}`;
|
|
150
|
+
return `${i + 1}. ${v.name}\n Asset: ${v.asset?.symbol} APY: ${apy} TVL: ${tvl}\n Address: ${addr}`;
|
|
151
|
+
});
|
|
152
|
+
return `Morpho Vaults on Base (sorted by APY):\n\n${lines.join("\n\n")}`;
|
|
153
|
+
}
|
|
154
|
+
// โโ Moonwell markets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
155
|
+
async function fetchMoonwellMarkets(asset) {
|
|
156
|
+
const res = await fetch(`${MOONWELL_API}?network=base`, {
|
|
157
|
+
signal: AbortSignal.timeout(15000),
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok)
|
|
160
|
+
throw new Error(`Moonwell API error: ${res.status}`);
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
let markets = Array.isArray(data) ? data : (data?.data ?? data?.markets ?? []);
|
|
163
|
+
// Filter deprecated markets
|
|
164
|
+
markets = markets.filter((m) => !m.deprecated);
|
|
165
|
+
if (asset) {
|
|
166
|
+
markets = markets.filter((m) => (m.asset ?? m.underlyingSymbol ?? m.symbol ?? "").toLowerCase().includes(asset.toLowerCase()));
|
|
167
|
+
}
|
|
168
|
+
if (!markets.length)
|
|
169
|
+
return "No markets found.";
|
|
170
|
+
const lines = markets.slice(0, 15).map((m, i) => {
|
|
171
|
+
const symbol = m.asset ?? m.underlyingSymbol ?? m.symbol ?? "?";
|
|
172
|
+
const supplyApy = pct((m.baseSupplyApy ?? m.supplyApy ?? m.supplyRate ?? 0) / 100);
|
|
173
|
+
const borrowApy = pct((m.baseBorrowApy ?? m.borrowApy ?? m.borrowRate ?? 0) / 100);
|
|
174
|
+
const liquidity = fmt(m.totalSupplyUsd ?? m.liquidityUsd ?? m.totalSupply ?? 0);
|
|
175
|
+
const util = m.utilization != null ? `${(m.utilization * 100).toFixed(1)}%` : "-";
|
|
176
|
+
return `${i + 1}. ${symbol}\n Supply APY: ${supplyApy} Borrow APY: ${borrowApy} Liquidity: ${liquidity} Util: ${util}`;
|
|
177
|
+
});
|
|
178
|
+
return `Moonwell Markets on Base:\n\n${lines.join("\n\n")}`;
|
|
179
|
+
}
|
|
180
|
+
// โโ Base chain stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
181
|
+
async function fetchBaseStats() {
|
|
182
|
+
const rpc = "https://mainnet.base.org";
|
|
183
|
+
const [blockRes, priceRes, gasRes] = await Promise.all([
|
|
184
|
+
fetch(rpc, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
|
|
188
|
+
signal: AbortSignal.timeout(8000),
|
|
189
|
+
}).then(r => r.json()).catch(() => null),
|
|
190
|
+
fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", {
|
|
191
|
+
signal: AbortSignal.timeout(8000),
|
|
192
|
+
}).then(r => r.json()).catch(() => null),
|
|
193
|
+
fetch(rpc, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "eth_gasPrice", params: [] }),
|
|
197
|
+
signal: AbortSignal.timeout(8000),
|
|
198
|
+
}).then(r => r.json()).catch(() => null),
|
|
199
|
+
]);
|
|
200
|
+
const block = blockRes?.result ? parseInt(blockRes.result, 16) : "-";
|
|
201
|
+
const ethPrice = priceRes?.ethereum?.usd ?? "-";
|
|
202
|
+
const gasPriceGwei = gasRes?.result
|
|
203
|
+
? (parseInt(gasRes.result, 16) / 1e9).toFixed(4)
|
|
204
|
+
: "-";
|
|
205
|
+
return `Base Chain Stats:\n\nโข ETH Price: $${ethPrice}\nโข Gas Price: ${gasPriceGwei} gwei\nโข Latest Block: ${block.toLocaleString()}\nโข Network: Base Mainnet (Chain ID 8453)`;
|
|
206
|
+
}
|
|
207
|
+
// โโ Prepare deposit info โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
208
|
+
async function prepareDeposit(vaultName, asset, amount) {
|
|
209
|
+
const gql = `{
|
|
210
|
+
vaults(
|
|
211
|
+
where: { chainId_in: [8453] }
|
|
212
|
+
orderBy: NetApy
|
|
213
|
+
orderDirection: Desc
|
|
214
|
+
first: 100
|
|
215
|
+
) {
|
|
216
|
+
items {
|
|
217
|
+
name
|
|
218
|
+
address
|
|
219
|
+
asset { symbol name }
|
|
220
|
+
state { netApy apy totalAssetsUsd }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}`;
|
|
224
|
+
const data = await fetchMorphoGql(gql);
|
|
225
|
+
let vaults = data?.data?.vaults?.items ?? [];
|
|
226
|
+
// Filter by asset first
|
|
227
|
+
vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase() === asset.toLowerCase());
|
|
228
|
+
// Filter by vault name if provided
|
|
229
|
+
if (vaultName) {
|
|
230
|
+
const match = vaults.find((v) => v.name?.toLowerCase().includes(vaultName.toLowerCase()));
|
|
231
|
+
if (match)
|
|
232
|
+
vaults = [match];
|
|
233
|
+
}
|
|
234
|
+
// Take best APY vault
|
|
235
|
+
const vault = vaults[0];
|
|
236
|
+
if (!vault) {
|
|
237
|
+
return `No Morpho vault found for ${asset} on Base. Try base_mcp_yield_vaults to see available vaults.`;
|
|
238
|
+
}
|
|
239
|
+
const apy = pct(vault.state?.netApy ?? vault.state?.apy ?? 0);
|
|
240
|
+
const tvl = fmt(vault.state?.totalAssetsUsd ?? 0);
|
|
241
|
+
return [
|
|
242
|
+
`Morpho Vault Deposit Instructions`,
|
|
243
|
+
``,
|
|
244
|
+
`Vault: ${vault.name}`,
|
|
245
|
+
`Asset: ${vault.asset?.symbol}`,
|
|
246
|
+
`APY: ${apy} | TVL: ${tvl}`,
|
|
247
|
+
`Contract: ${vault.address}`,
|
|
248
|
+
``,
|
|
249
|
+
`Steps to deposit ${amount} ${asset}:`,
|
|
250
|
+
`1. Go to app.morpho.org or use the vault address above`,
|
|
251
|
+
`2. Connect your wallet (ensure you have ${amount} ${asset})`,
|
|
252
|
+
`3. Approve the vault contract to spend your ${asset}`,
|
|
253
|
+
`4. Call deposit(${amount}, yourAddress) on the vault contract`,
|
|
254
|
+
`5. You'll receive vault shares representing your deposit`,
|
|
255
|
+
``,
|
|
256
|
+
`Expected yield: ~${apy} on ${amount} ${asset}`,
|
|
257
|
+
`Note: APY is variable and changes based on market conditions.`,
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
// โโ Handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
261
|
+
async function handleBaseTool(name, args) {
|
|
262
|
+
const a = (args ?? {});
|
|
263
|
+
switch (name) {
|
|
264
|
+
case "base_mcp_yield_vaults": {
|
|
265
|
+
const text = await fetchMorphoVaults(a.asset, a.limit ?? 10);
|
|
266
|
+
return { content: [{ type: "text", text }] };
|
|
267
|
+
}
|
|
268
|
+
case "base_mcp_lending_rates": {
|
|
269
|
+
const text = await fetchMoonwellMarkets(a.asset);
|
|
270
|
+
return { content: [{ type: "text", text }] };
|
|
271
|
+
}
|
|
272
|
+
case "base_mcp_deposit_guide": {
|
|
273
|
+
const text = await prepareDeposit(a.vaultName, a.asset, a.amount);
|
|
274
|
+
return { content: [{ type: "text", text }] };
|
|
275
|
+
}
|
|
276
|
+
case "base_mcp_network": {
|
|
277
|
+
const text = await fetchBaseStats();
|
|
278
|
+
return { content: [{ type: "text", text }] };
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CHRONICLE_TOOLS = void 0;
|
|
4
|
+
exports.buildChronicleList = buildChronicleList;
|
|
5
|
+
exports.buildChronicleSearch = buildChronicleSearch;
|
|
6
|
+
exports.buildChronicleStats = buildChronicleStats;
|
|
7
|
+
exports.handleChronicle = handleChronicle;
|
|
8
|
+
const convex_js_1 = require("../convex.js");
|
|
9
|
+
const CHRONICLE_TYPES = ["vault", "memory", "agent", "tool", "automation", "monitor", "system", "custom"];
|
|
10
|
+
exports.CHRONICLE_TOOLS = [
|
|
11
|
+
{
|
|
12
|
+
name: "chronicle_add",
|
|
13
|
+
description: "Log an event to Finch Chronicle - the system-wide audit log for your AI runtime. " +
|
|
14
|
+
"Records anything meaningful: vault saves, agent updates, automation triggers, " +
|
|
15
|
+
"custom milestones, research completions. Chronicle is your permanent timeline of what happened. " +
|
|
16
|
+
"Types: vault | memory | agent | tool | automation | monitor | system | custom.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
type: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: [...CHRONICLE_TYPES],
|
|
23
|
+
description: "Event category",
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Short event title, e.g. 'Saved ETH research to vault'",
|
|
28
|
+
},
|
|
29
|
+
detail: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Optional longer description or result summary",
|
|
32
|
+
},
|
|
33
|
+
metadata: {
|
|
34
|
+
type: "object",
|
|
35
|
+
description: "Optional extra data (key, agentId, topic, etc.)",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["type", "title"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "chronicle_list",
|
|
43
|
+
description: "Read the Finch Chronicle event log - your AI runtime timeline. Returns recent events in reverse chronological order. " +
|
|
44
|
+
"Filter by type to see only vault saves, agent activity, automations, etc.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
limit: {
|
|
49
|
+
type: "number",
|
|
50
|
+
description: "Max events to return (default 20, max 100)",
|
|
51
|
+
},
|
|
52
|
+
type: {
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: [...CHRONICLE_TYPES],
|
|
55
|
+
description: "Filter by event type (optional)",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "chronicle_search",
|
|
62
|
+
description: "Search the Finch Chronicle by keyword. Matches against event titles and details. " +
|
|
63
|
+
"Useful for finding when something specific happened: 'when did I last research ETH?' or 'find all vault saves for Base'.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
query: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Keyword or phrase to search for in event titles and details",
|
|
70
|
+
},
|
|
71
|
+
type: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: [...CHRONICLE_TYPES],
|
|
74
|
+
description: "Optional: filter by event type before searching",
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: "number",
|
|
78
|
+
description: "Max results to return (default 10, max 50)",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ["query"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "chronicle_stats",
|
|
86
|
+
description: "Activity stats for your AI runtime - breakdown by event type, daily activity heatmap, " +
|
|
87
|
+
"busiest days, and most active categories. Use to understand how heavily you're using the runtime.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
days: {
|
|
92
|
+
type: "number",
|
|
93
|
+
description: "How many days back to analyze (default 30, max 90)",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
// Keep "swarm" emoji for backward compatibility - legacy chronicle entries
|
|
100
|
+
// may still have this type even though it's no longer accepted on new writes.
|
|
101
|
+
const TYPE_EMOJI = {
|
|
102
|
+
vault: "๐๏ธ",
|
|
103
|
+
memory: "๐ง ",
|
|
104
|
+
agent: "๐ค",
|
|
105
|
+
tool: "๐ง",
|
|
106
|
+
automation: "โก",
|
|
107
|
+
monitor: "๐๏ธ",
|
|
108
|
+
system: "โ๏ธ",
|
|
109
|
+
custom: "๐",
|
|
110
|
+
swarm: "๐",
|
|
111
|
+
};
|
|
112
|
+
function formatEntry(e) {
|
|
113
|
+
const emoji = TYPE_EMOJI[e.type] ?? "๐";
|
|
114
|
+
const date = new Date(e.ts).toLocaleString("en-US", {
|
|
115
|
+
month: "short", day: "numeric",
|
|
116
|
+
hour: "2-digit", minute: "2-digit",
|
|
117
|
+
});
|
|
118
|
+
const lines = [`${emoji} **${e.title}** ยท \`${e.type}\` ยท ${date}`];
|
|
119
|
+
if (e.detail)
|
|
120
|
+
lines.push(` ${e.detail}`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
// โโ Structured output builders (schemas in output-schemas.ts) โโโโโโโโโโโโโโโ
|
|
124
|
+
function chronicleEntry(e) {
|
|
125
|
+
return { title: e.title ?? null, detail: e.detail ?? null, type: e.type ?? null, ts: e.ts ?? null };
|
|
126
|
+
}
|
|
127
|
+
function buildChronicleList(type, entries) {
|
|
128
|
+
return { type: type ?? null, count: entries.length, entries: entries.map(chronicleEntry) };
|
|
129
|
+
}
|
|
130
|
+
function buildChronicleSearch(query, type, matched) {
|
|
131
|
+
return { query, type: type ?? null, count: matched.length, entries: matched.map(chronicleEntry) };
|
|
132
|
+
}
|
|
133
|
+
function buildChronicleStats(days, entries) {
|
|
134
|
+
const byType = {};
|
|
135
|
+
const byDay = {};
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
byType[e.type] = (byType[e.type] ?? 0) + 1;
|
|
138
|
+
const day = new Date(e.ts).toISOString().slice(0, 10);
|
|
139
|
+
byDay[day] = (byDay[day] ?? 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
const busiestDays = Object.entries(byDay).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([day, count]) => ({ day, count }));
|
|
142
|
+
return {
|
|
143
|
+
days,
|
|
144
|
+
totalEvents: entries.length,
|
|
145
|
+
activeDays: Object.keys(byDay).length,
|
|
146
|
+
avgPerDay: entries.length / days,
|
|
147
|
+
byType: Object.entries(byType).sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count })),
|
|
148
|
+
busiestDays,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async function handleChronicle(name, args) {
|
|
152
|
+
if (name === "chronicle_add") {
|
|
153
|
+
const { type = "custom", title, detail, metadata } = args;
|
|
154
|
+
await (0, convex_js_1.callConvex)("/chronicle/add", "POST", {
|
|
155
|
+
type,
|
|
156
|
+
title,
|
|
157
|
+
detail,
|
|
158
|
+
metadata,
|
|
159
|
+
source: "mcp",
|
|
160
|
+
}, "chronicle_add");
|
|
161
|
+
const emoji = TYPE_EMOJI[type] ?? "๐";
|
|
162
|
+
return {
|
|
163
|
+
content: [{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: [
|
|
166
|
+
`${emoji} **Logged to Chronicle**`,
|
|
167
|
+
``,
|
|
168
|
+
`**${title}**${detail ? `\n${detail}` : ""}`,
|
|
169
|
+
``,
|
|
170
|
+
`Type: \`${type}\` ยท Use \`chronicle_list\` to view your timeline.`,
|
|
171
|
+
].join("\n"),
|
|
172
|
+
}],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (name === "chronicle_list") {
|
|
176
|
+
const limit = Math.min(Number(args.limit ?? 20), 100);
|
|
177
|
+
const type = args.type;
|
|
178
|
+
const data = await (0, convex_js_1.callConvex)(`/chronicle/list?limit=${limit}${type ? `&type=${type}` : ""}`, "GET", undefined, "chronicle_list");
|
|
179
|
+
const entries = data.entries ?? [];
|
|
180
|
+
if (entries.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
content: [{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: "No chronicle entries yet. Use `chronicle_add` to start logging events.",
|
|
185
|
+
}],
|
|
186
|
+
structuredContent: buildChronicleList(type, []),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const lines = [
|
|
190
|
+
`## ๐ Finch Chronicle${type ? ` ยท ${type}` : ""}`,
|
|
191
|
+
`*${entries.length} event${entries.length !== 1 ? "s" : ""}*`,
|
|
192
|
+
"",
|
|
193
|
+
];
|
|
194
|
+
for (const e of entries)
|
|
195
|
+
lines.push(formatEntry(e));
|
|
196
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildChronicleList(type, entries) };
|
|
197
|
+
}
|
|
198
|
+
if (name === "chronicle_search") {
|
|
199
|
+
const { query, type, limit = 10 } = args;
|
|
200
|
+
if (!query)
|
|
201
|
+
return { content: [{ type: "text", text: "query is required" }], isError: true };
|
|
202
|
+
const data = await (0, convex_js_1.callConvex)(`/chronicle/list?limit=100${type ? `&type=${type}` : ""}`, "GET", undefined, "chronicle_list");
|
|
203
|
+
const allEntries = data.entries ?? [];
|
|
204
|
+
const q = query.toLowerCase();
|
|
205
|
+
const matched = allEntries.filter((e) => (e.title ?? "").toLowerCase().includes(q) ||
|
|
206
|
+
(e.detail ?? "").toLowerCase().includes(q)).slice(0, Math.min(Number(limit), 50));
|
|
207
|
+
if (matched.length === 0) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `No chronicle events matching "${query}"${type ? ` (type: ${type})` : ""}. (searched most recent 100 entries)`,
|
|
212
|
+
}],
|
|
213
|
+
structuredContent: buildChronicleSearch(query, type, []),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const lines = [
|
|
217
|
+
`## ๐ Chronicle Search: "${query}"`,
|
|
218
|
+
`*${matched.length} match${matched.length !== 1 ? "es" : ""}${type ? ` ยท type: ${type}` : ""} ยท searched most recent 100 entries*`,
|
|
219
|
+
"",
|
|
220
|
+
];
|
|
221
|
+
for (const e of matched)
|
|
222
|
+
lines.push(formatEntry(e));
|
|
223
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildChronicleSearch(query, type, matched) };
|
|
224
|
+
}
|
|
225
|
+
if (name === "chronicle_stats") {
|
|
226
|
+
const days = Math.min(Number(args.days ?? 30), 90);
|
|
227
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
228
|
+
const data = await (0, convex_js_1.callConvex)(`/chronicle/list?limit=100`, "GET", undefined, "chronicle_list");
|
|
229
|
+
const allEntries = (data.entries ?? []).filter((e) => (e.ts ?? 0) >= cutoff);
|
|
230
|
+
if (allEntries.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `No chronicle events in the past ${days} days.` }],
|
|
233
|
+
structuredContent: buildChronicleStats(days, []),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// Count by type
|
|
237
|
+
const byType = {};
|
|
238
|
+
const byDay = {};
|
|
239
|
+
for (const e of allEntries) {
|
|
240
|
+
byType[e.type] = (byType[e.type] ?? 0) + 1;
|
|
241
|
+
const day = new Date(e.ts).toISOString().slice(0, 10);
|
|
242
|
+
byDay[day] = (byDay[day] ?? 0) + 1;
|
|
243
|
+
}
|
|
244
|
+
const sortedTypes = Object.entries(byType).sort((a, b) => b[1] - a[1]);
|
|
245
|
+
const sortedDays = Object.entries(byDay).sort((a, b) => b[1] - a[1]);
|
|
246
|
+
const activeDays = Object.keys(byDay).length;
|
|
247
|
+
const avgPerDay = (allEntries.length / days).toFixed(1);
|
|
248
|
+
const lines = [
|
|
249
|
+
`## ๐ Chronicle Stats โ last ${days} days`,
|
|
250
|
+
``,
|
|
251
|
+
`**Total events:** ${allEntries.length} across ${activeDays} active day${activeDays !== 1 ? "s" : ""} (avg ${avgPerDay}/day)`,
|
|
252
|
+
``,
|
|
253
|
+
`**By type:**`,
|
|
254
|
+
...sortedTypes.map(([t, n]) => {
|
|
255
|
+
const bar = "โ".repeat(Math.round((n / allEntries.length) * 20));
|
|
256
|
+
const emoji = TYPE_EMOJI[t] ?? "๐";
|
|
257
|
+
return ` ${emoji} ${t.padEnd(12)} ${String(n).padStart(3)} ${bar}`;
|
|
258
|
+
}),
|
|
259
|
+
``,
|
|
260
|
+
`**Busiest days:**`,
|
|
261
|
+
...sortedDays.slice(0, 5).map(([d, n]) => ` ${d} ${n} event${n !== 1 ? "s" : ""}`),
|
|
262
|
+
``,
|
|
263
|
+
`*Note: stats based on most recent 100 entries*`,
|
|
264
|
+
];
|
|
265
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildChronicleStats(days, allEntries) };
|
|
266
|
+
}
|
|
267
|
+
return { content: [{ type: "text", text: `Unknown chronicle tool: ${name}` }], isError: true };
|
|
268
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Solidity static analysis.
|
|
3
|
+
//
|
|
4
|
+
// This module used to also generate, explain and review code via a server-side
|
|
5
|
+
// LLM. Those were removed: the MCP client (Claude Code, Cursor, the CLI's own
|
|
6
|
+
// agent loop) is already a model, and it does that work better because it has
|
|
7
|
+
// the repository in context. What survives here is the part a model cannot do
|
|
8
|
+
// for itself โ a deterministic pattern scan over the source.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.CODER_TOOLS = void 0;
|
|
11
|
+
exports.handleCoderTool = handleCoderTool;
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const _solidity_scan_js_1 = require("./_solidity-scan.js");
|
|
14
|
+
function ok(text) {
|
|
15
|
+
return { content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
function err(text) {
|
|
18
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
exports.CODER_TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "audit_contract",
|
|
23
|
+
description: "Run a deterministic static scan over Solidity source for common antipatterns " +
|
|
24
|
+
"(tx.origin auth, reentrancy ordering, unchecked low-level calls, delegatecall hijack, " +
|
|
25
|
+
"floating pragma, etc). Returns the findings with severities plus a review rubric for YOU " +
|
|
26
|
+
"to work through โ you are the reviewer, and you have the repo in context. " +
|
|
27
|
+
"Needs no API key. Not a substitute for a professional audit (CertiK, Trail of Bits, " +
|
|
28
|
+
"OpenZeppelin) or formal verification.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
code: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The full Solidity contract source code to audit",
|
|
35
|
+
},
|
|
36
|
+
focus: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
description: "Optional: specific areas to focus on, e.g. ['reentrancy', 'access control', 'overflow']",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["code"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const AuditSchema = zod_1.z.object({
|
|
47
|
+
code: zod_1.z.string().min(10),
|
|
48
|
+
focus: zod_1.z.array(zod_1.z.string()).optional(),
|
|
49
|
+
});
|
|
50
|
+
async function handleCoderTool(name, args) {
|
|
51
|
+
if (name !== "audit_contract")
|
|
52
|
+
return null;
|
|
53
|
+
const p = AuditSchema.safeParse(args);
|
|
54
|
+
if (!p.success)
|
|
55
|
+
return err(p.error.issues[0].message);
|
|
56
|
+
const { code, focus = [] } = p.data;
|
|
57
|
+
// The pattern scan grounds the review. Without it an audit is pure model
|
|
58
|
+
// opinion โ someone might trust "looks safe" with nothing behind it.
|
|
59
|
+
const findings = (0, _solidity_scan_js_1.staticScanSolidity)(code);
|
|
60
|
+
const findingsBlock = (0, _solidity_scan_js_1.formatFindings)(findings);
|
|
61
|
+
const criticalCount = findings.filter((f) => f.severity === "critical").length;
|
|
62
|
+
const highCount = findings.filter((f) => f.severity === "high").length;
|
|
63
|
+
// The rubric travels with the findings rather than being applied out of sight,
|
|
64
|
+
// so the discipline the old server-side prompt enforced is preserved.
|
|
65
|
+
const rubric = (focus.length ? `**User-requested focus:** ${focus.join(", ")}\n\n` : "") +
|
|
66
|
+
`## How to review this\n\n` +
|
|
67
|
+
`**Overall risk** โ Critical / High / Medium / Low / Informational, plus one sentence of ` +
|
|
68
|
+
`justification. The scan found **${criticalCount} critical** and **${highCount} high**; where that ` +
|
|
69
|
+
`is โฅ1 critical or โฅ2 high, your rating must be at least High unless you explicitly refute those ` +
|
|
70
|
+
`findings with reasoning.\n\n` +
|
|
71
|
+
`**Static findings review** โ one line per finding, none skipped:\n` +
|
|
72
|
+
`\`- **<ID>** โ confirmed / refuted / needs-context โ (1-3 sentences)\`\n\n` +
|
|
73
|
+
`**Additional findings** the scan cannot catch (severity, location, description, recommendation), ` +
|
|
74
|
+
`or "No additional findings."\n\n` +
|
|
75
|
+
`**Gas optimizations** โ 2-5 concrete savings, skip if already tight.\n\n` +
|
|
76
|
+
`**Positive patterns** โ 3-5 bullets max.\n\n` +
|
|
77
|
+
`Never call the contract "secure" or "safe" โ that implies a guarantee no review can make. ` +
|
|
78
|
+
`Prefer "no obvious issues found in X under Y", and say where you are uncertain.`;
|
|
79
|
+
return ok([
|
|
80
|
+
`# Smart Contract Audit โ static scan`,
|
|
81
|
+
``,
|
|
82
|
+
`**${findings.length} finding(s)** โ ${criticalCount} critical ยท ${highCount} high ยท ` +
|
|
83
|
+
`${findings.length - criticalCount - highCount} medium/low/info`,
|
|
84
|
+
``,
|
|
85
|
+
`## Automated static scan findings`,
|
|
86
|
+
``,
|
|
87
|
+
findingsBlock,
|
|
88
|
+
``,
|
|
89
|
+
`---`,
|
|
90
|
+
``,
|
|
91
|
+
rubric,
|
|
92
|
+
_solidity_scan_js_1.AUDIT_DISCLAIMER,
|
|
93
|
+
].join("\n"));
|
|
94
|
+
}
|