@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.
- package/LICENSE +23 -0
- package/README.md +401 -0
- package/bin/tab.mjs +23 -0
- package/dist/_shared/abi.d.ts +150 -0
- package/dist/_shared/abi.d.ts.map +1 -0
- package/dist/_shared/abi.js +197 -0
- package/dist/_shared/abi.js.map +1 -0
- package/dist/_shared/chains.d.ts +118 -0
- package/dist/_shared/chains.d.ts.map +1 -0
- package/dist/_shared/chains.js +89 -0
- package/dist/_shared/chains.js.map +1 -0
- package/dist/_shared/hex.d.ts +35 -0
- package/dist/_shared/hex.d.ts.map +1 -0
- package/dist/_shared/hex.js +40 -0
- package/dist/_shared/hex.js.map +1 -0
- package/dist/_shared/index.d.ts +14 -0
- package/dist/_shared/index.d.ts.map +1 -0
- package/dist/_shared/index.js +14 -0
- package/dist/_shared/index.js.map +1 -0
- package/dist/_shared/keccak256.d.ts +29 -0
- package/dist/_shared/keccak256.d.ts.map +1 -0
- package/dist/_shared/keccak256.js +145 -0
- package/dist/_shared/keccak256.js.map +1 -0
- package/dist/_shared/result.d.ts +78 -0
- package/dist/_shared/result.d.ts.map +1 -0
- package/dist/_shared/result.js +61 -0
- package/dist/_shared/result.js.map +1 -0
- package/dist/cli/client-config.d.ts +155 -0
- package/dist/cli/client-config.d.ts.map +1 -0
- package/dist/cli/client-config.js +382 -0
- package/dist/cli/client-config.js.map +1 -0
- package/dist/cli/connect.d.ts +76 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +158 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/doctor.d.ts +57 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +253 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +45 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +371 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/client-402.d.ts +243 -0
- package/dist/http/client-402.d.ts.map +1 -0
- package/dist/http/client-402.js +515 -0
- package/dist/http/client-402.js.map +1 -0
- package/dist/http/headers.d.ts +173 -0
- package/dist/http/headers.d.ts.map +1 -0
- package/dist/http/headers.js +284 -0
- package/dist/http/headers.js.map +1 -0
- package/dist/http/index.d.ts +15 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +15 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/metering-claim.d.ts +82 -0
- package/dist/http/metering-claim.d.ts.map +1 -0
- package/dist/http/metering-claim.js +99 -0
- package/dist/http/metering-claim.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +40 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/assets.d.ts +31 -0
- package/dist/mcp/assets.d.ts.map +1 -0
- package/dist/mcp/assets.js +78 -0
- package/dist/mcp/assets.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/json-schema.d.ts +86 -0
- package/dist/mcp/json-schema.d.ts.map +1 -0
- package/dist/mcp/json-schema.js +215 -0
- package/dist/mcp/json-schema.js.map +1 -0
- package/dist/mcp/json.d.ts +43 -0
- package/dist/mcp/json.d.ts.map +1 -0
- package/dist/mcp/json.js +69 -0
- package/dist/mcp/json.js.map +1 -0
- package/dist/mcp/registry-client.d.ts +88 -0
- package/dist/mcp/registry-client.d.ts.map +1 -0
- package/dist/mcp/registry-client.js +158 -0
- package/dist/mcp/registry-client.js.map +1 -0
- package/dist/mcp/schemas.d.ts +82 -0
- package/dist/mcp/schemas.d.ts.map +1 -0
- package/dist/mcp/schemas.js +493 -0
- package/dist/mcp/schemas.js.map +1 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +285 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/settings.d.ts +90 -0
- package/dist/mcp/settings.d.ts.map +1 -0
- package/dist/mcp/settings.js +160 -0
- package/dist/mcp/settings.js.map +1 -0
- package/dist/mcp/toolset.d.ts +231 -0
- package/dist/mcp/toolset.d.ts.map +1 -0
- package/dist/mcp/toolset.js +760 -0
- package/dist/mcp/toolset.js.map +1 -0
- package/dist/payments/abi.d.ts +9 -0
- package/dist/payments/abi.d.ts.map +1 -0
- package/dist/payments/abi.js +17 -0
- package/dist/payments/abi.js.map +1 -0
- package/dist/payments/config.d.ts +199 -0
- package/dist/payments/config.d.ts.map +1 -0
- package/dist/payments/config.js +259 -0
- package/dist/payments/config.js.map +1 -0
- package/dist/payments/index.d.ts +13 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +13 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/kuru.d.ts +191 -0
- package/dist/payments/kuru.d.ts.map +1 -0
- package/dist/payments/kuru.js +377 -0
- package/dist/payments/kuru.js.map +1 -0
- package/dist/payments/monad.d.ts +69 -0
- package/dist/payments/monad.d.ts.map +1 -0
- package/dist/payments/monad.js +306 -0
- package/dist/payments/monad.js.map +1 -0
- package/dist/payments/permit2.d.ts +118 -0
- package/dist/payments/permit2.d.ts.map +1 -0
- package/dist/payments/permit2.js +366 -0
- package/dist/payments/permit2.js.map +1 -0
- package/dist/payments/registry.d.ts +119 -0
- package/dist/payments/registry.d.ts.map +1 -0
- package/dist/payments/registry.js +199 -0
- package/dist/payments/registry.js.map +1 -0
- package/dist/payments/strategy.d.ts +80 -0
- package/dist/payments/strategy.d.ts.map +1 -0
- package/dist/payments/strategy.js +103 -0
- package/dist/payments/strategy.js.map +1 -0
- package/dist/proxy/hooks.d.ts +90 -0
- package/dist/proxy/hooks.d.ts.map +1 -0
- package/dist/proxy/hooks.js +35 -0
- package/dist/proxy/hooks.js.map +1 -0
- package/dist/proxy/index.d.ts +9 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +9 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/proxy.d.ts +156 -0
- package/dist/proxy/proxy.d.ts.map +1 -0
- package/dist/proxy/proxy.js +366 -0
- package/dist/proxy/proxy.js.map +1 -0
- package/dist/server/adapters/express.d.ts +89 -0
- package/dist/server/adapters/express.d.ts.map +1 -0
- package/dist/server/adapters/express.js +215 -0
- package/dist/server/adapters/express.js.map +1 -0
- package/dist/server/adapters/hono.d.ts +52 -0
- package/dist/server/adapters/hono.d.ts.map +1 -0
- package/dist/server/adapters/hono.js +61 -0
- package/dist/server/adapters/hono.js.map +1 -0
- package/dist/server/adapters/next.d.ts +52 -0
- package/dist/server/adapters/next.d.ts.map +1 -0
- package/dist/server/adapters/next.js +56 -0
- package/dist/server/adapters/next.js.map +1 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +30 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/metering.d.ts +209 -0
- package/dist/server/metering.d.ts.map +1 -0
- package/dist/server/metering.js +365 -0
- package/dist/server/metering.js.map +1 -0
- package/dist/server/post-paid.d.ts +355 -0
- package/dist/server/post-paid.d.ts.map +1 -0
- package/dist/server/post-paid.js +512 -0
- package/dist/server/post-paid.js.map +1 -0
- package/dist/x402/client.d.ts +203 -0
- package/dist/x402/client.d.ts.map +1 -0
- package/dist/x402/client.js +337 -0
- package/dist/x402/client.js.map +1 -0
- package/dist/x402/hub.d.ts +79 -0
- package/dist/x402/hub.d.ts.map +1 -0
- package/dist/x402/hub.js +164 -0
- package/dist/x402/hub.js.map +1 -0
- package/dist/x402/index.d.ts +27 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +27 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/proxy.d.ts +162 -0
- package/dist/x402/proxy.d.ts.map +1 -0
- package/dist/x402/proxy.js +198 -0
- package/dist/x402/proxy.js.map +1 -0
- package/dist/x402/server.d.ts +162 -0
- package/dist/x402/server.d.ts.map +1 -0
- package/dist/x402/server.js +306 -0
- package/dist/x402/server.js.map +1 -0
- package/dist/x402/wire.d.ts +104 -0
- package/dist/x402/wire.d.ts.map +1 -0
- package/dist/x402/wire.js +265 -0
- package/dist/x402/wire.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema subset the MCP tools declare, and a validator for it.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is written here rather than pulled in
|
|
5
|
+
*
|
|
6
|
+
* An MCP tool declares its input and output shape as JSON Schema, and a model
|
|
7
|
+
* reads that declaration to decide what to send. A declaration nothing enforces
|
|
8
|
+
* is a promise, not a contract: the first tool that accepts an out-of-range
|
|
9
|
+
* `limit` or emits an amount as a `number` has broken the schema its caller was
|
|
10
|
+
* reasoning against, and nothing in the process notices. So the four tools in
|
|
11
|
+
* this package validate every input against the schema they publish, and the
|
|
12
|
+
* test suite validates every output against the schema they publish (task 16.3).
|
|
13
|
+
*
|
|
14
|
+
* The validator is 200 lines rather than a dependency because the schemas here
|
|
15
|
+
* use ten keywords between them, this package adds no dependency for the MCP
|
|
16
|
+
* surface, and a validator whose supported keyword set is visible in one file
|
|
17
|
+
* cannot silently ignore a keyword a schema relies on. {@link validateJsonValue}
|
|
18
|
+
* fails loudly on a keyword it does not implement instead of passing the value,
|
|
19
|
+
* which is the property a hand-written validator has to have to be trustworthy.
|
|
20
|
+
*
|
|
21
|
+
* ## Everything returns a Result
|
|
22
|
+
*
|
|
23
|
+
* Nothing here throws, including on a malformed schema. A schema is authored in
|
|
24
|
+
* this package, so a bad one is a bug rather than input, but a bug that surfaces
|
|
25
|
+
* as an `INTERNAL` `Result` at a tool boundary is reported to the model as a
|
|
26
|
+
* failed call, and a bug that surfaces as a thrown value takes the stdio
|
|
27
|
+
* transport down with it.
|
|
28
|
+
*
|
|
29
|
+
* Requirements: 21.5, 25.1, 25.2
|
|
30
|
+
*/
|
|
31
|
+
import { ok } from "../_shared/index.js";
|
|
32
|
+
import { fail, validationError } from "../errors.js";
|
|
33
|
+
/** Every keyword {@link validateJsonValue} understands. Anything else is a failure, not a pass. */
|
|
34
|
+
export const SUPPORTED_KEYWORDS = [
|
|
35
|
+
"type",
|
|
36
|
+
"title",
|
|
37
|
+
"description",
|
|
38
|
+
"properties",
|
|
39
|
+
"required",
|
|
40
|
+
"additionalProperties",
|
|
41
|
+
"items",
|
|
42
|
+
"enum",
|
|
43
|
+
"const",
|
|
44
|
+
"pattern",
|
|
45
|
+
"minLength",
|
|
46
|
+
"maxLength",
|
|
47
|
+
"minimum",
|
|
48
|
+
"maximum",
|
|
49
|
+
"minItems",
|
|
50
|
+
"maxItems",
|
|
51
|
+
"default",
|
|
52
|
+
"examples",
|
|
53
|
+
];
|
|
54
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
const typeNameOf = (value) => {
|
|
56
|
+
if (value === null)
|
|
57
|
+
return "null";
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return "array";
|
|
60
|
+
if (typeof value === "boolean")
|
|
61
|
+
return "boolean";
|
|
62
|
+
if (typeof value === "number")
|
|
63
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return "string";
|
|
66
|
+
return "object";
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Does a value satisfy one declared type name?
|
|
70
|
+
*
|
|
71
|
+
* `integer` accepts only a safe integer, and `number` accepts an integer too,
|
|
72
|
+
* which is JSON Schema's own rule. A `bigint` matches nothing: an amount in
|
|
73
|
+
* Asset base units crosses this boundary as a decimal string and never as a
|
|
74
|
+
* JavaScript number, so a `bigint` that reached a tool payload is a bug in the
|
|
75
|
+
* mapping and is reported rather than coerced.
|
|
76
|
+
*/
|
|
77
|
+
const matchesType = (value, declared) => {
|
|
78
|
+
if (typeof value === "bigint")
|
|
79
|
+
return false;
|
|
80
|
+
const actual = typeNameOf(value);
|
|
81
|
+
if (declared === "number")
|
|
82
|
+
return actual === "number" || actual === "integer";
|
|
83
|
+
if (declared === "integer")
|
|
84
|
+
return actual === "integer" && Number.isSafeInteger(value);
|
|
85
|
+
return actual === declared;
|
|
86
|
+
};
|
|
87
|
+
const declaredTypes = (schema) => {
|
|
88
|
+
if (schema.type === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
return typeof schema.type === "string" ? [schema.type] : schema.type;
|
|
91
|
+
};
|
|
92
|
+
/** A dotted path into the value, so a problem names the field a caller has to fix. */
|
|
93
|
+
const child = (path, segment) => (path === "" ? segment : `${path}.${segment}`);
|
|
94
|
+
/** Every unsupported keyword found anywhere in a schema, deepest last. */
|
|
95
|
+
function unsupportedKeywords(schema, path, found) {
|
|
96
|
+
for (const keyword of Object.keys(schema)) {
|
|
97
|
+
if (!SUPPORTED_KEYWORDS.includes(keyword)) {
|
|
98
|
+
found.push(`${path === "" ? "<root>" : path}: ${keyword}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (schema.properties !== undefined) {
|
|
102
|
+
for (const [name, property] of Object.entries(schema.properties)) {
|
|
103
|
+
unsupportedKeywords(property, child(path, name), found);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (schema.items !== undefined)
|
|
107
|
+
unsupportedKeywords(schema.items, `${path}[]`, found);
|
|
108
|
+
}
|
|
109
|
+
function collect(schema, value, path, problems) {
|
|
110
|
+
const where = path === "" ? "value" : path;
|
|
111
|
+
const types = declaredTypes(schema);
|
|
112
|
+
if (types !== undefined && !types.some((declared) => matchesType(value, declared))) {
|
|
113
|
+
problems.push(`${where}: expected ${types.join(" or ")}, received ${typeNameOf(value)}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
117
|
+
problems.push(`${where}: must equal ${JSON.stringify(schema.const)}`);
|
|
118
|
+
}
|
|
119
|
+
if (schema.enum !== undefined && !schema.enum.includes(value)) {
|
|
120
|
+
problems.push(`${where}: must be one of ${schema.enum.map((v) => JSON.stringify(v)).join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === "string") {
|
|
123
|
+
if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
|
|
124
|
+
problems.push(`${where}: must match ${schema.pattern}`);
|
|
125
|
+
}
|
|
126
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
127
|
+
problems.push(`${where}: must be at least ${schema.minLength} characters`);
|
|
128
|
+
}
|
|
129
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
130
|
+
problems.push(`${where}: must be at most ${schema.maxLength} characters`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === "number") {
|
|
134
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
135
|
+
problems.push(`${where}: must be at least ${schema.minimum}`);
|
|
136
|
+
}
|
|
137
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
138
|
+
problems.push(`${where}: must be at most ${schema.maximum}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
143
|
+
problems.push(`${where}: must hold at least ${schema.minItems} items`);
|
|
144
|
+
}
|
|
145
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
146
|
+
problems.push(`${where}: must hold at most ${schema.maxItems} items`);
|
|
147
|
+
}
|
|
148
|
+
if (schema.items !== undefined) {
|
|
149
|
+
value.forEach((entry, index) => collect(schema.items, entry, `${where}[${index}]`, problems));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (isPlainObject(value)) {
|
|
153
|
+
for (const name of schema.required ?? []) {
|
|
154
|
+
if (value[name] === undefined)
|
|
155
|
+
problems.push(`${child(where, name)}: required`);
|
|
156
|
+
}
|
|
157
|
+
const properties = schema.properties;
|
|
158
|
+
if (properties !== undefined) {
|
|
159
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
160
|
+
// An absent optional property is absent, not null. `exactOptionalPropertyTypes`
|
|
161
|
+
// holds the same line in the types, so the two agree.
|
|
162
|
+
if (value[name] !== undefined)
|
|
163
|
+
collect(property, value[name], child(where, name), problems);
|
|
164
|
+
}
|
|
165
|
+
if (schema.additionalProperties === false) {
|
|
166
|
+
for (const name of Object.keys(value)) {
|
|
167
|
+
if (!(name in properties))
|
|
168
|
+
problems.push(`${child(where, name)}: not a declared property`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Validates a value against a schema and returns it unchanged when it conforms.
|
|
176
|
+
*
|
|
177
|
+
* `code` names the failure so a caller can tell an input rejection from an
|
|
178
|
+
* output-shape bug without parsing the message.
|
|
179
|
+
*/
|
|
180
|
+
export function validateJsonValue(schema, value, label, code = "SCHEMA_MISMATCH") {
|
|
181
|
+
const unsupported = [];
|
|
182
|
+
unsupportedKeywords(schema, "", unsupported);
|
|
183
|
+
if (unsupported.length > 0) {
|
|
184
|
+
return fail("INTERNAL", "SCHEMA_KEYWORD_UNSUPPORTED", `the ${label} schema uses keywords this validator does not implement, so nothing was checked: ${unsupported.join("; ")}`, { details: { label, unsupported: unsupported.join("; ") } });
|
|
185
|
+
}
|
|
186
|
+
const problems = [];
|
|
187
|
+
collect(schema, value, "", problems);
|
|
188
|
+
if (problems.length > 0) {
|
|
189
|
+
return validationError(code, `${label} does not match its declared schema: ${problems.join("; ")}`, {
|
|
190
|
+
details: { label, problems: problems.join("; ") },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return ok(value);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Fills in the declared top-level defaults of an object schema.
|
|
197
|
+
*
|
|
198
|
+
* Only the top level, because that is where every default in this package's
|
|
199
|
+
* schemas sits and a defaulting pass that reached into arrays would be inventing
|
|
200
|
+
* entries a caller did not send. A non-object value is handed back untouched for
|
|
201
|
+
* {@link validateJsonValue} to reject with a type problem, which is a better
|
|
202
|
+
* message than one about defaults.
|
|
203
|
+
*/
|
|
204
|
+
export function applyJsonDefaults(schema, value) {
|
|
205
|
+
const properties = schema.properties;
|
|
206
|
+
if (properties === undefined || !isPlainObject(value))
|
|
207
|
+
return value;
|
|
208
|
+
const filled = { ...value };
|
|
209
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
210
|
+
if (filled[name] === undefined && property.default !== undefined)
|
|
211
|
+
filled[name] = property.default;
|
|
212
|
+
}
|
|
213
|
+
return filled;
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.js","sourceRoot":"","sources":["../../src/mcp/json-schema.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The JSON Schema subset the MCP tools declare, and a validator for it.\n *\n * ## Why this is written here rather than pulled in\n *\n * An MCP tool declares its input and output shape as JSON Schema, and a model\n * reads that declaration to decide what to send. A declaration nothing enforces\n * is a promise, not a contract: the first tool that accepts an out-of-range\n * `limit` or emits an amount as a `number` has broken the schema its caller was\n * reasoning against, and nothing in the process notices. So the four tools in\n * this package validate every input against the schema they publish, and the\n * test suite validates every output against the schema they publish (task 16.3).\n *\n * The validator is 200 lines rather than a dependency because the schemas here\n * use ten keywords between them, this package adds no dependency for the MCP\n * surface, and a validator whose supported keyword set is visible in one file\n * cannot silently ignore a keyword a schema relies on. {@link validateJsonValue}\n * fails loudly on a keyword it does not implement instead of passing the value,\n * which is the property a hand-written validator has to have to be trustworthy.\n *\n * ## Everything returns a Result\n *\n * Nothing here throws, including on a malformed schema. A schema is authored in\n * this package, so a bad one is a bug rather than input, but a bug that surfaces\n * as an `INTERNAL` `Result` at a tool boundary is reported to the model as a\n * failed call, and a bug that surfaces as a thrown value takes the stdio\n * transport down with it.\n *\n * Requirements: 21.5, 25.1, 25.2\n */\n\nimport type { Result } from \"../_shared/index.js\";\nimport { ok } from \"../_shared/index.js\";\n\nimport { fail, validationError } from \"../errors.js\";\n\n/** The seven JSON Schema primitive type names. */\nexport type JsonSchemaType =\n | \"object\"\n | \"array\"\n | \"string\"\n | \"number\"\n | \"integer\"\n | \"boolean\"\n | \"null\";\n\n/** Every keyword {@link validateJsonValue} understands. Anything else is a failure, not a pass. */\nexport const SUPPORTED_KEYWORDS = [\n \"type\",\n \"title\",\n \"description\",\n \"properties\",\n \"required\",\n \"additionalProperties\",\n \"items\",\n \"enum\",\n \"const\",\n \"pattern\",\n \"minLength\",\n \"maxLength\",\n \"minimum\",\n \"maximum\",\n \"minItems\",\n \"maxItems\",\n \"default\",\n \"examples\",\n] as const;\n\n/**\n * One node of the schema subset.\n *\n * `type` may be an array, which is how a nullable field is declared here:\n * `{ type: [\"string\", \"null\"] }`. There is no `nullable` keyword, because that\n * one is OpenAPI's rather than JSON Schema's and a model reading the tool\n * declaration would be reading a keyword that does not mean what it says.\n */\nexport interface JsonSchema {\n readonly type?: JsonSchemaType | readonly JsonSchemaType[];\n readonly title?: string;\n readonly description?: string;\n readonly properties?: Readonly<Record<string, JsonSchema>>;\n readonly required?: readonly string[];\n readonly additionalProperties?: boolean;\n readonly items?: JsonSchema;\n readonly enum?: readonly (string | number | boolean | null)[];\n readonly const?: string | number | boolean | null;\n readonly pattern?: string;\n readonly minLength?: number;\n readonly maxLength?: number;\n readonly minimum?: number;\n readonly maximum?: number;\n readonly minItems?: number;\n readonly maxItems?: number;\n readonly default?: unknown;\n readonly examples?: readonly unknown[];\n}\n\n/** An object-typed schema, which is what every tool input and output is. */\nexport interface JsonObjectSchema extends JsonSchema {\n readonly type: \"object\";\n readonly properties: Readonly<Record<string, JsonSchema>>;\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst typeNameOf = (value: unknown): JsonSchemaType => {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"number\") return Number.isInteger(value) ? \"integer\" : \"number\";\n if (typeof value === \"string\") return \"string\";\n return \"object\";\n};\n\n/**\n * Does a value satisfy one declared type name?\n *\n * `integer` accepts only a safe integer, and `number` accepts an integer too,\n * which is JSON Schema's own rule. A `bigint` matches nothing: an amount in\n * Asset base units crosses this boundary as a decimal string and never as a\n * JavaScript number, so a `bigint` that reached a tool payload is a bug in the\n * mapping and is reported rather than coerced.\n */\nconst matchesType = (value: unknown, declared: JsonSchemaType): boolean => {\n if (typeof value === \"bigint\") return false;\n const actual = typeNameOf(value);\n if (declared === \"number\") return actual === \"number\" || actual === \"integer\";\n if (declared === \"integer\") return actual === \"integer\" && Number.isSafeInteger(value);\n return actual === declared;\n};\n\nconst declaredTypes = (schema: JsonSchema): readonly JsonSchemaType[] | undefined => {\n if (schema.type === undefined) return undefined;\n return typeof schema.type === \"string\" ? [schema.type] : schema.type;\n};\n\n/** A dotted path into the value, so a problem names the field a caller has to fix. */\nconst child = (path: string, segment: string): string => (path === \"\" ? segment : `${path}.${segment}`);\n\n/** Every unsupported keyword found anywhere in a schema, deepest last. */\nfunction unsupportedKeywords(schema: JsonSchema, path: string, found: string[]): void {\n for (const keyword of Object.keys(schema)) {\n if (!(SUPPORTED_KEYWORDS as readonly string[]).includes(keyword)) {\n found.push(`${path === \"\" ? \"<root>\" : path}: ${keyword}`);\n }\n }\n if (schema.properties !== undefined) {\n for (const [name, property] of Object.entries(schema.properties)) {\n unsupportedKeywords(property, child(path, name), found);\n }\n }\n if (schema.items !== undefined) unsupportedKeywords(schema.items, `${path}[]`, found);\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, problems: string[]): void {\n const where = path === \"\" ? \"value\" : path;\n\n const types = declaredTypes(schema);\n if (types !== undefined && !types.some((declared) => matchesType(value, declared))) {\n problems.push(`${where}: expected ${types.join(\" or \")}, received ${typeNameOf(value)}`);\n return;\n }\n\n if (schema.const !== undefined && value !== schema.const) {\n problems.push(`${where}: must equal ${JSON.stringify(schema.const)}`);\n }\n if (schema.enum !== undefined && !schema.enum.includes(value as string)) {\n problems.push(`${where}: must be one of ${schema.enum.map((v) => JSON.stringify(v)).join(\", \")}`);\n }\n\n if (typeof value === \"string\") {\n if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {\n problems.push(`${where}: must match ${schema.pattern}`);\n }\n if (schema.minLength !== undefined && value.length < schema.minLength) {\n problems.push(`${where}: must be at least ${schema.minLength} characters`);\n }\n if (schema.maxLength !== undefined && value.length > schema.maxLength) {\n problems.push(`${where}: must be at most ${schema.maxLength} characters`);\n }\n }\n\n if (typeof value === \"number\") {\n if (schema.minimum !== undefined && value < schema.minimum) {\n problems.push(`${where}: must be at least ${schema.minimum}`);\n }\n if (schema.maximum !== undefined && value > schema.maximum) {\n problems.push(`${where}: must be at most ${schema.maximum}`);\n }\n }\n\n if (Array.isArray(value)) {\n if (schema.minItems !== undefined && value.length < schema.minItems) {\n problems.push(`${where}: must hold at least ${schema.minItems} items`);\n }\n if (schema.maxItems !== undefined && value.length > schema.maxItems) {\n problems.push(`${where}: must hold at most ${schema.maxItems} items`);\n }\n if (schema.items !== undefined) {\n value.forEach((entry, index) => collect(schema.items!, entry, `${where}[${index}]`, problems));\n }\n }\n\n if (isPlainObject(value)) {\n for (const name of schema.required ?? []) {\n if (value[name] === undefined) problems.push(`${child(where, name)}: required`);\n }\n const properties = schema.properties;\n if (properties !== undefined) {\n for (const [name, property] of Object.entries(properties)) {\n // An absent optional property is absent, not null. `exactOptionalPropertyTypes`\n // holds the same line in the types, so the two agree.\n if (value[name] !== undefined) collect(property, value[name], child(where, name), problems);\n }\n if (schema.additionalProperties === false) {\n for (const name of Object.keys(value)) {\n if (!(name in properties)) problems.push(`${child(where, name)}: not a declared property`);\n }\n }\n }\n }\n}\n\n/**\n * Validates a value against a schema and returns it unchanged when it conforms.\n *\n * `code` names the failure so a caller can tell an input rejection from an\n * output-shape bug without parsing the message.\n */\nexport function validateJsonValue<T>(\n schema: JsonSchema,\n value: unknown,\n label: string,\n code = \"SCHEMA_MISMATCH\",\n): Result<T> {\n const unsupported: string[] = [];\n unsupportedKeywords(schema, \"\", unsupported);\n if (unsupported.length > 0) {\n return fail(\n \"INTERNAL\",\n \"SCHEMA_KEYWORD_UNSUPPORTED\",\n `the ${label} schema uses keywords this validator does not implement, so nothing was checked: ${unsupported.join(\"; \")}`,\n { details: { label, unsupported: unsupported.join(\"; \") } },\n );\n }\n\n const problems: string[] = [];\n collect(schema, value, \"\", problems);\n if (problems.length > 0) {\n return validationError(code, `${label} does not match its declared schema: ${problems.join(\"; \")}`, {\n details: { label, problems: problems.join(\"; \") },\n });\n }\n return ok(value as T);\n}\n\n/**\n * Fills in the declared top-level defaults of an object schema.\n *\n * Only the top level, because that is where every default in this package's\n * schemas sits and a defaulting pass that reached into arrays would be inventing\n * entries a caller did not send. A non-object value is handed back untouched for\n * {@link validateJsonValue} to reject with a type problem, which is a better\n * message than one about defaults.\n */\nexport function applyJsonDefaults(schema: JsonSchema, value: unknown): unknown {\n const properties = schema.properties;\n if (properties === undefined || !isPlainObject(value)) return value;\n const filled: Record<string, unknown> = { ...value };\n for (const [name, property] of Object.entries(properties)) {\n if (filled[name] === undefined && property.default !== undefined) filled[name] = property.default;\n }\n return filled;\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive readers for JSON this package did not produce.
|
|
3
|
+
*
|
|
4
|
+
* The registry read API is a separate process on a separate release cadence, and
|
|
5
|
+
* a Service endpoint is a third party's code. Neither is a compile-time
|
|
6
|
+
* dependency of this package and neither should be: an interface mirrored here
|
|
7
|
+
* would be a copy that goes stale silently, and the failure it produces is a
|
|
8
|
+
* `TypeError` deep inside a tool handler rather than a message naming the field.
|
|
9
|
+
*
|
|
10
|
+
* So every field is read through one of these, each of which answers a value or
|
|
11
|
+
* a stated fallback and never throws. A field that went missing upstream becomes
|
|
12
|
+
* a null in the tool's output, which the schema declares as possible, instead of
|
|
13
|
+
* taking the call down.
|
|
14
|
+
*
|
|
15
|
+
* Requirements: 25.1, 25.2
|
|
16
|
+
*/
|
|
17
|
+
/** A JSON object, as far as anything here is concerned. */
|
|
18
|
+
export type JsonRecord = Readonly<Record<string, unknown>>;
|
|
19
|
+
export declare const isRecord: (value: unknown) => value is JsonRecord;
|
|
20
|
+
/** The value at `key`, when the container is an object. */
|
|
21
|
+
export declare const field: (value: unknown, key: string) => unknown;
|
|
22
|
+
/** The value at a dotted path, stopping at the first non-object. */
|
|
23
|
+
export declare const path: (value: unknown, ...keys: readonly string[]) => unknown;
|
|
24
|
+
export declare const asRecord: (value: unknown) => JsonRecord;
|
|
25
|
+
export declare const asArray: (value: unknown) => readonly unknown[];
|
|
26
|
+
export declare const asString: (value: unknown, fallback: string) => string;
|
|
27
|
+
export declare const asStringOrNull: (value: unknown) => string | null;
|
|
28
|
+
/** A finite number, or the fallback. `NaN` and an infinity are not numbers a schema accepts. */
|
|
29
|
+
export declare const asNumber: (value: unknown, fallback: number) => number;
|
|
30
|
+
export declare const asBoolean: (value: unknown, fallback: boolean) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* A `uint256` as the decimal string every amount crosses this boundary as.
|
|
33
|
+
*
|
|
34
|
+
* A JavaScript number is accepted only when it is a safe integer, because a
|
|
35
|
+
* larger one has already lost digits by the time it arrives and stringifying it
|
|
36
|
+
* would launder a rounded amount into something that looks exact.
|
|
37
|
+
*/
|
|
38
|
+
export declare const asDigits: (value: unknown, fallback: string) => string;
|
|
39
|
+
/** The same, but absence stays absence. */
|
|
40
|
+
export declare const asDigitsOrNull: (value: unknown) => string | null;
|
|
41
|
+
/** Seconds since the epoch, as an ISO instant. Null for anything unreadable. */
|
|
42
|
+
export declare const secondsToIso: (value: unknown) => string | null;
|
|
43
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/mcp/json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3D,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UACmB,CAAC;AAEvE,2DAA2D;AAC3D,eAAO,MAAM,KAAK,GAAI,OAAO,OAAO,EAAE,KAAK,MAAM,KAAG,OAAqD,CAAC;AAE1G,oEAAoE;AACpE,eAAO,MAAM,IAAI,GAAI,OAAO,OAAO,EAAE,GAAG,MAAM,SAAS,MAAM,EAAE,KAAG,OACE,CAAC;AAErE,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,UAA4C,CAAC;AAEvF,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,KAAG,SAAS,OAAO,EAAyC,CAAC;AAEnG,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,EAAE,UAAU,MAAM,KAAG,MACd,CAAC;AAE/C,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAAkD,CAAC;AAE5G,gGAAgG;AAChG,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,EAAE,UAAU,MAAM,KAAG,MAO3D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,EAAE,UAAU,OAAO,KAAG,OACf,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,EAAE,UAAU,MAAM,KAAG,MAK3D,CAAC;AAEF,2CAA2C;AAC3C,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAGxD,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAMtD,CAAC"}
|
package/dist/mcp/json.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive readers for JSON this package did not produce.
|
|
3
|
+
*
|
|
4
|
+
* The registry read API is a separate process on a separate release cadence, and
|
|
5
|
+
* a Service endpoint is a third party's code. Neither is a compile-time
|
|
6
|
+
* dependency of this package and neither should be: an interface mirrored here
|
|
7
|
+
* would be a copy that goes stale silently, and the failure it produces is a
|
|
8
|
+
* `TypeError` deep inside a tool handler rather than a message naming the field.
|
|
9
|
+
*
|
|
10
|
+
* So every field is read through one of these, each of which answers a value or
|
|
11
|
+
* a stated fallback and never throws. A field that went missing upstream becomes
|
|
12
|
+
* a null in the tool's output, which the schema declares as possible, instead of
|
|
13
|
+
* taking the call down.
|
|
14
|
+
*
|
|
15
|
+
* Requirements: 25.1, 25.2
|
|
16
|
+
*/
|
|
17
|
+
export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
/** The value at `key`, when the container is an object. */
|
|
19
|
+
export const field = (value, key) => (isRecord(value) ? value[key] : undefined);
|
|
20
|
+
/** The value at a dotted path, stopping at the first non-object. */
|
|
21
|
+
export const path = (value, ...keys) => keys.reduce((current, key) => field(current, key), value);
|
|
22
|
+
export const asRecord = (value) => (isRecord(value) ? value : {});
|
|
23
|
+
export const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
24
|
+
export const asString = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
25
|
+
export const asStringOrNull = (value) => (typeof value === "string" ? value : null);
|
|
26
|
+
/** A finite number, or the fallback. `NaN` and an infinity are not numbers a schema accepts. */
|
|
27
|
+
export const asNumber = (value, fallback) => {
|
|
28
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
29
|
+
return value;
|
|
30
|
+
if (typeof value === "string" && /^-?[0-9]+$/.test(value)) {
|
|
31
|
+
const parsed = Number(value);
|
|
32
|
+
if (Number.isSafeInteger(parsed))
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
return fallback;
|
|
36
|
+
};
|
|
37
|
+
export const asBoolean = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
38
|
+
/**
|
|
39
|
+
* A `uint256` as the decimal string every amount crosses this boundary as.
|
|
40
|
+
*
|
|
41
|
+
* A JavaScript number is accepted only when it is a safe integer, because a
|
|
42
|
+
* larger one has already lost digits by the time it arrives and stringifying it
|
|
43
|
+
* would launder a rounded amount into something that looks exact.
|
|
44
|
+
*/
|
|
45
|
+
export const asDigits = (value, fallback) => {
|
|
46
|
+
if (typeof value === "string" && /^[0-9]{1,39}$/.test(value))
|
|
47
|
+
return value;
|
|
48
|
+
if (typeof value === "bigint" && value >= 0n)
|
|
49
|
+
return value.toString();
|
|
50
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
51
|
+
return String(value);
|
|
52
|
+
return fallback;
|
|
53
|
+
};
|
|
54
|
+
/** The same, but absence stays absence. */
|
|
55
|
+
export const asDigitsOrNull = (value) => {
|
|
56
|
+
const digits = asDigits(value, "");
|
|
57
|
+
return digits === "" ? null : digits;
|
|
58
|
+
};
|
|
59
|
+
/** Seconds since the epoch, as an ISO instant. Null for anything unreadable. */
|
|
60
|
+
export const secondsToIso = (value) => {
|
|
61
|
+
const seconds = asDigits(value, "");
|
|
62
|
+
if (seconds === "")
|
|
63
|
+
return null;
|
|
64
|
+
const millis = Number(seconds) * 1000;
|
|
65
|
+
if (!Number.isFinite(millis) || millis <= 0)
|
|
66
|
+
return null;
|
|
67
|
+
return new Date(millis).toISOString();
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/mcp/json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Defensive readers for JSON this package did not produce.\n *\n * The registry read API is a separate process on a separate release cadence, and\n * a Service endpoint is a third party's code. Neither is a compile-time\n * dependency of this package and neither should be: an interface mirrored here\n * would be a copy that goes stale silently, and the failure it produces is a\n * `TypeError` deep inside a tool handler rather than a message naming the field.\n *\n * So every field is read through one of these, each of which answers a value or\n * a stated fallback and never throws. A field that went missing upstream becomes\n * a null in the tool's output, which the schema declares as possible, instead of\n * taking the call down.\n *\n * Requirements: 25.1, 25.2\n */\n\n/** A JSON object, as far as anything here is concerned. */\nexport type JsonRecord = Readonly<Record<string, unknown>>;\n\nexport const isRecord = (value: unknown): value is JsonRecord =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** The value at `key`, when the container is an object. */\nexport const field = (value: unknown, key: string): unknown => (isRecord(value) ? value[key] : undefined);\n\n/** The value at a dotted path, stopping at the first non-object. */\nexport const path = (value: unknown, ...keys: readonly string[]): unknown =>\n keys.reduce<unknown>((current, key) => field(current, key), value);\n\nexport const asRecord = (value: unknown): JsonRecord => (isRecord(value) ? value : {});\n\nexport const asArray = (value: unknown): readonly unknown[] => (Array.isArray(value) ? value : []);\n\nexport const asString = (value: unknown, fallback: string): string =>\n typeof value === \"string\" ? value : fallback;\n\nexport const asStringOrNull = (value: unknown): string | null => (typeof value === \"string\" ? value : null);\n\n/** A finite number, or the fallback. `NaN` and an infinity are not numbers a schema accepts. */\nexport const asNumber = (value: unknown, fallback: number): number => {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\" && /^-?[0-9]+$/.test(value)) {\n const parsed = Number(value);\n if (Number.isSafeInteger(parsed)) return parsed;\n }\n return fallback;\n};\n\nexport const asBoolean = (value: unknown, fallback: boolean): boolean =>\n typeof value === \"boolean\" ? value : fallback;\n\n/**\n * A `uint256` as the decimal string every amount crosses this boundary as.\n *\n * A JavaScript number is accepted only when it is a safe integer, because a\n * larger one has already lost digits by the time it arrives and stringifying it\n * would launder a rounded amount into something that looks exact.\n */\nexport const asDigits = (value: unknown, fallback: string): string => {\n if (typeof value === \"string\" && /^[0-9]{1,39}$/.test(value)) return value;\n if (typeof value === \"bigint\" && value >= 0n) return value.toString();\n if (typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0) return String(value);\n return fallback;\n};\n\n/** The same, but absence stays absence. */\nexport const asDigitsOrNull = (value: unknown): string | null => {\n const digits = asDigits(value, \"\");\n return digits === \"\" ? null : digits;\n};\n\n/** Seconds since the epoch, as an ISO instant. Null for anything unreadable. */\nexport const secondsToIso = (value: unknown): string | null => {\n const seconds = asDigits(value, \"\");\n if (seconds === \"\") return null;\n const millis = Number(seconds) * 1000;\n if (!Number.isFinite(millis) || millis <= 0) return null;\n return new Date(millis).toISOString();\n};\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyless read client for the Tab registry read API.
|
|
3
|
+
*
|
|
4
|
+
* `tab_discover` and `tab_status` answer questions about chain state -- which
|
|
5
|
+
* Services are registered, what each tool costs, what an Agent owes -- and every
|
|
6
|
+
* one of those answers is a public fact. Nothing here signs, nothing here
|
|
7
|
+
* authenticates, and nothing here writes. That is why `tab connect` can wire a
|
|
8
|
+
* client up without ever touching a key, and why `tab doctor` can check a
|
|
9
|
+
* deployment end to end from a laptop that holds none.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the read API rather than the chain
|
|
12
|
+
*
|
|
13
|
+
* A Credit Limit is not a storage slot. It is `LimitLib` recomputed over an
|
|
14
|
+
* Agent's committed Settlement history and the Bond ledger, cross-checked
|
|
15
|
+
* against `TabBook.creditLimit` at the same block before it is served. A Bond
|
|
16
|
+
* figure is a sum of `Bond`'s own events, checked against `Bond.ledgerOf`. An
|
|
17
|
+
* MCP server that read the chain directly would have to restate both, and a
|
|
18
|
+
* second restatement that drifts from the first is worse than one source of
|
|
19
|
+
* truth: it would put two different Credit Limits in front of the same model.
|
|
20
|
+
* So the figures come from the one process that already computes and checks
|
|
21
|
+
* them, and this client's whole job is to fetch and not to interpret.
|
|
22
|
+
*
|
|
23
|
+
* The consequence is stated rather than hidden: with no registry read API
|
|
24
|
+
* configured, `tab_discover` and `tab_status` fail with `UPSTREAM` and say so.
|
|
25
|
+
* `tab_call` does not go through here at all -- it reads its figures off the
|
|
26
|
+
* Service's own charge headers -- so a model can still call and be metered while
|
|
27
|
+
* the index is down.
|
|
28
|
+
*
|
|
29
|
+
* ## Structural web types
|
|
30
|
+
*
|
|
31
|
+
* This package compiles against ES2023 with no DOM types, so `fetch`, its
|
|
32
|
+
* response, and the abort signal are described by the shapes this module
|
|
33
|
+
* touches, exactly as `client-402.ts` does. A host `fetch`, `undici`, and a test
|
|
34
|
+
* closure all satisfy {@link RegistryFetch} without any of them being named.
|
|
35
|
+
*
|
|
36
|
+
* Requirements: 24.1, 24.4, 25.1, 25.3
|
|
37
|
+
*/
|
|
38
|
+
import type { Result } from "../_shared/index.js";
|
|
39
|
+
import { type Logger } from "../logger.js";
|
|
40
|
+
/** The two fields of a response this client reads. */
|
|
41
|
+
export interface RegistryResponse {
|
|
42
|
+
readonly status: number;
|
|
43
|
+
json(): Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** The `fetch` this client calls. A host `fetch` satisfies it as it stands. */
|
|
46
|
+
export type RegistryFetch = (url: string, init: Readonly<Record<string, unknown>>) => Promise<RegistryResponse>;
|
|
47
|
+
export interface RegistryReadClientOptions {
|
|
48
|
+
/** Absolute base URL of the read API, with or without a trailing slash. */
|
|
49
|
+
readonly baseUrl: string;
|
|
50
|
+
readonly fetchImpl?: RegistryFetch;
|
|
51
|
+
/** How long one read may take. Defaults to 10 seconds. */
|
|
52
|
+
readonly timeoutMs?: number;
|
|
53
|
+
readonly logger?: Logger;
|
|
54
|
+
}
|
|
55
|
+
/** Every read `tab_discover` and `tab_status` make, and nothing else. */
|
|
56
|
+
export interface RegistryReadClient {
|
|
57
|
+
/** The base URL reads go to, normalised, so an error message can name it. */
|
|
58
|
+
readonly baseUrl: string;
|
|
59
|
+
/** `GET /healthz`, which is what `doctor` asks before it trusts anything else. */
|
|
60
|
+
health(): Promise<Result<unknown>>;
|
|
61
|
+
/** A page of registered Services, hydrated with prices, collections and Bond. */
|
|
62
|
+
services(limit: number): Promise<Result<unknown>>;
|
|
63
|
+
/** One Service, or `NOT_FOUND` when nothing ever registered under the id. */
|
|
64
|
+
service(serviceId: string): Promise<Result<unknown>>;
|
|
65
|
+
/** One Agent's credit picture per Asset. An address with no history is a 200 with empty arrays. */
|
|
66
|
+
agent(address: string): Promise<Result<unknown>>;
|
|
67
|
+
/** Settlements, newest first, filtered by Agent and optionally by Asset. */
|
|
68
|
+
settlements(query: SettlementQuery): Promise<Result<unknown>>;
|
|
69
|
+
/** One Settlement with the transaction that paid it, named by its `settlementId`. */
|
|
70
|
+
settlement(settlementId: string): Promise<Result<unknown>>;
|
|
71
|
+
}
|
|
72
|
+
export interface SettlementQuery {
|
|
73
|
+
readonly agent: string;
|
|
74
|
+
/** The token address alone, which is how the index keys an Asset. */
|
|
75
|
+
readonly asset?: string;
|
|
76
|
+
readonly limit: number;
|
|
77
|
+
}
|
|
78
|
+
/** Drops a trailing slash so path joining never produces a double one. */
|
|
79
|
+
export declare const normaliseBaseUrl: (baseUrl: string) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Builds the client.
|
|
82
|
+
*
|
|
83
|
+
* Construction is total, like every other factory in this package: an unusable
|
|
84
|
+
* base URL is reported by the first read, which is the only place a caller can
|
|
85
|
+
* act on it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createRegistryReadClient(options: RegistryReadClientOptions): RegistryReadClient;
|
|
88
|
+
//# sourceMappingURL=registry-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-client.d.ts","sourceRoot":"","sources":["../../src/mcp/registry-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,eAAe,CAAC;AAItD,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAG1D,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAEhH,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACnC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAClD,6EAA6E;IAC7E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,mGAAmG;IACnG,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,4EAA4E;IAC5E,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,qFAAqF;IACrF,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAAqC,CAAC;AAsDzF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAsF/F"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyless read client for the Tab registry read API.
|
|
3
|
+
*
|
|
4
|
+
* `tab_discover` and `tab_status` answer questions about chain state -- which
|
|
5
|
+
* Services are registered, what each tool costs, what an Agent owes -- and every
|
|
6
|
+
* one of those answers is a public fact. Nothing here signs, nothing here
|
|
7
|
+
* authenticates, and nothing here writes. That is why `tab connect` can wire a
|
|
8
|
+
* client up without ever touching a key, and why `tab doctor` can check a
|
|
9
|
+
* deployment end to end from a laptop that holds none.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the read API rather than the chain
|
|
12
|
+
*
|
|
13
|
+
* A Credit Limit is not a storage slot. It is `LimitLib` recomputed over an
|
|
14
|
+
* Agent's committed Settlement history and the Bond ledger, cross-checked
|
|
15
|
+
* against `TabBook.creditLimit` at the same block before it is served. A Bond
|
|
16
|
+
* figure is a sum of `Bond`'s own events, checked against `Bond.ledgerOf`. An
|
|
17
|
+
* MCP server that read the chain directly would have to restate both, and a
|
|
18
|
+
* second restatement that drifts from the first is worse than one source of
|
|
19
|
+
* truth: it would put two different Credit Limits in front of the same model.
|
|
20
|
+
* So the figures come from the one process that already computes and checks
|
|
21
|
+
* them, and this client's whole job is to fetch and not to interpret.
|
|
22
|
+
*
|
|
23
|
+
* The consequence is stated rather than hidden: with no registry read API
|
|
24
|
+
* configured, `tab_discover` and `tab_status` fail with `UPSTREAM` and say so.
|
|
25
|
+
* `tab_call` does not go through here at all -- it reads its figures off the
|
|
26
|
+
* Service's own charge headers -- so a model can still call and be metered while
|
|
27
|
+
* the index is down.
|
|
28
|
+
*
|
|
29
|
+
* ## Structural web types
|
|
30
|
+
*
|
|
31
|
+
* This package compiles against ES2023 with no DOM types, so `fetch`, its
|
|
32
|
+
* response, and the abort signal are described by the shapes this module
|
|
33
|
+
* touches, exactly as `client-402.ts` does. A host `fetch`, `undici`, and a test
|
|
34
|
+
* closure all satisfy {@link RegistryFetch} without any of them being named.
|
|
35
|
+
*
|
|
36
|
+
* Requirements: 24.1, 24.4, 25.1, 25.3
|
|
37
|
+
*/
|
|
38
|
+
import { ok } from "../_shared/index.js";
|
|
39
|
+
import { tabError, upstreamError, validationError } from "../errors.js";
|
|
40
|
+
import { defaultLogger } from "../logger.js";
|
|
41
|
+
import { asString, isRecord } from "./json.js";
|
|
42
|
+
/** Drops a trailing slash so path joining never produces a double one. */
|
|
43
|
+
export const normaliseBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/, "");
|
|
44
|
+
/**
|
|
45
|
+
* An abort signal that fires after `ms`, when the host has one.
|
|
46
|
+
*
|
|
47
|
+
* Described structurally rather than typed as `AbortSignal`, for the same reason
|
|
48
|
+
* `fetch` is: no DOM types here. A host without `AbortSignal.timeout` gets no
|
|
49
|
+
* signal and the read runs to whatever timeout its transport imposes, which is
|
|
50
|
+
* worse than a bounded read but better than a failure to construct one.
|
|
51
|
+
*/
|
|
52
|
+
const timeoutSignal = (ms) => {
|
|
53
|
+
const ctor = globalThis.AbortSignal;
|
|
54
|
+
return typeof ctor?.timeout === "function" ? ctor.timeout(ms) : undefined;
|
|
55
|
+
};
|
|
56
|
+
const hostFetch = () => {
|
|
57
|
+
const candidate = globalThis.fetch;
|
|
58
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
59
|
+
};
|
|
60
|
+
const CATEGORIES = ["VALIDATION", "NOT_FOUND", "CONFLICT", "UPSTREAM", "CHAIN", "TIMEOUT", "INTERNAL"];
|
|
61
|
+
/**
|
|
62
|
+
* Reads the read API's own error body back into a `TabError`.
|
|
63
|
+
*
|
|
64
|
+
* Every route there fails with `{ error: { category, code, message } }` in this
|
|
65
|
+
* package's own vocabulary, so a 404 for an unregistered serviceId arrives here
|
|
66
|
+
* as `NOT_FOUND` / `SERVICE_NOT_REGISTERED` and reaches the model unchanged.
|
|
67
|
+
* Rewriting it as a generic upstream failure would throw away the one part a
|
|
68
|
+
* caller can act on.
|
|
69
|
+
*/
|
|
70
|
+
function upstreamFailure(url, status, body) {
|
|
71
|
+
const error = isRecord(body) ? body["error"] : undefined;
|
|
72
|
+
if (isRecord(error)) {
|
|
73
|
+
const category = asString(error["category"], "");
|
|
74
|
+
if (CATEGORIES.includes(category)) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: tabError(category, asString(error["code"], "REGISTRY_READ_FAILED"), asString(error["message"], `the registry read API answered ${status} for ${url}`), { details: { url, status } }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return upstreamError("REGISTRY_READ_FAILED", `the registry read API answered ${status} for ${url}`, { retryable: status >= 500, details: { url, status } });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Builds the client.
|
|
85
|
+
*
|
|
86
|
+
* Construction is total, like every other factory in this package: an unusable
|
|
87
|
+
* base URL is reported by the first read, which is the only place a caller can
|
|
88
|
+
* act on it.
|
|
89
|
+
*/
|
|
90
|
+
export function createRegistryReadClient(options) {
|
|
91
|
+
const baseUrl = normaliseBaseUrl(options.baseUrl);
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
93
|
+
const logger = options.logger ?? defaultLogger;
|
|
94
|
+
const get = async (pathname, query = {}) => {
|
|
95
|
+
if (!/^https?:\/\/[^\s]+$/.test(baseUrl)) {
|
|
96
|
+
return validationError("REGISTRY_URL_INVALID", `the registry read API base URL must be an absolute http or https URL, received \`${options.baseUrl}\``, { details: { baseUrl: options.baseUrl } });
|
|
97
|
+
}
|
|
98
|
+
const send = options.fetchImpl ?? hostFetch();
|
|
99
|
+
if (send === undefined) {
|
|
100
|
+
return upstreamError("FETCH_UNAVAILABLE", "this host has no global fetch; supply fetchImpl, or run on Node 20.10 or later");
|
|
101
|
+
}
|
|
102
|
+
const search = Object.entries(query)
|
|
103
|
+
.filter((entry) => entry[1] !== undefined)
|
|
104
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
105
|
+
.join("&");
|
|
106
|
+
const url = `${baseUrl}${pathname}${search === "" ? "" : `?${search}`}`;
|
|
107
|
+
const signal = timeoutSignal(timeoutMs);
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await send(url, {
|
|
111
|
+
method: "GET",
|
|
112
|
+
headers: { accept: "application/json" },
|
|
113
|
+
...(signal === undefined ? {} : { signal }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
118
|
+
// A transport failure and a timeout are the same shape here and different
|
|
119
|
+
// things to a caller, so the category splits on the abort name.
|
|
120
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
121
|
+
logger.warn("registry read failed", { url, reason });
|
|
122
|
+
// Both are UPSTREAM: the category vocabulary has no timeout of its own, and a
|
|
123
|
+
// read API that answered nothing in time is an upstream failure whichever way
|
|
124
|
+
// it failed. The code is what tells the two apart, and both are retryable.
|
|
125
|
+
return timedOut
|
|
126
|
+
? upstreamError("REGISTRY_READ_TIMEOUT", `the registry read API at ${url} did not answer within ${timeoutMs}ms`, { retryable: true, details: { url, timeoutMs } })
|
|
127
|
+
: upstreamError("REGISTRY_UNREACHABLE", `the registry read API at ${url} could not be reached: ${reason}`, {
|
|
128
|
+
retryable: true,
|
|
129
|
+
details: { url },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
let body;
|
|
133
|
+
try {
|
|
134
|
+
body = await response.json();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
138
|
+
return upstreamError("REGISTRY_BODY_UNREADABLE", `the registry read API at ${url} answered ${response.status} with a body that is not JSON: ${reason}`, { details: { url, status: response.status } });
|
|
139
|
+
}
|
|
140
|
+
if (response.status < 200 || response.status >= 300)
|
|
141
|
+
return upstreamFailure(url, response.status, body);
|
|
142
|
+
return ok(body);
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
baseUrl,
|
|
146
|
+
health: () => get("/healthz"),
|
|
147
|
+
services: (limit) => get("/services", { limit: String(limit) }),
|
|
148
|
+
service: (serviceId) => get(`/services/${encodeURIComponent(serviceId.toLowerCase())}`),
|
|
149
|
+
agent: (address) => get(`/agents/${encodeURIComponent(address.toLowerCase())}`),
|
|
150
|
+
settlements: (query) => get("/settlements", {
|
|
151
|
+
agent: query.agent.toLowerCase(),
|
|
152
|
+
asset: query.asset?.toLowerCase(),
|
|
153
|
+
limit: String(query.limit),
|
|
154
|
+
}),
|
|
155
|
+
settlement: (settlementId) => get(`/settlements/${encodeURIComponent(settlementId.toLowerCase())}`),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=registry-client.js.map
|