@agentwonderland/mcp 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/__tests__/api-client.test.js +6 -2
- package/dist/core/__tests__/link-cli.test.d.ts +1 -0
- package/dist/core/__tests__/link-cli.test.js +102 -0
- package/dist/core/__tests__/principal.test.js +25 -0
- package/dist/core/api-client.js +4 -2
- package/dist/core/link-cli.d.ts +5 -0
- package/dist/core/link-cli.js +25 -25
- 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__/run.test.js +26 -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/run.js +6 -0
- package/dist/tools/solve.js +8 -1
- 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__/link-cli.test.ts +125 -0
- package/src/core/__tests__/principal.test.ts +31 -0
- package/src/core/api-client.ts +4 -2
- package/src/core/link-cli.ts +25 -27
- 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__/run.test.ts +33 -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/run.ts +7 -0
- package/src/tools/solve.ts +8 -1
- 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
|
}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const { execCalls, outputs, state, mockExecFile, } = vi.hoisted(() => {
|
|
3
|
+
const execCalls = [];
|
|
4
|
+
const outputs = [];
|
|
5
|
+
const state = {
|
|
6
|
+
pending: null,
|
|
7
|
+
pendingWrites: [],
|
|
8
|
+
};
|
|
9
|
+
const mockExecFile = vi.fn((file, args, _options, callback) => {
|
|
10
|
+
execCalls.push({ file, args });
|
|
11
|
+
const next = outputs.shift();
|
|
12
|
+
if (next instanceof Error) {
|
|
13
|
+
callback(next);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
callback(null, JSON.stringify(next ?? null), "");
|
|
17
|
+
});
|
|
18
|
+
mockExecFile[Symbol.for("nodejs.util.promisify.custom")] = async (file, args) => {
|
|
19
|
+
execCalls.push({ file, args });
|
|
20
|
+
const next = outputs.shift();
|
|
21
|
+
if (next instanceof Error) {
|
|
22
|
+
throw next;
|
|
23
|
+
}
|
|
24
|
+
return { stdout: JSON.stringify(next ?? null), stderr: "" };
|
|
25
|
+
};
|
|
26
|
+
return { execCalls, outputs, state, mockExecFile };
|
|
27
|
+
});
|
|
28
|
+
vi.mock("node:child_process", () => ({
|
|
29
|
+
execFile: mockExecFile,
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../config.js", () => ({
|
|
32
|
+
getPendingLinkSpendRequest: () => state.pending,
|
|
33
|
+
setPendingLinkSpendRequest: (pending) => {
|
|
34
|
+
state.pending = pending;
|
|
35
|
+
state.pendingWrites.push(pending);
|
|
36
|
+
},
|
|
37
|
+
setLinkCooldown: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
describe("Link CLI spend requests", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
execCalls.length = 0;
|
|
43
|
+
outputs.length = 0;
|
|
44
|
+
state.pending = null;
|
|
45
|
+
state.pendingWrites = [];
|
|
46
|
+
});
|
|
47
|
+
it("returns an approval-required error immediately instead of blocking on Link approval", async () => {
|
|
48
|
+
outputs.push([
|
|
49
|
+
{
|
|
50
|
+
id: "lsrq_test_123",
|
|
51
|
+
approval_url: "https://link.example/approve/lsrq_test_123",
|
|
52
|
+
status: "pending_approval",
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
const { createLinkSharedPaymentToken, LinkApprovalRequiredError } = await import("../link-cli.js");
|
|
56
|
+
await expect(createLinkSharedPaymentToken({
|
|
57
|
+
amount: "10",
|
|
58
|
+
currency: "usd",
|
|
59
|
+
context: "Agent Wonderland test",
|
|
60
|
+
expiresAt: Math.floor(Date.now() / 1000) + 300,
|
|
61
|
+
networkId: "profile_test",
|
|
62
|
+
paymentMethodId: "csmrpd_test_123",
|
|
63
|
+
})).rejects.toBeInstanceOf(LinkApprovalRequiredError);
|
|
64
|
+
expect(execCalls).toHaveLength(1);
|
|
65
|
+
expect(execCalls[0]?.args).toContain("create");
|
|
66
|
+
expect(state.pendingWrites[0]).toMatchObject({
|
|
67
|
+
id: "lsrq_test_123",
|
|
68
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
69
|
+
amount: "10",
|
|
70
|
+
currency: "usd",
|
|
71
|
+
networkId: "profile_test",
|
|
72
|
+
paymentMethodId: "csmrpd_test_123",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("resumes a stored Link spend request and returns the approved shared payment token", async () => {
|
|
76
|
+
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
77
|
+
state.pending = {
|
|
78
|
+
id: "lsrq_test_123",
|
|
79
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
80
|
+
amount: "10",
|
|
81
|
+
currency: "usd",
|
|
82
|
+
context: "Agent Wonderland test",
|
|
83
|
+
expiresAt,
|
|
84
|
+
networkId: "profile_test",
|
|
85
|
+
paymentMethodId: "csmrpd_test_123",
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
outputs.push({ shared_payment_token: "spt_test_123" });
|
|
89
|
+
const { createLinkSharedPaymentToken } = await import("../link-cli.js");
|
|
90
|
+
await expect(createLinkSharedPaymentToken({
|
|
91
|
+
amount: "10",
|
|
92
|
+
currency: "usd",
|
|
93
|
+
context: "Agent Wonderland test",
|
|
94
|
+
expiresAt,
|
|
95
|
+
networkId: "profile_test",
|
|
96
|
+
paymentMethodId: "csmrpd_test_123",
|
|
97
|
+
})).resolves.toBe("spt_test_123");
|
|
98
|
+
expect(execCalls).toHaveLength(1);
|
|
99
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
|
|
100
|
+
expect(state.pendingWrites).toEqual([null]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -106,6 +106,19 @@ describe("consumer principal helpers", () => {
|
|
|
106
106
|
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
107
107
|
expect(state.addedWallets).toHaveLength(1);
|
|
108
108
|
});
|
|
109
|
+
it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
|
|
110
|
+
state.wallets = [
|
|
111
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
112
|
+
];
|
|
113
|
+
state.addresses = {
|
|
114
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
115
|
+
};
|
|
116
|
+
const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
|
|
117
|
+
const principal = await ensureConsumerPrincipalForMethod("link");
|
|
118
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
119
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
120
|
+
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
121
|
+
});
|
|
109
122
|
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
110
123
|
state.wallets = [
|
|
111
124
|
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
@@ -119,4 +132,16 @@ describe("consumer principal helpers", () => {
|
|
|
119
132
|
const principal = await getBaseRebatePrincipal();
|
|
120
133
|
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
121
134
|
});
|
|
135
|
+
it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
|
|
136
|
+
state.wallets = [
|
|
137
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
138
|
+
];
|
|
139
|
+
state.addresses = {
|
|
140
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
141
|
+
};
|
|
142
|
+
const { ensureBaseRebatePrincipal } = await import("../principal.js");
|
|
143
|
+
const principal = await ensureBaseRebatePrincipal();
|
|
144
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
145
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
146
|
+
});
|
|
122
147
|
});
|
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/link-cli.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare class LinkApprovalRequiredError extends Error {
|
|
2
|
+
readonly spendRequestId: string;
|
|
3
|
+
readonly approvalUrl?: string | undefined;
|
|
4
|
+
constructor(spendRequestId: string, approvalUrl?: string | undefined);
|
|
5
|
+
}
|
|
1
6
|
export interface LinkCliAuthStatus {
|
|
2
7
|
authenticated: boolean;
|
|
3
8
|
credentialsPath?: string;
|
package/dist/core/link-cli.js
CHANGED
|
@@ -4,8 +4,23 @@ import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
5
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
6
6
|
const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export class LinkApprovalRequiredError extends Error {
|
|
8
|
+
spendRequestId;
|
|
9
|
+
approvalUrl;
|
|
10
|
+
constructor(spendRequestId, approvalUrl) {
|
|
11
|
+
super(formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl));
|
|
12
|
+
this.spendRequestId = spendRequestId;
|
|
13
|
+
this.approvalUrl = approvalUrl;
|
|
14
|
+
this.name = "LinkApprovalRequiredError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl) {
|
|
18
|
+
return [
|
|
19
|
+
"Link approval required.",
|
|
20
|
+
"The agent has not run yet and no charge has been captured.",
|
|
21
|
+
approvalUrl ? `Approve this spend request in Link: ${approvalUrl}` : `Spend request: ${spendRequestId}`,
|
|
22
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
23
|
+
].join("\n");
|
|
9
24
|
}
|
|
10
25
|
async function runLinkCli(args, timeout = LINK_CLI_TIMEOUT_MS) {
|
|
11
26
|
try {
|
|
@@ -220,6 +235,9 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
220
235
|
}
|
|
221
236
|
catch (err) {
|
|
222
237
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
if (err instanceof LinkApprovalRequiredError) {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
223
241
|
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
224
242
|
throw err;
|
|
225
243
|
}
|
|
@@ -293,39 +311,21 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
293
311
|
if (approval.approvalUrl) {
|
|
294
312
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
295
313
|
}
|
|
296
|
-
|
|
297
|
-
setPendingLinkSpendRequest(null);
|
|
298
|
-
return retrievedSpt;
|
|
314
|
+
throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
|
|
299
315
|
}
|
|
300
316
|
{
|
|
301
317
|
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
302
318
|
}
|
|
303
319
|
}
|
|
304
320
|
async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
|
|
305
|
-
|
|
321
|
+
const retrieved = await runLinkCli([
|
|
306
322
|
"spend-request",
|
|
307
323
|
"retrieve",
|
|
308
324
|
spendRequestId,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
"--max-attempts",
|
|
312
|
-
"150",
|
|
313
|
-
]);
|
|
314
|
-
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
315
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
316
|
-
await sleep(2_000);
|
|
317
|
-
retrieved = await runLinkCli([
|
|
318
|
-
"spend-request",
|
|
319
|
-
"retrieve",
|
|
320
|
-
spendRequestId,
|
|
321
|
-
]);
|
|
322
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
323
|
-
}
|
|
325
|
+
], 30_000);
|
|
326
|
+
const retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
324
327
|
if (retrievedSpt) {
|
|
325
328
|
return retrievedSpt;
|
|
326
329
|
}
|
|
327
|
-
throw new
|
|
328
|
-
"Link spend request finished without a shared payment token.",
|
|
329
|
-
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
330
|
-
].filter(Boolean).join("\n"));
|
|
330
|
+
throw new LinkApprovalRequiredError(spendRequestId, approvalUrl);
|
|
331
331
|
}
|
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
|
+
});
|
|
@@ -146,4 +146,30 @@ describe("run_agent MCP tool", () => {
|
|
|
146
146
|
expect(text).toContain("Paid: $0.01 via card");
|
|
147
147
|
expect(text).toContain("Job ID: job-1");
|
|
148
148
|
});
|
|
149
|
+
it("returns Link approval instructions without wrapping them as an execution error", async () => {
|
|
150
|
+
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
151
|
+
mockGetCompatiblePaymentMethods.mockReturnValue(["link"]);
|
|
152
|
+
mockApiPostWithPayment.mockRejectedValueOnce(new Error([
|
|
153
|
+
"Link approval required.",
|
|
154
|
+
"The agent has not run yet and no charge has been captured.",
|
|
155
|
+
"Approve this spend request in Link: https://link.example/approve/lsrq_test",
|
|
156
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
157
|
+
].join("\n")));
|
|
158
|
+
const { registerRunTools } = await import("../run.js");
|
|
159
|
+
const harness = makeServerHarness();
|
|
160
|
+
registerRunTools(harness.server);
|
|
161
|
+
const runAgent = harness.handlers.get("run_agent");
|
|
162
|
+
expect(runAgent).toBeDefined();
|
|
163
|
+
const result = await runAgent({
|
|
164
|
+
agent_id: selectedAgent.id,
|
|
165
|
+
input: { text: "hello", target_language: "es" },
|
|
166
|
+
pay_with: "link",
|
|
167
|
+
confirmed: true,
|
|
168
|
+
});
|
|
169
|
+
const text = flattenToolText(result);
|
|
170
|
+
expect(text).toContain("Link approval required.");
|
|
171
|
+
expect(text).toContain("https://link.example/approve/lsrq_test");
|
|
172
|
+
expect(text).not.toContain("Error: Link approval required.");
|
|
173
|
+
expect(mockRecordSpend).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
149
175
|
});
|
|
@@ -124,6 +124,7 @@ vi.mock("../../core/ows-adapter.js", () => ({
|
|
|
124
124
|
}));
|
|
125
125
|
vi.mock("../../core/principal.js", () => ({
|
|
126
126
|
ensureConsumerPrincipal: async () => state.consumerPrincipal,
|
|
127
|
+
ensureConsumerPrincipalForMethod: async () => state.consumerPrincipal,
|
|
127
128
|
getConsumerPrincipal: async () => state.consumerPrincipal,
|
|
128
129
|
}));
|
|
129
130
|
function resetState() {
|
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/run.js
CHANGED
|
@@ -166,6 +166,9 @@ export function registerRunTools(server) {
|
|
|
166
166
|
` Cost: $${price.toFixed(2)}`,
|
|
167
167
|
` Payment: ${formatPaymentLabel(method)}`,
|
|
168
168
|
];
|
|
169
|
+
if (method === "link") {
|
|
170
|
+
quoteLines.push(" Link: after confirming here, approve the Link spend request and rerun the confirmed call.");
|
|
171
|
+
}
|
|
169
172
|
const creditPackLines = buildCreditPackOfferLines(agent);
|
|
170
173
|
if (creditPackLines.length > 0) {
|
|
171
174
|
quoteLines.push("", ...creditPackLines);
|
|
@@ -226,6 +229,9 @@ export function registerRunTools(server) {
|
|
|
226
229
|
].join("\n"));
|
|
227
230
|
}
|
|
228
231
|
const msg = apiErr?.message ?? "Failed to run agent";
|
|
232
|
+
if (msg.includes("Link approval required.")) {
|
|
233
|
+
return text(msg);
|
|
234
|
+
}
|
|
229
235
|
if (msg.includes("Missing required field") || msg.includes("validation failed")) {
|
|
230
236
|
return text(`Error: ${msg}\n\nUse get_agent("${agent_id}") to see the required input fields.`);
|
|
231
237
|
}
|
package/dist/tools/solve.js
CHANGED
|
@@ -241,6 +241,9 @@ export function registerSolveTools(server) {
|
|
|
241
241
|
`Best match: ${selected.name}`,
|
|
242
242
|
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
243
243
|
`Payment: ${formatPaymentLabel(method)}`,
|
|
244
|
+
...(method === "link"
|
|
245
|
+
? ["Link: after confirming here, approve the Link spend request and rerun the confirmed call."]
|
|
246
|
+
: []),
|
|
244
247
|
...(() => {
|
|
245
248
|
const summary = buildCreditPackSummary(selected);
|
|
246
249
|
return summary.length > 0 ? ["", ...summary] : [];
|
|
@@ -298,7 +301,11 @@ export function registerSolveTools(server) {
|
|
|
298
301
|
})(),
|
|
299
302
|
].join("\n"));
|
|
300
303
|
}
|
|
301
|
-
|
|
304
|
+
const msg = apiErr?.message ?? "Failed to run agent";
|
|
305
|
+
if (msg.includes("Link approval required.")) {
|
|
306
|
+
return text(msg);
|
|
307
|
+
}
|
|
308
|
+
return text(`Error: ${msg}`);
|
|
302
309
|
}
|
|
303
310
|
pendingSolves.delete(pendingKey);
|
|
304
311
|
const jobId = result.job_id ?? "";
|