x402-toolpick / tree / src/mcp-stdio.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
#!/usr/bin/env node
// ToolPick MCP server over stdio (newline-delimited JSON-RPC 2.0).
// Free by default: find_paid_api returns the hosted preview (match count + top 3 services with price and payers).
// With TOOLPICK_X402_PRIVATE_KEY set (and @x402/fetch, @x402/evm, viem installed) the server pays the hosted API per
// call over x402 (pick $0.01, market $0.02): https://api.youraitools.co/toolpick
//   node src/mcp-stdio.js   (run from a source checkout: https://agentgate-source.pages.dev/x402-toolpick/ ; remote MCP: https://api.youraitools.co/toolpick/mcp)
import { createInterface } from 'node:readline';
import { toQuery, preview, DISCLAIMER, FULL_DETAILS, API_BASE, PRICES } from './lite.js';
import { createPaidClient } from './paid.js';
import { VERSION_STR as VERSION } from './version.js';

export { VERSION };
const NEED = { type: 'string', description: 'The capability you need, e.g. "web search", "solana token safety", "pdf to text"' };
const FILTERS = { max_price: { type: 'number', description: 'Max USD per call, e.g. 0.01' }, network: { type: 'string', description: 'any, base, solana, polygon, arbitrum, optimism, ethereum or avalanche' } };
const RO = { readOnlyHint: true, openWorldHint: true, idempotentHint: true };
export const TOOLS = [
  { name: 'find_paid_api', title: 'Find and pick a paid x402 API for a task', description: `Which paid API should my agent call? Free by default: how many paid x402 resources match the need and the top 3 services (price, networks, 30-day payers). With TOOLPICK_X402_PRIVATE_KEY set, returns the paid pick tier (${PRICES.pick} USDC on Base per call over x402): top 10 with resource URLs and usage, live 402 checks of the top 5, a best verified pick, the cheapest verified alternative and the category price range. Listings are self-described by sellers; ToolPick ranks, it does not endorse.`, inputSchema: { type: 'object', properties: { need: NEED, ...FILTERS }, required: ['need'] }, annotations: RO },
  { name: 'x402_market_report', title: 'x402 market report for a category', description: `Paid only (needs TOOLPICK_X402_PRIVATE_KEY, ${PRICES.market} USDC on Base per call): for a category (or the whole x402 index), resources and seller hosts, listed price quartiles, the price where buyers are (payer-weighted median), 30-day calls, estimated revenue and top sellers. For pricing a new endpoint or checking a niche.`, inputSchema: { type: 'object', properties: { need: { ...NEED, description: 'Category, e.g. "token safety"; omit for the whole index' }, network: FILTERS.network } }, annotations: RO },
];
const PROTOCOLS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
const summarizePreview = (r, note) => `${note ? note + '\n' : ''}ToolPick "${r.need}": ${r.matches} matching paid x402 resources on ${r.hosts} hosts. Top: ${(r.top || []).map((t) => `${t.service} ($${t.price_usd}, ${t.payers_30d ?? 'n/a'} payers/30d)`).join('; ')}.\n${FULL_DETAILS} Set TOOLPICK_X402_PRIVATE_KEY to get them here.\n${DISCLAIMER}`;
const summarizePick = (r) => `ToolPick "${r.need}": best verified pick ${r.best_pick ? `${r.best_pick.service} ${r.best_pick.resource} ($${r.best_pick.price_usd})` : 'none (no top-5 candidate passed the live check)'}; ${r.matches} matches on ${r.hosts} hosts; buyers' median price $${r.stats?.price_with_buyers_usd?.median ?? 'n/a'}.\n${(r.results || []).slice(0, 10).map((x) => `${x.rank}. ${x.service} ${x.method} ${x.resource} $${x.price_usd} payers ${x.payers_30d ?? 'n/a'}${x.verified_live === true ? ' [live]' : x.verified_live === false ? ' [check failed]' : ''}`).join('\n')}\n${DISCLAIMER}`;
const summarizeMarket = (r) => `ToolPick market${r.need ? ` "${r.need}"` : ' (whole index)'}: ${r.stats?.resources} resources on ${r.stats?.hosts} hosts (${r.stats?.hosts_with_3plus_payers} with 3+ payers); listed median $${r.stats?.price_usd?.median}, buyers' payer-weighted median $${r.stats?.price_with_buyers_usd?.payer_weighted_median}; 30-day calls ${r.stats?.calls_30d}, est. revenue $${r.stats?.est_revenue_30d_usd} (upper bound).\nTop sellers: ${(r.top_sellers || []).slice(0, 5).map((s) => `${s.host} ($${s.est_revenue_30d_usd})`).join(', ')}\n${DISCLAIMER}`;

let paidClient;
const paid = async (opts) => { if (paidClient === undefined) paidClient = await createPaidClient(opts); return paidClient; };

export async function handle(m, opts = {}) {
  if (!m || m.jsonrpc !== '2.0' || typeof m.method !== 'string') return { jsonrpc: '2.0', id: m?.id ?? null, error: { code: -32600, message: 'Invalid Request' } };
  const note = m.id === undefined || m.id === null;
  const ok = (result) => (note ? null : { jsonrpc: '2.0', id: m.id, result });
  const err = (text) => ok({ isError: true, content: [{ type: 'text', text }] });
  switch (m.method) {
    case 'initialize': {
      const client = await paid(opts);
      const mode = client && !client.error ? `Paid mode: full data from ${client.apiBase} over x402.` : `Free preview mode; URLs, live checks and market reports at ${API_BASE} (x402).`;
      return ok({ protocolVersion: PROTOCOLS.includes(m.params?.protocolVersion) ? m.params.protocolVersion : '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'toolpick', title: 'ToolPick', version: VERSION }, instructions: `Call find_paid_api with the capability you need to find a paid x402 API for it. ${mode}` });
    }
    case 'ping': return ok({});
    case 'tools/list': return ok({ tools: TOOLS });
    case 'tools/call': {
      const name = m.params?.name;
      if (!TOOLS.some((t) => t.name === name)) return note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32602, message: `Unknown tool: ${name}` } };
      const args = m.params?.arguments || {};
      const client = await paid(opts);
      if (name === 'x402_market_report') {
        const q = toQuery(args, { needRequired: false }); if (q.error) return err(`bad_request: ${q.error}`);
        if (!client || client.error) return err(`x402_market_report is paid only: set TOOLPICK_X402_PRIVATE_KEY (Base USDC) or call ${API_BASE}/v1/market with any x402 client (${PRICES.market}).${client?.error ? ` (${client.error})` : ''}`);
        try { const r = await client.call('market', { need: q.need, network: q.network }); if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarizeMarket(r.result) }], structuredContent: { ...r.result, payer: client.payer }, isError: false }); return err(`${r.error}${r.detail ? `: ${r.detail}` : ''}`); } catch (e) { return err(`paid call failed: ${String(e?.message || e).slice(0, 200)}`); }
      }
      const q = toQuery(args);
      if (q.error) return err(`bad_request: ${q.error}`);
      let fallbackNote = '';
      if (client?.error) fallbackNote = client.error;
      else if (client) {
        try {
          const r = await client.call('pick', q);
          if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarizePick(r.result) }], structuredContent: { ...r.result, payer: client.payer, ...(r.paymentResponse ? { payment_response: r.paymentResponse } : {}) }, isError: false });
          fallbackNote = `Paid call failed (${r.error}${r.detail ? `: ${r.detail}` : ''}); showing the free preview.`;
        } catch (e) { fallbackNote = `Paid call failed (${String(e?.message || e).slice(0, 200)}); showing the free preview.`; }
      }
      const r = await preview(q, { fetchImpl: opts.fetchImpl, apiBase: opts.apiBase || process.env.TOOLPICK_API_URL || API_BASE });
      if (r.error) return err(`${r.error}: ${r.message}${fallbackNote ? ` (${fallbackNote})` : ''}`);
      delete r.full;
      return ok({ content: [{ type: 'text', text: summarizePreview(r, fallbackNote) }], structuredContent: fallbackNote ? { ...r, paid_error: fallbackNote } : r, isError: false });
    }
    default:
      return m.method.startsWith('notifications/') || note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32601, message: `Method not found: ${m.method}` } };
  }
}

export function serve({ input = process.stdin, output = process.stdout } = {}) {
  const rl = createInterface({ input, crlfDelay: Infinity });
  const write = (o) => output.write(JSON.stringify(o) + '\n');
  rl.on('line', async (l) => {
    if (!l.trim()) return;
    let msg; try { msg = JSON.parse(l); } catch { return write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); }
    const batch = Array.isArray(msg);
    const replies = (await Promise.all((batch ? msg : [msg]).map((x) => handle(x)))).filter(Boolean);
    if (replies.length) write(batch ? replies : replies[0]);
  });
  return rl;
}

const self = process.argv[1] || '';
if (import.meta.url === `file://${self}` || /toolpick(-mcp)?$/.test(self)) serve();

agentgate-source: code mirror on Cloudflare Pages. All repos