baconservices-mcp 0.1.2 → 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 +21 -21
- package/README.md +5 -4
- package/bin/baconservices-mcp.js +8 -8
- package/package.json +4 -2
- package/src/catalog.js +71 -71
- package/src/config.js +103 -103
- package/src/contracts.js +212 -194
- package/src/ledger.js +165 -165
- package/src/money.js +26 -26
- package/src/networks.js +39 -39
- package/src/payment.js +625 -621
- package/src/server.js +1 -1
- package/src/wallet.js +60 -60
package/src/contracts.js
CHANGED
|
@@ -1,194 +1,212 @@
|
|
|
1
|
-
// The capability contracts this version of the package understands.
|
|
2
|
-
//
|
|
3
|
-
// The live catalog decides what is FOR SALE (and at what price); this table decides what this
|
|
4
|
-
// package will EXECUTE. A capability becomes a paid tool only when its catalog entry matches one
|
|
5
|
-
// of these contracts exactly. A contract is the capability id, its behaviour `version`, its
|
|
6
|
-
// `input_version` and `output_version`, an input type (one image, or one web page URL) with the
|
|
7
|
-
// same option names, types and
|
|
8
|
-
// result), synchronous execution, and declared limits. Anything else — a new capability, a new
|
|
9
|
-
// version, a changed schema, a catalog contract_version this package does not know — is still
|
|
10
|
-
// shown by list_services, but is not executable until a package release that knows it.
|
|
11
|
-
|
|
12
|
-
/** Catalog document shapes (`contract_version`) this package understands. */
|
|
13
|
-
export const SUPPORTED_CATALOG_VERSIONS = [1];
|
|
14
|
-
|
|
15
|
-
export const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/webp'];
|
|
16
|
-
|
|
17
|
-
const INPUTS = {
|
|
18
|
-
image: { image_url: { type: 'string' }, image_base64: { type: 'string' } },
|
|
19
|
-
url: { url: { type: 'string' } },
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const CONTRACTS = [
|
|
23
|
-
{
|
|
24
|
-
tool: 'remove_background',
|
|
25
|
-
id: 'remove-background',
|
|
26
|
-
version: 1,
|
|
27
|
-
inputVersion: 1,
|
|
28
|
-
outputVersion: 1,
|
|
29
|
-
// Sold before catalog contract fields existed: also matches a catalog without them.
|
|
30
|
-
legacy: true,
|
|
31
|
-
title: 'Remove background',
|
|
32
|
-
summary: 'Removes the background from one photo and returns the subject on a transparent background as a PNG, at the photo\'s own size.',
|
|
33
|
-
input: 'image',
|
|
34
|
-
options: {},
|
|
35
|
-
output: { kind: 'artifact', mime: 'image/png' },
|
|
36
|
-
execution: 'sync',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
tool: 'optimize_image',
|
|
40
|
-
id: 'web-image-optimizer',
|
|
41
|
-
version: 1,
|
|
42
|
-
inputVersion: 1,
|
|
43
|
-
outputVersion: 1,
|
|
44
|
-
legacy: true,
|
|
45
|
-
title: 'Optimize image for the web',
|
|
46
|
-
summary: 'Resizes one image so its longest side is at most max_width pixels (never enlarging it) and encodes it as WebP at the given quality. Transparency is kept; metadata is not carried over.',
|
|
47
|
-
input: 'image',
|
|
48
|
-
options: {
|
|
49
|
-
max_width: { type: 'integer', minimum: 64, maximum: 4096, default: 1600, description: 'Longest side of the output, in pixels.' },
|
|
50
|
-
quality: { type: 'integer', minimum: 40, maximum: 95, default: 80, description: 'WebP quality.' },
|
|
51
|
-
},
|
|
52
|
-
output: { kind: 'artifact', mime: 'image/webp' },
|
|
53
|
-
execution: 'sync',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
tool: 'image_palette',
|
|
57
|
-
id: 'image-palette',
|
|
58
|
-
version: 1,
|
|
59
|
-
inputVersion: 1,
|
|
60
|
-
outputVersion: 1,
|
|
61
|
-
title: 'Image palette',
|
|
62
|
-
summary: 'Returns the colours one image is mostly made of, as JSON: up to count colours, most common first, each a hex value with its share of the image and whether it is a neutral.',
|
|
63
|
-
input: 'image',
|
|
64
|
-
options: {
|
|
65
|
-
count: { type: 'integer', minimum: 1, maximum: 12, default: 6, description: 'The most colours to return.' },
|
|
66
|
-
include_neutrals: { type: 'boolean', default: true, description: 'Count near-whites, near-blacks and greys too.' },
|
|
67
|
-
},
|
|
68
|
-
output: { kind: 'json', mime: 'application/json' },
|
|
69
|
-
execution: 'sync',
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
tool: 'brand_from_url',
|
|
73
|
-
id: 'brand-from-url',
|
|
74
|
-
version: 1,
|
|
75
|
-
inputVersion: 1,
|
|
76
|
-
outputVersion: 1,
|
|
77
|
-
title: 'Brand from URL',
|
|
78
|
-
summary: 'Reads one public web page and returns the brand it publishes about itself, as JSON: name, description, logo image URLs, colours, fonts and social profiles. No JavaScript runs; robots.txt is respected.',
|
|
79
|
-
input: 'url',
|
|
80
|
-
options: {},
|
|
81
|
-
output: { kind: 'json', mime: 'application/json' },
|
|
82
|
-
execution: 'sync',
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
reasons.push(
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
reasons.push(
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
// The capability contracts this version of the package understands.
|
|
2
|
+
//
|
|
3
|
+
// The live catalog decides what is FOR SALE (and at what price); this table decides what this
|
|
4
|
+
// package will EXECUTE. A capability becomes a paid tool only when its catalog entry matches one
|
|
5
|
+
// of these contracts exactly. A contract is the capability id, its behaviour `version`, its
|
|
6
|
+
// `input_version` and `output_version`, an input type (one image, or one web page URL) with the
|
|
7
|
+
// same option names, types, bounds and allowed values, an output (an artifact file of one MIME type, or a JSON
|
|
8
|
+
// result), synchronous execution, and declared limits. Anything else — a new capability, a new
|
|
9
|
+
// version, a changed schema, a catalog contract_version this package does not know — is still
|
|
10
|
+
// shown by list_services, but is not executable until a package release that knows it.
|
|
11
|
+
|
|
12
|
+
/** Catalog document shapes (`contract_version`) this package understands. */
|
|
13
|
+
export const SUPPORTED_CATALOG_VERSIONS = [1];
|
|
14
|
+
|
|
15
|
+
export const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/webp'];
|
|
16
|
+
|
|
17
|
+
const INPUTS = {
|
|
18
|
+
image: { image_url: { type: 'string' }, image_base64: { type: 'string' } },
|
|
19
|
+
url: { url: { type: 'string' } },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const CONTRACTS = [
|
|
23
|
+
{
|
|
24
|
+
tool: 'remove_background',
|
|
25
|
+
id: 'remove-background',
|
|
26
|
+
version: 1,
|
|
27
|
+
inputVersion: 1,
|
|
28
|
+
outputVersion: 1,
|
|
29
|
+
// Sold before catalog contract fields existed: also matches a catalog without them.
|
|
30
|
+
legacy: true,
|
|
31
|
+
title: 'Remove background',
|
|
32
|
+
summary: 'Removes the background from one photo and returns the subject on a transparent background as a PNG, at the photo\'s own size.',
|
|
33
|
+
input: 'image',
|
|
34
|
+
options: {},
|
|
35
|
+
output: { kind: 'artifact', mime: 'image/png' },
|
|
36
|
+
execution: 'sync',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
tool: 'optimize_image',
|
|
40
|
+
id: 'web-image-optimizer',
|
|
41
|
+
version: 1,
|
|
42
|
+
inputVersion: 1,
|
|
43
|
+
outputVersion: 1,
|
|
44
|
+
legacy: true,
|
|
45
|
+
title: 'Optimize image for the web',
|
|
46
|
+
summary: 'Resizes one image so its longest side is at most max_width pixels (never enlarging it) and encodes it as WebP at the given quality. Transparency is kept; metadata is not carried over.',
|
|
47
|
+
input: 'image',
|
|
48
|
+
options: {
|
|
49
|
+
max_width: { type: 'integer', minimum: 64, maximum: 4096, default: 1600, description: 'Longest side of the output, in pixels.' },
|
|
50
|
+
quality: { type: 'integer', minimum: 40, maximum: 95, default: 80, description: 'WebP quality.' },
|
|
51
|
+
},
|
|
52
|
+
output: { kind: 'artifact', mime: 'image/webp' },
|
|
53
|
+
execution: 'sync',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
tool: 'image_palette',
|
|
57
|
+
id: 'image-palette',
|
|
58
|
+
version: 1,
|
|
59
|
+
inputVersion: 1,
|
|
60
|
+
outputVersion: 1,
|
|
61
|
+
title: 'Image palette',
|
|
62
|
+
summary: 'Returns the colours one image is mostly made of, as JSON: up to count colours, most common first, each a hex value with its share of the image and whether it is a neutral.',
|
|
63
|
+
input: 'image',
|
|
64
|
+
options: {
|
|
65
|
+
count: { type: 'integer', minimum: 1, maximum: 12, default: 6, description: 'The most colours to return.' },
|
|
66
|
+
include_neutrals: { type: 'boolean', default: true, description: 'Count near-whites, near-blacks and greys too.' },
|
|
67
|
+
},
|
|
68
|
+
output: { kind: 'json', mime: 'application/json' },
|
|
69
|
+
execution: 'sync',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
tool: 'brand_from_url',
|
|
73
|
+
id: 'brand-from-url',
|
|
74
|
+
version: 1,
|
|
75
|
+
inputVersion: 1,
|
|
76
|
+
outputVersion: 1,
|
|
77
|
+
title: 'Brand from URL',
|
|
78
|
+
summary: 'Reads one public web page and returns the brand it publishes about itself, as JSON: name, description, logo image URLs, colours, fonts and social profiles. No JavaScript runs; robots.txt is respected.',
|
|
79
|
+
input: 'url',
|
|
80
|
+
options: {},
|
|
81
|
+
output: { kind: 'json', mime: 'application/json' },
|
|
82
|
+
execution: 'sync',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
tool: 'vectorize_image',
|
|
86
|
+
id: 'vectorize',
|
|
87
|
+
version: 1,
|
|
88
|
+
inputVersion: 1,
|
|
89
|
+
outputVersion: 1,
|
|
90
|
+
title: 'Vectorize image',
|
|
91
|
+
summary: 'Traces one PNG, JPEG or WebP image into an SVG of filled paths with up to colors flat colours. Best for logos, icons and flat artwork; a photo comes back as a stylised poster. The SVG keeps the image\'s width and height (capped at 4096).',
|
|
92
|
+
input: 'image',
|
|
93
|
+
options: {
|
|
94
|
+
colors: { type: 'integer', minimum: 2, maximum: 32, default: 16, description: 'How many flat colours to trace with. 2 is black and white.' },
|
|
95
|
+
detail: { type: 'string', enum: ['smooth', 'balanced', 'detailed'], default: 'balanced', description: 'smooth: fewer, simpler shapes. detailed: closer to the source, larger file.' },
|
|
96
|
+
},
|
|
97
|
+
output: { kind: 'artifact', mime: 'image/svg+xml' },
|
|
98
|
+
execution: 'sync',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export function contractId(id, version) {
|
|
103
|
+
return `${id}@${version}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function contractForTool(tool) {
|
|
107
|
+
return CONTRACTS.find((c) => c.tool === tool) ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function contractForEntry(entry) {
|
|
111
|
+
return CONTRACTS.find((c) => c.id === entry?.id) ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether a live catalog entry matches its contract, and every reason it does not.
|
|
116
|
+
* `expected`: the endpoint URL, the network's USDC address, and the catalog's contract_version
|
|
117
|
+
* (undefined for a catalog published before contract fields existed).
|
|
118
|
+
*/
|
|
119
|
+
export function checkEntry(entry, contract, expected) {
|
|
120
|
+
const reasons = [];
|
|
121
|
+
if (!contract) {
|
|
122
|
+
return [`this version of baconservices-mcp does not know the contract ${contractId(entry?.id, entry?.version)}; update the package to use it`];
|
|
123
|
+
}
|
|
124
|
+
const legacyCatalog = expected.catalogVersion === undefined;
|
|
125
|
+
if (!legacyCatalog && !SUPPORTED_CATALOG_VERSIONS.includes(expected.catalogVersion)) {
|
|
126
|
+
return [`the catalog's contract_version ${JSON.stringify(expected.catalogVersion)} is not one this package understands (${SUPPORTED_CATALOG_VERSIONS.join(', ')}); update the package`];
|
|
127
|
+
}
|
|
128
|
+
if (legacyCatalog && !contract.legacy) {
|
|
129
|
+
return ['the catalog does not declare contract versions, which this service needs'];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entry.version !== contract.version) {
|
|
133
|
+
reasons.push(`Bacon offers ${contractId(entry.id, entry.version)}, this package understands ${contractId(contract.id, contract.version)}; update the package`);
|
|
134
|
+
}
|
|
135
|
+
// A legacy catalog has no contract fields: its two services are input/output version 1 by definition.
|
|
136
|
+
const inputVersion = legacyCatalog ? (entry.input_version ?? 1) : entry.input_version;
|
|
137
|
+
const outputVersion = legacyCatalog ? (entry.output_version ?? 1) : entry.output_version;
|
|
138
|
+
if (inputVersion !== contract.inputVersion) {
|
|
139
|
+
reasons.push(`input_version is ${JSON.stringify(entry.input_version)}, this package understands ${contract.inputVersion}`);
|
|
140
|
+
}
|
|
141
|
+
if (outputVersion !== contract.outputVersion) {
|
|
142
|
+
reasons.push(`output_version is ${JSON.stringify(entry.output_version)}, this package understands ${contract.outputVersion}`);
|
|
143
|
+
}
|
|
144
|
+
if ((entry.execution ?? 'sync') !== contract.execution) {
|
|
145
|
+
reasons.push(`execution is "${entry.execution}", expected "${contract.execution}"`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const output = entry.output ?? {};
|
|
149
|
+
const kind = legacyCatalog ? (output.kind ?? 'artifact') : output.kind;
|
|
150
|
+
if (kind !== contract.output.kind || output.mime !== contract.output.mime) {
|
|
151
|
+
reasons.push(`output is ${kind ?? 'undeclared'} ${output.mime ?? ''}, expected ${contract.output.kind} ${contract.output.mime}`);
|
|
152
|
+
}
|
|
153
|
+
if (!legacyCatalog && (typeof output.schema !== 'object' || output.schema === null)) {
|
|
154
|
+
reasons.push('no output schema is declared');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const input = entry.input ?? {};
|
|
158
|
+
const expectedProperties = { ...INPUTS[contract.input], ...contract.options };
|
|
159
|
+
const properties = input.properties ?? {};
|
|
160
|
+
if (input.type !== 'object' || input.additionalProperties !== false) {
|
|
161
|
+
reasons.push('input schema is not a closed object');
|
|
162
|
+
}
|
|
163
|
+
const names = Object.keys(properties).sort().join(',');
|
|
164
|
+
if (names !== Object.keys(expectedProperties).sort().join(',')) {
|
|
165
|
+
reasons.push(`input properties are [${names}], expected [${Object.keys(expectedProperties).sort().join(',')}]`);
|
|
166
|
+
} else {
|
|
167
|
+
for (const [name, want] of Object.entries(expectedProperties)) {
|
|
168
|
+
for (const key of ['type', 'minimum', 'maximum', 'default', 'enum']) {
|
|
169
|
+
// An enum is compared whole: the same values in the same order.
|
|
170
|
+
const same = key === 'enum' ? JSON.stringify(want[key]) === JSON.stringify(properties[name][key]) : want[key] === properties[name][key];
|
|
171
|
+
if (!same) {
|
|
172
|
+
reasons.push(`input ${name}.${key} is ${JSON.stringify(properties[name][key])}, expected ${JSON.stringify(want[key])}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (contract.input === 'url' && !(Array.isArray(input.required) && input.required.includes('url'))) {
|
|
178
|
+
reasons.push('input does not require url');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const price = entry.price ?? {};
|
|
182
|
+
if (typeof price.amount !== 'string' || !/^\d{1,30}$/.test(price.amount)) {
|
|
183
|
+
reasons.push('price amount is missing or not atomic units');
|
|
184
|
+
}
|
|
185
|
+
if (price.symbol !== 'USDC' || price.decimals !== 6 || typeof price.asset !== 'string' || price.asset.toLowerCase() !== expected.usdc.toLowerCase()) {
|
|
186
|
+
reasons.push('price is not in USDC on the configured network');
|
|
187
|
+
}
|
|
188
|
+
if (entry.endpoint?.method !== 'POST' || entry.endpoint?.url !== expected.endpoint) {
|
|
189
|
+
reasons.push(`endpoint is ${entry.endpoint?.method} ${entry.endpoint?.url}, expected POST ${expected.endpoint}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const limits = entry.limits ?? {};
|
|
193
|
+
const positive = (value) => Number.isInteger(value) && value > 0;
|
|
194
|
+
if (!positive(limits.max_bytes)) {
|
|
195
|
+
reasons.push('no maximum input size is declared');
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(limits.formats) || limits.formats.length === 0
|
|
198
|
+
|| (contract.input === 'image' && limits.formats.some((f) => !IMAGE_FORMATS.includes(f)))) {
|
|
199
|
+
reasons.push(`input formats are missing${contract.input === 'image' ? ' or not PNG/JPEG/WebP' : ''}`);
|
|
200
|
+
}
|
|
201
|
+
if (!legacyCatalog && !positive(limits.max_request_bytes)) {
|
|
202
|
+
reasons.push('no maximum request size is declared');
|
|
203
|
+
}
|
|
204
|
+
if (contract.output.kind === 'json' && !positive(limits.max_response_bytes)) {
|
|
205
|
+
reasons.push('no maximum response size is declared for a JSON result');
|
|
206
|
+
}
|
|
207
|
+
if (limits.max_seconds !== undefined && !positive(limits.max_seconds)) {
|
|
208
|
+
reasons.push('max_seconds is not a positive whole number');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return reasons;
|
|
212
|
+
}
|