uvpdftool-mcp 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UV ResearchLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # uvpdftool-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for the
4
+ [UV pdfTool API](https://uvpdftool.com/developers): an AI assistant such as Claude Desktop can
5
+ compress, convert, OCR and check the PDFs in the folders you allow, and tell you where it saved
6
+ the results. Node.js 18.3 or later; it speaks MCP over standard input and output.
7
+
8
+ ## Set it up
9
+
10
+ Make a key on the [API keys page](https://uvpdftool.com/developers/keys) (a test key is free and
11
+ marks its results), then add the server to the assistant's MCP settings:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "uvpdftool": {
17
+ "command": "npx",
18
+ "args": ["-y", "uvpdftool-mcp", "--root", "/Users/you/Documents/PDFs"],
19
+ "env": { "UVPDFTOOL_API_KEY": "uvp_test_..." }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ `--root` names a folder the server may read files from and save results in; give it more than
26
+ once for more folders, or set `UVPDFTOOL_MCP_ROOTS` (separated as `PATH` is). Without either, the
27
+ folder the server is started in is the only one, unless that is the whole disk or your home
28
+ folder, where some assistants start their servers: then it stops and asks for `--root`. Anything
29
+ else on its command line stops it too, so a mistyped option never widens what it may use.
30
+
31
+ A path is judged on its text before the disk is touched, so a path to another machine
32
+ (`\\host\share`) is refused without being opened; then on its real path, so neither `..` nor a
33
+ link leads out. A result is created only where nothing is, a link included, so it never replaces
34
+ or writes through anything.
35
+
36
+ ## What the assistant can do
37
+
38
+ | Tool | Does |
39
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------- |
40
+ | `list_pdf_tools` | The API's tools, the files each takes and the options each accepts, as JSON Schema. |
41
+ | `run_pdf_tool` | Uploads files from an allowed folder, runs one tool with its options, and saves the results; answers with paths. |
42
+ | `pdf_api_account` | The key in use and the API tasks left. |
43
+
44
+ ## What it will not do
45
+
46
+ - Read a file, or save a result, outside the folders allowed; a path that leads out of them, by
47
+ `..` or by a link, is refused.
48
+ - Write over a file: a result whose name is taken gets a number added, as `report-compressed (2).pdf`.
49
+ - Keep anything: files go to the API for the one job, and come back.
50
+
51
+ Each run is one API task, as any other call is. The assistant decides which files to send, so
52
+ allow a folder of the documents you mean it to work on, not your whole home folder, and use a
53
+ test key until you trust what it does.
54
+
55
+ The tools, their options and the errors are documented at <https://uvpdftool.com/developers>.
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from 'node:os';
3
+ import { createInterface } from 'node:readline';
4
+ import { UvPdfTool } from 'uvpdftool';
5
+ import { createServer, rootsFrom, SERVER_VERSION } from './server.js';
6
+ /**
7
+ * The server as a program for an MCP client: messages on standard input, answers on standard
8
+ * output, anything else on standard error. The key comes from UVPDFTOOL_API_KEY; the folders it
9
+ * may use from --root (repeatable), or UVPDFTOOL_MCP_ROOTS, or the folder it was started in
10
+ * (never the whole disk or the home folder).
11
+ */
12
+ const chosen = rootsFrom(process.argv.slice(2), process.env, process.cwd(), homedir());
13
+ if ('error' in chosen) {
14
+ process.stderr.write(`uvpdftool-mcp: ${chosen.error}\n`);
15
+ process.exit(2);
16
+ }
17
+ const { roots } = chosen;
18
+ const apiKey = process.env.UVPDFTOOL_API_KEY ?? '';
19
+ if (apiKey === '') {
20
+ process.stderr.write('uvpdftool-mcp: set UVPDFTOOL_API_KEY to an API key (https://uvpdftool.com/developers/keys).\n');
21
+ process.exit(2);
22
+ }
23
+ const server = await createServer({
24
+ client: new UvPdfTool({ apiKey, userAgent: `uvpdftool-mcp/${SERVER_VERSION}` }),
25
+ roots,
26
+ });
27
+ process.stderr.write(`uvpdftool-mcp ${SERVER_VERSION}: serving ${roots.join(', ')}\n`);
28
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
29
+ lines.on('line', (line) => {
30
+ void server.handleLine(line).then((reply) => {
31
+ if (reply !== null)
32
+ process.stdout.write(`${JSON.stringify(reply)}\n`);
33
+ });
34
+ });
@@ -0,0 +1,47 @@
1
+ import { type UvPdfTool } from 'uvpdftool';
2
+ /**
3
+ * A Model Context Protocol server for the UV pdfTool API, spoken as JSON-RPC 2.0, one message
4
+ * a line: an assistant lists the tools, runs one on files in the folders it was allowed, and
5
+ * gets back where the results were saved. Files outside those folders are never read, results
6
+ * never land outside them, and nothing is written over an existing file.
7
+ */
8
+ export declare const SERVER_VERSION = "0.1.0";
9
+ /** Newest first: the one the client asks for when it is one of these, the newest otherwise. */
10
+ export declare const PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26", "2024-11-05"];
11
+ type Id = string | number | null;
12
+ export type Reply = {
13
+ jsonrpc: '2.0';
14
+ id: Id;
15
+ result: unknown;
16
+ } | {
17
+ jsonrpc: '2.0';
18
+ id: Id;
19
+ error: {
20
+ code: number;
21
+ message: string;
22
+ };
23
+ };
24
+ export interface ServerOptions {
25
+ client: Pick<UvPdfTool, 'tools' | 'account' | 'run' | 'download'>;
26
+ /** The folders the server may read files from and save results in. */
27
+ roots: readonly string[];
28
+ }
29
+ /**
30
+ * The folders from the command line (--root, repeatable), else UVPDFTOOL_MCP_ROOTS, else the
31
+ * folder the server was started in, unless that is the whole disk or the home folder, where an
32
+ * assistant's app may start servers without saying so. Anything else on the line is an error,
33
+ * so that a mistyped option never widens what the server may use.
34
+ */
35
+ export declare function rootsFrom(args: readonly string[], env: Readonly<Record<string, string | undefined>>, cwd: string, home: string): {
36
+ roots: string[];
37
+ } | {
38
+ error: string;
39
+ };
40
+ export interface McpServer {
41
+ /** One line from the client, answered, or null where nothing is to be answered. */
42
+ handleLine: (line: string) => Promise<Reply | Reply[] | null>;
43
+ handle: (message: unknown) => Promise<Reply | null>;
44
+ }
45
+ /** The server, over these folders, which must exist; they are taken as their real paths. */
46
+ export declare function createServer(options: ServerOptions): Promise<McpServer>;
47
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,308 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { mkdir, realpath, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { Readable } from 'node:stream';
5
+ import { ApiError, contentTypeOf, JobFailedError, safeName, saveResult, } from 'uvpdftool';
6
+ /**
7
+ * A Model Context Protocol server for the UV pdfTool API, spoken as JSON-RPC 2.0, one message
8
+ * a line: an assistant lists the tools, runs one on files in the folders it was allowed, and
9
+ * gets back where the results were saved. Files outside those folders are never read, results
10
+ * never land outside them, and nothing is written over an existing file.
11
+ */
12
+ export const SERVER_VERSION = '0.1.0';
13
+ /** Newest first: the one the client asks for when it is one of these, the newest otherwise. */
14
+ export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
15
+ const ERRORS = {
16
+ parse: -32700,
17
+ invalidRequest: -32600,
18
+ methodNotFound: -32601,
19
+ invalidParams: -32602,
20
+ internal: -32603,
21
+ };
22
+ /** What a tool call says went wrong, back to the assistant as the call's result. */
23
+ class ToolError extends Error {
24
+ }
25
+ const TOOLS = [
26
+ {
27
+ name: 'list_pdf_tools',
28
+ title: 'List the UV pdfTool tools',
29
+ description: 'The tools the UV pdfTool API offers (compress, office-to-pdf, pdf-to-office, ocr, pdf-to-pdfa, pdfa-check, web-to-pdf and more), the files each takes, and the options each accepts as JSON Schema. Call it before run_pdf_tool to know a tool’s options.',
30
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
31
+ annotations: { readOnlyHint: true, openWorldHint: true },
32
+ },
33
+ {
34
+ name: 'run_pdf_tool',
35
+ title: 'Run a UV pdfTool tool on local files',
36
+ description: 'Uploads local files to the UV pdfTool API, runs one tool on them with the options given, and saves the results in a folder; answers with the paths saved. Each run is one API task. The files and the folder must be inside the folders this server was allowed; a result never replaces an existing file, a number is added to its name instead.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ tool: {
41
+ type: 'string',
42
+ description: 'The tool, as list_pdf_tools names it: for example "compress".',
43
+ },
44
+ files: {
45
+ type: 'array',
46
+ items: { type: 'string' },
47
+ description: 'Paths of the files to send: absolute, or relative to the first allowed folder. None for a tool that takes no files, such as web-to-pdf.',
48
+ },
49
+ options: {
50
+ type: 'object',
51
+ description: 'The tool’s options, as list_pdf_tools describes them.',
52
+ },
53
+ outputDir: {
54
+ type: 'string',
55
+ description: 'Where to save the results; the folder of the first file when not given, or the first allowed folder.',
56
+ },
57
+ },
58
+ required: ['tool'],
59
+ additionalProperties: false,
60
+ },
61
+ annotations: {
62
+ readOnlyHint: false,
63
+ destructiveHint: false,
64
+ idempotentHint: false,
65
+ openWorldHint: true,
66
+ },
67
+ },
68
+ {
69
+ name: 'pdf_api_account',
70
+ title: 'The UV pdfTool API account',
71
+ description: 'The API key in use (its name, and live or test) and the API tasks left.',
72
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
73
+ annotations: { readOnlyHint: true, openWorldHint: true },
74
+ },
75
+ ];
76
+ const within = (root, target) => {
77
+ const relative = path.relative(root, target);
78
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
79
+ };
80
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
81
+ /**
82
+ * The folders from the command line (--root, repeatable), else UVPDFTOOL_MCP_ROOTS, else the
83
+ * folder the server was started in, unless that is the whole disk or the home folder, where an
84
+ * assistant's app may start servers without saying so. Anything else on the line is an error,
85
+ * so that a mistyped option never widens what the server may use.
86
+ */
87
+ export function rootsFrom(args, env, cwd, home) {
88
+ const roots = [];
89
+ for (let index = 0; index < args.length; index += 1) {
90
+ const arg = args[index] ?? '';
91
+ if (arg === '--root') {
92
+ const value = args[index + 1];
93
+ if (!value)
94
+ return { error: '--root needs a folder.' };
95
+ roots.push(value);
96
+ index += 1;
97
+ }
98
+ else if (arg.startsWith('--root=') && arg.length > '--root='.length) {
99
+ roots.push(arg.slice('--root='.length));
100
+ }
101
+ else {
102
+ return { error: `unknown argument ${arg}; give the folders with --root <folder>.` };
103
+ }
104
+ }
105
+ if (roots.length === 0 && env.UVPDFTOOL_MCP_ROOTS) {
106
+ roots.push(...env.UVPDFTOOL_MCP_ROOTS.split(path.delimiter).filter(Boolean));
107
+ }
108
+ if (roots.length > 0)
109
+ return { roots };
110
+ const here = path.resolve(cwd);
111
+ if (here === path.parse(here).root || path.relative(path.resolve(home), here) === '') {
112
+ return {
113
+ error: `started in ${here}, too wide to serve; give the folders with --root <folder>.`,
114
+ };
115
+ }
116
+ return { roots: [here] };
117
+ }
118
+ /** The server, over these folders, which must exist; they are taken as their real paths. */
119
+ export async function createServer(options) {
120
+ if (options.roots.length === 0)
121
+ throw new Error('At least one folder is needed.');
122
+ const roots = [];
123
+ for (const root of options.roots)
124
+ roots.push(await realpath(path.resolve(root)));
125
+ const [first] = roots;
126
+ const { client } = options;
127
+ // The folders as given too: a path written through a link to a folder is inside although its
128
+ // text is not the folder's real path.
129
+ const named = options.roots.map((root) => path.resolve(root));
130
+ const inside = (target) => [...roots, ...named].some((root) => within(root, target));
131
+ // Judged on the text before anything on disk is touched: opening a path to another machine
132
+ // (\\host\share) would send the user's Windows sign-in to it, and an answer of "no such
133
+ // file" would tell what exists outside the folders.
134
+ const outside = (given) => new ToolError(`${given} is outside the folders this server may use.`);
135
+ /** A file the assistant named, as its real path, if it is inside a folder allowed. */
136
+ const allowedFile = async (given) => {
137
+ const wanted = path.resolve(first, given);
138
+ if (!inside(wanted))
139
+ throw outside(given);
140
+ const real = await realpath(wanted).catch(() => null);
141
+ if (real === null)
142
+ throw new ToolError(`${given}: no such file.`);
143
+ if (!roots.some((root) => within(root, real)))
144
+ throw outside(given);
145
+ if (!(await stat(real)).isFile())
146
+ throw new ToolError(`${given} is not a file.`);
147
+ return real;
148
+ };
149
+ /** A folder to save in, made if need be, if it is inside a folder allowed. */
150
+ const allowedFolder = async (given) => {
151
+ const wanted = path.resolve(first, given);
152
+ if (!inside(wanted))
153
+ throw outside(given);
154
+ // The nearest part that exists, as its real path, decides where the rest would land.
155
+ let existing = wanted;
156
+ let rest = '';
157
+ for (;;) {
158
+ const real = await realpath(existing).catch(() => null);
159
+ if (real !== null) {
160
+ const target = path.join(real, rest);
161
+ if (!roots.some((root) => within(root, target)))
162
+ throw outside(given);
163
+ await mkdir(target, { recursive: true });
164
+ return target;
165
+ }
166
+ const parent = path.dirname(existing);
167
+ if (parent === existing)
168
+ throw new ToolError(`${given} cannot be made.`);
169
+ rest = path.join(path.basename(existing), rest);
170
+ existing = parent;
171
+ }
172
+ };
173
+ const runTool = async (args) => {
174
+ const { tool, files, options: toolOptions, outputDir } = args;
175
+ if (typeof tool !== 'string' || !/^[a-z0-9-]{1,40}$/u.test(tool)) {
176
+ throw new ToolError('Name the tool, as list_pdf_tools does.');
177
+ }
178
+ const given = files ?? [];
179
+ if (!Array.isArray(given) || !given.every((f) => typeof f === 'string')) {
180
+ throw new ToolError('Give the files as a list of paths.');
181
+ }
182
+ if (toolOptions !== undefined &&
183
+ (typeof toolOptions !== 'object' || toolOptions === null || Array.isArray(toolOptions))) {
184
+ throw new ToolError('The options are an object, as list_pdf_tools describes them.');
185
+ }
186
+ if (outputDir !== undefined && typeof outputDir !== 'string') {
187
+ throw new ToolError('outputDir is a folder path.');
188
+ }
189
+ const paths = [];
190
+ for (const file of given)
191
+ paths.push(await allowedFile(file));
192
+ const [firstFile] = paths;
193
+ const folder = await allowedFolder(outputDir ?? (firstFile === undefined ? first : path.dirname(firstFile)));
194
+ const inputs = [];
195
+ for (const file of paths) {
196
+ inputs.push({
197
+ name: path.basename(file),
198
+ contentType: contentTypeOf(file),
199
+ size: (await stat(file)).size,
200
+ data: Readable.toWeb(createReadStream(file)),
201
+ });
202
+ }
203
+ const job = await client.run({
204
+ tool,
205
+ files: inputs,
206
+ ...(toolOptions ? { options: toolOptions } : {}),
207
+ });
208
+ const saved = [];
209
+ for (const [index, output] of job.outputs.entries()) {
210
+ const data = await client.download(output);
211
+ saved.push(await saveResult(folder, safeName(output.name, index), data, false));
212
+ }
213
+ return {
214
+ ...text(`${tool} finished${job.mode === 'test' ? ' (a test job: its results are marked)' : ''}. Saved:\n${saved.join('\n')}`),
215
+ structuredContent: { job: { id: job.id, tool: job.tool, mode: job.mode }, saved },
216
+ };
217
+ };
218
+ const callTool = async (name, args) => {
219
+ const given = (typeof args === 'object' && args !== null ? args : {});
220
+ try {
221
+ if (name === 'list_pdf_tools') {
222
+ const tools = await client.tools();
223
+ return { ...text(JSON.stringify(tools, null, 2)), structuredContent: { tools } };
224
+ }
225
+ if (name === 'pdf_api_account') {
226
+ const account = await client.account();
227
+ return { ...text(JSON.stringify(account, null, 2)), structuredContent: account };
228
+ }
229
+ return await runTool(given);
230
+ }
231
+ catch (error) {
232
+ // Failures of the call itself go back to the assistant as the result, so it can act on them.
233
+ const message = error instanceof ToolError || error instanceof ApiError || error instanceof JobFailedError
234
+ ? error.message
235
+ : `The call failed: ${error instanceof Error ? error.message : String(error)}`;
236
+ return { ...text(message), isError: true };
237
+ }
238
+ };
239
+ const handle = async (message) => {
240
+ if (typeof message !== 'object' || message === null || Array.isArray(message)) {
241
+ return {
242
+ jsonrpc: '2.0',
243
+ id: null,
244
+ error: { code: ERRORS.invalidRequest, message: 'Not a request.' },
245
+ };
246
+ }
247
+ const { id, method, params } = message;
248
+ // Notifications are not answered; nor are replies to requests this server never makes.
249
+ if (typeof method !== 'string')
250
+ return null;
251
+ if (id === undefined)
252
+ return null;
253
+ const ok = (result) => ({ jsonrpc: '2.0', id, result });
254
+ const fail = (code, text) => ({
255
+ jsonrpc: '2.0',
256
+ id,
257
+ error: { code, message: text },
258
+ });
259
+ const given = (typeof params === 'object' && params !== null ? params : {});
260
+ try {
261
+ switch (method) {
262
+ case 'initialize': {
263
+ const asked = given.protocolVersion;
264
+ const version = PROTOCOL_VERSIONS.find((each) => each === asked) ?? PROTOCOL_VERSIONS[0];
265
+ return ok({
266
+ protocolVersion: version,
267
+ capabilities: { tools: { listChanged: false } },
268
+ serverInfo: { name: 'uvpdftool-mcp', title: 'UV pdfTool', version: SERVER_VERSION },
269
+ instructions: 'Use list_pdf_tools to see the tools and their options, then run_pdf_tool with paths of files in the allowed folders. Results are saved beside the first file unless outputDir says otherwise.',
270
+ });
271
+ }
272
+ case 'ping':
273
+ return ok({});
274
+ case 'tools/list':
275
+ return ok({ tools: TOOLS });
276
+ case 'tools/call': {
277
+ const name = given.name;
278
+ if (!TOOLS.some((tool) => tool.name === name)) {
279
+ return fail(ERRORS.invalidParams, `There is no tool ${String(name)}.`);
280
+ }
281
+ return ok(await callTool(name, given.arguments));
282
+ }
283
+ default:
284
+ return fail(ERRORS.methodNotFound, `There is no method ${method}.`);
285
+ }
286
+ }
287
+ catch (error) {
288
+ return fail(ERRORS.internal, error instanceof Error ? error.message : String(error));
289
+ }
290
+ };
291
+ const handleLine = async (line) => {
292
+ if (line.trim() === '')
293
+ return null;
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(line);
297
+ }
298
+ catch {
299
+ return { jsonrpc: '2.0', id: null, error: { code: ERRORS.parse, message: 'Not JSON.' } };
300
+ }
301
+ if (Array.isArray(parsed)) {
302
+ const replies = (await Promise.all(parsed.map(handle))).filter((reply) => reply !== null);
303
+ return replies.length > 0 ? replies : null;
304
+ }
305
+ return handle(parsed);
306
+ };
307
+ return { handle, handleLine };
308
+ }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "uvpdftool-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "A Model Context Protocol server for the UV pdfTool API: let an AI assistant compress, convert, OCR and check the PDFs in your folders.",
5
+ "license": "MIT",
6
+ "author": "UV ResearchLabs",
7
+ "homepage": "https://uvpdftool.com/developers",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/uvresearchlab/UV-pdfTool",
11
+ "directory": "packages/mcp"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "pdf",
17
+ "compress",
18
+ "ocr",
19
+ "convert",
20
+ "uvpdftool"
21
+ ],
22
+ "type": "module",
23
+ "engines": {
24
+ "node": ">=18.3"
25
+ },
26
+ "bin": {
27
+ "uvpdftool-mcp": "./dist/main.js"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/server.d.ts",
32
+ "import": "./dist/server.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "types": "./dist/server.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "dependencies": {
43
+ "uvpdftool": "^0.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "^10.0.1",
47
+ "@types/node": "^24.13.6",
48
+ "eslint": "^10.11.0",
49
+ "typescript": "~6.0.3",
50
+ "typescript-eslint": "^8.70.0",
51
+ "vitest": "^4.1.12"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.build.json",
55
+ "test": "vitest run",
56
+ "lint": "eslint .",
57
+ "typecheck": "tsc -p tsconfig.json"
58
+ }
6
59
  }