aixyz 0.0.1 → 0.1.2
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/accepts.ts +12 -0
- package/bin.js +2 -0
- package/config.ts +1 -0
- package/facilitator/coinbase.ts +387 -0
- package/facilitator/index.ts +12 -0
- package/index.ts +0 -0
- package/package.json +11 -18
- package/server/adapters/a2a.ts +118 -0
- package/server/adapters/mcp.ts +100 -0
- package/server/index.ts +96 -0
- package/dist/cli/bin.js +0 -16351
package/accepts.ts
ADDED
package/bin.js
ADDED
package/config.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@aixyz/config";
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { FacilitatorConfig } from "@x402/core/http";
|
|
2
|
+
import { SignJWT, importPKCS8, importJWK, JWTPayload } from "jose";
|
|
3
|
+
import { getRandomValues } from "crypto";
|
|
4
|
+
|
|
5
|
+
const COINBASE_FACILITATOR_BASE_URL = "https://api.cdp.coinbase.com";
|
|
6
|
+
const COINBASE_FACILITATOR_V2_ROUTE = "/platform/v2/x402";
|
|
7
|
+
|
|
8
|
+
const X402_SDK_VERSION = "2.1.0";
|
|
9
|
+
const CDP_SDK_VERSION = "1.29.0";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates an authorization header for a request to the Coinbase API.
|
|
13
|
+
*
|
|
14
|
+
* @param apiKeyId - The api key ID
|
|
15
|
+
* @param apiKeySecret - The api key secret
|
|
16
|
+
* @param requestMethod - The method for the request (e.g. 'POST')
|
|
17
|
+
* @param requestHost - The host for the request (e.g. 'https://x402.org/facilitator')
|
|
18
|
+
* @param requestPath - The path for the request (e.g. '/verify')
|
|
19
|
+
* @returns The authorization header string
|
|
20
|
+
*/
|
|
21
|
+
export async function createAuthHeader(
|
|
22
|
+
apiKeyId: string,
|
|
23
|
+
apiKeySecret: string,
|
|
24
|
+
requestMethod: string,
|
|
25
|
+
requestHost: string,
|
|
26
|
+
requestPath: string,
|
|
27
|
+
) {
|
|
28
|
+
const jwt = await generateJwt({
|
|
29
|
+
apiKeyId,
|
|
30
|
+
apiKeySecret,
|
|
31
|
+
requestMethod,
|
|
32
|
+
requestHost,
|
|
33
|
+
requestPath,
|
|
34
|
+
});
|
|
35
|
+
return `Bearer ${jwt}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a correlation header for a request to the Coinbase API.
|
|
40
|
+
*
|
|
41
|
+
* @returns The correlation header string
|
|
42
|
+
*/
|
|
43
|
+
export function createCorrelationHeader(): string {
|
|
44
|
+
const data: Record<string, string> = {
|
|
45
|
+
sdk_version: CDP_SDK_VERSION,
|
|
46
|
+
sdk_language: "typescript",
|
|
47
|
+
source: "x402",
|
|
48
|
+
source_version: X402_SDK_VERSION,
|
|
49
|
+
};
|
|
50
|
+
return Object.keys(data)
|
|
51
|
+
.map((key) => `${key}=${encodeURIComponent(data[key])}`)
|
|
52
|
+
.join(",");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a CDP auth header for the facilitator service
|
|
57
|
+
*
|
|
58
|
+
* @param apiKeyId - The CDP API key ID
|
|
59
|
+
* @param apiKeySecret - The CDP API key secret
|
|
60
|
+
* @returns A function that returns the auth headers
|
|
61
|
+
*/
|
|
62
|
+
export function createCdpAuthHeaders(apiKeyId?: string, apiKeySecret?: string): FacilitatorConfig["createAuthHeaders"] {
|
|
63
|
+
const requestHost = COINBASE_FACILITATOR_BASE_URL.replace("https://", "");
|
|
64
|
+
|
|
65
|
+
return async () => {
|
|
66
|
+
apiKeyId = apiKeyId ?? process.env.CDP_API_KEY_ID;
|
|
67
|
+
apiKeySecret = apiKeySecret ?? process.env.CDP_API_KEY_SECRET;
|
|
68
|
+
|
|
69
|
+
const headers = {
|
|
70
|
+
verify: {
|
|
71
|
+
"Correlation-Context": createCorrelationHeader(),
|
|
72
|
+
} as Record<string, string>,
|
|
73
|
+
settle: {
|
|
74
|
+
"Correlation-Context": createCorrelationHeader(),
|
|
75
|
+
} as Record<string, string>,
|
|
76
|
+
supported: {
|
|
77
|
+
"Correlation-Context": createCorrelationHeader(),
|
|
78
|
+
} as Record<string, string>,
|
|
79
|
+
list: {
|
|
80
|
+
"Correlation-Context": createCorrelationHeader(),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (apiKeyId && apiKeySecret) {
|
|
85
|
+
headers.verify.Authorization = await createAuthHeader(
|
|
86
|
+
apiKeyId,
|
|
87
|
+
apiKeySecret,
|
|
88
|
+
"POST",
|
|
89
|
+
requestHost,
|
|
90
|
+
`${COINBASE_FACILITATOR_V2_ROUTE}/verify`,
|
|
91
|
+
);
|
|
92
|
+
headers.settle.Authorization = await createAuthHeader(
|
|
93
|
+
apiKeyId,
|
|
94
|
+
apiKeySecret,
|
|
95
|
+
"POST",
|
|
96
|
+
requestHost,
|
|
97
|
+
`${COINBASE_FACILITATOR_V2_ROUTE}/settle`,
|
|
98
|
+
);
|
|
99
|
+
headers.supported.Authorization = await createAuthHeader(
|
|
100
|
+
apiKeyId,
|
|
101
|
+
apiKeySecret,
|
|
102
|
+
"GET",
|
|
103
|
+
requestHost,
|
|
104
|
+
`${COINBASE_FACILITATOR_V2_ROUTE}/supported`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return headers;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a facilitator config for the Coinbase X402 facilitator
|
|
114
|
+
*
|
|
115
|
+
* @param apiKeyId - The CDP API key ID
|
|
116
|
+
* @param apiKeySecret - The CDP API key secret
|
|
117
|
+
* @returns A facilitator config
|
|
118
|
+
*/
|
|
119
|
+
function createFacilitatorConfig(apiKeyId?: string, apiKeySecret?: string): FacilitatorConfig {
|
|
120
|
+
return {
|
|
121
|
+
url: `${COINBASE_FACILITATOR_BASE_URL}${COINBASE_FACILITATOR_V2_ROUTE}`,
|
|
122
|
+
createAuthHeaders: createCdpAuthHeaders(apiKeyId, apiKeySecret),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* JwtOptions contains configuration for JWT generation.
|
|
128
|
+
*
|
|
129
|
+
* This interface holds all necessary parameters for generating a JWT token
|
|
130
|
+
* for authenticating with Coinbase's REST APIs. It supports both EC (ES256)
|
|
131
|
+
* and Ed25519 (EdDSA) keys.
|
|
132
|
+
*/
|
|
133
|
+
export interface JwtOptions {
|
|
134
|
+
/**
|
|
135
|
+
* The API key ID
|
|
136
|
+
*
|
|
137
|
+
* Examples:
|
|
138
|
+
* 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
139
|
+
* 'organizations/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/apiKeys/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
140
|
+
*/
|
|
141
|
+
apiKeyId: string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The API key secret
|
|
145
|
+
*
|
|
146
|
+
* Examples:
|
|
147
|
+
* 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==' (Edwards key (Ed25519))
|
|
148
|
+
* '-----BEGIN EC PRIVATE KEY-----\n...\n...\n...==\n-----END EC PRIVATE KEY-----\n' (EC key (ES256))
|
|
149
|
+
*/
|
|
150
|
+
apiKeySecret: string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The HTTP method for the request (e.g. 'GET', 'POST'), or null for JWTs intended for websocket connections
|
|
154
|
+
*/
|
|
155
|
+
requestMethod?: string | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The host for the request (e.g. 'api.cdp.coinbase.com'), or null for JWTs intended for websocket connections
|
|
159
|
+
*/
|
|
160
|
+
requestHost?: string | null;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The path for the request (e.g. '/platform/v1/wallets'), or null for JWTs intended for websocket connections
|
|
164
|
+
*/
|
|
165
|
+
requestPath?: string | null;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Optional expiration time in seconds (defaults to 120)
|
|
169
|
+
*/
|
|
170
|
+
expiresIn?: number;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Optional audience claim for the JWT
|
|
174
|
+
*/
|
|
175
|
+
audience?: string[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generates a JWT (also known as a Bearer token) for authenticating with Coinbase's REST APIs.
|
|
180
|
+
* Supports both EC (ES256) and Ed25519 (EdDSA) keys. Also supports JWTs meant for
|
|
181
|
+
* websocket connections by allowing requestMethod, requestHost, and requestPath to all be
|
|
182
|
+
* null, in which case the 'uris' claim is omitted from the JWT.
|
|
183
|
+
*
|
|
184
|
+
* @param options - The configuration options for generating the JWT
|
|
185
|
+
* @returns The generated JWT (Bearer token) string
|
|
186
|
+
* @throws {Error} If required parameters are missing, invalid, or if JWT signing fails
|
|
187
|
+
*/
|
|
188
|
+
export async function generateJwt(options: JwtOptions): Promise<string> {
|
|
189
|
+
// Validate required parameters
|
|
190
|
+
if (!options.apiKeyId) {
|
|
191
|
+
throw new Error("Key name is required");
|
|
192
|
+
}
|
|
193
|
+
if (!options.apiKeySecret) {
|
|
194
|
+
throw new Error("Private key is required");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if we have a REST API request or a websocket connection
|
|
198
|
+
const hasAllRequestParams = Boolean(options.requestMethod && options.requestHost && options.requestPath);
|
|
199
|
+
const hasNoRequestParams =
|
|
200
|
+
(options.requestMethod === undefined || options.requestMethod === null) &&
|
|
201
|
+
(options.requestHost === undefined || options.requestHost === null) &&
|
|
202
|
+
(options.requestPath === undefined || options.requestPath === null);
|
|
203
|
+
|
|
204
|
+
// Ensure we either have all request parameters or none (for websocket)
|
|
205
|
+
if (!hasAllRequestParams && !hasNoRequestParams) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Either all request details (method, host, path) must be provided, or all must be null for JWTs intended for websocket connections",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const now = Math.floor(Date.now() / 1000);
|
|
212
|
+
const expiresIn = options.expiresIn || 120; // Default to 120 seconds if not specified
|
|
213
|
+
|
|
214
|
+
// Prepare the JWT payload
|
|
215
|
+
const claims: JWTPayload = {
|
|
216
|
+
sub: options.apiKeyId,
|
|
217
|
+
iss: "cdp",
|
|
218
|
+
aud: options.audience,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Add the uris claim only for REST API requests
|
|
222
|
+
if (hasAllRequestParams) {
|
|
223
|
+
claims.uris = [`${options.requestMethod} ${options.requestHost}${options.requestPath}`];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Generate random nonce for the header
|
|
227
|
+
const randomNonce = nonce();
|
|
228
|
+
|
|
229
|
+
// Determine if we're using EC or Edwards key based on the key format
|
|
230
|
+
if (await isValidECKey(options.apiKeySecret)) {
|
|
231
|
+
return await buildECJWT(options.apiKeySecret, options.apiKeyId, claims, now, expiresIn, randomNonce);
|
|
232
|
+
} else if (isValidEd25519Key(options.apiKeySecret)) {
|
|
233
|
+
return await buildEdwardsJWT(options.apiKeySecret, options.apiKeyId, claims, now, expiresIn, randomNonce);
|
|
234
|
+
} else {
|
|
235
|
+
throw new UserInputValidationError("Invalid key format - must be either PEM EC key or base64 Ed25519 key");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Builds a JWT using an EC key.
|
|
241
|
+
*
|
|
242
|
+
* @param privateKey - The EC private key in PEM format
|
|
243
|
+
* @param keyName - The key name/ID
|
|
244
|
+
* @param claims - The JWT claims
|
|
245
|
+
* @param now - Current timestamp in seconds
|
|
246
|
+
* @param expiresIn - Number of seconds until the token expires
|
|
247
|
+
* @param nonce - Random nonce for the JWT header
|
|
248
|
+
* @returns A JWT token signed with an EC key
|
|
249
|
+
* @throws {Error} If key conversion, import, or signing fails
|
|
250
|
+
*/
|
|
251
|
+
async function buildECJWT(
|
|
252
|
+
privateKey: string,
|
|
253
|
+
keyName: string,
|
|
254
|
+
claims: JWTPayload,
|
|
255
|
+
now: number,
|
|
256
|
+
expiresIn: number,
|
|
257
|
+
nonce: string,
|
|
258
|
+
): Promise<string> {
|
|
259
|
+
try {
|
|
260
|
+
// Import the key directly with jose
|
|
261
|
+
const ecKey = await importPKCS8(privateKey, "ES256");
|
|
262
|
+
|
|
263
|
+
// Sign and return the JWT
|
|
264
|
+
return await new SignJWT(claims)
|
|
265
|
+
.setProtectedHeader({ alg: "ES256", kid: keyName, typ: "JWT", nonce })
|
|
266
|
+
.setIssuedAt(Math.floor(now))
|
|
267
|
+
.setNotBefore(Math.floor(now))
|
|
268
|
+
.setExpirationTime(Math.floor(now + expiresIn))
|
|
269
|
+
.sign(ecKey);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new Error(`Failed to generate EC JWT: ${(error as Error).message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Builds a JWT using an Ed25519 key.
|
|
277
|
+
*
|
|
278
|
+
* @param privateKey - The Ed25519 private key in base64 format
|
|
279
|
+
* @param keyName - The key name/ID
|
|
280
|
+
* @param claims - The JWT claims
|
|
281
|
+
* @param now - Current timestamp in seconds
|
|
282
|
+
* @param expiresIn - Number of seconds until the token expires
|
|
283
|
+
* @param nonce - Random nonce for the JWT header
|
|
284
|
+
* @returns A JWT token using an Ed25519 key
|
|
285
|
+
* @throws {Error} If key parsing, import, or signing fails
|
|
286
|
+
*/
|
|
287
|
+
async function buildEdwardsJWT(
|
|
288
|
+
privateKey: string,
|
|
289
|
+
keyName: string,
|
|
290
|
+
claims: JWTPayload,
|
|
291
|
+
now: number,
|
|
292
|
+
expiresIn: number,
|
|
293
|
+
nonce: string,
|
|
294
|
+
): Promise<string> {
|
|
295
|
+
try {
|
|
296
|
+
// Decode the base64 key (expecting 64 bytes: 32 for seed + 32 for public key)
|
|
297
|
+
const decoded = Buffer.from(privateKey, "base64");
|
|
298
|
+
if (decoded.length !== 64) {
|
|
299
|
+
throw new UserInputValidationError("Invalid Ed25519 key length");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const seed = decoded.subarray(0, 32);
|
|
303
|
+
const publicKey = decoded.subarray(32);
|
|
304
|
+
|
|
305
|
+
// Create JWK from the key components
|
|
306
|
+
const jwk = {
|
|
307
|
+
kty: "OKP",
|
|
308
|
+
crv: "Ed25519",
|
|
309
|
+
d: seed.toString("base64url"),
|
|
310
|
+
x: publicKey.toString("base64url"),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Import the key for signing
|
|
314
|
+
const key = await importJWK(jwk, "EdDSA");
|
|
315
|
+
|
|
316
|
+
// Sign and return the JWT
|
|
317
|
+
return await new SignJWT(claims)
|
|
318
|
+
.setProtectedHeader({ alg: "EdDSA", kid: keyName, typ: "JWT", nonce })
|
|
319
|
+
.setIssuedAt(Math.floor(now))
|
|
320
|
+
.setNotBefore(Math.floor(now))
|
|
321
|
+
.setExpirationTime(Math.floor(now + expiresIn))
|
|
322
|
+
.sign(key);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Failed to generate Ed25519 JWT: ${(error as Error).message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* UserInputValidationError is thrown when validation of a user-supplied input fails.
|
|
330
|
+
*/
|
|
331
|
+
export class UserInputValidationError extends Error {
|
|
332
|
+
/**
|
|
333
|
+
* Initializes a new UserInputValidationError instance.
|
|
334
|
+
*
|
|
335
|
+
* @param message - The user input validation error message.
|
|
336
|
+
*/
|
|
337
|
+
constructor(message: string) {
|
|
338
|
+
super(message);
|
|
339
|
+
this.name = "UserInputValidationError";
|
|
340
|
+
if (Error.captureStackTrace) {
|
|
341
|
+
Error.captureStackTrace(this, UserInputValidationError);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Determines if a string could be a valid Ed25519 key
|
|
347
|
+
*
|
|
348
|
+
* @param str - The string to test
|
|
349
|
+
* @returns True if the string could be a valid Ed25519 key, false otherwise
|
|
350
|
+
*/
|
|
351
|
+
function isValidEd25519Key(str: string): boolean {
|
|
352
|
+
try {
|
|
353
|
+
const decoded = Buffer.from(str, "base64");
|
|
354
|
+
return decoded.length === 64;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Determines if a string is a valid EC private key in PEM format
|
|
362
|
+
*
|
|
363
|
+
* @param str - The string to test
|
|
364
|
+
* @returns True if the string is a valid EC private key in PEM format
|
|
365
|
+
*/
|
|
366
|
+
async function isValidECKey(str: string): Promise<boolean> {
|
|
367
|
+
try {
|
|
368
|
+
// Try to import the key with jose - if it works, it's a valid EC key
|
|
369
|
+
await importPKCS8(str, "ES256");
|
|
370
|
+
return true;
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Generates a random nonce for the JWT.
|
|
378
|
+
*
|
|
379
|
+
* @returns {string} The generated nonce.
|
|
380
|
+
*/
|
|
381
|
+
function nonce(): string {
|
|
382
|
+
const bytes = new Uint8Array(16);
|
|
383
|
+
getRandomValues(bytes);
|
|
384
|
+
return Buffer.from(bytes).toString("hex");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export const facilitator = createFacilitatorConfig();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
2
|
+
import { facilitator } from "./coinbase";
|
|
3
|
+
|
|
4
|
+
export function getFacilitatorClient() {
|
|
5
|
+
if (process.env.CDP_API_KEY_ID) {
|
|
6
|
+
return new HTTPFacilitatorClient(facilitator);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return new HTTPFacilitatorClient({
|
|
10
|
+
url: process.env.X402_FACILITATOR_URL || "https://www.x402.org/facilitator",
|
|
11
|
+
});
|
|
12
|
+
}
|
package/index.ts
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aixyz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AI agent framework for building autonomous agents with X402 and ERC-8004 support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -21,41 +21,34 @@
|
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"author": "AgentlyHQ",
|
|
24
|
-
"type": "
|
|
25
|
-
"bin":
|
|
26
|
-
"aixyz": "dist/cli/bin.js"
|
|
27
|
-
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": "bin.js",
|
|
28
26
|
"files": [
|
|
29
|
-
"
|
|
27
|
+
"**/*.ts"
|
|
30
28
|
],
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "bun build cli/bin.ts --outdir ./dist/cli --target bun",
|
|
33
|
-
"clean": "rm -rf dist"
|
|
34
|
-
},
|
|
35
29
|
"dependencies": {
|
|
36
30
|
"@a2a-js/sdk": "^0.3.10",
|
|
31
|
+
"@aixyz/cli": "workspace:*",
|
|
32
|
+
"@aixyz/config": "workspace:*",
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
37
34
|
"@next/env": "^16.1.6",
|
|
38
|
-
"@x402/core": "^2.
|
|
39
|
-
"@x402/evm": "^2.
|
|
40
|
-
"@x402/express": "^2.
|
|
35
|
+
"@x402/core": "^2.3.1",
|
|
36
|
+
"@x402/evm": "^2.3.1",
|
|
37
|
+
"@x402/express": "^2.3.0",
|
|
38
|
+
"@x402/mcp": "^2.3.0",
|
|
41
39
|
"commander": "^13.0.0",
|
|
42
40
|
"express": "^5.2.1",
|
|
43
41
|
"jose": "^5.10.0",
|
|
44
42
|
"zod": "^4.3.6"
|
|
45
43
|
},
|
|
46
44
|
"devDependencies": {
|
|
47
|
-
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
48
45
|
"@types/express": "^5.0.6",
|
|
49
46
|
"ai": "^6"
|
|
50
47
|
},
|
|
51
48
|
"peerDependencies": {
|
|
52
|
-
"@modelcontextprotocol/sdk": "^1",
|
|
53
49
|
"ai": "^6"
|
|
54
50
|
},
|
|
55
51
|
"peerDependenciesMeta": {
|
|
56
|
-
"@modelcontextprotocol/sdk": {
|
|
57
|
-
"optional": true
|
|
58
|
-
},
|
|
59
52
|
"ai": {
|
|
60
53
|
"optional": true
|
|
61
54
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
AgentExecutor,
|
|
4
|
+
DefaultRequestHandler,
|
|
5
|
+
ExecutionEventBus,
|
|
6
|
+
InMemoryTaskStore,
|
|
7
|
+
RequestContext,
|
|
8
|
+
TaskStore,
|
|
9
|
+
} from "@a2a-js/sdk/server";
|
|
10
|
+
import { AgentCard, Message, TextPart } from "@a2a-js/sdk";
|
|
11
|
+
import type { ToolLoopAgent, ToolSet } from "ai";
|
|
12
|
+
import { getAixyzConfig } from "../../config";
|
|
13
|
+
import { AixyzServer } from "../index";
|
|
14
|
+
import { agentCardHandler, jsonRpcHandler, UserBuilder } from "@a2a-js/sdk/server/express";
|
|
15
|
+
import { Accepts } from "../../accepts";
|
|
16
|
+
|
|
17
|
+
export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements AgentExecutor {
|
|
18
|
+
constructor(private agent: ToolLoopAgent<never, TOOLS>) {}
|
|
19
|
+
|
|
20
|
+
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
// Extract the user's message text
|
|
23
|
+
const userMessage = requestContext.userMessage;
|
|
24
|
+
const textParts = userMessage.parts.filter((part): part is TextPart => part.kind === "text");
|
|
25
|
+
const prompt = textParts.map((part) => part.text).join("\n");
|
|
26
|
+
|
|
27
|
+
// TODO(@fuxingloh): supporting streaming later
|
|
28
|
+
const result = await this.agent.generate({ prompt });
|
|
29
|
+
const responseMessage: Message = {
|
|
30
|
+
kind: "message",
|
|
31
|
+
messageId: randomUUID(),
|
|
32
|
+
role: "agent",
|
|
33
|
+
parts: [{ kind: "text", text: result.text }],
|
|
34
|
+
contextId: requestContext.contextId,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
eventBus.publish(responseMessage);
|
|
38
|
+
eventBus.finished();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Handle errors by publishing an error message
|
|
41
|
+
const errorMessage: Message = {
|
|
42
|
+
kind: "message",
|
|
43
|
+
messageId: randomUUID(),
|
|
44
|
+
role: "agent",
|
|
45
|
+
parts: [
|
|
46
|
+
{
|
|
47
|
+
kind: "text",
|
|
48
|
+
text: `Error: ${error instanceof Error ? error.message : "An unknown error occurred"}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
contextId: requestContext.contextId,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
eventBus.publish(errorMessage);
|
|
55
|
+
eventBus.finished();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async cancelTask(_taskId: string, eventBus: ExecutionEventBus): Promise<void> {
|
|
60
|
+
// TODO(@fuxingloh): The ToolLoopAgent doesn't support cancellation, so we just finish
|
|
61
|
+
eventBus.finished();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getAgentCard(): AgentCard {
|
|
66
|
+
const config = getAixyzConfig();
|
|
67
|
+
return {
|
|
68
|
+
name: config.name,
|
|
69
|
+
description: config.description,
|
|
70
|
+
protocolVersion: "0.3.0",
|
|
71
|
+
version: config.version,
|
|
72
|
+
url: new URL("/agent", config.url).toString(),
|
|
73
|
+
capabilities: {
|
|
74
|
+
streaming: false,
|
|
75
|
+
pushNotifications: false,
|
|
76
|
+
},
|
|
77
|
+
defaultInputModes: ["text/plain"],
|
|
78
|
+
defaultOutputModes: ["text/plain"],
|
|
79
|
+
skills: config.skills,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useA2A<TOOLS extends ToolSet = ToolSet>(
|
|
84
|
+
app: AixyzServer,
|
|
85
|
+
exports: {
|
|
86
|
+
default: ToolLoopAgent<never, TOOLS>;
|
|
87
|
+
accepts?: Accepts;
|
|
88
|
+
},
|
|
89
|
+
taskStore: TaskStore = new InMemoryTaskStore(),
|
|
90
|
+
): void {
|
|
91
|
+
if (!exports.accepts) {
|
|
92
|
+
// TODO(@fuxingloh): right now we just don't register the agent if accepts is not provided,
|
|
93
|
+
// but it might be a better idea to do it in aixyz-cli (aixyz-pack).
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const agentExecutor = new ToolLoopAgentExecutor(exports.default);
|
|
98
|
+
const requestHandler = new DefaultRequestHandler(getAgentCard(), taskStore, agentExecutor);
|
|
99
|
+
|
|
100
|
+
app.express.use(
|
|
101
|
+
"/.well-known/agent-card.json",
|
|
102
|
+
agentCardHandler({
|
|
103
|
+
agentCardProvider: requestHandler,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (exports.accepts.scheme === "exact") {
|
|
108
|
+
app.withX402("POST /agent", exports.accepts);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
app.express.use(
|
|
112
|
+
"/agent",
|
|
113
|
+
jsonRpcHandler({
|
|
114
|
+
requestHandler,
|
|
115
|
+
userBuilder: UserBuilder.noAuthentication,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import type { Tool } from "ai";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
import { AixyzServer } from "../index";
|
|
7
|
+
import { createPaymentWrapper } from "@x402/mcp";
|
|
8
|
+
import { Accepts, AcceptsX402 } from "../../accepts";
|
|
9
|
+
|
|
10
|
+
export class AixyzMCP {
|
|
11
|
+
private registeredTools: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
config: any;
|
|
14
|
+
handler: any;
|
|
15
|
+
}> = [];
|
|
16
|
+
|
|
17
|
+
constructor(private app: AixyzServer) {}
|
|
18
|
+
|
|
19
|
+
private createServer(): McpServer {
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: this.app.config.name,
|
|
22
|
+
version: this.app.config.version,
|
|
23
|
+
});
|
|
24
|
+
for (const { name, config, handler } of this.registeredTools) {
|
|
25
|
+
server.registerTool(name, config, handler);
|
|
26
|
+
}
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async connect() {
|
|
31
|
+
this.app.express.post("/mcp", express.json(), async (req, res) => {
|
|
32
|
+
const server = this.createServer();
|
|
33
|
+
const transport = new StreamableHTTPServerTransport({
|
|
34
|
+
sessionIdGenerator: undefined,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
await transport.handleRequest(req as unknown as IncomingMessage, res as unknown as ServerResponse, req.body);
|
|
39
|
+
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
transport.close();
|
|
42
|
+
server.close();
|
|
43
|
+
};
|
|
44
|
+
res.on("finish", cleanup);
|
|
45
|
+
res.on("close", cleanup);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async withPayment(accepts: AcceptsX402) {
|
|
50
|
+
const payments = await this.app.withPaymentRequirements(accepts);
|
|
51
|
+
return createPaymentWrapper(this.app, {
|
|
52
|
+
accepts: payments,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async register(
|
|
57
|
+
name: string,
|
|
58
|
+
exports: {
|
|
59
|
+
default: Tool;
|
|
60
|
+
accepts?: Accepts;
|
|
61
|
+
},
|
|
62
|
+
) {
|
|
63
|
+
if (!exports.accepts) {
|
|
64
|
+
// TODO(@fuxingloh): right now we just don't register the agent if accepts is not provided,
|
|
65
|
+
// but it might be a better idea to do it in aixyz-cli (aixyz-pack).
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tool = exports.default;
|
|
70
|
+
if (!tool.execute) {
|
|
71
|
+
throw new Error(`Tool "${name}" has no execute function`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// TODO(@fuxingloh): add ext-app support:
|
|
75
|
+
// import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
76
|
+
|
|
77
|
+
const execute = tool.execute;
|
|
78
|
+
const config = {
|
|
79
|
+
description: tool.description,
|
|
80
|
+
...(tool.inputSchema && "shape" in tool.inputSchema ? { inputSchema: tool.inputSchema.shape } : {}),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handler = async (args: Record<string, unknown>) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await execute(args, { toolCallId: name, messages: [] });
|
|
86
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
87
|
+
return { content: [{ type: "text" as const, text }] };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const text = error instanceof Error ? error.message : "An unknown error occurred";
|
|
90
|
+
return { content: [{ type: "text" as const, text: `Error: ${text}` }], isError: true };
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.registeredTools.push({
|
|
95
|
+
name,
|
|
96
|
+
config,
|
|
97
|
+
handler: exports.accepts.scheme === "exact" ? (await this.withPayment(exports.accepts))(handler) : handler,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|