@agentwonderland/mcp 0.1.51 → 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.
@@ -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
  }),
@@ -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,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
+ });
@@ -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
+ }
@@ -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.52",
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
  }),
@@ -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
  }
@@ -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
+ });
@@ -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";
@@ -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
+ }
@@ -39,7 +39,7 @@ import {
39
39
  installOws,
40
40
  platformSupportsOws,
41
41
  } from "../core/ows-adapter.js";
42
- import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
42
+ import { ensureConsumerPrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipal } from "../core/principal.js";
43
43
  import { MCP_PACKAGE_VERSION } from "../core/version.js";
44
44
 
45
45
  function text(t: string) {
@@ -292,7 +292,7 @@ export function registerWalletTools(server: McpServer): void {
292
292
  paymentMethodId: selected.id,
293
293
  label: name ?? selected.label,
294
294
  });
295
- const principal = await ensureConsumerPrincipal();
295
+ const principal = await ensureConsumerPrincipalForMethod("link");
296
296
  return text(
297
297
  [
298
298
  "Link payment method connected.",
@@ -330,7 +330,7 @@ export function registerWalletTools(server: McpServer): void {
330
330
  paymentMethodId: method.id,
331
331
  label: method.label,
332
332
  });
333
- const principal = await ensureConsumerPrincipal();
333
+ const principal = await ensureConsumerPrincipalForMethod("link");
334
334
  return text(
335
335
  [
336
336
  "Link payment method connected.",
@@ -380,7 +380,7 @@ export function registerWalletTools(server: McpServer): void {
380
380
  const result = await pollCardSetup(pendingToken, 250);
381
381
 
382
382
  if (result) {
383
- const principal = await ensureConsumerPrincipal();
383
+ const principal = await ensureConsumerPrincipalForMethod("card");
384
384
  return text(
385
385
  `Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
386
386
  `Consumer principal: ${principal}`,