bsv-mcp 0.3.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # BSV MCP Server Changelog
2
2
 
3
+ ## [0.3.4] - 2026-09-06
4
+
5
+ ### Added
6
+ - Discover owner-listed Droplit sponsors without a wallet or preselected faucet.
7
+ - Validate public responses and pagination; discovery never authorizes access or payment.
8
+
9
+
10
+ ## [0.3.3] - 2026-09-06
11
+
12
+ ### Fixed
13
+ - Honor `DROPLIT_SITE_URL` for sponsor approval links so staging requests reach the correct owner UI.
14
+ - Build approval links from the configured sponsor and verified wallet identity; reject unsafe site origins.
15
+
3
16
  ## [0.3.2] - 2026-09-06
4
17
 
5
18
  ### Added
package/README.md CHANGED
@@ -884,17 +884,36 @@ BRC100_WALLET_URL=http://127.0.0.1:3321
884
884
  BRC100_WALLET_ORIGINATOR=bsv-mcp.local
885
885
  DROPLIT_API_URL=https://api.droplit.dev/droplit
886
886
  DROPLIT_FAUCET_NAME=your-sponsor-slug
887
+ # Optional: set explicitly when the owner approval UI is hosted elsewhere.
888
+ DROPLIT_SITE_URL=https://droplit.dev
887
889
  ```
888
890
 
889
- Set both sponsor values explicitly. The URL includes the server's configured
891
+ Set both sponsor values explicitly for wallet operations. The URL includes the server's configured
890
892
  base path. Leave `USE_DROPLIT_API` unset: these sponsor tools appear alongside
891
893
  normal wallet tools and use the same connected signer identity. Existing local
892
894
  BRC-100 wallet contexts also support this sponsor pair. `1sat serve` exposes a
893
895
  wallet stack service; it is not itself a BRC-100 signer RPC endpoint.
894
896
 
897
+ For public sponsor discovery, set only `DROPLIT_API_URL` and call
898
+ `droplit_discover` with optional `limit` (1–50, default 20) and `after`
899
+ (the previous `next_cursor`). This unsigned read needs no selected sponsor or
900
+ wallet and returns only names, slugs, and `approval_required: true`. It neither
901
+ creates a wallet nor grants sponsor access. Normal MCP startup retains its
902
+ existing wallet initialization behavior; this tool itself makes no wallet calls.
903
+ An empty catalog means no owners have opted in. Select a listed slug, configure
904
+ `DROPLIT_FAUCET_NAME`, and use the existing wallet access/owner approval flow.
905
+ Listing does not promise funding or quota availability.
906
+
895
907
  Call `droplit_getAccess` to inspect your own authorization and quotas. An
896
- `approval_required` response includes a `https://droplit.dev` link for manual
908
+ `approval_required` response includes a link to `DROPLIT_SITE_URL` (default
909
+ `https://droplit.dev`) for manual
897
910
  owner review. Do not automatically open, approve, or register to obtain access.
911
+ The site URL must be an HTTPS origin (HTTP is allowed only for loopback),
912
+ without credentials, a path, query, or fragment. For staging, set it to the
913
+ trusted staging frontend origin independently of `DROPLIT_API_URL`. Approval
914
+ links use the configured sponsor and authenticated wallet identity; a server
915
+ response cannot redirect them to another site.
916
+
898
917
  Public status and public-key registration do not prove sponsor approval, and
899
918
  creating your own faucet requires funding rather than providing free credit.
900
919
  Missing quota kinds are unrestricted for authorized users; a configured zero
package/dist/index.js CHANGED
@@ -256042,7 +256042,7 @@ var package_default = {
256042
256042
  name: "bsv-mcp",
256043
256043
  module: "dist/index.js",
256044
256044
  type: "module",
256045
- version: "0.3.2",
256045
+ version: "0.3.4",
256046
256046
  license: "MIT",
256047
256047
  author: "satchmo",
256048
256048
  description: "A collection of Bitcoin SV (BSV) tools for the Model Context Protocol (MCP) framework",
@@ -284877,6 +284877,23 @@ function registerUtilsTools(server) {
284877
284877
 
284878
284878
  // utils/droplit.ts
284879
284879
  init_mod2();
284880
+ function droplitApiBaseUrl(apiUrl) {
284881
+ const url3 = new URL(apiUrl);
284882
+ const loopback = url3.hostname === "localhost" || url3.hostname === "127.0.0.1" || url3.hostname === "[::1]";
284883
+ if (url3.username || url3.password || url3.search || url3.hash || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
284884
+ throw new Error("Droplit API requires HTTPS or HTTP loopback without credentials, query or fragment");
284885
+ }
284886
+ return url3.href.replace(/\/$/, "");
284887
+ }
284888
+ function approvalSiteOrigin(value2 = "https://droplit.dev") {
284889
+ const url3 = new URL(value2);
284890
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url3.hostname);
284891
+ if (url3.username || url3.password || url3.pathname !== "/" || url3.href.includes("?") || url3.href.includes("#") || value2.includes("\\") || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
284892
+ throw new Error("DROPLIT_SITE_URL requires an HTTPS or HTTP loopback origin without credentials, path, query or fragment");
284893
+ }
284894
+ return url3.origin;
284895
+ }
284896
+
284880
284897
  class DroplitError extends Error {
284881
284898
  code;
284882
284899
  status;
@@ -284926,16 +284943,14 @@ function httpFailure(status, details = {}) {
284926
284943
  class DroplitClient {
284927
284944
  config;
284928
284945
  authFetch;
284946
+ siteOrigin;
284929
284947
  wallet;
284930
284948
  constructor(config2) {
284931
284949
  this.config = config2;
284950
+ this.siteOrigin = approvalSiteOrigin(config2.siteUrl);
284932
284951
  if (config2.wallet && config2.authKey)
284933
284952
  throw new Error("Configure Droplit wallet or authKey, not both");
284934
- const url3 = new URL(config2.apiUrl);
284935
- const loopback = url3.hostname === "localhost" || url3.hostname === "127.0.0.1" || url3.hostname === "[::1]";
284936
- if (url3.username || url3.password || url3.search || url3.hash || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
284937
- throw new Error("Droplit API requires HTTPS or HTTP loopback without credentials, query or fragment");
284938
- }
284953
+ droplitApiBaseUrl(config2.apiUrl);
284939
284954
  if (!config2.faucetName.trim())
284940
284955
  throw new Error("Droplit faucet name is required");
284941
284956
  this.wallet = config2.wallet ?? (config2.authKey ? new ProtoWallet_default(config2.authKey) : undefined);
@@ -285023,6 +285038,7 @@ class DroplitClient {
285023
285038
  throw new DroplitError("invalid_response", "Sponsor access response did not match the authenticated wallet.");
285024
285039
  }
285025
285040
  access.approval_path = `/droplit/${encodeURIComponent(this.config.faucetName)}?tab=api&request_key=${encodeURIComponent(identity6)}`;
285041
+ access.approval_url = new URL(access.approval_path, this.siteOrigin).href;
285026
285042
  return access;
285027
285043
  }
285028
285044
  async fund(rawtx) {
@@ -285059,6 +285075,74 @@ class DroplitClient {
285059
285075
  return this.authed(path3, init);
285060
285076
  }
285061
285077
  }
285078
+ var publicSponsorsSchema = exports_external.object({
285079
+ sponsors: exports_external.array(exports_external.object({
285080
+ name: exports_external.string(),
285081
+ slug: exports_external.string().min(1),
285082
+ approval_required: exports_external.literal(true)
285083
+ })).max(50),
285084
+ next_cursor: exports_external.string().min(1).nullable()
285085
+ });
285086
+ async function discoverDroplitSponsors(apiUrl, options = {}) {
285087
+ const limit = options.limit ?? 20;
285088
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
285089
+ throw new Error("limit must be an integer from 1 to 50");
285090
+ const url3 = new URL(`${droplitApiBaseUrl(apiUrl)}/sponsors`);
285091
+ url3.searchParams.set("limit", String(limit));
285092
+ if (options.after !== undefined)
285093
+ url3.searchParams.set("after", options.after);
285094
+ let response;
285095
+ try {
285096
+ response = await fetch(url3, {
285097
+ method: "GET",
285098
+ redirect: "error",
285099
+ credentials: "omit",
285100
+ signal: AbortSignal.timeout(15000)
285101
+ });
285102
+ } catch {
285103
+ throw new DroplitError("request_failed", "Could not read public sponsors. Check the configured API origin.");
285104
+ }
285105
+ if (!response.ok)
285106
+ throw new DroplitError("request_failed", "Could not read public sponsors.", response.status);
285107
+ try {
285108
+ return publicSponsorsSchema.parse(await response.json());
285109
+ } catch {
285110
+ throw new DroplitError("invalid_response", "Public sponsor response was invalid.");
285111
+ }
285112
+ }
285113
+
285114
+ // tools/wallet/droplitDiscovery.ts
285115
+ function registerDroplitDiscoveryTool(server, apiUrl) {
285116
+ const baseUrl = droplitApiBaseUrl(apiUrl);
285117
+ server.registerTool("droplit_discover", {
285118
+ description: "List publicly opted-in sponsors. No wallet or sponsor selection is required. Listing grants no access or funding; choose a slug, then request the sponsor owner's approval separately.",
285119
+ inputSchema: {
285120
+ limit: exports_external.number().int().min(1).max(50).optional(),
285121
+ after: exports_external.string().optional()
285122
+ },
285123
+ annotations: {
285124
+ readOnlyHint: true,
285125
+ destructiveHint: false,
285126
+ idempotentHint: true,
285127
+ openWorldHint: true
285128
+ }
285129
+ }, async (options) => {
285130
+ try {
285131
+ const data = await discoverDroplitSponsors(baseUrl, options);
285132
+ return { ...createSuccessResponse(data), structuredContent: data };
285133
+ } catch (error52) {
285134
+ const data = {
285135
+ error: error52 instanceof DroplitError ? error52.code : "invalid_request",
285136
+ message: error52 instanceof DroplitError ? error52.message : "Invalid sponsor discovery request."
285137
+ };
285138
+ return {
285139
+ ...createSuccessResponse(data),
285140
+ structuredContent: data,
285141
+ isError: true
285142
+ };
285143
+ }
285144
+ });
285145
+ }
285062
285146
 
285063
285147
  // tools/wallet/droplit.ts
285064
285148
  function errorResult(error52) {
@@ -285093,7 +285177,7 @@ function registerDroplitTools(server, client, disableBroadcasting = false) {
285093
285177
  ...!access.authorized ? {
285094
285178
  error: "approval_required",
285095
285179
  message: "Ask the sponsor owner to approve this wallet. Copy the approval URL for manual review; do not auto-open or submit approval.",
285096
- approval_url: `https://droplit.dev${access.approval_path}`
285180
+ approval_url: access.approval_url
285097
285181
  } : {}
285098
285182
  };
285099
285183
  return {
@@ -295163,7 +295247,7 @@ var package_default2 = {
295163
295247
  name: "bsv-mcp",
295164
295248
  module: "dist/index.js",
295165
295249
  type: "module",
295166
- version: "0.3.2",
295250
+ version: "0.3.4",
295167
295251
  license: "MIT",
295168
295252
  author: "satchmo",
295169
295253
  description: "A collection of Bitcoin SV (BSV) tools for the Model Context Protocol (MCP) framework",
@@ -297896,6 +297980,9 @@ function registerAllTools(server, config2 = {}) {
297896
297980
  }
297897
297981
  if (enableUtilsTools) {
297898
297982
  registerUtilsTools(server);
297983
+ const apiUrl = config2.droplitApiUrl ?? process.env.DROPLIT_API_URL ?? config2.droplitClient?.getConfig().apiUrl;
297984
+ if (apiUrl)
297985
+ registerDroplitDiscoveryTool(server, apiUrl);
297899
297986
  }
297900
297987
  if (enableA2bTools) {
297901
297988
  registerA2bDiscoverTool(server);
@@ -298351,12 +298438,39 @@ init_mod2();
298351
298438
  function readDroplitSponsorConfig(env = process.env) {
298352
298439
  const apiUrl = env.DROPLIT_API_URL;
298353
298440
  const faucetName = env.DROPLIT_FAUCET_NAME;
298354
- if (apiUrl === undefined && faucetName === undefined)
298441
+ const siteUrl = env.DROPLIT_SITE_URL;
298442
+ if (apiUrl === undefined && faucetName === undefined && siteUrl === undefined)
298443
+ return;
298444
+ if (apiUrl?.trim() && faucetName === undefined) {
298445
+ droplitApiBaseUrl2(apiUrl);
298446
+ if (siteUrl !== undefined)
298447
+ approvalSiteOrigin2(siteUrl);
298355
298448
  return;
298449
+ }
298356
298450
  if (!apiUrl?.trim() || !faucetName?.trim()) {
298357
298451
  throw new Error("DROPLIT_API_URL and DROPLIT_FAUCET_NAME must both be explicitly configured");
298358
298452
  }
298359
- return { apiUrl, faucetName };
298453
+ return {
298454
+ apiUrl,
298455
+ faucetName,
298456
+ ...siteUrl === undefined ? {} : { siteUrl: approvalSiteOrigin2(siteUrl) }
298457
+ };
298458
+ }
298459
+ function droplitApiBaseUrl2(apiUrl) {
298460
+ const url3 = new URL(apiUrl);
298461
+ const loopback = url3.hostname === "localhost" || url3.hostname === "127.0.0.1" || url3.hostname === "[::1]";
298462
+ if (url3.username || url3.password || url3.search || url3.hash || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
298463
+ throw new Error("Droplit API requires HTTPS or HTTP loopback without credentials, query or fragment");
298464
+ }
298465
+ return url3.href.replace(/\/$/, "");
298466
+ }
298467
+ function approvalSiteOrigin2(value2 = "https://droplit.dev") {
298468
+ const url3 = new URL(value2);
298469
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url3.hostname);
298470
+ if (url3.username || url3.password || url3.pathname !== "/" || url3.href.includes("?") || url3.href.includes("#") || value2.includes("\\") || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
298471
+ throw new Error("DROPLIT_SITE_URL requires an HTTPS or HTTP loopback origin without credentials, path, query or fragment");
298472
+ }
298473
+ return url3.origin;
298360
298474
  }
298361
298475
 
298362
298476
  class DroplitError2 extends Error {
@@ -298408,16 +298522,14 @@ function httpFailure2(status, details = {}) {
298408
298522
  class DroplitClient2 {
298409
298523
  config;
298410
298524
  authFetch;
298525
+ siteOrigin;
298411
298526
  wallet;
298412
298527
  constructor(config2) {
298413
298528
  this.config = config2;
298529
+ this.siteOrigin = approvalSiteOrigin2(config2.siteUrl);
298414
298530
  if (config2.wallet && config2.authKey)
298415
298531
  throw new Error("Configure Droplit wallet or authKey, not both");
298416
- const url3 = new URL(config2.apiUrl);
298417
- const loopback = url3.hostname === "localhost" || url3.hostname === "127.0.0.1" || url3.hostname === "[::1]";
298418
- if (url3.username || url3.password || url3.search || url3.hash || !(url3.protocol === "https:" || url3.protocol === "http:" && loopback)) {
298419
- throw new Error("Droplit API requires HTTPS or HTTP loopback without credentials, query or fragment");
298420
- }
298532
+ droplitApiBaseUrl2(config2.apiUrl);
298421
298533
  if (!config2.faucetName.trim())
298422
298534
  throw new Error("Droplit faucet name is required");
298423
298535
  this.wallet = config2.wallet ?? (config2.authKey ? new ProtoWallet_default(config2.authKey) : undefined);
@@ -298505,6 +298617,7 @@ class DroplitClient2 {
298505
298617
  throw new DroplitError2("invalid_response", "Sponsor access response did not match the authenticated wallet.");
298506
298618
  }
298507
298619
  access.approval_path = `/droplit/${encodeURIComponent(this.config.faucetName)}?tab=api&request_key=${encodeURIComponent(identity6)}`;
298620
+ access.approval_url = new URL(access.approval_path, this.siteOrigin).href;
298508
298621
  return access;
298509
298622
  }
298510
298623
  async fund(rawtx) {
@@ -298541,6 +298654,14 @@ class DroplitClient2 {
298541
298654
  return this.authed(path5, init);
298542
298655
  }
298543
298656
  }
298657
+ var publicSponsorsSchema2 = exports_external.object({
298658
+ sponsors: exports_external.array(exports_external.object({
298659
+ name: exports_external.string(),
298660
+ slug: exports_external.string().min(1),
298661
+ approval_required: exports_external.literal(true)
298662
+ })).max(50),
298663
+ next_cursor: exports_external.string().min(1).nullable()
298664
+ });
298544
298665
 
298545
298666
  // utils/externalWalletConfig.ts
298546
298667
  function readExternalWalletConfig(env = process.env) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "bsv-mcp",
3
3
  "module": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.3.2",
5
+ "version": "0.3.4",
6
6
  "license": "MIT",
7
7
  "author": "satchmo",
8
8
  "description": "A collection of Bitcoin SV (BSV) tools for the Model Context Protocol (MCP) framework",
package/tools/index.ts CHANGED
@@ -10,6 +10,7 @@ import { registerBsvTools } from "./bsv";
10
10
  import { registerMneeTools } from "./mnee";
11
11
  import { registerOrdinalsTools } from "./ordinals";
12
12
  import { registerUtilsTools } from "./utils";
13
+ import { registerDroplitDiscoveryTool } from "./wallet/droplitDiscovery";
13
14
  import { registerDroplitTools } from "./wallet/droplit";
14
15
  import { registerWalletGetBalanceDroplitTool } from "./wallet/getBalanceDroplit";
15
16
  import type { IntegratedWallet } from "./wallet/integratedWallet";
@@ -48,6 +49,7 @@ export interface ToolsConfig {
48
49
  ctx?: OneSatContext;
49
50
  services?: OneSatServices;
50
51
  droplitClient?: DroplitClient;
52
+ droplitApiUrl?: string;
51
53
  }
52
54
 
53
55
  /**
@@ -94,6 +96,11 @@ export function registerAllTools(
94
96
  // Register utility tools
95
97
  if (enableUtilsTools) {
96
98
  registerUtilsTools(server);
99
+ const apiUrl =
100
+ config.droplitApiUrl ??
101
+ process.env.DROPLIT_API_URL ??
102
+ config.droplitClient?.getConfig().apiUrl;
103
+ if (apiUrl) registerDroplitDiscoveryTool(server, apiUrl);
97
104
  }
98
105
 
99
106
  // Register agent-to-blockchain tools
@@ -53,7 +53,7 @@ export function registerDroplitTools(
53
53
  error: "approval_required",
54
54
  message:
55
55
  "Ask the sponsor owner to approve this wallet. Copy the approval URL for manual review; do not auto-open or submit approval.",
56
- approval_url: `https://droplit.dev${access.approval_path}`,
56
+ approval_url: access.approval_url,
57
57
  }
58
58
  : {}),
59
59
  };
@@ -0,0 +1,51 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import {
4
+ discoverDroplitSponsors,
5
+ droplitApiBaseUrl,
6
+ DroplitError,
7
+ } from "../../utils/droplit";
8
+ import { createSuccessResponse } from "../utils/errorHandler";
9
+
10
+ export function registerDroplitDiscoveryTool(
11
+ server: McpServer,
12
+ apiUrl: string,
13
+ ) {
14
+ const baseUrl = droplitApiBaseUrl(apiUrl);
15
+ server.registerTool(
16
+ "droplit_discover",
17
+ {
18
+ description:
19
+ "List publicly opted-in sponsors. No wallet or sponsor selection is required. Listing grants no access or funding; choose a slug, then request the sponsor owner's approval separately.",
20
+ inputSchema: {
21
+ limit: z.number().int().min(1).max(50).optional(),
22
+ after: z.string().optional(),
23
+ },
24
+ annotations: {
25
+ readOnlyHint: true,
26
+ destructiveHint: false,
27
+ idempotentHint: true,
28
+ openWorldHint: true,
29
+ },
30
+ },
31
+ async (options) => {
32
+ try {
33
+ const data = await discoverDroplitSponsors(baseUrl, options);
34
+ return { ...createSuccessResponse(data), structuredContent: data };
35
+ } catch (error) {
36
+ const data = {
37
+ error: error instanceof DroplitError ? error.code : "invalid_request",
38
+ message:
39
+ error instanceof DroplitError
40
+ ? error.message
41
+ : "Invalid sponsor discovery request.",
42
+ };
43
+ return {
44
+ ...createSuccessResponse(data),
45
+ structuredContent: data,
46
+ isError: true,
47
+ };
48
+ }
49
+ },
50
+ );
51
+ }