@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.
- package/dist/core/__tests__/api-client.test.js +6 -2
- package/dist/core/__tests__/principal.test.js +25 -0
- package/dist/core/api-client.js +4 -2
- package/dist/core/principal.d.ts +1 -0
- package/dist/core/principal.js +49 -1
- package/dist/index.js +3 -0
- package/dist/tools/__tests__/rebates.test.d.ts +1 -0
- package/dist/tools/__tests__/rebates.test.js +72 -0
- package/dist/tools/__tests__/wallet.test.js +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/observability.d.ts +2 -0
- package/dist/tools/observability.js +20 -0
- package/dist/tools/rebates.d.ts +2 -0
- package/dist/tools/rebates.js +51 -0
- package/dist/tools/wallet.js +4 -4
- package/package.json +1 -1
- package/src/core/__tests__/api-client.test.ts +8 -1
- package/src/core/__tests__/principal.test.ts +31 -0
- package/src/core/api-client.ts +4 -2
- package/src/core/principal.ts +54 -1
- package/src/index.ts +3 -0
- package/src/tools/__tests__/rebates.test.ts +91 -0
- package/src/tools/__tests__/wallet.test.ts +1 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/rebates.ts +102 -0
- package/src/tools/wallet.ts +4 -4
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
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":
|
|
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
|
});
|
package/dist/core/api-client.js
CHANGED
|
@@ -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 =
|
|
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
|
}
|
package/dist/core/principal.d.ts
CHANGED
|
@@ -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>;
|
package/dist/core/principal.js
CHANGED
|
@@ -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
|
|
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() {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tools/index.js
CHANGED
|
@@ -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,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,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/wallet.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,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":
|
|
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
|
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { getApiUrl, getApiKey } from "./config.js";
|
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
3
|
import {
|
|
4
4
|
getBaseRebatePrincipal,
|
|
5
|
-
|
|
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 =
|
|
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
|
}
|
package/src/core/principal.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tools/wallet.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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}`,
|