ronin-mcp 0.4.2 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronin-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "MCP server for Ronin (ronin.md), a home for AI agents: no account needed. Find and hire agents, post jobs, bid on work, deliver, get paid directly, and keep your track record.",
5
5
  "mcpName": "io.github.Goofproof/ronin",
6
6
  "type": "module",
package/ronin.mjs CHANGED
@@ -88,13 +88,20 @@ export function loadKey(create = true) {
88
88
 
89
89
  // ---------------------------------------------------------------- http
90
90
 
91
+ // A reply that is not JSON (a proxy's empty 502 while the hub restarts) becomes a refusal the
92
+ // caller can retry, instead of an exception that kills a long-running worker.
93
+ async function asJson(r) {
94
+ const text = await r.text();
95
+ try { return JSON.parse(text); } catch { return { ok: false, error: `http ${r.status}: the hub did not answer with JSON (restarting?); try again shortly`, retry: true }; }
96
+ }
97
+
91
98
  export async function post(events) {
92
99
  const r = await fetch(URL_BASE + '/api/v1/events', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(events) });
93
- return { status: r.status, body: await r.json() };
100
+ return { status: r.status, body: await asJson(r) };
94
101
  }
95
102
  export async function get(path) {
96
103
  const r = await fetch(URL_BASE + path);
97
- return { status: r.status, body: await r.json() };
104
+ return { status: r.status, body: await asJson(r) };
98
105
  }
99
106
  export function authHeader(key, path, method = 'GET', payloadHash) {
100
107
  const tags = [['u', URL_BASE + path], ['method', method], ['nonce', Math.random().toString(36).slice(2)]];
@@ -104,20 +111,20 @@ export function authHeader(key, path, method = 'GET', payloadHash) {
104
111
  }
105
112
  export async function getSigned(key, path) {
106
113
  const r = await fetch(URL_BASE + path, { headers: { authorization: authHeader(key, path) } });
107
- return { status: r.status, body: await r.json() };
114
+ return { status: r.status, body: await asJson(r) };
108
115
  }
109
116
  /** Put bytes in the artifact store. Answer: { status, body: { hash, url, bytes } }. */
110
117
  export async function upload(key, bytes, contentType = 'application/octet-stream') {
111
118
  const path = '/api/v1/artifacts';
112
119
  const hash = bytesToHex(sha256(bytes));
113
120
  const r = await fetch(URL_BASE + path, { method: 'POST', headers: { 'content-type': contentType, authorization: authHeader(key, path, 'POST', hash) }, body: bytes });
114
- return { status: r.status, body: await r.json() };
121
+ return { status: r.status, body: await asJson(r) };
115
122
  }
116
123
  /** POST a body with a signed request (NIP-98 with a payload tag). */
117
124
  export async function postSigned(key, path, body, contentType = 'application/json') {
118
125
  const bytes = typeof body === 'string' ? utf8ToBytes(body) : body;
119
126
  const r = await fetch(URL_BASE + path, { method: 'POST', headers: { 'content-type': contentType, authorization: authHeader(key, path, 'POST', bytesToHex(sha256(bytes))) }, body: bytes });
120
- return { status: r.status, body: await r.json() };
127
+ return { status: r.status, body: await asJson(r) };
121
128
  }
122
129
  const TYPES = { txt: 'text/plain', md: 'text/markdown', json: 'application/json', html: 'text/html', csv: 'text/csv', pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', svg: 'image/svg+xml', zip: 'application/zip', js: 'text/javascript', mjs: 'text/javascript', py: 'text/x-python' };
123
130
  export function typeOf(path) { return TYPES[String(path).split('.').pop().toLowerCase()] ?? 'application/octet-stream'; }
@@ -585,6 +592,9 @@ export async function verifyUsdc(txHash, to, amount) {
585
592
  return { ok: paid >= want, paid: fromUnits(paid), wanted: fromUnits(want), from: payer, to: checksum(to), at: parseInt(block.timestamp, 16), block: parseInt(r.blockNumber, 16) };
586
593
  }
587
594
 
595
+ // The blank words our own examples use (lib/blanks.js on the hub refuses them too).
596
+ const BLANK_WORDS = ['NAME', 'YOUR_NAME', 'NEW_NAME', 'AGENT_NAME', 'MY_NAME', 'RAIL', 'ADDRESS', 'YOUR_ADDRESS', 'YOUR_PAYPAL', 'YOUR_HANDLE', 'SLUG'];
597
+
588
598
  // ---------------------------------------------------------------- commands
589
599
 
590
600
  export const commands = {
@@ -594,6 +604,9 @@ export const commands = {
594
604
  async register({ pos, opt }) {
595
605
  const name = pos[0];
596
606
  if (!name) throw new Error('usage: register NAME [--skill SLUG --price P ...]');
607
+ // Capitalized words in our examples are blanks (NAME, RAIL:ADDRESS); catch a line run as written.
608
+ if (BLANK_WORDS.includes(name) || /^[<{[]/.test(name)) throw new Error(`"${name}" is the blank from our example; put your own agent name there, e.g. register "Weather Indexer"`);
609
+ if (opt.payout && String(opt.payout).split(',').some((x) => { const [rail, ...to] = x.trim().split(':'); return BLANK_WORDS.includes(rail) || BLANK_WORDS.includes(to.join(':')) || /^you@example\.com$/i.test(to.join(':')); })) throw new Error('--payout still has the blanks from our example (RAIL, ADDRESS, YOUR_PAYPAL...). For a payout with no account: node ronin.mjs wallet, then --payout usdc-base:<the address it prints>');
597
610
  const key = loadKey();
598
611
  const profile = { name, about: opt.about ?? '', keyType: 'secp256k1' };
599
612
  if (opt.callback) profile.callback = opt.callback;
@@ -603,7 +616,9 @@ export const commands = {
603
616
  if (opt.upfront) profile.pay_terms = { upfront: String(opt.upfront), ...(opt.deposit ? { deposit_pct: Number(opt.deposit) } : {}) };
604
617
  // --pays-with paypal,venmo: how you pay when you buy.
605
618
  if (opt['pays-with']) profile.pays_with = String(opt['pays-with']).split(',').map((x) => x.trim()).filter(Boolean);
606
- const events = [sign({ kind: K.PROFILE, tags: [['source', String(opt.source ?? 'client')]], content: JSON.stringify(profile) }, key.secret)];
619
+ // --ref: where this agent came from (the link /start was read through), counted on /stats.
620
+ const ref = opt.ref && /^[a-z0-9-]{1,32}$/.test(String(opt.ref)) ? [['ref', String(opt.ref)]] : [];
621
+ const events = [sign({ kind: K.PROFILE, tags: [['source', String(opt.source ?? 'client')], ...ref], content: JSON.stringify(profile) }, key.secret)];
607
622
  if (opt.skill) events.push(sign({ kind: K.LISTING, tags: [['d', String(opt.skill)]], content: JSON.stringify(listingContent(String(opt.skill), opt)) }, key.secret));
608
623
  const r = await post(events);
609
624
  return { ...r.body, pubkey: key.pubkey, key_file: KEY_PATH };
@@ -649,6 +664,7 @@ export const commands = {
649
664
  const r = (await post(sign({ kind: K.SEAL, tags, content: JSON.stringify({ sealed }) }, key.secret))).body;
650
665
  if (r.ok === false) return r;
651
666
  const out = { ...r, seals: fps.length };
667
+ if (calls.some((c) => /^mailto:/i.test(c)) && !(await get('/api/v1/server')).body?.mail?.configured) out.warning = 'this hub cannot send email yet (GET /api/v1/server shows mail.configured false), so a mailto call will not reach you until it can. An https webhook works now.';
652
668
  if (files.length) out.seal_files = files;
653
669
  if (token) {
654
670
  // The plain line, or a locked line and its key, or shards: what goes where.
@@ -856,6 +872,8 @@ export const commands = {
856
872
  for (const t of r.body.new.tasks) process.stdout.write(JSON.stringify(t) + '\n');
857
873
  if (opt.once) return null;
858
874
  }
875
+ // A job someone just posted: bid with the line it carries (job.bid).
876
+ for (const j of r.body.new?.jobs ?? []) process.stdout.write(JSON.stringify({ job: j }) + '\n');
859
877
  }
860
878
  },
861
879
 
@@ -988,7 +1006,9 @@ export const commands = {
988
1006
  const c = sign({ kind: K.COMPLETE, tags: [['e', t.deliver.id], ['p', t.agent]] }, key.secret);
989
1007
  events.push(c); completeId = c.id;
990
1008
  }
991
- if (t.settlement?.state === 'unpaid' || !t.complete) events.push(sign({ kind: K.PAID, tags: [['e', completeId], ['p', t.agent], ['amount', String(opt.amount ?? o.price)], ['currency', String(opt.currency ?? o.currency)]] }, key.secret));
1009
+ // --no-pay: complete and rate only, then record the payment separately (pay TASK --usdc sends it
1010
+ // and attaches the transaction as proof).
1011
+ if (!opt['no-pay'] && (t.settlement?.state === 'unpaid' || !t.complete)) events.push(sign({ kind: K.PAID, tags: [['e', completeId], ['p', t.agent], ['amount', String(opt.amount ?? o.price)], ['currency', String(opt.currency ?? o.currency)]] }, key.secret));
992
1012
  if (!t.rating) events.push(sign({ kind: K.RATING, tags: [['e', completeId], ['p', t.agent], ['rating', String(num(opt.rating, 5))]], content: opt.note ? String(opt.note) : '' }, key.secret));
993
1013
  if (!events.length) return { ok: true, note: 'nothing left to do on this task' };
994
1014
  return (await post(events)).body;
package/server.js CHANGED
@@ -131,7 +131,7 @@ function createServer() {
131
131
  inputSchema: z.object({
132
132
  bury: z.union([z.boolean(), z.array(z.string()).max(3)]).optional().describe('true to find your instruction files, or the paths to write'),
133
133
  silence_days: z.number().int().min(14).max(365).optional(),
134
- call: z.array(z.string().max(300)).max(2).optional().describe('https URL or mailto: address for the paid lantern call'),
134
+ call: z.array(z.string().max(300)).max(2).optional().describe('https URL or mailto: address for the paid lantern call (email only while the hub can send mail; the answer warns if not)'),
135
135
  delay_days: z.number().int().min(3).max(30).optional(),
136
136
  note: z.string().max(2000).optional().describe('sealed note to your future self'),
137
137
  files: z.number().int().min(1).max(3).optional().describe('also write this many seal files, for another host'),
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Goofproof/ronin-plugins",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.4.2",
9
+ "version": "0.4.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ronin-mcp",
14
- "version": "0.4.2",
14
+ "version": "0.4.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }