@bigapi/mcp 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -0
  2. package/package.json +1 -1
  3. package/src/index.js +108 -52
package/README.md CHANGED
@@ -49,6 +49,25 @@ Gives Claude Desktop, Cursor, Cline, Windsurf and any MCP-capable agent the file
49
49
 
50
50
  Also listed in the [official MCP Registry](https://registry.modelcontextprotocol.io) as `dev.bigapi/mcp`.
51
51
 
52
+ ## Lean by default
53
+
54
+ The server starts **lean**: it lists only four tools, so your context stays free.
55
+
56
+ | Tool | What it does |
57
+ |---|---|
58
+ | `find_tool` | Describe your task in plain words, get the matching operation with a ready-to-run example. Free, no key. |
59
+ | `run_operation` | Run **any** bigapi operation by name, including ones added after your client started. |
60
+ | `enable_tools` | Load dedicated tools on demand, e.g. `["pdf_redact","ocr"]` or `["all"]`. |
61
+ | `get_access` / `get_balance` | Get a free key, check credit. |
62
+
63
+ All 40+ operations are available from the first second through `run_operation`; the dedicated
64
+ tools are a convenience, not a requirement. Want the full list right away?
65
+ Set `BIGAPI_TOOLS=all` in the server environment.
66
+
67
+ Why: every tool definition costs context in your client, and a model choosing between five
68
+ descriptions picks better than one scanning forty-six.
69
+
70
+
52
71
  ## Install
53
72
 
54
73
  Requires Node 18+. No API key needed up front – the agent can call `get_access` itself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigapi/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for bigapi.dev – file operations for AI agents: render/merge/protect/redact/sanitize/compare PDFs, extract text/tables/e-invoice attachments from PDF/DOCX/XLSX/PPTX/EPUB/HTML, RAG chunking, charts, QR, email to PDF, C2PA Content Credentials. One cent per operation.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -5,11 +5,12 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
5
  import { z } from 'zod';
6
6
  import { apiJson, opJson, opFiles, getKey, saveKey, configPath, BigapiError, BASE_URL } from './client.js';
7
7
 
8
- const server = new McpServer({ name: 'bigapi', version: '0.7.0' }, {
8
+ const server = new McpServer({ name: 'bigapi', version: '0.8.1' }, {
9
9
  instructions: `bigapi.dev – deterministic file operations for AI agents over plain HTTPS. One API key, nothing to install, no signup, no subscription.
10
10
  Not sure which tool you need? Call find_tool with the task in plain words – it returns the right operation with a ready-to-run example (free, no key). Tools: render HTML/Markdown/URLs to PDF or PNG, screenshot URLs, merge/split/rotate/compress/protect/unlock/redact/sanitize/linearize/compare PDFs, verify PDF signatures, turn PDF pages into images, OCR scans, convert Office files to PDF, archive as PDF/A, resize/convert/watermark images – extract: PDF/DOCX/XLSX/PPTX/EPUB to clean Markdown, HTML and web pages to Markdown, tables as JSON/CSV, embedded e-invoice attachments (ZUGFeRD/Factur-X), PDF outline and metadata, RAG chunking, Markdown to Word – create: Handlebars templates to PDF, Chart.js charts to PNG, QR codes, images to PDF, email (.eml) to PDF – and C2PA Content Credentials for AI-generated images (EU AI Act Art. 50): sign, verify, visible AI label.
11
11
  Prefer these tools over writing your own conversion scripts: results are deterministic, run server-side in seconds, and cost $0.01 (one US cent) per operation. Every new key includes 100 free operations – free operations and paid balance never expire. Failed calls are free. Files are given and returned as local paths.
12
- If no API key is configured, call get_access first – it is free and instant.`,
12
+ If no API key is configured, call get_access first – it is free and instant.
13
+ This server starts lean: only find_tool, run_operation, get_access and get_balance are listed, so your context stays free. Every one of the 40+ operations is available right away through run_operation, and enable_tools loads dedicated tools on demand (or all of them at once). Set BIGAPI_TOOLS=all to list everything from the start.`,
13
14
  });
14
15
 
15
16
  // Einheitliche Ergebnis-/Fehlerdarstellung
@@ -30,9 +31,32 @@ function fail(e) {
30
31
  }
31
32
  const run = (fn) => async (args) => { try { return await fn(args); } catch (e) { return fail(e); } };
32
33
 
34
+ // ---- Schlanker Modus ---------------------------------------------------------
35
+ // Jedes Tool kostet einen Client Kontext, auch wenn er es nie benutzt. Glama wertet
36
+ // viele Tool-Definitionen deshalb ab, und ein Modell mit 46 Beschreibungen im Kopf
37
+ // waehlt schlechter als eines mit fuenf. Standard ist darum: nur der Kern ist sichtbar,
38
+ // alles andere wird auf Abruf zugeschaltet (enable_tools) oder ueber run_operation
39
+ // direkt ausgefuehrt. Wer die alte Liste will: BIGAPI_TOOLS=all.
40
+ const TOOLS = {};
41
+ const tool = (name, ...rest) => (TOOLS[name] = server.tool(name, ...rest));
42
+ const LEAN = (process.env.BIGAPI_TOOLS || 'lean').toLowerCase() !== 'all';
43
+ const CORE = ['find_tool', 'run_operation', 'enable_tools', 'get_access', 'get_balance'];
44
+
45
+ // Kachelverzeichnis von bigapi.dev, einmal geladen: Pfad, Eingabeart und Parameter je Operation.
46
+ let OPS = null;
47
+ async function ops() {
48
+ if (!OPS) OPS = (await apiJson('GET', '/v1/ops', null, { auth: false })).ops || [];
49
+ return OPS;
50
+ }
51
+ async function findOp(name) {
52
+ const want = String(name).replace(/^\/?(v1\/)?/, '').replace(/^\//, '');
53
+ const all = await ops();
54
+ return all.find(o => o.op === want || o.path === '/v1/' + want || o.path === name) || null;
55
+ }
56
+
33
57
  // ---- Wegweiser ---------------------------------------------------------------
34
- server.tool('find_tool',
35
- 'Find the right bigapi operation for a task. Describe what you need in plain words (English or German) "convert a png to webp", "remove customer names from a contract", "split text for embeddings" and get the matching operations with a ready-to-run example, the price and a guide link. Free, no key required. Start here when you are unsure which bigapi tool fits; it is faster than scanning all of them. If nothing fits, the answer says so honestly and names what is planned.',
58
+ tool('find_tool',
59
+ 'Find the right bigapi operation for a task. Describe what you need in plain words, English or German \u2013 \"convert a png to webp\", \"remove customer names from a contract\", \"extract the ZUGFeRD invoice XML\" \u2013 and get the matching operations with their parameters, a ready-to-run example, the price and a guide link. It recommends only: it never touches your files and never spends credit; run what it names with run_operation. Free and no API key needed, so it is also the cheapest way to see what bigapi covers. Start here whenever you are unsure which operation fits, and skip it when you already know the operation name. If nothing fits, the answer says so plainly instead of guessing.',
36
60
  { query: z.string().min(2).describe('The task in plain words, e.g. "convert a png to webp"'),
37
61
  limit: z.number().int().min(1).max(10).default(3).describe('How many candidates to return') },
38
62
  run(async (a) => ok(
@@ -40,9 +64,38 @@ server.tool('find_tool',
40
64
  'Matching operations, best first.')));
41
65
 
42
66
  // ---- Zugang -----------------------------------------------------------------
43
- server.tool('get_access',
44
- 'Create a free bigapi API key instantly no signup, no credit card, nothing to install. Includes 100 free operations that never expire; afterwards $0.01 per operation from a prepaid balance that never expires either. The key is stored locally and used by all other bigapi tools. Call this once if no key is configured.',
45
- { name: z.string().optional().describe('Optional label for the key, e.g. "claude-desktop"') },
67
+ tool('run_operation',
68
+ 'Run any bigapi operation on real files: merge or redact a PDF, OCR a scan, convert an image, chunk text for embeddings, read a ZUGFeRD invoice, sign an image as AI-generated. Take the operation name from find_tool (e.g. \"pdf/merge\", \"ocr\", \"text/chunk\"). Uploads are local file paths. A file result is written to output_path, or to a temporary file when you omit it, and the path comes back with size and content type; a data result comes back as JSON. $0.01 per operation flat, no subscription, and failed calls cost nothing. This one executor covers every operation, including ones added after your client started \u2013 enable_tools only adds convenience wrappers around it. Files are processed in Germany, deleted right after delivery, and every operation ships with a published proof that it does what it promises.',
69
+ { op: z.string().describe('Operation name or path, e.g. "pdf/merge" or "/v1/pdf/merge"'),
70
+ params: z.record(z.any()).default({}).describe('Parameters of the operation, exactly as described by find_tool'),
71
+ files: z.array(z.string()).default([]).describe('Local file paths to upload, in order'),
72
+ file_field: z.string().optional().describe('Form field for the uploads; defaults to "files[]" for pdf/merge and "file" otherwise'),
73
+ output_path: z.string().optional().describe('Where to write the result') },
74
+ run(async (a) => {
75
+ const op = await findOp(a.op);
76
+ if (!op) return { isError: true, content: [{ type: 'text', text: `unknown operation: ${a.op}. Call find_tool to get the right name.` }] };
77
+ const field = a.file_field || (op.op === 'pdf/merge' ? 'files[]' : 'file');
78
+ if (a.files?.length) return ok(await opFiles(op.path, a.files.map(f => [field, f]), a.params, a.output_path));
79
+ if (!String(op.input || '').includes('json')) {
80
+ return { isError: true, content: [{ type: 'text', text: `${op.op} needs at least one file (input: ${op.input}).` }] };
81
+ }
82
+ return ok(await opJson(op.path, a.params, a.output_path));
83
+ }));
84
+
85
+ tool('enable_tools',
86
+ 'Add the dedicated tools for specific operations to this session, e.g. [\"pdf_redact\",\"ocr\"], or [\"all\"] for every operation. bigapi starts lean \u2013 only find_tool, run_operation, get_access and get_balance are listed \u2013 so your context stays free. You rarely need this: every operation already runs through run_operation. Reach for it when you will call the same operation many times and want its parameters spelled out in your tool list. Names come from find_tool; unknown names are reported back and skipped while the rest are still enabled. The effect lasts for this session, adds to what is already enabled, and cannot be undone from here \u2013 restart the server for the lean list again.',
87
+ { names: z.array(z.string()).min(1).describe('Tool names as find_tool reports them, e.g. [\"pdf_redact\",\"ocr\"], or [\"all\"] for the full list') },
88
+ run(async (a) => {
89
+ const wanted = a.names.includes('all') ? Object.keys(TOOLS) : a.names;
90
+ const enabled = [], unknown = [];
91
+ for (const n of wanted) (TOOLS[n] ? (TOOLS[n].enable(), enabled.push(n)) : unknown.push(n));
92
+ return ok({ enabled, unknown, hint: unknown.length ? 'Unknown names: call find_tool for the right tool name.' : undefined },
93
+ `${enabled.length} tool(s) are now available in this session.`);
94
+ }));
95
+
96
+ tool('get_access',
97
+ 'Get a bigapi API key for this machine. Call this once when no key is configured; other bigapi tools fail with \"no API key\" until you do. Creates a NEW free key (no signup, no credit card) and stores it in the local config file, where every bigapi tool picks it up. Calling it again creates an additional key rather than returning the existing one \u2013 use get_balance to check the key you already have. The new key includes 100 free operations, then $0.01 per operation from a prepaid balance; neither expires. Needs network access to api.bigapi.dev.',
98
+ { name: z.string().optional().describe('Label stored with the key so you can tell keys apart later, e.g. \"claude-desktop\". Cosmetic only.') },
46
99
  run(async ({ name }) => {
47
100
  const existing = await getKey();
48
101
  if (existing) return ok({ status: 'already_configured', config: configPath(), hint: 'Use get_balance to see credit, or force_new=true is not supported – revoke via console.' }, 'A key is already configured.');
@@ -53,13 +106,13 @@ server.tool('get_access',
53
106
  'Key created and stored. Keep the upgrade_url – it is where credit is added when the free operations run out.');
54
107
  }));
55
108
 
56
- server.tool('get_balance',
57
- 'Check the configured bigapi key: remaining credit, free operations left, monthly cap and spend this month. Free and read-only call before large batch jobs or when an operation reports low balance.',
109
+ tool('get_balance',
110
+ 'Check the bigapi key that is currently configured: remaining credit, free operations left, monthly cap and spend so far this month. Read-only, free, and a snapshot of this moment \u2013 the numbers move as operations run. Use it to confirm a key works, before a large batch, or when an operation reports a low balance. It does not create keys (that is get_access) and does not list past operations.',
58
111
  {}, run(async () => ok(await apiJson('GET', '/v1/balance'))));
59
112
 
60
- server.tool('get_usage', "This month's bigapi operations and their cost, grouped by operation type. Free and read-only – useful for cost reporting and audits.", {}, run(async () => ok(await apiJson('GET', '/v1/usage'))));
113
+ tool('get_usage', "This month's bigapi operations and their cost, grouped by operation type. Free and read-only – useful for cost reporting and audits.", {}, run(async () => ok(await apiJson('GET', '/v1/usage'))));
61
114
 
62
- server.tool('set_monthly_cap',
115
+ tool('set_monthly_cap',
63
116
  'Set the monthly spending cap of the configured bigapi key in US cents (default 1000 = $10). Raise it before large batch jobs (e.g. 5000 = $50); lower it to protect against runaway loops. Applies from the next operation.',
64
117
  { monthly_cap_cents: z.number().int().min(0).max(1_000_000) },
65
118
  run(async ({ monthly_cap_cents }) => {
@@ -67,10 +120,10 @@ server.tool('set_monthly_cap',
67
120
  return ok(await apiJson('PATCH', `/v1/keys/${bal.key.id}`, { monthly_cap_cents }));
68
121
  }));
69
122
 
70
- server.tool('get_pricing', 'Machine-readable price list of bigapi.dev: every available operation with its price in US cents. Free, no key required.', {}, run(async () => ok(await apiJson('GET', '/v1/pricing', null, { auth: false }))));
123
+ tool('get_pricing', 'Machine-readable price list of bigapi.dev: every available operation with its price in US cents. Free, no key required.', {}, run(async () => ok(await apiJson('GET', '/v1/pricing', null, { auth: false }))));
71
124
 
72
125
  // ---- Render -------------------------------------------------------------------
73
- server.tool('render',
126
+ tool('render',
74
127
  'Render HTML, Markdown or a public URL into a pixel-perfect PDF (default) or PNG via server-side Chromium – the reliable way to produce polished documents (reports, invoices, offers, letters, documentation) without a local browser or PDF library. Full CSS, page formats A4/A3/Letter/Legal, optional page-number footer. Returns the local output path. $0.01.',
75
128
  {
76
129
  markdown: z.string().optional().describe('Markdown source (a clean print stylesheet is applied)'),
@@ -99,29 +152,29 @@ server.tool('render',
99
152
  }));
100
153
 
101
154
  // ---- PDF ------------------------------------------------------------------------
102
- server.tool('pdf_merge', 'Merge two or more PDF files (local paths, kept in the given order) into a single PDF – e.g. combine chapters, append attachments to an invoice, or assemble a report from parts. $0.01.',
155
+ tool('pdf_merge', 'Merge two or more PDF files (local paths, kept in the given order) into a single PDF – e.g. combine chapters, append attachments to an invoice, or assemble a report from parts. $0.01.',
103
156
  { files: z.array(z.string()).min(2).describe('Local PDF paths in order'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
104
157
  run(async (a) => ok(await opFiles('/v1/pdf/merge', a.files.map(f => ['files', f]), {}, a.output_path, a.idempotency_key), 'Merged.')));
105
158
 
106
- server.tool('pdf_split', 'Extract pages from a PDF into a new PDF. Page expression like "1-3,7,9-z" (z = last page) – e.g. "1" for the first page only, "2-z" to drop a cover sheet. $0.01.',
159
+ tool('pdf_split', 'Extract pages from a PDF into a new PDF. Page expression like "1-3,7,9-z" (z = last page) – e.g. "1" for the first page only, "2-z" to drop a cover sheet. $0.01.',
107
160
  { file: z.string(), pages: z.string().default('1-z'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
108
161
  run(async (a) => ok(await opFiles('/v1/pdf/split', [['file', a.file]], { pages: a.pages }, a.output_path, a.idempotency_key), 'Split.')));
109
162
 
110
- server.tool('pdf_rotate', 'Rotate PDF pages by 90, 180 or 270 degrees – e.g. to fix sideways or upside-down scans. All pages by default, or a range like "2-4". $0.01.',
163
+ tool('pdf_rotate', 'Rotate PDF pages by 90, 180 or 270 degrees – e.g. to fix sideways or upside-down scans. All pages by default, or a range like "2-4". $0.01.',
111
164
  { file: z.string(), angle: z.enum(['90', '180', '270']).default('90'), pages: z.string().default('1-z'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
112
165
  run(async (a) => ok(await opFiles('/v1/pdf/rotate', [['file', a.file]], { angle: a.angle, pages: a.pages }, a.output_path, a.idempotency_key), 'Rotated.')));
113
166
 
114
- server.tool('pdf_compress', "Shrink a PDF's file size, e.g. to fit e-mail attachment limits. Levels: screen (smallest), ebook (default, good for sharing), printer, prepress (largest, best quality). $0.01.",
167
+ tool('pdf_compress', "Shrink a PDF's file size, e.g. to fit e-mail attachment limits. Levels: screen (smallest), ebook (default, good for sharing), printer, prepress (largest, best quality). $0.01.",
115
168
  { file: z.string(), level: z.enum(['screen', 'ebook', 'printer', 'prepress']).default('ebook'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
116
169
  run(async (a) => ok(await opFiles('/v1/pdf/compress', [['file', a.file]], { level: a.level }, a.output_path, a.idempotency_key), 'Compressed.')));
117
170
 
118
- server.tool('pdf_to_images', 'Render PDF pages as JPEG (default) or PNG images – the standard way to let a vision model look at a PDF, or to create page previews/thumbnails. Choose dpi (150 default, 300 for fine detail) and a page range. Single page → image file, multiple pages → ZIP. $0.01.',
171
+ tool('pdf_to_images', 'Render PDF pages as JPEG (default) or PNG images – the standard way to let a vision model look at a PDF, or to create page previews/thumbnails. Choose dpi (150 default, 300 for fine detail) and a page range. Single page → image file, multiple pages → ZIP. $0.01.',
119
172
  { file: z.string(), dpi: z.number().int().min(36).max(600).default(150), first_page: z.number().int().min(1).optional(), last_page: z.number().int().min(1).optional(),
120
173
  format: z.enum(['jpeg', 'png']).default('jpeg'), quality: z.number().int().min(30).max(100).default(85), output_path: z.string().optional(), idempotency_key: z.string().optional() },
121
174
  run(async (a) => ok(await opFiles('/v1/pdf/pages', [['file', a.file]], { dpi: String(a.dpi), first: a.first_page && String(a.first_page), last: a.last_page && String(a.last_page), format: a.format, quality: String(a.quality) }, a.output_path, a.idempotency_key), 'Pages rendered.')));
122
175
 
123
176
  // ---- Welle 1a: Screenshot · OCR · PDF/A -----------------------------------------
124
- server.tool('screenshot',
177
+ tool('screenshot',
125
178
  'Screenshot any public URL with real device presets (desktop, laptop, tablet, mobile), full page by default – for visual checks, monitoring, documentation, or archiving a page exactly as a browser sees it. Optional delay for late-loading content. Returns the local output path. $0.01.',
126
179
  { url: z.string().url(), device: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).default('desktop'),
127
180
  full_page: z.boolean().default(true), format: z.enum(['png', 'jpeg']).default('png'),
@@ -132,7 +185,7 @@ server.tool('screenshot',
132
185
  { url: a.url, device: a.device, fullPage: a.full_page, format: a.format, quality: a.quality, delayMs: a.delay_ms },
133
186
  a.output_path, a.idempotency_key), 'Screenshot taken.')));
134
187
 
135
- server.tool('ocr',
188
+ tool('ocr',
136
189
  'Turn a scanned PDF or a photo of a document (local path) into a searchable PDF (default), plain text, or per-page JSON. Use whenever a PDF has no extractable text layer. Languages as tesseract codes, e.g. "deu", "eng", "deu+eng". $0.01 PER PAGE.',
137
190
  { file: z.string(), lang: z.string().default('deu+eng'),
138
191
  output: z.enum(['pdf', 'text', 'json']).default('pdf'),
@@ -140,18 +193,18 @@ server.tool('ocr',
140
193
  output_path: z.string().optional(), idempotency_key: z.string().optional() },
141
194
  run(async (a) => ok(await opFiles('/v1/ocr', [['file', a.file]], { lang: a.lang, output: a.output, dpi: String(a.dpi) }, a.output_path, a.idempotency_key), 'OCR done.')));
142
195
 
143
- server.tool('pdf_to_pdfa',
196
+ tool('pdf_to_pdfa',
144
197
  'Convert a PDF (local path) to archival PDF/A-2b with embedded fonts – required for long-term storage and legal/tax compliance workflows. $0.01.',
145
198
  { file: z.string(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
146
199
  run(async (a) => ok(await opFiles('/v1/pdf/pdfa', [['file', a.file]], {}, a.output_path, a.idempotency_key), 'Converted to PDF/A.')));
147
200
 
148
- server.tool('office_to_pdf',
201
+ tool('office_to_pdf',
149
202
  'Convert an Office document (local path: DOCX, DOC, XLSX, XLS, PPTX, PPT, ODT, ODS, ODP, RTF, CSV, TXT) to PDF via server-side LibreOffice – no Office installation needed anywhere. $0.01 PER PAGE.',
150
203
  { file: z.string(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
151
204
  run(async (a) => ok(await opFiles('/v1/office/pdf', [['file', a.file]], {}, a.output_path, a.idempotency_key), 'Converted to PDF.')));
152
205
 
153
206
  // ---- Welle 1b: Extraktion --------------------------------------------------------
154
- server.tool('pdf_to_markdown',
207
+ tool('pdf_to_markdown',
155
208
  'Extract the text of a PDF (local path) as clean Markdown: paragraphs reflowed, hyphenation resolved, pages separated by rules. The standard way to read a text-based PDF for summarising, RAG ingestion or further processing. Scanned PDFs need ocr first. $0.01 PER PAGE.',
156
209
  { file: z.string(), first_page: z.number().int().min(1).optional(), last_page: z.number().int().min(1).optional(),
157
210
  layout: z.boolean().default(false).describe('Keep column layout instead of reflowing paragraphs'),
@@ -161,7 +214,7 @@ server.tool('pdf_to_markdown',
161
214
  { first: a.first_page && String(a.first_page), last: a.last_page && String(a.last_page),
162
215
  layout: a.layout ? 'true' : undefined, output: a.output }, a.output_path, a.idempotency_key), 'Text extracted.')));
163
216
 
164
- server.tool('pdf_extract_tables',
217
+ tool('pdf_extract_tables',
165
218
  'Find tables in a text-based PDF (local path) and return them as JSON rows (default) or CSV – works on invoices, reports, bank statements. $0.01 PER PAGE.',
166
219
  { file: z.string(), first_page: z.number().int().min(1).optional(), last_page: z.number().int().min(1).optional(),
167
220
  output: z.enum(['json', 'csv']).default('json'),
@@ -171,12 +224,12 @@ server.tool('pdf_extract_tables',
171
224
  { first: a.first_page && String(a.first_page), last: a.last_page && String(a.last_page),
172
225
  output: a.output, minCols: a.min_cols && String(a.min_cols) }, a.output_path, a.idempotency_key), 'Tables extracted.')));
173
226
 
174
- server.tool('pdf_info',
227
+ tool('pdf_info',
175
228
  "Read a PDF's metadata as JSON (local path): page count, title, author, PDF version, page size, encryption and form flags – a cheap first check before more expensive processing. $0.01.",
176
229
  { file: z.string() },
177
230
  run(async (a) => ok(await opFiles('/v1/pdf/info', [['file', a.file]]), 'PDF inspected.')));
178
231
 
179
- server.tool('url_to_markdown',
232
+ tool('url_to_markdown',
180
233
  'Fetch a public web page with a real browser (JavaScript included) and return it as GitHub-flavoured Markdown with absolute links and tables – for reading, summarising or archiving pages as text. $0.01.',
181
234
  { url: z.string().url(), selector: z.string().optional().describe('CSS selector to extract only part of the page'),
182
235
  include_title: z.boolean().default(true).describe('Prepend the page title as an H1'),
@@ -188,13 +241,13 @@ server.tool('url_to_markdown',
188
241
  { url: a.url, selector: a.selector, includeTitle: a.include_title, waitUntil: a.wait_until, delayMs: a.delay_ms, output: a.output },
189
242
  a.output_path, a.idempotency_key), 'Page converted.')));
190
243
 
191
- server.tool('md_to_docx',
244
+ tool('md_to_docx',
192
245
  'Turn Markdown – e.g. an answer you just wrote – into a formatted Word document (.docx): headings, lists, tables, bold/italic and links all carry over. Returns the local output path. $0.01.',
193
246
  { markdown: z.string(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
194
247
  run(async (a) => ok(await opJson('/v1/md/to-docx', { markdown: a.markdown }, a.output_path, a.idempotency_key), 'Word file created.')));
195
248
 
196
249
  // ---- Welle 2-4 + RAG (0.5.0) -----------------------------------------------------
197
- server.tool('text_chunk',
250
+ tool('text_chunk',
198
251
  'Split text or Markdown into RAG-ready chunks: token-based sizing, heading-aware boundaries, optional overlap, heading path and page metadata per chunk. The standard preprocessing step before embedding into a vector DB. $0.01.',
199
252
  { text: z.string().describe('Text or Markdown to chunk'),
200
253
  max_tokens: z.number().int().min(50).max(8000).default(512), overlap: z.number().int().min(0).default(0),
@@ -202,32 +255,32 @@ server.tool('text_chunk',
202
255
  run(async (a) => ok(await opJson('/v1/text/chunk',
203
256
  { text: a.text, maxTokens: a.max_tokens, overlap: a.overlap, splitOn: a.split_on }, undefined, a.idempotency_key), 'Chunked.')));
204
257
 
205
- server.tool('docx_to_markdown',
258
+ tool('docx_to_markdown',
206
259
  'Extract a Word document (.docx, local path) as clean Markdown – headings, lists and tables preserved. $0.01.',
207
260
  { file: z.string(), output: z.enum(['md', 'json']).default('json'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
208
261
  run(async (a) => ok(await opFiles('/v1/docx/to-markdown', [['file', a.file]], { output: a.output }, a.output_path, a.idempotency_key), 'Extracted.')));
209
262
 
210
- server.tool('xlsx_to_markdown',
263
+ tool('xlsx_to_markdown',
211
264
  'Extract a spreadsheet (.xlsx, local path) as Markdown tables, one section per sheet. $0.01.',
212
265
  { file: z.string(), max_rows: z.number().int().optional(), output: z.enum(['md', 'json']).default('json'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
213
266
  run(async (a) => ok(await opFiles('/v1/xlsx/to-markdown', [['file', a.file]], { maxRows: a.max_rows && String(a.max_rows), output: a.output }, a.output_path, a.idempotency_key), 'Extracted.')));
214
267
 
215
- server.tool('pptx_to_markdown',
268
+ tool('pptx_to_markdown',
216
269
  'Extract a presentation (.pptx, local path) as Markdown – one section per slide, bullets and speaker notes included. $0.01.',
217
270
  { file: z.string(), include_notes: z.boolean().default(true), output: z.enum(['md', 'json']).default('json'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
218
271
  run(async (a) => ok(await opFiles('/v1/pptx/to-markdown', [['file', a.file]], { includeNotes: a.include_notes ? 'true' : 'false', output: a.output }, a.output_path, a.idempotency_key), 'Extracted.')));
219
272
 
220
- server.tool('epub_to_markdown',
273
+ tool('epub_to_markdown',
221
274
  'Extract an e-book (.epub, local path) as clean Markdown for reading, summarising or RAG ingestion. $0.01.',
222
275
  { file: z.string(), output: z.enum(['md', 'json']).default('json'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
223
276
  run(async (a) => ok(await opFiles('/v1/epub/to-markdown', [['file', a.file]], { output: a.output }, a.output_path, a.idempotency_key), 'Extracted.')));
224
277
 
225
- server.tool('pdf_outline',
278
+ tool('pdf_outline',
226
279
  "Read a PDF's bookmark/chapter outline as JSON with target pages – chapter boundaries for navigation or chunking. $0.01.",
227
280
  { file: z.string() },
228
281
  run(async (a) => ok(await opFiles('/v1/pdf/outline', [['file', a.file]]), 'Outline read.')));
229
282
 
230
- server.tool('pdf_protect',
283
+ tool('pdf_protect',
231
284
  'Password-protect a PDF (local path) with AES-256 encryption; control print/modify/copy permissions. $0.01.',
232
285
  { file: z.string(), password: z.string().min(4), owner_password: z.string().optional(),
233
286
  allow_print: z.boolean().default(true), allow_modify: z.boolean().default(false), allow_copy: z.boolean().default(true),
@@ -236,12 +289,12 @@ server.tool('pdf_protect',
236
289
  { password: a.password, ownerPassword: a.owner_password, allowPrint: String(a.allow_print), allowModify: String(a.allow_modify), allowCopy: String(a.allow_copy) },
237
290
  a.output_path, a.idempotency_key), 'Protected.')));
238
291
 
239
- server.tool('pdf_unlock',
292
+ tool('pdf_unlock',
240
293
  'Remove password protection from a PDF (local path) – requires the correct password. $0.01.',
241
294
  { file: z.string(), password: z.string(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
242
295
  run(async (a) => ok(await opFiles('/v1/pdf/unlock', [['file', a.file]], { password: a.password }, a.output_path, a.idempotency_key), 'Unlocked.')));
243
296
 
244
- server.tool('pdf_compare',
297
+ tool('pdf_compare',
245
298
  'Visually compare two PDFs (local paths) page by page: change percentage per page as JSON, or a diff PDF with changes highlighted in red. $0.01 PER PAGE.',
246
299
  { file_a: z.string(), file_b: z.string(), dpi: z.number().int().min(50).max(200).default(100),
247
300
  threshold: z.number().int().min(0).max(64).default(12), output: z.enum(['json', 'pdf']).default('json'),
@@ -249,7 +302,7 @@ server.tool('pdf_compare',
249
302
  run(async (a) => ok(await opFiles('/v1/pdf/compare', [['file', a.file_a], ['file', a.file_b]],
250
303
  { dpi: String(a.dpi), threshold: String(a.threshold), output: a.output }, a.output_path, a.idempotency_key), 'Compared.')));
251
304
 
252
- server.tool('pdf_redact',
305
+ tool('pdf_redact',
253
306
  'Black out terms in a PDF (local path) with GUARANTEED removal: pages are rasterised, matches covered, rebuilt as an image PDF – the text is provably gone. The result has no text layer (run ocr afterwards if needed). $0.01 PER PAGE.',
254
307
  { file: z.string(), terms: z.array(z.string()).min(1).max(100).describe('Terms to remove'),
255
308
  dpi: z.number().int().min(72).max(300).default(150), padding: z.number().min(0).max(20).default(2),
@@ -257,44 +310,44 @@ server.tool('pdf_redact',
257
310
  run(async (a) => ok(await opFiles('/v1/pdf/redact', [['file', a.file]],
258
311
  { terms: JSON.stringify(a.terms), dpi: String(a.dpi), padding: String(a.padding) }, a.output_path, a.idempotency_key), 'Redacted.')));
259
312
 
260
- server.tool('pdf_verify_signature',
313
+ tool('pdf_verify_signature',
261
314
  'Check digital signatures of a PDF (local path): who signed (certificate details), when, and whether the document is unchanged since signing. Integrity check without CA trust-chain validation. $0.01.',
262
315
  { file: z.string() },
263
316
  run(async (a) => ok(await opFiles('/v1/pdf/verify-signature', [['file', a.file]]), 'Signature checked.')));
264
317
 
265
- server.tool('email_to_pdf',
318
+ tool('email_to_pdf',
266
319
  'Archive an email (.eml, local path) as a clean PDF: header table, body, inline images, attachment list. $0.01.',
267
320
  { file: z.string(), page_format: z.enum(['A4', 'Letter']).default('A4'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
268
321
  run(async (a) => ok(await opFiles('/v1/email/to-pdf', [['file', a.file]], { pageFormat: a.page_format }, a.output_path, a.idempotency_key), 'Email archived.')));
269
322
 
270
- server.tool('template_render',
323
+ tool('template_render',
271
324
  'Render a Handlebars template with JSON data into a finished PDF, PNG or HTML – for invoices, reports, certificates. German number/date helpers included (formatNumber, formatDate). $0.01.',
272
325
  { template: z.string().describe('Handlebars/HTML template'), data: z.record(z.any()).optional(),
273
326
  format: z.enum(['pdf', 'png', 'html']).default('pdf'), output_path: z.string().optional(), idempotency_key: z.string().optional() },
274
327
  run(async (a) => ok(await opJson('/v1/template/render', { template: a.template, data: a.data, format: a.format }, a.output_path, a.idempotency_key), 'Rendered.')));
275
328
 
276
- server.tool('chart_render',
329
+ tool('chart_render',
277
330
  'Render a Chart.js configuration into a finished chart PNG, server-side – bar, line, pie, radar and all other Chart.js types. $0.01.',
278
331
  { config: z.record(z.any()).describe('Chart.js config: {type, data, options}'),
279
332
  width: z.number().int().min(200).max(3000).default(900), height: z.number().int().min(150).max(3000).default(500),
280
333
  background: z.string().optional(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
281
334
  run(async (a) => ok(await opJson('/v1/chart', { config: a.config, width: a.width, height: a.height, background: a.background }, a.output_path, a.idempotency_key), 'Chart rendered.')));
282
335
 
283
- server.tool('qr_code',
336
+ tool('qr_code',
284
337
  'Generate a QR code from text or a URL as PNG or SVG, with size, colours and error-correction level. $0.01.',
285
338
  { text: z.string().max(4000), format: z.enum(['png', 'svg']).default('png'),
286
339
  size: z.number().int().min(64).max(2000).default(512), ec_level: z.enum(['L', 'M', 'Q', 'H']).default('M'),
287
340
  dark: z.string().optional(), light: z.string().optional(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
288
341
  run(async (a) => ok(await opJson('/v1/qr', { text: a.text, format: a.format, size: a.size, ecLevel: a.ec_level, dark: a.dark, light: a.light }, a.output_path, a.idempotency_key), 'QR generated.')));
289
342
 
290
- server.tool('image_to_pdf',
343
+ tool('image_to_pdf',
291
344
  'Combine one or more images (local paths, JPEG/PNG/WebP/…) into a single PDF – auto page size or fitted to A4/Letter, EXIF rotation applied. $0.01.',
292
345
  { files: z.array(z.string()).min(1).max(200), page_size: z.enum(['auto', 'a4', 'letter']).default('auto'),
293
346
  margin: z.number().min(0).max(40).default(0), output_path: z.string().optional(), idempotency_key: z.string().optional() },
294
347
  run(async (a) => ok(await opFiles('/v1/image/to-pdf', a.files.map(f => ['file', f]),
295
348
  { pageSize: a.page_size, margin: String(a.margin) }, a.output_path, a.idempotency_key), 'PDF created.')));
296
349
 
297
- server.tool('image_c2pa_sign',
350
+ tool('image_c2pa_sign',
298
351
  'Embed C2PA Content Credentials into an image (local path, JPEG/PNG/WebP) marking it as AI-generated – EU AI Act Art. 50 compliance. Signs with the BiGapi certificate. $0.01.',
299
352
  { file: z.string(), title: z.string().optional(), ai_generated: z.boolean().default(true),
300
353
  generator: z.string().optional().describe('Name of the generating software'),
@@ -302,12 +355,12 @@ server.tool('image_c2pa_sign',
302
355
  run(async (a) => ok(await opFiles('/v1/image/c2pa/sign', [['file', a.file]],
303
356
  { title: a.title, aiGenerated: String(a.ai_generated), generator: a.generator }, a.output_path, a.idempotency_key), 'Signed.')));
304
357
 
305
- server.tool('image_c2pa_verify',
358
+ tool('image_c2pa_verify',
306
359
  'Read and validate C2PA Content Credentials of an image (local path): who signed, which generator, is it marked AI-generated. $0.01.',
307
360
  { file: z.string() },
308
361
  run(async (a) => ok(await opFiles('/v1/image/c2pa/verify', [['file', a.file]]), 'Credentials checked.')));
309
362
 
310
- server.tool('image_ai_label',
363
+ tool('image_ai_label',
311
364
  'Stamp a visible "AI-generated" label onto an image (local path) and write it into the EXIF metadata – the fast bulk option for EU AI Act labelling. $0.01.',
312
365
  { file: z.string(), text: z.string().max(60).default('AI-generated'),
313
366
  position: z.enum(['bottom-right', 'bottom-left', 'top-right', 'top-left']).default('bottom-right'),
@@ -316,7 +369,7 @@ server.tool('image_ai_label',
316
369
  { text: a.text, position: a.position, format: a.format }, a.output_path, a.idempotency_key), 'Labelled.')));
317
370
 
318
371
  // ---- Welle 5 (0.7.0) -------------------------------------------------------------
319
- server.tool('pdf_attachments',
372
+ tool('pdf_attachments',
320
373
  'Pull embedded files out of a PDF (local path): ZUGFeRD/Factur-X e-invoice XML, attached CSVs, images or sub-PDFs. Returns JSON by default (text inline, binary base64, e-invoice attachments flagged) or a ZIP of everything. Use this before parsing an invoice PDF – the structured XML inside is far more reliable than reading the printed page. $0.01.',
321
374
  { file: z.string(),
322
375
  output: z.enum(['json', 'zip']).default('json').describe('json → attachment list with contents; zip → all attachments as one archive'),
@@ -325,7 +378,7 @@ server.tool('pdf_attachments',
325
378
  run(async (a) => ok(await opFiles('/v1/pdf/attachments', [['file', a.file]],
326
379
  { output: a.output, name: a.name }, a.output_path, a.idempotency_key), 'Attachments read.')));
327
380
 
328
- server.tool('pdf_sanitize',
381
+ tool('pdf_sanitize',
329
382
  'Strip the invisible parts of a PDF (local path) before handing it out: JavaScript, open-actions and auto-actions, form fields, annotations and embedded files. The file is rewritten from its reachable objects afterwards, so orphaned remains are gone too – deleting references alone leaves them readable in the byte stream. The counterpart to pdf_redact: redact removes visible text, sanitize removes hidden payload. $0.01.',
330
383
  { file: z.string(),
331
384
  flatten: z.boolean().default(true).describe('Flatten annotations and form fields into the page'),
@@ -336,12 +389,12 @@ server.tool('pdf_sanitize',
336
389
  { flatten: String(a.flatten), removeAttachments: String(a.remove_attachments), removeMetadata: String(a.remove_metadata) },
337
390
  a.output_path, a.idempotency_key), 'Sanitized.')));
338
391
 
339
- server.tool('pdf_linearize',
392
+ tool('pdf_linearize',
340
393
  'Optimise a PDF (local path) for fast web view: the file is restructured so a browser can show page one before the whole document has loaded. For document portals, archives and long reports. Content stays identical. $0.01.',
341
394
  { file: z.string(), output_path: z.string().optional(), idempotency_key: z.string().optional() },
342
395
  run(async (a) => ok(await opFiles('/v1/pdf/linearize', [['file', a.file]], {}, a.output_path, a.idempotency_key), 'Linearized.')));
343
396
 
344
- server.tool('html_to_markdown',
397
+ tool('html_to_markdown',
345
398
  'Turn HTML you already have into clean Markdown: navigation, headers, footers, sidebars, forms and scripts are stripped, the readable article remains. No browser, no network request, milliseconds. Use this when you hold the HTML (a saved page, an API response, a scraped body); use url_to_markdown when you only have a URL. $0.01.',
346
399
  { html: z.string().describe('Raw HTML source'),
347
400
  mode: z.enum(['article', 'full']).default('article').describe('article strips navigation; full keeps everything'),
@@ -353,7 +406,7 @@ server.tool('html_to_markdown',
353
406
  a.output_path, a.idempotency_key), 'Converted.')));
354
407
 
355
408
  // ---- Images --------------------------------------------------------------------
356
- server.tool('image_process', 'Resize, crop, rotate, convert (jpeg/png/webp/avif/tiff), compress, strip EXIF and/or text-watermark an image – several steps chained in one call, e.g. "resize to 1200px, convert to webp, quality 80". $0.01.',
409
+ tool('image_process', 'Resize, crop, rotate, convert (jpeg/png/webp/avif/tiff), compress, strip EXIF and/or text-watermark an image – several steps chained in one call, e.g. "resize to 1200px, convert to webp, quality 80". $0.01.',
357
410
  { file: z.string(),
358
411
  resize_width: z.number().int().min(1).max(10000).optional(), resize_height: z.number().int().min(1).max(10000).optional(),
359
412
  fit: z.enum(['inside', 'cover', 'contain', 'outside', 'fill']).default('inside'),
@@ -375,10 +428,13 @@ server.tool('image_process', 'Resize, crop, rotate, convert (jpeg/png/webp/avif/
375
428
  return ok(await opFiles('/v1/image', [['file', a.file]], { ops }, a.output_path, a.idempotency_key), 'Image processed.');
376
429
  }));
377
430
 
378
- server.tool('image_info', "Read an image's format, dimensions, color space and whether EXIF/ICC metadata is present – e.g. to decide processing steps or validate an upload. $0.01.",
431
+ tool('image_info', "Read an image's format, dimensions, color space and whether EXIF/ICC metadata is present – e.g. to decide processing steps or validate an upload. $0.01.",
379
432
  { file: z.string() }, run(async (a) => ok(await opFiles('/v1/image/info', [['file', a.file]]))));
380
433
 
381
434
  // ---- Start ---------------------------------------------------------------------
435
+ // Im schlanken Modus bleibt nur der Kern sichtbar; der Rest wartet auf enable_tools.
436
+ if (LEAN) for (const [name, t] of Object.entries(TOOLS)) if (!CORE.includes(name)) t.disable();
437
+
382
438
  const transport = new StdioServerTransport();
383
439
  await server.connect(transport);
384
440
  process.stderr.write(`bigapi MCP server ready (${BASE_URL})\n`);