Skip to main content
Glama

Bitcoin SV MCP Server

by b-open-io
a2bPublishAgent.ts9.22 kB
import { PrivateKey, Utils } from "@bsv/sdk"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import type { ServerNotification, ServerRequest, } from "@modelcontextprotocol/sdk/types.js"; import { createOrdinals } from "js-1sat-ord"; import type { ChangeResult, CreateOrdinalsConfig, Destination, Inscription, LocalSigner, PreMAP, } from "js-1sat-ord"; import { Sigma } from "sigma-protocol"; import { z } from "zod"; import type { Wallet } from "./wallet"; const { toArray, toBase64 } = Utils; // https://raw.githubusercontent.com/google/A2A/refs/heads/main/specification/json/a2a.json // A2A AgentCard schema (per A2A spec) const AgentCapabilitiesSchema = z.object({ streaming: z.boolean().default(false), pushNotifications: z.boolean().default(false), stateTransitionHistory: z.boolean().default(false), }); const AgentSkillSchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), tags: z.array(z.string()).nullable(), examples: z.array(z.string()).nullable(), inputModes: z.array(z.string()).nullable(), outputModes: z.array(z.string()).nullable(), }); // Provider per A2A spec const AgentProviderSchema = z .object({ organization: z.string(), url: z.string().url().nullable().default(null), }) .nullable() .default(null); // Authentication per A2A spec const AgentAuthenticationSchema = z .object({ schemes: z.array(z.string()), credentials: z.string().nullable().default(null), }) .nullable() .default(null); // Pricing plan schema export const PricingSchema = z.object({ id: z.string(), description: z.string().nullable().optional(), currency: z.string(), amount: z .number() .describe( "Cost in standard units of currency (e.g., BSV, USD, not satoshis)", ), address: z.string(), acceptedCurrencies: z.array(z.string()).optional(), skills: z.array(z.string()).optional(), interval: z.enum(["day", "week", "month", "year"]).nullable().optional(), includedCalls: z.record(z.number()).optional(), }); export type PricingConfig = z.infer<typeof PricingSchema>; export const AgentCardSchema = z.object({ name: z.string(), description: z.string().nullable().default(null), url: z.string().url(), provider: AgentProviderSchema, version: z.string(), documentationUrl: z.string().url().nullable().default(null), capabilities: AgentCapabilitiesSchema, authentication: AgentAuthenticationSchema, defaultInputModes: z.array(z.string()).default(["text"]), defaultOutputModes: z.array(z.string()).default(["text"]), skills: z.array(AgentSkillSchema), "x-payment": z.array(PricingSchema).optional().default([]), }); // Schema for on-chain agent publish parameters export const a2bPublishArgsSchema = z.object({ agentUrl: z .string() .url() .describe("Agent base URL (e.g. https://example.com)"), agentName: z.string().describe("Human-friendly agent name"), description: z .string() .nullable() .optional() .describe("Optional agent description"), providerOrganization: z .string() .optional() .describe("Optional provider organization name"), providerUrl: z.string().url().optional().describe("Optional provider URL"), version: z.string().optional().describe("Optional agent version"), documentationUrl: z .string() .url() .nullable() .optional() .describe("Optional documentation URL"), streaming: z .boolean() .default(false) .describe("Supports SSE (tasks/sendSubscribe)"), pushNotifications: z .boolean() .default(false) .describe("Supports push notifications"), stateTransitionHistory: z .boolean() .default(false) .describe("Supports state transition history"), defaultInputModes: z .array(z.string()) .default(["text"]) .describe("Default input modes"), defaultOutputModes: z .array(z.string()) .default(["text"]) .describe("Default output modes"), skills: z .array(AgentSkillSchema) .optional() .default([]) .describe("List of agent skills"), destinationAddress: z .string() .optional() .describe("Optional target address for inscription"), }); export type A2bPublishArgs = z.infer<typeof a2bPublishArgsSchema>; /** * Registers the wallet_a2bPublish tool for publishing an agent record on-chain */ export function registerA2bPublishAgentTool(server: McpServer, wallet: Wallet) { server.tool( "wallet_a2bPublish", "Publish an agent.json record on-chain via Ordinal inscription", { args: a2bPublishArgsSchema }, async ( { args }: { args: A2bPublishArgs }, extra: RequestHandlerExtra<ServerRequest, ServerNotification>, ) => { try { const paymentPk = wallet.getPrivateKey(); if (!paymentPk) throw new Error("No private key available"); const { paymentUtxos } = await wallet.getUtxos(); if (!paymentUtxos?.length) throw new Error("No payment UTXOs available to fund inscription"); // TODO: Get the skills from actually running the MCP instead of trusting the agent args for args.skills const walletAddress = paymentPk.toAddress().toString(); // Create pricing plans using the new schema const pricingConfig: PricingConfig[] = [ // { // id: "subscription-premium", // description: "Premium subscription with all features", // currency: "USD", // amount: 10, // address: walletAddress, // interval: "month", // skills: ["wallet_a2bPublish", "wallet_a2bCall"], // }, // { // id: "subscription-free", // description: "Free tier with limited features", // currency: "USD", // amount: 0, // address: walletAddress, // interval: "month", // skills: ["wallet_a2bCall"], // }, { id: "pay-per-call-default", description: "Pay-per-call for publish operations", currency: "USD", amount: 1, address: walletAddress, skills: ["wallet_a2bPublish"], interval: null, }, // { // id: "pay-per-call-royalty", // description: "Royalty payment for agent", // currency: "USD", // amount: 1, // address: "1JOExxxxxxxxxxxx", // skills: ["wallet_a2bPublish"], // interval: null, // } ]; // Assemble AgentCard with defaults and user overrides const agentCard = { name: args.agentName, description: args.description ?? null, url: args.agentUrl, provider: args.providerOrganization && args.providerUrl ? { organization: args.providerOrganization, url: args.providerUrl, } : null, version: args.version ?? "1.0.0", documentationUrl: args.documentationUrl ?? null, capabilities: { streaming: args.streaming, pushNotifications: args.pushNotifications, stateTransitionHistory: args.stateTransitionHistory, }, authentication: null, defaultInputModes: args.defaultInputModes, defaultOutputModes: args.defaultOutputModes, skills: args.skills, "x-payment-config": pricingConfig, }; // Validate compliance AgentCardSchema.parse(agentCard); const fileContent = JSON.stringify(agentCard, null, 2); // Base64 payload for inscription const dataB64 = toBase64(toArray(fileContent)); const inscription: Inscription = { dataB64, contentType: "application/json", }; // Destination for the ordinal const targetAddress = args.destinationAddress ?? walletAddress; const destinations: Destination[] = [ { address: targetAddress, inscription }, ]; // Default MAP metadata: file path, content type, encoding const metaData: PreMAP = { app: "bsv-mcp", type: "a2b" }; const createOrdinalsConfig: CreateOrdinalsConfig = { utxos: paymentUtxos, destinations, paymentPk, changeAddress: walletAddress, metaData, }; const identityPk = process.env.IDENTITY_KEY_WIF ? PrivateKey.fromWif(process.env.IDENTITY_KEY_WIF) : undefined; if (identityPk) { createOrdinalsConfig.signer = { idKey: identityPk, } as LocalSigner; } // Inscribe the ordinal on-chain via js-1sat-ord const result = await createOrdinals(createOrdinalsConfig); const changeResult = result as ChangeResult; const disableBroadcasting = process.env.DISABLE_BROADCASTING === "true"; if (!disableBroadcasting) { await changeResult.tx.broadcast(); // Refresh UTXOs try { await wallet.refreshUtxos(); } catch {} // Return transaction details return { content: [ { type: "text", text: JSON.stringify({ txid: changeResult.tx.id("hex"), spentOutpoints: changeResult.spentOutpoints, payChange: changeResult.payChange, inscriptionAddress: targetAddress, agentCard, }), }, ], }; } return { content: [ { type: "text", text: changeResult.tx.toHex(), }, ], }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { content: [{ type: "text", text: msg }], isError: true }; } }, ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/b-open-io/bsv-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server