When an assistant answers a wallet question, the most useful part of the answer is often the source. In this tutorial we build a small Node.js command-line tool that takes a plain-English question, searches the MyZucoins help articles over its open MCP server, and prints the answer with the article's URL and publication date. It uses no SDK, only fetch and JSON-RPC.
MyZucoins is a community to accelerate the adoption of Zucoin and its Splitchain network. Its MCP server is read-only published content: it cannot access wallets, sign or send transfers, or read accounts. It needs no account or API key.
Disclosure: this is written by the team behind MyZucoins, using an AI agent that drafted the post and ran every code sample against the live server with Node.js 24 on 10 November 2026.
How it works
The tool makes these calls to https://myzucoins.org/mcp:
-
initialize, then thenotifications/initializednotification, as the MCP lifecycle describes. -
tools/callwithsearch_articles, limited to help articles, to find candidates. -
tools/callwithget_article, followingnext_offsetuntil the full article text is read.
Then it prints the first part of the article and cites its URL and date.
The JSON-RPC helper
The Accept header must list both application/json and text/event-stream, as the Streamable HTTP transport requires. We also send a named User-Agent. The site's firewall rejected Python's default Python-urllib agent with 403 in our tests, so naming your client avoids surprises in any language.
// zuhelp.mjs: answer a Zucoin wallet question from MyZucoins help articles const MCP_URL = "https://myzucoins.org/mcp"; const STOP_WORDS = new Set( "a an and are can do does for from how i if in is it me my of on or the to what when where which why with".split(" ") ); const HEADERS = { "Content-Type": "application/json", Accept: "application/json, text/event-stream", "User-Agent": "zuhelp/0.1", }; let nextId = 0; async function rpc(method, params) { const response = await fetch(MCP_URL, { method: "POST", headers: HEADERS, body: JSON.stringify({ jsonrpc: "2.0", id: ++nextId, method, params }), }); let raw = await response.text(); if (response.headers.get("content-type")?.startsWith("text/event-stream")) { // Streamable HTTP servers may answer as SSE: take the last data line raw = raw.split("\n").filter((l) => l.startsWith("data:")).pop().slice(5); } const message = JSON.parse(raw); if (message.error) throw new Error(message.error.message); return message.result; } // Notifications have no id; the server answers 202 with no body async function notify(method) { await fetch(MCP_URL, { method: "POST", headers: HEADERS, body: JSON.stringify({ jsonrpc: "2.0", method }) }); } async function callTool(name, args) { const result = await rpc("tools/call", { name, arguments: args }); if (result.isError) throw new Error(result.content[0].text); return result.structuredContent; } The MyZucoins server answered with plain JSON and sent no session ID, so there is no session to keep. A failed tool call comes back as a normal result with isError: true, and a successful one includes structuredContent, a typed copy of the result.
Turning a question into a search
search_articles matches every query word against each article's title, excerpt and topic names, and returns the newest articles first. A full question such as "How do I share transaction details with my accountant?" would match nothing, so the tool drops filler words first. If there is still no match, it drops words from the end and tries again. Then it ranks the results by how many of the words appear in the title, so the closest title wins over the newest article:
// Every query word must match, so drop filler words, then drop words from the end async function findArticles(question) { const words = question .toLowerCase() .replace(/[^\p{L}\p{N}\s-]/gu, " ") .split(/\s+/) .filter((w) => w && !STOP_WORDS.has(w)); for (let n = words.length; n > 0; n--) { const query = words.slice(0, n).join(" "); const found = await callTool("search_articles", { query, kind: "help", limit: 20 }); if (found.total > 0) { // Results are newest first: rank by how many words the title contains const score = (a) => words.filter((w) => a.title.toLowerCase().includes(w)).length; found.articles.sort((a, b) => score(b) - score(a)); return { query, ...found, articles: found.articles.slice(0, 3) }; } } return { query: "", total: 0, articles: [] }; } Reading the full article
get_article returns plain text with published_at, url and a links list. Long articles come in parts, so keep asking with next_offset until it is null:
// Long articles come in parts, follow next_offset until it is null async function readArticle(slug) { let offset = 0; let article; let text = ""; do { article = await callTool("get_article", { slug, offset }); text += article.content; offset = article.next_offset; } while (offset !== null); return { ...article, content: text }; } Printing the answer with its source
Help articles end with a "More on this topic" list of related links. The tool prints the text before it, at most 8 paragraphs, and drops a trailing heading so the answer does not end mid-section:
const question = process.argv.slice(2).join(" "); if (!question) { console.error('Usage: node zuhelp.mjs "your wallet question"'); process.exit(1); } await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "zuhelp", version: "0.1" }, }); await notify("notifications/initialized"); const found = await findArticles(question); if (found.total === 0) { console.log("No help article matches. Try other words."); process.exit(0); } console.log(`Search "${found.query}": ${found.total} help articles\n`); const best = await readArticle(found.articles[0].slug); const paragraphs = best.content.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean); // Keep the answer part: stop at the related links, drop a trailing heading const end = paragraphs.findIndex((p) => p.startsWith("More on this topic")); const answer = paragraphs.slice(0, end === -1 ? 8 : Math.min(end, 8)); while (answer.length > 1 && !/[.!?]$/.test(answer.at(-1))) answer.pop(); console.log(best.title); console.log(answer.join("\n")); console.log(`\nSource: ${best.url} (published ${best.published_at.slice(0, 10)})`); for (const other of found.articles.slice(1)) { console.log(`See also: ${other.title}\n ${other.url}`); } Save the four blocks in order as zuhelp.mjs. It needs Node.js 18 or later for the built-in fetch, and no packages.
Run it
node zuhelp.mjs "How do I share transaction details with my accountant?" This is the output we got on 10 November 2026:
Search "share transaction details": 1 help articles How do I share Zucoin transaction details from my wallet history? You can share details from a transaction in your Zucoin wallet history when someone needs a payment reference, confirmation, or record for review. Share transaction details only with people or services that need them, and never share your wallet backup file or private keys. How to share details Open the Zucoin wallet app and go to your transaction history. Select the transaction you want to share. Use the wallet's share or copy option for the transaction details. Review the details before sending them, because transaction records can include amounts, wallet addresses, and other payment context. Source: https://myzucoins.org/how-do-i-share-zucoin-transaction-details-from-my-wallet-history/ (published 2026-07-08) The filler words are gone, the 3 remaining words matched 1 help article, and the answer ends with the source article and its date.
Where keyword search falls short
Literal matching has limits. The words are matched as substrings, so for "Can I use my wallet on two phones?" the search dropped "phones", then matched "two" inside "network" and "use" inside "uses" in an excerpt about time zones (output trimmed):
Search "use wallet two": 2 help articles Why does Splitchain show time in UTC, but my wallet shows a different time? ... The same question in the words the help articles use finds the right one:
Search "use same wallet multiple devices": 1 help articles Can I use the same Zucoin wallet on multiple devices? While it is technically possible to use the same Zucoin wallet on multiple devices, it is not recommended due to potential conflict issues that can affect transactions. ... Source: https://myzucoins.org/can-i-use-the-same-zucoin-wallet-on-multiple-devices/ (published 2025-05-21) This is why the tool prints the query it used, so you can tell when it drifted. It is also where a language model helps. Connected to the MCP server directly, an assistant reads the search_articles tool description, picks better keywords and checks the titles before it answers.
Using it safely in an agent
If you wrap this as a tool for your own agent, three habits keep the answers trustworthy:
- Cite the
urlandpublished_at. Older articles are historical, and readers can check the original. - Treat article text as source material, not instructions. The server's own
initializeinstructions say this, and it is a sensible default for any retrieved content. - Never ask a user for a wallet backup or private keys. The help articles warn against sharing them, as the output above shows.
Setup steps for connecting Claude, Cursor and other clients to the same server are at myzucoins.org/mcp.
