@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,50 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tool: compare-accounts (Blurt — side-by-side account metrics)
|
|
7
|
+
*/
|
|
8
|
+
export function registerCompareAccounts(server, client) {
|
|
9
|
+
registerBlurtTool(server, "compare-accounts", {
|
|
10
|
+
title: "Compare Blurt accounts",
|
|
11
|
+
description: "Compare 2 to 5 Blurt accounts side by side on their key metrics in a single call. Use this when " +
|
|
12
|
+
"the user wants to compare accounts — by stake, reach, output or earnings — instead of fetching each " +
|
|
13
|
+
"one separately. Parameter: usernames (array of 2-5 account names). Returns, per account: Blurt Power " +
|
|
14
|
+
"(staked, in BLURT), liquid balance, follower and following counts, post count, last post time, and " +
|
|
15
|
+
"cumulative author + curation rewards (BLURT). Missing accounts are reported in a `not_found` list.",
|
|
16
|
+
annotations: { readOnlyHint: true },
|
|
17
|
+
inputSchema: {
|
|
18
|
+
usernames: z.array(z.string().min(3)).min(2).max(5)
|
|
19
|
+
.describe("Account names to compare (2-5), e.g. ['nalexadre','beblurt']"),
|
|
20
|
+
},
|
|
21
|
+
}, async ({ usernames }) => {
|
|
22
|
+
logger.debug(`compare-accounts: ${usernames.join(",")}`);
|
|
23
|
+
try {
|
|
24
|
+
const models = await Promise.all(usernames.map((u) => client.read.getAccountSummary(u).then((summary) => ({ username: u, summary }), () => ({ username: u, summary: null }))));
|
|
25
|
+
const rows = models
|
|
26
|
+
.filter((m) => !!m.summary)
|
|
27
|
+
.map(({ summary }) => ({
|
|
28
|
+
account: summary.account.name,
|
|
29
|
+
blurt_power: Number(summary.wallet.vesting_to_blurt.toFixed(3)),
|
|
30
|
+
liquid_balance: summary.wallet.balance,
|
|
31
|
+
followers: summary.account.followers ?? null,
|
|
32
|
+
following: summary.account.following ?? null,
|
|
33
|
+
post_count: summary.account.post_count,
|
|
34
|
+
last_post: summary.account.last_post,
|
|
35
|
+
cumulative_author_rewards_blurt: Number(summary.rewards.cumulative_posting_rewards.replace(" BLURT", "")),
|
|
36
|
+
cumulative_curation_rewards_blurt: Number(summary.rewards.cumulative_curation_rewards.replace(" BLURT", "")),
|
|
37
|
+
}));
|
|
38
|
+
const notFound = models.filter((m) => !m.summary).map((m) => m.username);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{ type: "text", text: JSON.stringify({ accounts: rows, not_found: notFound }, null, 2) },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
logger.error(`[compare-accounts] Error: ${e.stack}`);
|
|
47
|
+
return { content: [{ type: "text", text: "Error comparing accounts." }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { utils as blurtUtils } from "@beblurt/dblurt";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { parseBlurtUri, idToBlurtUri } from "../utils/blurtUri.js";
|
|
6
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
7
|
+
function jsonContent(obj) {
|
|
8
|
+
return [{ type: "text", text: JSON.stringify(obj, null, 2) }];
|
|
9
|
+
}
|
|
10
|
+
export function registerFetchTool(server, client) {
|
|
11
|
+
registerBlurtTool(server, "fetch", {
|
|
12
|
+
title: "Fetch Blurt resource",
|
|
13
|
+
description: [
|
|
14
|
+
"Retrieve the raw JSON of a Blurt resource from a blurt:// URI or shorthand id. Use this to resolve a resource link",
|
|
15
|
+
"returned by the search tool, or when you already have a blurt:// URI. For most direct questions the dedicated tools",
|
|
16
|
+
"(get-account, get-publications, get-post, …) return friendlier, summarised output.",
|
|
17
|
+
"",
|
|
18
|
+
"Supported IDs/URIs:",
|
|
19
|
+
"- `blurt://account/<username>` → account object",
|
|
20
|
+
"- `blurt://history/<username>?limit=N` → recent operations for an account",
|
|
21
|
+
"- `blurt://posts/<sort>/<tag>?limit=N` → ranked posts (e.g., trending, hot, created)",
|
|
22
|
+
"- `blurt://account-posts/<sort>/<account>?limit=N` → account-related posts (blog, feed, comments, replies, payout)",
|
|
23
|
+
"- `blurt://post/<author>/<permlink>[?with_comments=true]` → single post or full discussion thread",
|
|
24
|
+
"",
|
|
25
|
+
"You may also use shorthand aliases like:",
|
|
26
|
+
"- `account-<username>`, `history-<username>`, `posts-<sort>-<tag>`, or `author/permlink`.",
|
|
27
|
+
"",
|
|
28
|
+
"Returns the full JSON representation of the resource (pretty-printed)."
|
|
29
|
+
].join("\n"),
|
|
30
|
+
// The ChatGPT connector spec expects a single required "id" parameter.
|
|
31
|
+
annotations: { readOnlyHint: true },
|
|
32
|
+
inputSchema: {
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
},
|
|
35
|
+
}, async ({ id }) => {
|
|
36
|
+
logger.debug(`FETCH called with id="${id}"`);
|
|
37
|
+
const target = idToBlurtUri(id);
|
|
38
|
+
if (!target) {
|
|
39
|
+
logger.warn(`FETCH: unsupported id "${id}"`);
|
|
40
|
+
return { content: [{ type: "text", text: `Unsupported id: ${id}` }], isError: true };
|
|
41
|
+
}
|
|
42
|
+
const parsed = parseBlurtUri(target);
|
|
43
|
+
logger.info(`FETCH resolved to ${parsed.kind} (${target})`);
|
|
44
|
+
if (parsed.kind === "account" && parsed.username) {
|
|
45
|
+
const [account] = await client.condenser.getAccounts([parsed.username]);
|
|
46
|
+
if (!account) {
|
|
47
|
+
logger.warn(`FETCH: account not found "${parsed.username}"`);
|
|
48
|
+
return { content: [{ type: "text", text: `Account not found: ${parsed.username}` }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
return { content: jsonContent(account) };
|
|
51
|
+
}
|
|
52
|
+
if (parsed.kind === "history" && parsed.username) {
|
|
53
|
+
const limit = Math.max(1, Math.min(1000, parsed.limit ?? 50));
|
|
54
|
+
let bitmask;
|
|
55
|
+
if (parsed.ops && parsed.ops.length) {
|
|
56
|
+
const opEnum = blurtUtils.operationOrders;
|
|
57
|
+
const selected = parsed.ops.map(n => opEnum[n]).filter((v) => typeof v === "number");
|
|
58
|
+
bitmask = blurtUtils.makeBitMaskFilter(selected);
|
|
59
|
+
}
|
|
60
|
+
const history = await client.condenser.getAccountHistory(parsed.username, -1, limit, bitmask);
|
|
61
|
+
return { content: jsonContent(history) };
|
|
62
|
+
}
|
|
63
|
+
if (parsed.kind === "account-posts" && parsed.sort && parsed.account) {
|
|
64
|
+
const limit = Math.max(1, Math.min(100, parsed.limit ?? 20));
|
|
65
|
+
const posts = await client.nexus.getAccountPosts({
|
|
66
|
+
sort: parsed.sort,
|
|
67
|
+
account: parsed.account,
|
|
68
|
+
limit,
|
|
69
|
+
observer: parsed.observer ?? null,
|
|
70
|
+
});
|
|
71
|
+
return { content: jsonContent(posts) };
|
|
72
|
+
}
|
|
73
|
+
if (parsed.kind === "post" && parsed.author && parsed.permlink) {
|
|
74
|
+
if (parsed.with_comments) {
|
|
75
|
+
const discussion = await client.nexus.getDiscussion(parsed.author, parsed.permlink);
|
|
76
|
+
return { content: jsonContent(discussion) };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const post = await client.nexus.getPost({ author: parsed.author, permlink: parsed.permlink });
|
|
80
|
+
return { content: jsonContent(post) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (parsed.kind === "posts" && parsed.sort) {
|
|
84
|
+
const limit = Math.max(1, Math.min(100, parsed.limit ?? 20));
|
|
85
|
+
const posts = await client.nexus.getRankedPosts({ sort: parsed.sort, limit, tag: parsed.tag ?? null });
|
|
86
|
+
return { content: jsonContent(posts) };
|
|
87
|
+
}
|
|
88
|
+
logger.error(`FETCH: unsupported target "${target}"`);
|
|
89
|
+
return { content: [{ type: "text", text: `Unsupported target: ${target}` }], isError: true };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logger from "../utils/logger.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { follow, withinCap, dryRunDefault } from "../utils/signer.js";
|
|
4
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
5
|
+
const CAP_MAX = 30;
|
|
6
|
+
const CAP_WINDOW_MS = 60 * 60 * 1000;
|
|
7
|
+
/** Tool: blurt-follow (Blurt — WRITE) — follow/unfollow an account. */
|
|
8
|
+
export function registerFollow(server, client, ctx) {
|
|
9
|
+
registerBlurtTool(server, "blurt-follow", {
|
|
10
|
+
title: "Follow or unfollow a Blurt account",
|
|
11
|
+
description: "WRITE operation (custom_json `follow`, signed with the local posting key): make the configured " +
|
|
12
|
+
"account follow or unfollow another Blurt account. Use this when the user asks to follow/unfollow " +
|
|
13
|
+
"someone. Parameters: account (the account to follow/unfollow), action ('follow' or 'unfollow', " +
|
|
14
|
+
"default 'follow'), dry_run (default follows BLURT_DRY_RUN_DEFAULT; false when unset — preview without broadcasting). Available only on the " +
|
|
15
|
+
"local stdio server; rate-limited.",
|
|
16
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17
|
+
inputSchema: {
|
|
18
|
+
account: z.string().min(3).describe("Account to follow/unfollow (e.g., 'nalexadre')"),
|
|
19
|
+
action: z.enum(["follow", "unfollow"]).default("follow")
|
|
20
|
+
.describe("'follow' to start following, 'unfollow' to stop (default 'follow')"),
|
|
21
|
+
dry_run: z.boolean().default(dryRunDefault()).describe("Preview only, without broadcasting"),
|
|
22
|
+
},
|
|
23
|
+
}, async ({ account, action, dry_run }) => {
|
|
24
|
+
logger.debug(`blurt-follow: ${action} ${account} (dry_run=${dry_run})`);
|
|
25
|
+
try {
|
|
26
|
+
if (!dry_run && !withinCap("follow", CAP_MAX, CAP_WINDOW_MS)) {
|
|
27
|
+
return { content: [{ type: "text", text: `Rate limit reached for follow (max ${CAP_MAX}/hour).` }], isError: true };
|
|
28
|
+
}
|
|
29
|
+
const result = await follow(client, ctx, { account, action, dryRun: dry_run });
|
|
30
|
+
if (result.done)
|
|
31
|
+
logger.info(`${action} @${account} (tx ${result.tx_id}).`);
|
|
32
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
logger.error(`[blurt-follow] Error: ${e.message}`);
|
|
36
|
+
return { content: [{ type: "text", text: `Error trying to ${action} ${account}` }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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 (Blurt — account details)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Retrieve full account information from the Blurt blockchain,
|
|
10
|
+
* combining low-level condenser data with enriched Nexus profile stats.
|
|
11
|
+
*
|
|
12
|
+
* Parameters (inputSchema)
|
|
13
|
+
* - username (string, required) — the Blurt account name to query
|
|
14
|
+
*
|
|
15
|
+
* Behavior
|
|
16
|
+
* Calls:
|
|
17
|
+
* - client.condenser.getAccounts([username]) for balances, vesting, witness votes, rewards
|
|
18
|
+
* - client.read.getAccountSummary(username) for SDK-owned account/stake/reward modeling
|
|
19
|
+
*
|
|
20
|
+
* Summary output includes:
|
|
21
|
+
* - Account: name, about, created (pre-fork detection), last active, last post, last vote, post_count, following, followers, referrer
|
|
22
|
+
* - Wallet: balance, savings, Blurt Power (converted from VESTS), mana %, delegation in/out, power down status
|
|
23
|
+
* - Witness: number of witness votes
|
|
24
|
+
* - Rewards: posting_rewards and curation_rewards converted to BLURT
|
|
25
|
+
*
|
|
26
|
+
* Returns
|
|
27
|
+
* JSON text with structured account, wallet, witness and rewards information.
|
|
28
|
+
*
|
|
29
|
+
* Example
|
|
30
|
+
* tools/call get-account {"username": "nalexadre"}
|
|
31
|
+
*/
|
|
32
|
+
export function registerGetAccount(server, client) {
|
|
33
|
+
registerBlurtTool(server, 'get-account', {
|
|
34
|
+
title: 'Get Blurt blockchain account details',
|
|
35
|
+
description: "Retrieve a structured profile of a Blurt account: identity, wallet, stake, witness votes and lifetime rewards. " +
|
|
36
|
+
"Use this as the starting point for almost any account question, or to get context before other account tools. " +
|
|
37
|
+
"Parameter: username. Returns: account (name, bio, created, last active/post/vote, post_count, followers/following, mana %), " +
|
|
38
|
+
"wallet (liquid balance, savings, Blurt Power converted from vested VESTS, delegations in/out, power-down status), the " +
|
|
39
|
+
"number of witness votes, and cumulative author + curation rewards. Amounts are in BLURT — pair with get-blurt-price to " +
|
|
40
|
+
"convert to USD. For more detail use get-pending-rewards (unclaimed rewards), get-account-witness-votes (which witnesses), " +
|
|
41
|
+
"get-account-relationships (social graph) or get-vote-value (upvote worth).",
|
|
42
|
+
annotations: { readOnlyHint: true },
|
|
43
|
+
inputSchema: {
|
|
44
|
+
username: z.string()
|
|
45
|
+
.min(3)
|
|
46
|
+
.describe("Blurt account name (e.g., 'nalexadre')"),
|
|
47
|
+
},
|
|
48
|
+
}, async ({ username }) => {
|
|
49
|
+
logger.debug(`get-account tool called with username: ${username}`);
|
|
50
|
+
try {
|
|
51
|
+
const model = await client.read.getAccountSummary(username);
|
|
52
|
+
const summary = {
|
|
53
|
+
...model,
|
|
54
|
+
wallet: {
|
|
55
|
+
...model.wallet,
|
|
56
|
+
vesting_to_blurt: `${model.wallet.vesting_to_blurt.toFixed(3)} BLURT`,
|
|
57
|
+
delegation_in: `${model.wallet.delegation_in_blurt.toFixed(3)} BLURT`,
|
|
58
|
+
delegation_out: `${model.wallet.delegation_out_blurt.toFixed(3)} BLURT`,
|
|
59
|
+
power_down: `${model.wallet.power_down_blurt.toFixed(3)} BLURT`,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
delete summary.wallet.delegation_in_blurt;
|
|
63
|
+
delete summary.wallet.delegation_out_blurt;
|
|
64
|
+
delete summary.wallet.power_down_blurt;
|
|
65
|
+
logger.info(`Account successfully retrieved: ${username}`);
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{ type: 'text', text: JSON.stringify(summary, null, 2) }
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.error(`Error retrieving ${username}'s account: ${err}`);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: `Error retrieving account: ${username}` }],
|
|
76
|
+
isError: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { utils as blurtUtils } from "@beblurt/dblurt";
|
|
5
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
6
|
+
/**
|
|
7
|
+
* Tool: get-account-history (Blurt — account history)
|
|
8
|
+
*
|
|
9
|
+
* Purpose
|
|
10
|
+
* Retrieve operations from a Blurt account history, with optional filtering by operation types.
|
|
11
|
+
*
|
|
12
|
+
* Parameters (inputSchema)
|
|
13
|
+
* - username (string, required) — Blurt account name (e.g., 'nalexadre')
|
|
14
|
+
* - from (number, default -1) — starting index (-1 = most recent)
|
|
15
|
+
* - limit (number, 1..1000, default 100) — maximum number of entries to fetch
|
|
16
|
+
* - operations(string[], optional) — list of operation names to filter (e.g., ["vote", "comment", "transfer"])
|
|
17
|
+
*
|
|
18
|
+
* Behavior
|
|
19
|
+
* Calls: client.condenser.getAccountHistory(username, from, limit, bitmaskFilter)
|
|
20
|
+
* If operations are provided, applies a bitmask filter built from blurtUtils.operationOrders.
|
|
21
|
+
*
|
|
22
|
+
* Returns
|
|
23
|
+
* - A short summary (counts by operation type)
|
|
24
|
+
* - A pretty-printed JSON array of the retrieved operations
|
|
25
|
+
*
|
|
26
|
+
* Example
|
|
27
|
+
* tools/call get-account-history {"username":"nalexadre","limit":20,"operations":["transfer","vote"]}
|
|
28
|
+
*/
|
|
29
|
+
export function registerGetAccountHistory(server, client) {
|
|
30
|
+
registerBlurtTool(server, 'get-account-history', {
|
|
31
|
+
title: 'Get Blurt account history',
|
|
32
|
+
description: "Retrieve the raw on-chain operation history of a Blurt account (transfers, votes, comments, claims, witness ops, …), " +
|
|
33
|
+
"newest first. Use this to audit or trace activity, find recent transfers/cashouts, or build an activity timeline. " +
|
|
34
|
+
"Parameters: username; from (history index to start from, -1 = most recent, default -1); limit (1-1000, default 100); " +
|
|
35
|
+
"operations (optional allow-list of operation type names to keep, e.g. ['transfer','vote'] — omit for all types). " +
|
|
36
|
+
"Returns a JSON array of [sequence, operation] entries exactly as recorded on chain. Note: the output can be large — " +
|
|
37
|
+
"narrow it with the operations filter and a small limit. For readable post activity, prefer get-account-posts.",
|
|
38
|
+
annotations: { readOnlyHint: true },
|
|
39
|
+
inputSchema: {
|
|
40
|
+
username: z.string()
|
|
41
|
+
.min(3)
|
|
42
|
+
.describe("Blurt account name (e.g., 'nalexadre')"),
|
|
43
|
+
from: z.number()
|
|
44
|
+
.default(-1)
|
|
45
|
+
.describe("Starting index in history (-1 = most recent)"),
|
|
46
|
+
limit: z.number()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(1000)
|
|
49
|
+
.default(100)
|
|
50
|
+
.describe("Maximum number of operations to retrieve (1-1000)"),
|
|
51
|
+
operations: z.array(z.enum([
|
|
52
|
+
"vote", "comment", "transfer", "transfer_to_vesting", "withdraw_vesting",
|
|
53
|
+
"account_create", "account_update", "witness_update", "account_witness_vote",
|
|
54
|
+
"account_witness_proxy", "custom", "delete_comment", "custom_json",
|
|
55
|
+
"comment_options", "set_withdraw_vesting_route", "claim_account",
|
|
56
|
+
"create_claimed_account", "request_account_recovery", "recover_account",
|
|
57
|
+
"change_recovery_account", "escrow_transfer", "escrow_dispute",
|
|
58
|
+
"escrow_release", "escrow_approve", "transfer_to_savings",
|
|
59
|
+
"transfer_from_savings", "cancel_transfer_from_savings", "custom_binary",
|
|
60
|
+
"decline_voting_rights", "reset_account", "set_reset_account",
|
|
61
|
+
"claim_reward_balance", "delegate_vesting_shares", "witness_set_properties",
|
|
62
|
+
"create_proposal", "update_proposal_votes", "remove_proposal",
|
|
63
|
+
"author_reward", "curation_reward", "comment_reward", "fill_vesting_withdraw",
|
|
64
|
+
"shutdown_witness", "fill_transfer_from_savings", "hardfork",
|
|
65
|
+
"comment_payout_update", "return_vesting_delegation",
|
|
66
|
+
"comment_benefactor_reward", "producer_reward", "clear_null_account_balance",
|
|
67
|
+
"proposal_pay", "sps_fund", "fee_pay"
|
|
68
|
+
])).optional()
|
|
69
|
+
.describe("Optional list of operation types to filter (e.g., ['vote','transfer','comment'])"),
|
|
70
|
+
}
|
|
71
|
+
}, async ({ username, from, limit, operations }) => {
|
|
72
|
+
logger.debug(`get-account-history called with username=${username}, from=${from}, limit=${limit}, operations=${JSON.stringify(operations)}`);
|
|
73
|
+
try {
|
|
74
|
+
let bitmask;
|
|
75
|
+
if (operations && operations.length > 0) {
|
|
76
|
+
// Map noms -> codes via utils.operationOrders
|
|
77
|
+
const opEnum = blurtUtils.operationOrders;
|
|
78
|
+
const selected = [];
|
|
79
|
+
for (const name of operations) {
|
|
80
|
+
const key = name;
|
|
81
|
+
if (opEnum[key] !== undefined)
|
|
82
|
+
selected.push(opEnum[key]);
|
|
83
|
+
}
|
|
84
|
+
if (selected.length === 0) {
|
|
85
|
+
logger.warn(`No matching operations found for filter: ${JSON.stringify(operations)}`);
|
|
86
|
+
}
|
|
87
|
+
bitmask = blurtUtils.makeBitMaskFilter(selected);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logger.warn('No operations provided for filtering.');
|
|
91
|
+
}
|
|
92
|
+
const history = await client.condenser.getAccountHistory(username, from, limit, bitmask);
|
|
93
|
+
// history: Array<[seq, AppliedOperation]>
|
|
94
|
+
logger.info(`Retrieved account history for ${username}, entries count: ${history.length}`);
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{ type: 'text', text: JSON.stringify(history, null, 2) }
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger.error(`Error in get-account-history for username=${username}: ${error}`);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: 'text', text: `Error retrieving account history: ${username}` }],
|
|
105
|
+
isError: true
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logger from "../utils/logger.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerBlurtTool } from "../contracts/registerBlurtTool.js";
|
|
4
|
+
/** Tool: get-account-notifications (Blurt) — an account's notifications feed. */
|
|
5
|
+
export function registerGetAccountNotifications(server, client) {
|
|
6
|
+
registerBlurtTool(server, "get-account-notifications", {
|
|
7
|
+
title: "Get a Blurt account's notifications",
|
|
8
|
+
description: "Get an account's recent notifications (mentions, replies, votes, follows, reblogs) and its unread " +
|
|
9
|
+
"count. Use this when the user asks 'what are my notifications', 'who mentioned/replied to me', or " +
|
|
10
|
+
"to review recent activity directed at an account. Parameters: username, limit (1-100, default 30). " +
|
|
11
|
+
"Returns: unread (count) and last_read timestamp, plus a list of notifications, each with type, a " +
|
|
12
|
+
"human message, score, date and url. To mark them read afterwards, use blurt-read-notifications " +
|
|
13
|
+
"(write, local only).",
|
|
14
|
+
annotations: { readOnlyHint: true },
|
|
15
|
+
inputSchema: {
|
|
16
|
+
username: z.string().min(3).describe("Account name (e.g., 'nalexadre')"),
|
|
17
|
+
limit: z.number().int().min(1).max(100).default(30).describe("Max notifications to return"),
|
|
18
|
+
},
|
|
19
|
+
}, async ({ username, limit }) => {
|
|
20
|
+
logger.debug(`get-account-notifications: ${username}, limit=${limit}`);
|
|
21
|
+
try {
|
|
22
|
+
const [notifications, unread] = await Promise.all([
|
|
23
|
+
client.nexus.accountNotifications({ account: username, limit }),
|
|
24
|
+
client.nexus.unreadNotifications({ account: username }),
|
|
25
|
+
]);
|
|
26
|
+
const summary = {
|
|
27
|
+
account: username,
|
|
28
|
+
unread: unread.unread,
|
|
29
|
+
last_read: unread.lastread,
|
|
30
|
+
returned: notifications.length,
|
|
31
|
+
notifications,
|
|
32
|
+
};
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
logger.error(`[get-account-notifications] Error: ${e.message}`);
|
|
37
|
+
return { content: [{ type: "text", text: `Error retrieving notifications for ${username}` }], isError: true };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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-posts (Blurt — Nexus account posts)
|
|
7
|
+
*
|
|
8
|
+
* Purpose
|
|
9
|
+
* Retrieve a list of posts related to a given Blurt account using the Nexus Layer-2 API.
|
|
10
|
+
*
|
|
11
|
+
* Sort options (sort parameter)
|
|
12
|
+
* - "blog" → posts authored by the account (excluding communities unless reblogged) + reblogs, ranked by creation/reblog time
|
|
13
|
+
* - "feed" → posts from blogs of accounts that the given account follows (max age = 1 month)
|
|
14
|
+
* - "posts" → top posts authored by the account, newer first
|
|
15
|
+
* - "comments"→ comments authored by the account, newer first
|
|
16
|
+
* - "replies" → replies to the account’s posts, newer first
|
|
17
|
+
* - "payout" → all posts authored by the account that have not yet been cashed out
|
|
18
|
+
*
|
|
19
|
+
* Parameters (inputSchema)
|
|
20
|
+
* - account (string, required) — the Blurt account to fetch
|
|
21
|
+
* - sort (enum, required) — one of "blog", "feed", "posts", "comments", "replies", "payout"
|
|
22
|
+
* - start_author (string | null, optional) — author name for pagination (use with start_permlink)
|
|
23
|
+
* - start_permlink (string | null, optional) — permlink for pagination (use with start_author)
|
|
24
|
+
* - limit (number, default 20, max 1000) — number of posts to fetch
|
|
25
|
+
* - observer (string | null, optional) — observer account (affects visibility/muted content)
|
|
26
|
+
*
|
|
27
|
+
* Behavior
|
|
28
|
+
* Calls: client.nexus.getAccountPosts({ sort, account, start_author, start_permlink, limit, observer })
|
|
29
|
+
*
|
|
30
|
+
* Returns
|
|
31
|
+
* - A human-readable summary of the retrieved posts (title, created, votes, payout, app, comments count, optional description).
|
|
32
|
+
* - Full JSON of the retrieved NexusPost[] (pretty-printed) for deeper analysis.
|
|
33
|
+
*
|
|
34
|
+
* Example
|
|
35
|
+
* tools/call get-account-posts {"account":"nalexadre","sort":"blog","limit":5}
|
|
36
|
+
*/
|
|
37
|
+
export function registerGetAccountPosts(server, client) {
|
|
38
|
+
registerBlurtTool(server, "get-account-posts", {
|
|
39
|
+
title: "Get Blurt account posts",
|
|
40
|
+
description: "Retrieve posts related to a Blurt account via the Nexus L2 API. Use this to read what an account has published or " +
|
|
41
|
+
"engaged with — their blog, authored posts, comments, replies received, their feed, or pending-payout posts.\n" +
|
|
42
|
+
"Sort modes:\n" +
|
|
43
|
+
" - blog: posts authored by the account (excluding communities unless reblogged) plus reblogs, by time.\n" +
|
|
44
|
+
" - feed: posts from accounts the given account follows (up to ~1 month old).\n" +
|
|
45
|
+
" - posts: top-level posts authored by the account, newest first.\n" +
|
|
46
|
+
" - comments: comments authored by the account, newest first.\n" +
|
|
47
|
+
" - replies: replies to the account's posts (by anyone), newest first.\n" +
|
|
48
|
+
" - payout: the account's posts not yet cashed out (pending payout).\n" +
|
|
49
|
+
"Parameters: account, sort (required); limit (1-1000, default 20); start_author + start_permlink (pagination cursor taken " +
|
|
50
|
+
"from the last item of a previous page); observer (optional, affects muted/visibility). Returns a readable per-post summary " +
|
|
51
|
+
"plus the post JSON (each post's body is truncated to keep the response small — use get-post for a post's full content). " +
|
|
52
|
+
"Use get-post to open one post, get-post-votes for its voters.",
|
|
53
|
+
annotations: { readOnlyHint: true },
|
|
54
|
+
inputSchema: {
|
|
55
|
+
account: z.string().describe("Blurt account to fetch posts for (e.g., 'nalexadre')"),
|
|
56
|
+
sort: z.enum(["blog", "feed", "posts", "comments", "replies", "payout"])
|
|
57
|
+
.describe("Sorting mode: blog, feed, posts, comments, replies, or payout"),
|
|
58
|
+
start_author: z.string().nullable().optional()
|
|
59
|
+
.describe("Author name for pagination (must be used with start_permlink)"),
|
|
60
|
+
start_permlink: z.string().nullable().optional()
|
|
61
|
+
.describe("Permlink for pagination (must be used with start_author)"),
|
|
62
|
+
limit: z.number().int().min(1).max(1000).default(20)
|
|
63
|
+
.describe("Number of results to fetch (max 1000)"),
|
|
64
|
+
observer: z.string().nullable().optional()
|
|
65
|
+
.describe("Observer account (affects visibility/muted content)"),
|
|
66
|
+
},
|
|
67
|
+
}, async ({ account, sort, start_author, start_permlink, limit, observer }) => {
|
|
68
|
+
logger.debug(`GET-ACCOUNT-POSTS via Nexus: account=${account}, sort=${sort}, limit=${limit}, start_author=${start_author}, start_permlink=${start_permlink}, observer=${observer}`);
|
|
69
|
+
try {
|
|
70
|
+
const posts = await client.nexus.getAccountPosts({
|
|
71
|
+
sort,
|
|
72
|
+
account,
|
|
73
|
+
start_author: start_author ?? undefined,
|
|
74
|
+
start_permlink: start_permlink ?? undefined,
|
|
75
|
+
limit,
|
|
76
|
+
observer: observer ?? null,
|
|
77
|
+
});
|
|
78
|
+
if (!posts || posts.length === 0) {
|
|
79
|
+
logger.warn(`No posts found for account=${account}, sort=${sort}`);
|
|
80
|
+
return { content: [{ type: "text", text: `No posts found.` }] };
|
|
81
|
+
}
|
|
82
|
+
// Human-readable summary
|
|
83
|
+
const summary = posts
|
|
84
|
+
.map((p, i) => {
|
|
85
|
+
const appRaw = p.json_metadata?.app ?? "unknown";
|
|
86
|
+
const app = appRaw.split("/")[0];
|
|
87
|
+
const desc = p.json_metadata?.description
|
|
88
|
+
? `\n - description: ${p.json_metadata.description}`
|
|
89
|
+
: "";
|
|
90
|
+
return `${i + 1}. ${p.author}/${p.permlink} (${p.title})
|
|
91
|
+
- created: ${p.created}
|
|
92
|
+
- votes: ${p.stats?.total_votes ?? 0}
|
|
93
|
+
- payout: ${p.payout}
|
|
94
|
+
- app: ${app}
|
|
95
|
+
- cover: ${p.json_metadata?.image ? p.json_metadata.image[0] ?? p.json_metadata.image : "none"}
|
|
96
|
+
- comments: ${p.children}${desc}`;
|
|
97
|
+
})
|
|
98
|
+
.join("\n\n");
|
|
99
|
+
// Truncate each post body so the JSON stays a reasonable size. The full
|
|
100
|
+
// body of a specific post remains reachable via get-post / fetch.
|
|
101
|
+
const MAX_BODY_CHARS = 800;
|
|
102
|
+
const trimmed = posts.map((p) => {
|
|
103
|
+
const body = typeof p.body === "string" ? p.body : "";
|
|
104
|
+
if (body.length <= MAX_BODY_CHARS)
|
|
105
|
+
return p;
|
|
106
|
+
return {
|
|
107
|
+
...p,
|
|
108
|
+
body: body.slice(0, MAX_BODY_CHARS) + "… [truncated]",
|
|
109
|
+
body_length: body.length,
|
|
110
|
+
body_truncated: true,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{ type: "text", text: `Posts for @${account} (${sort}, limit=${limit})\n${summary}` },
|
|
116
|
+
{ type: "text", text: JSON.stringify(trimmed, null, 2) }, // bodies truncated; use get-post for full content
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
logger.error(`[get-account-posts] Error: ${e.stack}`);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{ type: "text", text: `Error retrieving posts for ${account}` },
|
|
125
|
+
],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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-relationships (Blurt — followers & following)
|
|
7
|
+
*/
|
|
8
|
+
export function registerGetAccountRelationships(server, client) {
|
|
9
|
+
registerBlurtTool(server, "get-account-relationships", {
|
|
10
|
+
title: "Get a Blurt account's followers and following",
|
|
11
|
+
description: "Get an account's social graph: total follower and following counts, plus a sample of each. " +
|
|
12
|
+
"Use this when the user asks 'who follows X', 'who does X follow', or about an account's reach and " +
|
|
13
|
+
"network. Parameters: username, and sample (how many of each list to include, 0-100, default 10 — " +
|
|
14
|
+
"set 0 for counts only). Returns: follower_count, following_count, and sample arrays of follower / " +
|
|
15
|
+
"following account names. The lists are large for popular accounts, so this returns a sample, not " +
|
|
16
|
+
"the full graph.",
|
|
17
|
+
annotations: { readOnlyHint: true },
|
|
18
|
+
inputSchema: {
|
|
19
|
+
username: z.string().min(3).describe("Account name (e.g., 'nalexadre')"),
|
|
20
|
+
sample: z.number().int().min(0).max(100).default(10)
|
|
21
|
+
.describe("How many followers and following to include as a sample (0 = counts only)"),
|
|
22
|
+
},
|
|
23
|
+
}, async ({ username, sample }) => {
|
|
24
|
+
logger.debug(`get-account-relationships: ${username}, sample=${sample}`);
|
|
25
|
+
try {
|
|
26
|
+
const summary = await client.read.getSocialGraphSummary(username, sample);
|
|
27
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
logger.error(`[get-account-relationships] Error: ${e.stack}`);
|
|
31
|
+
return { content: [{ type: "text", text: `Error retrieving relationships for ${username}` }], isError: true };
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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-subscriptions (Blurt — communities an account follows)
|
|
7
|
+
*/
|
|
8
|
+
export function registerGetAccountSubscriptions(server, client) {
|
|
9
|
+
registerBlurtTool(server, "get-account-subscriptions", {
|
|
10
|
+
title: "Get a Blurt account's community subscriptions",
|
|
11
|
+
description: "List the communities an account is subscribed to, with the account's role in each. Use this when " +
|
|
12
|
+
"the user asks 'which communities does X follow/belong to' or to understand an account's interests " +
|
|
13
|
+
"and community involvement. Parameter: username. Returns, per subscription: the community name (id, " +
|
|
14
|
+
"e.g. 'blurt-101010'), its title, the account's role there (guest, member, mod, admin, owner) and " +
|
|
15
|
+
"any role title. Use get-community for details on one of the returned communities.",
|
|
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-subscriptions: ${username}`);
|
|
22
|
+
try {
|
|
23
|
+
const subs = await client.nexus.listAllSubscriptions(username);
|
|
24
|
+
if (!subs || subs.length === 0) {
|
|
25
|
+
return { content: [{ type: "text", text: `@${username} has no community subscriptions.` }] };
|
|
26
|
+
}
|
|
27
|
+
// Each entry is [community_name, title, role, role_title].
|
|
28
|
+
const subscriptions = subs.map(([name, title, role, roleTitle]) => ({
|
|
29
|
+
name,
|
|
30
|
+
title,
|
|
31
|
+
role,
|
|
32
|
+
role_title: roleTitle || null,
|
|
33
|
+
}));
|
|
34
|
+
const summary = `@${username} is subscribed to ${subscriptions.length} communities:\n` +
|
|
35
|
+
subscriptions
|
|
36
|
+
.map((s, i) => `${i + 1}. ${s.name} — ${s.title} (role: ${s.role}${s.role_title ? `, ${s.role_title}` : ""})`)
|
|
37
|
+
.join("\n");
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{ type: "text", text: summary },
|
|
41
|
+
{ type: "text", text: JSON.stringify(subscriptions, null, 2) },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
logger.error(`[get-account-subscriptions] Error: ${e.stack}`);
|
|
47
|
+
return { content: [{ type: "text", text: `Error retrieving subscriptions for ${username}` }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|