@blurt-blockchain/blurt-mcp-server 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/LICENSE +682 -0
- package/README.md +117 -0
- package/SECURITY.md +107 -0
- package/dist/app.js +88 -0
- package/dist/buildServer.js +146 -0
- package/dist/contracts/registerBlurtTool.js +53 -0
- package/dist/contracts/toolRegistry.js +384 -0
- package/dist/resources/blurtResource.js +82 -0
- package/dist/server-stdio.js +37 -0
- package/dist/server.js +35 -0
- package/dist/tools/claimRewards.js +48 -0
- package/dist/tools/comment.js +58 -0
- package/dist/tools/compareAccounts.js +50 -0
- package/dist/tools/fetch.js +91 -0
- package/dist/tools/follow.js +39 -0
- package/dist/tools/getAccount.js +80 -0
- package/dist/tools/getAccountHistory.js +109 -0
- package/dist/tools/getAccountNotifications.js +40 -0
- package/dist/tools/getAccountPosts.js +130 -0
- package/dist/tools/getAccountRelationships.js +34 -0
- package/dist/tools/getAccountSubscriptions.js +50 -0
- package/dist/tools/getAccountWitnessVotes.js +46 -0
- package/dist/tools/getBlurtPrice.js +43 -0
- package/dist/tools/getChainStatus.js +94 -0
- package/dist/tools/getCommunity.js +75 -0
- package/dist/tools/getDelegations.js +37 -0
- package/dist/tools/getPendingRewards.js +53 -0
- package/dist/tools/getPost.js +88 -0
- package/dist/tools/getPostReblogs.js +29 -0
- package/dist/tools/getPostVotes.js +78 -0
- package/dist/tools/getPublications.js +109 -0
- package/dist/tools/getReferrals.js +39 -0
- package/dist/tools/getVoteValue.js +67 -0
- package/dist/tools/getWitness.js +46 -0
- package/dist/tools/listCommunities.js +90 -0
- package/dist/tools/listWitnesses.js +48 -0
- package/dist/tools/lookupAccounts.js +30 -0
- package/dist/tools/mute.js +39 -0
- package/dist/tools/post.js +42 -0
- package/dist/tools/readNotifications.js +35 -0
- package/dist/tools/reblog.js +39 -0
- package/dist/tools/search.js +189 -0
- package/dist/tools/subscribeCommunity.js +39 -0
- package/dist/tools/upvote.js +48 -0
- package/dist/utils/blurtUri.js +61 -0
- package/dist/utils/loadEnv.js +21 -0
- package/dist/utils/logger.js +63 -0
- package/dist/utils/price.js +21 -0
- package/dist/utils/rpc.js +126 -0
- package/dist/utils/signer.js +350 -0
- package/docs/adr/0001-neutral-infrastructure.md +50 -0
- package/docs/architecture.md +62 -0
- package/docs/cache-policy.md +42 -0
- package/docs/clients.md +78 -0
- package/docs/deployment.md +102 -0
- package/docs/development.md +51 -0
- package/docs/install-snippets.md +236 -0
- package/docs/load-testing.md +51 -0
- package/docs/operations.md +56 -0
- package/docs/release-provenance.md +63 -0
- package/docs/tools.generated.md +89 -0
- package/docs/tools.md +102 -0
- package/docs/usage.md +157 -0
- package/docs/write-operations.md +223 -0
- package/package.json +77 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-account-witness-votes (Blurt — which witnesses an account supports)
|
|
7
|
+
*/
|
|
8
|
+
export function registerGetAccountWitnessVotes(server, client) {
|
|
9
|
+
registerBlurtTool(server, "get-account-witness-votes", {
|
|
10
|
+
title: "Get an account's witness votes",
|
|
11
|
+
description: "List the witnesses an account votes for (its governance support), and the proxy it uses if any. " +
|
|
12
|
+
"Use this when the user asks 'who does X support as witness' or to audit an account's governance " +
|
|
13
|
+
"choices. Parameter: username. Returns: the list of witness account names voted for (up to 30, the " +
|
|
14
|
+
"chain maximum), the count, and the witness proxy account if the account delegated its witness " +
|
|
15
|
+
"voting (in which case it has no direct votes of its own).",
|
|
16
|
+
annotations: { readOnlyHint: true },
|
|
17
|
+
inputSchema: {
|
|
18
|
+
username: z.string().min(3).describe("Account name (e.g., 'nalexadre')"),
|
|
19
|
+
},
|
|
20
|
+
}, async ({ username }) => {
|
|
21
|
+
logger.debug(`get-account-witness-votes: ${username}`);
|
|
22
|
+
try {
|
|
23
|
+
const [account] = await client.condenser.getAccounts([username]);
|
|
24
|
+
if (!account) {
|
|
25
|
+
logger.warn(`Account not found: ${username}`);
|
|
26
|
+
return { content: [{ type: "text", text: `Account not found: ${username}` }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
const votes = (account.witness_votes ?? []);
|
|
29
|
+
const proxy = (account.proxy ?? "");
|
|
30
|
+
const summary = {
|
|
31
|
+
account: username,
|
|
32
|
+
proxy: proxy || null,
|
|
33
|
+
witness_votes_count: votes.length,
|
|
34
|
+
witness_votes: votes,
|
|
35
|
+
note: proxy
|
|
36
|
+
? `Witness voting is delegated to the proxy account "${proxy}".`
|
|
37
|
+
: undefined,
|
|
38
|
+
};
|
|
39
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
logger.error(`[get-account-witness-votes] Error: ${e.stack}`);
|
|
43
|
+
return { content: [{ type: "text", text: `Error retrieving witness votes for ${username}` }], isError: true };
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { getBlurtPrice } from "../utils/price.js";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-blurt-price (Blurt — market price)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Return the current BLURT price in USD and BTC from the public price feed
|
|
10
|
+
* (https://api.blurt.blog/price_info).
|
|
11
|
+
*
|
|
12
|
+
* Parameters
|
|
13
|
+
* (none)
|
|
14
|
+
*
|
|
15
|
+
* Returns
|
|
16
|
+
* JSON: { price_usd, price_btc, source, fetched_at }
|
|
17
|
+
*/
|
|
18
|
+
export function registerGetBlurtPrice(server) {
|
|
19
|
+
registerBlurtTool(server, "get-blurt-price", {
|
|
20
|
+
title: "Get BLURT market price",
|
|
21
|
+
description: "Get the current market price of the BLURT token, in USD and BTC, from the public Blurt price feed. " +
|
|
22
|
+
"Call this whenever the user asks about the BLURT price or token value, or when you need to convert " +
|
|
23
|
+
"any on-chain BLURT amount (wallet balances, rewards, pending payouts, vote values) into fiat. " +
|
|
24
|
+
"Takes no parameters. Returns: price_usd, price_btc, the source URL, and an ISO fetched_at timestamp. " +
|
|
25
|
+
"The value is cached for ~60 seconds, so repeated calls are cheap.",
|
|
26
|
+
annotations: { readOnlyHint: true },
|
|
27
|
+
inputSchema: {},
|
|
28
|
+
}, async () => {
|
|
29
|
+
logger.debug("get-blurt-price tool called");
|
|
30
|
+
try {
|
|
31
|
+
const price = await getBlurtPrice();
|
|
32
|
+
logger.info(`BLURT price: ${price.price_usd} USD`);
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(price, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
logger.error(`[get-blurt-price] Error: ${err.message}`);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: "Error retrieving BLURT price." }],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { Asset } from "@beblurt/dblurt";
|
|
4
|
+
import { getBlurtPrice } from "../utils/price.js";
|
|
5
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
6
|
+
/**
|
|
7
|
+
* Tool: get-chain-status (Blurt — network & market overview)
|
|
8
|
+
*
|
|
9
|
+
* Purpose
|
|
10
|
+
* Provide a dashboard-style snapshot of the Blurt blockchain combining
|
|
11
|
+
* on-chain global properties, the content reward fund and the market price.
|
|
12
|
+
*
|
|
13
|
+
* Parameters
|
|
14
|
+
* (none)
|
|
15
|
+
*
|
|
16
|
+
* Returns
|
|
17
|
+
* JSON with:
|
|
18
|
+
* - network: head block, current witness, last irreversible block, block participation %
|
|
19
|
+
* - supply: current BLURT supply, total vesting fund (Blurt Power backing)
|
|
20
|
+
* - rewards: content reward pool balance, curation/author split
|
|
21
|
+
* - market: price_usd, price_btc, estimated market cap (supply × price)
|
|
22
|
+
*
|
|
23
|
+
* Notes
|
|
24
|
+
* The market price is best-effort; if the external feed is unavailable the
|
|
25
|
+
* on-chain data is still returned (market = null).
|
|
26
|
+
*/
|
|
27
|
+
export function registerGetChainStatus(server, client) {
|
|
28
|
+
registerBlurtTool(server, "get-chain-status", {
|
|
29
|
+
title: "Get Blurt chain status",
|
|
30
|
+
description: "Get a single-call network + market overview (dashboard) of the Blurt blockchain. " +
|
|
31
|
+
"Use this to answer questions about the state or health of the chain, its token economics, or market " +
|
|
32
|
+
"capitalization, instead of fetching several low-level properties separately. Takes no parameters. Returns:\n" +
|
|
33
|
+
"- network: head_block_number, last_irreversible_block_num, current_witness, time, block_participation_percent\n" +
|
|
34
|
+
"- supply: current_supply (total BLURT), total_vesting_fund_blurt and total_vesting_shares (the Blurt Power backing)\n" +
|
|
35
|
+
"- rewards: content_reward_balance (the post reward pool), curation_rewards_percent (author/curation split) and the reward curves\n" +
|
|
36
|
+
"- market: price_usd, price_btc, and estimated market_cap_usd (current_supply × price_usd)\n" +
|
|
37
|
+
"The market block is best-effort: it is null if the external price feed is unavailable, but the on-chain data is always returned.",
|
|
38
|
+
annotations: { readOnlyHint: true },
|
|
39
|
+
inputSchema: {},
|
|
40
|
+
}, async () => {
|
|
41
|
+
logger.debug("get-chain-status tool called");
|
|
42
|
+
try {
|
|
43
|
+
const [dgp, rewardFund] = await Promise.all([
|
|
44
|
+
client.condenser.getDynamicGlobalProperties(),
|
|
45
|
+
client.condenser.getRewardFund("post"),
|
|
46
|
+
]);
|
|
47
|
+
// Price is best-effort: don't fail the whole tool if the feed is down.
|
|
48
|
+
const price = await getBlurtPrice().catch((e) => {
|
|
49
|
+
logger.warn(`get-chain-status: price feed unavailable (${e.message})`);
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
const supply = Asset.from(dgp.current_supply).amount;
|
|
53
|
+
// recent_slots_filled is a 128-bit mask; participation_count is the populated count.
|
|
54
|
+
const participation = Number(((dgp.participation_count / 128) * 100).toFixed(1));
|
|
55
|
+
const summary = {
|
|
56
|
+
network: {
|
|
57
|
+
head_block_number: dgp.head_block_number,
|
|
58
|
+
last_irreversible_block_num: dgp.last_irreversible_block_num,
|
|
59
|
+
current_witness: dgp.current_witness,
|
|
60
|
+
time: dgp.time,
|
|
61
|
+
block_participation_percent: participation,
|
|
62
|
+
},
|
|
63
|
+
supply: {
|
|
64
|
+
current_supply: dgp.current_supply,
|
|
65
|
+
total_vesting_fund_blurt: dgp.total_vesting_fund_blurt,
|
|
66
|
+
total_vesting_shares: dgp.total_vesting_shares,
|
|
67
|
+
},
|
|
68
|
+
rewards: {
|
|
69
|
+
content_reward_balance: rewardFund.reward_balance,
|
|
70
|
+
curation_rewards_percent: rewardFund.percent_curation_rewards / 100,
|
|
71
|
+
author_reward_curve: rewardFund.author_reward_curve,
|
|
72
|
+
curation_reward_curve: rewardFund.curation_reward_curve,
|
|
73
|
+
},
|
|
74
|
+
market: price
|
|
75
|
+
? {
|
|
76
|
+
price_usd: price.price_usd,
|
|
77
|
+
price_btc: price.price_btc,
|
|
78
|
+
market_cap_usd: Number((supply * price.price_usd).toFixed(0)),
|
|
79
|
+
source: price.source,
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
82
|
+
};
|
|
83
|
+
logger.info(`Chain status retrieved (head block ${dgp.head_block_number})`);
|
|
84
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.error(`[get-chain-status] Error: ${err.stack}`);
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: "Error retrieving chain status." }],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-community (Blurt — Nexus community details)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Retrieve detailed information about a single Blurt community.
|
|
10
|
+
*
|
|
11
|
+
* Parameters (inputSchema)
|
|
12
|
+
* - name (string, required) — community name (e.g., "blurt-192372")
|
|
13
|
+
* - observer (string | null, optional) — observer account (subscription/role context)
|
|
14
|
+
*
|
|
15
|
+
* Behavior
|
|
16
|
+
* Calls: client.nexus.getCommunity({ name, observer })
|
|
17
|
+
*
|
|
18
|
+
* Returns
|
|
19
|
+
* - Human-readable summary (title, subscribers, authors, pending payout, team)
|
|
20
|
+
* - Full JSON of the community object
|
|
21
|
+
*/
|
|
22
|
+
export function registerGetCommunity(server, client) {
|
|
23
|
+
registerBlurtTool(server, "get-community", {
|
|
24
|
+
title: "Get Blurt community details",
|
|
25
|
+
description: "Get full details about one Blurt community, identified by its 'name' (e.g. 'blurt-192372' — obtained from " +
|
|
26
|
+
"list-communities, or from a post's category). Use this when the user asks about a specific community: its size, " +
|
|
27
|
+
"purpose, rules, moderators, or activity level. " +
|
|
28
|
+
"Parameters: name (required), observer (optional account, adds the caller's subscription/role context). " +
|
|
29
|
+
"Returns a readable summary (title, subscribers, author count, pending posts and payout, admins, about/description) " +
|
|
30
|
+
"plus the full JSON object. To list or search communities first, use list-communities.",
|
|
31
|
+
annotations: { readOnlyHint: true },
|
|
32
|
+
inputSchema: {
|
|
33
|
+
name: z.string().min(1)
|
|
34
|
+
.describe("Community name (e.g., 'blurt-192372')"),
|
|
35
|
+
observer: z.string().nullable().optional()
|
|
36
|
+
.describe("Observer account (affects subscription/role context)"),
|
|
37
|
+
},
|
|
38
|
+
}, async ({ name, observer }) => {
|
|
39
|
+
logger.debug(`get-community: name=${name}, observer=${observer}`);
|
|
40
|
+
try {
|
|
41
|
+
const community = await client.nexus.getCommunity({ name, observer: observer ?? null });
|
|
42
|
+
if (!community) {
|
|
43
|
+
logger.warn(`Community not found: ${name}`);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: `Community not found: ${name}` }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const admins = Array.isArray(community.admins)
|
|
50
|
+
? community.admins.join(", ")
|
|
51
|
+
: "—";
|
|
52
|
+
const summary = `Community ${community.name} — ${community.title}
|
|
53
|
+
- subscribers: ${community.subscribers}
|
|
54
|
+
- authors: ${community.num_authors}
|
|
55
|
+
- pending posts: ${community.num_pending}
|
|
56
|
+
- pending payout: ${community.sum_pending} BLURT
|
|
57
|
+
- lang: ${community.lang}${community.is_nsfw ? " (NSFW)" : ""}
|
|
58
|
+
- admins: ${admins}
|
|
59
|
+
- about: ${community.about ?? "—"}`;
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{ type: "text", text: summary },
|
|
63
|
+
{ type: "text", text: JSON.stringify(community, null, 2) },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
logger.error(`[get-community] Error: ${e.stack}`);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `Error retrieving community ${name}` }],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logger from "../utils/logger.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
4
|
+
/** Tool: get-delegations (Blurt) — Blurt Power an account delegates out. */
|
|
5
|
+
export function registerGetDelegations(server, client) {
|
|
6
|
+
registerBlurtTool(server, "get-delegations", {
|
|
7
|
+
title: "Get a Blurt account's outgoing Blurt Power delegations",
|
|
8
|
+
description: "List the active Blurt Power (vesting) delegations an account is granting to others, with each " +
|
|
9
|
+
"amount converted from VESTS to BLURT for readability. Use this when the user asks 'who does X " +
|
|
10
|
+
"delegate to', about an account's delegated stake, or to audit how its Blurt Power is shared. " +
|
|
11
|
+
"Parameters: username (the delegator), limit (1-100, default 50). Returns: total delegated in BLURT " +
|
|
12
|
+
"and a per-delegatee list (delegatee, blurt_power, raw VESTS, min_delegation_time). Note: this is " +
|
|
13
|
+
"outgoing delegation; for an account's own stake use get-account.",
|
|
14
|
+
annotations: { readOnlyHint: true },
|
|
15
|
+
inputSchema: {
|
|
16
|
+
username: z.string().min(3).describe("Delegator account name (e.g., 'nalexadre')"),
|
|
17
|
+
limit: z.number().int().min(1).max(100).default(50).describe("Max delegations to return"),
|
|
18
|
+
},
|
|
19
|
+
}, async ({ username, limit }) => {
|
|
20
|
+
logger.debug(`get-delegations: ${username}, limit=${limit}`);
|
|
21
|
+
try {
|
|
22
|
+
const model = await client.read.getOutgoingDelegationSummary(username, limit);
|
|
23
|
+
const summary = {
|
|
24
|
+
...model,
|
|
25
|
+
delegations: model.delegations.map((d) => ({
|
|
26
|
+
...d,
|
|
27
|
+
blurt_power: `${d.blurt_power.toFixed(3)} BLURT`,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
logger.error(`[get-delegations] Error: ${e.message}`);
|
|
34
|
+
return { content: [{ type: "text", text: `Error retrieving delegations for ${username}` }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Asset } from "@beblurt/dblurt";
|
|
5
|
+
import { getBlurtPrice } from "../utils/price.js";
|
|
6
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
7
|
+
/**
|
|
8
|
+
* Tool: get-pending-rewards (Blurt — unclaimed rewards)
|
|
9
|
+
*/
|
|
10
|
+
export function registerGetPendingRewards(server, client) {
|
|
11
|
+
registerBlurtTool(server, "get-pending-rewards", {
|
|
12
|
+
title: "Get a Blurt account's unclaimed rewards",
|
|
13
|
+
description: "Get the rewards an account has earned but not yet claimed (claimable now via a claim_reward " +
|
|
14
|
+
"operation). Use this when the user asks 'how much can X claim' or about pending/unclaimed earnings. " +
|
|
15
|
+
"Parameter: username. Returns: liquid BLURT reward, the reward to be received as Blurt Power " +
|
|
16
|
+
"(vesting), the combined total in BLURT, and a best-effort USD value (null if the price feed is " +
|
|
17
|
+
"unavailable). Note: these are cumulative-since-last-claim balances, distinct from an account's " +
|
|
18
|
+
"lifetime reward totals.",
|
|
19
|
+
annotations: { readOnlyHint: true },
|
|
20
|
+
inputSchema: {
|
|
21
|
+
username: z.string().min(3).describe("Account name (e.g., 'nalexadre')"),
|
|
22
|
+
},
|
|
23
|
+
}, async ({ username }) => {
|
|
24
|
+
logger.debug(`get-pending-rewards: ${username}`);
|
|
25
|
+
try {
|
|
26
|
+
const [account] = await client.condenser.getAccounts([username]);
|
|
27
|
+
if (!account) {
|
|
28
|
+
logger.warn(`Account not found: ${username}`);
|
|
29
|
+
return { content: [{ type: "text", text: `Account not found: ${username}` }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
const liquid = Asset.from(account.reward_blurt_balance.toString()).amount;
|
|
32
|
+
const vesting = Asset.from(account.reward_vesting_blurt.toString()).amount;
|
|
33
|
+
const total = liquid + vesting;
|
|
34
|
+
const price = await getBlurtPrice().catch((e) => {
|
|
35
|
+
logger.warn(`get-pending-rewards: price feed unavailable (${e.message})`);
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
const summary = {
|
|
39
|
+
account: username,
|
|
40
|
+
pending_liquid_blurt: account.reward_blurt_balance,
|
|
41
|
+
pending_power_blurt: account.reward_vesting_blurt,
|
|
42
|
+
total_pending_blurt: Number(total.toFixed(3)),
|
|
43
|
+
total_pending_usd: price ? Number((total * price.price_usd).toFixed(4)) : null,
|
|
44
|
+
claimable: total > 0,
|
|
45
|
+
};
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
logger.error(`[get-pending-rewards] Error: ${e.stack}`);
|
|
50
|
+
return { content: [{ type: "text", text: `Error retrieving pending rewards for ${username}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-post (Blurt — Nexus post/discussion)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Retrieve details of a single Blurt post, with the option to also include its full discussion (comments).
|
|
10
|
+
*
|
|
11
|
+
* Parameters
|
|
12
|
+
* - author (string, required): Blurt account name of the post’s author
|
|
13
|
+
* - permlink (string, required): Unique permlink (slug) of the post
|
|
14
|
+
* - with_comments (boolean, default false): If true, include the full discussion (comments)
|
|
15
|
+
*
|
|
16
|
+
* Behavior
|
|
17
|
+
* - If with_comments = false → calls client.nexus.getPost({ author, permlink })
|
|
18
|
+
* - If with_comments = true → calls client.nexus.getDiscussion(author, permlink)
|
|
19
|
+
*
|
|
20
|
+
* Returns
|
|
21
|
+
* - Human-readable summary of the post (title, author, category, created, votes, payout, comments count)
|
|
22
|
+
* - Full JSON of the post or discussion (pretty-printed)
|
|
23
|
+
*/
|
|
24
|
+
export function registerGetPost(server, client) {
|
|
25
|
+
registerBlurtTool(server, "get-post", {
|
|
26
|
+
title: "Get Blurt blockchain post or discussion",
|
|
27
|
+
description: "Retrieve a single Blurt post, or its full discussion tree (post + all comments), via the Nexus API. Use this to read a " +
|
|
28
|
+
"specific post's metadata or the whole thread. Identify the post by author + permlink (from a post URL of the form " +
|
|
29
|
+
"author/permlink, or from get-account-posts / get-publications / search results). Parameters: author, permlink, " +
|
|
30
|
+
"with_comments (default false; set true to include the full discussion). Returns a readable summary (title, author/permlink, " +
|
|
31
|
+
"category, created, votes, payout, comments) plus a blurt:// resource URI — fetch that URI with the fetch tool to get the " +
|
|
32
|
+
"complete JSON. Use get-post-votes to analyse who voted on the post.",
|
|
33
|
+
annotations: { readOnlyHint: true },
|
|
34
|
+
inputSchema: {
|
|
35
|
+
author: z.string().describe("Blurt account name of the post’s author"),
|
|
36
|
+
permlink: z.string().describe("Unique permlink (slug) of the post"),
|
|
37
|
+
with_comments: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.default(false)
|
|
40
|
+
.describe("Whether to include comments (discussion tree)"),
|
|
41
|
+
},
|
|
42
|
+
}, async ({ author, permlink, with_comments }) => {
|
|
43
|
+
logger.debug(`GET-POST via Nexus: author=${author}, permlink=${permlink}, with_comments=${with_comments}`);
|
|
44
|
+
try {
|
|
45
|
+
let result;
|
|
46
|
+
if (with_comments) {
|
|
47
|
+
result = await client.nexus.getDiscussion(author, permlink);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result = await client.nexus.getPost({ author, permlink });
|
|
51
|
+
}
|
|
52
|
+
if (!result) {
|
|
53
|
+
logger.warn(`Post not found: ${author}/${permlink}`);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `Post not found: ${author}/${permlink}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Extract main post (for discussion results, it's keyed as "author/permlink")
|
|
60
|
+
const post = with_comments ? result[`${author}/${permlink}`] : result;
|
|
61
|
+
const summary = `Post ${author}/${permlink} (${post.title})
|
|
62
|
+
- category: ${post.category}
|
|
63
|
+
- created: ${post.created}
|
|
64
|
+
- votes: ${post.stats?.total_votes ?? 0}
|
|
65
|
+
- payout: ${post.payout}
|
|
66
|
+
- comments: ${post.children}
|
|
67
|
+
- cover: ${post.json_metadata?.image ? post.json_metadata.image[0] ?? post.json_metadata.image : "none"}`;
|
|
68
|
+
const resourceUri = `blurt://post/${encodeURIComponent(author)}/${encodeURIComponent(permlink)}${with_comments ? "?with_comments=true" : ""}`;
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `${summary}\n\nFull JSON available via resource: ${resourceUri}\nTip: use the fetch tool with this URI to get the complete JSON.`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
logger.error(`[get-post] Error: ${e.stack}`);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{ type: "text", text: `Error retrieving post ${author}/${permlink}` },
|
|
83
|
+
],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logger from "../utils/logger.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
4
|
+
/** Tool: get-post-reblogs (Blurt) — accounts that reblogged a post. */
|
|
5
|
+
export function registerGetPostReblogs(server, client) {
|
|
6
|
+
registerBlurtTool(server, "get-post-reblogs", {
|
|
7
|
+
title: "Get the accounts that reblogged a Blurt post",
|
|
8
|
+
description: "List the accounts that reblogged (re-shared) a specific Blurt post, and how many. Use this when the " +
|
|
9
|
+
"user asks 'who reblogged X' or to measure how widely a post was re-shared (a reach signal distinct " +
|
|
10
|
+
"from votes). Identify the post by author + permlink. Parameters: author, permlink. Returns: count " +
|
|
11
|
+
"and the list of accounts that reblogged it. Complements get-post-votes.",
|
|
12
|
+
annotations: { readOnlyHint: true },
|
|
13
|
+
inputSchema: {
|
|
14
|
+
author: z.string().min(1).describe("Author of the post"),
|
|
15
|
+
permlink: z.string().min(1).describe("Permlink of the post"),
|
|
16
|
+
},
|
|
17
|
+
}, async ({ author, permlink }) => {
|
|
18
|
+
logger.debug(`get-post-reblogs: ${author}/${permlink}`);
|
|
19
|
+
try {
|
|
20
|
+
const accounts = await client.condenser.getRebloggedBy(author, permlink);
|
|
21
|
+
const summary = { author, permlink, count: accounts.length, accounts };
|
|
22
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
logger.error(`[get-post-reblogs] Error: ${e.message}`);
|
|
26
|
+
return { content: [{ type: "text", text: `Error retrieving reblogs for ${author}/${permlink}` }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-post-votes (Blurt — active votes on a post)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* List the votes cast on a given post/comment, ranked by weight (rshares),
|
|
10
|
+
* useful for curation analysis.
|
|
11
|
+
*
|
|
12
|
+
* Parameters (inputSchema)
|
|
13
|
+
* - author (string, required) — post author
|
|
14
|
+
* - permlink (string, required) — post permlink
|
|
15
|
+
* - limit (number, 1..1000, default 50) — max voters to display in the summary
|
|
16
|
+
*
|
|
17
|
+
* Behavior
|
|
18
|
+
* Calls: client.condenser.getActiveVotes(author, permlink)
|
|
19
|
+
*
|
|
20
|
+
* Returns
|
|
21
|
+
* - Summary: total voters, aggregate rshares, and the top voters (voter, percent, rshares, time)
|
|
22
|
+
* - Full JSON of the active votes array
|
|
23
|
+
*/
|
|
24
|
+
export function registerGetPostVotes(server, client) {
|
|
25
|
+
registerBlurtTool(server, "get-post-votes", {
|
|
26
|
+
title: "Get votes on a Blurt post",
|
|
27
|
+
description: "List the individual votes cast on a Blurt post or comment, ranked by weight, for curation and engagement analysis. " +
|
|
28
|
+
"Use this to answer 'who upvoted this post', to find the biggest curators on a post, or to see how vote weight is " +
|
|
29
|
+
"distributed among voters. Identify the post by author + permlink (from a post URL of the form author/permlink, or " +
|
|
30
|
+
"from the results of get-account-posts / get-publications / get-post). " +
|
|
31
|
+
"Parameters: author, permlink, limit (max voters shown in the readable summary, 1-1000, default 50 — the full list is " +
|
|
32
|
+
"always included as JSON regardless of limit). " +
|
|
33
|
+
"Returns: total voter count, aggregate rshares, and per-voter rows (voter, percent, rshares, time) sorted by rshares " +
|
|
34
|
+
"descending. 'rshares' is the raw vote weight that determines each voter's share of the rewards; 'percent' is the " +
|
|
35
|
+
"voter's chosen vote strength (100% = full power).",
|
|
36
|
+
annotations: { readOnlyHint: true },
|
|
37
|
+
inputSchema: {
|
|
38
|
+
author: z.string().min(1).describe("Post author"),
|
|
39
|
+
permlink: z.string().min(1).describe("Post permlink"),
|
|
40
|
+
limit: z.number().int().min(1).max(1000).default(50)
|
|
41
|
+
.describe("Max voters to show in the summary (full list still in JSON)"),
|
|
42
|
+
},
|
|
43
|
+
}, async ({ author, permlink, limit }) => {
|
|
44
|
+
logger.debug(`get-post-votes: ${author}/${permlink}, limit=${limit}`);
|
|
45
|
+
try {
|
|
46
|
+
const votes = await client.condenser.getActiveVotes(author, permlink);
|
|
47
|
+
if (!votes || votes.length === 0) {
|
|
48
|
+
logger.warn(`No votes for ${author}/${permlink}`);
|
|
49
|
+
return { content: [{ type: "text", text: `No votes found for ${author}/${permlink}.` }] };
|
|
50
|
+
}
|
|
51
|
+
const sorted = [...votes].sort((a, b) => Number(b.rshares) - Number(a.rshares));
|
|
52
|
+
const totalRshares = sorted.reduce((acc, v) => acc + Number(v.rshares), 0);
|
|
53
|
+
const top = sorted
|
|
54
|
+
.slice(0, limit)
|
|
55
|
+
.map((v, i) => `${i + 1}. ${v.voter} — ${v.percent / 100}% | rshares: ${v.rshares} | ${v.time}`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
const summary = `Votes on ${author}/${permlink}
|
|
58
|
+
- total voters: ${votes.length}
|
|
59
|
+
- aggregate rshares: ${totalRshares}
|
|
60
|
+
|
|
61
|
+
Top voters (showing ${Math.min(limit, sorted.length)}):
|
|
62
|
+
${top}`;
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{ type: "text", text: summary },
|
|
66
|
+
{ type: "text", text: JSON.stringify(sorted, null, 2) },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
logger.error(`[get-post-votes] Error: ${e.stack}`);
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: `Error retrieving votes for ${author}/${permlink}` }],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: get-publications (Blurt — Nexus getRankedPosts)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Returns ranked posts from Blurt’s Layer-2 Nexus API.
|
|
10
|
+
*
|
|
11
|
+
* Sort options
|
|
12
|
+
* "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"
|
|
13
|
+
*
|
|
14
|
+
* Parameters (inputSchema)
|
|
15
|
+
* - sort (enum, default "trending")
|
|
16
|
+
* - tag (string | null, optional) // filter by primary tag; null = no tag filter
|
|
17
|
+
* - limit (number, 1..100, default 20)
|
|
18
|
+
* - start_author (string | null, optional) // pagination cursor; use together with start_permlink
|
|
19
|
+
* - start_permlink(string | null, optional) // pagination cursor; use together with start_author
|
|
20
|
+
* - observer (string | null, optional) // observer account (affects visibility/muted content)
|
|
21
|
+
*
|
|
22
|
+
* Behavior
|
|
23
|
+
* Calls: client.nexus.getRankedPosts({ sort, start_author, start_permlink, limit, tag, observer })
|
|
24
|
+
* On success, returns two text contents:
|
|
25
|
+
* 1) A human-readable summary per post including:
|
|
26
|
+
* author/permlink, title, stats.total_votes, payout (unified), app (name only, from json_metadata.app),
|
|
27
|
+
* category, children (comment count), created, and optional json_metadata.description.
|
|
28
|
+
* 2) Pretty-printed JSON (NexusPost[]) for full downstream analysis.
|
|
29
|
+
*
|
|
30
|
+
* Notes
|
|
31
|
+
* - “app” keeps only the application name (e.g. "beblurt" from "beblurt/1.8.8").
|
|
32
|
+
* - “payout” is the unified numeric payout (not pending_payout_value), so it’s stable pre/post cashout.
|
|
33
|
+
* - Returns "No publications found." when the result set is empty.
|
|
34
|
+
*
|
|
35
|
+
* Examples
|
|
36
|
+
* tools/call get-publications {"sort":"trending","tag":"blurt","limit":10}
|
|
37
|
+
* tools/call get-publications {"sort":"created","start_author":"alice","start_permlink":"my-post"}
|
|
38
|
+
*/
|
|
39
|
+
export function registerGetPublications(server, client) {
|
|
40
|
+
registerBlurtTool(server, 'get-publications', {
|
|
41
|
+
title: 'Get Blurt blockchain publications (Nexus ranked posts)',
|
|
42
|
+
description: "Retrieve ranked posts from across the Blurt blockchain via the Nexus L2 API (getRankedPosts). Use this for discovery — " +
|
|
43
|
+
"what's trending/hot/new or top payouts — optionally within a tag. Sort modes: trending (recent engagement), hot (current " +
|
|
44
|
+
"attention), created (newest), promoted (paid promotion), payout (highest total payout), payout_comments (top-paid comments), " +
|
|
45
|
+
"muted (hidden by moderators). Parameters: sort (default trending); tag (optional, filter by a single tag e.g. 'blurt'); " +
|
|
46
|
+
"limit (1-100, default 20); start_author + start_permlink (pagination cursor from the last item of a previous page); observer " +
|
|
47
|
+
"(optional, affects muted/visibility). Returns a readable per-post summary (author/permlink, title, votes, payout, app, " +
|
|
48
|
+
"category, community, created). Use get-post to open one, or get-account-posts for a specific account's posts.",
|
|
49
|
+
annotations: { readOnlyHint: true },
|
|
50
|
+
inputSchema: {
|
|
51
|
+
sort: z.enum([
|
|
52
|
+
"trending",
|
|
53
|
+
"hot",
|
|
54
|
+
"created",
|
|
55
|
+
"promoted",
|
|
56
|
+
"payout",
|
|
57
|
+
"payout_comments",
|
|
58
|
+
"muted"
|
|
59
|
+
]).default("trending"),
|
|
60
|
+
tag: z.string().nullable().optional().describe("Blurt tag filter (e.g., 'blurt')"),
|
|
61
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Limit the number of posts returned"),
|
|
62
|
+
start_author: z.string().nullable().optional().describe("Start author for pagination (must be used with start_permlink)"),
|
|
63
|
+
start_permlink: z.string().nullable().optional().describe("Start permlink for pagination (must be used with start_author)"),
|
|
64
|
+
observer: z.string().nullable().optional().describe("Observer account (affects visibility, muted content, etc.)"),
|
|
65
|
+
},
|
|
66
|
+
}, async ({ sort, tag, limit, start_author, start_permlink, observer }) => {
|
|
67
|
+
logger.debug(`GET-PUBLICATIONS via Nexus.getRankedPosts: sort=${sort}, tag=${tag}, limit=${limit}, start_author=${start_author}, start_permlink=${start_permlink}, observer=${observer}`);
|
|
68
|
+
try {
|
|
69
|
+
const posts = await client.nexus.getRankedPosts({
|
|
70
|
+
sort,
|
|
71
|
+
start_author: start_author ?? undefined,
|
|
72
|
+
start_permlink: start_permlink ?? undefined,
|
|
73
|
+
limit,
|
|
74
|
+
tag: tag ?? null,
|
|
75
|
+
observer: observer ?? null,
|
|
76
|
+
});
|
|
77
|
+
if (!posts || posts.length === 0) {
|
|
78
|
+
logger.warn(`No publications found (sort=${sort}, tag=${tag})`);
|
|
79
|
+
return { content: [{ type: "text", text: `No publications found.` }] };
|
|
80
|
+
}
|
|
81
|
+
// Short readable summary
|
|
82
|
+
const summary = posts
|
|
83
|
+
.map((p, i) => {
|
|
84
|
+
const appRaw = p.json_metadata?.app ?? "unknown";
|
|
85
|
+
const app = appRaw.split("/")[0];
|
|
86
|
+
const description = p.json_metadata?.description ? `\n - description: ${p.json_metadata.description}` : "";
|
|
87
|
+
const community = p.community_title ? `\n - community: ${p.community_title}` : "";
|
|
88
|
+
return `${i + 1}. ${p.author}/${p.permlink} (${p.title})
|
|
89
|
+
- votes: ${p.stats?.total_votes ?? 0}
|
|
90
|
+
- payout: ${p.payout} BLURT
|
|
91
|
+
- app: ${app}
|
|
92
|
+
- category: ${p.category}
|
|
93
|
+
- comments: ${p.children}
|
|
94
|
+
- created: ${p.created}${description}${community}
|
|
95
|
+
- cover: ${p.json_metadata?.image ? p.json_metadata.image[0] ?? p.json_metadata.image : "none"}`;
|
|
96
|
+
})
|
|
97
|
+
.join("\n\n");
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{ type: "text", text: `Posts ${sort} (limit=${limit}, tag=${tag ?? "all"})\n${summary}` },
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
logger.error(`[get-publications] Error: ${e.stack}`);
|
|
106
|
+
return { content: [{ type: "text", text: "Error retrieving publications." }], isError: true };
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|