@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,350 @@
|
|
|
1
|
+
// Write-operation support: posting-key handling, least-privilege validation,
|
|
2
|
+
// rate caps, and the broadcast operations (claim rewards, upvote, comment).
|
|
3
|
+
//
|
|
4
|
+
// SECURITY (see SECURITY.md):
|
|
5
|
+
// - Only a Blurt POSTING key is ever accepted (active/owner are refused).
|
|
6
|
+
// - The key flows ONLY into dblurt's local signing; never to the network, logs,
|
|
7
|
+
// or any other dependency.
|
|
8
|
+
// - Write is enabled ONLY on the local stdio server by default. HTTP signing is
|
|
9
|
+
// possible only behind an explicit, dangerously named operator override.
|
|
10
|
+
import { PrivateKey, Asset, buildReplyOperation, buildPostOperation, normalizeContentTags, buildFollowOperation, buildUnfollowOperation, buildMuteOperation, buildUnmuteOperation, buildCommunitySubscribeOperation, buildCommunityUnsubscribeOperation, buildReadNotificationOperation, buildReblogOperation, buildUndoReblogOperation, } from "@beblurt/dblurt";
|
|
11
|
+
import logger from "./logger.js";
|
|
12
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
13
|
+
/** json_metadata.app value stamped on content created through this connector. */
|
|
14
|
+
const APP_TAG = `blurt-mcp/${pkg.version}`;
|
|
15
|
+
/** Footer appended to every comment body. */
|
|
16
|
+
const MCP_FOOTER = "<sub>via [Blurt-MCP](https://gitlab.com/blurt-blockchain/blurt-mcp-server)</sub>";
|
|
17
|
+
/** Canonical list of every write tool's allowlist name. */
|
|
18
|
+
export const WRITE_TOOLS = [
|
|
19
|
+
"claim-rewards",
|
|
20
|
+
"upvote",
|
|
21
|
+
"comment",
|
|
22
|
+
"follow",
|
|
23
|
+
"mute",
|
|
24
|
+
"subscribe-community",
|
|
25
|
+
"read-notifications",
|
|
26
|
+
"reblog",
|
|
27
|
+
"post",
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Semantic write profiles. These are the recommended operator-facing control
|
|
31
|
+
* surface; individual tool names remain available as a subtractive legacy
|
|
32
|
+
* denylist for compatibility and emergency lockdown.
|
|
33
|
+
*/
|
|
34
|
+
export const WRITE_PROFILES = {
|
|
35
|
+
none: [],
|
|
36
|
+
// Low-risk curation/account-maintenance profile for new users.
|
|
37
|
+
curator: ["claim-rewards", "upvote"],
|
|
38
|
+
// Social graph/community/reblog operations, but no original text publishing.
|
|
39
|
+
social: [
|
|
40
|
+
"claim-rewards",
|
|
41
|
+
"upvote",
|
|
42
|
+
"follow",
|
|
43
|
+
"mute",
|
|
44
|
+
"subscribe-community",
|
|
45
|
+
"read-notifications",
|
|
46
|
+
"reblog",
|
|
47
|
+
],
|
|
48
|
+
// All current content/social/reward tools. Future high-risk tools should be
|
|
49
|
+
// classified deliberately before they are added to this profile.
|
|
50
|
+
publisher: [
|
|
51
|
+
"claim-rewards",
|
|
52
|
+
"upvote",
|
|
53
|
+
"comment",
|
|
54
|
+
"follow",
|
|
55
|
+
"mute",
|
|
56
|
+
"subscribe-community",
|
|
57
|
+
"read-notifications",
|
|
58
|
+
"reblog",
|
|
59
|
+
"post",
|
|
60
|
+
],
|
|
61
|
+
// Explicit full-access profile and legacy unset-profile behavior.
|
|
62
|
+
full: WRITE_TOOLS,
|
|
63
|
+
};
|
|
64
|
+
function normalizeWriteProfile(profileEnv) {
|
|
65
|
+
const raw = profileEnv?.trim().toLowerCase();
|
|
66
|
+
if (!raw)
|
|
67
|
+
return "full"; // backward compatibility for existing deployments
|
|
68
|
+
if (raw === "all" || raw === "legacy")
|
|
69
|
+
return "full";
|
|
70
|
+
if (raw in WRITE_PROFILES)
|
|
71
|
+
return raw;
|
|
72
|
+
throw new Error(`Invalid BLURT_WRITE_PROFILE=${profileEnv}. Expected one of: ${Object.keys(WRITE_PROFILES).join(", ")}.`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve which write tools are enabled.
|
|
76
|
+
*
|
|
77
|
+
* Backward compatibility: when `BLURT_WRITE_PROFILE` is unset, the historical
|
|
78
|
+
* behavior is preserved: every write tool is enabled and
|
|
79
|
+
* `BLURT_WRITE_TOOLS_BANNED` subtracts individual tools.
|
|
80
|
+
*
|
|
81
|
+
* New deployments should set a semantic `BLURT_WRITE_PROFILE` such as
|
|
82
|
+
* `curator`, `social`, or `publisher`, then optionally subtract individual tools
|
|
83
|
+
* with `BLURT_WRITE_TOOLS_BANNED`.
|
|
84
|
+
*/
|
|
85
|
+
export function enabledWriteTools(bannedEnv = process.env.BLURT_WRITE_TOOLS_BANNED, profileEnv = process.env.BLURT_WRITE_PROFILE) {
|
|
86
|
+
const profile = normalizeWriteProfile(profileEnv);
|
|
87
|
+
const banned = new Set((bannedEnv ?? "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
88
|
+
return new Set(WRITE_PROFILES[profile].filter((t) => !banned.has(t)));
|
|
89
|
+
}
|
|
90
|
+
export function dryRunDefault(env = process.env) {
|
|
91
|
+
const raw = env.BLURT_DRY_RUN_DEFAULT?.trim().toLowerCase();
|
|
92
|
+
if (!raw)
|
|
93
|
+
return false;
|
|
94
|
+
if (["1", "true", "yes", "on"].includes(raw))
|
|
95
|
+
return true;
|
|
96
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
97
|
+
return false;
|
|
98
|
+
throw new Error("Invalid BLURT_DRY_RUN_DEFAULT. Expected true/false, 1/0, yes/no, or on/off.");
|
|
99
|
+
}
|
|
100
|
+
export const HTTP_SIGNING_UNSAFE_OVERRIDE_ENV = "BLURT_UNSAFE_ALLOW_HTTP_SIGNING_WITH_POSTING_KEY";
|
|
101
|
+
export const HTTP_SIGNING_UNSAFE_OVERRIDE_VALUE = "I_ACCEPT_FULL_RESPONSIBILITY_FOR_EXPOSING_BLURT_WRITE_TOOLS_OVER_HTTP";
|
|
102
|
+
const HTTP_SIGNING_UNSAFE_WARNING = `
|
|
103
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
104
|
+
!!! UNSAFE HTTP SIGNING ENABLED !!!
|
|
105
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
106
|
+
The Blurt MCP HTTP server is starting with BLURT_POSTING_KEY configured and
|
|
107
|
+
${HTTP_SIGNING_UNSAFE_OVERRIDE_ENV}=${HTTP_SIGNING_UNSAFE_OVERRIDE_VALUE}.
|
|
108
|
+
|
|
109
|
+
This exposes Blurt write/signing tools through the HTTP MCP endpoint. HTTP MCP
|
|
110
|
+
clients may be remote, automated, compromised, misconfigured, or reachable through
|
|
111
|
+
reverse-proxy mistakes. Any tool call that reaches this server can request signed
|
|
112
|
+
posting operations for the configured account.
|
|
113
|
+
|
|
114
|
+
Use this only for private, authenticated, access-controlled trusted deployments.
|
|
115
|
+
Do not use it on the public hosted endpoint or on an unauthenticated LAN/WAN
|
|
116
|
+
service. The operator accepts full responsibility for every signature, post,
|
|
117
|
+
vote, custom_json, claim, or other on-chain operation produced by this process.
|
|
118
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
|
|
119
|
+
/**
|
|
120
|
+
* Guard for the HTTP entrypoint: refuse to start if a posting key is configured
|
|
121
|
+
* unless the operator sets the exact, intentionally dangerous override value.
|
|
122
|
+
*
|
|
123
|
+
* Returns true only when HTTP signing has been explicitly allowed.
|
|
124
|
+
*/
|
|
125
|
+
export function assertNoKeyForHttp(options = {}) {
|
|
126
|
+
const env = options.env ?? process.env;
|
|
127
|
+
const warn = options.warn ?? ((message) => logger.error(message));
|
|
128
|
+
if (!env.BLURT_POSTING_KEY)
|
|
129
|
+
return false;
|
|
130
|
+
if (env[HTTP_SIGNING_UNSAFE_OVERRIDE_ENV] === HTTP_SIGNING_UNSAFE_OVERRIDE_VALUE) {
|
|
131
|
+
warn(HTTP_SIGNING_UNSAFE_WARNING);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const suffix = env[HTTP_SIGNING_UNSAFE_OVERRIDE_ENV]
|
|
135
|
+
? ` To override anyway, ${HTTP_SIGNING_UNSAFE_OVERRIDE_ENV} must exactly equal ${HTTP_SIGNING_UNSAFE_OVERRIDE_VALUE}.`
|
|
136
|
+
: "";
|
|
137
|
+
throw new Error("BLURT_POSTING_KEY is set but the HTTP server must stay read-only by default. " +
|
|
138
|
+
"Run the stdio server (npm run start:stdio) for normal signing operations." +
|
|
139
|
+
suffix);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build the write context from the environment. Returns null when no key is
|
|
143
|
+
* configured (read-only). Throws when a key is present but invalid or is not the
|
|
144
|
+
* account's posting key — the caller must then refuse to start.
|
|
145
|
+
*/
|
|
146
|
+
export async function createWriteContext(client) {
|
|
147
|
+
const account = process.env.BLURT_ACCOUNT?.trim();
|
|
148
|
+
const wif = process.env.BLURT_POSTING_KEY?.trim();
|
|
149
|
+
if (!account && !wif)
|
|
150
|
+
return null; // read-only
|
|
151
|
+
if (!account || !wif) {
|
|
152
|
+
throw new Error("Both BLURT_ACCOUNT and BLURT_POSTING_KEY must be set to enable write operations.");
|
|
153
|
+
}
|
|
154
|
+
let key;
|
|
155
|
+
try {
|
|
156
|
+
key = PrivateKey.fromString(wif);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new Error("BLURT_POSTING_KEY is not a valid WIF private key.");
|
|
160
|
+
}
|
|
161
|
+
await assertIsPostingKey(client, account, key);
|
|
162
|
+
return { account, key, enabledTools: enabledWriteTools() };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Least privilege: require a key that satisfies POSTING authority over the
|
|
166
|
+
* account, using dblurt's canonical Layer 1 authority traversal. The SDK reports
|
|
167
|
+
* owner/active matches as metadata rather than treating them as protocol errors;
|
|
168
|
+
* this MCP local-signing mode deliberately rejects those broader authorities as
|
|
169
|
+
* application policy.
|
|
170
|
+
*/
|
|
171
|
+
async function assertIsPostingKey(client, account, key) {
|
|
172
|
+
const result = await client.condenser.validatePostingAuthority(account, key);
|
|
173
|
+
if (result.matches.owner.authorized || result.matches.active.authorized) {
|
|
174
|
+
throw new Error("Refusing to use an owner/active key — only POSTING authority is allowed.");
|
|
175
|
+
}
|
|
176
|
+
if (!result.authorized) {
|
|
177
|
+
throw new Error(`The provided key has no posting authority over @${account} ` +
|
|
178
|
+
`(reason: ${result.reason}).`);
|
|
179
|
+
}
|
|
180
|
+
const viaDelegation = (result.matches.posting.approvedAccounts ?? []).length > 0;
|
|
181
|
+
logger.info(`Write enabled for @${account} (posting authority validated${viaDelegation ? ", via delegation" : ""}).`);
|
|
182
|
+
}
|
|
183
|
+
// --- Simple in-memory, per-tool rate cap -----------------------------------
|
|
184
|
+
const callTimes = {};
|
|
185
|
+
export function withinCap(tool, max, windowMs) {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const recent = (callTimes[tool] = (callTimes[tool] ?? []).filter((t) => now - t < windowMs));
|
|
188
|
+
if (recent.length >= max)
|
|
189
|
+
return false;
|
|
190
|
+
recent.push(now);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
export async function claimRewards(client, ctx, dryRun) {
|
|
194
|
+
const [acc] = await client.condenser.getAccounts([ctx.account]);
|
|
195
|
+
if (!acc)
|
|
196
|
+
throw new Error(`Account not found: @${ctx.account}`);
|
|
197
|
+
const reward_blurt = acc.reward_blurt_balance.toString();
|
|
198
|
+
const reward_vests = acc.reward_vesting_balance.toString();
|
|
199
|
+
const nothing = Asset.from(reward_blurt).amount === 0 && Asset.from(reward_vests).amount === 0;
|
|
200
|
+
const base = {
|
|
201
|
+
account: ctx.account,
|
|
202
|
+
pending_blurt: reward_blurt,
|
|
203
|
+
pending_vests: reward_vests,
|
|
204
|
+
pending_power_blurt: acc.reward_vesting_blurt.toString(),
|
|
205
|
+
nothing_to_claim: nothing,
|
|
206
|
+
};
|
|
207
|
+
if (dryRun || nothing) {
|
|
208
|
+
return { ...base, dry_run: dryRun, claimed: false };
|
|
209
|
+
}
|
|
210
|
+
const result = await client.broadcast.claimRewardBalance({ account: ctx.account, reward_blurt, reward_vests }, ctx.key);
|
|
211
|
+
return { ...base, dry_run: false, claimed: true, tx_id: result.id };
|
|
212
|
+
}
|
|
213
|
+
export async function upvote(client, ctx, args) {
|
|
214
|
+
const base = {
|
|
215
|
+
voter: ctx.account,
|
|
216
|
+
author: args.author,
|
|
217
|
+
permlink: args.permlink,
|
|
218
|
+
weight_percent: args.weight,
|
|
219
|
+
};
|
|
220
|
+
if (args.dryRun)
|
|
221
|
+
return { ...base, dry_run: true, voted: false };
|
|
222
|
+
// weight is a percentage (1-100); on chain 100% = 10000.
|
|
223
|
+
const result = await client.broadcast.vote({ voter: ctx.account, author: args.author, permlink: args.permlink, weight: args.weight * 100 }, ctx.key);
|
|
224
|
+
return { ...base, dry_run: false, voted: true, tx_id: result.id };
|
|
225
|
+
}
|
|
226
|
+
export async function comment(client, ctx, args) {
|
|
227
|
+
const body = `${args.body}\n\n${MCP_FOOTER}`;
|
|
228
|
+
const op = buildReplyOperation({
|
|
229
|
+
parentAuthor: args.parentAuthor,
|
|
230
|
+
parentPermlink: args.parentPermlink,
|
|
231
|
+
author: ctx.account,
|
|
232
|
+
body,
|
|
233
|
+
app: APP_TAG,
|
|
234
|
+
format: "markdown",
|
|
235
|
+
permlinkSuffix: Date.now().toString(36),
|
|
236
|
+
});
|
|
237
|
+
const payload = op[1];
|
|
238
|
+
const base = {
|
|
239
|
+
author: payload.author,
|
|
240
|
+
permlink: payload.permlink,
|
|
241
|
+
parent_author: payload.parent_author,
|
|
242
|
+
parent_permlink: payload.parent_permlink,
|
|
243
|
+
app: APP_TAG,
|
|
244
|
+
body: payload.body,
|
|
245
|
+
};
|
|
246
|
+
if (args.dryRun)
|
|
247
|
+
return { ...base, dry_run: true, posted: false };
|
|
248
|
+
const result = await client.broadcast.sendOperations([op], ctx.key);
|
|
249
|
+
return { ...base, dry_run: false, posted: true, tx_id: txId(result) };
|
|
250
|
+
}
|
|
251
|
+
// --- shared helpers ---------------------------------------------------------
|
|
252
|
+
const txId = (r) => r.id;
|
|
253
|
+
export async function follow(client, ctx, args) {
|
|
254
|
+
const operation = args.action === "follow"
|
|
255
|
+
? buildFollowOperation({ follower: ctx.account, following: args.account })
|
|
256
|
+
: buildUnfollowOperation({ follower: ctx.account, following: args.account });
|
|
257
|
+
const payload = JSON.parse(operation[1].json);
|
|
258
|
+
const base = { follower: ctx.account, following: args.account, action: args.action };
|
|
259
|
+
if (args.dryRun)
|
|
260
|
+
return { ...base, dry_run: true, done: false, tx_id: undefined };
|
|
261
|
+
const result = args.action === "follow"
|
|
262
|
+
? await client.broadcast.follow(ctx.account, args.account, ctx.key)
|
|
263
|
+
: await client.broadcast.unfollow(ctx.account, args.account, ctx.key);
|
|
264
|
+
void payload;
|
|
265
|
+
return { ...base, dry_run: false, done: true, tx_id: txId(result) };
|
|
266
|
+
}
|
|
267
|
+
export async function mute(client, ctx, args) {
|
|
268
|
+
const operation = args.action === "mute"
|
|
269
|
+
? buildMuteOperation({ follower: ctx.account, following: args.account })
|
|
270
|
+
: buildUnmuteOperation({ follower: ctx.account, following: args.account });
|
|
271
|
+
const base = { follower: ctx.account, account: args.account, action: args.action };
|
|
272
|
+
if (args.dryRun) {
|
|
273
|
+
void operation;
|
|
274
|
+
return { ...base, dry_run: true, done: false, tx_id: undefined };
|
|
275
|
+
}
|
|
276
|
+
const result = args.action === "mute"
|
|
277
|
+
? await client.broadcast.mute(ctx.account, args.account, ctx.key)
|
|
278
|
+
: await client.broadcast.unmute(ctx.account, args.account, ctx.key);
|
|
279
|
+
return { ...base, dry_run: false, done: true, tx_id: txId(result) };
|
|
280
|
+
}
|
|
281
|
+
export async function subscribeCommunity(client, ctx, args) {
|
|
282
|
+
const operation = args.action === "subscribe"
|
|
283
|
+
? buildCommunitySubscribeOperation({ account: ctx.account, community: args.community })
|
|
284
|
+
: buildCommunityUnsubscribeOperation({ account: ctx.account, community: args.community });
|
|
285
|
+
const base = { account: ctx.account, community: args.community, action: args.action };
|
|
286
|
+
if (args.dryRun) {
|
|
287
|
+
void operation;
|
|
288
|
+
return { ...base, dry_run: true, done: false, tx_id: undefined };
|
|
289
|
+
}
|
|
290
|
+
const result = args.action === "subscribe"
|
|
291
|
+
? await client.broadcast.communitySubscribe(ctx.account, args.community, ctx.key)
|
|
292
|
+
: await client.broadcast.communityUnsubscribe(ctx.account, args.community, ctx.key);
|
|
293
|
+
return { ...base, dry_run: false, done: true, tx_id: txId(result) };
|
|
294
|
+
}
|
|
295
|
+
// --- mark notifications read ------------------------------------------------
|
|
296
|
+
export async function readNotifications(client, ctx, args) {
|
|
297
|
+
const date = new Date().toISOString().slice(0, 19); // YYYY-MM-DDTHH:mm:ss
|
|
298
|
+
const operation = buildReadNotificationOperation({ account: ctx.account, date });
|
|
299
|
+
const base = { account: ctx.account, marked_read_up_to: date };
|
|
300
|
+
if (args.dryRun) {
|
|
301
|
+
void operation;
|
|
302
|
+
return { ...base, dry_run: true, done: false, tx_id: undefined };
|
|
303
|
+
}
|
|
304
|
+
const result = await client.broadcast.readNotification(ctx.account, date, ctx.key);
|
|
305
|
+
return { ...base, dry_run: false, done: true, tx_id: txId(result) };
|
|
306
|
+
}
|
|
307
|
+
// --- reblog -----------------------------------------------------------------
|
|
308
|
+
export async function reblog(client, ctx, args) {
|
|
309
|
+
const operation = args.undo
|
|
310
|
+
? buildUndoReblogOperation({ account: ctx.account, author: args.author, permlink: args.permlink })
|
|
311
|
+
: buildReblogOperation({ account: ctx.account, author: args.author, permlink: args.permlink });
|
|
312
|
+
const base = { account: ctx.account, author: args.author, permlink: args.permlink, undo: args.undo };
|
|
313
|
+
if (args.dryRun) {
|
|
314
|
+
void operation;
|
|
315
|
+
return { ...base, dry_run: true, done: false, tx_id: undefined };
|
|
316
|
+
}
|
|
317
|
+
const result = args.undo
|
|
318
|
+
? await client.broadcast.undoReblog(ctx.account, args.author, args.permlink, ctx.key)
|
|
319
|
+
: await client.broadcast.reblog(ctx.account, args.author, args.permlink, ctx.key);
|
|
320
|
+
return { ...base, dry_run: false, done: true, tx_id: txId(result) };
|
|
321
|
+
}
|
|
322
|
+
export async function post(client, ctx, args) {
|
|
323
|
+
const tags = normalizeContentTags(args.tags);
|
|
324
|
+
if (tags.length === 0)
|
|
325
|
+
throw new Error("At least one tag is required.");
|
|
326
|
+
const body = `${args.body}\n\n${MCP_FOOTER}`;
|
|
327
|
+
const op = buildPostOperation({
|
|
328
|
+
author: ctx.account,
|
|
329
|
+
title: args.title,
|
|
330
|
+
body,
|
|
331
|
+
tags,
|
|
332
|
+
app: APP_TAG,
|
|
333
|
+
format: "markdown",
|
|
334
|
+
permlinkSuffix: Date.now().toString(36),
|
|
335
|
+
});
|
|
336
|
+
const payload = op[1];
|
|
337
|
+
const base = {
|
|
338
|
+
author: payload.author,
|
|
339
|
+
permlink: payload.permlink,
|
|
340
|
+
parent_permlink: payload.parent_permlink,
|
|
341
|
+
title: payload.title,
|
|
342
|
+
tags,
|
|
343
|
+
app: APP_TAG,
|
|
344
|
+
body: payload.body,
|
|
345
|
+
};
|
|
346
|
+
if (args.dryRun)
|
|
347
|
+
return { ...base, dry_run: true, posted: false };
|
|
348
|
+
const result = await client.broadcast.sendOperations([op], ctx.key);
|
|
349
|
+
return { ...base, dry_run: false, posted: true, tx_id: txId(result) };
|
|
350
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ADR 0001 — Official server remains neutral infrastructure
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The Blurt MCP server is the official MCP interface for Blurt capabilities. It exposes public chain data and optional posting-authority write tools to AI clients.
|
|
10
|
+
|
|
11
|
+
During the Phase 3 write-security review, an earlier roadmap considered richer operator policy files: allowed/blocked accounts, blocked communities/tags, posting quotas, and a separate transaction-preview architecture. That direction was re-evaluated against Blurt's Layer 1 guarantees and ecosystem values:
|
|
12
|
+
|
|
13
|
+
- posting authority cannot move funds;
|
|
14
|
+
- Layer 1 already enforces resource costs and operational limits;
|
|
15
|
+
- Blurt is intentionally designed around freedom of expression;
|
|
16
|
+
- operators can enforce private deployment policy outside the official server if they need it.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
The official Blurt MCP server remains neutral infrastructure.
|
|
21
|
+
|
|
22
|
+
It provides secure, predictable access to Blurt capabilities, but does not impose content, account, community, tag, or usage policy beyond protocol safety, transport safety, and explicit operator-selected execution mode.
|
|
23
|
+
|
|
24
|
+
The server may implement neutral safeguards such as:
|
|
25
|
+
|
|
26
|
+
- read-only public HTTP by default;
|
|
27
|
+
- posting-authority validation;
|
|
28
|
+
- refusal of owner/active keys;
|
|
29
|
+
- explicit unsafe override for HTTP signing;
|
|
30
|
+
- local signing and secret-handling protections;
|
|
31
|
+
- capability/profile selection for which write tools are registered;
|
|
32
|
+
- dry-run defaults and exact previews;
|
|
33
|
+
- small technical rate caps to prevent accidental loops.
|
|
34
|
+
|
|
35
|
+
The server should not become an official policy engine for:
|
|
36
|
+
|
|
37
|
+
- allowed or blocked accounts;
|
|
38
|
+
- allowed or blocked tags/communities;
|
|
39
|
+
- content moderation rules;
|
|
40
|
+
- posting/comment quotas as social policy;
|
|
41
|
+
- paternalistic restrictions on how users use posting authority.
|
|
42
|
+
|
|
43
|
+
Those policies belong in clients, wrappers, gateways, reverse proxies, external signers, custom deployments, or forks when an operator needs them.
|
|
44
|
+
|
|
45
|
+
## Consequences
|
|
46
|
+
|
|
47
|
+
- Phase 3 is reduced to the completed signing baseline, write-surface ergonomics, `BLURT_DRY_RUN_DEFAULT`, and documentation closeout.
|
|
48
|
+
- The previously proposed policy-file work is removed from the official server roadmap.
|
|
49
|
+
- A standalone transaction-preview architecture is deferred until a concrete external-signer, wallet, offline-signing, or multi-operation transaction milestone requires it.
|
|
50
|
+
- Future write features such as governance, witness management, wallet/external-signer flows, and moderation tools should be evaluated against this neutrality principle before adding server-side policy controls.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Architecture — how it works
|
|
2
|
+
|
|
3
|
+
- **Two entrypoints, one server.** `server.ts` exposes the tools over **Streamable HTTP** — a single
|
|
4
|
+
Express route, `POST /mcp`, in **stateless** mode (no session id), with a fresh `McpServer` built
|
|
5
|
+
per request via `buildServer()` (`GET`/`DELETE` return `405`). It also exposes cheap operational
|
|
6
|
+
probes outside MCP: `GET /healthz` and `GET /readyz`. `server-stdio.ts` exposes the same tools over
|
|
7
|
+
**stdio** for local desktop use. Both call `buildServer()`.
|
|
8
|
+
- **Shared Blurt client.** One `@beblurt/dblurt` `Client` is created on first use and reused so RPC
|
|
9
|
+
connections stay warm (only the first call pays the cold-connection cost). It is configured with
|
|
10
|
+
`rpcTransport: "core"` — the default "legacy" transport aborts each attempt after `(tries+1)*500ms`
|
|
11
|
+
and then crashes (`error.code.includes is not a function`) on slow requests under the HTTP server
|
|
12
|
+
context. The choice is guarded by `test/resilience.test.ts`.
|
|
13
|
+
- **RPC node selection.** A background checker
|
|
14
|
+
([`@beblurt/blurt-nodes-checker`](https://gitlab.com/beblurt/blurt-nodes-checker)) continuously
|
|
15
|
+
scores the configured nodes (latency, chain lag, version, reliability) and repoints the shared
|
|
16
|
+
client at the healthy ones, best-first — so a slow, stale, forked or offline node is avoided rather
|
|
17
|
+
than failing a request (`src/utils/rpc.ts`). dblurt's own failover then covers transient blips
|
|
18
|
+
within that healthy set.
|
|
19
|
+
- **Two data layers.** Account, wallet, history and raw votes come from the **condenser API**
|
|
20
|
+
(Layer 1); profiles, ranked posts, account posts, discussions and communities come from the
|
|
21
|
+
**Nexus / Bridge API** (Layer 2).
|
|
22
|
+
- **Market price.** `get-blurt-price`, `get-chain-status` and `get-vote-value` read the external price
|
|
23
|
+
feed (`BLURT_PRICE_URL`) through a shared helper that caches it for ~60s. Price is best-effort: if
|
|
24
|
+
the feed is down, on-chain data is still returned and USD fields are `null`.
|
|
25
|
+
- **`search` / `fetch`.** `search` maps a free-form query to `blurt://…` resource links without any
|
|
26
|
+
network call; `fetch` resolves such a URI (or a shorthand id) to the raw JSON. This mirrors the
|
|
27
|
+
search/fetch connector convention used by ChatGPT-style clients.
|
|
28
|
+
- **Write path (opt-in).** When a posting key is present, write tools are registered through a guarded
|
|
29
|
+
write context (`src/utils/signer.ts`); the key is validated as the account's posting authority, used
|
|
30
|
+
only for local signing, and never logged or sent to the network. New deployments should use semantic
|
|
31
|
+
`BLURT_WRITE_PROFILE` capability profiles; the historical `BLURT_WRITE_TOOLS_BANNED` denylist still
|
|
32
|
+
works as a subtractive compatibility control. `BLURT_DRY_RUN_DEFAULT=true` is a neutral preview-first
|
|
33
|
+
execution-mode default: omitted `dry_run` parameters preview without broadcasting, but explicit
|
|
34
|
+
`dry_run: false` still executes. HTTP stays read-only by default and exposes signing only behind the
|
|
35
|
+
explicit unsafe trusted-deployment override. The official server does not enforce content/account/
|
|
36
|
+
community policy files; see [ADR 0001](./adr/0001-neutral-infrastructure.md),
|
|
37
|
+
[write operations](./write-operations.md), and [SECURITY.md](../SECURITY.md).
|
|
38
|
+
- **Errors.** Tool failures return `isError: true` with a short message rather than throwing, so the
|
|
39
|
+
model can react instead of seeing a transport error.
|
|
40
|
+
|
|
41
|
+
## Project layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
server.ts # HTTP entrypoint: builds the app and listens
|
|
46
|
+
server-stdio.ts # stdio entrypoint: same tools as a local child process
|
|
47
|
+
app.ts # Builds the Express app + /mcp route (no side effects, testable)
|
|
48
|
+
buildServer.ts # Registers all tools/resources on an McpServer; BLURT_CLIENT_OPTIONS
|
|
49
|
+
tools/ # One file per MCP tool
|
|
50
|
+
resources/ # blurt:// resource templates
|
|
51
|
+
utils/
|
|
52
|
+
logger.ts # Leveled logger (human / JSON), writes to stderr
|
|
53
|
+
loadEnv.ts # Loads BLURT_ENV_FILE / project .env regardless of cwd
|
|
54
|
+
rpc.ts # Shared client + RPC node selection (blurt-nodes-checker)
|
|
55
|
+
signer.ts # Write context: posting-key validation, rate caps, signing
|
|
56
|
+
price.ts # Cached BLURT price feed helper
|
|
57
|
+
blurtUri.ts # blurt:// URI parsing / shorthand mapping (used by search/fetch)
|
|
58
|
+
test/
|
|
59
|
+
uri / search / resilience / write # offline (no external network)
|
|
60
|
+
live # live, in-memory MCP client
|
|
61
|
+
http # live, real Streamable HTTP transport (E2E)
|
|
62
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Cache and freshness policy
|
|
2
|
+
|
|
3
|
+
The official MCP server should avoid broad response caching until result contracts consistently carry
|
|
4
|
+
source/freshness metadata. Incorrectly cached blockchain data can mislead AI summaries and users.
|
|
5
|
+
|
|
6
|
+
## Current cache
|
|
7
|
+
|
|
8
|
+
The only intentional application cache is the BLURT price helper cache. It is narrow and short-lived:
|
|
9
|
+
|
|
10
|
+
- external price feed only;
|
|
11
|
+
- approximately 60 seconds;
|
|
12
|
+
- best-effort;
|
|
13
|
+
- failures do not invalidate on-chain data;
|
|
14
|
+
- USD/BTC values may be `null` when unavailable.
|
|
15
|
+
|
|
16
|
+
## No generic response cache yet
|
|
17
|
+
|
|
18
|
+
Do not add a generic cache layer for account, wallet, history, witness, vote, post, community or pending
|
|
19
|
+
reward tools in Phase 4. These values have different freshness expectations and source layers:
|
|
20
|
+
|
|
21
|
+
- Layer 1 account/wallet/reward/vote/witness state changes with blocks and account activity;
|
|
22
|
+
- Nexus/Bridge ranked posts and community views have their own indexing freshness;
|
|
23
|
+
- external market prices have separate freshness and failure modes;
|
|
24
|
+
- mixed dashboard-style tools combine several sources.
|
|
25
|
+
|
|
26
|
+
## Requirement before broader caching
|
|
27
|
+
|
|
28
|
+
Before introducing per-tool TTLs, structured results should expose provenance/freshness metadata such as:
|
|
29
|
+
|
|
30
|
+
- source layer (`layer1`, `nexus`, `external`, `mixed`);
|
|
31
|
+
- retrieval timestamp;
|
|
32
|
+
- head block or index/version when available;
|
|
33
|
+
- cache hit/miss or freshness age;
|
|
34
|
+
- caveats for best-effort external data.
|
|
35
|
+
|
|
36
|
+
Until then, prefer correctness and transparent latency over silent stale responses.
|
|
37
|
+
|
|
38
|
+
## Deployment-level caching
|
|
39
|
+
|
|
40
|
+
Reverse proxies and CDNs should not cache `/mcp` responses by default. MCP requests are JSON-RPC POSTs
|
|
41
|
+
whose results depend on tool arguments and upstream state. If an operator experiments with caching, it
|
|
42
|
+
should be explicit, endpoint-specific, and validated against stale-data risk.
|
package/docs/clients.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# MCP client compatibility
|
|
2
|
+
|
|
3
|
+
How to connect AI clients to this server. **"Remote HTTP"** means native MCP Streamable HTTP or legacy
|
|
4
|
+
HTTP+SSE — those clients can use the hosted endpoint `https://mcp.blurt-blockchain.com/mcp` directly.
|
|
5
|
+
Clients that only speak **stdio** can still reach a remote server through the
|
|
6
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge, or run the local stdio build.
|
|
7
|
+
|
|
8
|
+
> The 10 most relevant clients are summarised in the [README](../README.md#compatible-clients). This
|
|
9
|
+
> page lists the additional clients, full config-file paths, and the sources for **both** tables.
|
|
10
|
+
> For copy-paste hosted/package snippets, see [install snippets](./install-snippets.md).
|
|
11
|
+
>
|
|
12
|
+
> This matrix is community-maintained and clients evolve fast — corrections welcome. **Information
|
|
13
|
+
> current as of 2026-06-28.**
|
|
14
|
+
|
|
15
|
+
## Highlighted clients (top 10) — full detail
|
|
16
|
+
|
|
17
|
+
| Client | Type | Local (stdio) | Remote (HTTP) | Config file | Open source | Notes |
|
|
18
|
+
| --- | --- | ---: | ---: | --- | ---: | --- |
|
|
19
|
+
| ChatGPT web — Apps / Connectors | Web | No | Yes | UI only; no config file documented | No | Remote MCP over HTTPS; local dev requires a tunnel; Developer Mode / workspace limits. [1] |
|
|
20
|
+
| Claude Desktop | Desktop app | Yes | Yes | `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`) | No | Local stdio via config; remote via custom connectors. [2] |
|
|
21
|
+
| Claude Code | CLI | Yes | Yes | `~/.claude.json`; project `.mcp.json` | No | Supports `stdio`, `http`, `sse`; project config can be shared. [3] |
|
|
22
|
+
| OpenAI Codex — CLI / IDE extension | CLI / IDE | Yes | Yes | `~/.codex/config.toml`; project `.codex/config.toml` | Yes | MCP in CLI and IDE extension; local stdio + Streamable HTTP. [4] |
|
|
23
|
+
| VS Code — GitHub Copilot agent mode | IDE | Yes | Yes | `.vscode/mcp.json`; user profile MCP config | Partial | Tries HTTP-stream first, falls back to SSE; Copilot/agent policies may restrict. [5] |
|
|
24
|
+
| Cursor | IDE | Yes | Yes | `.cursor/mcp.json`; `~/.cursor/mcp.json` | No | Supports `stdio`, SSE, Streamable HTTP; Team/Enterprise admin controls. [6] |
|
|
25
|
+
| Windsurf / Cascade / Devin Desktop | IDE | Yes | Yes | `mcp_config.json` (Codeium/Windsurf paths) | No | Supports stdio, Streamable HTTP, SSE; enterprise policy may restrict. [7] |
|
|
26
|
+
| Gemini CLI | CLI | Yes | Yes | `~/.gemini/settings.json`; project `.gemini/settings.json` | Yes | Supports stdio, SSE, Streamable HTTP; `gemini mcp add`. [8] |
|
|
27
|
+
| JetBrains AI Assistant | IDE | Yes | Yes | Settings → Tools → AI Assistant → MCP (file path Unknown) | No | MCP in JetBrains IDEs; large user base. [9] |
|
|
28
|
+
| Cline | IDE extension / CLI | Yes | Yes | CLI `~/.cline/mcp.json`; extension MCP settings JSON | Yes | VS Code ecosystem client. [10] |
|
|
29
|
+
|
|
30
|
+
## Other compatible clients
|
|
31
|
+
|
|
32
|
+
| Client | Type | Local (stdio) | Remote (HTTP) | Config file | Open source | Notes |
|
|
33
|
+
| --- | --- | ---: | ---: | --- | ---: | --- |
|
|
34
|
+
| GitHub Copilot CLI | CLI | Yes | Yes | `~/.copilot/mcp-config.json` | Unknown | Local and remote HTTP examples documented; Copilot subscription/policies apply. [11] |
|
|
35
|
+
| Visual Studio — GitHub Copilot agent mode | IDE | Yes | Yes | `.mcp.json`, `.vs/mcp.json`, `.vscode/mcp.json` (by scope) | Partial | Separate from VS Code; Windows/.NET audience. [12] |
|
|
36
|
+
| ChatGPT Desktop app | Desktop app | Unknown | Unknown | Unknown | No | "Work with Apps" docs are not MCP client config; document separately from ChatGPT web connectors. [13] |
|
|
37
|
+
| Mistral Vibe Work / former Le Chat | Web / mobile | No | Yes | UI only; no config file documented | No | "Le Chat is now Vibe"; Work custom MCP connectors require an admin and a server URL. [14] |
|
|
38
|
+
| Mistral Vibe Code CLI | CLI / IDE / Web | Yes | Yes | `config.toml`; project `./.vibe/config.toml`; user `~/.vibe/config.toml` | Yes | Supports `stdio`, `http`, `streamable-http`; no OAuth MCP in CLI yet. [15] |
|
|
39
|
+
| Hermes Agent — Nous Research | CLI / local web UI | Yes | Yes | `~/.hermes/config.yaml` | Yes | Agent runtime by Nous Research; local/self-hosted workflows. [16] |
|
|
40
|
+
| Google Antigravity | IDE | Yes | Yes | `~/.gemini/config/mcp_config.json` | Unknown | Agentic IDE; docs show local and remote MCP config. [17] |
|
|
41
|
+
| OpenClaw | CLI / Web gateway | Yes | Yes | `~/.openclaw/openclaw.json` | Yes | Personal AI assistant/gateway; MCP client registry and server. [18] |
|
|
42
|
+
| Zed Agent | IDE | Yes | Yes | Zed `settings.json` / `context_servers` (path varies) | Yes | MCP context servers; good dev audience. [19] |
|
|
43
|
+
| Roo Code | IDE extension | Yes | Yes | Global `mcp_settings.json`; project `.roo/mcp.json` | Yes | Supports STDIO, Streamable HTTP, SSE; Cline ecosystem. [20] |
|
|
44
|
+
| Continue | IDE extension / CLI | Yes | Yes | `.continue/mcpServers/*.yaml` or `.../mcp.json` | Yes | Agent-mode only; `stdio`, `sse`, `streamable-http`. [21] |
|
|
45
|
+
| LM Studio | Desktop app | Yes | Yes | `mcp.json` | No | Strong local-LLM audience; local and remote MCP since 0.3.17. [22] |
|
|
46
|
+
| Open WebUI | Web | No | Yes | Admin Settings → External Tools | Yes | Native MCP is Streamable HTTP only; admin-only; stdio needs a proxy. [23] |
|
|
47
|
+
| LibreChat | Web | Yes | Yes | `librechat.yaml` | Yes | `stdio`, `websocket`, `streamable-http`, `sse`; restart after config change. [24] |
|
|
48
|
+
| OpenCode | CLI / TUI | Yes | Yes | `opencode.json` / `opencode.jsonc` | Yes | Local and remote MCP under `mcp`; OAuth for remote servers. [25] |
|
|
49
|
+
|
|
50
|
+
## Sources
|
|
51
|
+
|
|
52
|
+
Covering both the README top-10 table and the tables above.
|
|
53
|
+
|
|
54
|
+
1. https://developers.openai.com/api/docs/guides/developer-mode — ChatGPT Developer mode
|
|
55
|
+
2. https://modelcontextprotocol.io/docs/develop/connect-local-servers — Connect to local MCP servers
|
|
56
|
+
3. https://code.claude.com/docs/en/mcp — Connect Claude Code to tools via MCP
|
|
57
|
+
4. https://developers.openai.com/codex/mcp — Model Context Protocol – Codex
|
|
58
|
+
5. https://code.visualstudio.com/docs/copilot/customization/mcp-servers — Add and manage MCP servers in VS Code
|
|
59
|
+
6. https://cursor.com/docs/mcp.md — Cursor MCP docs
|
|
60
|
+
7. https://docs.windsurf.com/windsurf/cascade/mcp — Cascade MCP Integration
|
|
61
|
+
8. https://geminicli.com/docs/tools/mcp-server/ — MCP servers with Gemini CLI
|
|
62
|
+
9. https://www.jetbrains.com/help/ai-assistant/mcp.html — Model Context Protocol (MCP) | AI Assistant
|
|
63
|
+
10. https://docs.cline.bot/mcp/mcp-overview — Cline MCP
|
|
64
|
+
11. https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers — Adding MCP servers for GitHub Copilot CLI
|
|
65
|
+
12. https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers — Use MCP Servers to Extend GitHub Copilot (Visual Studio)
|
|
66
|
+
13. https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt — Developer mode and MCP apps in ChatGPT
|
|
67
|
+
14. https://docs.mistral.ai/vibe/overview — Vibe | Mistral Docs
|
|
68
|
+
15. https://docs.mistral.ai/vibe/code/cli/mcp-servers — MCP servers | Mistral Docs
|
|
69
|
+
16. https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp — MCP | Hermes Agent
|
|
70
|
+
17. https://codelabs.developers.google.com/getting-started-google-antigravity — Getting Started with Google Antigravity
|
|
71
|
+
18. https://docs.openclaw.ai/cli/mcp — MCP - OpenClaw
|
|
72
|
+
19. https://zed.dev/docs/assistant/model-context-protocol — Model Context Protocol (MCP) in Zed
|
|
73
|
+
20. https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo — Using MCP in Roo Code
|
|
74
|
+
21. https://docs.continue.dev/customize/deep-dives/mcp — Set Up MCP in Continue
|
|
75
|
+
22. https://lmstudio.ai/docs/app/mcp — Use MCP Servers (LM Studio)
|
|
76
|
+
23. https://docs.openwebui.com/features/extensibility/mcp/ — MCP | Open WebUI
|
|
77
|
+
24. https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/mcp_servers — MCP Servers (LibreChat)
|
|
78
|
+
25. https://opencode.ai/docs/mcp-servers/ — MCP servers | OpenCode
|