@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/LICENSE +682 -0
  3. package/README.md +117 -0
  4. package/SECURITY.md +107 -0
  5. package/dist/app.js +88 -0
  6. package/dist/buildServer.js +146 -0
  7. package/dist/contracts/registerBlurtTool.js +53 -0
  8. package/dist/contracts/toolRegistry.js +384 -0
  9. package/dist/resources/blurtResource.js +82 -0
  10. package/dist/server-stdio.js +37 -0
  11. package/dist/server.js +35 -0
  12. package/dist/tools/claimRewards.js +48 -0
  13. package/dist/tools/comment.js +58 -0
  14. package/dist/tools/compareAccounts.js +50 -0
  15. package/dist/tools/fetch.js +91 -0
  16. package/dist/tools/follow.js +39 -0
  17. package/dist/tools/getAccount.js +80 -0
  18. package/dist/tools/getAccountHistory.js +109 -0
  19. package/dist/tools/getAccountNotifications.js +40 -0
  20. package/dist/tools/getAccountPosts.js +130 -0
  21. package/dist/tools/getAccountRelationships.js +34 -0
  22. package/dist/tools/getAccountSubscriptions.js +50 -0
  23. package/dist/tools/getAccountWitnessVotes.js +46 -0
  24. package/dist/tools/getBlurtPrice.js +43 -0
  25. package/dist/tools/getChainStatus.js +94 -0
  26. package/dist/tools/getCommunity.js +75 -0
  27. package/dist/tools/getDelegations.js +37 -0
  28. package/dist/tools/getPendingRewards.js +53 -0
  29. package/dist/tools/getPost.js +88 -0
  30. package/dist/tools/getPostReblogs.js +29 -0
  31. package/dist/tools/getPostVotes.js +78 -0
  32. package/dist/tools/getPublications.js +109 -0
  33. package/dist/tools/getReferrals.js +39 -0
  34. package/dist/tools/getVoteValue.js +67 -0
  35. package/dist/tools/getWitness.js +46 -0
  36. package/dist/tools/listCommunities.js +90 -0
  37. package/dist/tools/listWitnesses.js +48 -0
  38. package/dist/tools/lookupAccounts.js +30 -0
  39. package/dist/tools/mute.js +39 -0
  40. package/dist/tools/post.js +42 -0
  41. package/dist/tools/readNotifications.js +35 -0
  42. package/dist/tools/reblog.js +39 -0
  43. package/dist/tools/search.js +189 -0
  44. package/dist/tools/subscribeCommunity.js +39 -0
  45. package/dist/tools/upvote.js +48 -0
  46. package/dist/utils/blurtUri.js +61 -0
  47. package/dist/utils/loadEnv.js +21 -0
  48. package/dist/utils/logger.js +63 -0
  49. package/dist/utils/price.js +21 -0
  50. package/dist/utils/rpc.js +126 -0
  51. package/dist/utils/signer.js +350 -0
  52. package/docs/adr/0001-neutral-infrastructure.md +50 -0
  53. package/docs/architecture.md +62 -0
  54. package/docs/cache-policy.md +42 -0
  55. package/docs/clients.md +78 -0
  56. package/docs/deployment.md +102 -0
  57. package/docs/development.md +51 -0
  58. package/docs/install-snippets.md +236 -0
  59. package/docs/load-testing.md +51 -0
  60. package/docs/operations.md +56 -0
  61. package/docs/release-provenance.md +63 -0
  62. package/docs/tools.generated.md +89 -0
  63. package/docs/tools.md +102 -0
  64. package/docs/usage.md +157 -0
  65. package/docs/write-operations.md +223 -0
  66. 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
+ }