@tabai/sdk 0.2.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 (204) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -0
  3. package/bin/tab.mjs +23 -0
  4. package/dist/_shared/abi.d.ts +150 -0
  5. package/dist/_shared/abi.d.ts.map +1 -0
  6. package/dist/_shared/abi.js +197 -0
  7. package/dist/_shared/abi.js.map +1 -0
  8. package/dist/_shared/chains.d.ts +118 -0
  9. package/dist/_shared/chains.d.ts.map +1 -0
  10. package/dist/_shared/chains.js +89 -0
  11. package/dist/_shared/chains.js.map +1 -0
  12. package/dist/_shared/hex.d.ts +35 -0
  13. package/dist/_shared/hex.d.ts.map +1 -0
  14. package/dist/_shared/hex.js +40 -0
  15. package/dist/_shared/hex.js.map +1 -0
  16. package/dist/_shared/index.d.ts +14 -0
  17. package/dist/_shared/index.d.ts.map +1 -0
  18. package/dist/_shared/index.js +14 -0
  19. package/dist/_shared/index.js.map +1 -0
  20. package/dist/_shared/keccak256.d.ts +29 -0
  21. package/dist/_shared/keccak256.d.ts.map +1 -0
  22. package/dist/_shared/keccak256.js +145 -0
  23. package/dist/_shared/keccak256.js.map +1 -0
  24. package/dist/_shared/result.d.ts +78 -0
  25. package/dist/_shared/result.d.ts.map +1 -0
  26. package/dist/_shared/result.js +61 -0
  27. package/dist/_shared/result.js.map +1 -0
  28. package/dist/cli/client-config.d.ts +155 -0
  29. package/dist/cli/client-config.d.ts.map +1 -0
  30. package/dist/cli/client-config.js +382 -0
  31. package/dist/cli/client-config.js.map +1 -0
  32. package/dist/cli/connect.d.ts +76 -0
  33. package/dist/cli/connect.d.ts.map +1 -0
  34. package/dist/cli/connect.js +158 -0
  35. package/dist/cli/connect.js.map +1 -0
  36. package/dist/cli/doctor.d.ts +57 -0
  37. package/dist/cli/doctor.d.ts.map +1 -0
  38. package/dist/cli/doctor.js +253 -0
  39. package/dist/cli/doctor.js.map +1 -0
  40. package/dist/cli/index.d.ts +13 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +13 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/cli/main.d.ts +45 -0
  45. package/dist/cli/main.d.ts.map +1 -0
  46. package/dist/cli/main.js +371 -0
  47. package/dist/cli/main.js.map +1 -0
  48. package/dist/errors.d.ts +29 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +37 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/http/client-402.d.ts +243 -0
  53. package/dist/http/client-402.d.ts.map +1 -0
  54. package/dist/http/client-402.js +515 -0
  55. package/dist/http/client-402.js.map +1 -0
  56. package/dist/http/headers.d.ts +173 -0
  57. package/dist/http/headers.d.ts.map +1 -0
  58. package/dist/http/headers.js +284 -0
  59. package/dist/http/headers.js.map +1 -0
  60. package/dist/http/index.d.ts +15 -0
  61. package/dist/http/index.d.ts.map +1 -0
  62. package/dist/http/index.js +15 -0
  63. package/dist/http/index.js.map +1 -0
  64. package/dist/http/metering-claim.d.ts +82 -0
  65. package/dist/http/metering-claim.d.ts.map +1 -0
  66. package/dist/http/metering-claim.js +99 -0
  67. package/dist/http/metering-claim.js.map +1 -0
  68. package/dist/index.d.ts +48 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +51 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/logger.d.ts +40 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +50 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/assets.d.ts +31 -0
  77. package/dist/mcp/assets.d.ts.map +1 -0
  78. package/dist/mcp/assets.js +78 -0
  79. package/dist/mcp/assets.js.map +1 -0
  80. package/dist/mcp/index.d.ts +19 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/index.js +19 -0
  83. package/dist/mcp/index.js.map +1 -0
  84. package/dist/mcp/json-schema.d.ts +86 -0
  85. package/dist/mcp/json-schema.d.ts.map +1 -0
  86. package/dist/mcp/json-schema.js +215 -0
  87. package/dist/mcp/json-schema.js.map +1 -0
  88. package/dist/mcp/json.d.ts +43 -0
  89. package/dist/mcp/json.d.ts.map +1 -0
  90. package/dist/mcp/json.js +69 -0
  91. package/dist/mcp/json.js.map +1 -0
  92. package/dist/mcp/registry-client.d.ts +88 -0
  93. package/dist/mcp/registry-client.d.ts.map +1 -0
  94. package/dist/mcp/registry-client.js +158 -0
  95. package/dist/mcp/registry-client.js.map +1 -0
  96. package/dist/mcp/schemas.d.ts +82 -0
  97. package/dist/mcp/schemas.d.ts.map +1 -0
  98. package/dist/mcp/schemas.js +493 -0
  99. package/dist/mcp/schemas.js.map +1 -0
  100. package/dist/mcp/server.d.ts +97 -0
  101. package/dist/mcp/server.d.ts.map +1 -0
  102. package/dist/mcp/server.js +285 -0
  103. package/dist/mcp/server.js.map +1 -0
  104. package/dist/mcp/settings.d.ts +90 -0
  105. package/dist/mcp/settings.d.ts.map +1 -0
  106. package/dist/mcp/settings.js +160 -0
  107. package/dist/mcp/settings.js.map +1 -0
  108. package/dist/mcp/toolset.d.ts +231 -0
  109. package/dist/mcp/toolset.d.ts.map +1 -0
  110. package/dist/mcp/toolset.js +760 -0
  111. package/dist/mcp/toolset.js.map +1 -0
  112. package/dist/payments/abi.d.ts +9 -0
  113. package/dist/payments/abi.d.ts.map +1 -0
  114. package/dist/payments/abi.js +17 -0
  115. package/dist/payments/abi.js.map +1 -0
  116. package/dist/payments/config.d.ts +199 -0
  117. package/dist/payments/config.d.ts.map +1 -0
  118. package/dist/payments/config.js +259 -0
  119. package/dist/payments/config.js.map +1 -0
  120. package/dist/payments/index.d.ts +13 -0
  121. package/dist/payments/index.d.ts.map +1 -0
  122. package/dist/payments/index.js +13 -0
  123. package/dist/payments/index.js.map +1 -0
  124. package/dist/payments/kuru.d.ts +191 -0
  125. package/dist/payments/kuru.d.ts.map +1 -0
  126. package/dist/payments/kuru.js +377 -0
  127. package/dist/payments/kuru.js.map +1 -0
  128. package/dist/payments/monad.d.ts +69 -0
  129. package/dist/payments/monad.d.ts.map +1 -0
  130. package/dist/payments/monad.js +306 -0
  131. package/dist/payments/monad.js.map +1 -0
  132. package/dist/payments/permit2.d.ts +118 -0
  133. package/dist/payments/permit2.d.ts.map +1 -0
  134. package/dist/payments/permit2.js +366 -0
  135. package/dist/payments/permit2.js.map +1 -0
  136. package/dist/payments/registry.d.ts +119 -0
  137. package/dist/payments/registry.d.ts.map +1 -0
  138. package/dist/payments/registry.js +199 -0
  139. package/dist/payments/registry.js.map +1 -0
  140. package/dist/payments/strategy.d.ts +80 -0
  141. package/dist/payments/strategy.d.ts.map +1 -0
  142. package/dist/payments/strategy.js +103 -0
  143. package/dist/payments/strategy.js.map +1 -0
  144. package/dist/proxy/hooks.d.ts +90 -0
  145. package/dist/proxy/hooks.d.ts.map +1 -0
  146. package/dist/proxy/hooks.js +35 -0
  147. package/dist/proxy/hooks.js.map +1 -0
  148. package/dist/proxy/index.d.ts +9 -0
  149. package/dist/proxy/index.d.ts.map +1 -0
  150. package/dist/proxy/index.js +9 -0
  151. package/dist/proxy/index.js.map +1 -0
  152. package/dist/proxy/proxy.d.ts +156 -0
  153. package/dist/proxy/proxy.d.ts.map +1 -0
  154. package/dist/proxy/proxy.js +366 -0
  155. package/dist/proxy/proxy.js.map +1 -0
  156. package/dist/server/adapters/express.d.ts +89 -0
  157. package/dist/server/adapters/express.d.ts.map +1 -0
  158. package/dist/server/adapters/express.js +215 -0
  159. package/dist/server/adapters/express.js.map +1 -0
  160. package/dist/server/adapters/hono.d.ts +52 -0
  161. package/dist/server/adapters/hono.d.ts.map +1 -0
  162. package/dist/server/adapters/hono.js +61 -0
  163. package/dist/server/adapters/hono.js.map +1 -0
  164. package/dist/server/adapters/next.d.ts +52 -0
  165. package/dist/server/adapters/next.d.ts.map +1 -0
  166. package/dist/server/adapters/next.js +56 -0
  167. package/dist/server/adapters/next.js.map +1 -0
  168. package/dist/server/index.d.ts +30 -0
  169. package/dist/server/index.d.ts.map +1 -0
  170. package/dist/server/index.js +30 -0
  171. package/dist/server/index.js.map +1 -0
  172. package/dist/server/metering.d.ts +209 -0
  173. package/dist/server/metering.d.ts.map +1 -0
  174. package/dist/server/metering.js +365 -0
  175. package/dist/server/metering.js.map +1 -0
  176. package/dist/server/post-paid.d.ts +355 -0
  177. package/dist/server/post-paid.d.ts.map +1 -0
  178. package/dist/server/post-paid.js +512 -0
  179. package/dist/server/post-paid.js.map +1 -0
  180. package/dist/x402/client.d.ts +203 -0
  181. package/dist/x402/client.d.ts.map +1 -0
  182. package/dist/x402/client.js +337 -0
  183. package/dist/x402/client.js.map +1 -0
  184. package/dist/x402/hub.d.ts +79 -0
  185. package/dist/x402/hub.d.ts.map +1 -0
  186. package/dist/x402/hub.js +164 -0
  187. package/dist/x402/hub.js.map +1 -0
  188. package/dist/x402/index.d.ts +27 -0
  189. package/dist/x402/index.d.ts.map +1 -0
  190. package/dist/x402/index.js +27 -0
  191. package/dist/x402/index.js.map +1 -0
  192. package/dist/x402/proxy.d.ts +162 -0
  193. package/dist/x402/proxy.d.ts.map +1 -0
  194. package/dist/x402/proxy.js +198 -0
  195. package/dist/x402/proxy.js.map +1 -0
  196. package/dist/x402/server.d.ts +162 -0
  197. package/dist/x402/server.d.ts.map +1 -0
  198. package/dist/x402/server.js +306 -0
  199. package/dist/x402/server.js.map +1 -0
  200. package/dist/x402/wire.d.ts +104 -0
  201. package/dist/x402/wire.d.ts.map +1 -0
  202. package/dist/x402/wire.js +265 -0
  203. package/dist/x402/wire.js.map +1 -0
  204. package/package.json +61 -0
@@ -0,0 +1,760 @@
1
+ /**
2
+ * The four Tab tools, as functions.
3
+ *
4
+ * The MCP protocol wiring lives next door in `server.ts`. This file is the
5
+ * behaviour, and it is separate so the tools can be exercised without a
6
+ * transport: every test in `test/mcp-tools.test.mjs` calls these directly, which
7
+ * is what lets task 16.3 validate every output against its declared schema
8
+ * without standing a server up.
9
+ *
10
+ * ## Nothing here throws, including into the transport
11
+ *
12
+ * A tool that throws over stdio does not produce a bad answer, it produces no
13
+ * answer: the MCP client sees a protocol error, the model sees nothing it can
14
+ * reason about, and a long-running server can lose its transport to one
15
+ * malformed argument. So every entry point below returns a value that validates
16
+ * against the tool's declared output schema, on every path, including the paths
17
+ * where the work failed. A failure is `error: { category, code, message }` in
18
+ * the payload, and for `tab_call` and `tab_settle` it is `ok: false` beside it.
19
+ *
20
+ * `LIMIT_EXCEEDED` is the case that earns the discipline. An Agent out of
21
+ * headroom is not an error in the Agent, the Service, or the call -- it is the
22
+ * credit facility working. The tool answers `ok: false` with
23
+ * `requiredBaseUnits` and `headroomBaseUnits` both filled in, so the model can
24
+ * settle the difference and call again instead of retrying into the same wall.
25
+ * (R21.5)
26
+ *
27
+ * ## Every input is validated against the schema that was published
28
+ *
29
+ * Not a hand-written check that resembles it. {@link applyJsonDefaults} fills in
30
+ * the declared defaults and {@link validateJsonValue} enforces the same document
31
+ * `tools/list` served, so a `limit` of 500 is refused with the same bound the
32
+ * model was shown.
33
+ *
34
+ * Requirements: 21.5, 25.1, 25.2, 25.3, 25.4
35
+ */
36
+ import { isAddress, ok } from "../_shared/index.js";
37
+ import { decodeBytes32String, encodeBytes32String } from "ethers";
38
+ import { fail, notFoundError, tabError, upstreamError, validationError } from "../errors.js";
39
+ import { createTab402Client } from "../http/client-402.js";
40
+ import { defaultLogger } from "../logger.js";
41
+ import { loadTabConfig } from "../payments/config.js";
42
+ import { moduleStrategyRegistry } from "../payments/registry.js";
43
+ import { assetFacts, assetStringOf, formatAsset, parseAsset } from "./assets.js";
44
+ import { applyJsonDefaults, validateJsonValue } from "./json-schema.js";
45
+ import { asArray, asBoolean, asDigits, asDigitsOrNull, asNumber, asRecord, asString, asStringOrNull, field, isRecord, path as jsonPath, secondsToIso, } from "./json.js";
46
+ import { createRegistryReadClient } from "./registry-client.js";
47
+ import { fetchHubManifest } from "../x402/hub.js";
48
+ import { TAB_CALL_INPUT, TAB_DISCOVER_INPUT, TAB_SETTLE_INPUT, TAB_STATUS_INPUT, tabToolByName, } from "./schemas.js";
49
+ const errorOf = (error) => {
50
+ const details = asRecord(error.details);
51
+ const required = asDigitsOrNull(details["requiredBaseUnits"]);
52
+ const headroom = asDigitsOrNull(details["headroomBaseUnits"]);
53
+ return {
54
+ category: error.category,
55
+ code: error.code,
56
+ message: error.message,
57
+ retryable: error.retryable,
58
+ ...(required === null ? {} : { requiredBaseUnits: required }),
59
+ ...(headroom === null ? {} : { headroomBaseUnits: headroom }),
60
+ };
61
+ };
62
+ /** Validates a tool input against the schema that was published for it. */
63
+ function readInput(toolName, raw) {
64
+ const declaration = tabToolByName(toolName);
65
+ if (declaration === undefined) {
66
+ return notFoundError("TOOL_UNKNOWN", `\`${toolName}\` is not a tool this server declares`, {
67
+ details: { tool: toolName },
68
+ });
69
+ }
70
+ const supplied = raw === undefined || raw === null ? {} : raw;
71
+ return validateJsonValue(declaration.inputSchema, applyJsonDefaults(declaration.inputSchema, supplied), `${toolName} input`, "INPUT_INVALID");
72
+ }
73
+ /** The ascii a 32-byte word decodes to, or null when it is not padded ascii. */
74
+ function wordToName(word) {
75
+ try {
76
+ const decoded = decodeBytes32String(word);
77
+ return /^[\x20-\x7e]+$/.test(decoded) ? decoded : null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** A tool name as its 32-byte key: a hex word passes through, ascii is padded. */
84
+ function toolKeyOf(tool) {
85
+ if (/^0x[a-fA-F0-9]{64}$/.test(tool))
86
+ return ok(tool.toLowerCase());
87
+ try {
88
+ return ok(encodeBytes32String(tool).toLowerCase());
89
+ }
90
+ catch {
91
+ return validationError("TOOL_NAME_TOO_LONG", `\`${tool}\` cannot be a tool key: a tool name is stored as 31 bytes of ascii, zero-padded, or given as a 32-byte hex word`, { details: { tool } });
92
+ }
93
+ }
94
+ const ZERO_WORD = `0x${"00".repeat(32)}`;
95
+ export function createTabToolset(options) {
96
+ const settings = options.settings;
97
+ const logger = options.logger ?? defaultLogger;
98
+ const now = options.now ?? (() => Date.now());
99
+ const env = options.env ?? process.env;
100
+ const chainId = settings.chainId;
101
+ /**
102
+ * The strategy registry, filled from `tab.config` on first use.
103
+ *
104
+ * Deferred, and this is the one piece of laziness in the package that is not
105
+ * an optimisation. Resolving a strategy entry runs consumer code: a factory in
106
+ * a config file typically builds a signer, and a signer is built from a key.
107
+ * Doing that at startup would mean `tab_discover`, `tab_status` and `tab
108
+ * doctor` -- three surfaces whose whole claim is that they need no key --
109
+ * loading one every time the server starts. So the load happens inside
110
+ * `tab_settle`, the only tool that signs, at the moment it needs a strategy.
111
+ *
112
+ * Memoised, so a second Settlement does not re-import the config, and
113
+ * registration is idempotent by strategy id in any case.
114
+ */
115
+ let loading;
116
+ const strategiesFor = () => {
117
+ if (options.strategies !== undefined)
118
+ return Promise.resolve(options.strategies);
119
+ loading ??= (async () => {
120
+ const registry = moduleStrategyRegistry(logger);
121
+ if (registry.list().length > 0)
122
+ return registry;
123
+ const loaded = await loadTabConfig({
124
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
125
+ registry,
126
+ logger,
127
+ });
128
+ if (!loaded.ok) {
129
+ logger.warn("tab.config could not be loaded, so no payment strategy is registered", {
130
+ code: loaded.error.code,
131
+ message: loaded.error.message,
132
+ });
133
+ }
134
+ return registry;
135
+ })();
136
+ return loading;
137
+ };
138
+ const directory = new Map(settings.services.map((entry) => [entry.serviceId.toLowerCase(), entry]));
139
+ /** The registry client, or the one failure that stands in for it. */
140
+ const registryOf = () => {
141
+ if (options.registry !== undefined)
142
+ return ok(options.registry);
143
+ if (settings.registryUrl === undefined) {
144
+ return upstreamError("REGISTRY_UNCONFIGURED", "no Tab registry read API is configured, so on-chain discovery and status cannot be read; set NEXT_PUBLIC_REGISTRY_API_URL, or registryUrl in tab.config");
145
+ }
146
+ return ok(createRegistryReadClient({
147
+ baseUrl: settings.registryUrl,
148
+ ...(options.registryFetch === undefined ? {} : { fetchImpl: options.registryFetch }),
149
+ logger,
150
+ }));
151
+ };
152
+ // ------------------------------------------------------------- tab_discover
153
+ const discover = async (raw) => {
154
+ const input = readInput("tab_discover", raw);
155
+ if (!input.ok)
156
+ return { services: [], error: errorOf(input.error) };
157
+ let wanted;
158
+ if (input.value.asset !== undefined) {
159
+ const parsed = parseAsset(input.value.asset);
160
+ if (!parsed.ok)
161
+ return { services: [], error: errorOf(parsed.error) };
162
+ wanted = parsed.value;
163
+ }
164
+ const registry = registryOf();
165
+ if (!registry.ok)
166
+ return { services: [], error: errorOf(registry.error) };
167
+ // A filtered request has to over-fetch: the read API pages by registration
168
+ // order and filters nothing, so asking for `limit` rows would return `limit`
169
+ // rows before the filter and fewer after it.
170
+ const filtered = wanted !== undefined || input.value.search !== undefined || input.value.tier !== "any";
171
+ const body = await registry.value.services(filtered ? 200 : input.value.limit);
172
+ if (!body.ok)
173
+ return { services: [], error: errorOf(body.error) };
174
+ const services = await Promise.all(asArray(field(body.value, "services"))
175
+ .map((entry) => toDiscoveredService(entry, directory, chainId, env))
176
+ .filter((service) => matches(service, input.value.tier, input.value.search, wanted))
177
+ .slice(0, input.value.limit)
178
+ .map((service) => withHub(service, directory.get(service.serviceId), options.hubFetch)));
179
+ return { services };
180
+ };
181
+ // ---------------------------------------------------------------- tab_call
182
+ const call = async (raw) => {
183
+ const input = readInput("tab_call", raw);
184
+ if (!input.ok)
185
+ return { ok: false, error: errorOf(input.error) };
186
+ if (settings.agent === undefined)
187
+ return { ok: false, error: errorOf(agentUnconfigured()) };
188
+ const serviceId = input.value.serviceId.toLowerCase();
189
+ const entry = directory.get(serviceId);
190
+ if (entry === undefined) {
191
+ return {
192
+ ok: false,
193
+ error: errorOf(tabError("NOT_FOUND", "SERVICE_ENDPOINT_UNKNOWN", `no endpoint is configured for Service ${serviceId}; the chain records no URL, so add it to the \`services\` list in tab.config`, { details: { serviceId, configured: [...directory.keys()].join(", ") } })),
194
+ };
195
+ }
196
+ const toolKey = toolKeyOf(input.value.tool);
197
+ if (!toolKey.ok)
198
+ return { ok: false, error: errorOf(toolKey.error) };
199
+ const client = createTab402Client({
200
+ baseUrl: entry.endpoint,
201
+ agent: settings.agent,
202
+ // The credit decision is the Service's, and this tool reports it rather than
203
+ // arguing with it. A repeat would meter the same call twice on the path where
204
+ // the first attempt was declined, so the repeat is off.
205
+ maxRetries: 0,
206
+ logger,
207
+ now,
208
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
209
+ ...(settings.strategyId === undefined ? {} : { strategyId: settings.strategyId }),
210
+ // The prepaid fallback, only when tab.config declared a signer factory. The
211
+ // factory is called at the moment a refusal carries an x402 offer and not
212
+ // before, which is what keeps this tool keyless on every other path.
213
+ ...(settings.x402 === undefined ? {} : { x402: { signer: settings.x402, chainId: BigInt(chainId) } }),
214
+ });
215
+ // A Service meters its tools under `/meter/<tool>` off its published
216
+ // endpoint, the shape the reference gateway serves and the Dashboard calls,
217
+ // so the endpoint in the directory is the Service's root and `/hub/<prefix>`
218
+ // sits beside the tools rather than under them.
219
+ const url = `${entry.endpoint.replace(/\/+$/, "")}/meter/${encodeURIComponent(input.value.tool)}`;
220
+ const supplied = await serviceHeaders(entry, {
221
+ method: "POST",
222
+ url,
223
+ tool: input.value.tool,
224
+ agent: settings.agent,
225
+ serviceId,
226
+ });
227
+ if (!supplied.ok)
228
+ return { ok: false, error: errorOf(supplied.error) };
229
+ const response = await client.fetch(url, {
230
+ method: "POST",
231
+ headers: { "content-type": "application/json", ...supplied.value },
232
+ body: JSON.stringify(input.value.arguments ?? {}),
233
+ signal: timeoutSignal(input.value.timeoutMs),
234
+ });
235
+ const charges = client.charges();
236
+ const charge = charges.at(-1);
237
+ const payment = client.payments().at(-1);
238
+ const x402Block = payment === undefined
239
+ ? undefined
240
+ : {
241
+ txHash: payment.txHash,
242
+ network: payment.network,
243
+ amountBaseUnits: payment.amount.toString(10),
244
+ asset: formatAsset(Number(payment.chainId), payment.asset),
245
+ payTo: payment.payTo,
246
+ payer: payment.payer,
247
+ explorerUrl: explorerTx(settings.explorerUrl, payment.txHash),
248
+ };
249
+ const chargeBlock = charge === undefined
250
+ ? undefined
251
+ : {
252
+ amountBaseUnits: charge.amount.toString(10),
253
+ asset: formatAsset(charge.asset.chainId, charge.asset.address),
254
+ tool: charge.tool,
255
+ };
256
+ const tabBlock = charge === undefined
257
+ ? undefined
258
+ : {
259
+ openTabBaseUnits: charge.openTab.toString(10),
260
+ headroomBaseUnits: charge.headroom.toString(10),
261
+ asset: formatAsset(charge.asset.chainId, charge.asset.address),
262
+ settlementDueIso: null,
263
+ };
264
+ if (!response.ok) {
265
+ // A 402 that still stands is the headroom case, and it is the one failure
266
+ // this tool restates rather than passes through: the model needs both
267
+ // figures side by side to decide whether to settle. A standing 402 that
268
+ // an x402 signer tried and failed to pay keeps the payment error instead,
269
+ // because "the facilitator refused the signature" is the actionable fact.
270
+ if (charge !== undefined && charge.outcome === "declined" && response.error.code === "LIMIT_EXCEEDED") {
271
+ return {
272
+ ok: false,
273
+ ...(chargeBlock === undefined ? {} : { charge: chargeBlock }),
274
+ ...(tabBlock === undefined ? {} : { tab: tabBlock }),
275
+ error: {
276
+ // `LIMIT` and not `CONFLICT`: the category maps to HTTP 402 everywhere
277
+ // else in Tab, which is exactly what the Service answered.
278
+ category: "LIMIT",
279
+ code: "LIMIT_EXCEEDED",
280
+ message: `this call needs ${charge.amount.toString(10)} base units and the Agent has ${charge.headroom.toString(10)} of headroom for the Asset; settle with tab_settle and call again`,
281
+ retryable: false,
282
+ requiredBaseUnits: charge.amount.toString(10),
283
+ headroomBaseUnits: charge.headroom.toString(10),
284
+ },
285
+ };
286
+ }
287
+ return {
288
+ ok: false,
289
+ ...(chargeBlock === undefined ? {} : { charge: chargeBlock }),
290
+ ...(tabBlock === undefined ? {} : { tab: tabBlock }),
291
+ error: errorOf(response.error),
292
+ };
293
+ }
294
+ const status = response.value.status;
295
+ const body = await readBody(response.value);
296
+ if (status < 200 || status >= 300) {
297
+ return {
298
+ ok: false,
299
+ ...(chargeBlock === undefined ? {} : { charge: chargeBlock }),
300
+ ...(tabBlock === undefined ? {} : { tab: tabBlock }),
301
+ ...(x402Block === undefined ? {} : { x402: x402Block }),
302
+ error: serviceRefusal(status, body, input.value.tool),
303
+ };
304
+ }
305
+ const result = body;
306
+ return {
307
+ ok: true,
308
+ result,
309
+ // A prepaid call carries the credit refusal it was answered with, so the
310
+ // model sees both what the tab could not cover and what paid for it.
311
+ ...(chargeBlock === undefined ? {} : { charge: chargeBlock }),
312
+ ...(tabBlock === undefined ? {} : { tab: tabBlock }),
313
+ ...(x402Block === undefined ? {} : { x402: x402Block }),
314
+ };
315
+ };
316
+ // -------------------------------------------------------------- tab_status
317
+ const status = async (raw) => {
318
+ const input = readInput("tab_status", raw);
319
+ const agent = (input.ok ? input.value.agent : undefined) ?? settings.agent;
320
+ const subject = agent ?? "0x0000000000000000000000000000000000000000";
321
+ if (!input.ok)
322
+ return { agent: subject, indexedBlock: null, perAsset: [], error: errorOf(input.error) };
323
+ if (agent === undefined) {
324
+ return { agent: subject, indexedBlock: null, perAsset: [], error: errorOf(agentUnconfigured()) };
325
+ }
326
+ let assetFilter;
327
+ if (input.value.asset !== undefined) {
328
+ const parsed = parseAsset(input.value.asset);
329
+ if (!parsed.ok)
330
+ return { agent: agent.toLowerCase(), indexedBlock: null, perAsset: [], error: errorOf(parsed.error) };
331
+ assetFilter = parsed.value.address.toLowerCase();
332
+ }
333
+ const registry = registryOf();
334
+ if (!registry.ok)
335
+ return { agent: agent.toLowerCase(), indexedBlock: null, perAsset: [], error: errorOf(registry.error) };
336
+ const body = await registry.value.agent(agent);
337
+ if (!body.ok)
338
+ return { agent: agent.toLowerCase(), indexedBlock: null, perAsset: [], error: errorOf(body.error) };
339
+ // The block the index had read to when it answered. Stated, because a
340
+ // Settlement the Agent sent a moment ago sits in a later block and an
341
+ // answer that looked current while omitting it would be the misleading kind.
342
+ const horizon = jsonPath(body.value, "index", "lastBlock");
343
+ const indexedBlock = typeof horizon === "number" && Number.isInteger(horizon) && horizon >= 0 ? horizon : null;
344
+ const perAsset = asArray(field(body.value, "assets"))
345
+ .map((entry) => toPerAsset(entry, input.value.historyLimit, chainId))
346
+ .filter((entry) => entry !== null)
347
+ .filter((entry) => assetFilter === undefined || entry.assetAddress === assetFilter)
348
+ .map(({ assetAddress: _assetAddress, ...rest }) => rest);
349
+ let settlements = [];
350
+ if (input.value.historyLimit > 0) {
351
+ const settled = await registry.value.settlements({
352
+ agent,
353
+ ...(assetFilter === undefined ? {} : { asset: assetFilter }),
354
+ limit: input.value.historyLimit,
355
+ });
356
+ if (settled.ok) {
357
+ settlements = asArray(field(settled.value, "settlements")).map((row) => toSettlementRow(row, settings.explorerUrl, chainId));
358
+ }
359
+ else {
360
+ logger.warn("the settlement feed could not be read; status reports credit without history", {
361
+ code: settled.error.code,
362
+ });
363
+ }
364
+ }
365
+ return { agent: agent.toLowerCase(), indexedBlock, perAsset, settlements };
366
+ };
367
+ // -------------------------------------------------------------- tab_settle
368
+ const settle = async (raw) => {
369
+ const input = readInput("tab_settle", raw);
370
+ if (!input.ok)
371
+ return { ok: false, dryRun: false, amountBaseUnits: "0", error: errorOf(input.error) };
372
+ const dryRun = input.value.dryRun;
373
+ const amountBaseUnits = input.value.amountBaseUnits;
374
+ const failure = (error) => ({
375
+ ok: false,
376
+ dryRun,
377
+ amountBaseUnits,
378
+ error: errorOf(error),
379
+ });
380
+ if (settings.agent === undefined)
381
+ return failure(agentUnconfigured());
382
+ const asset = parseAsset(input.value.asset, "asset", env);
383
+ if (!asset.ok)
384
+ return failure(asset.error);
385
+ const amount = BigInt(amountBaseUnits);
386
+ if (amount === 0n) {
387
+ return failure(tabError("VALIDATION", "AMOUNT_ZERO", "a Settlement of zero base units moves nothing and proves nothing"));
388
+ }
389
+ // The strategy is resolved before the Service is read, because it is a local
390
+ // lookup and the Service is a network read: an Agent with no strategy for the
391
+ // Asset should be told that, not made to wait for a read it could not use.
392
+ const strategy = resolveStrategy(await strategiesFor(), input.value.strategyId ?? settings.strategyId, asset.value);
393
+ if (!strategy.ok)
394
+ return failure(strategy.error);
395
+ const serviceId = input.value.serviceId.toLowerCase();
396
+ const accepted = await serviceAcceptsAsset(registryOf(), serviceId, asset.value);
397
+ if (!accepted.ok)
398
+ return failure(accepted.error);
399
+ const request = {
400
+ agent: settings.agent,
401
+ serviceId: serviceId,
402
+ asset: asset.value,
403
+ amount,
404
+ };
405
+ if (dryRun) {
406
+ const quote = await strategy.value.quote(request);
407
+ if (!quote.ok)
408
+ return failure(quote.error);
409
+ return {
410
+ ok: true,
411
+ dryRun: true,
412
+ txHash: null,
413
+ chainId: Number(asset.value.chainId),
414
+ amountBaseUnits,
415
+ settlementId: null,
416
+ appliedBaseUnits: null,
417
+ prepaidBaseUnits: null,
418
+ explorerUrl: null,
419
+ };
420
+ }
421
+ const receipt = await strategy.value.settle(request);
422
+ if (!receipt.ok)
423
+ return failure(receipt.error);
424
+ return {
425
+ ok: true,
426
+ dryRun: false,
427
+ txHash: receipt.value.txHash,
428
+ chainId: Number(receipt.value.chainId),
429
+ amountBaseUnits: receipt.value.amount.toString(10),
430
+ settlementId: receipt.value.settlementId,
431
+ appliedBaseUnits: receipt.value.applied === null ? null : receipt.value.applied.toString(10),
432
+ prepaidBaseUnits: receipt.value.toPrepaid === null ? null : receipt.value.toPrepaid.toString(10),
433
+ explorerUrl: explorerTx(settings.explorerUrl, receipt.value.txHash),
434
+ };
435
+ };
436
+ // -------------------------------------------------------------- dispatch
437
+ const invoke = async (name, input) => {
438
+ switch (name) {
439
+ case "tab_discover":
440
+ return ok(await discover(input));
441
+ case "tab_call":
442
+ return ok(await call(input));
443
+ case "tab_status":
444
+ return ok(await status(input));
445
+ case "tab_settle":
446
+ return ok(await settle(input));
447
+ default:
448
+ return notFoundError("TOOL_UNKNOWN", `\`${name}\` is not a tool this server declares`, {
449
+ details: { tool: name },
450
+ });
451
+ }
452
+ };
453
+ return { discover, call, status, settle, invoke };
454
+ }
455
+ // ---------------------------------------------------------------- mapping
456
+ /**
457
+ * The one setting no tool that touches a tab can proceed without.
458
+ *
459
+ * An Agent address, never a key. Deriving the address from a signing key would
460
+ * mean this process holds one, and the whole point of the split is that
461
+ * discovery, calling and status hold none.
462
+ */
463
+ const agentUnconfigured = () => tabError("VALIDATION", "AGENT_UNCONFIGURED", "no Agent address is configured, so there is no Open Tab to meter against; set `agent` in tab.config, or pass it to this tool where the schema allows it");
464
+ /**
465
+ * Names an Asset the index reports as a bare address.
466
+ *
467
+ * `TabBook` keys an Asset by token address alone, one ledger per address and no
468
+ * chain column, while everything that crosses the MCP boundary is
469
+ * `chainId:address`, because the SDK may be pointed at Mainnet or Testnet and a
470
+ * bare address is ambiguous between the two. The chain id is the one the
471
+ * settings resolved, which is the one the registry this toolset reads indexes.
472
+ */
473
+ const assetStringFor = (address, chainId) => formatAsset(chainId, address.toLowerCase());
474
+ /** A transaction link on the configured explorer, or null for anything that is not a hash. */
475
+ const explorerTx = (explorerUrl, txHash) => /^0x[a-fA-F0-9]{64}$/.test(txHash) ? `${explorerUrl.replace(/\/+$/, "")}/tx/${txHash}` : null;
476
+ function toDiscoveredService(entry, directory, chainId, env) {
477
+ const serviceId = asString(field(entry, "serviceId"), "").toLowerCase();
478
+ const configured = directory.get(serviceId);
479
+ const assets = [];
480
+ for (const accepted of asArray(field(entry, "acceptedAssets"))) {
481
+ const address = asString(field(accepted, "asset"), "").toLowerCase();
482
+ if (!isAddress(address))
483
+ continue;
484
+ const collection = asStringOrNull(field(accepted, "collection"));
485
+ const facts = assetFacts(chainId, address, env);
486
+ assets.push({
487
+ chainId,
488
+ address,
489
+ symbol: facts.symbol,
490
+ decimals: facts.decimals,
491
+ collectionAddress: collection === null ? null : collection.toLowerCase(),
492
+ curatedAsset: facts.curatedAsset,
493
+ });
494
+ }
495
+ const nameAsset = (address) => formatAsset(chainId, address);
496
+ const tools = asArray(field(entry, "prices")).map((price) => {
497
+ const tool = asString(field(price, "tool"), ZERO_WORD).toLowerCase();
498
+ return {
499
+ tool,
500
+ toolName: wordToName(tool),
501
+ asset: nameAsset(asString(field(price, "asset"), "")),
502
+ priceBaseUnits: asDigits(field(price, "baseUnits"), "0"),
503
+ };
504
+ });
505
+ const bonds = asArray(field(entry, "bond")).map((bond) => ({
506
+ asset: nameAsset(asString(field(bond, "asset"), "")),
507
+ stakedBaseUnits: asDigits(field(bond, "staked"), "0"),
508
+ freeBaseUnits: asDigits(field(bond, "free"), "0"),
509
+ }));
510
+ const pending = asArray(field(entry, "pendingChanges"))[0];
511
+ const pendingChange = pending === undefined
512
+ ? null
513
+ : {
514
+ changeId: asString(field(pending, "changeId"), ZERO_WORD).toLowerCase(),
515
+ kind: asStringOrNull(field(pending, "kindName")),
516
+ etaIso: asStringOrNull(field(pending, "etaIso")),
517
+ };
518
+ const tierName = asString(jsonPath(entry, "tier", "name"), "").toLowerCase();
519
+ return {
520
+ serviceId,
521
+ name: configured?.name ?? wordToName(serviceId),
522
+ endpoint: configured?.endpoint ?? null,
523
+ tier: tierName === "curated" ? "curated" : tierName === "permissionless" ? "permissionless" : "unknown",
524
+ settlementWindowSeconds: asNumber(jsonPath(entry, "settlementWindowSeconds", "value"), 0),
525
+ assets,
526
+ tools,
527
+ bonds,
528
+ pendingChange,
529
+ };
530
+ }
531
+ /**
532
+ * Attaches the fronted API Hub provider's endpoints to a discovered Service.
533
+ *
534
+ * A manifest that cannot be read is reported inside the `hub` block rather than
535
+ * failing discovery: the Service is still real and its on-chain tools are still
536
+ * listed, and the Hub's catalogue is an addition the chain does not carry.
537
+ */
538
+ async function withHub(service, configured, hubFetch) {
539
+ const hub = configured?.hub;
540
+ if (hub === undefined)
541
+ return service;
542
+ const prefix = (hub.prefix ?? hub.provider).replace(/^\/+|\/+$/g, "");
543
+ const manifest = await fetchHubManifest({
544
+ provider: hub.provider,
545
+ ...(hub.manifestUrl === undefined ? {} : { manifestUrl: hub.manifestUrl }),
546
+ ...(hubFetch === undefined ? {} : { fetchImpl: hubFetch }),
547
+ });
548
+ if (!manifest.ok) {
549
+ return { ...service, hub: { provider: hub.provider, prefix, endpoints: [], total: 0, error: errorOf(manifest.error) } };
550
+ }
551
+ return {
552
+ ...service,
553
+ hub: {
554
+ provider: manifest.value.provider,
555
+ prefix,
556
+ endpoints: manifest.value.endpoints.map((endpoint) => ({
557
+ endpoint: endpoint.endpoint,
558
+ name: endpoint.name,
559
+ description: endpoint.description,
560
+ priceType: endpoint.priceType,
561
+ priceUsd: endpoint.priceUsd,
562
+ priceBaseUnits: endpoint.priceBaseUnits,
563
+ networks: endpoint.networks,
564
+ })),
565
+ total: manifest.value.total,
566
+ },
567
+ };
568
+ }
569
+ function matches(service, tier, search, asset) {
570
+ if (tier !== "any" && service.tier !== tier)
571
+ return false;
572
+ if (asset !== undefined) {
573
+ const wanted = assetStringOf(asset);
574
+ if (!service.assets.some((entry) => formatAsset(entry.chainId, entry.address) === wanted))
575
+ return false;
576
+ }
577
+ if (search !== undefined) {
578
+ const needle = search.toLowerCase();
579
+ const haystack = `${service.serviceId} ${service.name ?? ""}`.toLowerCase();
580
+ if (!haystack.includes(needle))
581
+ return false;
582
+ }
583
+ return true;
584
+ }
585
+ function toPerAsset(entry, historyLimit, chainId) {
586
+ const address = asString(field(entry, "asset"), "").toLowerCase();
587
+ if (!isAddress(address))
588
+ return null;
589
+ const openTab = asDigitsOrNull(jsonPath(entry, "headroom", "openTab")) ?? asDigits(jsonPath(entry, "openTab", "observed"), "0");
590
+ const tabs = asArray(jsonPath(entry, "openTab", "tabs"))
591
+ .slice(0, historyLimit)
592
+ .map((tab) => {
593
+ // The index keys a tab observation by Agent, Service and Asset, not by the
594
+ // `TabBook` tabId, so the identity is often genuinely absent. Reporting the
595
+ // zero word here would name a tab that does not exist.
596
+ const tabId = asStringOrNull(field(tab, "tabId"));
597
+ return {
598
+ tabId: tabId !== null && /^0x[a-fA-F0-9]{64}$/.test(tabId) ? tabId.toLowerCase() : null,
599
+ serviceId: asString(field(tab, "serviceId"), ZERO_WORD).toLowerCase(),
600
+ openBaseUnits: asDigits(field(tab, "openAfter"), "0"),
601
+ dueIso: null,
602
+ };
603
+ });
604
+ return {
605
+ assetAddress: address,
606
+ asset: assetStringFor(address, chainId),
607
+ creditLimitBaseUnits: asDigitsOrNull(jsonPath(entry, "creditLimit", "value")),
608
+ openTabBaseUnits: openTab,
609
+ prepaidBaseUnits: asDigitsOrNull(jsonPath(entry, "settlements", "prepaidTotal")),
610
+ headroomBaseUnits: asDigitsOrNull(jsonPath(entry, "headroom", "value")),
611
+ delinquent: asBoolean(jsonPath(entry, "delinquency", "delinquent"), false),
612
+ tabs,
613
+ };
614
+ }
615
+ function toSettlementRow(row, explorerUrl, chainId) {
616
+ const txHash = asString(jsonPath(row, "monad", "txHash"), "");
617
+ return {
618
+ settlementId: asString(field(row, "settlementId"), ZERO_WORD).toLowerCase(),
619
+ txHash: txHash.toLowerCase(),
620
+ serviceId: asString(field(row, "serviceId"), ZERO_WORD).toLowerCase(),
621
+ asset: formatAsset(chainId, asString(field(row, "asset"), "")),
622
+ amountBaseUnits: asDigits(field(row, "amount"), "0"),
623
+ appliedBaseUnits: asDigits(field(row, "applied"), "0"),
624
+ prepaidBaseUnits: asDigits(field(row, "toPrepaid"), "0"),
625
+ explorerUrl: explorerTx(explorerUrl, txHash),
626
+ };
627
+ }
628
+ const SERVICE_CATEGORIES = [
629
+ "VALIDATION",
630
+ "AUTHORISATION",
631
+ "NOT_FOUND",
632
+ "LIMIT",
633
+ "CHAIN",
634
+ "UPSTREAM",
635
+ "CONFLICT",
636
+ "UNAVAILABLE",
637
+ "INTERNAL",
638
+ ];
639
+ /**
640
+ * What a Service's refusal means, in this package's vocabulary.
641
+ *
642
+ * A Service that speaks Tab answers `{ error: { category, code, message } }` in
643
+ * the same vocabulary, and that is passed through unchanged: a model told
644
+ * `METERING_SIGNATURE_ABSENT` can act on it, where a flattened
645
+ * "the Service answered 403" leaves it guessing. A Service that answers
646
+ * something else has its status mapped, which is the most that can be said about
647
+ * a body this package cannot read.
648
+ */
649
+ function serviceRefusal(status, body, tool) {
650
+ const declared = field(body, "error");
651
+ if (isRecord(declared)) {
652
+ const category = asString(declared["category"], "");
653
+ if (SERVICE_CATEGORIES.includes(category)) {
654
+ return {
655
+ category: category,
656
+ code: asString(declared["code"], "SERVICE_REFUSED"),
657
+ message: asString(declared["message"], `the Service answered ${status} for ${tool}`),
658
+ retryable: asBoolean(declared["retryable"], status >= 500),
659
+ };
660
+ }
661
+ }
662
+ const category = status === 401 || status === 403
663
+ ? "AUTHORISATION"
664
+ : status === 402
665
+ ? "LIMIT"
666
+ : status === 404
667
+ ? "NOT_FOUND"
668
+ : status === 409
669
+ ? "CONFLICT"
670
+ : status === 408 || status === 429 || status === 503 || status === 504
671
+ ? "UNAVAILABLE"
672
+ : status >= 500
673
+ ? "UPSTREAM"
674
+ : "VALIDATION";
675
+ return {
676
+ category,
677
+ code: "SERVICE_REFUSED",
678
+ message: `the Service answered ${status} for ${tool}`,
679
+ retryable: status >= 500 || status === 408 || status === 429,
680
+ };
681
+ }
682
+ /**
683
+ * The headers one Service requires for one call.
684
+ *
685
+ * A provider is consumer code, so it is called inside a `catch`: a config file
686
+ * that throws becomes a failed tool call carrying the reason, not an unhandled
687
+ * rejection that takes the transport with it.
688
+ */
689
+ async function serviceHeaders(entry, request) {
690
+ const declared = entry.headers;
691
+ if (declared === undefined)
692
+ return ok({});
693
+ if (typeof declared !== "function")
694
+ return ok(declared);
695
+ try {
696
+ return ok(await declared(request));
697
+ }
698
+ catch (error) {
699
+ return fail("UPSTREAM", "SERVICE_HEADERS_FAILED", `the header provider configured for Service ${request.serviceId} failed, so the call was not sent`, {
700
+ details: { serviceId: request.serviceId },
701
+ cause: { code: "PROVIDER_THREW", message: error instanceof Error ? error.message : String(error) },
702
+ });
703
+ }
704
+ }
705
+ /** Reads the body of whatever the Service answered, without assuming a `Response`. */
706
+ async function readBody(response) {
707
+ const candidate = response;
708
+ if (typeof candidate.json === "function") {
709
+ try {
710
+ return await candidate.json();
711
+ }
712
+ catch {
713
+ // fall through to text
714
+ }
715
+ }
716
+ if (typeof candidate.text === "function") {
717
+ try {
718
+ return await candidate.text();
719
+ }
720
+ catch {
721
+ return null;
722
+ }
723
+ }
724
+ return candidate.body ?? null;
725
+ }
726
+ /** An abort signal for one call, described structurally because there are no DOM types here. */
727
+ function timeoutSignal(ms) {
728
+ const ctor = globalThis.AbortSignal;
729
+ return typeof ctor?.timeout === "function" ? ctor.timeout(ms) : undefined;
730
+ }
731
+ /**
732
+ * Checks that the Service accepts the Asset before a transaction is built.
733
+ *
734
+ * `TabSettlement` would refuse it anyway, but a revert costs the Agent gas and
735
+ * says less than this does. The registry read is keyless and one request.
736
+ */
737
+ async function serviceAcceptsAsset(registry, serviceId, asset) {
738
+ if (!registry.ok)
739
+ return registry;
740
+ const body = await registry.value.service(serviceId);
741
+ if (!body.ok)
742
+ return body;
743
+ const wanted = asset.address.toLowerCase();
744
+ for (const accepted of asArray(jsonPath(body.value, "service", "acceptedAssets"))) {
745
+ if (asString(field(accepted, "asset"), "").toLowerCase() === wanted)
746
+ return ok(undefined);
747
+ }
748
+ return notFoundError("ASSET_NOT_ACCEPTED", `Service ${serviceId} does not accept ${assetStringOf(asset)}, so a Settlement in it would be refused`, { details: { serviceId, asset: assetStringOf(asset) } });
749
+ }
750
+ function resolveStrategy(registry, strategyId, asset) {
751
+ const resolved = registry.resolve({
752
+ ...(strategyId === undefined ? {} : { strategyId }),
753
+ asset,
754
+ });
755
+ if (resolved.ok)
756
+ return resolved;
757
+ const cause = resolved.error;
758
+ return fail(cause.category, cause.code, `${cause.message}; tab_settle signs with the Agent's own key through a payment strategy, so one must be registered for ${assetStringOf(asset)} before a Settlement can be built`, { details: { asset: assetStringOf(asset), ...(strategyId === undefined ? {} : { strategyId }) } });
759
+ }
760
+ //# sourceMappingURL=toolset.js.map