baconservices-mcp 0.2.0 → 0.4.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/README.md +9 -6
- package/package.json +2 -2
- package/src/contracts.js +56 -5
- package/src/ledger.js +15 -8
- package/src/payment.js +61 -1
- package/src/server.js +13 -3
package/README.md
CHANGED
|
@@ -18,19 +18,22 @@ spending limits.
|
|
|
18
18
|
|
|
19
19
|
| Tool | Does | Price (2026-10-17) |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| `remove_background` | Removes the background from one photo; returns a transparent PNG at the photo's size | 0.
|
|
21
|
+
| `remove_background` | Removes the background from one photo; returns a transparent PNG at the photo's size | 0.05 USDC |
|
|
22
22
|
| `optimize_image` | Resizes so the longest side ≤ `max_width` (64–4096, default 1600) and encodes WebP at `quality` (40–95, default 80) | 0.01 USDC |
|
|
23
23
|
| `image_palette` | The colours one image is mostly made of, as JSON: up to `count` (1–12, default 6) hex colours with their share, `include_neutrals` (default true) | 0.01 USDC |
|
|
24
24
|
| `brand_from_url` | Reads one public web page (`url`) and returns its brand as JSON: name, description, logo URLs, colours, fonts, social profiles. No JavaScript runs; robots.txt is respected | 0.03 USDC |
|
|
25
|
-
| `
|
|
25
|
+
| `page_metadata` | Reads one public web page (`url`) and returns its SEO and link-preview metadata as JSON: title and description with lengths, canonical, redirects, robots rules, Open Graph and Twitter cards, headings, JSON-LD types, and a list of issues. Nothing it links to is fetched; robots.txt is respected | 0.01 USDC |
|
|
26
|
+
| `accessibility_check` | An automated WCAG check of one public web page (`url`) with axe-core: `standard` (`wcag22aa` default, or `wcag21aa`), `viewport` (`desktop` default, or `mobile`). JSON: failing rules with WCAG criteria, element counts and examples, what needs review, and the criteria a person must test. The page's own scripts don't run. Not a certification of WCAG or ADA conformance | 0.05 USDC |
|
|
27
|
+
| `vectorize_image` | Traces one image into an SVG of flat-colour paths: `colors` (2–32, default 16), `detail` (`smooth`, `balanced` or `detailed`, default `balanced`). Best for logos and icons; a photo's SVG is larger than the photo, and `colors` sets the size | 0.05 USDC |
|
|
28
|
+
| `clean_svg` | Cleans and simplifies one SVG (`svg` markup, `svg_url` or `svg_path`, up to 1 MB) and returns it as JSON with a report. Rebuilt from an allowlist: scripts, event handlers, animation, links and outside references are removed; editor data, unused definitions and hidden or empty elements go; numbers are rounded to `precision` (1–6, default 3) and paths rewritten short. `simplify` (`none` default, `light`, `strong`) moves points on straight runs; `keep_ids` (default false) keeps ids and classes | 0.01 USDC |
|
|
26
29
|
|
|
27
30
|
Image tools take PNG, JPEG or WebP up to 8 MB, as `image_url` (public https) or `image_path` (a
|
|
28
31
|
local file). `remove_background`, `optimize_image` and `vectorize_image` return a signed download
|
|
29
|
-
link (valid 24 hours); `image_palette` and `
|
|
30
|
-
`save_to` to write either kind to disk. Prices come from Bacon's live catalog, and each paid tool
|
|
32
|
+
link (valid 24 hours); `image_palette`, `brand_from_url`, `page_metadata`, `accessibility_check` and `clean_svg` return their JSON result in the tool result. Add
|
|
33
|
+
`save_to` to write either kind to disk. A JSON result over 256 KB (a large cleaned SVG, say) is not shown, so pass `save_to` for one. Prices come from Bacon's live catalog, and each paid tool
|
|
31
34
|
states its current price in its description.
|
|
32
35
|
|
|
33
|
-
`image_palette`, `brand_from_url`
|
|
36
|
+
`image_palette`, `brand_from_url`, `page_metadata`, `accessibility_check`, `vectorize_image` and `clean_svg` appear once Bacon's catalog publishes
|
|
34
37
|
them with contract versions. A service the installed package doesn't understand is listed by `list_services` but
|
|
35
38
|
can't be called until you update the package.
|
|
36
39
|
|
|
@@ -152,7 +155,7 @@ All of these checks happen before anything is signed:
|
|
|
152
155
|
cap is set to your per-call limit as a second lock.
|
|
153
156
|
- **Funds.** It reads your USDC balance first. If it's too low, or the balance can't be read,
|
|
154
157
|
you get a plain message and nothing is signed.
|
|
155
|
-
- **Your approval, where possible.** If the MCP host supports it, you're asked *"Pay 0.
|
|
158
|
+
- **Your approval, where possible.** If the MCP host supports it, you're asked *"Pay 0.05 USDC on
|
|
156
159
|
Base to Bacon Services to run remove_background?"* Not every host can ask, so the limits are
|
|
157
160
|
the real boundary.
|
|
158
161
|
- **No double payments.** Each purchase signs one payment for one request. If the connection drops,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baconservices-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server that lets an AI agent buy Bacon Services tools per call in USDC over x402: remove a background, optimize an image for the web, read an image's colour palette, read a brand from a URL, trace an image into an SVG. With local spending limits.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "MCP server that lets an AI agent buy Bacon Services tools per call in USDC over x402: remove a background, optimize an image for the web, read an image's colour palette, read a brand from a URL, read a page's SEO metadata, run an automated WCAG accessibility check, trace an image into an SVG, clean up an SVG. With local spending limits.",
|
|
5
5
|
"mcpName": "io.github.afmontoya/baconservices-mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/src/contracts.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The live catalog decides what is FOR SALE (and at what price); this table decides what this
|
|
4
4
|
// package will EXECUTE. A capability becomes a paid tool only when its catalog entry matches one
|
|
5
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,
|
|
6
|
+
// `input_version` and `output_version`, an input type (one image, one web page URL, or one SVG) with the
|
|
7
7
|
// same option names, types, bounds and allowed values, an output (an artifact file of one MIME type, or a JSON
|
|
8
8
|
// result), synchronous execution, and declared limits. Anything else — a new capability, a new
|
|
9
9
|
// version, a changed schema, a catalog contract_version this package does not know — is still
|
|
@@ -14,9 +14,13 @@ export const SUPPORTED_CATALOG_VERSIONS = [1];
|
|
|
14
14
|
|
|
15
15
|
export const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/webp'];
|
|
16
16
|
|
|
17
|
+
/** The input formats each input type may declare. A URL input declares the page's, unchecked here. */
|
|
18
|
+
const FORMATS = { image: { allowed: IMAGE_FORMATS, label: 'PNG/JPEG/WebP' }, svg: { allowed: ['image/svg+xml'], label: 'SVG' } };
|
|
19
|
+
|
|
17
20
|
const INPUTS = {
|
|
18
21
|
image: { image_url: { type: 'string' }, image_base64: { type: 'string' } },
|
|
19
22
|
url: { url: { type: 'string' } },
|
|
23
|
+
svg: { svg: { type: 'string' }, svg_url: { type: 'string' } },
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
export const CONTRACTS = [
|
|
@@ -81,6 +85,35 @@ export const CONTRACTS = [
|
|
|
81
85
|
output: { kind: 'json', mime: 'application/json' },
|
|
82
86
|
execution: 'sync',
|
|
83
87
|
},
|
|
88
|
+
{
|
|
89
|
+
tool: 'page_metadata',
|
|
90
|
+
id: 'page-metadata',
|
|
91
|
+
version: 1,
|
|
92
|
+
inputVersion: 1,
|
|
93
|
+
outputVersion: 1,
|
|
94
|
+
title: 'Page metadata',
|
|
95
|
+
summary: 'Reads one public web page and returns what it tells search engines and link previews, as JSON: title and description with lengths, canonical, redirects, robots rules, Open Graph and Twitter cards, headings, JSON-LD types and a list of SEO issues. No JavaScript runs; robots.txt is respected.',
|
|
96
|
+
input: 'url',
|
|
97
|
+
options: {},
|
|
98
|
+
output: { kind: 'json', mime: 'application/json' },
|
|
99
|
+
execution: 'sync',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
tool: 'accessibility_check',
|
|
103
|
+
id: 'accessibility-check',
|
|
104
|
+
version: 1,
|
|
105
|
+
inputVersion: 1,
|
|
106
|
+
outputVersion: 1,
|
|
107
|
+
title: 'Accessibility check',
|
|
108
|
+
summary: 'An automated WCAG check of one public web page (axe-core), as JSON: each failing rule with its impact, WCAG criteria, element count and examples; what needs review; and the criteria a person must test. Not a certification of WCAG or ADA conformance. Scripts on the page do not run; robots.txt is respected.',
|
|
109
|
+
input: 'url',
|
|
110
|
+
options: {
|
|
111
|
+
standard: { type: 'string', enum: ['wcag22aa', 'wcag21aa'], default: 'wcag22aa', description: 'wcag22aa: WCAG 2.2 A and AA. wcag21aa: WCAG 2.1 A and AA.' },
|
|
112
|
+
viewport: { type: 'string', enum: ['desktop', 'mobile'], default: 'desktop', description: 'desktop: 1280 x 800. mobile: 390 x 844, touch.' },
|
|
113
|
+
},
|
|
114
|
+
output: { kind: 'json', mime: 'application/json' },
|
|
115
|
+
execution: 'sync',
|
|
116
|
+
},
|
|
84
117
|
{
|
|
85
118
|
tool: 'vectorize_image',
|
|
86
119
|
id: 'vectorize',
|
|
@@ -88,15 +121,32 @@ export const CONTRACTS = [
|
|
|
88
121
|
inputVersion: 1,
|
|
89
122
|
outputVersion: 1,
|
|
90
123
|
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).',
|
|
124
|
+
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, and its SVG is larger than the photo (often two to three times a PNG of it at 16 colours; colors sets the size). The SVG keeps the image\'s width and height (capped at 4096).',
|
|
92
125
|
input: 'image',
|
|
93
126
|
options: {
|
|
94
|
-
colors: { type: 'integer', minimum: 2, maximum: 32, default: 16, description: 'How many flat colours to trace with. 2 is black and white.' },
|
|
127
|
+
colors: { type: 'integer', minimum: 2, maximum: 32, default: 16, description: 'How many flat colours to trace with. 2 is black and white. Fewer colours make a much smaller file; on a photo, 8 is about 40% of 16.' },
|
|
95
128
|
detail: { type: 'string', enum: ['smooth', 'balanced', 'detailed'], default: 'balanced', description: 'smooth: fewer, simpler shapes. detailed: closer to the source, larger file.' },
|
|
96
129
|
},
|
|
97
130
|
output: { kind: 'artifact', mime: 'image/svg+xml' },
|
|
98
131
|
execution: 'sync',
|
|
99
132
|
},
|
|
133
|
+
{
|
|
134
|
+
tool: 'clean_svg',
|
|
135
|
+
id: 'svg-cleanup',
|
|
136
|
+
version: 1,
|
|
137
|
+
inputVersion: 1,
|
|
138
|
+
outputVersion: 1,
|
|
139
|
+
title: 'Clean SVG',
|
|
140
|
+
summary: 'Cleans and simplifies one SVG and returns it as JSON, with a report (bytes saved, what was removed, warnings). The SVG is rebuilt from an allowlist: scripts, event handlers, animation, links and references outside the file are removed; editor data, unused definitions, hidden and empty elements go; numbers are rounded and path data rewritten short. It looks the same unless simplify is light or strong. The cleaned SVG is result.svg.',
|
|
141
|
+
input: 'svg',
|
|
142
|
+
options: {
|
|
143
|
+
precision: { type: 'integer', minimum: 1, maximum: 6, default: 3, description: 'Decimals kept on coordinates. A drawing under 100 units across keeps one more per factor of ten.' },
|
|
144
|
+
simplify: { type: 'string', enum: ['none', 'light', 'strong'], default: 'none', description: 'none: looks the same. light or strong: points on straight runs move within 0.05% or 0.2% of the drawing\'s diagonal.' },
|
|
145
|
+
keep_ids: { type: 'boolean', default: false, description: 'Keep every id and class, for an SVG a page\'s own CSS or script uses.' },
|
|
146
|
+
},
|
|
147
|
+
output: { kind: 'json', mime: 'application/json' },
|
|
148
|
+
execution: 'sync',
|
|
149
|
+
},
|
|
100
150
|
];
|
|
101
151
|
|
|
102
152
|
export function contractId(id, version) {
|
|
@@ -194,9 +244,10 @@ export function checkEntry(entry, contract, expected) {
|
|
|
194
244
|
if (!positive(limits.max_bytes)) {
|
|
195
245
|
reasons.push('no maximum input size is declared');
|
|
196
246
|
}
|
|
247
|
+
const formats = FORMATS[contract.input];
|
|
197
248
|
if (!Array.isArray(limits.formats) || limits.formats.length === 0
|
|
198
|
-
|| (
|
|
199
|
-
reasons.push(`input formats are missing${
|
|
249
|
+
|| (formats && limits.formats.some((f) => !formats.allowed.includes(f)))) {
|
|
250
|
+
reasons.push(`input formats are missing${formats ? ` or not ${formats.label}` : ''}`);
|
|
200
251
|
}
|
|
201
252
|
if (!legacyCatalog && !positive(limits.max_request_bytes)) {
|
|
202
253
|
reasons.push('no maximum request size is declared');
|
package/src/ledger.js
CHANGED
|
@@ -24,11 +24,14 @@ const COUNTS = new Set(['reserved', 'spent', 'uncertain']);
|
|
|
24
24
|
export class LedgerError extends Error {}
|
|
25
25
|
|
|
26
26
|
export class Ledger {
|
|
27
|
-
constructor(dir, { now = () => Date.now() } = {}) {
|
|
27
|
+
constructor(dir, { now = () => Date.now(), openLock = openSync, platform = process.platform, lockWaitMs = LOCK_WAIT_MS } = {}) {
|
|
28
28
|
this.dir = dir;
|
|
29
29
|
this.file = join(dir, 'spend-ledger.json');
|
|
30
30
|
this.lockFile = join(dir, 'spend-ledger.lock');
|
|
31
31
|
this.now = now;
|
|
32
|
+
this.openLock = openLock;
|
|
33
|
+
this.platform = platform;
|
|
34
|
+
this.lockWaitMs = lockWaitMs;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** Total that counts against the daily ceiling on this network, in atomic units. */
|
|
@@ -132,11 +135,18 @@ export class Ledger {
|
|
|
132
135
|
const started = Date.now();
|
|
133
136
|
for (;;) {
|
|
134
137
|
try {
|
|
135
|
-
closeSync(
|
|
138
|
+
closeSync(this.openLock(this.lockFile, 'wx'));
|
|
136
139
|
break;
|
|
137
140
|
} catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
// On Windows, opening a lock file that another process is still deleting fails with
|
|
142
|
+
// EPERM, not EEXIST. Only a successful open takes the lock, so waiting is safe; an
|
|
143
|
+
// EPERM that outlasts the wait is a real permission problem and refuses like any other.
|
|
144
|
+
const held = error.code === 'EEXIST';
|
|
145
|
+
const contended = held || (error.code === 'EPERM' && this.platform === 'win32');
|
|
146
|
+
if (!contended || Date.now() - started > this.lockWaitMs) {
|
|
147
|
+
throw new LedgerError(held
|
|
148
|
+
? `The spend ledger is locked by another process (${this.lockFile}).`
|
|
149
|
+
: `The spend ledger lock ${this.lockFile} cannot be created (${error.code}).`);
|
|
140
150
|
}
|
|
141
151
|
try {
|
|
142
152
|
if (Date.now() - statSync(this.lockFile).mtimeMs > LOCK_STALE_MS) {
|
|
@@ -144,10 +154,7 @@ export class Ledger {
|
|
|
144
154
|
continue;
|
|
145
155
|
}
|
|
146
156
|
} catch {
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (Date.now() - started > LOCK_WAIT_MS) {
|
|
150
|
-
throw new LedgerError(`The spend ledger is locked by another process (${this.lockFile}).`);
|
|
157
|
+
// Gone already, or it cannot be removed: wait, so the deadline still applies.
|
|
151
158
|
}
|
|
152
159
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
153
160
|
}
|
package/src/payment.js
CHANGED
|
@@ -160,7 +160,9 @@ export class Purchaser {
|
|
|
160
160
|
const endpoint = endpointFor(apiUrl, service.id);
|
|
161
161
|
|
|
162
162
|
// 1. Input, options, destination and the caller's price ceiling: all before any request.
|
|
163
|
-
const image = contract.input === 'url' ? prepareUrl(args, service.entry.input)
|
|
163
|
+
const image = contract.input === 'url' ? prepareUrl(args, service.entry.input)
|
|
164
|
+
: contract.input === 'svg' ? prepareSvg(args, service.entry)
|
|
165
|
+
: prepareImage(args, service.entry.limits);
|
|
164
166
|
const options = prepareOptions(args, contract);
|
|
165
167
|
const saveTo = prepareSaveTo(args);
|
|
166
168
|
let maxPrice = null;
|
|
@@ -536,6 +538,64 @@ export function prepareImage(args, limits) {
|
|
|
536
538
|
return { body: { image_base64: bytes.toString('base64') }, identity: `sha256:${sha256(bytes)}` };
|
|
537
539
|
}
|
|
538
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Exactly one of svg (the markup), svg_url (https) or svg_path (a local file, sent as svg), within
|
|
543
|
+
* Bacon's size limit. The file must be UTF-8 and hold an <svg>; Bacon checks the markup fully
|
|
544
|
+
* (well-formed, no entities, an <svg> root) before it quotes a price.
|
|
545
|
+
*/
|
|
546
|
+
export function prepareSvg(args, entry) {
|
|
547
|
+
const given = ['svg', 'svg_url', 'svg_path'].filter((name) => typeof args[name] === 'string' && args[name].trim() !== '');
|
|
548
|
+
if (given.length !== 1) {
|
|
549
|
+
throw new Refusal('Give exactly one of svg, svg_url or svg_path.');
|
|
550
|
+
}
|
|
551
|
+
const { max_bytes: maxBytes } = entry.limits;
|
|
552
|
+
if (given[0] === 'svg_url') {
|
|
553
|
+
const raw = args.svg_url.trim();
|
|
554
|
+
const maxLength = entry.input?.properties?.svg_url?.maxLength ?? 2048;
|
|
555
|
+
if (raw.length > maxLength) {
|
|
556
|
+
throw new Refusal(`svg_url is ${raw.length} characters; Bacon takes at most ${maxLength}.`);
|
|
557
|
+
}
|
|
558
|
+
let url;
|
|
559
|
+
try {
|
|
560
|
+
url = new URL(raw);
|
|
561
|
+
} catch {
|
|
562
|
+
throw new Refusal('svg_url is not a URL.');
|
|
563
|
+
}
|
|
564
|
+
if (url.protocol !== 'https:') {
|
|
565
|
+
throw new Refusal('svg_url must be a public https URL.');
|
|
566
|
+
}
|
|
567
|
+
return { body: { svg_url: url.href }, identity: `url:${url.href}` };
|
|
568
|
+
}
|
|
569
|
+
let markup;
|
|
570
|
+
if (given[0] === 'svg') {
|
|
571
|
+
markup = args.svg;
|
|
572
|
+
} else {
|
|
573
|
+
const path = resolve(args.svg_path.trim());
|
|
574
|
+
let size;
|
|
575
|
+
try {
|
|
576
|
+
size = statSync(path).size;
|
|
577
|
+
} catch {
|
|
578
|
+
throw new Refusal(`svg_path ${path} cannot be read.`);
|
|
579
|
+
}
|
|
580
|
+
if (size > maxBytes) {
|
|
581
|
+
throw new Refusal(`svg_path is ${size} bytes; Bacon takes at most ${maxBytes}.`);
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
markup = new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(path));
|
|
585
|
+
} catch {
|
|
586
|
+
throw new Refusal('svg_path must be a UTF-8 text file.');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const bytes = Buffer.byteLength(markup);
|
|
590
|
+
if (bytes > maxBytes) {
|
|
591
|
+
throw new Refusal(`The SVG is ${bytes} bytes; Bacon takes at most ${maxBytes}.`);
|
|
592
|
+
}
|
|
593
|
+
if (!markup.includes('<svg')) {
|
|
594
|
+
throw new Refusal(`${given[0]} does not hold an <svg> element.`);
|
|
595
|
+
}
|
|
596
|
+
return { body: { svg: markup }, identity: `sha256:${sha256(markup)}` };
|
|
597
|
+
}
|
|
598
|
+
|
|
539
599
|
/**
|
|
540
600
|
* One public web page for a URL capability: http(s), no user:pass@, ports 80/443 only, within
|
|
541
601
|
* the catalog's maxLength. Bacon checks it again (and that it is public) before quoting.
|
package/src/server.js
CHANGED
|
@@ -11,10 +11,10 @@ import { CONTRACTS } from './contracts.js';
|
|
|
11
11
|
import { loadConfig } from './config.js';
|
|
12
12
|
import { Ledger } from './ledger.js';
|
|
13
13
|
import { formatUsdc } from './money.js';
|
|
14
|
-
import { Purchaser, Refusal } from './payment.js';
|
|
14
|
+
import { HARD_MAX_RESULT_BYTES, Purchaser, Refusal } from './payment.js';
|
|
15
15
|
import { balanceReader, loadWallet, redact } from './wallet.js';
|
|
16
16
|
|
|
17
|
-
export const VERSION = '0.
|
|
17
|
+
export const VERSION = '0.4.0';
|
|
18
18
|
const CATALOG_TTL_MS = 5 * 60 * 1000;
|
|
19
19
|
|
|
20
20
|
const FREE_TOOLS = [
|
|
@@ -133,14 +133,24 @@ export function createBaconServer({ env = process.env, fetch = globalThis.fetch,
|
|
|
133
133
|
const network = config.network;
|
|
134
134
|
const options = Object.fromEntries(Object.entries(contract.options).map(([name, spec]) => [name, { ...spec }]));
|
|
135
135
|
const price = `Costs ${service.price.usdc} USDC per call${network.testnet ? ' (test USDC)' : ' (real money)'} on ${network.label}, paid from the buyer's wallet over x402 after the buyer's spending limits are checked.`;
|
|
136
|
+
// A service may declare a larger JSON result than this package shows: say so before anyone pays.
|
|
137
|
+
const unshown = limits.max_response_bytes > HARD_MAX_RESULT_BYTES
|
|
138
|
+
? ` A result over ${HARD_MAX_RESULT_BYTES / 1024} KB is not shown; pass save_to to keep it.` : '';
|
|
136
139
|
const output = contract.output.kind === 'json'
|
|
137
|
-
?
|
|
140
|
+
? `Output: JSON, returned in the tool result.${unshown}`
|
|
138
141
|
: `Output: ${contract.output.mime}, as a download link (and a file with save_to).`;
|
|
139
142
|
let input;
|
|
140
143
|
let source;
|
|
141
144
|
if (contract.input === 'url') {
|
|
142
145
|
input = 'Give url: one public web page (http or https, standard ports).';
|
|
143
146
|
source = { url: { type: 'string', description: 'A public web page, usually the homepage.', maxLength: service.entry.input.properties.url.maxLength ?? 2048 } };
|
|
147
|
+
} else if (contract.input === 'svg') {
|
|
148
|
+
input = `Input: one SVG, up to ${Math.floor(limits.max_bytes / 1048576)} MB. Give exactly one of svg, svg_url or svg_path.`;
|
|
149
|
+
source = {
|
|
150
|
+
svg: { type: 'string', description: 'The SVG markup itself.' },
|
|
151
|
+
svg_url: { type: 'string', description: 'A public https URL of an SVG file.', maxLength: service.entry.input.properties.svg_url.maxLength ?? 2048 },
|
|
152
|
+
svg_path: { type: 'string', description: 'A local SVG file to send (read by this server as UTF-8 text).' },
|
|
153
|
+
};
|
|
144
154
|
} else {
|
|
145
155
|
const formats = limits.formats.map((f) => f.replace('image/', '').toUpperCase()).join(', ');
|
|
146
156
|
input = `Input: ${formats}, up to ${Math.floor(limits.max_bytes / 1048576)} MB${limits.max_dimension ? `, at most ${limits.max_dimension} px` : ''}. Give exactly one of image_url or image_path.`;
|