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