@agentwonderland/mcp 0.1.50 → 0.1.52

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__/payments.test.js +23 -2
  3. package/dist/core/__tests__/principal.test.js +25 -0
  4. package/dist/core/api-client.js +4 -2
  5. package/dist/core/config.d.ts +22 -0
  6. package/dist/core/config.js +20 -0
  7. package/dist/core/link-cli.js +97 -25
  8. package/dist/core/payments.js +20 -6
  9. package/dist/core/principal.d.ts +1 -0
  10. package/dist/core/principal.js +49 -1
  11. package/dist/core/version.d.ts +1 -1
  12. package/dist/core/version.js +1 -1
  13. package/dist/index.js +3 -0
  14. package/dist/tools/__tests__/rebates.test.d.ts +1 -0
  15. package/dist/tools/__tests__/rebates.test.js +72 -0
  16. package/dist/tools/__tests__/wallet.test.js +4 -0
  17. package/dist/tools/index.d.ts +1 -0
  18. package/dist/tools/index.js +1 -0
  19. package/dist/tools/observability.d.ts +2 -0
  20. package/dist/tools/observability.js +20 -0
  21. package/dist/tools/rebates.d.ts +2 -0
  22. package/dist/tools/rebates.js +51 -0
  23. package/dist/tools/wallet.js +11 -5
  24. package/package.json +1 -1
  25. package/src/core/__tests__/api-client.test.ts +8 -1
  26. package/src/core/__tests__/payments.test.ts +36 -2
  27. package/src/core/__tests__/principal.test.ts +31 -0
  28. package/src/core/api-client.ts +4 -2
  29. package/src/core/config.ts +46 -0
  30. package/src/core/link-cli.ts +114 -28
  31. package/src/core/payments.ts +21 -6
  32. package/src/core/principal.ts +54 -1
  33. package/src/core/version.ts +1 -1
  34. package/src/index.ts +3 -0
  35. package/src/tools/__tests__/rebates.test.ts +91 -0
  36. package/src/tools/__tests__/wallet.test.ts +10 -0
  37. package/src/tools/index.ts +1 -0
  38. package/src/tools/rebates.ts +102 -0
  39. package/src/tools/wallet.ts +11 -4
@@ -16,6 +16,7 @@ import {
16
16
  getDefaultWallet,
17
17
  getCardConfig,
18
18
  getLinkConfig,
19
+ getLinkCooldown,
19
20
  resolveWalletAndChain,
20
21
  getApiUrl,
21
22
  type WalletEntry,
@@ -41,8 +42,6 @@ const REGISTRY_METHOD_MAP: Record<string, string> = {
41
42
  link: "stripe_card",
42
43
  };
43
44
 
44
- const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 2_000;
45
-
46
45
  const ACCEPTED_PAYMENT_ALIASES: Record<string, string[]> = {
47
46
  tempo: ["tempo_usdc", "tempo"],
48
47
  base: ["base_usdc", "base"],
@@ -200,10 +199,25 @@ function formatMinorCurrencyAmount(currency: string, amount: string): string {
200
199
  function getLinkApprovalLimitAmount(actualAmount: string): string {
201
200
  const actualAmountCents = Number(actualAmount);
202
201
  const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
203
- const defaultLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
204
- ? Math.floor(configuredLimit)
205
- : DEFAULT_LINK_APPROVAL_LIMIT_CENTS;
206
- return String(Math.max(actualAmountCents, defaultLimit));
202
+ const approvalLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
203
+ ? Math.max(actualAmountCents, Math.floor(configuredLimit))
204
+ : actualAmountCents;
205
+ return String(approvalLimit);
206
+ }
207
+
208
+ function assertLinkNotCoolingDown(): void {
209
+ const cooldown = getLinkCooldown();
210
+ if (!cooldown) return;
211
+ const blockedUntil = Date.parse(cooldown.blockedUntil);
212
+ if (!Number.isFinite(blockedUntil) || blockedUntil <= Date.now()) return;
213
+ throw new Error(
214
+ [
215
+ "Link is temporarily blocked by Stripe's projected-spend cap.",
216
+ "Reauthing Link or switching cards in the same Link account will not fix this.",
217
+ `Try again after ${cooldown.blockedUntil}, use USDC, or ask Stripe to raise/clear the merchant projected-spend cap.`,
218
+ `Last Link error: ${cooldown.reason}`,
219
+ ].join("\n"),
220
+ );
207
221
  }
208
222
 
209
223
  function buildLinkApprovalContext(params: {
@@ -239,6 +253,7 @@ async function initLink(): Promise<typeof fetch | null> {
239
253
  expiresAt: number;
240
254
  metadata?: Record<string, string>;
241
255
  }) => {
256
+ assertLinkNotCoolingDown();
242
257
  const approvalAmount = getLinkApprovalLimitAmount(params.amount);
243
258
  console.error(
244
259
  `Requesting Link approval up to ${formatMinorCurrencyAmount(params.currency, approvalAmount)} ` +
@@ -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}". ` +
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.50";
1
+ export const MCP_PACKAGE_VERSION = "0.1.51";
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
+ });
@@ -29,6 +29,12 @@ type PendingLinkSetup = {
29
29
  createdAt: string;
30
30
  } | null;
31
31
 
32
+ type LinkCooldown = {
33
+ reason: string;
34
+ createdAt: string;
35
+ blockedUntil: string;
36
+ } | null;
37
+
32
38
  type WalletToolResult = {
33
39
  content: Array<{ type: "text"; text: string }>;
34
40
  };
@@ -39,6 +45,7 @@ const state = vi.hoisted(() => ({
39
45
  card: null as CardConfig,
40
46
  link: null as LinkConfig,
41
47
  pendingLinkSetup: null as PendingLinkSetup,
48
+ linkCooldown: null as LinkCooldown,
42
49
  pendingCardSetupToken: null as string | null,
43
50
  spendPolicies: {} as Record<string, unknown>,
44
51
  defaultPaymentMethod: undefined as string | undefined,
@@ -93,6 +100,7 @@ vi.mock("../../core/config.js", () => ({
93
100
  },
94
101
  getCardConfig: () => state.card,
95
102
  getLinkConfig: () => state.link,
103
+ getLinkCooldown: () => state.linkCooldown,
96
104
  getPendingLinkSetup: () => state.pendingLinkSetup,
97
105
  getPendingCardSetupToken: () => state.pendingCardSetupToken,
98
106
  getSpendPolicy: (walletId: string) => state.spendPolicies[walletId] ?? null,
@@ -167,6 +175,7 @@ vi.mock("../../core/ows-adapter.js", () => ({
167
175
 
168
176
  vi.mock("../../core/principal.js", () => ({
169
177
  ensureConsumerPrincipal: async () => state.consumerPrincipal,
178
+ ensureConsumerPrincipalForMethod: async () => state.consumerPrincipal,
170
179
  getConsumerPrincipal: async () => state.consumerPrincipal,
171
180
  }));
172
181
 
@@ -176,6 +185,7 @@ function resetState(): void {
176
185
  state.card = null;
177
186
  state.link = null;
178
187
  state.pendingLinkSetup = null;
188
+ state.linkCooldown = null;
179
189
  state.pendingCardSetupToken = null;
180
190
  state.spendPolicies = {};
181
191
  state.defaultPaymentMethod = undefined;
@@ -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,102 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { apiGet } from "../core/api-client.js";
3
+
4
+ type RebateStatus = {
5
+ consumer_principal: string;
6
+ principal_type: string;
7
+ principal_value: string;
8
+ payout_address: string | null;
9
+ payout_chain: string;
10
+ auto_payout_threshold_usd: string;
11
+ totals: {
12
+ lifetime_earned_usd: string;
13
+ pending_usd: string;
14
+ paid_usd: string;
15
+ blocked_usd: string;
16
+ total_count: number;
17
+ pending_count: number;
18
+ paid_count: number;
19
+ blocked_count: number;
20
+ };
21
+ by_source: Array<{
22
+ source_type: string;
23
+ status: string;
24
+ count: number;
25
+ rebate_usd: string;
26
+ }>;
27
+ recent_payouts: Array<{
28
+ id: string;
29
+ status: string;
30
+ amount_usd: string;
31
+ earning_count: number;
32
+ tx_hash: string | null;
33
+ processed_at: string | null;
34
+ }>;
35
+ };
36
+
37
+ function text(t: string) {
38
+ return { content: [{ type: "text" as const, text: t }] };
39
+ }
40
+
41
+ function money(value: string | number): string {
42
+ const numeric = Number(value);
43
+ return Number.isFinite(numeric) ? `$${numeric.toFixed(4)}` : `$${value}`;
44
+ }
45
+
46
+ function sourceLabel(value: string): string {
47
+ return value.replace(/_/g, " ");
48
+ }
49
+
50
+ export function registerRebateTools(server: McpServer): void {
51
+ server.tool(
52
+ "rebate_status",
53
+ "Show your Agent Wonderland consumer rebate balance, payout threshold, blocked rebates, and recent payout transactions.",
54
+ {},
55
+ async () => {
56
+ const status = await apiGet<RebateStatus>("/rebates/status", { ensureConsumerPrincipal: true });
57
+ const threshold = Number(status.auto_payout_threshold_usd);
58
+ const pending = Number(status.totals.pending_usd);
59
+ const remaining = Math.max(0, threshold - pending);
60
+
61
+ const lines = [
62
+ "Rebate status",
63
+ `Consumer principal: ${status.consumer_principal}`,
64
+ `Base payout address: ${status.payout_address ?? "not configured"}`,
65
+ "",
66
+ `Lifetime earned: ${money(status.totals.lifetime_earned_usd)} across ${status.totals.total_count} rebate(s)`,
67
+ `Paid out: ${money(status.totals.paid_usd)} across ${status.totals.paid_count} rebate(s)`,
68
+ `Pending: ${money(status.totals.pending_usd)} across ${status.totals.pending_count} rebate(s)`,
69
+ `Blocked: ${money(status.totals.blocked_usd)} across ${status.totals.blocked_count} rebate(s)`,
70
+ ];
71
+
72
+ if (Number.isFinite(threshold) && threshold > 0) {
73
+ lines.push(
74
+ pending >= threshold
75
+ ? `Auto payout: eligible for the next processor run (threshold ${money(threshold)})`
76
+ : `Auto payout: ${money(remaining)} until the ${money(threshold)} threshold`,
77
+ );
78
+ }
79
+
80
+ if (status.by_source.length > 0) {
81
+ lines.push("", "By source:");
82
+ for (const row of status.by_source) {
83
+ lines.push(` ${sourceLabel(row.source_type)} / ${row.status}: ${money(row.rebate_usd)} (${row.count})`);
84
+ }
85
+ }
86
+
87
+ if (status.recent_payouts.length > 0) {
88
+ lines.push("", "Recent payouts:");
89
+ for (const payout of status.recent_payouts.slice(0, 5)) {
90
+ const tx = payout.tx_hash ? ` tx=${payout.tx_hash}` : "";
91
+ lines.push(` ${payout.status}: ${money(payout.amount_usd)} (${payout.earning_count})${tx}`);
92
+ }
93
+ }
94
+
95
+ if (!status.payout_address) {
96
+ lines.push("", "No Base payout address is configured, so new rebates may be blocked.");
97
+ }
98
+
99
+ return text(lines.join("\n"));
100
+ },
101
+ );
102
+ }
@@ -4,6 +4,7 @@ import {
4
4
  getWallets,
5
5
  getCardConfig,
6
6
  getLinkConfig,
7
+ getLinkCooldown,
7
8
  getPendingLinkSetup,
8
9
  setPendingLinkSetup,
9
10
  setLinkConfig,
@@ -38,7 +39,7 @@ import {
38
39
  installOws,
39
40
  platformSupportsOws,
40
41
  } from "../core/ows-adapter.js";
41
- import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
42
+ import { ensureConsumerPrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipal } from "../core/principal.js";
42
43
  import { MCP_PACKAGE_VERSION } from "../core/version.js";
43
44
 
44
45
  function text(t: string) {
@@ -157,6 +158,12 @@ export function registerWalletTools(server: McpServer): void {
157
158
  lines.push(auth.authenticated
158
159
  ? " Link CLI: authenticated"
159
160
  : " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
161
+ const cooldown = getLinkCooldown();
162
+ const blockedUntil = cooldown ? Date.parse(cooldown.blockedUntil) : NaN;
163
+ if (cooldown && Number.isFinite(blockedUntil) && blockedUntil > Date.now()) {
164
+ lines.push(` Link status: temporarily blocked by Stripe projected-spend cap until ${cooldown.blockedUntil}`);
165
+ lines.push(" Link note: reauthing or switching cards in the same Link account will not clear this cap.");
166
+ }
160
167
  }
161
168
 
162
169
  if (pendingCardSetupToken) {
@@ -285,7 +292,7 @@ export function registerWalletTools(server: McpServer): void {
285
292
  paymentMethodId: selected.id,
286
293
  label: name ?? selected.label,
287
294
  });
288
- const principal = await ensureConsumerPrincipal();
295
+ const principal = await ensureConsumerPrincipalForMethod("link");
289
296
  return text(
290
297
  [
291
298
  "Link payment method connected.",
@@ -323,7 +330,7 @@ export function registerWalletTools(server: McpServer): void {
323
330
  paymentMethodId: method.id,
324
331
  label: method.label,
325
332
  });
326
- const principal = await ensureConsumerPrincipal();
333
+ const principal = await ensureConsumerPrincipalForMethod("link");
327
334
  return text(
328
335
  [
329
336
  "Link payment method connected.",
@@ -373,7 +380,7 @@ export function registerWalletTools(server: McpServer): void {
373
380
  const result = await pollCardSetup(pendingToken, 250);
374
381
 
375
382
  if (result) {
376
- const principal = await ensureConsumerPrincipal();
383
+ const principal = await ensureConsumerPrincipalForMethod("card");
377
384
  return text(
378
385
  `Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
379
386
  `Consumer principal: ${principal}`,