@agentwonderland/mcp 0.1.37 → 0.1.39
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 +4 -0
- package/dist/core/__tests__/mpp-client.test.d.ts +1 -0
- package/dist/core/__tests__/mpp-client.test.js +46 -0
- package/dist/core/__tests__/payments.test.js +5 -14
- package/dist/core/api-client.js +27 -5
- package/dist/core/base-charge.js +1 -1
- package/dist/core/formatters.d.ts +4 -0
- package/dist/core/formatters.js +2 -1
- package/dist/core/mpp-client.d.ts +62 -0
- package/dist/core/mpp-client.js +208 -0
- package/dist/core/payments.js +3 -3
- package/dist/core/solana-charge.d.ts +1 -0
- package/dist/core/solana-charge.js +7 -3
- package/dist/core/tempo-charge.js +1 -1
- package/dist/core/types.d.ts +13 -0
- package/dist/index.js +2 -0
- package/dist/tools/__tests__/wallet.test.js +1 -0
- package/dist/tools/agent-info.js +4 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/providers.d.ts +2 -0
- package/dist/tools/providers.js +40 -0
- package/dist/tools/run.js +1 -1
- package/dist/tools/search.js +7 -1
- package/dist/tools/solve.js +1 -1
- package/package.json +3 -4
- package/src/core/__tests__/api-client.test.ts +4 -0
- package/src/core/__tests__/mpp-client.test.ts +53 -0
- package/src/core/__tests__/payments.test.ts +6 -20
- package/src/core/api-client.ts +29 -5
- package/src/core/base-charge.ts +1 -1
- package/src/core/formatters.ts +3 -1
- package/src/core/mpp-client.ts +286 -0
- package/src/core/payments.ts +3 -3
- package/src/core/solana-charge.ts +7 -3
- package/src/core/tempo-charge.ts +1 -1
- package/src/core/types.ts +13 -0
- package/src/index.ts +2 -0
- package/src/tools/__tests__/wallet.test.ts +1 -0
- package/src/tools/agent-info.ts +11 -1
- package/src/tools/index.ts +1 -0
- package/src/tools/providers.ts +64 -0
- package/src/tools/run.ts +1 -1
- package/src/tools/search.ts +5 -1
- package/src/tools/solve.ts +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet } from "../core/api-client.js";
|
|
3
|
+
import { agentLine } from "../core/formatters.js";
|
|
4
|
+
function text(t) {
|
|
5
|
+
return { content: [{ type: "text", text: t }] };
|
|
6
|
+
}
|
|
7
|
+
export function registerProviderTools(server) {
|
|
8
|
+
server.tool("search_providers", "Search API providers listed on Agent Wonderland.", {
|
|
9
|
+
query: z.string().optional().describe("Provider name or keyword"),
|
|
10
|
+
limit: z.number().int().min(1).max(25).default(10),
|
|
11
|
+
}, async ({ query, limit }) => {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
if (query)
|
|
14
|
+
params.set("q", query);
|
|
15
|
+
params.set("limit", String(limit));
|
|
16
|
+
const providers = await apiGet(`/providers?${params}`);
|
|
17
|
+
if (providers.length === 0)
|
|
18
|
+
return text(query ? `No providers found matching "${query}".` : "No providers found.");
|
|
19
|
+
return text([
|
|
20
|
+
`Found ${providers.length} provider${providers.length === 1 ? "" : "s"}:`,
|
|
21
|
+
"",
|
|
22
|
+
...providers.map((provider) => ` ${provider.name} (${provider.slug}) • ${provider.stats?.live_endpoints ?? 0} endpoints`),
|
|
23
|
+
].join("\n"));
|
|
24
|
+
});
|
|
25
|
+
server.tool("get_provider", "Get an API provider profile and its live payable endpoints.", {
|
|
26
|
+
provider: z.string().describe("Provider slug"),
|
|
27
|
+
limit: z.number().int().min(1).max(100).default(50),
|
|
28
|
+
}, async ({ provider, limit }) => {
|
|
29
|
+
const profile = await apiGet(`/providers/${provider}`);
|
|
30
|
+
const agents = await apiGet(`/providers/${provider}/agents?limit=${limit}`);
|
|
31
|
+
return text([
|
|
32
|
+
`${profile.name} (${profile.slug})`,
|
|
33
|
+
profile.description ?? "",
|
|
34
|
+
...(profile.website_url ? [`Website: ${profile.website_url}`] : []),
|
|
35
|
+
`Live endpoints: ${profile.stats?.live_endpoints ?? agents.length}`,
|
|
36
|
+
"",
|
|
37
|
+
...agents.map((agent) => ` ${agentLine(agent)}`),
|
|
38
|
+
].filter(Boolean).join("\n"));
|
|
39
|
+
});
|
|
40
|
+
}
|
package/dist/tools/run.js
CHANGED
|
@@ -87,7 +87,7 @@ function buildCreditPackOfferLines(agent) {
|
|
|
87
87
|
export function registerRunTools(server) {
|
|
88
88
|
server.tool("run_agent", "Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking. If spending confirmation is enabled, first call returns a price quote — call again with confirmed: true to execute. Local file paths in the input (e.g. /Users/.../photo.jpg) are automatically uploaded to temporary storage and replaced with download URLs before execution. If a file you need isn't on this MCP server's filesystem (e.g. a sandboxed /mnt/... attachment), call upload_file first to get a presigned upload URL, PUT the bytes to it, then pass the returned GET URL as input instead — that keeps the bytes out of the conversation context.", {
|
|
89
89
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
90
|
-
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
90
|
+
input: z.record(z.string(), z.unknown()).describe("Input payload for the agent"),
|
|
91
91
|
pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
92
92
|
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
93
93
|
}, async ({ agent_id, input, pay_with, confirmed }) => {
|
package/dist/tools/search.js
CHANGED
|
@@ -9,17 +9,23 @@ function text(t) {
|
|
|
9
9
|
export function registerSearchTools(server) {
|
|
10
10
|
server.tool("search_agents", "Search the Agent Wonderland marketplace for AI agents. Returns a ranked list of matching agents with ratings, pricing, and job counts.", {
|
|
11
11
|
query: z.string().optional().describe("Search query (natural language or keywords)"),
|
|
12
|
+
provider: z.string().optional().describe("Filter by API provider name or slug"),
|
|
13
|
+
provider_slug: z.string().optional().describe("Filter by exact API provider slug"),
|
|
12
14
|
tag: z.string().optional().describe("Filter by tag (e.g. 'code', 'image', 'data')"),
|
|
13
15
|
limit: z.number().optional().default(10).describe("Max results (1-50)"),
|
|
14
16
|
max_price: z.number().optional().describe("Maximum price per request in USD"),
|
|
15
17
|
min_rating: z.number().min(1).max(5).optional().describe("Minimum star rating (1-5)"),
|
|
16
18
|
sort: z.enum(["relevance", "price", "rating", "popularity", "newest"]).optional()
|
|
17
19
|
.describe("Sort results by: relevance (default), price, rating, popularity, or newest"),
|
|
18
|
-
}, async ({ query, tag, limit, max_price, min_rating, sort }) => {
|
|
20
|
+
}, async ({ query, provider, provider_slug, tag, limit, max_price, min_rating, sort }) => {
|
|
19
21
|
const requestedLimit = Math.max(1, Math.min(50, limit ?? 10));
|
|
20
22
|
const params = new URLSearchParams();
|
|
21
23
|
if (query)
|
|
22
24
|
params.set("q", query);
|
|
25
|
+
if (provider_slug)
|
|
26
|
+
params.set("provider_slug", provider_slug);
|
|
27
|
+
else if (provider)
|
|
28
|
+
params.set("provider", provider);
|
|
23
29
|
if (tag)
|
|
24
30
|
params.set("tag", tag);
|
|
25
31
|
// min_rating is filtered client-side on avgRating. Request extra candidates
|
package/dist/tools/solve.js
CHANGED
|
@@ -110,7 +110,7 @@ export function registerSolveTools(server) {
|
|
|
110
110
|
.string()
|
|
111
111
|
.describe("What you want to accomplish (natural language)"),
|
|
112
112
|
input: z
|
|
113
|
-
.record(z.unknown())
|
|
113
|
+
.record(z.string(), z.unknown())
|
|
114
114
|
.optional()
|
|
115
115
|
.default({})
|
|
116
116
|
.describe("Input payload for the agent"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentwonderland/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for the Agent Wonderland AI agent marketplace",
|
|
6
6
|
"bin": {
|
|
@@ -24,11 +24,10 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
26
|
"@solana/spl-token": "^0.4.14",
|
|
27
|
-
"@solana/web3.js": "
|
|
28
|
-
"mppx": "^0.5.10",
|
|
27
|
+
"@solana/web3.js": "1.95.8",
|
|
29
28
|
"qrcode": "^1.5.4",
|
|
30
29
|
"viem": "^2.47.6",
|
|
31
|
-
"zod": "^3.
|
|
30
|
+
"zod": "^4.3.6"
|
|
32
31
|
},
|
|
33
32
|
"optionalDependencies": {
|
|
34
33
|
"@open-wallet-standard/core": "^1.0.0"
|
|
@@ -71,6 +71,10 @@ describe("api-client headers", () => {
|
|
|
71
71
|
"did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
72
72
|
"X-AW-Rebate-Principal":
|
|
73
73
|
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
74
|
+
"X-AW-Surface": "mcp",
|
|
75
|
+
"X-AW-MCP-Version": "0.1.37",
|
|
76
|
+
"X-AW-MCP-Tool": "run_agent",
|
|
77
|
+
"X-AW-MCP-Action": "execute",
|
|
74
78
|
}),
|
|
75
79
|
}),
|
|
76
80
|
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Credential, Method, Mppx, z } from "../mpp-client.js";
|
|
3
|
+
|
|
4
|
+
describe("local MPP client", () => {
|
|
5
|
+
it("handles a 402 challenge and retries with a payment credential", async () => {
|
|
6
|
+
const challengeRequest = Buffer.from(JSON.stringify({
|
|
7
|
+
amount: "1000000",
|
|
8
|
+
currency: "USDC",
|
|
9
|
+
recipient: "0x0000000000000000000000000000000000000001",
|
|
10
|
+
})).toString("base64url");
|
|
11
|
+
const fetchMock = vi
|
|
12
|
+
.fn<typeof fetch>()
|
|
13
|
+
.mockResolvedValueOnce(new Response("payment required", {
|
|
14
|
+
status: 402,
|
|
15
|
+
headers: {
|
|
16
|
+
"WWW-Authenticate": `Payment id="test-id", realm="api.test", method="base", intent="charge", request="${challengeRequest}"`,
|
|
17
|
+
},
|
|
18
|
+
}))
|
|
19
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
20
|
+
|
|
21
|
+
const method = Method.toClient(
|
|
22
|
+
Method.from({
|
|
23
|
+
name: "base",
|
|
24
|
+
intent: "charge",
|
|
25
|
+
schema: {
|
|
26
|
+
credential: { payload: z.object({ type: z.literal("hash"), hash: z.string() }) },
|
|
27
|
+
request: z.object({ amount: z.string() }),
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
{
|
|
31
|
+
createCredential({ challenge }) {
|
|
32
|
+
return Credential.serialize({
|
|
33
|
+
challenge,
|
|
34
|
+
payload: { type: "hash", hash: "0xabc" },
|
|
35
|
+
source: "did:pkh:eip155:8453:0x0000000000000000000000000000000000000002",
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const client = Mppx.create({ fetch: fetchMock, methods: [method], polyfill: false });
|
|
42
|
+
const response = await client.fetch("https://api.test/agents/example/run", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: "{}",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(response.status).toBe(200);
|
|
49
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
50
|
+
const retryInit = fetchMock.mock.calls[1]?.[1];
|
|
51
|
+
expect(new Headers(retryInit?.headers).get("Authorization")).toMatch(/^Payment\s+/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -28,7 +28,7 @@ vi.mock("../config.js", () => ({
|
|
|
28
28
|
resolveWalletAndChain: () => currentResolvedMethod,
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
-
vi.mock("
|
|
31
|
+
vi.mock("../mpp-client.js", () => ({
|
|
32
32
|
Mppx: {
|
|
33
33
|
create: (config: unknown) => mockMppxCreate(config),
|
|
34
34
|
},
|
|
@@ -63,28 +63,14 @@ describe("payment method initialization", () => {
|
|
|
63
63
|
.mockReturnValueOnce({ fetch: createdFetches[3] });
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it("
|
|
66
|
+
it("rejects explicit card payment while card payments are launch-gated", async () => {
|
|
67
67
|
const { getPaymentFetch } = await import("../payments.js");
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
consumerToken: "consumer_two",
|
|
72
|
-
paymentMethodId: "pm_two",
|
|
73
|
-
last4: "2222",
|
|
74
|
-
brand: "mastercard",
|
|
75
|
-
};
|
|
76
|
-
const secondFetch = await getPaymentFetch("card");
|
|
77
|
-
|
|
78
|
-
expect(firstFetch).not.toBe(secondFetch);
|
|
79
|
-
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
80
|
-
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
81
|
-
1,
|
|
82
|
-
expect.objectContaining({ polyfill: false }),
|
|
83
|
-
);
|
|
84
|
-
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
85
|
-
2,
|
|
86
|
-
expect.objectContaining({ polyfill: false }),
|
|
69
|
+
await expect(getPaymentFetch("card")).rejects.toThrow(
|
|
70
|
+
"Card payments are temporarily unavailable",
|
|
87
71
|
);
|
|
72
|
+
expect(mockMppxCreate).not.toHaveBeenCalled();
|
|
73
|
+
expect(mockStripe).not.toHaveBeenCalled();
|
|
88
74
|
});
|
|
89
75
|
|
|
90
76
|
it("initializes only the Base method when base is requested", async () => {
|
package/src/core/api-client.ts
CHANGED
|
@@ -29,10 +29,34 @@ interface RequestOptions {
|
|
|
29
29
|
extraHeaders?: Record<string, string>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const MCP_VERSION = "0.1.37";
|
|
33
|
+
|
|
34
|
+
function inferToolHeaders(path: string, method: string): Record<string, string> {
|
|
35
|
+
if (path === "/solve") {
|
|
36
|
+
return { "X-AW-MCP-Tool": "solve", "X-AW-MCP-Action": method === "POST" ? "execute" : "view" };
|
|
37
|
+
}
|
|
38
|
+
if (/^\/agents\/[^/]+\/run$/.test(path)) {
|
|
39
|
+
return { "X-AW-MCP-Tool": "run_agent", "X-AW-MCP-Action": method === "POST" ? "execute" : "view" };
|
|
40
|
+
}
|
|
41
|
+
if (path.startsWith("/agents?") || path === "/agents" || path === "/agents/search") {
|
|
42
|
+
return { "X-AW-MCP-Tool": "search_agents", "X-AW-MCP-Action": "search" };
|
|
43
|
+
}
|
|
44
|
+
if (/^\/agents\/[^/]+$/.test(path)) {
|
|
45
|
+
return { "X-AW-MCP-Tool": "get_agent", "X-AW-MCP-Action": "view" };
|
|
46
|
+
}
|
|
47
|
+
if (path.startsWith("/jobs")) {
|
|
48
|
+
return { "X-AW-MCP-Tool": "get_job", "X-AW-MCP-Action": "poll" };
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function buildHeaders(path: string, method: string, options?: RequestOptions): Promise<Record<string, string>> {
|
|
33
54
|
const headers: Record<string, string> = {
|
|
34
55
|
"Content-Type": "application/json",
|
|
35
56
|
Accept: "application/json",
|
|
57
|
+
"X-AW-Surface": "mcp",
|
|
58
|
+
"X-AW-MCP-Version": MCP_VERSION,
|
|
59
|
+
...inferToolHeaders(path, method),
|
|
36
60
|
};
|
|
37
61
|
|
|
38
62
|
const apiKey = getApiKey();
|
|
@@ -119,7 +143,7 @@ export async function apiGet<T>(path: string, options?: RequestOptions): Promise
|
|
|
119
143
|
const url = `${getApiUrl()}${path}`;
|
|
120
144
|
const response = await fetch(url, {
|
|
121
145
|
method: "GET",
|
|
122
|
-
headers: await buildHeaders(options),
|
|
146
|
+
headers: await buildHeaders(path, "GET", options),
|
|
123
147
|
});
|
|
124
148
|
const result = await handleResponse<T>(response);
|
|
125
149
|
return attachResponseMetadata(result, response);
|
|
@@ -129,7 +153,7 @@ export async function apiPost<T>(path: string, body: unknown, options?: RequestO
|
|
|
129
153
|
const url = `${getApiUrl()}${path}`;
|
|
130
154
|
const response = await fetch(url, {
|
|
131
155
|
method: "POST",
|
|
132
|
-
headers: await buildHeaders(options),
|
|
156
|
+
headers: await buildHeaders(path, "POST", options),
|
|
133
157
|
body: JSON.stringify(body),
|
|
134
158
|
});
|
|
135
159
|
const result = await handleResponse<T>(response);
|
|
@@ -151,7 +175,7 @@ export async function apiPostWithPayment<T>(
|
|
|
151
175
|
const paymentFetch = await getPaymentFetch(payWith);
|
|
152
176
|
const response = await paymentFetch(url, {
|
|
153
177
|
method: "POST",
|
|
154
|
-
headers: await buildHeaders({
|
|
178
|
+
headers: await buildHeaders(path, "POST", {
|
|
155
179
|
ensureConsumerPrincipal: true,
|
|
156
180
|
principalMethod: payWith,
|
|
157
181
|
...options,
|
|
@@ -166,7 +190,7 @@ export async function apiPut<T>(path: string, body: unknown, options?: RequestOp
|
|
|
166
190
|
const url = `${getApiUrl()}${path}`;
|
|
167
191
|
const response = await fetch(url, {
|
|
168
192
|
method: "PUT",
|
|
169
|
-
headers: await buildHeaders(options),
|
|
193
|
+
headers: await buildHeaders(path, "PUT", options),
|
|
170
194
|
body: JSON.stringify(body),
|
|
171
195
|
});
|
|
172
196
|
const result = await handleResponse<T>(response);
|
package/src/core/base-charge.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Signs and sends a standard ERC-20 transfer on Base chain, then returns
|
|
5
5
|
* the tx hash as a credential. Plugs into mppx's compose/dispatch system.
|
|
6
6
|
*/
|
|
7
|
-
import { Method, Credential, z } from "
|
|
7
|
+
import { Method, Credential, z } from "./mpp-client.js";
|
|
8
8
|
import type { LocalAccount } from "viem/accounts";
|
|
9
9
|
import { toAtomicAmount } from "./amount-utils.js";
|
|
10
10
|
|
package/src/core/formatters.ts
CHANGED
|
@@ -53,6 +53,7 @@ interface AgentLike {
|
|
|
53
53
|
totalExecutions?: number;
|
|
54
54
|
pricePerRunUsd?: string;
|
|
55
55
|
stats?: { completedJobs?: number; avgRating?: number | null };
|
|
56
|
+
provider?: { name?: string; slug?: string } | null;
|
|
56
57
|
[key: string]: unknown;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -62,10 +63,11 @@ export function agentLine(agent: AgentLike): string {
|
|
|
62
63
|
const rating = agent.avgRating ?? agent.stats?.avgRating ?? null;
|
|
63
64
|
const jobs = agent.stats?.completedJobs ?? agent.totalExecutions ?? 0;
|
|
64
65
|
const price = formatPrice(agent.pricePerRunUsd);
|
|
66
|
+
const provider = agent.provider?.name ? ` by ${agent.provider.name}` : "";
|
|
65
67
|
const reliability = agent.successRate != null && Number(agent.successRate) < 1
|
|
66
68
|
? ` • ${(Number(agent.successRate) * 100).toFixed(0)}% reliable`
|
|
67
69
|
: "";
|
|
68
|
-
return `${name}${slug} ${stars(rating)} ${compactNumber(jobs)} jobs • ${price}${reliability}`;
|
|
70
|
+
return `${name}${slug}${provider} ${stars(rating)} ${compactNumber(jobs)} jobs • ${price}${reliability}`;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export function formatLastActive(lastActiveAt: string | null | undefined): string | null {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as z from "zod/mini";
|
|
2
|
+
|
|
3
|
+
type Challenge = {
|
|
4
|
+
id: string;
|
|
5
|
+
realm: string;
|
|
6
|
+
method: string;
|
|
7
|
+
intent: string;
|
|
8
|
+
request: Record<string, unknown>;
|
|
9
|
+
description?: string;
|
|
10
|
+
digest?: string;
|
|
11
|
+
expires?: string;
|
|
12
|
+
opaque?: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ClientMethod = {
|
|
16
|
+
name: string;
|
|
17
|
+
intent: string;
|
|
18
|
+
context?: { parse(value: unknown): unknown };
|
|
19
|
+
createCredential(args: { challenge: Challenge; context?: unknown }): Promise<string> | string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type MethodDefinition = {
|
|
23
|
+
name: string;
|
|
24
|
+
intent: string;
|
|
25
|
+
schema?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type CreateConfig = {
|
|
29
|
+
methods: ClientMethod[];
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
polyfill?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type CredentialInput = {
|
|
35
|
+
challenge: Challenge;
|
|
36
|
+
payload: unknown;
|
|
37
|
+
source?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const PAYMENT_FETCH_WRAPPER = Symbol.for("agentwonderland.mpp.fetch.wrapper");
|
|
41
|
+
|
|
42
|
+
export { z };
|
|
43
|
+
|
|
44
|
+
export const Method = {
|
|
45
|
+
from<T extends MethodDefinition>(method: T): T {
|
|
46
|
+
return method;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
toClient<T extends MethodDefinition>(
|
|
50
|
+
method: T,
|
|
51
|
+
options: {
|
|
52
|
+
context?: { parse(value: unknown): unknown };
|
|
53
|
+
createCredential(args: { challenge: Challenge; context?: unknown }): Promise<string> | string;
|
|
54
|
+
},
|
|
55
|
+
): T & ClientMethod {
|
|
56
|
+
return {
|
|
57
|
+
...method,
|
|
58
|
+
context: options.context,
|
|
59
|
+
createCredential: options.createCredential,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Credential = {
|
|
65
|
+
serialize(credential: CredentialInput): string {
|
|
66
|
+
const wire = {
|
|
67
|
+
challenge: {
|
|
68
|
+
...credential.challenge,
|
|
69
|
+
request: serializePaymentRequest(credential.challenge.request),
|
|
70
|
+
},
|
|
71
|
+
payload: credential.payload,
|
|
72
|
+
...(credential.source ? { source: credential.source } : {}),
|
|
73
|
+
};
|
|
74
|
+
return `Payment ${base64UrlEncode(JSON.stringify(wire))}`;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Mppx = {
|
|
79
|
+
create(config: CreateConfig): { fetch: typeof fetch; methods: ClientMethod[]; rawFetch: typeof fetch } {
|
|
80
|
+
const rawFetch = unwrapFetch(config.fetch ?? globalThis.fetch);
|
|
81
|
+
const methods = config.methods.flat();
|
|
82
|
+
const paymentFetch = createPaymentFetch(rawFetch, methods);
|
|
83
|
+
|
|
84
|
+
if (config.polyfill) {
|
|
85
|
+
globalThis.fetch = paymentFetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
fetch: paymentFetch,
|
|
90
|
+
methods,
|
|
91
|
+
rawFetch,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function stripe(_parameters?: unknown): ClientMethod {
|
|
97
|
+
throw new Error("Stripe card payments are temporarily unavailable.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createPaymentFetch(baseFetch: typeof fetch, methods: ClientMethod[]): typeof fetch {
|
|
101
|
+
const wrapped = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
102
|
+
const response = await baseFetch(input, init);
|
|
103
|
+
if (response.status !== 402) {
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const challenges = challengesFromResponse(response);
|
|
108
|
+
const method = methods.find((candidate) =>
|
|
109
|
+
challenges.some((challenge) => challenge.method === candidate.name && challenge.intent === candidate.intent),
|
|
110
|
+
);
|
|
111
|
+
const challenge = method
|
|
112
|
+
? challenges.find((candidate) => candidate.method === method.name && candidate.intent === method.intent)
|
|
113
|
+
: undefined;
|
|
114
|
+
|
|
115
|
+
if (!method || !challenge) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`No method found for challenges: ${challenges.map((item) => `${item.method}.${item.intent}`).join(", ")}. ` +
|
|
118
|
+
`Available: ${methods.map((item) => `${item.name}.${item.intent}`).join(", ")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const context = (init as RequestInit & { context?: unknown } | undefined)?.context;
|
|
123
|
+
const parsedContext = method.context && context !== undefined ? method.context.parse(context) : undefined;
|
|
124
|
+
const credential = await method.createCredential(
|
|
125
|
+
parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return baseFetch(input, {
|
|
129
|
+
...init,
|
|
130
|
+
headers: withAuthorizationHeader(init?.headers, credential),
|
|
131
|
+
});
|
|
132
|
+
}) as typeof fetch & { [PAYMENT_FETCH_WRAPPER]?: typeof fetch };
|
|
133
|
+
|
|
134
|
+
wrapped[PAYMENT_FETCH_WRAPPER] = baseFetch;
|
|
135
|
+
return wrapped;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function challengesFromResponse(response: Response): Challenge[] {
|
|
139
|
+
const header = response.headers.get("WWW-Authenticate");
|
|
140
|
+
if (!header) {
|
|
141
|
+
throw new Error("Missing WWW-Authenticate header.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const starts = Array.from(header.matchAll(/Payment\s+/gi), (match) => match.index).filter((index) => index !== undefined);
|
|
145
|
+
if (starts.length === 0) {
|
|
146
|
+
throw new Error("No Payment schemes found.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return starts.map((start, index) => {
|
|
150
|
+
const end = index + 1 < starts.length ? starts[index + 1] : header.length;
|
|
151
|
+
return deserializeChallenge(header.slice(start, end).replace(/,\s*$/, ""));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function deserializeChallenge(value: string): Challenge {
|
|
156
|
+
const paramsStart = value.search(/\s/);
|
|
157
|
+
if (!/^Payment\s+/i.test(value) || paramsStart < 0) {
|
|
158
|
+
throw new Error("Missing Payment scheme.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const params = parseAuthParams(value.slice(paramsStart + 1));
|
|
162
|
+
if (!params.request) {
|
|
163
|
+
throw new Error("Missing request parameter.");
|
|
164
|
+
}
|
|
165
|
+
if (!params.id || !params.realm || !params.method || !params.intent) {
|
|
166
|
+
throw new Error("Malformed payment challenge.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
id: params.id,
|
|
171
|
+
realm: params.realm,
|
|
172
|
+
method: params.method,
|
|
173
|
+
intent: params.intent,
|
|
174
|
+
request: deserializePaymentRequest(params.request),
|
|
175
|
+
...(params.description ? { description: params.description } : {}),
|
|
176
|
+
...(params.digest ? { digest: params.digest } : {}),
|
|
177
|
+
...(params.expires ? { expires: params.expires } : {}),
|
|
178
|
+
...(params.opaque ? { opaque: deserializePaymentRequest(params.opaque) as Record<string, string> } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseAuthParams(input: string): Record<string, string> {
|
|
183
|
+
const result: Record<string, string> = {};
|
|
184
|
+
let index = 0;
|
|
185
|
+
|
|
186
|
+
while (index < input.length) {
|
|
187
|
+
while (index < input.length && /[\s,]/.test(input[index] ?? "")) index++;
|
|
188
|
+
if (index >= input.length) break;
|
|
189
|
+
|
|
190
|
+
const keyStart = index;
|
|
191
|
+
while (index < input.length && /[A-Za-z0-9_-]/.test(input[index] ?? "")) index++;
|
|
192
|
+
const key = input.slice(keyStart, index);
|
|
193
|
+
if (!key) throw new Error("Malformed auth-param.");
|
|
194
|
+
|
|
195
|
+
while (index < input.length && /\s/.test(input[index] ?? "")) index++;
|
|
196
|
+
if (input[index] !== "=") break;
|
|
197
|
+
index++;
|
|
198
|
+
while (index < input.length && /\s/.test(input[index] ?? "")) index++;
|
|
199
|
+
|
|
200
|
+
const [value, nextIndex] = readAuthParamValue(input, index);
|
|
201
|
+
result[key] = value;
|
|
202
|
+
index = nextIndex;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readAuthParamValue(input: string, start: number): [string, number] {
|
|
209
|
+
if (input[start] !== "\"") {
|
|
210
|
+
let index = start;
|
|
211
|
+
while (index < input.length && input[index] !== ",") index++;
|
|
212
|
+
return [input.slice(start, index).trim(), index];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let index = start + 1;
|
|
216
|
+
let value = "";
|
|
217
|
+
let escaped = false;
|
|
218
|
+
while (index < input.length) {
|
|
219
|
+
const char = input[index];
|
|
220
|
+
index++;
|
|
221
|
+
if (escaped) {
|
|
222
|
+
value += char;
|
|
223
|
+
escaped = false;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === "\\") {
|
|
227
|
+
escaped = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (char === "\"") {
|
|
231
|
+
return [value, index];
|
|
232
|
+
}
|
|
233
|
+
value += char;
|
|
234
|
+
}
|
|
235
|
+
throw new Error("Unterminated quoted-string.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function withAuthorizationHeader(headers: HeadersInit | undefined, credential: string): HeadersInit {
|
|
239
|
+
const next = new Headers(headers);
|
|
240
|
+
next.set("Authorization", credential);
|
|
241
|
+
return next;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function unwrapFetch(candidate: typeof fetch): typeof fetch {
|
|
245
|
+
let current = candidate as typeof fetch & { [PAYMENT_FETCH_WRAPPER]?: typeof fetch };
|
|
246
|
+
while (current[PAYMENT_FETCH_WRAPPER]) {
|
|
247
|
+
current = current[PAYMENT_FETCH_WRAPPER] as typeof fetch & { [PAYMENT_FETCH_WRAPPER]?: typeof fetch };
|
|
248
|
+
}
|
|
249
|
+
return current;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function deserializePaymentRequest(encoded: string): Record<string, unknown> {
|
|
253
|
+
return JSON.parse(base64UrlDecode(encoded)) as Record<string, unknown>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function serializePaymentRequest(request: Record<string, unknown>): string {
|
|
257
|
+
return base64UrlEncode(stableStringify(request));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stableStringify(value: unknown): string {
|
|
261
|
+
if (value === null || typeof value !== "object") {
|
|
262
|
+
return JSON.stringify(value);
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const record = value as Record<string, unknown>;
|
|
269
|
+
return `{${Object.keys(record)
|
|
270
|
+
.sort()
|
|
271
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
272
|
+
.join(",")}}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function base64UrlEncode(value: string): string {
|
|
276
|
+
return Buffer.from(value, "utf8")
|
|
277
|
+
.toString("base64")
|
|
278
|
+
.replace(/=/g, "")
|
|
279
|
+
.replace(/\+/g, "-")
|
|
280
|
+
.replace(/\//g, "_");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function base64UrlDecode(value: string): string {
|
|
284
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
285
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
286
|
+
}
|
package/src/core/payments.ts
CHANGED
|
@@ -77,7 +77,7 @@ async function initEvmMppForChain(
|
|
|
77
77
|
chain: "tempo" | "base",
|
|
78
78
|
): Promise<typeof fetch | null> {
|
|
79
79
|
try {
|
|
80
|
-
const { Mppx } = await import("
|
|
80
|
+
const { Mppx } = await import("./mpp-client.js");
|
|
81
81
|
let account;
|
|
82
82
|
|
|
83
83
|
if (wallet.keyType === "ows" && wallet.owsWalletId) {
|
|
@@ -112,7 +112,7 @@ async function initSolanaMpp(wallet: WalletEntry): Promise<typeof fetch | null>
|
|
|
112
112
|
return null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
const { Mppx } = await import("
|
|
115
|
+
const { Mppx } = await import("./mpp-client.js");
|
|
116
116
|
const { solanaChargeClient } = await import("./solana-charge.js");
|
|
117
117
|
const mppx = Mppx.create({
|
|
118
118
|
methods: [solanaChargeClient({ wallet })] as any,
|
|
@@ -129,7 +129,7 @@ async function initCard(): Promise<typeof fetch | null> {
|
|
|
129
129
|
if (!cardConfig) return null;
|
|
130
130
|
|
|
131
131
|
try {
|
|
132
|
-
const { Mppx, stripe } = await import("
|
|
132
|
+
const { Mppx, stripe } = await import("./mpp-client.js");
|
|
133
133
|
const apiUrl = getApiUrl();
|
|
134
134
|
const pmId = cardConfig.paymentMethodId ?? undefined;
|
|
135
135
|
const mppx = Mppx.create({
|