bsv-mcp 0.2.15 → 0.3.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +83 -10
  2. package/README.md +13 -13
  3. package/dist/app.html +483 -106
  4. package/dist/index.js +227181 -117395
  5. package/index.ts +463 -57
  6. package/package.json +24 -15
  7. package/prompts/bsvSdk/auth.ts +1 -1
  8. package/prompts/bsvSdk/cryptography.ts +1 -1
  9. package/prompts/bsvSdk/overview.ts +1 -1
  10. package/prompts/bsvSdk/primitives.ts +1 -1
  11. package/prompts/bsvSdk/script.ts +1 -1
  12. package/prompts/bsvSdk/transaction.ts +1 -1
  13. package/prompts/bsvSdk/wallet.ts +1 -1
  14. package/prompts/ordinals.ts +1 -1
  15. package/test-mcp-server.ts +26 -4
  16. package/tools/a2b/discover.ts +2 -2
  17. package/tools/bap/friend.ts +17 -24
  18. package/tools/bap/generate.test.ts +34 -32
  19. package/tools/bap/generate.ts +48 -43
  20. package/tools/bap/getCurrentAddress.ts +1 -3
  21. package/tools/bap/getId.test.ts +18 -10
  22. package/tools/bap/getId.ts +8 -6
  23. package/tools/bap/utils.ts +1 -1
  24. package/tools/bsocial/bmapReadPosts.ts +16 -2
  25. package/tools/bsocial/createPost.ts +21 -10
  26. package/tools/bsocial/readPosts.ts +10 -4
  27. package/tools/bsv/decodeTransaction.ts +1 -1
  28. package/tools/bsv/explore.ts +0 -4
  29. package/tools/bsv/getPrice.ts +1 -1
  30. package/tools/bsv/token.ts +14 -6
  31. package/tools/constants.ts +6 -2
  32. package/tools/index.ts +10 -10
  33. package/tools/mnee/getBalance.ts +1 -1
  34. package/tools/mnee/parseTx.ts +1 -1
  35. package/tools/mnee/sendMnee.ts +30 -8
  36. package/tools/ordinals/getInscription.ts +1 -3
  37. package/tools/ordinals/getTokenByIdOrTicker.ts +1 -3
  38. package/tools/ordinals/index.ts +4 -1
  39. package/tools/ordinals/marketListings.ts +1 -1
  40. package/tools/ordinals/marketSales.ts +1 -1
  41. package/tools/ordinals/searchInscriptions.ts +1 -1
  42. package/tools/utils/aip.ts +31 -22
  43. package/tools/utils/installAgentMaster.ts +8 -9
  44. package/tools/utils/toolRegistration.ts +21 -22
  45. package/tools/utils/transactionBuilder.ts +21 -14
  46. package/tools/wallet/a2bPublishMcp.ts +15 -5
  47. package/tools/wallet/brc100.ts +541 -310
  48. package/tools/wallet/cancelListing.ts +29 -37
  49. package/tools/wallet/createOrdinals.ts +43 -27
  50. package/tools/wallet/fetchPaymentUtxos.ts +7 -5
  51. package/tools/wallet/gatherCollectionInfo.ts +3 -5
  52. package/tools/wallet/getAddress.ts +1 -4
  53. package/tools/wallet/getBalance.ts +15 -15
  54. package/tools/wallet/{getBalanceDroplet.ts → getBalanceDroplit.ts} +6 -6
  55. package/tools/wallet/getBsv21Balances.ts +20 -14
  56. package/tools/wallet/getLockData.ts +11 -11
  57. package/tools/wallet/getOrdinals.ts +26 -20
  58. package/tools/wallet/integratedWallet.ts +55 -53
  59. package/tools/wallet/listOrdinal.ts +40 -39
  60. package/tools/wallet/listTokens.ts +18 -17
  61. package/tools/wallet/lockBsv.ts +36 -22
  62. package/tools/wallet/mintCollection.ts +15 -10
  63. package/tools/wallet/opnsDeregister.ts +29 -39
  64. package/tools/wallet/opnsRegister.ts +29 -39
  65. package/tools/wallet/purchaseListing.test.ts +27 -0
  66. package/tools/wallet/purchaseListing.ts +59 -40
  67. package/tools/wallet/refreshUtxos.ts +1 -1
  68. package/tools/wallet/schemas.ts +0 -13
  69. package/tools/wallet/sendAllBsv.ts +26 -20
  70. package/tools/wallet/sendBsv.ts +107 -0
  71. package/tools/wallet/setupDroplit.ts +157 -0
  72. package/tools/wallet/signBsm.ts +18 -18
  73. package/tools/wallet/sweepBsv.ts +48 -27
  74. package/tools/wallet/sweepBsv21.ts +42 -28
  75. package/tools/wallet/sweepOrdinals.ts +38 -26
  76. package/tools/wallet/tools.ts +11 -13
  77. package/tools/wallet/transactionHelper.ts +13 -5
  78. package/tools/wallet/transferOrdToken.ts +72 -84
  79. package/tools/wallet/unlockBsv.ts +23 -17
  80. package/tools/wallet/utxo.ts +1 -0
  81. package/tools/wallet/wallet.ts +1 -13
  82. package/tools/wallet/sendToAddress.ts +0 -74
  83. package/tools/wallet/setupDroplet.ts +0 -161
@@ -1,7 +1,5 @@
1
1
  import { PrivateKey } from "@bsv/sdk";
2
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { z } from "zod";
4
- import type { Wallet } from "../wallet/wallet";
5
3
 
6
4
  /**
7
5
  * Register the tool to get the BAP identity address
@@ -26,7 +24,7 @@ export function registerBapGetCurrentAddressTool(
26
24
  if (identityKeyWifEnv) {
27
25
  try {
28
26
  pkToUse = PrivateKey.fromWif(identityKeyWifEnv);
29
- } catch (e) {
27
+ } catch {
30
28
  // Let the final check handle the error
31
29
  }
32
30
  }
@@ -1,9 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
2
  import { fetchProfile } from "./getId";
3
3
  import type { IdentityData, SigmaIdentityProfile } from "./types";
4
+ import { SchemaType } from "./types";
4
5
 
5
6
  // Store the original fetch function
6
- const originalFetch = global.fetch;
7
+ const _originalFetch = global.fetch;
7
8
  let fetchMock: ReturnType<typeof spyOn>;
8
9
 
9
10
  describe("BAP getId - fetchProfile", () => {
@@ -21,18 +22,25 @@ describe("BAP getId - fetchProfile", () => {
21
22
  it("should return identity data for a valid idKey", async () => {
22
23
  const mockIdKey = "testIdKey";
23
24
  const mockIdentityData: IdentityData = {
24
- "@context": ["https://w3id.org/did/v0.11", "https://w3id.org/bap/v1"],
25
- id: `did:bap:id:${mockIdKey}`,
26
- publicKey: [
25
+ idKey: mockIdKey,
26
+ rootAddress: "1SomeRootAddress",
27
+ currentAddress: "1SomeCurrentAddress",
28
+ addresses: [
27
29
  {
28
- id: `did:bap:id:${mockIdKey}#root`,
29
- controller: `did:bap:id:${mockIdKey}`,
30
- type: "EcdsaSecp256k1VerificationKey2019",
31
- bitcoinAddress: "1SomeBitcoinAddress",
30
+ address: "1SomeCurrentAddress",
31
+ txId: "a1b2c3d4".repeat(8),
32
+ block: 800000,
32
33
  },
33
34
  ],
34
- authentication: ["#root"],
35
- assertionMethod: ["#root"],
35
+ block: 800000,
36
+ firstSeen: 799999,
37
+ timestamp: 1700000000,
38
+ valid: true,
39
+ identity: {
40
+ "@context": "https://schema.org",
41
+ "@type": SchemaType.Person,
42
+ alternateName: "Test User",
43
+ },
36
44
  };
37
45
  const mockApiResponse: SigmaIdentityProfile = { result: mockIdentityData };
38
46
 
@@ -82,14 +82,16 @@ export function registerBapGetIdTool(
82
82
 
83
83
  if (!targetIdKey) {
84
84
  // First priority: Use authenticated user's BAP ID from OAuth session
85
- const authInfo = (extra as any).authInfo;
86
- if (authInfo?.metadata?.bapId) {
87
- targetIdKey = authInfo.metadata.bapId;
85
+ const authExtra = extra.authInfo?.extra;
86
+ const authBapId = authExtra?.bapId;
87
+ const authPubkey = authExtra?.pubkey;
88
+ if (typeof authBapId === "string") {
89
+ targetIdKey = authBapId;
88
90
  console.error(`Using authenticated user's BAP ID: ${targetIdKey}`);
89
91
  }
90
92
  // Second priority: Use authenticated user's pubkey
91
- else if (authInfo?.metadata?.pubkey) {
92
- targetIdKey = authInfo.metadata.pubkey;
93
+ else if (typeof authPubkey === "string") {
94
+ targetIdKey = authPubkey;
93
95
  console.error(`Using authenticated user's pubkey: ${targetIdKey}`);
94
96
  }
95
97
  // Fallback: Attempt to get idKey from the passed identityPk or environment variable
@@ -101,7 +103,7 @@ export function registerBapGetIdTool(
101
103
  if (identityKeyWifEnv) {
102
104
  try {
103
105
  pkToUse = PrivateKey.fromWif(identityKeyWifEnv);
104
- } catch (e) {
106
+ } catch {
105
107
  // Don't error here, let it be handled by the final check
106
108
  }
107
109
  }
@@ -1,4 +1,4 @@
1
- import type { Identity, IdentityData, SchemaType } from "./types";
1
+ import type { Identity, SchemaType } from "./types";
2
2
 
3
3
  type AllFields = {
4
4
  schemaType: SchemaType;
@@ -111,9 +111,23 @@ export function registerBmapReadPostsTool(server: McpServer) {
111
111
  "bmap_readPosts",
112
112
  "Read social posts from the BMAP API (query layer). Can fetch posts by author (BAP ID), specific post by transaction ID, or recent posts from all users. Supports pagination and feed functionality.",
113
113
  { ...bmapReadPostsArgsSchema.shape },
114
- async ({ bapId, txid, limit, page, feed, address }): Promise<CallToolResult> => {
114
+ async ({
115
+ bapId,
116
+ txid,
117
+ limit,
118
+ page,
119
+ feed,
120
+ address,
121
+ }): Promise<CallToolResult> => {
115
122
  try {
116
- const result = await readBmapPosts({ bapId, txid, limit, page, feed, address });
123
+ const result = await readBmapPosts({
124
+ bapId,
125
+ txid,
126
+ limit,
127
+ page,
128
+ feed,
129
+ address,
130
+ });
117
131
 
118
132
  if (result.success) {
119
133
  return {
@@ -27,7 +27,9 @@ const createPostArgsSchema = z.object({
27
27
  additionalMapData: z
28
28
  .string()
29
29
  .optional()
30
- .describe("Additional MAP protocol key-value pairs as a JSON object string, e.g. '{\"key\": \"value\"}'"),
30
+ .describe(
31
+ 'Additional MAP protocol key-value pairs as a JSON object string, e.g. \'{"key": "value"}\'',
32
+ ),
31
33
  });
32
34
 
33
35
  type CreatePostArgs = z.infer<typeof createPostArgsSchema>;
@@ -48,7 +50,6 @@ export async function createSocialPost(
48
50
  const { content, contentType, app, additionalMapData } = args;
49
51
 
50
52
  // Get wallet info
51
- const address = await wallet.getAddress();
52
53
  const paymentKey = wallet.getPaymentKey();
53
54
  if (!paymentKey) {
54
55
  return {
@@ -56,6 +57,7 @@ export async function createSocialPost(
56
57
  error: "Payment key not available in wallet",
57
58
  };
58
59
  }
60
+ const address = paymentKey.toAddress();
59
61
 
60
62
  // Prepare file extension based on content type
61
63
  const fileExtension = contentType === "text/markdown" ? "md" : "txt";
@@ -71,7 +73,7 @@ export async function createSocialPost(
71
73
  ];
72
74
 
73
75
  // Build MAP protocol data
74
- const mapData: Uint8Array[] = [
76
+ const mapData: number[][] = [
75
77
  toArray(MAP_PREFIX, "utf8"),
76
78
  toArray("SET", "utf8"),
77
79
  toArray("app", "utf8"),
@@ -82,7 +84,8 @@ export async function createSocialPost(
82
84
 
83
85
  // Add any additional MAP data (parse JSON string if provided)
84
86
  if (additionalMapData) {
85
- const parsedMapData: Record<string, string> = JSON.parse(additionalMapData);
87
+ const parsedMapData: Record<string, string> =
88
+ JSON.parse(additionalMapData);
86
89
  for (const [key, value] of Object.entries(parsedMapData)) {
87
90
  mapData.push(toArray(key, "utf8"), toArray(value, "utf8"));
88
91
  }
@@ -96,20 +99,20 @@ export async function createSocialPost(
96
99
  const { signedData } = await signOpReturnWithAIP(
97
100
  dataToSign,
98
101
  paymentKey,
99
- address.toString(),
102
+ address,
100
103
  );
101
104
 
102
105
  // Create OP_RETURN script
103
106
  const script = buildOpReturnScript(signedData);
104
107
 
105
108
  // Get UTXOs
106
- const utxos = await wallet.getPaymentUtxos();
109
+ const { paymentUtxos } = await wallet.getUtxos();
107
110
 
108
111
  // Build and send transaction using the new utility
109
112
  return await buildAndSendTransaction({
110
113
  outputs: [{ script, satoshis: 0 }],
111
- utxos,
112
- changeAddress: address.toString(),
114
+ utxos: paymentUtxos,
115
+ changeAddress: address,
113
116
  paymentKey,
114
117
  });
115
118
  } catch (error) {
@@ -129,8 +132,16 @@ export function registerCreatePostTool(server: McpServer, wallet: Wallet) {
129
132
  "bsocial_createPost",
130
133
  "Create a social post on the BSV blockchain using B:// and MAP protocols. Posts are stored permanently on-chain and can include plain text or markdown content.",
131
134
  { ...createPostArgsSchema.shape },
132
- async ({ content, contentType, app, additionalMapData }): Promise<CallToolResult> => {
133
- const result = await createSocialPost({ content, contentType, app, additionalMapData }, wallet);
135
+ async ({
136
+ content,
137
+ contentType,
138
+ app,
139
+ additionalMapData,
140
+ }): Promise<CallToolResult> => {
141
+ const result = await createSocialPost(
142
+ { content, contentType, app, additionalMapData },
143
+ wallet,
144
+ );
134
145
  return createResponse(result);
135
146
  },
136
147
  );
@@ -83,9 +83,9 @@ export async function readSocialPosts(args: ReadPostsArgs): Promise<{
83
83
 
84
84
  return {
85
85
  success: true,
86
- posts: [data.result.post],
87
- signers: data.result.signers,
88
- meta: [data.result.meta],
86
+ posts: [data.data.post],
87
+ signers: data.data.signers,
88
+ meta: [data.data.meta],
89
89
  };
90
90
  }
91
91
 
@@ -201,7 +201,13 @@ export function registerReadPostsTool(server: McpServer) {
201
201
  { ...readPostsArgsSchema.shape },
202
202
  async ({ bapId, txid, limit, page, feed }): Promise<CallToolResult> => {
203
203
  try {
204
- const result = await readSocialPosts({ bapId, txid, limit, page, feed });
204
+ const result = await readSocialPosts({
205
+ bapId,
206
+ txid,
207
+ limit,
208
+ page,
209
+ feed,
210
+ });
205
211
 
206
212
  if (result.success && result.posts) {
207
213
  // Format posts for display
@@ -126,7 +126,7 @@ export function registerDecodeTransactionTool(server: McpServer): void {
126
126
  decodeTransactionArgsSchema.shape,
127
127
  async (
128
128
  { tx, encoding },
129
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
129
+ _extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
130
130
  ) => {
131
131
  try {
132
132
  let transaction: Transaction;
@@ -94,9 +94,6 @@ const exploreArgsSchema = z.object({
94
94
  .describe("Number of days for miner stats endpoints (defaults to 7)"),
95
95
  });
96
96
 
97
- // Type for the tool arguments
98
- type ExploreArgs = z.infer<typeof exploreArgsSchema>;
99
-
100
97
  /**
101
98
  * Register the bsv_explore tool with the MCP server
102
99
  * @param server The MCP server instance
@@ -134,7 +131,6 @@ export function registerExploreTool(server: McpServer): void {
134
131
  exploreArgsSchema.shape,
135
132
  async (params) => {
136
133
  try {
137
-
138
134
  // Validate required parameters for specific endpoints
139
135
  if (
140
136
  params.endpoint === ExploreEndpoint.BLOCK_BY_HASH &&
@@ -64,7 +64,7 @@ export function registerGetPriceTool(server: McpServer): void {
64
64
  },
65
65
  ],
66
66
  };
67
- } catch (err) {
67
+ } catch {
68
68
  return {
69
69
  content: [{ type: "text", text: "Error fetching BSV price." }],
70
70
  isError: true,
@@ -17,7 +17,9 @@ export function registerTokenTools(server: McpServer): void {
17
17
  server.tool(
18
18
  "bsv_toSatoshi",
19
19
  {
20
- bitcoin: z.string().describe("Bitcoin amount (accepts numeric strings like '1.5')"),
20
+ bitcoin: z
21
+ .string()
22
+ .describe("Bitcoin amount (accepts numeric strings like '1.5')"),
21
23
  returnType: z.enum(["number", "string", "bigint"]).optional(),
22
24
  },
23
25
  async ({ bitcoin, returnType }) => {
@@ -46,7 +48,9 @@ export function registerTokenTools(server: McpServer): void {
46
48
  server.tool(
47
49
  "bsv_toBitcoin",
48
50
  {
49
- satoshis: z.string().describe("Satoshi amount (accepts numeric strings like '100000000')"),
51
+ satoshis: z
52
+ .string()
53
+ .describe("Satoshi amount (accepts numeric strings like '100000000')"),
50
54
  returnType: z.enum(["number", "string", "bigint"]).optional(),
51
55
  },
52
56
  async ({ satoshis, returnType }) => {
@@ -58,7 +62,7 @@ export function registerTokenTools(server: McpServer): void {
58
62
  try {
59
63
  result = toBitcoin(satoshis, ReturnTypes.BigInt);
60
64
  return { content: [{ type: "text", text: result.toString() }] };
61
- } catch (e) {
65
+ } catch {
62
66
  return {
63
67
  content: [
64
68
  {
@@ -87,7 +91,9 @@ export function registerTokenTools(server: McpServer): void {
87
91
  server.tool(
88
92
  "bsv_toTokenSatoshi",
89
93
  {
90
- token: z.string().describe("Token amount (accepts numeric strings like '1.5')"),
94
+ token: z
95
+ .string()
96
+ .describe("Token amount (accepts numeric strings like '1.5')"),
91
97
  decimals: z.number().int().min(0),
92
98
  returnType: z.enum(["number", "string", "bigint"]).optional(),
93
99
  },
@@ -117,7 +123,9 @@ export function registerTokenTools(server: McpServer): void {
117
123
  server.tool(
118
124
  "bsv_toToken",
119
125
  {
120
- tokenSatoshi: z.string().describe("Token satoshi amount (accepts numeric strings)"),
126
+ tokenSatoshi: z
127
+ .string()
128
+ .describe("Token satoshi amount (accepts numeric strings)"),
121
129
  decimals: z.number().int().min(0),
122
130
  returnType: z.enum(["number", "string", "bigint"]).optional(),
123
131
  },
@@ -130,7 +138,7 @@ export function registerTokenTools(server: McpServer): void {
130
138
  try {
131
139
  result = toToken(tokenSatoshi, decimals, ReturnTypes.BigInt);
132
140
  return { content: [{ type: "text", text: result.toString() }] };
133
- } catch (e) {
141
+ } catch {
134
142
  return {
135
143
  content: [
136
144
  {
@@ -2,6 +2,8 @@
2
2
  * Constants for BSV MCP Tools
3
3
  */
4
4
 
5
+ import { AIP_BITCOM_ADDRESS } from "bsv-bap";
6
+
5
7
  /**
6
8
  * Market fee percentage applied to all marketplace purchases
7
9
  * Expressed as a decimal (e.g., 0.03 = 3%)
@@ -23,7 +25,9 @@ export const MINIMUM_MARKET_FEE_SATOSHIS = 10000; // 10000 satoshis = 0.0001 BSV
23
25
  export const BAP_PREFIX = "1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT";
24
26
  export const B_PREFIX = "19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut";
25
27
  export const MAP_PREFIX = "1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5";
26
- export const AIP_PREFIX = "1HPcP7a4kQjpJzyV4HWHKagon76KC3BsZA";
28
+ // Sourced from bsv-bap so it stays in step with the verifiers (bsv-bap,
29
+ // @1sat/templates, go-templates) rather than drifting from them.
30
+ export const AIP_PREFIX: string = AIP_BITCOM_ADDRESS;
27
31
  export const STORAGE_MNEMONIC_KEY = "id_mnemonic";
28
32
  export const PUBLIC_URL = process.env.PUBLIC_URL || "";
29
33
  export const BMAP_URL =
@@ -38,5 +42,5 @@ export const YOURS_INSTALL_URL =
38
42
  // process.env.PUBLIC_BSOCIAL_API_URL ||
39
43
  // "https://bsocial-overlay-production.up.railway.app/api/v1";
40
44
 
41
- export const BSOCIAL_API_URL = "https://api.sigmaidentity.com/v1";
45
+ export const BSOCIAL_API_URL = "https://sigma.1sat.app/1sat/bap";
42
46
  export const V5_API_URL = "https://ordinals.1sat.app/v5";
package/tools/index.ts CHANGED
@@ -9,9 +9,9 @@ import { registerBsvTools } from "./bsv";
9
9
  import { registerMneeTools } from "./mnee";
10
10
  import { registerOrdinalsTools } from "./ordinals";
11
11
  import { registerUtilsTools } from "./utils";
12
- import { registerWalletGetBalanceDropletTool } from "./wallet/getBalanceDroplet";
12
+ import { registerWalletGetBalanceDroplitTool } from "./wallet/getBalanceDroplit";
13
13
  import type { IntegratedWallet } from "./wallet/integratedWallet";
14
- import { registerSetupDropletTool } from "./wallet/setupDroplet";
14
+ import { registerSetupDroplitTools } from "./wallet/setupDroplit";
15
15
  import { registerWalletTools } from "./wallet/tools";
16
16
  import type { Wallet } from "./wallet/wallet";
17
17
 
@@ -116,16 +116,16 @@ export function registerAllTools(
116
116
 
117
117
  // Register Wallet tools themselves
118
118
  if (enableWalletTools) {
119
- if (config.integratedWallet?.isDropletMode) {
120
- // Register Droplet-mode wallet tools
121
- const dropletClient = config.integratedWallet.getDropletClient();
122
- if (dropletClient) {
123
- // Register Droplet-specific tools
124
- registerWalletGetBalanceDropletTool(server, dropletClient);
119
+ if (config.integratedWallet?.isDroplitMode) {
120
+ // Register Droplit-mode wallet tools
121
+ const droplitClient = config.integratedWallet.getDroplitClient();
122
+ if (droplitClient) {
123
+ // Register Droplit-specific tools
124
+ registerWalletGetBalanceDroplitTool(server, droplitClient);
125
125
  if (config.integratedWallet) {
126
- registerSetupDropletTool(server, config.integratedWallet);
126
+ registerSetupDroplitTools(server, config.integratedWallet);
127
127
  }
128
- console.error("Registered Droplet mode wallet tools");
128
+ console.error("Registered Droplit mode wallet tools");
129
129
  }
130
130
  } else if (config.wallet) {
131
131
  // Register normal wallet tools
@@ -22,7 +22,7 @@ export function registerGetBalanceTool(
22
22
  {},
23
23
  async (
24
24
  _params,
25
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
25
+ _extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
26
26
  ) => {
27
27
  try {
28
28
  // Get private key from wallet
@@ -27,7 +27,7 @@ export function registerParseTxTool(
27
27
  { ...parseTxArgsSchema.shape },
28
28
  async (
29
29
  { txid },
30
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
30
+ _extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
31
31
  ): Promise<CallToolResult> => {
32
32
  try {
33
33
  const result: ParseTxResponse = await mnee.parseTx(txid);
@@ -8,13 +8,18 @@ import type {
8
8
  import type Mnee from "mnee";
9
9
  import type { SendMNEE, TransferResponse } from "mnee";
10
10
  import { z } from "zod";
11
+ import { assertBroadcastAllowed } from "../../utils/broadcastGuard";
11
12
 
12
13
  /**
13
14
  * Schema for the sendMnee tool arguments.
14
15
  */
15
16
  export const sendMneeArgsSchema = z.object({
16
- address: z.string().describe("The recipient's address"),
17
- amount: z.number().describe("Amount to send"),
17
+ address: z.string().min(1).describe("The recipient's address"),
18
+ amount: z
19
+ .number()
20
+ .positive()
21
+ .finite()
22
+ .describe("Amount to send; must be greater than zero"),
18
23
  currency: z
19
24
  .enum(["MNEE", "USD"])
20
25
  .default("MNEE")
@@ -45,10 +50,13 @@ export function registerSendMneeTool(server: McpServer, mnee: Mnee): void {
45
50
  { ...sendMneeArgsSchema.shape },
46
51
  async (
47
52
  { address, amount, currency },
48
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
53
+ _extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
49
54
  ): Promise<CallToolResult> => {
50
55
  try {
51
- // Since 1 MNEE = $1, the amount is the same in both currencies
56
+ assertBroadcastAllowed("mnee_sendMnee");
57
+ // MNEE is dollar-pegged, so a USD amount transfers one-for-one. The
58
+ // currency the caller named is echoed in the response rather than
59
+ // dropped, so the interpretation is visible to whoever called.
52
60
  const mneeAmount = amount;
53
61
 
54
62
  const transferRequest: SendMNEE[] = [
@@ -81,13 +89,25 @@ export function registerSendMneeTool(server: McpServer, mnee: Mnee): void {
81
89
  };
82
90
  }
83
91
 
92
+ // transfer() throws on failure. On success it returns the cosigner's
93
+ // broadcast ticket id — the txid is only known once the ticket
94
+ // resolves, so it is read back via getTxStatus.
84
95
  const result: TransferResponse = await mnee.transfer(
85
96
  transferRequest,
86
97
  wif,
87
98
  );
88
99
 
89
- if (result.error) {
90
- throw new Error(result.error);
100
+ if (!result.ticketId) {
101
+ throw new Error(
102
+ "MNEE transfer returned no ticket id; the transaction was not submitted for broadcast.",
103
+ );
104
+ }
105
+
106
+ const status = await mnee.getTxStatus(result.ticketId);
107
+ if (status.status === "FAILED") {
108
+ throw new Error(
109
+ `MNEE transfer ${result.ticketId} failed: ${status.errors ?? "no error detail returned"}`,
110
+ );
91
111
  }
92
112
 
93
113
  return {
@@ -97,8 +117,10 @@ export function registerSendMneeTool(server: McpServer, mnee: Mnee): void {
97
117
  text: JSON.stringify(
98
118
  {
99
119
  success: true,
100
- txid: result.txid,
101
- rawtx: result.rawtx,
120
+ ticketId: result.ticketId,
121
+ txid: status.tx_id,
122
+ status: status.status,
123
+ requestedCurrency: currency,
102
124
  mneeAmount: mneeAmount,
103
125
  usdAmount: formatUSD(mneeAmount),
104
126
  recipient: address,
@@ -13,9 +13,7 @@ export function registerGetInscriptionTool(
13
13
  "ordinals_getInscription",
14
14
  "Retrieves metadata for an inscription by its outpoint. Returns content type, file info, origin, MAP data, and sequence info.",
15
15
  {
16
- outpoint: z
17
- .string()
18
- .describe("Outpoint in format 'txid_vout'"),
16
+ outpoint: z.string().describe("Outpoint in format 'txid_vout'"),
19
17
  },
20
18
  async ({ outpoint }) => {
21
19
  try {
@@ -13,9 +13,7 @@ export function registerGetTokenByIdOrTickerTool(
13
13
  "ordinals_getTokenByIdOrTicker",
14
14
  "Retrieves detailed information about a BSV21 token by its ID (txid_vout format). Returns token data including symbol, supply, decimals, funding status, and current state.",
15
15
  {
16
- id: z
17
- .string()
18
- .describe("BSV21 token ID in outpoint format (txid_vout)"),
16
+ id: z.string().describe("BSV21 token ID in outpoint format (txid_vout)"),
19
17
  },
20
18
  async ({ id }) => {
21
19
  try {
@@ -11,7 +11,10 @@ import { registerSearchInscriptionsTool } from "./searchInscriptions";
11
11
  * @param server The MCP server instance
12
12
  * @param ctx OneSat context for SDK service access
13
13
  */
14
- export function registerOrdinalsTools(server: McpServer, ctx?: OneSatContext): void {
14
+ export function registerOrdinalsTools(
15
+ server: McpServer,
16
+ ctx?: OneSatContext,
17
+ ): void {
15
18
  registerGetInscriptionTool(server, ctx);
16
19
  registerSearchInscriptionsTool(server, ctx);
17
20
  registerMarketListingsTool(server, ctx);
@@ -6,7 +6,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  */
7
7
  export function registerMarketListingsTool(
8
8
  server: McpServer,
9
- ctx?: OneSatContext,
9
+ _ctx?: OneSatContext,
10
10
  ): void {
11
11
  server.tool(
12
12
  "ordinals_marketListings",
@@ -6,7 +6,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  */
7
7
  export function registerMarketSalesTool(
8
8
  server: McpServer,
9
- ctx?: OneSatContext,
9
+ _ctx?: OneSatContext,
10
10
  ): void {
11
11
  server.tool(
12
12
  "ordinals_marketSales",
@@ -6,7 +6,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  */
7
7
  export function registerSearchInscriptionsTool(
8
8
  server: McpServer,
9
- ctx?: OneSatContext,
9
+ _ctx?: OneSatContext,
10
10
  ): void {
11
11
  server.tool(
12
12
  "ordinals_searchInscriptions",
@@ -1,45 +1,54 @@
1
- import { BSM, type PrivateKey, Utils } from "@bsv/sdk";
1
+ import { BigNumber, BSM, OP, type PrivateKey, Utils } from "@bsv/sdk";
2
2
  import { AIP_PREFIX } from "../constants";
3
3
 
4
- const { toArray, toBase64, toUTF8 } = Utils;
4
+ const { toArray } = Utils;
5
+
6
+ const PIPE = 0x7c;
5
7
 
6
8
  /**
7
9
  * Sign OP_RETURN data with AIP (Author Identity Protocol)
8
- * @param dataArrays - Array of data arrays to sign
10
+ *
11
+ * The signed message is the OP_RETURN opcode, then every push payload, then
12
+ * the `|` protocol separator — the reconstruction every AIP verifier performs
13
+ * (bsv-bap `verifyAttestationWithAIP`, `@1sat/templates` `AIP.validateAIP`,
14
+ * go-templates). The signature is written as raw compact bytes, not as base64
15
+ * text; verifiers base64-encode the pushdata themselves.
16
+ *
17
+ * @param dataArrays - Push payloads that follow OP_RETURN
9
18
  * @param signingKey - Private key to sign with
10
19
  * @param signingAddress - Address associated with the signing key
11
- * @returns Object containing signed data array
20
+ * @returns Object containing the payloads with the AIP fields appended
12
21
  */
13
22
  export async function signOpReturnWithAIP(
14
- dataArrays: Uint8Array[],
23
+ dataArrays: number[][],
15
24
  signingKey: PrivateKey,
16
25
  signingAddress: string,
17
- ): Promise<{ signedData: Uint8Array[] }> {
18
- // Concatenate all data arrays to create message to sign
19
- let messageToSign = new Uint8Array();
20
- for (const data of dataArrays) {
21
- const temp = new Uint8Array(messageToSign.length + data.length);
22
- temp.set(messageToSign);
23
- temp.set(data, messageToSign.length);
24
- messageToSign = temp;
25
- }
26
+ ): Promise<{ signedData: number[][] }> {
27
+ const messageToSign = [OP.OP_RETURN, ...dataArrays.flat(), PIPE];
26
28
 
27
- // Create BSM signature
28
- const signature = BSM.sign(messageToSign, signingKey);
29
+ const signature = BSM.sign(messageToSign, signingKey, "raw");
30
+ if (typeof signature === "string") {
31
+ throw new Error("BSM.sign returned a string in raw mode");
32
+ }
29
33
 
30
- // Convert signature to base64
31
- const signatureBase64 = toBase64(signature.toCompact());
34
+ const magicHash = new BigNumber(BSM.magicHash(messageToSign));
35
+ const recovery = signature.CalculateRecoveryFactor(
36
+ signingKey.toPublicKey(),
37
+ magicHash,
38
+ );
39
+ const compactSignature = signature.toCompact(recovery, true);
40
+ if (!Array.isArray(compactSignature)) {
41
+ throw new Error("Signature.toCompact returned a string without encoding");
42
+ }
32
43
 
33
- // Add AIP data to the arrays
34
- const aipData: Uint8Array[] = [
44
+ const aipData: number[][] = [
35
45
  toArray("|", "utf8"), // Separator
36
46
  toArray(AIP_PREFIX, "utf8"),
37
47
  toArray("BITCOIN_ECDSA", "utf8"),
38
48
  toArray(signingAddress, "utf8"),
39
- toArray(signatureBase64, "utf8"),
49
+ compactSignature,
40
50
  ];
41
51
 
42
- // Return combined data
43
52
  return {
44
53
  signedData: [...dataArrays, ...aipData],
45
54
  };