@agentwonderland/mcp 0.1.51 → 0.1.53

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 (39) hide show
  1. package/dist/core/__tests__/api-client.test.js +6 -2
  2. package/dist/core/__tests__/link-cli.test.d.ts +1 -0
  3. package/dist/core/__tests__/link-cli.test.js +102 -0
  4. package/dist/core/__tests__/principal.test.js +25 -0
  5. package/dist/core/api-client.js +4 -2
  6. package/dist/core/link-cli.d.ts +5 -0
  7. package/dist/core/link-cli.js +25 -25
  8. package/dist/core/principal.d.ts +1 -0
  9. package/dist/core/principal.js +49 -1
  10. package/dist/index.js +3 -0
  11. package/dist/tools/__tests__/rebates.test.d.ts +1 -0
  12. package/dist/tools/__tests__/rebates.test.js +72 -0
  13. package/dist/tools/__tests__/run.test.js +26 -0
  14. package/dist/tools/__tests__/wallet.test.js +1 -0
  15. package/dist/tools/index.d.ts +1 -0
  16. package/dist/tools/index.js +1 -0
  17. package/dist/tools/observability.d.ts +2 -0
  18. package/dist/tools/observability.js +20 -0
  19. package/dist/tools/rebates.d.ts +2 -0
  20. package/dist/tools/rebates.js +51 -0
  21. package/dist/tools/run.js +6 -0
  22. package/dist/tools/solve.js +8 -1
  23. package/dist/tools/wallet.js +4 -4
  24. package/package.json +1 -1
  25. package/src/core/__tests__/api-client.test.ts +8 -1
  26. package/src/core/__tests__/link-cli.test.ts +125 -0
  27. package/src/core/__tests__/principal.test.ts +31 -0
  28. package/src/core/api-client.ts +4 -2
  29. package/src/core/link-cli.ts +25 -27
  30. package/src/core/principal.ts +54 -1
  31. package/src/index.ts +3 -0
  32. package/src/tools/__tests__/rebates.test.ts +91 -0
  33. package/src/tools/__tests__/run.test.ts +33 -0
  34. package/src/tools/__tests__/wallet.test.ts +1 -0
  35. package/src/tools/index.ts +1 -0
  36. package/src/tools/rebates.ts +102 -0
  37. package/src/tools/run.ts +7 -0
  38. package/src/tools/solve.ts +8 -1
  39. package/src/tools/wallet.ts +4 -4
@@ -6,7 +6,7 @@ import { getSettings } from "../core/settings.js";
6
6
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
7
7
  import { getLinkCliAuthStatus, listLinkPaymentMethods, openLinkPaymentMethodAdd, startLinkCliLogin, } from "../core/link-cli.js";
8
8
  import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, installOws, platformSupportsOws, } from "../core/ows-adapter.js";
9
- import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
9
+ import { ensureConsumerPrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipal } from "../core/principal.js";
10
10
  import { MCP_PACKAGE_VERSION } from "../core/version.js";
11
11
  function text(t) {
12
12
  return { content: [{ type: "text", text: t }] };
@@ -221,7 +221,7 @@ export function registerWalletTools(server) {
221
221
  paymentMethodId: selected.id,
222
222
  label: name ?? selected.label,
223
223
  });
224
- const principal = await ensureConsumerPrincipal();
224
+ const principal = await ensureConsumerPrincipalForMethod("link");
225
225
  return text([
226
226
  "Link payment method connected.",
227
227
  ` Payment method: ${selected.label ?? selected.id}${selected.label ? ` (${selected.id})` : ""}`,
@@ -254,7 +254,7 @@ export function registerWalletTools(server) {
254
254
  paymentMethodId: method.id,
255
255
  label: method.label,
256
256
  });
257
- const principal = await ensureConsumerPrincipal();
257
+ const principal = await ensureConsumerPrincipalForMethod("link");
258
258
  return text([
259
259
  "Link payment method connected.",
260
260
  ` Payment method: ${method.id}${method.label ? ` (${method.label})` : ""}`,
@@ -293,7 +293,7 @@ export function registerWalletTools(server) {
293
293
  if (pendingToken) {
294
294
  const result = await pollCardSetup(pendingToken, 250);
295
295
  if (result) {
296
- const principal = await ensureConsumerPrincipal();
296
+ const principal = await ensureConsumerPrincipalForMethod("card");
297
297
  return text(`Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
298
298
  `Consumer principal: ${principal}`);
299
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -1,10 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { MCP_PACKAGE_VERSION } from "../version.js";
2
3
 
3
4
  const {
4
5
  mockGetApiUrl,
5
6
  mockGetApiKey,
6
7
  mockGetPaymentFetch,
7
8
  mockEnsureConsumerPrincipalForMethod,
9
+ mockEnsureBaseRebatePrincipal,
8
10
  mockGetConsumerPrincipalForMethod,
9
11
  mockGetBaseRebatePrincipal,
10
12
  mockPaymentFetch,
@@ -13,6 +15,7 @@ const {
13
15
  mockGetApiKey: vi.fn(),
14
16
  mockGetPaymentFetch: vi.fn(),
15
17
  mockEnsureConsumerPrincipalForMethod: vi.fn(),
18
+ mockEnsureBaseRebatePrincipal: vi.fn(),
16
19
  mockGetConsumerPrincipalForMethod: vi.fn(),
17
20
  mockGetBaseRebatePrincipal: vi.fn(),
18
21
  mockPaymentFetch: vi.fn(),
@@ -29,6 +32,7 @@ vi.mock("../payments.js", () => ({
29
32
 
30
33
  vi.mock("../principal.js", () => ({
31
34
  ensureConsumerPrincipal: vi.fn(),
35
+ ensureBaseRebatePrincipal: (...args: unknown[]) => mockEnsureBaseRebatePrincipal(...args),
32
36
  ensureConsumerPrincipalForMethod: (...args: unknown[]) => mockEnsureConsumerPrincipalForMethod(...args),
33
37
  getConsumerPrincipal: vi.fn(),
34
38
  getConsumerPrincipalForMethod: (...args: unknown[]) => mockGetConsumerPrincipalForMethod(...args),
@@ -50,6 +54,9 @@ describe("api-client headers", () => {
50
54
  mockGetBaseRebatePrincipal.mockResolvedValue(
51
55
  "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
52
56
  );
57
+ mockEnsureBaseRebatePrincipal.mockResolvedValue(
58
+ "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
59
+ );
53
60
  mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
54
61
  status: 200,
55
62
  headers: { "content-type": "application/json" },
@@ -72,7 +79,7 @@ describe("api-client headers", () => {
72
79
  "X-AW-Rebate-Principal":
73
80
  "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
74
81
  "X-AW-Surface": "mcp",
75
- "X-AW-MCP-Version": "0.1.44",
82
+ "X-AW-MCP-Version": MCP_PACKAGE_VERSION,
76
83
  "X-AW-MCP-Tool": "run_agent",
77
84
  "X-AW-MCP-Action": "execute",
78
85
  }),
@@ -0,0 +1,125 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { PendingLinkSpendRequest } from "../config.js";
3
+
4
+ const {
5
+ execCalls,
6
+ outputs,
7
+ state,
8
+ mockExecFile,
9
+ } = vi.hoisted(() => {
10
+ const execCalls: Array<{ file: string; args: string[] }> = [];
11
+ const outputs: unknown[] = [];
12
+ const state: { pending: PendingLinkSpendRequest | null; pendingWrites: Array<PendingLinkSpendRequest | null> } = {
13
+ pending: null,
14
+ pendingWrites: [],
15
+ };
16
+
17
+ type MockExecFile = ((file: string, args: string[], options: unknown, callback: (err: Error | null, stdout?: string, stderr?: string) => void) => void) & {
18
+ [key: symbol]: unknown;
19
+ };
20
+ const mockExecFile = vi.fn((file: string, args: string[], _options: unknown, callback: (err: Error | null, stdout?: string, stderr?: string) => void) => {
21
+ execCalls.push({ file, args });
22
+ const next = outputs.shift();
23
+ if (next instanceof Error) {
24
+ callback(next);
25
+ return;
26
+ }
27
+ callback(null, JSON.stringify(next ?? null), "");
28
+ }) as unknown as MockExecFile;
29
+ mockExecFile[Symbol.for("nodejs.util.promisify.custom")] = async (file: string, args: string[]) => {
30
+ execCalls.push({ file, args });
31
+ const next = outputs.shift();
32
+ if (next instanceof Error) {
33
+ throw next;
34
+ }
35
+ return { stdout: JSON.stringify(next ?? null), stderr: "" };
36
+ };
37
+
38
+ return { execCalls, outputs, state, mockExecFile };
39
+ });
40
+
41
+ vi.mock("node:child_process", () => ({
42
+ execFile: mockExecFile,
43
+ }));
44
+
45
+ vi.mock("../config.js", () => ({
46
+ getPendingLinkSpendRequest: () => state.pending,
47
+ setPendingLinkSpendRequest: (pending: PendingLinkSpendRequest | null) => {
48
+ state.pending = pending;
49
+ state.pendingWrites.push(pending);
50
+ },
51
+ setLinkCooldown: vi.fn(),
52
+ }));
53
+
54
+ describe("Link CLI spend requests", () => {
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ execCalls.length = 0;
58
+ outputs.length = 0;
59
+ state.pending = null;
60
+ state.pendingWrites = [];
61
+ });
62
+
63
+ it("returns an approval-required error immediately instead of blocking on Link approval", async () => {
64
+ outputs.push([
65
+ {
66
+ id: "lsrq_test_123",
67
+ approval_url: "https://link.example/approve/lsrq_test_123",
68
+ status: "pending_approval",
69
+ },
70
+ ]);
71
+
72
+ const { createLinkSharedPaymentToken, LinkApprovalRequiredError } = await import("../link-cli.js");
73
+
74
+ await expect(createLinkSharedPaymentToken({
75
+ amount: "10",
76
+ currency: "usd",
77
+ context: "Agent Wonderland test",
78
+ expiresAt: Math.floor(Date.now() / 1000) + 300,
79
+ networkId: "profile_test",
80
+ paymentMethodId: "csmrpd_test_123",
81
+ })).rejects.toBeInstanceOf(LinkApprovalRequiredError);
82
+
83
+ expect(execCalls).toHaveLength(1);
84
+ expect(execCalls[0]?.args).toContain("create");
85
+ expect(state.pendingWrites[0]).toMatchObject({
86
+ id: "lsrq_test_123",
87
+ approvalUrl: "https://link.example/approve/lsrq_test_123",
88
+ amount: "10",
89
+ currency: "usd",
90
+ networkId: "profile_test",
91
+ paymentMethodId: "csmrpd_test_123",
92
+ });
93
+ });
94
+
95
+ it("resumes a stored Link spend request and returns the approved shared payment token", async () => {
96
+ const expiresAt = Math.floor(Date.now() / 1000) + 300;
97
+ state.pending = {
98
+ id: "lsrq_test_123",
99
+ approvalUrl: "https://link.example/approve/lsrq_test_123",
100
+ amount: "10",
101
+ currency: "usd",
102
+ context: "Agent Wonderland test",
103
+ expiresAt,
104
+ networkId: "profile_test",
105
+ paymentMethodId: "csmrpd_test_123",
106
+ createdAt: new Date().toISOString(),
107
+ };
108
+ outputs.push({ shared_payment_token: "spt_test_123" });
109
+
110
+ const { createLinkSharedPaymentToken } = await import("../link-cli.js");
111
+
112
+ await expect(createLinkSharedPaymentToken({
113
+ amount: "10",
114
+ currency: "usd",
115
+ context: "Agent Wonderland test",
116
+ expiresAt,
117
+ networkId: "profile_test",
118
+ paymentMethodId: "csmrpd_test_123",
119
+ })).resolves.toBe("spt_test_123");
120
+
121
+ expect(execCalls).toHaveLength(1);
122
+ expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
123
+ expect(state.pendingWrites).toEqual([null]);
124
+ });
125
+ });
@@ -137,6 +137,22 @@ describe("consumer principal helpers", () => {
137
137
  expect(state.addedWallets).toHaveLength(1);
138
138
  });
139
139
 
140
+ it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
141
+ state.wallets = [
142
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
143
+ ];
144
+ state.addresses = {
145
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
146
+ };
147
+
148
+ const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
149
+ const principal = await ensureConsumerPrincipalForMethod("link");
150
+
151
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
152
+ expect(state.addedWallets).toHaveLength(1);
153
+ expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
154
+ });
155
+
140
156
  it("returns the Base rebate principal when an EVM wallet is available", async () => {
141
157
  state.wallets = [
142
158
  { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
@@ -152,4 +168,19 @@ describe("consumer principal helpers", () => {
152
168
 
153
169
  expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
154
170
  });
171
+
172
+ it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
173
+ state.wallets = [
174
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
175
+ ];
176
+ state.addresses = {
177
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
178
+ };
179
+
180
+ const { ensureBaseRebatePrincipal } = await import("../principal.js");
181
+ const principal = await ensureBaseRebatePrincipal();
182
+
183
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
184
+ expect(state.addedWallets).toHaveLength(1);
185
+ });
155
186
  });
@@ -2,7 +2,7 @@ import { getApiUrl, getApiKey } from "./config.js";
2
2
  import { getPaymentFetch } from "./payments.js";
3
3
  import {
4
4
  getBaseRebatePrincipal,
5
- ensureConsumerPrincipal,
5
+ ensureBaseRebatePrincipal,
6
6
  ensureConsumerPrincipalForMethod,
7
7
  getConsumerPrincipal,
8
8
  getConsumerPrincipalForMethod,
@@ -70,7 +70,9 @@ async function buildHeaders(path: string, method: string, options?: RequestOptio
70
70
  headers["X-AW-Consumer-Principal"] = principal;
71
71
  }
72
72
 
73
- const rebatePrincipal = await getBaseRebatePrincipal();
73
+ const rebatePrincipal = options?.ensureConsumerPrincipal
74
+ ? await ensureBaseRebatePrincipal()
75
+ : await getBaseRebatePrincipal();
74
76
  if (rebatePrincipal) {
75
77
  headers["X-AW-Rebate-Principal"] = rebatePrincipal;
76
78
  }
@@ -11,8 +11,23 @@ const execFileAsync = promisify(execFile);
11
11
  const LINK_CLI_PACKAGE = "@stripe/link-cli";
12
12
  const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
13
13
 
14
- function sleep(ms: number): Promise<void> {
15
- return new Promise((resolve) => setTimeout(resolve, ms));
14
+ export class LinkApprovalRequiredError extends Error {
15
+ constructor(
16
+ public readonly spendRequestId: string,
17
+ public readonly approvalUrl?: string,
18
+ ) {
19
+ super(formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl));
20
+ this.name = "LinkApprovalRequiredError";
21
+ }
22
+ }
23
+
24
+ function formatLinkApprovalRequiredMessage(spendRequestId: string, approvalUrl?: string): string {
25
+ return [
26
+ "Link approval required.",
27
+ "The agent has not run yet and no charge has been captured.",
28
+ approvalUrl ? `Approve this spend request in Link: ${approvalUrl}` : `Spend request: ${spendRequestId}`,
29
+ "After approving, rerun the same tool call with confirmed: true.",
30
+ ].join("\n");
16
31
  }
17
32
 
18
33
  export interface LinkCliAuthStatus {
@@ -264,6 +279,9 @@ export async function createLinkSharedPaymentToken(params: {
264
279
  return spt;
265
280
  } catch (err) {
266
281
  const message = err instanceof Error ? err.message : String(err);
282
+ if (err instanceof LinkApprovalRequiredError) {
283
+ throw err;
284
+ }
267
285
  if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
268
286
  throw err;
269
287
  }
@@ -344,9 +362,7 @@ export async function createLinkSharedPaymentToken(params: {
344
362
  if (approval.approvalUrl) {
345
363
  console.error(`Link approval required: ${approval.approvalUrl}`);
346
364
  }
347
- const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
348
- setPendingLinkSpendRequest(null);
349
- return retrievedSpt;
365
+ throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
350
366
  }
351
367
 
352
368
  {
@@ -355,32 +371,14 @@ export async function createLinkSharedPaymentToken(params: {
355
371
  }
356
372
 
357
373
  async function retrieveSharedPaymentToken(spendRequestId: string, approvalUrl?: string): Promise<string> {
358
- let retrieved = await runLinkCli([
374
+ const retrieved = await runLinkCli([
359
375
  "spend-request",
360
376
  "retrieve",
361
377
  spendRequestId,
362
- "--interval",
363
- "2",
364
- "--max-attempts",
365
- "150",
366
- ]);
367
- let retrievedSpt = extractSharedPaymentToken(retrieved);
368
- for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
369
- await sleep(2_000);
370
- retrieved = await runLinkCli([
371
- "spend-request",
372
- "retrieve",
373
- spendRequestId,
374
- ]);
375
- retrievedSpt = extractSharedPaymentToken(retrieved);
376
- }
378
+ ], 30_000);
379
+ const retrievedSpt = extractSharedPaymentToken(retrieved);
377
380
  if (retrievedSpt) {
378
381
  return retrievedSpt;
379
382
  }
380
- throw new Error(
381
- [
382
- "Link spend request finished without a shared payment token.",
383
- approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
384
- ].filter(Boolean).join("\n"),
385
- );
383
+ throw new LinkApprovalRequiredError(spendRequestId, approvalUrl);
386
384
  }
@@ -80,6 +80,43 @@ async function createFallbackIdentityWallet(): Promise<WalletEntry> {
80
80
  return entry;
81
81
  }
82
82
 
83
+ async function ensureEvmIdentityWallet(): Promise<WalletEntry> {
84
+ const existing = preferredWallets().find((wallet) => walletSupportsEvm(wallet));
85
+ if (existing) return existing;
86
+
87
+ if (await isOwsAvailable()) {
88
+ const existingOwsWallets = await listOwsWallets();
89
+ const linked = existingOwsWallets[0];
90
+ if (linked) {
91
+ const entry: WalletEntry = {
92
+ id: linked.name,
93
+ keyType: "ows",
94
+ owsWalletId: linked.id,
95
+ chains: ["tempo", "base"],
96
+ defaultChain: "base",
97
+ label: linked.name,
98
+ };
99
+ addWallet(entry);
100
+ return entry;
101
+ }
102
+
103
+ const walletName = `aw-identity-${Date.now()}`;
104
+ const created = await createOwsWallet(walletName, "evm");
105
+ const entry: WalletEntry = {
106
+ id: walletName,
107
+ keyType: "ows",
108
+ owsWalletId: created.walletId,
109
+ chains: ["tempo", "base"],
110
+ defaultChain: "base",
111
+ label: AUTO_IDENTITY_LABEL,
112
+ };
113
+ addWallet(entry);
114
+ return entry;
115
+ }
116
+
117
+ return createFallbackIdentityWallet();
118
+ }
119
+
83
120
  async function ensureIdentityWallet(): Promise<WalletEntry> {
84
121
  const wallets = preferredWallets();
85
122
  const existing = wallets.find((wallet) => walletSupportsEvm(wallet) || walletSupportsSolana(wallet));
@@ -131,9 +168,21 @@ export async function getBaseRebatePrincipal(): Promise<string | null> {
131
168
  return (await principalForChain("base")) ?? principalForChain("tempo");
132
169
  }
133
170
 
171
+ export async function ensureBaseRebatePrincipal(): Promise<string> {
172
+ const existing = await getBaseRebatePrincipal();
173
+ if (existing) return existing;
174
+
175
+ const wallet = await ensureEvmIdentityWallet();
176
+ const principal = await walletPrincipal(wallet);
177
+ if (!principal || !principal.startsWith(`did:pkh:eip155:${BASE_CHAIN_ID}:`)) {
178
+ throw new Error("Could not derive a Base rebate principal from the configured identity wallet.");
179
+ }
180
+ return principal;
181
+ }
182
+
134
183
  export async function getConsumerPrincipalForMethod(method?: string): Promise<string | null> {
135
184
  if (!method || method === "card" || method === "link") {
136
- return getConsumerPrincipal();
185
+ return getBaseRebatePrincipal();
137
186
  }
138
187
 
139
188
  const resolved = resolveWalletAndChain(method);
@@ -158,6 +207,10 @@ export async function ensureConsumerPrincipalForMethod(method?: string): Promise
158
207
  const existing = await getConsumerPrincipalForMethod(method);
159
208
  if (existing) return existing;
160
209
 
210
+ if (!method || method === "card" || method === "link") {
211
+ return ensureBaseRebatePrincipal();
212
+ }
213
+
161
214
  if (method && method !== "card" && method !== "link") {
162
215
  throw new Error(
163
216
  `Could not derive a consumer principal for payment method "${method}". ` +
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { registerWalletTools } from "./tools/wallet.js";
14
14
  import { registerFavoriteTools } from "./tools/favorites.js";
15
15
  import { registerTipTools } from "./tools/tip.js";
16
16
  import { registerPassTools } from "./tools/passes.js";
17
+ import { registerRebateTools } from "./tools/rebates.js";
17
18
  import { registerUploadTools } from "./tools/upload.js";
18
19
  import { registerProbeTools } from "./tools/probe.js";
19
20
  import { registerProviderTools } from "./tools/providers.js";
@@ -71,6 +72,7 @@ export async function startMcpServer(): Promise<void> {
71
72
  "",
72
73
  "WALLET HYGIENE:",
73
74
  "- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
75
+ "- rebate_status shows accrued, pending, paid, and blocked consumer rebates plus recent payout transactions.",
74
76
  "- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
75
77
  "- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
76
78
  "- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
@@ -89,6 +91,7 @@ export async function startMcpServer(): Promise<void> {
89
91
  registerFavoriteTools(server);
90
92
  registerTipTools(server);
91
93
  registerPassTools(server);
94
+ registerRebateTools(server);
92
95
  registerUploadTools(server);
93
96
  registerProbeTools(server);
94
97
  registerProviderTools(server);
@@ -0,0 +1,91 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ type ToolResult = {
5
+ content: Array<{ type: "text"; text: string }>;
6
+ };
7
+
8
+ const { mockApiGet } = vi.hoisted(() => ({
9
+ mockApiGet: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("../../core/api-client.js", () => ({
13
+ apiGet: (...args: unknown[]) => mockApiGet(...args),
14
+ }));
15
+
16
+ async function getRebateStatusTool(): Promise<(args: Record<string, unknown>) => Promise<ToolResult>> {
17
+ const tools = new Map<string, (args: Record<string, unknown>) => Promise<ToolResult>>();
18
+ const server = {
19
+ tool: (
20
+ name: string,
21
+ _description: string,
22
+ _schema: unknown,
23
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>,
24
+ ) => {
25
+ tools.set(name, handler);
26
+ },
27
+ } as unknown as McpServer;
28
+
29
+ const { registerRebateTools } = await import("../rebates.js");
30
+ registerRebateTools(server);
31
+
32
+ const tool = tools.get("rebate_status");
33
+ if (!tool) throw new Error("rebate_status tool was not registered");
34
+ return tool;
35
+ }
36
+
37
+ function flattenText(result: ToolResult): string {
38
+ return result.content.map((item) => item.text).join("\n");
39
+ }
40
+
41
+ describe("rebate_status tool", () => {
42
+ beforeEach(() => {
43
+ vi.resetModules();
44
+ mockApiGet.mockReset();
45
+ });
46
+
47
+ it("formats rebate balances and recent payouts", async () => {
48
+ mockApiGet.mockResolvedValueOnce({
49
+ consumer_principal: "did:pkh:eip155:8453:0xabc",
50
+ principal_type: "evm",
51
+ principal_value: "0xabc",
52
+ payout_address: "0xabc",
53
+ payout_chain: "base",
54
+ auto_payout_threshold_usd: "5.000000",
55
+ totals: {
56
+ lifetime_earned_usd: "2.500000",
57
+ pending_usd: "1.250000",
58
+ paid_usd: "1.000000",
59
+ blocked_usd: "0.250000",
60
+ total_count: 5,
61
+ pending_count: 2,
62
+ paid_count: 2,
63
+ blocked_count: 1,
64
+ },
65
+ by_source: [
66
+ { source_type: "job_execution", status: "earned", count: 2, rebate_usd: "1.250000" },
67
+ ],
68
+ recent_payouts: [
69
+ {
70
+ id: "payout-1",
71
+ status: "completed",
72
+ amount_usd: "1.000000",
73
+ earning_count: 2,
74
+ tx_hash: "0xhash",
75
+ processed_at: "2026-05-09T12:00:00.000Z",
76
+ },
77
+ ],
78
+ });
79
+
80
+ const tool = await getRebateStatusTool();
81
+ const result = await tool({});
82
+ const text = flattenText(result);
83
+
84
+ expect(mockApiGet).toHaveBeenCalledWith("/rebates/status", { ensureConsumerPrincipal: true });
85
+ expect(text).toContain("Lifetime earned: $2.5000 across 5 rebate(s)");
86
+ expect(text).toContain("Pending: $1.2500 across 2 rebate(s)");
87
+ expect(text).toContain("Auto payout: $3.7500 until the $5.0000 threshold");
88
+ expect(text).toContain("job execution / earned: $1.2500 (2)");
89
+ expect(text).toContain("completed: $1.0000 (2) tx=0xhash");
90
+ });
91
+ });
@@ -173,4 +173,37 @@ describe("run_agent MCP tool", () => {
173
173
  expect(text).toContain("Paid: $0.01 via card");
174
174
  expect(text).toContain("Job ID: job-1");
175
175
  });
176
+
177
+ it("returns Link approval instructions without wrapping them as an execution error", async () => {
178
+ mockGetConfiguredMethods.mockReturnValue(["link"]);
179
+ mockGetCompatiblePaymentMethods.mockReturnValue(["link"]);
180
+ mockApiPostWithPayment.mockRejectedValueOnce(
181
+ new Error([
182
+ "Link approval required.",
183
+ "The agent has not run yet and no charge has been captured.",
184
+ "Approve this spend request in Link: https://link.example/approve/lsrq_test",
185
+ "After approving, rerun the same tool call with confirmed: true.",
186
+ ].join("\n")),
187
+ );
188
+
189
+ const { registerRunTools } = await import("../run.js");
190
+ const harness = makeServerHarness();
191
+ registerRunTools(harness.server as never);
192
+
193
+ const runAgent = harness.handlers.get("run_agent");
194
+ expect(runAgent).toBeDefined();
195
+
196
+ const result = await runAgent!({
197
+ agent_id: selectedAgent.id,
198
+ input: { text: "hello", target_language: "es" },
199
+ pay_with: "link",
200
+ confirmed: true,
201
+ });
202
+ const text = flattenToolText(result);
203
+
204
+ expect(text).toContain("Link approval required.");
205
+ expect(text).toContain("https://link.example/approve/lsrq_test");
206
+ expect(text).not.toContain("Error: Link approval required.");
207
+ expect(mockRecordSpend).not.toHaveBeenCalled();
208
+ });
176
209
  });
@@ -175,6 +175,7 @@ vi.mock("../../core/ows-adapter.js", () => ({
175
175
 
176
176
  vi.mock("../../core/principal.js", () => ({
177
177
  ensureConsumerPrincipal: async () => state.consumerPrincipal,
178
+ ensureConsumerPrincipalForMethod: async () => state.consumerPrincipal,
178
179
  getConsumerPrincipal: async () => state.consumerPrincipal,
179
180
  }));
180
181
 
@@ -8,6 +8,7 @@ export { registerWalletTools } from "./wallet.js";
8
8
  export { registerFavoriteTools } from "./favorites.js";
9
9
  export { registerTipTools } from "./tip.js";
10
10
  export { registerPassTools } from "./passes.js";
11
+ export { registerRebateTools } from "./rebates.js";
11
12
  export { registerUploadTools } from "./upload.js";
12
13
  export { registerProbeTools } from "./probe.js";
13
14
  export { registerProviderTools } from "./providers.js";