@three-ws/mcp-server 1.1.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/src/index.js ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // @three-ws/mcp-server entry point.
3
+ //
4
+ // Boots an MCP server over stdio that exposes paid tools for pose generation,
5
+ // pump.fun snapshots, ERC-8004 reputation lookups, and Solana vanity mining.
6
+ // Tool calls without payment return the v2 MCP-transport `PaymentRequired`
7
+ // envelope (per @x402/mcp + transports-v2/mcp.md). Successful settlements are
8
+ // reported back to the client under `_meta["x402/payment-response"]`.
9
+ //
10
+ // Run standalone:
11
+ // node mcp-server/src/index.js
12
+ //
13
+ // Or wire into Claude Desktop / Cursor as documented in README.md.
14
+ //
15
+ // Testability: `buildServer()` constructs and returns the fully-registered
16
+ // McpServer WITHOUT connecting the stdio transport and WITHOUT requiring any
17
+ // runtime payment env — tool registration (names/descriptions/schemas) is
18
+ // secret-free. Only an actual paid tool *invocation* requires
19
+ // MCP_SVM_PAYMENT_ADDRESS (enforced lazily inside `paid()`). The stdio boot in
20
+ // `main()` runs only when this file is the process entry point.
21
+
22
+ import { createRequire } from 'node:module';
23
+ import { pathToFileURL } from 'node:url';
24
+ import { realpathSync } from 'node:fs';
25
+
26
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
27
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
+
29
+ import { assertPaymentEnv, getLastFacilitatorInitError, getResourceServer } from './payments.js';
30
+ import { buildPoseSeedTool } from './tools/pose-seed.js';
31
+ import { buildPumpSnapshotTool } from './tools/pump-snapshot.js';
32
+ import { buildAgentReputationTool } from './tools/agent-reputation.js';
33
+ import { buildVanityGrinderTool } from './tools/vanity-grinder.js';
34
+ import { buildTextToAvatarTool } from './tools/text-to-avatar.js';
35
+ import { buildMeshForgeTool } from './tools/mesh-forge.js';
36
+ import { buildRigMeshTool } from './tools/rig-mesh.js';
37
+ import { buildSentimentPulseTool } from './tools/sentiment-pulse.js';
38
+ import { buildEnsSnsResolveTool } from './tools/ens-sns-resolve.js';
39
+ import { buildAgentDelegateActionTool } from './tools/agent-delegate-action.js';
40
+ import { buildAgenCListTasksTool } from './tools/agenc-list-tasks.js';
41
+ import { buildAgenCGetTaskTool } from './tools/agenc-get-task.js';
42
+ import { buildAgenCGetAgentTool } from './tools/agenc-get-agent.js';
43
+ import { buildAixbtIntelTool } from './tools/aixbt-intel.js';
44
+ import { buildAixbtProjectsTool } from './tools/aixbt-projects.js';
45
+
46
+ const SERVER_INSTRUCTIONS =
47
+ 'Paid x402 MCP tools from three.ws. Each tool quotes its USDC price in its description. ' +
48
+ 'Tool calls without an x402 payment payload in _meta return a PaymentRequired structuredContent ' +
49
+ '(v2 MCP transport spec). Tools cover: 3D avatar generation (text_to_avatar), ' +
50
+ 'text/image-to-3D mesh generation via a Granite-directed model chain (mesh_forge), ' +
51
+ 'auto-rigging a GLB into an animation-ready model (rig_mesh), ' +
52
+ 'ENS + SNS name resolution ' +
53
+ '(ens_sns_resolve), agent-to-agent delegation (agent_delegate_action), token sentiment pulse ' +
54
+ '(sentiment_pulse), pose generation (get_pose_seed), Solana token snapshots (pump_snapshot), ' +
55
+ 'ERC-8004 agent reputation (agent_reputation), Solana vanity address mining ' +
56
+ '(vanity_grinder), and AgenC coordination protocol reads — ' +
57
+ 'task discovery, task status + lifecycle, and agent registry lookup ' +
58
+ '(agenc_list_tasks, agenc_get_task, agenc_get_agent), and aixbt market ' +
59
+ 'intelligence — narrative intel feed and momentum-ranked project scans ' +
60
+ '(aixbt_intel, aixbt_projects).';
61
+
62
+ // The advertised MCP server version comes straight from package.json so it
63
+ // can never drift from the published npm version.
64
+ const { version: PKG_VERSION } = createRequire(import.meta.url)('../package.json');
65
+
66
+ // Every tool builder. Each returns a descriptor
67
+ // { name, title, description, inputSchema (Zod shape), annotations, handler }.
68
+ // None of these require payment env — the env requirement is deferred to the
69
+ // first paid call.
70
+ const TOOL_BUILDERS = [
71
+ buildTextToAvatarTool,
72
+ buildMeshForgeTool,
73
+ buildRigMeshTool,
74
+ buildEnsSnsResolveTool,
75
+ buildAgentDelegateActionTool,
76
+ buildSentimentPulseTool,
77
+ buildPoseSeedTool,
78
+ buildPumpSnapshotTool,
79
+ buildAgentReputationTool,
80
+ buildVanityGrinderTool,
81
+ buildAgenCListTasksTool,
82
+ buildAgenCGetTaskTool,
83
+ buildAgenCGetAgentTool,
84
+ buildAixbtIntelTool,
85
+ buildAixbtProjectsTool,
86
+ ];
87
+
88
+ /**
89
+ * Build every tool descriptor. Side-effect-free w.r.t. payment env and the
90
+ * stdio transport — safe to call from tests to enumerate the tool surface.
91
+ *
92
+ * @returns {Promise<Array<{name:string,title:string,description:string,inputSchema:object,annotations:object,handler:Function}>>}
93
+ */
94
+ export async function buildTools() {
95
+ return Promise.all(TOOL_BUILDERS.map((build) => build()));
96
+ }
97
+
98
+ /**
99
+ * Construct and return a fully-registered McpServer WITHOUT connecting any
100
+ * transport and WITHOUT requiring runtime payment env. Tool registration
101
+ * (names/descriptions/schemas) works with no secrets; only an actual paid tool
102
+ * invocation requires MCP_SVM_PAYMENT_ADDRESS.
103
+ *
104
+ * @returns {Promise<McpServer>}
105
+ */
106
+ export async function buildServer() {
107
+ const server = new McpServer(
108
+ {
109
+ // Stable MCP identity — matches the bin name and the registry
110
+ // mcpName suffix (io.github.nirholas/3d-agent-mcp). Deliberately
111
+ // NOT derived from the scoped npm package name.
112
+ name: '3d-agent-mcp',
113
+ version: PKG_VERSION,
114
+ },
115
+ {
116
+ // Declare full tools capability so clients on the strict MCP 2025-06-18
117
+ // spec know we don't push tools/list_changed notifications (our tool
118
+ // surface is fixed per-process). `resources` + `logging` left
119
+ // undeclared because we don't ship resource or logging APIs over this
120
+ // transport; declaring them empty would mislead clients into calling
121
+ // resources/list and getting a method-not-found.
122
+ capabilities: { tools: { listChanged: false } },
123
+ instructions: SERVER_INSTRUCTIONS,
124
+ },
125
+ );
126
+
127
+ const tools = await buildTools();
128
+ for (const t of tools) {
129
+ server.registerTool(
130
+ t.name,
131
+ {
132
+ title: t.title,
133
+ description: t.description,
134
+ inputSchema: t.inputSchema,
135
+ // MCP ToolAnnotations (readOnlyHint / destructiveHint /
136
+ // idempotentHint / openWorldHint) — lets clients gate
137
+ // confirmation prompts per tool instead of treating every call
138
+ // as a destructive write.
139
+ annotations: t.annotations,
140
+ },
141
+ t.handler,
142
+ );
143
+ }
144
+
145
+ return server;
146
+ }
147
+
148
+ /**
149
+ * Connect the server to stdio. Runs only as the process entry point. Eagerly
150
+ * warms the shared x402 resource server so the first paid call doesn't pay the
151
+ * /supported fetch cost, then connects the StdioServerTransport.
152
+ */
153
+ async function main() {
154
+ // Fail fast: a running server that can't receive payments is useless. This
155
+ // is the ONLY startup env gate — it does not run during buildServer()/tests.
156
+ assertPaymentEnv();
157
+
158
+ // Force the shared x402 resource server to initialize before any tool is
159
+ // invoked — this fetches /supported from each facilitator so verify + settle
160
+ // don't pay that cost on the first paid call.
161
+ await getResourceServer();
162
+ const initErr = getLastFacilitatorInitError();
163
+ if (initErr) {
164
+ console.error(`[mcp-server] facilitator init returned warnings: ${initErr.message}`);
165
+ }
166
+
167
+ const server = await buildServer();
168
+ const transport = new StdioServerTransport();
169
+ await server.connect(transport);
170
+ // Log to stderr so the stdout channel stays clean for MCP JSON-RPC frames.
171
+ console.error('[mcp-server] ready — paid tools registered over stdio');
172
+ }
173
+
174
+ // Run the stdio boot ONLY when this file is the process entry point. Importing
175
+ // the module for tests (or to reuse buildServer/buildTools) must NOT connect a
176
+ // transport or require payment env.
177
+ //
178
+ // The entry path is compared both directly and via its realpath: when launched
179
+ // through the npm bin (`node_modules/.bin/3d-agent-mcp`, a symlink to this file),
180
+ // process.argv[1] is the symlink while import.meta.url is the resolved target —
181
+ // so a direct compare alone would wrongly treat the bin launch as "imported" and
182
+ // never start the server.
183
+ function isProcessEntryPoint() {
184
+ const argvPath = process.argv[1];
185
+ if (!argvPath) return false;
186
+ if (import.meta.url === pathToFileURL(argvPath).href) return true;
187
+ try {
188
+ return import.meta.url === pathToFileURL(realpathSync(argvPath)).href;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+ const isEntryPoint = isProcessEntryPoint();
194
+
195
+ if (isEntryPoint) {
196
+ main().catch((err) => {
197
+ // One clean, actionable line to stderr — never a raw multi-line stack.
198
+ console.error(`mcp-server: ${err?.message || err}`);
199
+ process.exit(1);
200
+ });
201
+ }
@@ -0,0 +1,130 @@
1
+ // Multi-endpoint EVM RPC with automatic failover + bounded timeouts.
2
+ //
3
+ // `agent_reputation` and `ens_sns_resolve` previously talked to a SINGLE,
4
+ // hard-coded public RPC per chain with NO timeout and NO backup — a public
5
+ // endpoint rate-limiting or stalling took the whole tool down (and, with no
6
+ // timeout, could hang a paid call indefinitely).
7
+ //
8
+ // This module returns an ethers provider backed by a prioritized list of
9
+ // endpoints. With more than one endpoint it builds a `FallbackProvider` with
10
+ // `quorum: 1`, so the highest-priority healthy endpoint answers and a failure
11
+ // transparently falls over to the next. Every endpoint carries a request
12
+ // timeout, so no RPC call can hang without bound.
13
+ //
14
+ // Endpoint precedence (highest first):
15
+ // 1. caller-supplied override URLs (e.g. MCP_AGENT_REP_RPC_<id> / MCP_ENS_RPC_URL)
16
+ // 2. MCP_EVM_RPC_<chainId> — comma-separated env list
17
+ // 3. built-in public endpoints for that chain
18
+
19
+ import { FallbackProvider, FetchRequest, JsonRpcProvider, Network } from 'ethers';
20
+
21
+ // Built-in redundancy: at least two public endpoints per supported chain so a
22
+ // single provider outage is survivable out of the box. Operators who need
23
+ // guaranteed throughput should pin dedicated endpoints via MCP_EVM_RPC_<id>.
24
+ const BUILTIN_RPCS = {
25
+ 1: [
26
+ 'https://eth.llamarpc.com',
27
+ 'https://ethereum-rpc.publicnode.com',
28
+ 'https://cloudflare-eth.com',
29
+ ],
30
+ 8453: [
31
+ 'https://mainnet.base.org',
32
+ 'https://base-rpc.publicnode.com',
33
+ 'https://base.llamarpc.com',
34
+ ],
35
+ 42161: ['https://arb1.arbitrum.io/rpc', 'https://arbitrum-one-rpc.publicnode.com'],
36
+ 10: ['https://mainnet.optimism.io', 'https://optimism-rpc.publicnode.com'],
37
+ 137: ['https://polygon-rpc.com', 'https://polygon-bor-rpc.publicnode.com'],
38
+ 56: ['https://bsc-dataseed1.binance.org', 'https://bsc-rpc.publicnode.com'],
39
+ 43114: [
40
+ 'https://api.avax.network/ext/bc/C/rpc',
41
+ 'https://avalanche-c-chain-rpc.publicnode.com',
42
+ ],
43
+ 42220: ['https://forno.celo.org'],
44
+ 59144: ['https://rpc.linea.build'],
45
+ 534352: ['https://rpc.scroll.io'],
46
+ };
47
+
48
+ function dedupe(list) {
49
+ const seen = new Set();
50
+ const out = [];
51
+ for (const item of list) {
52
+ const v = (item || '').trim();
53
+ if (v && !seen.has(v)) {
54
+ seen.add(v);
55
+ out.push(v);
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /**
62
+ * Resolve the ordered RPC endpoint list for a chain. Caller overrides come
63
+ * first, then the MCP_EVM_RPC_<chainId> env list, then the built-in set.
64
+ *
65
+ * @param {number} chainId
66
+ * @param {string[]} [overrides] caller-supplied URLs to try first
67
+ * @returns {string[]}
68
+ */
69
+ export function getEvmRpcUrls(chainId, overrides = []) {
70
+ const fromEnv = (process.env[`MCP_EVM_RPC_${chainId}`] || '')
71
+ .split(',')
72
+ .map((s) => s.trim())
73
+ .filter(Boolean);
74
+ const builtin = BUILTIN_RPCS[chainId] || [];
75
+ const ordered = dedupe([...(overrides || []), ...fromEnv, ...builtin]);
76
+ if (!ordered.length) {
77
+ throw new Error(`evm-rpc: no endpoints known for chainId ${chainId}`);
78
+ }
79
+ return ordered;
80
+ }
81
+
82
+ function timedRequest(url, timeoutMs) {
83
+ const req = new FetchRequest(url);
84
+ req.timeout = timeoutMs;
85
+ return req;
86
+ }
87
+
88
+ /**
89
+ * Build an ethers provider for a chain with endpoint failover.
90
+ *
91
+ * With a single endpoint, returns a plain timed `JsonRpcProvider`. With more
92
+ * than one, returns a `FallbackProvider` (quorum 1) that answers from the
93
+ * first healthy endpoint and fails over on error — so the multi-read callers
94
+ * (resolveAgentId + identity + reputation + events) keep using one provider
95
+ * object while every underlying call is redundant and bounded.
96
+ *
97
+ * @param {number} chainId
98
+ * @param {object} [opts]
99
+ * @param {string[]} [opts.overrides] caller-supplied URLs to try first
100
+ * @param {number} [opts.timeoutMs=12000]
101
+ * @returns {import('ethers').AbstractProvider}
102
+ */
103
+ export function makeEvmProvider(chainId, opts = {}) {
104
+ const { overrides = [], timeoutMs = 12_000 } = opts;
105
+ const urls = getEvmRpcUrls(chainId, overrides);
106
+ const network = Network.from(chainId);
107
+
108
+ if (urls.length === 1) {
109
+ return new JsonRpcProvider(timedRequest(urls[0], timeoutMs), network, {
110
+ staticNetwork: network,
111
+ });
112
+ }
113
+
114
+ const configs = urls.map((url, i) => ({
115
+ provider: new JsonRpcProvider(timedRequest(url, timeoutMs), network, {
116
+ staticNetwork: network,
117
+ }),
118
+ priority: i + 1, // lower number = tried first
119
+ // Move on to the next endpoint if this one stalls, well before the hard
120
+ // per-request timeout, so failover is responsive.
121
+ stallTimeout: Math.min(timeoutMs, 3_000),
122
+ weight: 1,
123
+ }));
124
+
125
+ // quorum 1: a single endpoint's answer is authoritative; the rest are pure
126
+ // failover, not a consensus requirement.
127
+ return new FallbackProvider(configs, network, { quorum: 1 });
128
+ }
129
+
130
+ export { BUILTIN_RPCS };