@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
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# blurt-mcp-server
|
|
2
|
+
|
|
3
|
+
[](https://gitlab.com/blurt-blockchain/blurt-mcp-server/-/pipelines)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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
|
+
}
|