@agentwonderland/mcp 0.1.25 → 0.1.26
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.d.ts +1 -0
- package/dist/core/__tests__/api-client.test.js +51 -0
- package/dist/core/__tests__/formatters.test.js +10 -0
- package/dist/core/__tests__/passes-api.test.d.ts +1 -0
- package/dist/core/__tests__/passes-api.test.js +27 -0
- package/dist/core/__tests__/payments.test.js +10 -6
- package/dist/core/__tests__/principal.test.js +41 -4
- package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
- package/dist/core/__tests__/solana-charge.test.js +50 -0
- package/dist/core/api-client.d.ts +1 -0
- package/dist/core/api-client.js +8 -3
- package/dist/core/balances.d.ts +1 -0
- package/dist/core/balances.js +56 -0
- package/dist/core/base-charge.js +13 -6
- package/dist/core/formatters.d.ts +3 -2
- package/dist/core/formatters.js +7 -1
- package/dist/core/passes.d.ts +1 -1
- package/dist/core/passes.js +5 -2
- package/dist/core/payments.d.ts +1 -0
- package/dist/core/payments.js +20 -7
- package/dist/core/principal.d.ts +3 -0
- package/dist/core/principal.js +29 -1
- package/dist/core/settings.d.ts +20 -0
- package/dist/core/settings.js +19 -0
- package/dist/core/solana-charge.d.ts +5 -0
- package/dist/core/solana-charge.js +29 -7
- package/dist/core/tempo-charge.d.ts +7 -0
- package/dist/core/tempo-charge.js +84 -0
- package/dist/index.js +5 -4
- package/dist/tools/__tests__/jobs.test.d.ts +1 -0
- package/dist/tools/__tests__/jobs.test.js +71 -0
- package/dist/tools/__tests__/run.test.d.ts +1 -0
- package/dist/tools/__tests__/run.test.js +149 -0
- package/dist/tools/__tests__/solve.test.d.ts +1 -0
- package/dist/tools/__tests__/solve.test.js +158 -0
- package/dist/tools/__tests__/wallet.test.d.ts +1 -0
- package/dist/tools/__tests__/wallet.test.js +230 -0
- package/dist/tools/_payment-confirmation.js +1 -1
- package/dist/tools/jobs.js +8 -1
- package/dist/tools/passes.js +11 -6
- package/dist/tools/run.js +16 -12
- package/dist/tools/solve.js +22 -15
- package/dist/tools/wallet.js +32 -12
- package/package.json +2 -2
- package/src/core/__tests__/api-client.test.ts +78 -0
- package/src/core/__tests__/formatters.test.ts +12 -0
- package/src/core/__tests__/passes-api.test.ts +33 -0
- package/src/core/__tests__/payments.test.ts +17 -6
- package/src/core/__tests__/principal.test.ts +49 -4
- package/src/core/__tests__/solana-charge.test.ts +59 -0
- package/src/core/api-client.ts +16 -3
- package/src/core/balances.ts +63 -0
- package/src/core/base-charge.ts +13 -6
- package/src/core/formatters.ts +10 -3
- package/src/core/passes.ts +5 -2
- package/src/core/payments.ts +22 -7
- package/src/core/principal.ts +42 -1
- package/src/core/settings.ts +36 -0
- package/src/core/solana-charge.ts +43 -9
- package/src/core/tempo-charge.ts +104 -0
- package/src/index.ts +5 -4
- package/src/tools/__tests__/jobs.test.ts +89 -0
- package/src/tools/__tests__/run.test.ts +176 -0
- package/src/tools/__tests__/solve.test.ts +186 -0
- package/src/tools/__tests__/wallet.test.ts +289 -0
- package/src/tools/_payment-confirmation.ts +1 -1
- package/src/tools/jobs.ts +10 -1
- package/src/tools/passes.ts +11 -5
- package/src/tools/run.ts +19 -11
- package/src/tools/solve.ts +25 -14
- package/src/tools/wallet.ts +30 -14
|
@@ -9,6 +9,7 @@ import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction
|
|
|
9
9
|
import {
|
|
10
10
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
11
11
|
TOKEN_PROGRAM_ID,
|
|
12
|
+
createAssociatedTokenAccountInstruction,
|
|
12
13
|
createTransferCheckedInstruction,
|
|
13
14
|
getAssociatedTokenAddressSync,
|
|
14
15
|
} from "@solana/spl-token";
|
|
@@ -75,24 +76,40 @@ interface SolanaChargeClientConfig {
|
|
|
75
76
|
rpcUrl?: string;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
async function resolveRecipientTokenAccount(
|
|
79
|
+
export async function resolveRecipientTokenAccount(
|
|
79
80
|
connection: Connection,
|
|
80
81
|
mint: PublicKey,
|
|
81
82
|
recipient: PublicKey,
|
|
82
|
-
): Promise<PublicKey> {
|
|
83
|
+
): Promise<{ tokenAccount: PublicKey; needsCreateAssociatedAccount: boolean }> {
|
|
83
84
|
const accountInfo = await connection.getParsedAccountInfo(recipient, "confirmed");
|
|
84
85
|
const owner = accountInfo.value?.owner;
|
|
85
86
|
if (owner && owner.toBase58() === TOKEN_PROGRAM_ID.toBase58()) {
|
|
86
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
tokenAccount: recipient,
|
|
89
|
+
needsCreateAssociatedAccount: false,
|
|
90
|
+
};
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
const associatedTokenAccount = getAssociatedTokenAddressSync(
|
|
90
94
|
mint,
|
|
91
95
|
recipient,
|
|
92
96
|
false,
|
|
93
97
|
TOKEN_PROGRAM_ID,
|
|
94
98
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
95
99
|
);
|
|
100
|
+
const associatedAccountInfo = await connection.getParsedAccountInfo(associatedTokenAccount, "confirmed");
|
|
101
|
+
const associatedOwner = associatedAccountInfo.value?.owner;
|
|
102
|
+
if (associatedOwner && associatedOwner.toBase58() === TOKEN_PROGRAM_ID.toBase58()) {
|
|
103
|
+
return {
|
|
104
|
+
tokenAccount: associatedTokenAccount,
|
|
105
|
+
needsCreateAssociatedAccount: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
tokenAccount: associatedTokenAccount,
|
|
111
|
+
needsCreateAssociatedAccount: true,
|
|
112
|
+
};
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
export function solanaChargeClient(config: SolanaChargeClientConfig) {
|
|
@@ -114,27 +131,44 @@ export function solanaChargeClient(config: SolanaChargeClientConfig) {
|
|
|
114
131
|
TOKEN_PROGRAM_ID,
|
|
115
132
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
116
133
|
);
|
|
117
|
-
const
|
|
134
|
+
const destination = await resolveRecipientTokenAccount(
|
|
118
135
|
connection,
|
|
119
136
|
mint,
|
|
120
137
|
recipient,
|
|
121
138
|
);
|
|
139
|
+
const instructions = [];
|
|
140
|
+
|
|
141
|
+
if (destination.needsCreateAssociatedAccount) {
|
|
142
|
+
instructions.push(
|
|
143
|
+
createAssociatedTokenAccountInstruction(
|
|
144
|
+
owner,
|
|
145
|
+
destination.tokenAccount,
|
|
146
|
+
recipient,
|
|
147
|
+
mint,
|
|
148
|
+
TOKEN_PROGRAM_ID,
|
|
149
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
122
153
|
|
|
123
|
-
|
|
154
|
+
instructions.push(createTransferCheckedInstruction(
|
|
124
155
|
sourceTokenAccount,
|
|
125
156
|
mint,
|
|
126
|
-
|
|
157
|
+
destination.tokenAccount,
|
|
127
158
|
owner,
|
|
128
159
|
amount,
|
|
129
160
|
decimals,
|
|
130
|
-
);
|
|
161
|
+
));
|
|
131
162
|
|
|
132
163
|
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
133
164
|
|
|
134
165
|
const transaction = new Transaction({
|
|
135
166
|
feePayer: owner,
|
|
136
167
|
recentBlockhash: blockhash,
|
|
137
|
-
})
|
|
168
|
+
});
|
|
169
|
+
for (const instruction of instructions) {
|
|
170
|
+
transaction.add(instruction);
|
|
171
|
+
}
|
|
138
172
|
|
|
139
173
|
const signature = await sendAndConfirmTransaction(connection, transaction, [keypair], {
|
|
140
174
|
commitment: "confirmed",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom MPP payment method: Tempo EVM USDC charge (client-side).
|
|
3
|
+
*
|
|
4
|
+
* Sends a standard ERC-20 transfer on Tempo and returns the tx hash as the
|
|
5
|
+
* payment credential.
|
|
6
|
+
*/
|
|
7
|
+
import { Method, Credential, z } from "mppx";
|
|
8
|
+
import type { LocalAccount } from "viem/accounts";
|
|
9
|
+
import { toAtomicAmount } from "./amount-utils.js";
|
|
10
|
+
|
|
11
|
+
const TEMPO_USDC = "0x20c000000000000000000000b9537d11c60e8b50" as const;
|
|
12
|
+
const PATH_USD = "0x20c0000000000000000000000000000000000000" as const;
|
|
13
|
+
const TEMPO_MAINNET_CHAIN_ID = 4217;
|
|
14
|
+
const TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
15
|
+
|
|
16
|
+
const ERC20_ABI = [
|
|
17
|
+
{
|
|
18
|
+
type: "function",
|
|
19
|
+
name: "transfer",
|
|
20
|
+
inputs: [
|
|
21
|
+
{ name: "to", type: "address" },
|
|
22
|
+
{ name: "value", type: "uint256" },
|
|
23
|
+
],
|
|
24
|
+
outputs: [{ name: "", type: "bool" }],
|
|
25
|
+
stateMutability: "nonpayable",
|
|
26
|
+
},
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const tempoChargeMethod = Method.from({
|
|
30
|
+
name: "tempo" as const,
|
|
31
|
+
intent: "charge" as const,
|
|
32
|
+
schema: {
|
|
33
|
+
credential: {
|
|
34
|
+
payload: z.object({
|
|
35
|
+
hash: z.string(),
|
|
36
|
+
type: z.literal("hash"),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
request: z.pipe(
|
|
40
|
+
z.object({
|
|
41
|
+
amount: z.string(),
|
|
42
|
+
currency: z.string(),
|
|
43
|
+
recipient: z.string(),
|
|
44
|
+
chainId: z.optional(z.number()),
|
|
45
|
+
decimals: z.optional(z.number()),
|
|
46
|
+
}),
|
|
47
|
+
z.transform((v: any) => ({
|
|
48
|
+
...v,
|
|
49
|
+
methodDetails: {
|
|
50
|
+
chainId: v.chainId ?? TEMPO_MAINNET_CHAIN_ID,
|
|
51
|
+
},
|
|
52
|
+
})),
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
interface TempoChargeClientConfig {
|
|
58
|
+
account: LocalAccount;
|
|
59
|
+
rpcUrl?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function tempoChargeClient(config: TempoChargeClientConfig) {
|
|
63
|
+
return Method.toClient(tempoChargeMethod as any, {
|
|
64
|
+
async createCredential({ challenge }: any) {
|
|
65
|
+
const { request } = challenge;
|
|
66
|
+
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
67
|
+
const recipient = request.recipient as `0x${string}`;
|
|
68
|
+
const currency = (request.currency ?? TEMPO_USDC) as `0x${string}`;
|
|
69
|
+
const chainId = request.methodDetails?.chainId ?? request.chainId ?? TEMPO_MAINNET_CHAIN_ID;
|
|
70
|
+
|
|
71
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
72
|
+
const { tempo, tempoModerato } = await import("viem/chains");
|
|
73
|
+
|
|
74
|
+
const chain = chainId === TEMPO_TESTNET_CHAIN_ID ? tempoModerato : tempo;
|
|
75
|
+
const rpcUrl = config.rpcUrl ?? chain.rpcUrls.default.http[0];
|
|
76
|
+
|
|
77
|
+
const walletClient = createWalletClient({
|
|
78
|
+
account: config.account,
|
|
79
|
+
chain,
|
|
80
|
+
transport: http(rpcUrl),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const publicClient = createPublicClient({
|
|
84
|
+
chain,
|
|
85
|
+
transport: http(rpcUrl),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const hash = await walletClient.writeContract({
|
|
89
|
+
address: currency,
|
|
90
|
+
abi: ERC20_ABI,
|
|
91
|
+
functionName: "transfer",
|
|
92
|
+
args: [recipient, amount],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await publicClient.waitForTransactionReceipt({ hash });
|
|
96
|
+
|
|
97
|
+
return Credential.serialize({
|
|
98
|
+
challenge,
|
|
99
|
+
payload: { hash, type: "hash" as const },
|
|
100
|
+
source: `did:pkh:eip155:${chainId}:${config.account.address}`,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -47,11 +47,12 @@ export async function startMcpServer(): Promise<void> {
|
|
|
47
47
|
"5. Ask user to rate or tip after a successful run",
|
|
48
48
|
"",
|
|
49
49
|
"PAYMENT:",
|
|
50
|
-
"-
|
|
51
|
-
"-
|
|
50
|
+
"- Crypto wallets (Tempo USDC, Base USDC, Solana USDC) are the core supported rails.",
|
|
51
|
+
"- Saved cards can also be used, but card-backed MPP depends on Stripe Shared Payment Token availability in the current environment.",
|
|
52
52
|
"- Card and crypto are SEPARATE payment methods. Card charges a credit card; crypto sends USDC on-chain.",
|
|
53
|
-
"-
|
|
54
|
-
"- run_agent() and solve()
|
|
53
|
+
"- When a card is configured, it becomes the default payment method. Use wallet_status to confirm whether `Card MPP: ready` before relying on it.",
|
|
54
|
+
"- run_agent() and solve() auto-detect the default compatible payment method when pay_with is omitted.",
|
|
55
|
+
"- Include pay_with explicitly when you need a specific rail or want deterministic control over the payment method used.",
|
|
55
56
|
"- Use wallet_status to see the exact payment methods the user has configured before calling a paid tool.",
|
|
56
57
|
"- Use open_observability_dashboard() to open a secure web usage dashboard for runs/spend/rebates.",
|
|
57
58
|
"- Payment is automatic once configured. Users are never charged for failed runs.",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockApiGet = vi.fn();
|
|
4
|
+
const mockIsAuthenticated = vi.fn();
|
|
5
|
+
const mockHasWalletConfigured = vi.fn();
|
|
6
|
+
const mockGetWalletAddress = vi.fn();
|
|
7
|
+
const mockFormatRunResult = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock("../../core/api-client.js", () => ({
|
|
10
|
+
apiGet: mockApiGet,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../../core/config.js", () => ({
|
|
14
|
+
isAuthenticated: mockIsAuthenticated,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../../core/payments.js", () => ({
|
|
18
|
+
hasWalletConfigured: mockHasWalletConfigured,
|
|
19
|
+
getWalletAddress: mockGetWalletAddress,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../../core/formatters.js", () => ({
|
|
23
|
+
formatRunResult: mockFormatRunResult,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
function flattenToolText(result: unknown): string {
|
|
27
|
+
const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
|
|
28
|
+
return content
|
|
29
|
+
.filter((item) => item?.type === "text")
|
|
30
|
+
.map((item) => item.text ?? "")
|
|
31
|
+
.join("\n\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeServerHarness() {
|
|
35
|
+
const handlers = new Map<string, (args: Record<string, unknown>) => Promise<unknown>>();
|
|
36
|
+
return {
|
|
37
|
+
handlers,
|
|
38
|
+
server: {
|
|
39
|
+
tool(name: string, _description: string, _schema: unknown, handler: (args: Record<string, unknown>) => Promise<unknown>) {
|
|
40
|
+
handlers.set(name, handler);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("job MCP tools", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules();
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
mockIsAuthenticated.mockReturnValue(false);
|
|
51
|
+
mockHasWalletConfigured.mockReturnValue(true);
|
|
52
|
+
mockGetWalletAddress.mockResolvedValue("0xabc123");
|
|
53
|
+
mockFormatRunResult.mockReturnValue("formatted job result");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("appends the wallet address when looking up a job for an unauthenticated consumer", async () => {
|
|
57
|
+
mockApiGet.mockResolvedValueOnce({ status: "completed", job_id: "job-123" });
|
|
58
|
+
|
|
59
|
+
const { registerJobTools } = await import("../jobs.js");
|
|
60
|
+
const harness = makeServerHarness();
|
|
61
|
+
registerJobTools(harness.server as never);
|
|
62
|
+
|
|
63
|
+
const getJob = harness.handlers.get("get_job");
|
|
64
|
+
expect(getJob).toBeDefined();
|
|
65
|
+
|
|
66
|
+
const result = await getJob!({ job_id: "job-123" });
|
|
67
|
+
const text = flattenToolText(result);
|
|
68
|
+
|
|
69
|
+
expect(mockApiGet).toHaveBeenCalledWith("/jobs/job-123?wallet=0xabc123");
|
|
70
|
+
expect(text).toContain("formatted job result");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns a processing message while the async job is still running", async () => {
|
|
74
|
+
mockApiGet.mockResolvedValueOnce({ status: "processing" });
|
|
75
|
+
|
|
76
|
+
const { registerJobTools } = await import("../jobs.js");
|
|
77
|
+
const harness = makeServerHarness();
|
|
78
|
+
registerJobTools(harness.server as never);
|
|
79
|
+
|
|
80
|
+
const getJob = harness.handlers.get("get_job");
|
|
81
|
+
expect(getJob).toBeDefined();
|
|
82
|
+
|
|
83
|
+
const result = await getJob!({ job_id: "job-456" });
|
|
84
|
+
const text = flattenToolText(result);
|
|
85
|
+
|
|
86
|
+
expect(mockApiGet).toHaveBeenCalledWith("/jobs/job-456?wallet=0xabc123");
|
|
87
|
+
expect(text).toContain("Job job-456 is still processing");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockApiGet = vi.fn();
|
|
4
|
+
const mockApiPost = vi.fn();
|
|
5
|
+
const mockApiPostWithPayment = vi.fn();
|
|
6
|
+
const mockGetCompatiblePaymentMethods = vi.fn();
|
|
7
|
+
const mockHasWalletConfigured = vi.fn();
|
|
8
|
+
const mockGetConfiguredMethods = vi.fn();
|
|
9
|
+
const mockGetWalletAddress = vi.fn();
|
|
10
|
+
const mockNormalizePaymentMethod = vi.fn();
|
|
11
|
+
const mockRequiresSpendConfirmation = vi.fn();
|
|
12
|
+
const mockGetDefaultTipAmount = vi.fn();
|
|
13
|
+
const mockCanSpend = vi.fn();
|
|
14
|
+
const mockRecordSpend = vi.fn();
|
|
15
|
+
const mockRequiresPolicyConfirmation = vi.fn();
|
|
16
|
+
const mockUploadLocalFiles = vi.fn();
|
|
17
|
+
const mockStoreFeedbackToken = vi.fn();
|
|
18
|
+
const mockGetActiveCreditPack = vi.fn();
|
|
19
|
+
const mockGetCreditPackInventory = vi.fn();
|
|
20
|
+
const mockGetCreditPackProgram = vi.fn();
|
|
21
|
+
const mockGetOrCreatePendingCardSetup = vi.fn();
|
|
22
|
+
const mockFormatCardSetupBlocks = vi.fn();
|
|
23
|
+
|
|
24
|
+
vi.mock("../../core/api-client.js", () => ({
|
|
25
|
+
apiGet: mockApiGet,
|
|
26
|
+
apiPost: mockApiPost,
|
|
27
|
+
apiPostWithPayment: mockApiPostWithPayment,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("../../core/card-setup.js", () => ({
|
|
31
|
+
getOrCreatePendingCardSetup: mockGetOrCreatePendingCardSetup,
|
|
32
|
+
formatCardSetupBlocks: mockFormatCardSetupBlocks,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("../../core/payments.js", () => ({
|
|
36
|
+
getCompatiblePaymentMethods: mockGetCompatiblePaymentMethods,
|
|
37
|
+
getConfiguredMethods: mockGetConfiguredMethods,
|
|
38
|
+
hasWalletConfigured: mockHasWalletConfigured,
|
|
39
|
+
getWalletAddress: mockGetWalletAddress,
|
|
40
|
+
normalizePaymentMethod: mockNormalizePaymentMethod,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("../../core/config.js", () => ({
|
|
44
|
+
requiresSpendConfirmation: mockRequiresSpendConfirmation,
|
|
45
|
+
getDefaultTipAmount: mockGetDefaultTipAmount,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock("../../core/spend-policy.js", () => ({
|
|
49
|
+
canSpend: mockCanSpend,
|
|
50
|
+
recordSpend: mockRecordSpend,
|
|
51
|
+
requiresPolicyConfirmation: mockRequiresPolicyConfirmation,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock("../../core/file-upload.js", () => ({
|
|
55
|
+
uploadLocalFiles: mockUploadLocalFiles,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock("../_token-cache.js", () => ({
|
|
59
|
+
storeFeedbackToken: mockStoreFeedbackToken,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
vi.mock("../../core/passes.js", () => ({
|
|
63
|
+
formatCreditPackOffer: vi.fn(),
|
|
64
|
+
getActiveCreditPack: mockGetActiveCreditPack,
|
|
65
|
+
getCreditPackInventory: mockGetCreditPackInventory,
|
|
66
|
+
getCreditPackProgram: mockGetCreditPackProgram,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
function flattenToolText(result: unknown): string {
|
|
70
|
+
const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
|
|
71
|
+
return content
|
|
72
|
+
.filter((item) => item?.type === "text")
|
|
73
|
+
.map((item) => item.text ?? "")
|
|
74
|
+
.join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makeServerHarness() {
|
|
78
|
+
const handlers = new Map<string, (args: Record<string, unknown>) => Promise<unknown>>();
|
|
79
|
+
return {
|
|
80
|
+
handlers,
|
|
81
|
+
server: {
|
|
82
|
+
tool(name: string, _description: string, _schema: unknown, handler: (args: Record<string, unknown>) => Promise<unknown>) {
|
|
83
|
+
handlers.set(name, handler);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const selectedAgent = {
|
|
90
|
+
id: "agent-1",
|
|
91
|
+
name: "MockFixedPrice",
|
|
92
|
+
pricePerRunUsd: "0.01",
|
|
93
|
+
tags: ["translation"],
|
|
94
|
+
mcpSchema: null,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
describe("run_agent MCP tool", () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.resetModules();
|
|
100
|
+
vi.clearAllMocks();
|
|
101
|
+
|
|
102
|
+
mockHasWalletConfigured.mockReturnValue(true);
|
|
103
|
+
mockGetConfiguredMethods.mockReturnValue(["card"]);
|
|
104
|
+
mockGetCompatiblePaymentMethods.mockReturnValue(["card"]);
|
|
105
|
+
mockNormalizePaymentMethod.mockImplementation((method: string) => method);
|
|
106
|
+
mockRequiresSpendConfirmation.mockReturnValue(true);
|
|
107
|
+
mockGetDefaultTipAmount.mockReturnValue(0);
|
|
108
|
+
mockCanSpend.mockReturnValue({ ok: true, message: "" });
|
|
109
|
+
mockRequiresPolicyConfirmation.mockReturnValue(false);
|
|
110
|
+
mockUploadLocalFiles.mockResolvedValue({ input: { text: "hello", target_language: "es" }, uploads: [] });
|
|
111
|
+
mockGetActiveCreditPack.mockReturnValue(null);
|
|
112
|
+
mockGetCreditPackInventory.mockResolvedValue([]);
|
|
113
|
+
mockGetCreditPackProgram.mockReturnValue(null);
|
|
114
|
+
mockGetOrCreatePendingCardSetup.mockResolvedValue({ url: "https://example.com/card-setup" });
|
|
115
|
+
mockFormatCardSetupBlocks.mockReturnValue(["card setup"]);
|
|
116
|
+
mockGetWalletAddress.mockResolvedValue("0xabc");
|
|
117
|
+
mockApiGet.mockResolvedValue(selectedAgent);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("quotes explicit agent execution before running", async () => {
|
|
121
|
+
const { registerRunTools } = await import("../run.js");
|
|
122
|
+
const harness = makeServerHarness();
|
|
123
|
+
registerRunTools(harness.server as never);
|
|
124
|
+
|
|
125
|
+
const runAgent = harness.handlers.get("run_agent");
|
|
126
|
+
expect(runAgent).toBeDefined();
|
|
127
|
+
|
|
128
|
+
const result = await runAgent!({
|
|
129
|
+
agent_id: selectedAgent.id,
|
|
130
|
+
input: { text: "hello", target_language: "es" },
|
|
131
|
+
pay_with: "card",
|
|
132
|
+
});
|
|
133
|
+
const text = flattenToolText(result);
|
|
134
|
+
|
|
135
|
+
expect(text).toContain("Ready to run MockFixedPrice");
|
|
136
|
+
expect(text).toContain('run_agent({ agent_id: "agent-1", input: <same>, pay_with: "card", confirmed: true })');
|
|
137
|
+
expect(mockApiPostWithPayment).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("runs the explicitly selected agent when confirmed", async () => {
|
|
141
|
+
mockApiPostWithPayment.mockResolvedValueOnce({
|
|
142
|
+
status: "success",
|
|
143
|
+
job_id: "job-1",
|
|
144
|
+
agent_id: selectedAgent.id,
|
|
145
|
+
agent_name: selectedAgent.name,
|
|
146
|
+
output: { translated_text: "hola" },
|
|
147
|
+
cost: 0.01,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { registerRunTools } = await import("../run.js");
|
|
151
|
+
const harness = makeServerHarness();
|
|
152
|
+
registerRunTools(harness.server as never);
|
|
153
|
+
|
|
154
|
+
const runAgent = harness.handlers.get("run_agent");
|
|
155
|
+
expect(runAgent).toBeDefined();
|
|
156
|
+
|
|
157
|
+
const result = await runAgent!({
|
|
158
|
+
agent_id: selectedAgent.id,
|
|
159
|
+
input: { text: "hello", target_language: "es" },
|
|
160
|
+
pay_with: "card",
|
|
161
|
+
confirmed: true,
|
|
162
|
+
});
|
|
163
|
+
const text = flattenToolText(result);
|
|
164
|
+
|
|
165
|
+
expect(mockApiPostWithPayment).toHaveBeenCalledWith(
|
|
166
|
+
"/agents/agent-1/run",
|
|
167
|
+
{ input: { text: "hello", target_language: "es" } },
|
|
168
|
+
"card",
|
|
169
|
+
);
|
|
170
|
+
expect(mockRecordSpend).toHaveBeenCalledWith("card", 0.01);
|
|
171
|
+
expect(text).toContain("translated_text");
|
|
172
|
+
expect(text).toContain("✓ MockFixedPrice");
|
|
173
|
+
expect(text).toContain("Paid: $0.01 via card");
|
|
174
|
+
expect(text).toContain("Job ID: job-1");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockApiGet = vi.fn();
|
|
4
|
+
const mockApiPost = vi.fn();
|
|
5
|
+
const mockApiPostWithPayment = vi.fn();
|
|
6
|
+
const mockGetCompatiblePaymentMethods = vi.fn();
|
|
7
|
+
const mockHasWalletConfigured = vi.fn();
|
|
8
|
+
const mockGetConfiguredMethods = vi.fn();
|
|
9
|
+
const mockGetAcceptedPaymentMethods = vi.fn();
|
|
10
|
+
const mockGetWalletAddress = vi.fn();
|
|
11
|
+
const mockNormalizePaymentMethod = vi.fn();
|
|
12
|
+
const mockToRegistryPaymentMethod = vi.fn();
|
|
13
|
+
const mockRequiresSpendConfirmation = vi.fn();
|
|
14
|
+
const mockGetDefaultTipAmount = vi.fn();
|
|
15
|
+
const mockCanSpend = vi.fn();
|
|
16
|
+
const mockRecordSpend = vi.fn();
|
|
17
|
+
const mockRequiresPolicyConfirmation = vi.fn();
|
|
18
|
+
const mockUploadLocalFiles = vi.fn();
|
|
19
|
+
const mockStoreFeedbackToken = vi.fn();
|
|
20
|
+
const mockGetActiveCreditPack = vi.fn();
|
|
21
|
+
const mockGetCreditPackInventory = vi.fn();
|
|
22
|
+
const mockGetCreditPackProgram = vi.fn();
|
|
23
|
+
const mockGetOrCreatePendingCardSetup = vi.fn();
|
|
24
|
+
const mockFormatCardSetupBlocks = vi.fn();
|
|
25
|
+
|
|
26
|
+
vi.mock("../../core/api-client.js", () => ({
|
|
27
|
+
apiGet: mockApiGet,
|
|
28
|
+
apiPost: mockApiPost,
|
|
29
|
+
apiPostWithPayment: mockApiPostWithPayment,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("../../core/card-setup.js", () => ({
|
|
33
|
+
getOrCreatePendingCardSetup: mockGetOrCreatePendingCardSetup,
|
|
34
|
+
formatCardSetupBlocks: mockFormatCardSetupBlocks,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("../../core/payments.js", () => ({
|
|
38
|
+
getCompatiblePaymentMethods: mockGetCompatiblePaymentMethods,
|
|
39
|
+
hasWalletConfigured: mockHasWalletConfigured,
|
|
40
|
+
getConfiguredMethods: mockGetConfiguredMethods,
|
|
41
|
+
getAcceptedPaymentMethods: mockGetAcceptedPaymentMethods,
|
|
42
|
+
getWalletAddress: mockGetWalletAddress,
|
|
43
|
+
normalizePaymentMethod: mockNormalizePaymentMethod,
|
|
44
|
+
toRegistryPaymentMethod: mockToRegistryPaymentMethod,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("../../core/config.js", () => ({
|
|
48
|
+
requiresSpendConfirmation: mockRequiresSpendConfirmation,
|
|
49
|
+
getDefaultTipAmount: mockGetDefaultTipAmount,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock("../../core/spend-policy.js", () => ({
|
|
53
|
+
canSpend: mockCanSpend,
|
|
54
|
+
recordSpend: mockRecordSpend,
|
|
55
|
+
requiresPolicyConfirmation: mockRequiresPolicyConfirmation,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock("../../core/file-upload.js", () => ({
|
|
59
|
+
uploadLocalFiles: mockUploadLocalFiles,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
vi.mock("../_token-cache.js", () => ({
|
|
63
|
+
storeFeedbackToken: mockStoreFeedbackToken,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
vi.mock("../../core/passes.js", () => ({
|
|
67
|
+
getActiveCreditPack: mockGetActiveCreditPack,
|
|
68
|
+
getCreditPackInventory: mockGetCreditPackInventory,
|
|
69
|
+
getCreditPackProgram: mockGetCreditPackProgram,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
function flattenToolText(result: unknown): string {
|
|
73
|
+
const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
|
|
74
|
+
return content
|
|
75
|
+
.filter((item) => item?.type === "text")
|
|
76
|
+
.map((item) => item.text ?? "")
|
|
77
|
+
.join("\n\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeServerHarness() {
|
|
81
|
+
const handlers = new Map<string, (args: Record<string, unknown>) => Promise<unknown>>();
|
|
82
|
+
return {
|
|
83
|
+
handlers,
|
|
84
|
+
server: {
|
|
85
|
+
tool(name: string, _description: string, _schema: unknown, handler: (args: Record<string, unknown>) => Promise<unknown>) {
|
|
86
|
+
handlers.set(name, handler);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const selectedAgent = {
|
|
93
|
+
id: "agent-1",
|
|
94
|
+
name: "MockFixedPrice",
|
|
95
|
+
pricePerRunUsd: "0.01",
|
|
96
|
+
tags: ["translation"],
|
|
97
|
+
mcpSchema: null,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
describe("solve MCP tool", () => {
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.resetModules();
|
|
103
|
+
vi.clearAllMocks();
|
|
104
|
+
|
|
105
|
+
mockHasWalletConfigured.mockReturnValue(true);
|
|
106
|
+
mockGetConfiguredMethods.mockReturnValue(["card"]);
|
|
107
|
+
mockGetAcceptedPaymentMethods.mockReturnValue(["card"]);
|
|
108
|
+
mockGetCompatiblePaymentMethods.mockReturnValue(["card"]);
|
|
109
|
+
mockNormalizePaymentMethod.mockImplementation((method: string) => method);
|
|
110
|
+
mockToRegistryPaymentMethod.mockImplementation((method: string) => method);
|
|
111
|
+
mockRequiresSpendConfirmation.mockReturnValue(true);
|
|
112
|
+
mockGetDefaultTipAmount.mockReturnValue(0);
|
|
113
|
+
mockCanSpend.mockReturnValue({ ok: true, message: "" });
|
|
114
|
+
mockRequiresPolicyConfirmation.mockReturnValue(false);
|
|
115
|
+
mockUploadLocalFiles.mockResolvedValue({ input: { text: "hello", target_language: "es" }, uploads: [] });
|
|
116
|
+
mockGetActiveCreditPack.mockReturnValue(null);
|
|
117
|
+
mockGetCreditPackInventory.mockResolvedValue([]);
|
|
118
|
+
mockGetCreditPackProgram.mockReturnValue(null);
|
|
119
|
+
mockGetOrCreatePendingCardSetup.mockResolvedValue({ url: "https://example.com/card-setup" });
|
|
120
|
+
mockFormatCardSetupBlocks.mockReturnValue(["card setup"]);
|
|
121
|
+
mockGetWalletAddress.mockResolvedValue("0xabc");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("falls back to a quoted confirmation flow when /solve returns a 402 challenge", async () => {
|
|
125
|
+
mockApiPost.mockRejectedValueOnce(Object.assign(new Error("Payment Required"), { status: 402 }));
|
|
126
|
+
mockApiGet.mockResolvedValueOnce([selectedAgent]);
|
|
127
|
+
|
|
128
|
+
const { registerSolveTools } = await import("../solve.js");
|
|
129
|
+
const harness = makeServerHarness();
|
|
130
|
+
registerSolveTools(harness.server as never);
|
|
131
|
+
|
|
132
|
+
const solve = harness.handlers.get("solve");
|
|
133
|
+
expect(solve).toBeDefined();
|
|
134
|
+
|
|
135
|
+
const result = await solve!({
|
|
136
|
+
intent: "MockFixedPrice",
|
|
137
|
+
input: { text: "hello", target_language: "es" },
|
|
138
|
+
budget: 0.05,
|
|
139
|
+
pay_with: "card",
|
|
140
|
+
});
|
|
141
|
+
const text = flattenToolText(result);
|
|
142
|
+
|
|
143
|
+
expect(text).toContain("Best match: MockFixedPrice");
|
|
144
|
+
expect(text).toContain('solve({ intent: "MockFixedPrice"');
|
|
145
|
+
expect(text).toContain('pay_with: "card"');
|
|
146
|
+
expect(mockApiPostWithPayment).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("falls back to paid execution when confirmed after a 402 challenge", async () => {
|
|
150
|
+
mockApiPost.mockRejectedValueOnce(Object.assign(new Error("Payment Required"), { status: 402 }));
|
|
151
|
+
mockApiGet.mockResolvedValueOnce([selectedAgent]);
|
|
152
|
+
mockApiPostWithPayment.mockResolvedValueOnce({
|
|
153
|
+
status: "success",
|
|
154
|
+
job_id: "job-1",
|
|
155
|
+
agent_id: selectedAgent.id,
|
|
156
|
+
agent_name: selectedAgent.name,
|
|
157
|
+
output: { translated_text: "hola" },
|
|
158
|
+
cost: 0.01,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const { registerSolveTools } = await import("../solve.js");
|
|
162
|
+
const harness = makeServerHarness();
|
|
163
|
+
registerSolveTools(harness.server as never);
|
|
164
|
+
|
|
165
|
+
const solve = harness.handlers.get("solve");
|
|
166
|
+
expect(solve).toBeDefined();
|
|
167
|
+
|
|
168
|
+
const result = await solve!({
|
|
169
|
+
intent: "MockFixedPrice",
|
|
170
|
+
input: { text: "hello", target_language: "es" },
|
|
171
|
+
budget: 0.05,
|
|
172
|
+
pay_with: "card",
|
|
173
|
+
confirmed: true,
|
|
174
|
+
});
|
|
175
|
+
const text = flattenToolText(result);
|
|
176
|
+
|
|
177
|
+
expect(mockApiPostWithPayment).toHaveBeenCalledWith(
|
|
178
|
+
"/agents/agent-1/run",
|
|
179
|
+
{ input: { text: "hello", target_language: "es" } },
|
|
180
|
+
"card",
|
|
181
|
+
);
|
|
182
|
+
expect(text).toContain("Running MockFixedPrice — best match");
|
|
183
|
+
expect(text).toContain("Job ID: job-1");
|
|
184
|
+
expect(text).toContain("Paid: $0.01 via card");
|
|
185
|
+
});
|
|
186
|
+
});
|