@absol-labs/agent 0.1.0

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/dist/discovery/registry.d.ts +68 -0
  4. package/dist/discovery/registry.d.ts.map +1 -0
  5. package/dist/discovery/registry.js +148 -0
  6. package/dist/discovery/registry.js.map +1 -0
  7. package/dist/frameworks/agentkit.d.ts +158 -0
  8. package/dist/frameworks/agentkit.d.ts.map +1 -0
  9. package/dist/frameworks/agentkit.js +339 -0
  10. package/dist/frameworks/agentkit.js.map +1 -0
  11. package/dist/frameworks/crewai.d.ts +19 -0
  12. package/dist/frameworks/crewai.d.ts.map +1 -0
  13. package/dist/frameworks/crewai.js +73 -0
  14. package/dist/frameworks/crewai.js.map +1 -0
  15. package/dist/frameworks/eliza.d.ts +393 -0
  16. package/dist/frameworks/eliza.d.ts.map +1 -0
  17. package/dist/frameworks/eliza.js +542 -0
  18. package/dist/frameworks/eliza.js.map +1 -0
  19. package/dist/frameworks/langchain.d.ts +31 -0
  20. package/dist/frameworks/langchain.d.ts.map +1 -0
  21. package/dist/frameworks/langchain.js +206 -0
  22. package/dist/frameworks/langchain.js.map +1 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +16 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mandates/env.d.ts +51 -0
  28. package/dist/mandates/env.d.ts.map +1 -0
  29. package/dist/mandates/env.js +148 -0
  30. package/dist/mandates/env.js.map +1 -0
  31. package/dist/mandates/mandate.d.ts +214 -0
  32. package/dist/mandates/mandate.d.ts.map +1 -0
  33. package/dist/mandates/mandate.js +172 -0
  34. package/dist/mandates/mandate.js.map +1 -0
  35. package/dist/mcp/http-server.d.ts +2 -0
  36. package/dist/mcp/http-server.d.ts.map +1 -0
  37. package/dist/mcp/http-server.js +32 -0
  38. package/dist/mcp/http-server.js.map +1 -0
  39. package/dist/mcp/http.d.ts +114 -0
  40. package/dist/mcp/http.d.ts.map +1 -0
  41. package/dist/mcp/http.js +428 -0
  42. package/dist/mcp/http.js.map +1 -0
  43. package/dist/mcp/server.d.ts +119 -0
  44. package/dist/mcp/server.d.ts.map +1 -0
  45. package/dist/mcp/server.js +679 -0
  46. package/dist/mcp/server.js.map +1 -0
  47. package/dist/mcp/stdio.d.ts +2 -0
  48. package/dist/mcp/stdio.d.ts.map +1 -0
  49. package/dist/mcp/stdio.js +6 -0
  50. package/dist/mcp/stdio.js.map +1 -0
  51. package/dist/sdk/client.d.ts +78 -0
  52. package/dist/sdk/client.d.ts.map +1 -0
  53. package/dist/sdk/client.js +99 -0
  54. package/dist/sdk/client.js.map +1 -0
  55. package/dist/wallet/provider.d.ts +51 -0
  56. package/dist/wallet/provider.d.ts.map +1 -0
  57. package/dist/wallet/provider.js +107 -0
  58. package/dist/wallet/provider.js.map +1 -0
  59. package/dist/x402/facilitator.d.ts +315 -0
  60. package/dist/x402/facilitator.d.ts.map +1 -0
  61. package/dist/x402/facilitator.js +194 -0
  62. package/dist/x402/facilitator.js.map +1 -0
  63. package/dist/zktls/reclaim.d.ts +137 -0
  64. package/dist/zktls/reclaim.d.ts.map +1 -0
  65. package/dist/zktls/reclaim.js +245 -0
  66. package/dist/zktls/reclaim.js.map +1 -0
  67. package/package.json +86 -0
  68. package/src/discovery/registry.ts +255 -0
  69. package/src/frameworks/agentkit.ts +537 -0
  70. package/src/frameworks/crewai.ts +102 -0
  71. package/src/frameworks/eliza.ts +776 -0
  72. package/src/frameworks/langchain.ts +325 -0
  73. package/src/index.ts +179 -0
  74. package/src/mandates/env.ts +222 -0
  75. package/src/mandates/mandate.ts +280 -0
  76. package/src/mcp/http-server.ts +38 -0
  77. package/src/mcp/http.ts +620 -0
  78. package/src/mcp/server.ts +1006 -0
  79. package/src/mcp/stdio.ts +8 -0
  80. package/src/sdk/client.ts +222 -0
  81. package/src/wallet/provider.ts +197 -0
  82. package/src/x402/facilitator.ts +336 -0
  83. package/src/zktls/reclaim.ts +424 -0
@@ -0,0 +1,679 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { http, isAddress, isHex } from "viem";
6
+ import { baseSepolia } from "viem/chains";
7
+ import { z } from "zod";
8
+ import { discoverServices as defaultDiscoverServices, } from "../discovery/registry.js";
9
+ import { checkMandate } from "../mandates/mandate.js";
10
+ import { parseServerMandateCapsEnv, signServerMandate, ServerMandateConfigError, } from "../mandates/env.js";
11
+ import { createWalletBackedAgentClient, parseAgentWalletEnv, } from "../wallet/provider.js";
12
+ import { createReclaimConsumerProofServiceFromEnv, } from "../zktls/reclaim.js";
13
+ /**
14
+ * MCP server for the agent lane.
15
+ *
16
+ * The server exposes hire/status/reclaim/list tools over the MCP SDK while
17
+ * keeping all fund-moving operations fail-closed behind a signed spend mandate.
18
+ */
19
+ const addressSchema = z
20
+ .string()
21
+ .refine((value) => isAddress(value), "must be an EVM address");
22
+ const bytes32Schema = z
23
+ .string()
24
+ .refine((value) => isHex(value, { strict: true }) && value.length === 66, "must be bytes32 hex");
25
+ const decimalStringSchema = z
26
+ .string()
27
+ .regex(/^(0|[1-9]\d*)$/, "must be a base-10 integer string");
28
+ // The spend mandate is NOT a tool argument. An MCP caller (an LLM) cannot
29
+ // produce an EIP-712 signature, so the mandate is configured once at server
30
+ // construction (see `signedMandate` in VerifiedStreamMcpServerOptions) exactly
31
+ // like the AgentKit action provider. Tool inputs carry only the hire terms.
32
+ const hireVerifiedServiceSchema = {
33
+ operator: addressSchema.describe("Seller payout wallet (EVM address) that receives USDC for oracle-verified delivery."),
34
+ serviceRef: bytes32Schema.describe("bytes32 service identifier the oracle verifies delivery against (from discover_services)."),
35
+ budgetUsdc: decimalStringSchema.describe('Total USDC to escrow for this stream, in ATOMIC units (6 decimals): 1 USDC = "1000000".'),
36
+ ratePerSecondUsdc: decimalStringSchema.describe("Accrual to the seller per verified second, in USDC ATOMIC units (6 decimals)."),
37
+ durationSeconds: z
38
+ .number()
39
+ .int()
40
+ .positive()
41
+ .optional()
42
+ .describe("Optional max stream lifetime in seconds. Defaults to the server mandate's max duration."),
43
+ };
44
+ const getStreamStatusSchema = {
45
+ streamId: bytes32Schema.describe("Stream id returned by hire_verified_service."),
46
+ };
47
+ const reclaimStreamSchema = {
48
+ streamId: bytes32Schema.describe("Stream id to reclaim unspent USDC from."),
49
+ closeFirst: z
50
+ .boolean()
51
+ .optional()
52
+ .describe("Close the stream before reclaiming. Use when the stream is still active."),
53
+ };
54
+ const proveHttpsResponseSchema = {
55
+ streamId: bytes32Schema,
56
+ url: z.string().url(),
57
+ method: z.enum(["GET", "POST", "PUT"]).optional(),
58
+ headers: z.record(z.string()).optional(),
59
+ body: z.string().optional(),
60
+ responseMatches: z
61
+ .array(z.object({
62
+ type: z.enum(["regex", "contains"]),
63
+ value: z.string().min(1),
64
+ invert: z.boolean().optional(),
65
+ isOptional: z.boolean().optional(),
66
+ }))
67
+ .min(1),
68
+ responseRedactions: z
69
+ .array(z
70
+ .object({
71
+ regex: z.string().optional(),
72
+ jsonPath: z.string().optional(),
73
+ xPath: z.string().optional(),
74
+ hash: z.enum(["oprf", "oprf-raw"]).optional(),
75
+ })
76
+ .refine((value) => value.regex !== undefined ||
77
+ value.jsonPath !== undefined ||
78
+ value.xPath !== undefined, "response redaction needs regex, jsonPath, or xPath"))
79
+ .optional(),
80
+ geoLocation: z.string().optional(),
81
+ useTee: z.boolean().optional(),
82
+ zkEngine: z.enum(["snarkjs", "stwo"]).optional(),
83
+ };
84
+ const metrikAgentEnvSchema = z.object({
85
+ METRIK_AGENT_RPC_URL: z.string().url(),
86
+ METRIK_AGENT_ESCROW: addressSchema,
87
+ METRIK_AGENT_USDC: addressSchema,
88
+ METRIK_AGENT_CHAIN_ID: z.string().optional(),
89
+ METRIK_AGENT_CHAIN_NAME: z.string().optional(),
90
+ METRIK_AGENT_MCP_STATE_FILE: z.string().optional(),
91
+ });
92
+ export const METRIK_MCP_TOOLS = [
93
+ {
94
+ name: "discover_services",
95
+ description: "Discover verified third-party services on the Metrik marketplace. Returns operator + serviceRef + endpoint for listings whose operator signature is cryptographically verified, so an agent can go from discovery to hire without pre-known addresses.",
96
+ movesFunds: false,
97
+ },
98
+ {
99
+ name: "hire_verified_service",
100
+ description: "Open a verified payment stream for a third-party service. Pays only for oracle-verified delivered time. Subject to the agent's spend mandate.",
101
+ movesFunds: true,
102
+ },
103
+ {
104
+ name: "check_stream_status",
105
+ description: "Read a stream's live status, accrued amount, and claimable/reclaimable balances.",
106
+ movesFunds: false,
107
+ },
108
+ {
109
+ name: "reclaim_unspent",
110
+ description: "Close a stream and reclaim unspent funds to the buyer after failure or completion.",
111
+ movesFunds: true,
112
+ },
113
+ {
114
+ name: "list_streams",
115
+ description: "List the agent's streams and their current state.",
116
+ movesFunds: false,
117
+ },
118
+ {
119
+ name: "prove_https_response",
120
+ description: "Generate a zkTLS proof of the exact HTTPS response consumed for a tracked stream. This is an L4 delivery signal and must be cross-checked with oracle/L1 evidence.",
121
+ movesFunds: false,
122
+ },
123
+ ];
124
+ /** @deprecated Renamed to METRIK_MCP_TOOLS; retained for back-compat. */
125
+ export const STREAMPROOF_MCP_TOOLS = METRIK_MCP_TOOLS;
126
+ export class InMemoryStreamRegistry {
127
+ records = new Map();
128
+ async put(record) {
129
+ this.records.set(record.streamId, record);
130
+ }
131
+ async get(streamId) {
132
+ return this.records.get(streamId) ?? null;
133
+ }
134
+ async list() {
135
+ return Array.from(this.records.values());
136
+ }
137
+ }
138
+ export class FileStreamRegistry {
139
+ filePath;
140
+ constructor(filePath) {
141
+ this.filePath = filePath;
142
+ }
143
+ async put(record) {
144
+ const records = await this.readAll();
145
+ records.set(record.streamId, record);
146
+ await this.writeAll(records);
147
+ }
148
+ async get(streamId) {
149
+ const records = await this.readAll();
150
+ return records.get(streamId) ?? null;
151
+ }
152
+ async list() {
153
+ return Array.from((await this.readAll()).values());
154
+ }
155
+ async readAll() {
156
+ const contents = await fs.readFile(this.filePath, "utf8").catch((error) => {
157
+ if (error.code === "ENOENT") {
158
+ return "[]";
159
+ }
160
+ throw error;
161
+ });
162
+ const parsed = z
163
+ .array(z.object({
164
+ streamId: bytes32Schema,
165
+ buyer: addressSchema,
166
+ operator: addressSchema,
167
+ serviceRef: bytes32Schema,
168
+ budgetUsdc: decimalStringSchema,
169
+ ratePerSecondUsdc: decimalStringSchema,
170
+ maxDurationSeconds: z.number().int().positive(),
171
+ openedAtSeconds: z.number().int().nonnegative(),
172
+ mandateId: bytes32Schema,
173
+ }))
174
+ .parse(JSON.parse(contents));
175
+ return new Map(parsed.map((record) => [
176
+ record.streamId,
177
+ {
178
+ streamId: record.streamId,
179
+ buyer: record.buyer,
180
+ operator: record.operator,
181
+ serviceRef: record.serviceRef,
182
+ budgetUsdc: BigInt(record.budgetUsdc),
183
+ ratePerSecondUsdc: BigInt(record.ratePerSecondUsdc),
184
+ maxDurationSeconds: record.maxDurationSeconds,
185
+ openedAtSeconds: record.openedAtSeconds,
186
+ mandateId: record.mandateId,
187
+ },
188
+ ]));
189
+ }
190
+ async writeAll(records) {
191
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
192
+ await fs.writeFile(this.filePath, JSON.stringify(Array.from(records.values()).map((record) => ({
193
+ ...record,
194
+ budgetUsdc: record.budgetUsdc.toString(),
195
+ ratePerSecondUsdc: record.ratePerSecondUsdc.toString(),
196
+ })), null, 2));
197
+ }
198
+ }
199
+ /**
200
+ * Re-sign the server mandate when it is within this many seconds of expiry, so a
201
+ * long-running server never bricks hires on a lapsed construction-time mandate.
202
+ */
203
+ const MANDATE_REFRESH_WINDOW_SECONDS = 300;
204
+ const warnedResolverGaps = new Set();
205
+ /** Emits a stderr warning at most once per process for a given key. */
206
+ function warnOnce(key, message) {
207
+ if (warnedResolverGaps.has(key)) {
208
+ return;
209
+ }
210
+ warnedResolverGaps.add(key);
211
+ // stderr, never stdout: stdout is the MCP stdio transport channel.
212
+ console.error(message);
213
+ }
214
+ const DISCOVER_TOOL_SPEC = METRIK_MCP_TOOLS[0];
215
+ const HIRE_TOOL_SPEC = METRIK_MCP_TOOLS[1];
216
+ const STATUS_TOOL_SPEC = METRIK_MCP_TOOLS[2];
217
+ const RECLAIM_TOOL_SPEC = METRIK_MCP_TOOLS[3];
218
+ const LIST_TOOL_SPEC = METRIK_MCP_TOOLS[4];
219
+ const PROVE_TOOL_SPEC = METRIK_MCP_TOOLS[5];
220
+ const discoverServicesSchema = {
221
+ category: z
222
+ .string()
223
+ .min(1)
224
+ .optional()
225
+ .describe("Optional marketplace category to filter listings by."),
226
+ limit: z
227
+ .number()
228
+ .int()
229
+ .positive()
230
+ .optional()
231
+ .describe("Optional cap on the number of verified listings to return."),
232
+ };
233
+ export function createVerifiedStreamMcpServer(options) {
234
+ const registry = options.registry ?? new InMemoryStreamRegistry();
235
+ // Fail-closed mandate accounting defaults (security):
236
+ // - Cumulative-cap (`maxTotalUsdc`) is enforced by DEFAULT: spent-so-far is
237
+ // summed from escrowed budget tracked in the stream registry, so the total
238
+ // cap applies with no external wiring. This is conservative (counts full
239
+ // escrowed budget, buyer-favoring) — wire an explicit resolver for exact
240
+ // settled-spend accounting.
241
+ // - Revocation cannot be known without an external store, so it defaults to
242
+ // "none revoked" but emits a loud one-time warning: a revoked mandate is NOT
243
+ // rejected until `getRevokedMandateIds` is wired.
244
+ const registryBackedSpent = async (owner) => {
245
+ const records = await registry.list();
246
+ return records
247
+ .filter((record) => record.buyer.toLowerCase() === owner.toLowerCase())
248
+ .reduce((sum, record) => sum + record.budgetUsdc, 0n);
249
+ };
250
+ if (options.mandateStateResolver?.getRevokedMandateIds === undefined) {
251
+ warnOnce("mcp-revocation", "[metrik] MCP server started with no mandate revocation resolver: revoked mandates will NOT be rejected until getRevokedMandateIds is wired. Cumulative spend is bounded by escrowed budget from the stream registry.");
252
+ }
253
+ const resolver = {
254
+ getSpentSoFarUsdc: options.mandateStateResolver?.getSpentSoFarUsdc ?? registryBackedSpent,
255
+ getRevokedMandateIds: options.mandateStateResolver?.getRevokedMandateIds ?? (async () => []),
256
+ };
257
+ // Resolve the CURRENT mandate per fund-moving call. A provider (env builder)
258
+ // re-signs before expiry so a long-running server keeps authorizing hires;
259
+ // without one, the static construction-time mandate is used.
260
+ const resolveMandate = options.mandateProvider ?? (() => options.signedMandate);
261
+ // Per-owner serialization for hires. `getSpentSoFarUsdc` reads the registry
262
+ // BEFORE the new stream's record is written, so concurrent hires from one
263
+ // owner could both observe stale spend and overshoot `maxTotalUsdc`. Chaining
264
+ // hires per owner makes the read→check→open→record sequence atomic per owner.
265
+ const ownerHireChains = new Map();
266
+ const serializeHire = (owner, task) => {
267
+ const key = owner.toLowerCase();
268
+ const prev = ownerHireChains.get(key) ?? Promise.resolve();
269
+ const run = prev.then(task, task);
270
+ ownerHireChains.set(key, run.then(() => undefined, () => undefined));
271
+ return run;
272
+ };
273
+ const now = options.now ?? (() => Math.floor(Date.now() / 1000));
274
+ const discover = options.discoverServices ?? defaultDiscoverServices;
275
+ const mcpServer = new McpServer(options.serverInfo ?? {
276
+ name: "metrik-agent-mcp",
277
+ version: "0.1.0",
278
+ });
279
+ mcpServer.registerTool("discover_services", {
280
+ description: DISCOVER_TOOL_SPEC.description,
281
+ inputSchema: discoverServicesSchema,
282
+ annotations: {
283
+ idempotentHint: true,
284
+ openWorldHint: true,
285
+ readOnlyHint: true,
286
+ title: "Discover Verified Services",
287
+ },
288
+ }, async ({ category, limit }) => {
289
+ try {
290
+ const listings = await discover({
291
+ ...(category === undefined ? {} : { category }),
292
+ ...(limit === undefined ? {} : { limit }),
293
+ });
294
+ return successResult(`discovered ${listings.length} verified service${listings.length === 1 ? "" : "s"}`, { services: listings.map(serializeServiceListing) });
295
+ }
296
+ catch (error) {
297
+ return safeToolError(error);
298
+ }
299
+ });
300
+ mcpServer.registerTool("hire_verified_service", {
301
+ description: HIRE_TOOL_SPEC.description,
302
+ inputSchema: hireVerifiedServiceSchema,
303
+ annotations: {
304
+ destructiveHint: true,
305
+ idempotentHint: false,
306
+ openWorldHint: true,
307
+ readOnlyHint: false,
308
+ title: "Hire Verified Service",
309
+ },
310
+ }, async (args) => {
311
+ try {
312
+ const signedMandate = await resolveMandate();
313
+ const owner = signedMandate.owner;
314
+ const nowSeconds = now();
315
+ const request = {
316
+ operator: args.operator,
317
+ budgetUsdc: BigInt(args.budgetUsdc),
318
+ ratePerSecondUsdc: BigInt(args.ratePerSecondUsdc),
319
+ maxDurationSeconds: args.durationSeconds ?? signedMandate.mandate.maxDurationSeconds,
320
+ };
321
+ // Serialize per owner: the spent-so-far read, cap check, open, and
322
+ // registry write must be atomic, or concurrent hires could both pass
323
+ // the cumulative-cap check on stale spend and overshoot maxTotalUsdc.
324
+ return await serializeHire(owner, async () => {
325
+ const spentSoFarUsdc = await resolver.getSpentSoFarUsdc(owner);
326
+ const revokedMandateIds = await resolver.getRevokedMandateIds(owner);
327
+ const decision = await checkMandate(signedMandate, request, spentSoFarUsdc, {
328
+ nowSeconds,
329
+ revokedMandateIds,
330
+ });
331
+ if (!decision.allowed) {
332
+ return errorResult(`mandate denied: ${decision.reason ?? "unknown"}`);
333
+ }
334
+ const result = await options.agentClient.openVerifiedStream({
335
+ operator: request.operator,
336
+ serviceRef: args.serviceRef,
337
+ budgetUsdc: request.budgetUsdc,
338
+ ratePerSecondUsdc: request.ratePerSecondUsdc,
339
+ maxDurationSeconds: request.maxDurationSeconds,
340
+ signedMandate,
341
+ spentSoFarUsdc,
342
+ nowSeconds,
343
+ revokedMandateIds,
344
+ });
345
+ await registry.put({
346
+ streamId: result.streamId,
347
+ buyer: owner,
348
+ operator: request.operator,
349
+ serviceRef: args.serviceRef,
350
+ budgetUsdc: request.budgetUsdc,
351
+ ratePerSecondUsdc: request.ratePerSecondUsdc,
352
+ maxDurationSeconds: request.maxDurationSeconds,
353
+ openedAtSeconds: nowSeconds,
354
+ mandateId: signedMandate.mandateId,
355
+ });
356
+ return successResult(`opened verified stream ${result.streamId} on chain ${result.handle.chainId}`, {
357
+ streamId: result.streamId,
358
+ txHash: result.txHash,
359
+ approveTxHash: result.approveTxHash,
360
+ handle: {
361
+ streamId: result.handle.streamId,
362
+ escrow: result.handle.escrow,
363
+ chainId: result.handle.chainId,
364
+ },
365
+ });
366
+ });
367
+ }
368
+ catch (error) {
369
+ return safeToolError(error);
370
+ }
371
+ });
372
+ mcpServer.registerTool("check_stream_status", {
373
+ description: STATUS_TOOL_SPEC.description,
374
+ inputSchema: getStreamStatusSchema,
375
+ annotations: {
376
+ idempotentHint: true,
377
+ openWorldHint: false,
378
+ readOnlyHint: true,
379
+ title: "Get Stream Status",
380
+ },
381
+ }, async ({ streamId }) => {
382
+ try {
383
+ const status = await options.agentClient.getStreamStatus(streamId);
384
+ return successResult(`stream ${streamId} is ${status.stream.status}`, serializeStreamStatus(status, streamId));
385
+ }
386
+ catch (error) {
387
+ return safeToolError(error);
388
+ }
389
+ });
390
+ mcpServer.registerTool("reclaim_unspent", {
391
+ description: RECLAIM_TOOL_SPEC.description,
392
+ inputSchema: reclaimStreamSchema,
393
+ annotations: {
394
+ destructiveHint: true,
395
+ idempotentHint: false,
396
+ openWorldHint: true,
397
+ readOnlyHint: false,
398
+ title: "Reclaim Stream",
399
+ },
400
+ }, async ({ streamId, closeFirst }) => {
401
+ try {
402
+ const record = await registry.get(streamId);
403
+ if (record === null) {
404
+ return errorResult(`unknown stream: ${streamId}`);
405
+ }
406
+ const signedMandate = await resolveMandate();
407
+ // Reclaim/close only RETURN unspent funds to the buyer or stop payment.
408
+ // They are authorized on mandate-owner signature ALONE (see the SDK
409
+ // client's authorizeBuyerRecovery) — never gated by expiry, revocation,
410
+ // or caps, so a lapsed/revoked mandate can never strand buyer funds.
411
+ const result = await options.agentClient.reclaimStream({
412
+ streamId: streamId,
413
+ signedMandate,
414
+ spentSoFarUsdc: 0n,
415
+ nowSeconds: now(),
416
+ ...(closeFirst === undefined ? {} : { closeFirst }),
417
+ });
418
+ return successResult(`reclaimed stream ${streamId}`, {
419
+ closeTxHash: result.closeResult?.txHash ?? null,
420
+ reclaimTxHash: result.reclaimResult.txHash,
421
+ });
422
+ }
423
+ catch (error) {
424
+ return safeToolError(error);
425
+ }
426
+ });
427
+ mcpServer.registerTool("list_streams", {
428
+ description: LIST_TOOL_SPEC.description,
429
+ annotations: {
430
+ idempotentHint: true,
431
+ openWorldHint: false,
432
+ readOnlyHint: true,
433
+ title: "List Streams",
434
+ },
435
+ }, async () => {
436
+ try {
437
+ const records = await registry.list();
438
+ const entries = await Promise.all(records.map(async (record) => {
439
+ const status = await options.agentClient
440
+ .getStreamStatus(record.streamId)
441
+ .then((value) => serializeStreamStatus(value, record.streamId))
442
+ .catch((error) => ({
443
+ streamId: record.streamId,
444
+ statusError: asErrorMessage(error),
445
+ }));
446
+ return {
447
+ streamId: record.streamId,
448
+ buyer: record.buyer,
449
+ operator: record.operator,
450
+ serviceRef: record.serviceRef,
451
+ budgetUsdc: record.budgetUsdc.toString(),
452
+ ratePerSecondUsdc: record.ratePerSecondUsdc.toString(),
453
+ maxDurationSeconds: record.maxDurationSeconds,
454
+ openedAtSeconds: record.openedAtSeconds,
455
+ mandateId: record.mandateId,
456
+ status,
457
+ };
458
+ }));
459
+ return successResult(`tracked ${entries.length} stream${entries.length === 1 ? "" : "s"}`, { streams: entries });
460
+ }
461
+ catch (error) {
462
+ return safeToolError(error);
463
+ }
464
+ });
465
+ if (options.deliveryProofService !== undefined) {
466
+ const deliveryProofService = options.deliveryProofService;
467
+ mcpServer.registerTool("prove_https_response", {
468
+ description: PROVE_TOOL_SPEC.description,
469
+ inputSchema: proveHttpsResponseSchema,
470
+ annotations: {
471
+ idempotentHint: false,
472
+ openWorldHint: true,
473
+ readOnlyHint: true,
474
+ title: "Prove HTTPS Response",
475
+ },
476
+ }, async ({ streamId, url, method, headers, body, responseMatches, responseRedactions, geoLocation, useTee, zkEngine, }) => {
477
+ try {
478
+ const record = await registry.get(streamId);
479
+ if (record === null) {
480
+ return errorResult(`unknown stream: ${streamId}`);
481
+ }
482
+ const result = await deliveryProofService.proveConsumedHttpsResponse({
483
+ url,
484
+ ...(method === undefined ? {} : { method }),
485
+ ...(headers === undefined ? {} : { headers }),
486
+ ...(body === undefined ? {} : { body }),
487
+ responseMatches,
488
+ ...(responseRedactions === undefined ? {} : { responseRedactions }),
489
+ ...(geoLocation === undefined ? {} : { geoLocation }),
490
+ ...(useTee === undefined ? {} : { useTee }),
491
+ ...(zkEngine === undefined ? {} : { zkEngine }),
492
+ delivery: {
493
+ owner: record.buyer,
494
+ streamId: record.streamId,
495
+ operator: record.operator,
496
+ serviceRef: record.serviceRef,
497
+ mandateId: record.mandateId,
498
+ },
499
+ });
500
+ return successResult(`generated zkTLS response proof for stream ${streamId}`, serializeConsumedResponseProof(record.streamId, result));
501
+ }
502
+ catch (error) {
503
+ return safeToolError(error);
504
+ }
505
+ });
506
+ }
507
+ return {
508
+ mcpServer,
509
+ registry,
510
+ connectStdio: async () => {
511
+ await mcpServer.connect(new StdioServerTransport());
512
+ },
513
+ close: async () => {
514
+ await mcpServer.close();
515
+ },
516
+ };
517
+ }
518
+ export function parseMetrikAgentEnv(env = process.env) {
519
+ const parsed = metrikAgentEnvSchema.parse(env);
520
+ const chainId = Number(parsed.METRIK_AGENT_CHAIN_ID ?? baseSepolia.id);
521
+ if (!Number.isInteger(chainId) || chainId <= 0) {
522
+ throw new Error("METRIK_AGENT_CHAIN_ID must be a positive integer");
523
+ }
524
+ if (chainId !== baseSepolia.id) {
525
+ throw new Error(`unsupported METRIK_AGENT_CHAIN_ID ${chainId}; phase 2 is Base Sepolia only`);
526
+ }
527
+ return {
528
+ rpcUrl: parsed.METRIK_AGENT_RPC_URL,
529
+ escrow: parsed.METRIK_AGENT_ESCROW,
530
+ usdc: parsed.METRIK_AGENT_USDC,
531
+ chain: {
532
+ ...baseSepolia,
533
+ name: parsed.METRIK_AGENT_CHAIN_NAME ?? baseSepolia.name,
534
+ },
535
+ ...(parsed.METRIK_AGENT_MCP_STATE_FILE === undefined
536
+ ? {}
537
+ : {
538
+ stateFile: parsed.METRIK_AGENT_MCP_STATE_FILE,
539
+ }),
540
+ wallet: parseAgentWalletEnv(env),
541
+ };
542
+ }
543
+ /**
544
+ * Builds the {@link VerifiedStreamMcpServerOptions} for a single wallet-backed
545
+ * tenant from environment configuration.
546
+ *
547
+ * This is split out from {@link createVerifiedStreamMcpServerFromEnv} so a
548
+ * multi-session transport (see `mcp/http.ts`) can construct a FRESH
549
+ * {@link McpServer} per MCP session while sharing one tenant's wallet-backed
550
+ * agent client and stream registry. A single `McpServer` can only be bound to
551
+ * one transport at a time, so concurrent sessions each need their own server
552
+ * instance built from these shared options.
553
+ */
554
+ export async function createVerifiedStreamMcpServerOptionsFromEnv(env = process.env) {
555
+ const parsed = parseMetrikAgentEnv(env);
556
+ const caps = parseServerMandateCapsEnv(env);
557
+ if (caps === null) {
558
+ throw new ServerMandateConfigError("no spend mandate configured: set METRIK_AGENT_MANDATE_MAX_PER_STREAM_USDC, " +
559
+ "METRIK_AGENT_MANDATE_MAX_TOTAL_USDC, METRIK_AGENT_MANDATE_MAX_RATE_PER_SECOND_USDC, " +
560
+ "and METRIK_AGENT_MANDATE_MAX_DURATION_SECONDS. A fund-moving server refuses to run " +
561
+ "without an explicit spend ceiling.");
562
+ }
563
+ const deliveryProofService = createReclaimConsumerProofServiceFromEnv(env);
564
+ const { agentClient, wallet } = await createWalletBackedAgentClient({
565
+ chain: parsed.chain,
566
+ transport: http(parsed.rpcUrl),
567
+ escrow: parsed.escrow,
568
+ usdc: parsed.usdc,
569
+ settlementTarget: {
570
+ chainId: parsed.chain.id,
571
+ escrow: parsed.escrow,
572
+ },
573
+ }, parsed.wallet);
574
+ // Self-sign the mandate from the server's own funding wallet (its address is
575
+ // the mandate owner). The MCP caller never signs — see hire_verified_service.
576
+ // A lazily-re-signing provider keeps a long-running server authorizing hires:
577
+ // when the cached mandate is within the refresh window of its expiry, it is
578
+ // re-signed. (Reclaim/close are signature-only, so they never expire.)
579
+ const chainId = parsed.chain.id;
580
+ const signMandate = () => signServerMandate(wallet.account, caps, { chainId });
581
+ const signedMandate = await signMandate();
582
+ let cachedMandate = signedMandate;
583
+ const mandateProvider = async () => {
584
+ const nowSeconds = Math.floor(Date.now() / 1000);
585
+ if (nowSeconds >=
586
+ cachedMandate.mandate.expiresAt - MANDATE_REFRESH_WINDOW_SECONDS) {
587
+ cachedMandate = await signMandate();
588
+ }
589
+ return cachedMandate;
590
+ };
591
+ return {
592
+ agentClient,
593
+ signedMandate,
594
+ mandateProvider,
595
+ registry: parsed.stateFile === undefined
596
+ ? new InMemoryStreamRegistry()
597
+ : new FileStreamRegistry(parsed.stateFile),
598
+ ...(deliveryProofService === null ? {} : { deliveryProofService }),
599
+ };
600
+ }
601
+ export async function createVerifiedStreamMcpServerFromEnv(env = process.env) {
602
+ return createVerifiedStreamMcpServer(await createVerifiedStreamMcpServerOptionsFromEnv(env));
603
+ }
604
+ export async function startVerifiedStreamMcpServerStdio(env = process.env) {
605
+ const server = await createVerifiedStreamMcpServerFromEnv(env);
606
+ await server.connectStdio();
607
+ }
608
+ function serializeServiceListing(listing) {
609
+ return {
610
+ serviceRef: listing.serviceRef,
611
+ operator: listing.operator,
612
+ publicUrl: listing.publicUrl,
613
+ category: listing.category,
614
+ ...(listing.targetMetadata === undefined
615
+ ? {}
616
+ : { targetMetadata: listing.targetMetadata }),
617
+ };
618
+ }
619
+ function serializeStreamStatus(status, streamId) {
620
+ return {
621
+ streamId,
622
+ stream: {
623
+ buyer: status.stream.buyer,
624
+ operator: status.stream.operator,
625
+ serviceRef: status.stream.serviceRef,
626
+ deposit: status.stream.deposit.toString(),
627
+ ratePerSecond: status.stream.ratePerSecond.toString(),
628
+ accrued: status.stream.accrued.toString(),
629
+ claimed: status.stream.claimed.toString(),
630
+ openedAt: status.stream.openedAt,
631
+ expiresAt: status.stream.expiresAt,
632
+ lastVerifiedAt: status.stream.lastVerifiedAt,
633
+ lastSequence: status.stream.lastSequence.toString(),
634
+ consecutiveFailures: status.stream.consecutiveFailures,
635
+ status: status.stream.status,
636
+ },
637
+ claimable: status.claimable.toString(),
638
+ reclaimable: status.reclaimable.toString(),
639
+ };
640
+ }
641
+ function serializeConsumedResponseProof(streamId, proof) {
642
+ return {
643
+ streamId,
644
+ proof: proof.proof,
645
+ request: {
646
+ ...proof.request,
647
+ responseMatches: [...proof.request.responseMatches],
648
+ responseRedactions: [...proof.request.responseRedactions],
649
+ },
650
+ verification: proof.verification,
651
+ };
652
+ }
653
+ function successResult(text, structuredContent) {
654
+ return {
655
+ content: [{ type: "text", text }],
656
+ ...(structuredContent === undefined ? {} : { structuredContent }),
657
+ };
658
+ }
659
+ function errorResult(text) {
660
+ return {
661
+ content: [{ type: "text", text }],
662
+ isError: true,
663
+ };
664
+ }
665
+ function asErrorMessage(error) {
666
+ return error instanceof Error ? error.message : String(error);
667
+ }
668
+ /**
669
+ * Logs the full error server-side (stderr) and returns a GENERIC message to the
670
+ * MCP caller. Underlying viem/CDP/SDK error strings can carry internal detail
671
+ * (RPC URLs, addresses, revert traces); tool callers see only that it failed.
672
+ * Deliberate, caller-actionable messages (mandate denied, unknown stream) are
673
+ * returned directly via errorResult and are unaffected.
674
+ */
675
+ function safeToolError(error) {
676
+ console.error("[metrik] MCP tool error:", asErrorMessage(error));
677
+ return errorResult("The tool call failed. See server logs for details.");
678
+ }
679
+ //# sourceMappingURL=server.js.map