pion-mcp 0.2.0 → 0.4.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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Arming logic and money-safety helpers for Tier C (A2U payments).
3
+ *
4
+ * This is the only part of Pion that can move value, so it is disabled unless
5
+ * every guard below passes. Possessing credentials is deliberately NOT enough
6
+ * to arm it: PION_ENABLE_PAYMENTS is a separate, explicit switch, and
7
+ * PION_MAX_PAYMENT_PI is a required ceiling that bounds worst-case loss no
8
+ * matter how the agent is steered.
9
+ *
10
+ * Neither the server API key nor the wallet secret is ever accepted as a tool
11
+ * argument, returned in a result, or logged.
12
+ */
13
+ import { z } from "zod";
14
+ import { type PiNetwork } from "./networks.js";
15
+ export interface PaymentsConfig {
16
+ serverApiKey: string;
17
+ walletSecret: string;
18
+ maxAmountStroops: bigint;
19
+ maxAmountPi: string;
20
+ }
21
+ export type PaymentsArming = {
22
+ armed: true;
23
+ config: PaymentsConfig;
24
+ } | {
25
+ armed: false;
26
+ reason: string;
27
+ };
28
+ /**
29
+ * Converts a decimal Pi amount to stroops exactly. Comparisons against the
30
+ * spend cap are done in integer stroops rather than floats — this is money,
31
+ * and 0.1 + 0.2 problems are not acceptable in a ceiling check.
32
+ */
33
+ export declare function toStroops(amount: string): bigint | null;
34
+ /**
35
+ * Metadata for a create-payment call, guaranteed non-empty.
36
+ *
37
+ * Pi rejects `POST /v2/payments` with `400 invalid_metadata` — "Metadata can't
38
+ * be empty" — when the field is `{}`. This is undocumented, and it is invisible
39
+ * in testing if every probe happens to pass something: ours did, so an omitted
40
+ * metadata argument stayed broken until the first real send (2026-08-01).
41
+ *
42
+ * The default carries provenance and nothing about the user.
43
+ */
44
+ export declare function paymentMetadata(supplied?: Record<string, unknown>): Record<string, unknown>;
45
+ /**
46
+ * Runtime shape check for the create-payment fields `send_payment` depends on.
47
+ *
48
+ * Parsed rather than cast, on purpose. An earlier version of this codebase
49
+ * declared the recipient wallet as `recipient` — a name Pi never returns — and
50
+ * a cast turned that into `undefined` at runtime instead of a type error. Every
51
+ * A2U payment would have failed while building the transaction, stranding a
52
+ * record each time, with nothing in the failure to point at the cause.
53
+ *
54
+ * Deliberately narrow: it covers only the fields actually read, so an unrelated
55
+ * addition to Pi's response never blocks a payment, while a rename of something
56
+ * load-bearing stops it before anything is signed.
57
+ *
58
+ * Field names verified against a live response (`npm run probe:a2u`, 2026-07-31).
59
+ */
60
+ export declare const createdPaymentSchema: z.ZodObject<{
61
+ identifier: z.ZodString;
62
+ to_address: z.ZodString;
63
+ from_address: z.ZodString;
64
+ amount: z.ZodNumber;
65
+ status: z.ZodObject<{
66
+ developer_approved: z.ZodBoolean;
67
+ cancelled: z.ZodBoolean;
68
+ }, z.core.$strip>;
69
+ }, z.core.$strip>;
70
+ export type CreatedPayment = z.infer<typeof createdPaymentSchema>;
71
+ export type ParsedCreate = {
72
+ ok: true;
73
+ payment: CreatedPayment;
74
+ } | {
75
+ ok: false;
76
+ issues: string;
77
+ identifier: string | undefined;
78
+ };
79
+ /**
80
+ * Validates a create-payment response.
81
+ *
82
+ * On failure it still digs the identifier out of the raw body if one is there:
83
+ * a record may exist even when the response cannot be understood, and a
84
+ * stranded payment with no id is far worse than one with an id.
85
+ */
86
+ export declare function parseCreatedPayment(raw: unknown): ParsedCreate;
87
+ /**
88
+ * Converts Pi's recorded amount to stroops for comparison against the request.
89
+ *
90
+ * Never via `String(amount)`: Pi returns amounts as JSON numbers and small ones
91
+ * arrive in exponential notation — a real `1e-7` was observed — which does not
92
+ * match the decimal pattern `toStroops` expects.
93
+ */
94
+ export declare function recordedAmountToStroops(amount: number): bigint;
95
+ /**
96
+ * Decides whether payments may run at all. Returns a specific reason on
97
+ * refusal so an operator can tell a missing switch from a missing credential.
98
+ */
99
+ export declare function checkPaymentsArming(network: PiNetwork): PaymentsArming;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Arming logic and money-safety helpers for Tier C (A2U payments).
3
+ *
4
+ * This is the only part of Pion that can move value, so it is disabled unless
5
+ * every guard below passes. Possessing credentials is deliberately NOT enough
6
+ * to arm it: PION_ENABLE_PAYMENTS is a separate, explicit switch, and
7
+ * PION_MAX_PAYMENT_PI is a required ceiling that bounds worst-case loss no
8
+ * matter how the agent is steered.
9
+ *
10
+ * Neither the server API key nor the wallet secret is ever accepted as a tool
11
+ * argument, returned in a result, or logged.
12
+ */
13
+ import { z } from "zod";
14
+ import { PI_TESTNET } from "./networks.js";
15
+ /** Stellar amounts carry 7 decimal places; 1 Pi = 10^7 stroops. */
16
+ const STROOPS_PER_PI = 10000000n;
17
+ const AMOUNT_PATTERN = /^\d+(\.\d{1,7})?$/;
18
+ /** Stellar public key: 56 base32 characters beginning with G. */
19
+ const STELLAR_ADDRESS = /^G[A-Z2-7]{55}$/;
20
+ /** Stellar secret seed: 56 base32 characters beginning with S. */
21
+ const SECRET_PATTERN = /^S[A-Z2-7]{55}$/;
22
+ /**
23
+ * Converts a decimal Pi amount to stroops exactly. Comparisons against the
24
+ * spend cap are done in integer stroops rather than floats — this is money,
25
+ * and 0.1 + 0.2 problems are not acceptable in a ceiling check.
26
+ */
27
+ export function toStroops(amount) {
28
+ if (!AMOUNT_PATTERN.test(amount))
29
+ return null;
30
+ const dot = amount.indexOf(".");
31
+ const whole = dot === -1 ? amount : amount.slice(0, dot);
32
+ const fraction = dot === -1 ? "" : amount.slice(dot + 1);
33
+ return BigInt(whole) * STROOPS_PER_PI + BigInt(fraction.padEnd(7, "0"));
34
+ }
35
+ /**
36
+ * Metadata for a create-payment call, guaranteed non-empty.
37
+ *
38
+ * Pi rejects `POST /v2/payments` with `400 invalid_metadata` — "Metadata can't
39
+ * be empty" — when the field is `{}`. This is undocumented, and it is invisible
40
+ * in testing if every probe happens to pass something: ours did, so an omitted
41
+ * metadata argument stayed broken until the first real send (2026-08-01).
42
+ *
43
+ * The default carries provenance and nothing about the user.
44
+ */
45
+ export function paymentMetadata(supplied) {
46
+ if (supplied && Object.keys(supplied).length > 0)
47
+ return supplied;
48
+ return { source: "pion-mcp" };
49
+ }
50
+ /**
51
+ * Runtime shape check for the create-payment fields `send_payment` depends on.
52
+ *
53
+ * Parsed rather than cast, on purpose. An earlier version of this codebase
54
+ * declared the recipient wallet as `recipient` — a name Pi never returns — and
55
+ * a cast turned that into `undefined` at runtime instead of a type error. Every
56
+ * A2U payment would have failed while building the transaction, stranding a
57
+ * record each time, with nothing in the failure to point at the cause.
58
+ *
59
+ * Deliberately narrow: it covers only the fields actually read, so an unrelated
60
+ * addition to Pi's response never blocks a payment, while a rename of something
61
+ * load-bearing stops it before anything is signed.
62
+ *
63
+ * Field names verified against a live response (`npm run probe:a2u`, 2026-07-31).
64
+ */
65
+ export const createdPaymentSchema = z.object({
66
+ identifier: z.string().min(1),
67
+ /** The recipient's wallet. Present on create — no separate lookup needed. */
68
+ to_address: z.string().regex(STELLAR_ADDRESS, "is not a Stellar public key"),
69
+ /**
70
+ * The wallet Pi expects to send from — the app wallet *selected* in the
71
+ * Developer Portal, which is not necessarily the one `PI_WALLET_SECRET`
72
+ * unlocks. Observed 2026-08-01: Pi returns the selected wallet here
73
+ * regardless of what key the app actually holds, so this is the only place
74
+ * the two can be compared before signing.
75
+ */
76
+ from_address: z.string().regex(STELLAR_ADDRESS, "is not a Stellar public key"),
77
+ /** Pi's record of the amount, to be cross-checked against what was asked. */
78
+ amount: z.number().finite(),
79
+ status: z.object({
80
+ developer_approved: z.boolean(),
81
+ cancelled: z.boolean(),
82
+ }),
83
+ });
84
+ /**
85
+ * Validates a create-payment response.
86
+ *
87
+ * On failure it still digs the identifier out of the raw body if one is there:
88
+ * a record may exist even when the response cannot be understood, and a
89
+ * stranded payment with no id is far worse than one with an id.
90
+ */
91
+ export function parseCreatedPayment(raw) {
92
+ const parsed = createdPaymentSchema.safeParse(raw);
93
+ if (parsed.success)
94
+ return { ok: true, payment: parsed.data };
95
+ const loose = raw?.identifier;
96
+ return {
97
+ ok: false,
98
+ issues: parsed.error.issues
99
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
100
+ .join("; "),
101
+ identifier: typeof loose === "string" && loose.length > 0 ? loose : undefined,
102
+ };
103
+ }
104
+ /**
105
+ * Converts Pi's recorded amount to stroops for comparison against the request.
106
+ *
107
+ * Never via `String(amount)`: Pi returns amounts as JSON numbers and small ones
108
+ * arrive in exponential notation — a real `1e-7` was observed — which does not
109
+ * match the decimal pattern `toStroops` expects.
110
+ */
111
+ export function recordedAmountToStroops(amount) {
112
+ return BigInt(Math.round(amount * Number(STROOPS_PER_PI)));
113
+ }
114
+ /**
115
+ * Decides whether payments may run at all. Returns a specific reason on
116
+ * refusal so an operator can tell a missing switch from a missing credential.
117
+ */
118
+ export function checkPaymentsArming(network) {
119
+ const enable = process.env.PION_ENABLE_PAYMENTS;
120
+ if (enable !== "1" && enable?.toLowerCase() !== "true") {
121
+ return {
122
+ armed: false,
123
+ reason: "PION_ENABLE_PAYMENTS is not set to 1 — payments are off by default",
124
+ };
125
+ }
126
+ // A2U is testnet-only per Pi's payments_advanced.md. Refuse anything else
127
+ // rather than discovering the restriction mid-flow with a created payment.
128
+ //
129
+ // Two conditions, deliberately. Until v0.4 this was a single substring test
130
+ // for "testnet" in the Horizon URL, which any string containing that word
131
+ // satisfied — https://api.mainnet.minepi.com/#testnet included. Arming now
132
+ // requires the resolved network to *be* Pi Testnet, and a custom endpoint
133
+ // resolves to `custom` however testnet-ish its URL looks.
134
+ if (!network.isTestnet) {
135
+ return {
136
+ armed: false,
137
+ reason: `the selected network is ${network.label} (${network.horizonUrl}), not Pi Testnet. ` +
138
+ "Pi restricts App-to-User payments to testnet, and Pion will not attempt them " +
139
+ "elsewhere. Payments cannot be armed while PION_NETWORK=mainnet or while " +
140
+ "PION_HORIZON_URL points at an endpoint Pion cannot identify.",
141
+ };
142
+ }
143
+ // Self-consistency assertion, not a second guess at the network. It cannot
144
+ // fire from any environment variable — only from someone editing the table in
145
+ // networks.ts so that `isTestnet` and the URL disagree.
146
+ if (network.horizonUrl !== PI_TESTNET.horizonUrl) {
147
+ return {
148
+ armed: false,
149
+ reason: `network table inconsistency: a network marked testnet points at ` +
150
+ `${network.horizonUrl}, not ${PI_TESTNET.horizonUrl}. Refusing to arm.`,
151
+ };
152
+ }
153
+ const serverApiKey = process.env.PI_SERVER_API_KEY;
154
+ if (!serverApiKey) {
155
+ return { armed: false, reason: "PI_SERVER_API_KEY is not set" };
156
+ }
157
+ const walletSecret = process.env.PI_WALLET_SECRET;
158
+ if (!walletSecret) {
159
+ return { armed: false, reason: "PI_WALLET_SECRET is not set" };
160
+ }
161
+ if (!SECRET_PATTERN.test(walletSecret)) {
162
+ // Never echo the value — say only that the shape is wrong.
163
+ return {
164
+ armed: false,
165
+ reason: "PI_WALLET_SECRET is not a valid Stellar secret seed (expected 56 characters starting with S)",
166
+ };
167
+ }
168
+ const rawCap = process.env.PION_MAX_PAYMENT_PI;
169
+ if (!rawCap) {
170
+ return {
171
+ armed: false,
172
+ reason: "PION_MAX_PAYMENT_PI is not set. A per-payment ceiling is required — " +
173
+ "it is what bounds the damage if the agent is manipulated.",
174
+ };
175
+ }
176
+ const maxAmountStroops = toStroops(rawCap.trim());
177
+ if (maxAmountStroops === null || maxAmountStroops <= 0n) {
178
+ return {
179
+ armed: false,
180
+ reason: `PION_MAX_PAYMENT_PI must be a positive decimal amount of Pi, got "${rawCap}"`,
181
+ };
182
+ }
183
+ return {
184
+ armed: true,
185
+ config: { serverApiKey, walletSecret, maxAmountStroops, maxAmountPi: rawCap.trim() },
186
+ };
187
+ }
@@ -22,4 +22,7 @@ export declare class PlatformAuthError extends Error {
22
22
  readonly status: number;
23
23
  constructor(message: string, status: number);
24
24
  }
25
+ /** Authenticated as a user, via their access token. */
25
26
  export declare function platformGet<T>(path: string, accessToken: string): Promise<T>;
27
+ /** Authenticated as the app, via the server API key. Server-side only. */
28
+ export declare function platformPostAsApp<T>(path: string, serverApiKey: string, body?: unknown): Promise<T>;
package/dist/platform.js CHANGED
@@ -32,16 +32,19 @@ export class PlatformAuthError extends Error {
32
32
  this.name = "PlatformAuthError";
33
33
  }
34
34
  }
35
- export async function platformGet(path, accessToken) {
35
+ async function platformRequest(method, path, auth, body) {
36
36
  const controller = new AbortController();
37
37
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
38
38
  let response;
39
39
  try {
40
40
  response = await fetch(`${PLATFORM_URL}${path}`, {
41
+ method,
41
42
  headers: {
42
- authorization: `Bearer ${accessToken}`,
43
+ authorization: `${auth.scheme} ${auth.credential}`,
43
44
  accept: "application/json",
45
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
44
46
  },
47
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
45
48
  signal: controller.signal,
46
49
  });
47
50
  }
@@ -56,12 +59,39 @@ export async function platformGet(path, accessToken) {
56
59
  clearTimeout(timer);
57
60
  }
58
61
  if (response.status === 401 || response.status === 403) {
62
+ // Do NOT assume a 401 means bad credentials. /v2/payments returns 401 with
63
+ // {"error":"missing_scope"} when the *recipient* has not granted
64
+ // wallet_address — nothing to do with the caller's key. The body carries
65
+ // the real reason, so read it before blaming the credential.
66
+ const raw = (await response.text().catch(() => "")).trim();
67
+ let apiError;
68
+ let apiMessage;
69
+ try {
70
+ const parsed = JSON.parse(raw);
71
+ apiError = parsed.error;
72
+ apiMessage = parsed.error_message;
73
+ }
74
+ catch {
75
+ // Empty or non-JSON body — fall back to the generic wording below.
76
+ }
77
+ if (apiError !== undefined) {
78
+ throw new PlatformAuthError(`Pi rejected the request (${response.status} ${apiError})` +
79
+ (apiMessage ? `: ${apiMessage}` : "") +
80
+ (apiError === "missing_scope"
81
+ ? "\n\nThis is a consent problem, not a credential problem. Your credentials are " +
82
+ "fine: the recipient has not authorized the required scope for your app. For " +
83
+ "A2U that is wallet_address, which lets Pi resolve their wallet.\n\n" +
84
+ "The verified way to obtain it is a Pi Browser SDK grant — the recipient runs " +
85
+ "Pi.authenticate for your app including wallet_address, inside the Pi Browser. " +
86
+ "Whether a Pi Sign-in grant also satisfies this is untested."
87
+ : ""), response.status);
88
+ }
89
+ const subject = auth.scheme === "Bearer" ? "access token" : "server API key";
59
90
  throw new PlatformAuthError(response.status === 401
60
- ? "The Pi Platform API rejected this access token. It is invalid, expired, or was issued for a different app."
61
- : "This access token is valid but lacks the scope required for this call.", response.status);
91
+ ? `The Pi Platform API rejected this ${subject}. It is invalid, expired, or was issued for a different app.`
92
+ : `This ${subject} is valid but lacks the permission required for this call.`, response.status);
62
93
  }
63
94
  if (!response.ok) {
64
- // 401 responses come back with an empty body, so never assume JSON here.
65
95
  const body = await response.text().catch(() => "");
66
96
  const detail = body.trim().slice(0, 300);
67
97
  throw new PlatformError(detail.length > 0
@@ -70,4 +100,11 @@ export async function platformGet(path, accessToken) {
70
100
  }
71
101
  return (await response.json());
72
102
  }
73
- //# sourceMappingURL=platform.js.map
103
+ /** Authenticated as a user, via their access token. */
104
+ export function platformGet(path, accessToken) {
105
+ return platformRequest("GET", path, { scheme: "Bearer", credential: accessToken });
106
+ }
107
+ /** Authenticated as the app, via the server API key. Server-side only. */
108
+ export function platformPostAsApp(path, serverApiKey, body) {
109
+ return platformRequest("POST", path, { scheme: "Key", credential: serverApiKey }, body);
110
+ }
@@ -1,5 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { PiNetwork } from "../networks.js";
4
+ /**
5
+ * The sentence every Tier A tool description ends with.
6
+ *
7
+ * Named once so all three read tools say the same thing about the same fact.
8
+ * An agent that only ever sees a tool description — never the startup banner —
9
+ * still learns which chain it is reading and why that matters.
10
+ *
11
+ * The wording is deliberate about what a wrong-chain read looks like. Measured
12
+ * 2026-08-14: some Pi addresses hold a balance on *both* chains, with different
13
+ * amounts (docs/FINDINGS.md, finding 5). So querying the wrong network does not
14
+ * reliably produce a not-found error — it can produce a well-formed, plausible,
15
+ * wrong number. That is why the network is stated rather than implied.
16
+ */
17
+ export declare function networkNote(network: PiNetwork): string;
3
18
  /** Stellar/Pi public key: 56 base32 characters beginning with G. */
4
19
  export declare const walletAddress: z.ZodString;
5
20
  /** Stellar transaction hash: 64 hex characters. */
@@ -1,6 +1,27 @@
1
1
  import { z } from "zod";
2
2
  import { HorizonError } from "../horizon.js";
3
3
  import { PlatformError } from "../platform.js";
4
+ /**
5
+ * The sentence every Tier A tool description ends with.
6
+ *
7
+ * Named once so all three read tools say the same thing about the same fact.
8
+ * An agent that only ever sees a tool description — never the startup banner —
9
+ * still learns which chain it is reading and why that matters.
10
+ *
11
+ * The wording is deliberate about what a wrong-chain read looks like. Measured
12
+ * 2026-08-14: some Pi addresses hold a balance on *both* chains, with different
13
+ * amounts (docs/FINDINGS.md, finding 5). So querying the wrong network does not
14
+ * reliably produce a not-found error — it can produce a well-formed, plausible,
15
+ * wrong number. That is why the network is stated rather than implied.
16
+ */
17
+ export function networkNote(network) {
18
+ return (`This server reads ${network.label}, and every result repeats that in its ` +
19
+ '"network" field — always report which chain a figure came from. Pi Mainnet ' +
20
+ "and Pi Testnet are separate ledgers sharing one address format, and the same " +
21
+ "address can hold different balances on each, so a result from the wrong chain " +
22
+ "looks entirely normal. Testnet Pi has no monetary value: never present a " +
23
+ "testnet balance as real holdings.");
24
+ }
4
25
  /** Stellar/Pi public key: 56 base32 characters beginning with G. */
5
26
  export const walletAddress = z
6
27
  .string()
@@ -43,4 +64,3 @@ export function fail(error) {
43
64
  : `Unexpected error: ${error instanceof Error ? error.message : String(error)}`;
44
65
  return { content: [{ type: "text", text: message }], isError: true };
45
66
  }
46
- //# sourceMappingURL=common.js.map
@@ -1,2 +1,3 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerGetAccountPayments(server: McpServer, network: string): void;
2
+ import type { PiNetwork } from "../networks.js";
3
+ export declare function registerGetAccountPayments(server: McpServer, network: PiNetwork): void;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { cursorFromLink, formatAsset, horizonGet } from "../horizon.js";
3
- import { fail, ok, pagingCursor, pagingLimit, pagingOrder, walletAddress } from "./common.js";
3
+ import { fail, networkNote, ok, pagingCursor, pagingLimit, pagingOrder, walletAddress, } from "./common.js";
4
4
  const paymentShape = z.object({
5
5
  id: z.string(),
6
6
  type: z.string(),
@@ -72,7 +72,8 @@ export function registerGetAccountPayments(server, network) {
72
72
  "a payment arrived, who funded an account, or what it recently sent. Covers " +
73
73
  "payments, account creations, path payments, and account merges. Results are " +
74
74
  "paginated: pass the returned `next_cursor` back as `cursor` for the next page. " +
75
- "Reads public ledger data only.",
75
+ "Reads public ledger data only. " +
76
+ networkNote(network),
76
77
  inputSchema: {
77
78
  address: walletAddress,
78
79
  limit: pagingLimit,
@@ -93,7 +94,7 @@ export function registerGetAccountPayments(server, network) {
93
94
  // set. Only surface a cursor when the page came back full.
94
95
  const nextCursor = payments.length === limit ? cursorFromLink(page._links?.next?.href) : undefined;
95
96
  return ok({
96
- network,
97
+ network: network.label,
97
98
  account_id: address,
98
99
  count: payments.length,
99
100
  ...(nextCursor !== undefined ? { next_cursor: nextCursor } : {}),
@@ -105,4 +106,3 @@ export function registerGetAccountPayments(server, network) {
105
106
  }
106
107
  });
107
108
  }
108
- //# sourceMappingURL=get-account-payments.js.map
@@ -1,2 +1,3 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerGetWalletBalance(server: McpServer, network: string): void;
2
+ import type { PiNetwork } from "../networks.js";
3
+ export declare function registerGetWalletBalance(server: McpServer, network: PiNetwork): void;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatAsset, horizonGet } from "../horizon.js";
3
- import { fail, ok, walletAddress } from "./common.js";
3
+ import { fail, networkNote, ok, walletAddress } from "./common.js";
4
4
  const outputSchema = {
5
5
  network: z.string(),
6
6
  account_id: z.string(),
@@ -21,7 +21,8 @@ export function registerGetWalletBalance(server, network) {
21
21
  description: "Read the current Pi and custom-token balances of a Pi wallet address. " +
22
22
  "Call this whenever you need to know how much Pi an address holds, whether it " +
23
23
  "holds a particular token, or whether the account exists on-chain at all. " +
24
- "Reads public ledger data only — it cannot move funds and needs no credentials.",
24
+ "Reads public ledger data only — it cannot move funds and needs no credentials. " +
25
+ networkNote(network),
25
26
  inputSchema: { address: walletAddress },
26
27
  outputSchema,
27
28
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -29,7 +30,7 @@ export function registerGetWalletBalance(server, network) {
29
30
  try {
30
31
  const account = await horizonGet(`/accounts/${address}`);
31
32
  return ok({
32
- network,
33
+ network: network.label,
33
34
  account_id: account.account_id,
34
35
  sequence: account.sequence,
35
36
  subentry_count: account.subentry_count,
@@ -51,4 +52,3 @@ export function registerGetWalletBalance(server, network) {
51
52
  }
52
53
  });
53
54
  }
54
- //# sourceMappingURL=get-wallet-balance.js.map
@@ -1,2 +1,3 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerQueryTransaction(server: McpServer, network: string): void;
2
+ import type { PiNetwork } from "../networks.js";
3
+ export declare function registerQueryTransaction(server: McpServer, network: PiNetwork): void;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { horizonGet } from "../horizon.js";
3
- import { fail, ok, transactionHash } from "./common.js";
3
+ import { fail, networkNote, ok, transactionHash } from "./common.js";
4
4
  const outputSchema = {
5
5
  network: z.string(),
6
6
  hash: z.string(),
@@ -23,7 +23,8 @@ export function registerQueryTransaction(server, network) {
23
23
  "which ledger it landed in, who submitted it, the fee charged, and its memo. " +
24
24
  "Call this to verify that a specific transaction actually went through — a user " +
25
25
  "or another service claiming a payment was made is not proof; this is. " +
26
- "Reads public ledger data only.",
26
+ "Reads public ledger data only. " +
27
+ networkNote(network),
27
28
  inputSchema: { hash: transactionHash },
28
29
  outputSchema,
29
30
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -31,7 +32,7 @@ export function registerQueryTransaction(server, network) {
31
32
  try {
32
33
  const tx = await horizonGet(`/transactions/${hash.toLowerCase()}`);
33
34
  return ok({
34
- network,
35
+ network: network.label,
35
36
  hash: tx.hash,
36
37
  successful: tx.successful,
37
38
  ledger: tx.ledger,
@@ -51,4 +52,3 @@ export function registerQueryTransaction(server, network) {
51
52
  }
52
53
  });
53
54
  }
54
- //# sourceMappingURL=query-transaction.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type PaymentsConfig } from "../payments.js";
3
+ export declare function registerSendPayment(server: McpServer, config: PaymentsConfig): void;