@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,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.
@@ -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