@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
@@ -1,9 +1,11 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
2
+ import { MCP_PACKAGE_VERSION } from "../version.js";
3
+ const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockEnsureBaseRebatePrincipal, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
3
4
  mockGetApiUrl: vi.fn(),
4
5
  mockGetApiKey: vi.fn(),
5
6
  mockGetPaymentFetch: vi.fn(),
6
7
  mockEnsureConsumerPrincipalForMethod: vi.fn(),
8
+ mockEnsureBaseRebatePrincipal: vi.fn(),
7
9
  mockGetConsumerPrincipalForMethod: vi.fn(),
8
10
  mockGetBaseRebatePrincipal: vi.fn(),
9
11
  mockPaymentFetch: vi.fn(),
@@ -17,6 +19,7 @@ vi.mock("../payments.js", () => ({
17
19
  }));
18
20
  vi.mock("../principal.js", () => ({
19
21
  ensureConsumerPrincipal: vi.fn(),
22
+ ensureBaseRebatePrincipal: (...args) => mockEnsureBaseRebatePrincipal(...args),
20
23
  ensureConsumerPrincipalForMethod: (...args) => mockEnsureConsumerPrincipalForMethod(...args),
21
24
  getConsumerPrincipal: vi.fn(),
22
25
  getConsumerPrincipalForMethod: (...args) => mockGetConsumerPrincipalForMethod(...args),
@@ -31,6 +34,7 @@ describe("api-client headers", () => {
31
34
  mockEnsureConsumerPrincipalForMethod.mockResolvedValue("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
32
35
  mockGetConsumerPrincipalForMethod.mockResolvedValue("did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
33
36
  mockGetBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
37
+ mockEnsureBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
34
38
  mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
35
39
  status: 200,
36
40
  headers: { "content-type": "application/json" },
@@ -46,7 +50,7 @@ describe("api-client headers", () => {
46
50
  "X-AW-Consumer-Principal": "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
47
51
  "X-AW-Rebate-Principal": "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
48
52
  "X-AW-Surface": "mcp",
49
- "X-AW-MCP-Version": "0.1.44",
53
+ "X-AW-MCP-Version": MCP_PACKAGE_VERSION,
50
54
  "X-AW-MCP-Tool": "run_agent",
51
55
  "X-AW-MCP-Action": "execute",
52
56
  }),
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { execCalls, outputs, state, mockExecFile, } = vi.hoisted(() => {
3
+ const execCalls = [];
4
+ const outputs = [];
5
+ const state = {
6
+ pending: null,
7
+ pendingWrites: [],
8
+ };
9
+ const mockExecFile = vi.fn((file, args, _options, callback) => {
10
+ execCalls.push({ file, args });
11
+ const next = outputs.shift();
12
+ if (next instanceof Error) {
13
+ callback(next);
14
+ return;
15
+ }
16
+ callback(null, JSON.stringify(next ?? null), "");
17
+ });
18
+ mockExecFile[Symbol.for("nodejs.util.promisify.custom")] = async (file, args) => {
19
+ execCalls.push({ file, args });
20
+ const next = outputs.shift();
21
+ if (next instanceof Error) {
22
+ throw next;
23
+ }
24
+ return { stdout: JSON.stringify(next ?? null), stderr: "" };
25
+ };
26
+ return { execCalls, outputs, state, mockExecFile };
27
+ });
28
+ vi.mock("node:child_process", () => ({
29
+ execFile: mockExecFile,
30
+ }));
31
+ vi.mock("../config.js", () => ({
32
+ getPendingLinkSpendRequest: () => state.pending,
33
+ setPendingLinkSpendRequest: (pending) => {
34
+ state.pending = pending;
35
+ state.pendingWrites.push(pending);
36
+ },
37
+ setLinkCooldown: vi.fn(),
38
+ }));
39
+ describe("Link CLI spend requests", () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ execCalls.length = 0;
43
+ outputs.length = 0;
44
+ state.pending = null;
45
+ state.pendingWrites = [];
46
+ });
47
+ it("returns an approval-required error immediately instead of blocking on Link approval", async () => {
48
+ outputs.push([
49
+ {
50
+ id: "lsrq_test_123",
51
+ approval_url: "https://link.example/approve/lsrq_test_123",
52
+ status: "pending_approval",
53
+ },
54
+ ]);
55
+ const { createLinkSharedPaymentToken, LinkApprovalRequiredError } = await import("../link-cli.js");
56
+ await expect(createLinkSharedPaymentToken({
57
+ amount: "10",
58
+ currency: "usd",
59
+ context: "Agent Wonderland test",
60
+ expiresAt: Math.floor(Date.now() / 1000) + 300,
61
+ networkId: "profile_test",
62
+ paymentMethodId: "csmrpd_test_123",
63
+ })).rejects.toBeInstanceOf(LinkApprovalRequiredError);
64
+ expect(execCalls).toHaveLength(1);
65
+ expect(execCalls[0]?.args).toContain("create");
66
+ expect(state.pendingWrites[0]).toMatchObject({
67
+ id: "lsrq_test_123",
68
+ approvalUrl: "https://link.example/approve/lsrq_test_123",
69
+ amount: "10",
70
+ currency: "usd",
71
+ networkId: "profile_test",
72
+ paymentMethodId: "csmrpd_test_123",
73
+ });
74
+ });
75
+ it("resumes a stored Link spend request and returns the approved shared payment token", async () => {
76
+ const expiresAt = Math.floor(Date.now() / 1000) + 300;
77
+ state.pending = {
78
+ id: "lsrq_test_123",
79
+ approvalUrl: "https://link.example/approve/lsrq_test_123",
80
+ amount: "10",
81
+ currency: "usd",
82
+ context: "Agent Wonderland test",
83
+ expiresAt,
84
+ networkId: "profile_test",
85
+ paymentMethodId: "csmrpd_test_123",
86
+ createdAt: new Date().toISOString(),
87
+ };
88
+ outputs.push({ shared_payment_token: "spt_test_123" });
89
+ const { createLinkSharedPaymentToken } = await import("../link-cli.js");
90
+ await expect(createLinkSharedPaymentToken({
91
+ amount: "10",
92
+ currency: "usd",
93
+ context: "Agent Wonderland test",
94
+ expiresAt,
95
+ networkId: "profile_test",
96
+ paymentMethodId: "csmrpd_test_123",
97
+ })).resolves.toBe("spt_test_123");
98
+ expect(execCalls).toHaveLength(1);
99
+ expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
100
+ expect(state.pendingWrites).toEqual([null]);
101
+ });
102
+ });
@@ -106,6 +106,19 @@ describe("consumer principal helpers", () => {
106
106
  expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
107
107
  expect(state.addedWallets).toHaveLength(1);
108
108
  });
109
+ it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
110
+ state.wallets = [
111
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
112
+ ];
113
+ state.addresses = {
114
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
115
+ };
116
+ const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
117
+ const principal = await ensureConsumerPrincipalForMethod("link");
118
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
119
+ expect(state.addedWallets).toHaveLength(1);
120
+ expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
121
+ });
109
122
  it("returns the Base rebate principal when an EVM wallet is available", async () => {
110
123
  state.wallets = [
111
124
  { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
@@ -119,4 +132,16 @@ describe("consumer principal helpers", () => {
119
132
  const principal = await getBaseRebatePrincipal();
120
133
  expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
121
134
  });
135
+ it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
136
+ state.wallets = [
137
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
138
+ ];
139
+ state.addresses = {
140
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
141
+ };
142
+ const { ensureBaseRebatePrincipal } = await import("../principal.js");
143
+ const principal = await ensureBaseRebatePrincipal();
144
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
145
+ expect(state.addedWallets).toHaveLength(1);
146
+ });
122
147
  });
@@ -1,6 +1,6 @@
1
1
  import { getApiUrl, getApiKey } from "./config.js";
2
2
  import { getPaymentFetch } from "./payments.js";
3
- import { getBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
3
+ import { getBaseRebatePrincipal, ensureBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
4
4
  import { MCP_PACKAGE_VERSION } from "./version.js";
5
5
  // ── Error class ────────────────────────────────────────────────────
6
6
  export class ApiError extends Error {
@@ -49,7 +49,9 @@ async function buildHeaders(path, method, options) {
49
49
  if (principal) {
50
50
  headers["X-AW-Consumer-Principal"] = principal;
51
51
  }
52
- const rebatePrincipal = await getBaseRebatePrincipal();
52
+ const rebatePrincipal = options?.ensureConsumerPrincipal
53
+ ? await ensureBaseRebatePrincipal()
54
+ : await getBaseRebatePrincipal();
53
55
  if (rebatePrincipal) {
54
56
  headers["X-AW-Rebate-Principal"] = rebatePrincipal;
55
57
  }
@@ -1,3 +1,8 @@
1
+ export declare class LinkApprovalRequiredError extends Error {
2
+ readonly spendRequestId: string;
3
+ readonly approvalUrl?: string | undefined;
4
+ constructor(spendRequestId: string, approvalUrl?: string | undefined);
5
+ }
1
6
  export interface LinkCliAuthStatus {
2
7
  authenticated: boolean;
3
8
  credentialsPath?: string;
@@ -4,8 +4,23 @@ import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest
4
4
  const execFileAsync = promisify(execFile);
5
5
  const LINK_CLI_PACKAGE = "@stripe/link-cli";
6
6
  const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
7
- function sleep(ms) {
8
- return new Promise((resolve) => setTimeout(resolve, ms));
7
+ export class LinkApprovalRequiredError extends Error {
8
+ spendRequestId;
9
+ approvalUrl;
10
+ constructor(spendRequestId, approvalUrl) {
11
+ super(formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl));
12
+ this.spendRequestId = spendRequestId;
13
+ this.approvalUrl = approvalUrl;
14
+ this.name = "LinkApprovalRequiredError";
15
+ }
16
+ }
17
+ function formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl) {
18
+ return [
19
+ "Link approval required.",
20
+ "The agent has not run yet and no charge has been captured.",
21
+ approvalUrl ? `Approve this spend request in Link: ${approvalUrl}` : `Spend request: ${spendRequestId}`,
22
+ "After approving, rerun the same tool call with confirmed: true.",
23
+ ].join("\n");
9
24
  }
10
25
  async function runLinkCli(args, timeout = LINK_CLI_TIMEOUT_MS) {
11
26
  try {
@@ -220,6 +235,9 @@ export async function createLinkSharedPaymentToken(params) {
220
235
  }
221
236
  catch (err) {
222
237
  const message = err instanceof Error ? err.message : String(err);
238
+ if (err instanceof LinkApprovalRequiredError) {
239
+ throw err;
240
+ }
223
241
  if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
224
242
  throw err;
225
243
  }
@@ -293,39 +311,21 @@ export async function createLinkSharedPaymentToken(params) {
293
311
  if (approval.approvalUrl) {
294
312
  console.error(`Link approval required: ${approval.approvalUrl}`);
295
313
  }
296
- const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
297
- setPendingLinkSpendRequest(null);
298
- return retrievedSpt;
314
+ throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
299
315
  }
300
316
  {
301
317
  throw new Error("Link spend request completed without a shared payment token in the CLI response.");
302
318
  }
303
319
  }
304
320
  async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
305
- let retrieved = await runLinkCli([
321
+ const retrieved = await runLinkCli([
306
322
  "spend-request",
307
323
  "retrieve",
308
324
  spendRequestId,
309
- "--interval",
310
- "2",
311
- "--max-attempts",
312
- "150",
313
- ]);
314
- let retrievedSpt = extractSharedPaymentToken(retrieved);
315
- for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
316
- await sleep(2_000);
317
- retrieved = await runLinkCli([
318
- "spend-request",
319
- "retrieve",
320
- spendRequestId,
321
- ]);
322
- retrievedSpt = extractSharedPaymentToken(retrieved);
323
- }
325
+ ], 30_000);
326
+ const retrievedSpt = extractSharedPaymentToken(retrieved);
324
327
  if (retrievedSpt) {
325
328
  return retrievedSpt;
326
329
  }
327
- throw new Error([
328
- "Link spend request finished without a shared payment token.",
329
- approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
330
- ].filter(Boolean).join("\n"));
330
+ throw new LinkApprovalRequiredError(spendRequestId, approvalUrl);
331
331
  }
@@ -1,5 +1,6 @@
1
1
  export declare function getConsumerPrincipal(): Promise<string | null>;
2
2
  export declare function getBaseRebatePrincipal(): Promise<string | null>;
3
+ export declare function ensureBaseRebatePrincipal(): Promise<string>;
3
4
  export declare function getConsumerPrincipalForMethod(method?: string): Promise<string | null>;
4
5
  export declare function ensureConsumerPrincipal(): Promise<string>;
5
6
  export declare function ensureConsumerPrincipalForMethod(method?: string): Promise<string>;
@@ -59,6 +59,40 @@ async function createFallbackIdentityWallet() {
59
59
  addWallet(entry);
60
60
  return entry;
61
61
  }
62
+ async function ensureEvmIdentityWallet() {
63
+ const existing = preferredWallets().find((wallet) => walletSupportsEvm(wallet));
64
+ if (existing)
65
+ return existing;
66
+ if (await isOwsAvailable()) {
67
+ const existingOwsWallets = await listOwsWallets();
68
+ const linked = existingOwsWallets[0];
69
+ if (linked) {
70
+ const entry = {
71
+ id: linked.name,
72
+ keyType: "ows",
73
+ owsWalletId: linked.id,
74
+ chains: ["tempo", "base"],
75
+ defaultChain: "base",
76
+ label: linked.name,
77
+ };
78
+ addWallet(entry);
79
+ return entry;
80
+ }
81
+ const walletName = `aw-identity-${Date.now()}`;
82
+ const created = await createOwsWallet(walletName, "evm");
83
+ const entry = {
84
+ id: walletName,
85
+ keyType: "ows",
86
+ owsWalletId: created.walletId,
87
+ chains: ["tempo", "base"],
88
+ defaultChain: "base",
89
+ label: AUTO_IDENTITY_LABEL,
90
+ };
91
+ addWallet(entry);
92
+ return entry;
93
+ }
94
+ return createFallbackIdentityWallet();
95
+ }
62
96
  async function ensureIdentityWallet() {
63
97
  const wallets = preferredWallets();
64
98
  const existing = wallets.find((wallet) => walletSupportsEvm(wallet) || walletSupportsSolana(wallet));
@@ -105,9 +139,20 @@ export async function getConsumerPrincipal() {
105
139
  export async function getBaseRebatePrincipal() {
106
140
  return (await principalForChain("base")) ?? principalForChain("tempo");
107
141
  }
142
+ export async function ensureBaseRebatePrincipal() {
143
+ const existing = await getBaseRebatePrincipal();
144
+ if (existing)
145
+ return existing;
146
+ const wallet = await ensureEvmIdentityWallet();
147
+ const principal = await walletPrincipal(wallet);
148
+ if (!principal || !principal.startsWith(`did:pkh:eip155:${BASE_CHAIN_ID}:`)) {
149
+ throw new Error("Could not derive a Base rebate principal from the configured identity wallet.");
150
+ }
151
+ return principal;
152
+ }
108
153
  export async function getConsumerPrincipalForMethod(method) {
109
154
  if (!method || method === "card" || method === "link") {
110
- return getConsumerPrincipal();
155
+ return getBaseRebatePrincipal();
111
156
  }
112
157
  const resolved = resolveWalletAndChain(method);
113
158
  if (!resolved)
@@ -129,6 +174,9 @@ export async function ensureConsumerPrincipalForMethod(method) {
129
174
  const existing = await getConsumerPrincipalForMethod(method);
130
175
  if (existing)
131
176
  return existing;
177
+ if (!method || method === "card" || method === "link") {
178
+ return ensureBaseRebatePrincipal();
179
+ }
132
180
  if (method && method !== "card" && method !== "link") {
133
181
  throw new Error(`Could not derive a consumer principal for payment method "${method}". ` +
134
182
  "Check wallet_status and confirm that chain is configured for the active wallet.");
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerWalletTools } from "./tools/wallet.js";
12
12
  import { registerFavoriteTools } from "./tools/favorites.js";
13
13
  import { registerTipTools } from "./tools/tip.js";
14
14
  import { registerPassTools } from "./tools/passes.js";
15
+ import { registerRebateTools } from "./tools/rebates.js";
15
16
  import { registerUploadTools } from "./tools/upload.js";
16
17
  import { registerProbeTools } from "./tools/probe.js";
17
18
  import { registerProviderTools } from "./tools/providers.js";
@@ -64,6 +65,7 @@ export async function startMcpServer() {
64
65
  "",
65
66
  "WALLET HYGIENE:",
66
67
  "- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
68
+ "- rebate_status shows accrued, pending, paid, and blocked consumer rebates plus recent payout transactions.",
67
69
  "- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
68
70
  "- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
69
71
  "- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
@@ -80,6 +82,7 @@ export async function startMcpServer() {
80
82
  registerFavoriteTools(server);
81
83
  registerTipTools(server);
82
84
  registerPassTools(server);
85
+ registerRebateTools(server);
83
86
  registerUploadTools(server);
84
87
  registerProbeTools(server);
85
88
  registerProviderTools(server);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { mockApiGet } = vi.hoisted(() => ({
3
+ mockApiGet: vi.fn(),
4
+ }));
5
+ vi.mock("../../core/api-client.js", () => ({
6
+ apiGet: (...args) => mockApiGet(...args),
7
+ }));
8
+ async function getRebateStatusTool() {
9
+ const tools = new Map();
10
+ const server = {
11
+ tool: (name, _description, _schema, handler) => {
12
+ tools.set(name, handler);
13
+ },
14
+ };
15
+ const { registerRebateTools } = await import("../rebates.js");
16
+ registerRebateTools(server);
17
+ const tool = tools.get("rebate_status");
18
+ if (!tool)
19
+ throw new Error("rebate_status tool was not registered");
20
+ return tool;
21
+ }
22
+ function flattenText(result) {
23
+ return result.content.map((item) => item.text).join("\n");
24
+ }
25
+ describe("rebate_status tool", () => {
26
+ beforeEach(() => {
27
+ vi.resetModules();
28
+ mockApiGet.mockReset();
29
+ });
30
+ it("formats rebate balances and recent payouts", async () => {
31
+ mockApiGet.mockResolvedValueOnce({
32
+ consumer_principal: "did:pkh:eip155:8453:0xabc",
33
+ principal_type: "evm",
34
+ principal_value: "0xabc",
35
+ payout_address: "0xabc",
36
+ payout_chain: "base",
37
+ auto_payout_threshold_usd: "5.000000",
38
+ totals: {
39
+ lifetime_earned_usd: "2.500000",
40
+ pending_usd: "1.250000",
41
+ paid_usd: "1.000000",
42
+ blocked_usd: "0.250000",
43
+ total_count: 5,
44
+ pending_count: 2,
45
+ paid_count: 2,
46
+ blocked_count: 1,
47
+ },
48
+ by_source: [
49
+ { source_type: "job_execution", status: "earned", count: 2, rebate_usd: "1.250000" },
50
+ ],
51
+ recent_payouts: [
52
+ {
53
+ id: "payout-1",
54
+ status: "completed",
55
+ amount_usd: "1.000000",
56
+ earning_count: 2,
57
+ tx_hash: "0xhash",
58
+ processed_at: "2026-05-09T12:00:00.000Z",
59
+ },
60
+ ],
61
+ });
62
+ const tool = await getRebateStatusTool();
63
+ const result = await tool({});
64
+ const text = flattenText(result);
65
+ expect(mockApiGet).toHaveBeenCalledWith("/rebates/status", { ensureConsumerPrincipal: true });
66
+ expect(text).toContain("Lifetime earned: $2.5000 across 5 rebate(s)");
67
+ expect(text).toContain("Pending: $1.2500 across 2 rebate(s)");
68
+ expect(text).toContain("Auto payout: $3.7500 until the $5.0000 threshold");
69
+ expect(text).toContain("job execution / earned: $1.2500 (2)");
70
+ expect(text).toContain("completed: $1.0000 (2) tx=0xhash");
71
+ });
72
+ });
@@ -146,4 +146,30 @@ describe("run_agent MCP tool", () => {
146
146
  expect(text).toContain("Paid: $0.01 via card");
147
147
  expect(text).toContain("Job ID: job-1");
148
148
  });
149
+ it("returns Link approval instructions without wrapping them as an execution error", async () => {
150
+ mockGetConfiguredMethods.mockReturnValue(["link"]);
151
+ mockGetCompatiblePaymentMethods.mockReturnValue(["link"]);
152
+ mockApiPostWithPayment.mockRejectedValueOnce(new Error([
153
+ "Link approval required.",
154
+ "The agent has not run yet and no charge has been captured.",
155
+ "Approve this spend request in Link: https://link.example/approve/lsrq_test",
156
+ "After approving, rerun the same tool call with confirmed: true.",
157
+ ].join("\n")));
158
+ const { registerRunTools } = await import("../run.js");
159
+ const harness = makeServerHarness();
160
+ registerRunTools(harness.server);
161
+ const runAgent = harness.handlers.get("run_agent");
162
+ expect(runAgent).toBeDefined();
163
+ const result = await runAgent({
164
+ agent_id: selectedAgent.id,
165
+ input: { text: "hello", target_language: "es" },
166
+ pay_with: "link",
167
+ confirmed: true,
168
+ });
169
+ const text = flattenToolText(result);
170
+ expect(text).toContain("Link approval required.");
171
+ expect(text).toContain("https://link.example/approve/lsrq_test");
172
+ expect(text).not.toContain("Error: Link approval required.");
173
+ expect(mockRecordSpend).not.toHaveBeenCalled();
174
+ });
149
175
  });
@@ -124,6 +124,7 @@ vi.mock("../../core/ows-adapter.js", () => ({
124
124
  }));
125
125
  vi.mock("../../core/principal.js", () => ({
126
126
  ensureConsumerPrincipal: async () => state.consumerPrincipal,
127
+ ensureConsumerPrincipalForMethod: async () => state.consumerPrincipal,
127
128
  getConsumerPrincipal: async () => state.consumerPrincipal,
128
129
  }));
129
130
  function resetState() {
@@ -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";
@@ -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";
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerObservabilityTools(server: McpServer): void;
@@ -0,0 +1,20 @@
1
+ import { apiPost } from "../core/api-client.js";
2
+ function text(t) {
3
+ return { content: [{ type: "text", text: t }] };
4
+ }
5
+ export function registerObservabilityTools(server) {
6
+ server.tool("open_observability_dashboard", "Generate a secure one-click sign-in URL for the Agent Wonderland web observability dashboard. The dashboard shows your agent runs, spend, rebates, and recent activity.", {}, async () => {
7
+ const result = await apiPost("/observability/link", {}, { ensureConsumerPrincipal: true });
8
+ const lines = [
9
+ "Your secure observability link is ready:",
10
+ result.url,
11
+ "",
12
+ `Expires: ${result.expires_at}`,
13
+ ];
14
+ if (result.consumer_principal) {
15
+ lines.push(`Consumer principal: ${result.consumer_principal}`);
16
+ }
17
+ lines.push("", "Open the link in your browser to view usage metrics, spend, rebates, and recent runs.");
18
+ return text(lines.join("\n"));
19
+ });
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerRebateTools(server: McpServer): void;
@@ -0,0 +1,51 @@
1
+ import { apiGet } from "../core/api-client.js";
2
+ function text(t) {
3
+ return { content: [{ type: "text", text: t }] };
4
+ }
5
+ function money(value) {
6
+ const numeric = Number(value);
7
+ return Number.isFinite(numeric) ? `$${numeric.toFixed(4)}` : `$${value}`;
8
+ }
9
+ function sourceLabel(value) {
10
+ return value.replace(/_/g, " ");
11
+ }
12
+ export function registerRebateTools(server) {
13
+ server.tool("rebate_status", "Show your Agent Wonderland consumer rebate balance, payout threshold, blocked rebates, and recent payout transactions.", {}, async () => {
14
+ const status = await apiGet("/rebates/status", { ensureConsumerPrincipal: true });
15
+ const threshold = Number(status.auto_payout_threshold_usd);
16
+ const pending = Number(status.totals.pending_usd);
17
+ const remaining = Math.max(0, threshold - pending);
18
+ const lines = [
19
+ "Rebate status",
20
+ `Consumer principal: ${status.consumer_principal}`,
21
+ `Base payout address: ${status.payout_address ?? "not configured"}`,
22
+ "",
23
+ `Lifetime earned: ${money(status.totals.lifetime_earned_usd)} across ${status.totals.total_count} rebate(s)`,
24
+ `Paid out: ${money(status.totals.paid_usd)} across ${status.totals.paid_count} rebate(s)`,
25
+ `Pending: ${money(status.totals.pending_usd)} across ${status.totals.pending_count} rebate(s)`,
26
+ `Blocked: ${money(status.totals.blocked_usd)} across ${status.totals.blocked_count} rebate(s)`,
27
+ ];
28
+ if (Number.isFinite(threshold) && threshold > 0) {
29
+ lines.push(pending >= threshold
30
+ ? `Auto payout: eligible for the next processor run (threshold ${money(threshold)})`
31
+ : `Auto payout: ${money(remaining)} until the ${money(threshold)} threshold`);
32
+ }
33
+ if (status.by_source.length > 0) {
34
+ lines.push("", "By source:");
35
+ for (const row of status.by_source) {
36
+ lines.push(` ${sourceLabel(row.source_type)} / ${row.status}: ${money(row.rebate_usd)} (${row.count})`);
37
+ }
38
+ }
39
+ if (status.recent_payouts.length > 0) {
40
+ lines.push("", "Recent payouts:");
41
+ for (const payout of status.recent_payouts.slice(0, 5)) {
42
+ const tx = payout.tx_hash ? ` tx=${payout.tx_hash}` : "";
43
+ lines.push(` ${payout.status}: ${money(payout.amount_usd)} (${payout.earning_count})${tx}`);
44
+ }
45
+ }
46
+ if (!status.payout_address) {
47
+ lines.push("", "No Base payout address is configured, so new rebates may be blocked.");
48
+ }
49
+ return text(lines.join("\n"));
50
+ });
51
+ }
package/dist/tools/run.js CHANGED
@@ -166,6 +166,9 @@ export function registerRunTools(server) {
166
166
  ` Cost: $${price.toFixed(2)}`,
167
167
  ` Payment: ${formatPaymentLabel(method)}`,
168
168
  ];
169
+ if (method === "link") {
170
+ quoteLines.push(" Link: after confirming here, approve the Link spend request and rerun the confirmed call.");
171
+ }
169
172
  const creditPackLines = buildCreditPackOfferLines(agent);
170
173
  if (creditPackLines.length > 0) {
171
174
  quoteLines.push("", ...creditPackLines);
@@ -226,6 +229,9 @@ export function registerRunTools(server) {
226
229
  ].join("\n"));
227
230
  }
228
231
  const msg = apiErr?.message ?? "Failed to run agent";
232
+ if (msg.includes("Link approval required.")) {
233
+ return text(msg);
234
+ }
229
235
  if (msg.includes("Missing required field") || msg.includes("validation failed")) {
230
236
  return text(`Error: ${msg}\n\nUse get_agent("${agent_id}") to see the required input fields.`);
231
237
  }
@@ -241,6 +241,9 @@ export function registerSolveTools(server) {
241
241
  `Best match: ${selected.name}`,
242
242
  `Cost: $${estimatedCost.toFixed(2)}`,
243
243
  `Payment: ${formatPaymentLabel(method)}`,
244
+ ...(method === "link"
245
+ ? ["Link: after confirming here, approve the Link spend request and rerun the confirmed call."]
246
+ : []),
244
247
  ...(() => {
245
248
  const summary = buildCreditPackSummary(selected);
246
249
  return summary.length > 0 ? ["", ...summary] : [];
@@ -298,7 +301,11 @@ export function registerSolveTools(server) {
298
301
  })(),
299
302
  ].join("\n"));
300
303
  }
301
- return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
304
+ const msg = apiErr?.message ?? "Failed to run agent";
305
+ if (msg.includes("Link approval required.")) {
306
+ return text(msg);
307
+ }
308
+ return text(`Error: ${msg}`);
302
309
  }
303
310
  pendingSolves.delete(pendingKey);
304
311
  const jobId = result.job_id ?? "";