@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
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # blurt-mcp-server
2
+
3
+ [![pipeline status](https://gitlab.com/blurt-blockchain/blurt-mcp-server/badges/main/pipeline.svg)](https://gitlab.com/blurt-blockchain/blurt-mcp-server/-/pipelines)
4
+ [![license: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE)
5
+ [![node >= 18](https://img.shields.io/badge/node-%E2%89%A518-brightgreen.svg)](docs/usage.md#requirements)
6
+
7
+ **Talk to the [Blurt blockchain](https://blurt.blog) from any AI.**
8
+
9
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets Claude, ChatGPT,
10
+ Grok, Mistral, Cursor and other MCP-capable assistants read Blurt in plain language — accounts, posts,
11
+ communities, curation, witnesses and the BLURT market price. Built on
12
+ [`@beblurt/dblurt`](https://www.npmjs.com/package/@beblurt/dblurt). **Read-only by default** — no
13
+ private keys, no broadcasting.
14
+
15
+ > 🟢 **Try it in 30 seconds — nothing to install.** A public hosted instance is live at
16
+ > **`https://mcp.blurt-blockchain.com/mcp`**. Add it as a connector in your AI app
17
+ > (see [Quick start](#quick-start)). Clone the repo only to self-host or contribute.
18
+
19
+ ## What you can ask
20
+
21
+ You never name a tool — just ask, and the AI picks and chains them:
22
+
23
+ - *"What is Blurt's market cap and how many BLURT are in circulation?"*
24
+ - *"Give me a deep analysis of the account `nalexadre`: profile, vote value in USD, who follows them."*
25
+ - *"Find a community about cats and show me its details."*
26
+ - *"List the top 10 Blurt witnesses and flag any that look inactive or behind on version."*
27
+ - *"Who are the biggest curators on this post, and how much is each of their upvotes worth?"*
28
+
29
+ → More examples and the full **25-tool** reference: **[docs/tools.md](docs/tools.md)**.
30
+
31
+ ## What it can do
32
+
33
+ | Area | Tools |
34
+ | --- | --- |
35
+ | 👤 **Accounts & wallets** | profiles, balances, Blurt Power, operation history, rewards, pending rewards, relationships, delegations, `compare-accounts`, account lookup |
36
+ | ✍️ **Content** | a post or its full discussion, an account's posts, trending/ranked publications |
37
+ | 🌐 **Communities** | discovery, details, an account's subscriptions |
38
+ | 🌱 **Onboarding** | an account's referrals (accounts it brought to Blurt) and their count |
39
+ | 🏅 **Curation** | a post's voters & rebloggers, an upvote's value in BLURT & USD |
40
+ | 🔔 **Notifications** | an account's notifications feed + unread count |
41
+ | 🏛️ **Governance** | witness ranking + health, an account's witness votes |
42
+ | 💰 **Market & network** | BLURT price, market cap, chain status |
43
+ | 🔎 **search / fetch** | connector-style discovery of any Blurt resource |
44
+ | ✍️ **Write** *(opt-in, local by default)* | claim, vote, comment, post, follow, mute, reblog, subscribe, read notifications — signed locally; HTTP signing requires an explicit unsafe trusted-deployment override ([details](docs/write-operations.md)) |
45
+
46
+ ## Quick start
47
+
48
+ **1 — Hosted, native connector (Claude, ChatGPT, Grok, Mistral, …):** add a custom/remote connector
49
+ and paste `https://mcp.blurt-blockchain.com/mcp`.
50
+
51
+ **2 — Desktop app via a JSON config (e.g. Claude Desktop):** bridge with `mcp-remote`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "blurt": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.blurt-blockchain.com/mcp"] }
57
+ }
58
+ }
59
+ ```
60
+
61
+ **3 — Fully local (stdio):** run the official package bin:
62
+
63
+ ```bash
64
+ npx -y -p @blurt-blockchain/blurt-mcp-server blurt-mcp-stdio
65
+ ```
66
+
67
+ → Full install, configuration, package and per-client guide: **[docs/usage.md](docs/usage.md)** and
68
+ **[docs/install-snippets.md](docs/install-snippets.md)**.
69
+
70
+ ## Compatible clients
71
+
72
+ The 10 most relevant MCP clients. **Remote (HTTP)** clients use the hosted endpoint directly;
73
+ stdio-only clients can bridge with `mcp-remote` or run the local stdio build.
74
+
75
+ | Client | Type | Local (stdio) | Remote (HTTP) | Config file | Open source |
76
+ | --- | --- | ---: | ---: | --- | ---: |
77
+ | ChatGPT web — Apps / Connectors | Web | No | Yes | UI only | No |
78
+ | Claude Desktop | Desktop | Yes | Yes | `claude_desktop_config.json` | No |
79
+ | Claude Code | CLI | Yes | Yes | `~/.claude.json`, `.mcp.json` | No |
80
+ | OpenAI Codex (CLI / IDE) | CLI / IDE | Yes | Yes | `~/.codex/config.toml` | Yes |
81
+ | VS Code — Copilot agent | IDE | Yes | Yes | `.vscode/mcp.json` | Partial |
82
+ | Cursor | IDE | Yes | Yes | `.cursor/mcp.json` | No |
83
+ | Windsurf / Cascade | IDE | Yes | Yes | `mcp_config.json` | No |
84
+ | Gemini CLI | CLI | Yes | Yes | `~/.gemini/settings.json` | Yes |
85
+ | JetBrains AI Assistant | IDE | Yes | Yes | Settings UI | No |
86
+ | Cline | IDE ext. / CLI | Yes | Yes | `~/.cline/mcp.json` | Yes |
87
+
88
+ → Full config paths, more clients (Copilot CLI, Mistral Vibe, Hermes, Antigravity, Zed, LM Studio,
89
+ LibreChat, …) and sources: **[docs/clients.md](docs/clients.md)**. *(Community-maintained; as of 2026-06-28.)*
90
+
91
+ ## Documentation
92
+
93
+ - [Tools & resources](docs/tools.md) — the full tool reference and example prompts
94
+ - [Installation & usage](docs/usage.md) — requirements, configuration, running, connecting clients
95
+ - [Install snippets](docs/install-snippets.md) — copy-paste hosted, package, stdio and client configs
96
+ - [Write operations](docs/write-operations.md) — opt-in local signing (claim, vote, comment, post, follow, …)
97
+ - [Compatible clients](docs/clients.md) — the full compatibility matrix
98
+ - [Architecture](docs/architecture.md) — how it works and the project layout
99
+ - [Deployment](docs/deployment.md) — HTTP deployment and reverse-proxy guidance
100
+ - [Operations](docs/operations.md) — public endpoint operations notes
101
+ - [Cache policy](docs/cache-policy.md) · [Load checks](docs/load-testing.md)
102
+ - [ADR 0001](docs/adr/0001-neutral-infrastructure.md) — neutrality principle for the official server
103
+ - [Development](docs/development.md) — testing and releasing
104
+ - [Release/provenance notes](docs/release-provenance.md) — package validation and publish constraints
105
+ - [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md)
106
+
107
+ ## Security
108
+
109
+ The server is **read-only** by default and holds **no private keys**. Optional write tools are
110
+ **opt-in and local (stdio) by default**, use a **posting key only** (which cannot move funds), and are
111
+ not exposed over HTTP unless an operator deliberately enables the unsafe trusted-deployment override.
112
+ New write deployments should start with a semantic capability profile such as `BLURT_WRITE_PROFILE=curator`.
113
+ See **[SECURITY.md](SECURITY.md)** and **[write operations](docs/write-operations.md)**.
114
+
115
+ ## License
116
+
117
+ [GPL-3.0-or-later](LICENSE) © nalexadre — Blurt blockchain
package/SECURITY.md ADDED
@@ -0,0 +1,107 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report security issues privately via a
6
+ [GitLab issue](https://gitlab.com/blurt-blockchain/blurt-mcp-server/-/issues) marked confidential,
7
+ or by contacting the maintainer (`@nalexadre` on Blurt). Please do not disclose publicly until a fix
8
+ is available.
9
+
10
+ ## Security model
11
+
12
+ This connector is **read-only by default**. The public HTTP server reads public Blurt blockchain data and serves it over MCP. It holds **no private keys**, performs **no signing**, and can **not** post, vote, transfer, or otherwise modify on-chain state.
13
+
14
+ Optional write tools exist for the local stdio server when the user explicitly configures a posting key. HTTP stays read-only by default and refuses to start if a posting key is present. An intentionally dangerous override exists for experienced operators of private, authenticated deployments, but it is disabled by default and must never be used for the public hosted endpoint. As long as the HTTP deployment remains read-only, its attack surface is limited to availability/abuse and to the correctness of the data returned.
15
+
16
+ ## Deployment hardening (HTTP)
17
+
18
+ The HTTP endpoint (`POST /mcp`) has **no built-in authentication** and runs in stateless mode.
19
+
20
+ - **Run it behind a reverse proxy** (nginx, Caddy, Traefik, …). The proxy should terminate **TLS**
21
+ and handle **rate limiting** and **`Host`/origin filtering** (this also covers DNS-rebinding
22
+ concerns). The application assumes a trusted proxy in front of it. See [deployment guidance](docs/deployment.md).
23
+ - Keep the request body limit small (MCP requests are tiny; large payloads are responses, not
24
+ requests).
25
+ - The public hosted endpoint intentionally remains anonymous and **read-only**. OAuth is deferred until user-specific capabilities, quotas, preferences or private resources become real requirements.
26
+
27
+ ## Private keys & write operations
28
+
29
+ The connector exposes a set of opt-in write tools — claim rewards, upvote, comment, post, follow, mute,
30
+ subscribe to a community, reblog, and mark notifications read — each signed locally. Because this
31
+ introduces a private key into the system, the following principles are **mandatory** and define how
32
+ every write tool is built.
33
+
34
+ ### 1. Least privilege — posting key only
35
+
36
+ Only a Blurt **posting key** is ever used. The posting key authorizes social operations
37
+ (`vote`, `comment`, `claim_reward_balance`, and posting `custom_json` such as follow / community /
38
+ reblog / notify) but **cannot move funds** (transfers require the `active` key). Active and owner keys are **never** accepted; on startup the server verifies the key has
39
+ **posting authority** over the account — either its own posting key, or a **delegated** posting
40
+ authority (an account granted access via `account_auths`) — and refuses otherwise. Delegation is the
41
+ recommended setup: it can be revoked in one operation without rotating your key (see
42
+ [docs/write-operations.md](docs/write-operations.md)).
43
+
44
+ ### 2. Write is stdio-only by default; HTTP signing requires an unsafe override
45
+
46
+ Write/signing tools are **only** registered on the local **stdio** server by default, and **only** when a valid key is present. The normal HTTP instance is public and unauthenticated; if a key is detected while running over HTTP, the server **refuses to start**.
47
+
48
+ Experienced operators may override this guard only for private, trusted, access-controlled deployments by setting the intentionally alarming environment variable below to the exact value shown:
49
+
50
+ ```bash
51
+ BLURT_UNSAFE_ALLOW_HTTP_SIGNING_WITH_POSTING_KEY=I_ACCEPT_FULL_RESPONSIBILITY_FOR_EXPOSING_BLURT_WRITE_TOOLS_OVER_HTTP
52
+ ```
53
+
54
+ This opt-in is disabled by default. Any other value is ignored and startup still fails. When enabled, startup prints a large warning to `stderr`. The operator is solely responsible for authentication, network isolation, reverse-proxy configuration, client behavior, monitoring and every on-chain operation signed by the process. **Do not use this mode for the public hosted endpoint or any unauthenticated LAN/WAN service.** This override does not relax posting-key-only validation, owner/active-key refusal, write-tool gating, or rate caps.
55
+
56
+ ### 3. Human-in-the-loop at the signing layer
57
+
58
+ The human approval that gates a write happens **in the AI client app** (it prompts before sending a
59
+ tool call) — but this is a **client feature, not guaranteed by MCP**: some agent runners execute
60
+ tools autonomously. Therefore the connector does **not** rely on the client alone:
61
+
62
+ - **Server-side defense-in-depth**: semantic capability profiles (`BLURT_WRITE_PROFILE`, recommended
63
+ for new deployments), the backward-compatible denylist (`BLURT_WRITE_TOOLS_BANNED`, which removes
64
+ specific write tools so they are never registered), the neutral preview-first default
65
+ (`BLURT_DRY_RUN_DEFAULT=true`, which changes omitted `dry_run` parameters without restricting what
66
+ users may do), and **rate caps** (per-tool, per-window limits) that do not depend on the client.
67
+ The official server intentionally does not enforce content/account/community policy files; see
68
+ [ADR 0001](docs/adr/0001-neutral-infrastructure.md).
69
+ - **Client-agnostic human gate (target)**: delegate signing to an external keystore/wallet that
70
+ prompts the human for each signature — **WhaleVault** (browser flows) or, ideally, Blurt's
71
+ roadmapped **SSM Wallet** (incl. headless server mode). The connector sends an **unsigned**
72
+ operation and receives a **signed** transaction, **never touching the key**.
73
+
74
+ ### 4. Compartmentalized, validated tools
75
+
76
+ Each operation is its own tool (`blurt-vote`, `blurt-comment`, `blurt-post`, `blurt-claim-rewards`),
77
+ not a single multi-purpose `broadcast` tool — so each has a tight, validated schema and a clear name
78
+ in the client's approval dialog. Parameters are **never auto-derived from fetched on-chain content**:
79
+ all content read from the chain is treated as **data, never instructions** (indirect prompt-injection
80
+ defense).
81
+
82
+ ### 5. Key handling
83
+
84
+ - The key never appears in the repository. Prefer keeping it in a file **outside** the repo and
85
+ pointing the stdio launcher at it with `BLURT_ENV_FILE` (so the launcher config holds no secret and
86
+ the key is not in the working tree). The launcher's own `env` block, or a secrets mechanism, are
87
+ also acceptable; the project `.env` is not (the HTTP server refuses to start when a key is present).
88
+ - On a desktop, an **OS keychain** may be used. On a **headless server**, prefer **systemd
89
+ credentials** (`LoadCredential` / `systemd-creds`, encrypted at rest) or a **secrets manager**
90
+ (Vault, cloud KMS); a `chmod 600` env file owned by the service user is the minimum baseline.
91
+ - The key flows **only** into dblurt's local signing function — **never** to the network (only the
92
+ signed transaction is broadcast), **never** to logs, error messages, or any other dependency.
93
+
94
+ ### 6. Logging
95
+
96
+ Logs go to `stderr`. The signing path must **never** log the key or a raw signed transaction. Run
97
+ production with `LOG_LEVEL=info` (not `debug`). A test asserts the signing tool does not emit the key.
98
+
99
+ ## Supply chain
100
+
101
+ The signing path runs in the same process as its dependencies (dblurt, the SDK, the node checker), so
102
+ dependency trust matters once a key is present:
103
+
104
+ - A committed lockfile pins the dependency tree; `npm audit` runs in CI.
105
+ - Updates to libraries on the signing path (especially `@beblurt/dblurt`) are reviewed before
106
+ merging.
107
+ - The write path keeps its dependencies minimal.
package/dist/app.js ADDED
@@ -0,0 +1,88 @@
1
+ // Logger — log level is read from process.env.LOG_LEVEL inside the logger itself.
2
+ import logger from "./utils/logger.js";
3
+ import express from "express";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { buildServer } from "./buildServer.js";
6
+ import pkg from "../package.json" with { type: "json" };
7
+ import { getBlurtClient, rpcReadiness } from "./utils/rpc.js";
8
+ /**
9
+ * Build the Express app exposing the MCP server over a stateless Streamable HTTP
10
+ * transport. Exported (without side effects like `listen`) so tests can run the
11
+ * exact production request path against an ephemeral port.
12
+ */
13
+ export function createApp(options = {}) {
14
+ const app = express();
15
+ // MCP requests are small JSON-RPC payloads; cap the body to avoid memory abuse.
16
+ // (Large data is in responses, which this limit does not affect.)
17
+ app.use(express.json({ limit: "256kb" }));
18
+ const requestLogger = options.requestLogger ?? ((message) => logger.info(message));
19
+ app.use((req, res, next) => {
20
+ const started = process.hrtime.bigint();
21
+ res.on("finish", () => {
22
+ const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
23
+ requestLogger(`http_request method=${req.method} path=${req.path} status=${res.statusCode} duration_ms=${durationMs}`);
24
+ });
25
+ next();
26
+ });
27
+ app.get("/healthz", (_req, res) => {
28
+ res.status(200).json({
29
+ status: "ok",
30
+ service: "blurt-mcp-server",
31
+ version: pkg.version || "0.0.0",
32
+ time: new Date().toISOString(),
33
+ });
34
+ });
35
+ app.get("/readyz", (_req, res) => {
36
+ const rpc = rpcReadiness();
37
+ const ready = rpc.configured_nodes > 0 && rpc.active_nodes > 0;
38
+ res.status(ready ? 200 : 503).json({
39
+ status: ready ? "ready" : "not_ready",
40
+ service: "blurt-mcp-server",
41
+ version: pkg.version || "0.0.0",
42
+ time: new Date().toISOString(),
43
+ rpc,
44
+ });
45
+ });
46
+ app.post("/mcp", async (req, res) => {
47
+ try {
48
+ // Shared client, repointed at the healthiest RPC nodes by the node checker.
49
+ const server = buildServer(getBlurtClient(), { write: options.write });
50
+ const transport = new StreamableHTTPServerTransport({
51
+ sessionIdGenerator: undefined, // or custom for logs
52
+ });
53
+ res.on("close", () => {
54
+ logger.debug("Closing MCP transport/session");
55
+ transport.close();
56
+ server.close();
57
+ });
58
+ await server.connect(transport);
59
+ await transport.handleRequest(req, res, req.body);
60
+ }
61
+ catch (err) {
62
+ logger.error("MCP error: " + err.stack);
63
+ if (!res.headersSent) {
64
+ res.status(500).json({
65
+ jsonrpc: "2.0",
66
+ error: { code: -32603, message: "Internal server error" },
67
+ id: null,
68
+ });
69
+ }
70
+ }
71
+ });
72
+ // GET and DELETE → not supported in stateless mode
73
+ app.get("/mcp", (_req, res) => {
74
+ res.status(405).json({
75
+ jsonrpc: "2.0",
76
+ error: { code: -32000, message: "Method not allowed." },
77
+ id: null,
78
+ });
79
+ });
80
+ app.delete("/mcp", (_req, res) => {
81
+ res.status(405).json({
82
+ jsonrpc: "2.0",
83
+ error: { code: -32000, message: "Method not allowed." },
84
+ id: null,
85
+ });
86
+ });
87
+ return app;
88
+ }
@@ -0,0 +1,146 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import logger from "./utils/logger.js";
3
+ import pkg from "../package.json" with { type: "json" };
4
+ // Tools
5
+ import { registerGetAccount } from "./tools/getAccount.js";
6
+ import { registerGetAccountHistory } from "./tools/getAccountHistory.js";
7
+ import { registerGetAccountPosts } from "./tools/getAccountPosts.js";
8
+ import { registerGetPost } from "./tools/getPost.js";
9
+ import { registerGetPublications } from "./tools/getPublications.js";
10
+ import { registerSearchTool } from "./tools/search.js";
11
+ import { registerFetchTool } from "./tools/fetch.js";
12
+ // Network / investor
13
+ import { registerGetBlurtPrice } from "./tools/getBlurtPrice.js";
14
+ import { registerGetChainStatus } from "./tools/getChainStatus.js";
15
+ // Social
16
+ import { registerListCommunities } from "./tools/listCommunities.js";
17
+ import { registerGetCommunity } from "./tools/getCommunity.js";
18
+ // Curation
19
+ import { registerGetPostVotes } from "./tools/getPostVotes.js";
20
+ import { registerGetVoteValue } from "./tools/getVoteValue.js";
21
+ // Governance
22
+ import { registerListWitnesses } from "./tools/listWitnesses.js";
23
+ import { registerGetWitness } from "./tools/getWitness.js";
24
+ import { registerGetAccountWitnessVotes } from "./tools/getAccountWitnessVotes.js";
25
+ // Account depth / social
26
+ import { registerGetAccountRelationships } from "./tools/getAccountRelationships.js";
27
+ import { registerGetPendingRewards } from "./tools/getPendingRewards.js";
28
+ import { registerGetAccountSubscriptions } from "./tools/getAccountSubscriptions.js";
29
+ import { registerCompareAccounts } from "./tools/compareAccounts.js";
30
+ import { registerGetReferrals } from "./tools/getReferrals.js";
31
+ import { registerGetAccountNotifications } from "./tools/getAccountNotifications.js";
32
+ import { registerGetPostReblogs } from "./tools/getPostReblogs.js";
33
+ import { registerGetDelegations } from "./tools/getDelegations.js";
34
+ import { registerLookupAccounts } from "./tools/lookupAccounts.js";
35
+ // Write (signing) — registered only when a write context is provided
36
+ import { registerClaimRewards } from "./tools/claimRewards.js";
37
+ import { registerUpvote } from "./tools/upvote.js";
38
+ import { registerComment } from "./tools/comment.js";
39
+ import { registerFollow } from "./tools/follow.js";
40
+ import { registerMute } from "./tools/mute.js";
41
+ import { registerSubscribeCommunity } from "./tools/subscribeCommunity.js";
42
+ import { registerReadNotifications } from "./tools/readNotifications.js";
43
+ import { registerReblog } from "./tools/reblog.js";
44
+ import { registerPost } from "./tools/post.js";
45
+ // Resources
46
+ import { registerBlurtResources } from "./resources/blurtResource.js";
47
+ /**
48
+ * Build a fully-wired MCP server (read-only tools + resources, plus write tools
49
+ * when `options.write` is present) bound to the given Blurt client.
50
+ */
51
+ export function buildServer(client, options = {}) {
52
+ const server = new McpServer({
53
+ name: pkg.name || "blurt-mcp-server",
54
+ version: pkg.version || "0.1.0",
55
+ });
56
+ logger.info("Registering tools and resources...");
57
+ // Tools
58
+ registerGetAccount(server, client);
59
+ registerGetAccountHistory(server, client);
60
+ registerGetAccountPosts(server, client);
61
+ registerGetPost(server, client);
62
+ registerGetPublications(server, client);
63
+ // Network / investor
64
+ registerGetBlurtPrice(server);
65
+ registerGetChainStatus(server, client);
66
+ // Social (communities)
67
+ registerListCommunities(server, client);
68
+ registerGetCommunity(server, client);
69
+ // Curation
70
+ registerGetPostVotes(server, client);
71
+ registerGetVoteValue(server, client);
72
+ // Governance
73
+ registerListWitnesses(server, client);
74
+ registerGetWitness(server, client);
75
+ registerGetAccountWitnessVotes(server, client);
76
+ // Account depth / social
77
+ registerGetAccountRelationships(server, client);
78
+ registerGetPendingRewards(server, client);
79
+ registerGetAccountSubscriptions(server, client);
80
+ registerCompareAccounts(server, client);
81
+ registerGetReferrals(server, client);
82
+ registerGetAccountNotifications(server, client);
83
+ registerGetPostReblogs(server, client);
84
+ registerGetDelegations(server, client);
85
+ registerLookupAccounts(server, client);
86
+ // Search/fetch + resources
87
+ registerSearchTool(server);
88
+ registerFetchTool(server, client);
89
+ registerBlurtResources(server, client);
90
+ // Write tools — only when a validated posting key is present (stdio only).
91
+ if (options.write) {
92
+ const { enabledTools } = options.write;
93
+ if (enabledTools.has("claim-rewards"))
94
+ registerClaimRewards(server, client, options.write);
95
+ if (enabledTools.has("upvote"))
96
+ registerUpvote(server, client, options.write);
97
+ if (enabledTools.has("comment"))
98
+ registerComment(server, client, options.write);
99
+ if (enabledTools.has("follow"))
100
+ registerFollow(server, client, options.write);
101
+ if (enabledTools.has("mute"))
102
+ registerMute(server, client, options.write);
103
+ if (enabledTools.has("subscribe-community"))
104
+ registerSubscribeCommunity(server, client, options.write);
105
+ if (enabledTools.has("read-notifications"))
106
+ registerReadNotifications(server, client, options.write);
107
+ if (enabledTools.has("reblog"))
108
+ registerReblog(server, client, options.write);
109
+ if (enabledTools.has("post"))
110
+ registerPost(server, client, options.write);
111
+ logger.info(`Write tools enabled: ${[...enabledTools].join(", ") || "(none)"}`);
112
+ }
113
+ return server;
114
+ }
115
+ /**
116
+ * Options for the Blurt RPC client.
117
+ *
118
+ * `rpcTransport: "core"` is required: the default "legacy" transport aborts each
119
+ * attempt after (tries+1)*500ms and then crashes on the resulting abort error
120
+ * (`error.code.includes is not a function`) when a request is slow, which makes
121
+ * every RPC call fail under the Streamable HTTP server context.
122
+ */
123
+ export const BLURT_CLIENT_OPTIONS = {
124
+ timeout: 4000,
125
+ failoverThreshold: 3,
126
+ rpcTransport: "core",
127
+ };
128
+ /**
129
+ * Optional non-mainnet (testnet) override. When `BLURT_CHAIN_ID` and
130
+ * `BLURT_ADDRESS_PREFIX` are set, they are passed to the dblurt client to target
131
+ * that chain. When unset, this returns `{}` and dblurt's mainnet defaults apply
132
+ * (chain id `cd8d90…`, address prefix `BLT`) — nothing to configure for mainnet.
133
+ *
134
+ * A testnet needs **both** values; if only one is set the other falls back to the
135
+ * mainnet default (see the warning logged in utils/rpc.ts).
136
+ */
137
+ export function networkOptions() {
138
+ const chainId = process.env.BLURT_CHAIN_ID?.trim();
139
+ const addressPrefix = process.env.BLURT_ADDRESS_PREFIX?.trim();
140
+ return {
141
+ ...(chainId ? { chainId } : {}),
142
+ ...(addressPrefix ? { addressPrefix } : {}),
143
+ };
144
+ }
145
+ // The shared client and its RPC node selection live in ./utils/rpc.ts
146
+ // (getBlurtClient / startNodeChecker), which scores nodes via blurt-nodes-checker.
@@ -0,0 +1,53 @@
1
+ import { TOOL_REGISTRY, TOOL_RESULT_CONTRACT, TOOL_RESULT_OUTPUT_SCHEMA, toolSources, ttlSeconds, } from "./toolRegistry.js";
2
+ export function buildToolResultMeta(name) {
3
+ const metadata = TOOL_REGISTRY[name];
4
+ return {
5
+ contract: TOOL_RESULT_CONTRACT,
6
+ tool: name,
7
+ sources: metadata ? toolSources(metadata) : ["mcp"],
8
+ retrieved_at: new Date().toISOString(),
9
+ freshness: {
10
+ cache: metadata?.cache ?? "none",
11
+ ttl_seconds: metadata ? ttlSeconds(metadata.cache) : null,
12
+ },
13
+ confidence: "high",
14
+ caveats: metadata && "caveats" in metadata && metadata.caveats ? [...metadata.caveats] : [],
15
+ };
16
+ }
17
+ function parseTextContent(text) {
18
+ try {
19
+ return JSON.parse(text);
20
+ }
21
+ catch {
22
+ return text;
23
+ }
24
+ }
25
+ function inferData(result) {
26
+ const content = result.content ?? [];
27
+ if (content.length === 1 && content[0]?.type === "text") {
28
+ return parseTextContent(content[0].text ?? "");
29
+ }
30
+ return content;
31
+ }
32
+ export function withStandardStructuredContent(name, result) {
33
+ if (result.isError || result.structuredContent)
34
+ return result;
35
+ const structured = {
36
+ data: inferData(result),
37
+ meta: buildToolResultMeta(name),
38
+ };
39
+ return { ...result, structuredContent: structured };
40
+ }
41
+ export function registerBlurtTool(server, name, config, callback) {
42
+ const metadata = TOOL_REGISTRY[name];
43
+ const mergedConfig = {
44
+ ...config,
45
+ title: config.title ?? metadata.title,
46
+ description: config.description ?? metadata.description,
47
+ outputSchema: config.outputSchema ?? TOOL_RESULT_OUTPUT_SCHEMA,
48
+ };
49
+ return server.registerTool(name, mergedConfig, async (args, extra) => {
50
+ const result = await callback(args, extra);
51
+ return withStandardStructuredContent(name, result);
52
+ });
53
+ }