autoform-mcp-server 1.6.0 → 1.7.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 CHANGED
@@ -38,13 +38,14 @@ Config file location:
38
38
  - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
39
39
  - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
40
40
 
41
- ## Tools (9)
41
+ ## Tools (11)
42
42
 
43
43
  ### PDF Analysis
44
44
  | Tool | Description |
45
45
  |------|-------------|
46
46
  | `autoform_get_pdf_info` | Get page count and dimensions of a PDF |
47
47
  | `autoform_detect_fields` | Detect AcroForm fields in interactive PDFs |
48
+ | `autoform_analyze_static_pdf` | Classify PDF type and suggest field positions for static PDFs |
48
49
 
49
50
  ### Fill Single PDF
50
51
  | Tool | Description |
@@ -55,6 +56,7 @@ Config file location:
55
56
  ### Batch Generation
56
57
  | Tool | Description |
57
58
  |------|-------------|
59
+ | `autoform_fill_batch_acroform` | Generate multiple PDFs from an AcroForm PDF + data array |
58
60
  | `autoform_fill_batch_at_coordinates` | Generate multiple PDFs from one template + data, using coordinates |
59
61
  | `autoform_generate_batch` | Generate multiple PDFs using a saved template + data array |
60
62
 
@@ -65,6 +67,20 @@ Config file location:
65
67
  | `autoform_import_template` | Import a template JSON exported from the AutoForm web app |
66
68
  | `autoform_list_templates` | List all saved templates |
67
69
 
70
+ ### Experimental Parameters (v1.7.0+)
71
+
72
+ All tools accept optional `_session_id` and `_mode` parameters for experimental instrumentation:
73
+
74
+ - `_session_id`: Groups related tool calls in `metrics.jsonl` (format: `<pdf_id>_<mode>_<rep>`)
75
+ - `_mode`: Labels the autonomy mode (`1_manual_web`, `2_hybrid`, `3a_acroform`, `3b_vision`)
76
+
77
+ ### Metrics (v1.7.0+)
78
+
79
+ Every tool call is logged to `~/.autoform-mcp/metrics.jsonl` with:
80
+ - Timestamp, tool name, duration, success/error
81
+ - Per-field details: coordinates (PDF points), text written, field IDs
82
+ - Session and mode labels for experimental analysis
83
+
68
84
  ## Usage Examples
69
85
 
70
86
  ### Fill a certificate (Claude analyzes the PDF visually)
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { handleGenerateBatch, generateBatchSchema } from './tools/generateBatch.
13
13
  import { handleListTemplates, listTemplatesSchema } from './tools/manageTemplates.js';
14
14
  import { handleImportTemplate, importTemplateSchema } from './tools/importTemplate.js';
15
15
  import { withMetrics } from './services/metricsLogger.js';
16
- const server = new Server({ name: 'autoform', version: '1.6.0' }, { capabilities: { tools: {} } });
16
+ const server = new Server({ name: 'autoform', version: '1.7.0' }, { capabilities: { tools: {} } });
17
17
  // Register all tools
18
18
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
19
  tools: [
@@ -78,4 +78,4 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
78
78
  // Start server
79
79
  const transport = new StdioServerTransport();
80
80
  await server.connect(transport);
81
- console.error('[AutoForm MCP] Server v1.6.0 started — 11 tools registered (metrics enabled)');
81
+ console.error('[AutoForm MCP] Server v1.7.0 started — 11 tools registered (metrics enabled)');
@@ -5,13 +5,10 @@ export interface MetricsEvent {
5
5
  success: boolean;
6
6
  error_message?: string;
7
7
  session_id?: string;
8
- input?: Record<string, number | string | boolean>;
8
+ mode?: string;
9
+ input?: Record<string, any>;
9
10
  output?: Record<string, number | string | boolean>;
10
11
  }
11
- /**
12
- * Wrap a tool handler to automatically measure and log metrics.
13
- * The handler's behavior is unchanged; metrics are captured transparently.
14
- */
15
12
  export declare function withMetrics<T>(toolName: string, args: any, handler: (args: any) => Promise<T>): Promise<T>;
16
13
  /**
17
14
  * Generate a new random session ID (not used automatically — exported for future use).
@@ -22,10 +22,6 @@ import * as crypto from 'crypto';
22
22
  const METRICS_DIR = path.join(os.homedir(), '.autoform-mcp');
23
23
  const METRICS_FILE = path.join(METRICS_DIR, 'metrics.jsonl');
24
24
  const METRICS_DISABLED = process.env.AUTOFORM_METRICS_DISABLED === '1';
25
- /**
26
- * Extract structural stats from tool arguments without leaking raw data.
27
- * Only records COUNTS and sizes, never the actual content of fields or user data.
28
- */
29
25
  function summarizeInput(toolName, args) {
30
26
  const summary = {};
31
27
  if (!args || typeof args !== 'object')
@@ -72,8 +68,93 @@ function summarizeInput(toolName, args) {
72
68
  if (typeof args.template_name === 'string') {
73
69
  summary.template_name = args.template_name;
74
70
  }
71
+ // ─── Ítem 1: Exact coordinate logging per field written ───
72
+ // For fill tools, extract detailed per-field info (coordinates + text)
73
+ try {
74
+ if (toolName === 'autoform_fill_at_coordinates' && fields) {
75
+ summary.fields_written = extractFieldsWritten(fields);
76
+ }
77
+ if (toolName === 'autoform_fill_batch_at_coordinates' && fieldDefs && dataRows) {
78
+ // For batch: log fields per row (first row only for brevity, full set in fields_written_all)
79
+ summary.fields_written = extractFieldDefsWritten(fieldDefs, dataRows[0]);
80
+ if (dataRows.length > 1) {
81
+ summary.fields_written_all_rows = dataRows.map((row) => extractFieldDefsWritten(fieldDefs, row));
82
+ }
83
+ }
84
+ if (toolName === 'autoform_fill_batch_acroform' && dataRows) {
85
+ // AcroForm: we know the field names from data_rows keys + optional field_map
86
+ const fieldMap = args.field_map && typeof args.field_map === 'object' ? args.field_map : null;
87
+ summary.fields_written = extractAcroFormFieldsWritten(dataRows[0], fieldMap);
88
+ if (dataRows.length > 1) {
89
+ summary.fields_written_all_rows = dataRows.map((row) => extractAcroFormFieldsWritten(row, fieldMap));
90
+ }
91
+ }
92
+ if (toolName === 'autoform_fill_pdf') {
93
+ // fill_pdf with field_values — AcroForm or template-based
94
+ if (args.field_values && typeof args.field_values === 'object') {
95
+ const fieldMap = args.field_map && typeof args.field_map === 'object' ? args.field_map : null;
96
+ summary.fields_written = extractAcroFormFieldsWritten(args.field_values, fieldMap);
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ // Never let field extraction break the logger
102
+ }
75
103
  return summary;
76
104
  }
105
+ /** Extract per-field details from fill_at_coordinates fields array */
106
+ function extractFieldsWritten(fields) {
107
+ return fields.map((f) => {
108
+ const detail = {
109
+ text: String(f.text ?? ''),
110
+ x_pt: Number(f.x) || 0,
111
+ y_pt: Number(f.y) || 0,
112
+ page: Number(f.page) || 1,
113
+ coordinate_origin: 'bottom_left'
114
+ };
115
+ if (f.width != null)
116
+ detail.width_pt = Number(f.width);
117
+ if (f.height != null)
118
+ detail.height_pt = Number(f.height);
119
+ if (f.fontSize != null)
120
+ detail.fontSize = Number(f.fontSize);
121
+ return detail;
122
+ });
123
+ }
124
+ /** Extract per-field details from fill_batch_at_coordinates (field_definitions + one data row) */
125
+ function extractFieldDefsWritten(fieldDefs, dataRow) {
126
+ if (!dataRow || typeof dataRow !== 'object')
127
+ return [];
128
+ return fieldDefs.map((fd) => {
129
+ const label = fd.label || '';
130
+ const detail = {
131
+ text: String(dataRow[label] ?? ''),
132
+ x_pt: Number(fd.x) || 0,
133
+ y_pt: Number(fd.y) || 0,
134
+ page: Number(fd.page) || 1,
135
+ coordinate_origin: 'bottom_left',
136
+ semantic_label: label
137
+ };
138
+ if (fd.width != null)
139
+ detail.width_pt = Number(fd.width);
140
+ if (fd.height != null)
141
+ detail.height_pt = Number(fd.height);
142
+ if (fd.fontSize != null)
143
+ detail.fontSize = Number(fd.fontSize);
144
+ return detail;
145
+ });
146
+ }
147
+ /** Extract per-field details from AcroForm fill (field_values object) */
148
+ function extractAcroFormFieldsWritten(fieldValues, fieldMap) {
149
+ if (!fieldValues || typeof fieldValues !== 'object')
150
+ return [];
151
+ return Object.entries(fieldValues).map(([key, value]) => ({
152
+ text: String(value ?? ''),
153
+ coordinates_available: false, // AcroForm positions handled internally by pdf-lib — harness uses field_id matching
154
+ field_id: fieldMap ? (fieldMap[key] || key) : key,
155
+ semantic_label: key
156
+ }));
157
+ }
77
158
  /**
78
159
  * Extract structural stats from the MCP tool response.
79
160
  * Parses the returned JSON text to extract counts (documents_generated, total_fields, etc.).
@@ -175,13 +256,18 @@ function writeEvent(event) {
175
256
  * Wrap a tool handler to automatically measure and log metrics.
176
257
  * The handler's behavior is unchanged; metrics are captured transparently.
177
258
  */
259
+ /** Valid experimental modes for _mode parameter */
260
+ const VALID_MODES = ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'];
178
261
  export async function withMetrics(toolName, args, handler) {
179
262
  const startTime = Date.now();
180
263
  const sessionId = typeof args?._session_id === 'string' ? args._session_id : undefined;
181
- // Strip _session_id from args before passing to handler (internal only)
182
- const cleanArgs = args && typeof args === 'object' && '_session_id' in args
183
- ? (() => { const { _session_id, ...rest } = args; return rest; })()
184
- : args;
264
+ const mode = typeof args?._mode === 'string' && VALID_MODES.includes(args._mode) ? args._mode : undefined;
265
+ // Strip _session_id and _mode from args before passing to handler (internal only)
266
+ let cleanArgs = args;
267
+ if (args && typeof args === 'object' && ('_session_id' in args || '_mode' in args)) {
268
+ const { _session_id, _mode, ...rest } = args;
269
+ cleanArgs = rest;
270
+ }
185
271
  let result;
186
272
  let success = true;
187
273
  let errorMessage;
@@ -208,6 +294,7 @@ export async function withMetrics(toolName, args, handler) {
208
294
  success: false,
209
295
  error_message: errorMessage,
210
296
  ...(sessionId ? { session_id: sessionId } : {}),
297
+ ...(mode ? { mode } : {}),
211
298
  input: summarizeInput(toolName, cleanArgs)
212
299
  };
213
300
  writeEvent(event);
@@ -220,6 +307,7 @@ export async function withMetrics(toolName, args, handler) {
220
307
  success,
221
308
  ...(errorMessage ? { error_message: errorMessage } : {}),
222
309
  ...(sessionId ? { session_id: sessionId } : {}),
310
+ ...(mode ? { mode } : {}),
223
311
  input: summarizeInput(toolName, cleanArgs),
224
312
  output: summarizeOutput(result)
225
313
  };
@@ -11,16 +11,31 @@ export interface TextItem {
11
11
  page: number;
12
12
  }
13
13
  /**
14
- * Horizontal line detected as a drawing operator.
14
+ * A graphic element detected from PDF drawing operators.
15
15
  */
16
- export interface HorizontalLine {
16
+ export interface GraphicElement {
17
+ type: 'checkbox' | 'field_box' | 'horizontal_line' | 'rectangle';
17
18
  x: number;
18
19
  y: number;
19
20
  width: number;
21
+ height: number;
20
22
  page: number;
23
+ nearbyLabel?: string;
24
+ labelPosition?: 'left' | 'right' | 'above' | 'below';
25
+ }
26
+ /**
27
+ * Complete page information including text AND graphic elements.
28
+ */
29
+ export interface PageFullInfo {
30
+ page: number;
31
+ width: number;
32
+ height: number;
33
+ textItems: TextItem[];
34
+ graphicElements: GraphicElement[];
35
+ totalCharacters: number;
21
36
  }
22
37
  /**
23
- * A page's extracted information.
38
+ * A page's extracted information (text only, for backward compatibility).
24
39
  */
25
40
  export interface PageTextInfo {
26
41
  page: number;
@@ -40,6 +55,38 @@ export type PdfContentType = 'acroform' | 'text_vectorial' | 'image_only' | 'mix
40
55
  export declare class PdfTextExtractor {
41
56
  /** Extract all text items from all pages of a PDF */
42
57
  static extractAllText(pdfPath: string): Promise<PageTextInfo[]>;
58
+ /**
59
+ * Extract ALL information from a PDF: text items + graphic elements (rectangles, lines).
60
+ * Uses both getTextContent() and getOperatorList() to build a complete map of the page.
61
+ *
62
+ * Graphic elements are classified as:
63
+ * - checkbox: small square (6-16pt per side), likely a check box
64
+ * - field_box: medium/large rectangle (width > 40pt), likely an input field area
65
+ * - horizontal_line: very thin rectangle (height < 2pt, width > 30pt), likely an underline for writing
66
+ * - rectangle: anything else
67
+ *
68
+ * Each graphic element gets a nearbyLabel from the closest text item.
69
+ */
70
+ static extractFullPageInfo(pdfPath: string): Promise<PageFullInfo[]>;
71
+ /**
72
+ * Extract all text items from a PDF with positions converted to top-left origin.
73
+ * Used for post-hoc validation: verify that pdf-lib actually wrote where the MCP said it wrote.
74
+ *
75
+ * Returns items grouped by proximity (consecutive items on the same Y line are merged).
76
+ * Coordinates: PDF points, origin top-left (y=0 at top of page).
77
+ *
78
+ * This is an internal function called by the analysis harness (research/scripts/analyze-experiment.js),
79
+ * NOT exposed as an MCP tool.
80
+ */
81
+ static extractWrittenTextPositions(pdfPath: string): Promise<Array<{
82
+ text: string;
83
+ x: number;
84
+ y: number;
85
+ width: number;
86
+ height: number;
87
+ fontSize: number;
88
+ page: number;
89
+ }>>;
43
90
  /**
44
91
  * Classify a PDF based on extractable text density.
45
92
  * Thresholds are empirical; can be tuned.
@@ -55,6 +55,253 @@ export class PdfTextExtractor {
55
55
  }
56
56
  return pages;
57
57
  }
58
+ /**
59
+ * Extract ALL information from a PDF: text items + graphic elements (rectangles, lines).
60
+ * Uses both getTextContent() and getOperatorList() to build a complete map of the page.
61
+ *
62
+ * Graphic elements are classified as:
63
+ * - checkbox: small square (6-16pt per side), likely a check box
64
+ * - field_box: medium/large rectangle (width > 40pt), likely an input field area
65
+ * - horizontal_line: very thin rectangle (height < 2pt, width > 30pt), likely an underline for writing
66
+ * - rectangle: anything else
67
+ *
68
+ * Each graphic element gets a nearbyLabel from the closest text item.
69
+ */
70
+ static async extractFullPageInfo(pdfPath) {
71
+ const bytes = fs.readFileSync(pdfPath);
72
+ const pdf = await pdfjsLib.getDocument({
73
+ data: new Uint8Array(bytes),
74
+ useSystemFonts: true,
75
+ verbosity: 0
76
+ }).promise;
77
+ const results = [];
78
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
79
+ const page = await pdf.getPage(pageNum);
80
+ const viewport = page.getViewport({ scale: 1 });
81
+ // 1. Extract text (same as extractAllText)
82
+ const textContent = await page.getTextContent();
83
+ const textItems = [];
84
+ let totalChars = 0;
85
+ for (const raw of textContent.items) {
86
+ const text = String(raw.str ?? '').trim();
87
+ if (text === '')
88
+ continue;
89
+ const [, , , scaleY, tx, ty] = raw.transform;
90
+ const fontSize = Math.abs(scaleY);
91
+ const width = Math.abs(raw.width ?? 0);
92
+ const height = Math.abs(raw.height ?? fontSize);
93
+ textItems.push({ text, x: tx, y: ty, width, height, fontSize, page: pageNum });
94
+ totalChars += text.length;
95
+ }
96
+ // 2. Extract graphic elements from operator list
97
+ const operatorList = await page.getOperatorList();
98
+ const graphicElements = [];
99
+ // pdfjs OPS constants
100
+ const OPS = pdfjsLib.OPS;
101
+ // Track current transform matrix for coordinate conversion
102
+ // The operator list contains transform operations + rectangle draws
103
+ let currentTransform = [1, 0, 0, 1, 0, 0]; // identity
104
+ for (let i = 0; i < operatorList.fnArray.length; i++) {
105
+ const fn = operatorList.fnArray[i];
106
+ const args = operatorList.argsArray[i];
107
+ // Track transform changes
108
+ if (fn === OPS.transform) {
109
+ if (args && args.length >= 6) {
110
+ currentTransform = args;
111
+ }
112
+ }
113
+ // Detect rectangle operations: constructPath contains 'rectangle' ops
114
+ if (fn === OPS.constructPath) {
115
+ const ops = args[0]; // array of sub-operations
116
+ const pathArgs = args[1]; // array of coordinates [x, y, w, h, ...]
117
+ if (!ops || !pathArgs)
118
+ continue;
119
+ let argIdx = 0;
120
+ for (const op of ops) {
121
+ // op 13 = rectangle in pdfjs (OPS.rectangle)
122
+ if (op === 13 && argIdx + 3 < pathArgs.length) {
123
+ const rx = pathArgs[argIdx];
124
+ const ry = pathArgs[argIdx + 1];
125
+ const rw = pathArgs[argIdx + 2];
126
+ const rh = pathArgs[argIdx + 3];
127
+ // Apply current transform to get absolute coordinates
128
+ const absX = currentTransform[4] + rx * currentTransform[0];
129
+ const absY = currentTransform[5] + ry * currentTransform[3];
130
+ const absW = Math.abs(rw * currentTransform[0]);
131
+ const absH = Math.abs(rh * currentTransform[3]);
132
+ // Filter: skip tiny rects (artifacts) and page-sized rects (backgrounds)
133
+ if (absW < 4 || absH < 1) {
134
+ argIdx += 4;
135
+ continue;
136
+ }
137
+ if (absW > viewport.width * 0.95 && absH > viewport.height * 0.95) {
138
+ argIdx += 4;
139
+ continue;
140
+ }
141
+ // Classify the rectangle
142
+ let type;
143
+ if (absH < 2.5 && absW > 30) {
144
+ type = 'horizontal_line';
145
+ }
146
+ else if (absW >= 6 && absW <= 25 && absH >= 6 && absH <= 25 && Math.abs(absW - absH) < 6) {
147
+ type = 'checkbox';
148
+ }
149
+ else if (absW > 50 && absH > 10 && absH < 40) {
150
+ type = 'field_box';
151
+ }
152
+ else {
153
+ type = 'rectangle';
154
+ }
155
+ graphicElements.push({
156
+ type,
157
+ x: Math.round(absX * 100) / 100,
158
+ y: Math.round(absY * 100) / 100,
159
+ width: Math.round(absW * 100) / 100,
160
+ height: Math.round(absH * 100) / 100,
161
+ page: pageNum
162
+ });
163
+ argIdx += 4;
164
+ }
165
+ else {
166
+ // Other path ops: moveTo(2 args), lineTo(2 args), etc.
167
+ if (op === 14 || op === 15)
168
+ argIdx += 2; // moveTo, lineTo
169
+ else if (op === 16)
170
+ argIdx += 6; // bezierCurveTo
171
+ else if (op === 17)
172
+ argIdx += 0; // closePath
173
+ else
174
+ argIdx += 2; // safe default
175
+ }
176
+ }
177
+ }
178
+ }
179
+ // 2.5. Deduplicate overlapping graphic elements of the same type
180
+ // PDFs often have multiple border layers for the same visual element
181
+ const deduped = [];
182
+ for (const elem of graphicElements) {
183
+ const isDuplicate = deduped.some(existing => existing.type === elem.type &&
184
+ existing.page === elem.page &&
185
+ Math.abs(existing.x - elem.x) < 8 &&
186
+ Math.abs(existing.y - elem.y) < 8 &&
187
+ Math.abs(existing.width - elem.width) < 10 &&
188
+ Math.abs(existing.height - elem.height) < 10);
189
+ if (!isDuplicate)
190
+ deduped.push(elem);
191
+ }
192
+ graphicElements.length = 0;
193
+ graphicElements.push(...deduped);
194
+ // 3. Correlate each graphic element with the nearest text label
195
+ for (const elem of graphicElements) {
196
+ const elemCenterX = elem.x + elem.width / 2;
197
+ const elemCenterY = elem.y + elem.height / 2;
198
+ let bestLabel = '';
199
+ let bestDist = Infinity;
200
+ let bestPos = 'left';
201
+ for (const ti of textItems) {
202
+ const textCenterX = ti.x + ti.width / 2;
203
+ const textCenterY = ti.y + ti.height / 2;
204
+ const dx = elemCenterX - textCenterX;
205
+ const dy = elemCenterY - textCenterY;
206
+ const dist = Math.sqrt(dx * dx + dy * dy);
207
+ if (dist < bestDist && dist < 200) { // max 200pt search radius
208
+ bestDist = dist;
209
+ bestLabel = ti.text;
210
+ const absDx = Math.abs(dx);
211
+ const absDy = Math.abs(dy);
212
+ if (absDx > absDy) {
213
+ bestPos = dx > 0 ? 'left' : 'right';
214
+ }
215
+ else {
216
+ bestPos = dy > 0 ? 'below' : 'above';
217
+ }
218
+ }
219
+ }
220
+ if (bestLabel) {
221
+ elem.nearbyLabel = bestLabel;
222
+ elem.labelPosition = bestPos;
223
+ }
224
+ }
225
+ results.push({
226
+ page: pageNum,
227
+ width: viewport.width,
228
+ height: viewport.height,
229
+ textItems,
230
+ graphicElements,
231
+ totalCharacters: totalChars
232
+ });
233
+ }
234
+ return results;
235
+ }
236
+ /**
237
+ * Extract all text items from a PDF with positions converted to top-left origin.
238
+ * Used for post-hoc validation: verify that pdf-lib actually wrote where the MCP said it wrote.
239
+ *
240
+ * Returns items grouped by proximity (consecutive items on the same Y line are merged).
241
+ * Coordinates: PDF points, origin top-left (y=0 at top of page).
242
+ *
243
+ * This is an internal function called by the analysis harness (research/scripts/analyze-experiment.js),
244
+ * NOT exposed as an MCP tool.
245
+ */
246
+ static async extractWrittenTextPositions(pdfPath) {
247
+ const bytes = fs.readFileSync(pdfPath);
248
+ const pdf = await pdfjsLib.getDocument({
249
+ data: new Uint8Array(bytes),
250
+ useSystemFonts: true,
251
+ verbosity: 0
252
+ }).promise;
253
+ const results = [];
254
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
255
+ const page = await pdf.getPage(pageNum);
256
+ const viewport = page.getViewport({ scale: 1 });
257
+ const pageHeight = viewport.height;
258
+ const textContent = await page.getTextContent();
259
+ // Collect raw items with top-left coordinates
260
+ const rawItems = [];
261
+ for (const raw of textContent.items) {
262
+ const text = String(raw.str ?? '');
263
+ if (text === '')
264
+ continue;
265
+ const [, , , scaleY, tx, ty] = raw.transform;
266
+ const fontSize = Math.abs(scaleY);
267
+ const width = Math.abs(raw.width ?? 0);
268
+ const height = Math.abs(raw.height ?? fontSize);
269
+ // Convert from PDF bottom-left to top-left origin
270
+ // ty is the text baseline; subtract height to get the top edge of the text
271
+ const yTopLeft = pageHeight - ty - height;
272
+ rawItems.push({
273
+ text,
274
+ x: tx,
275
+ y: yTopLeft,
276
+ width,
277
+ height,
278
+ fontSize,
279
+ page: pageNum
280
+ });
281
+ }
282
+ // Group consecutive items on the same Y line (within 2pt tolerance)
283
+ // pdfjs-dist fragments text into runs; pdf-lib may write a whole string as one item
284
+ const grouped = [];
285
+ for (const item of rawItems) {
286
+ const last = grouped[grouped.length - 1];
287
+ if (last &&
288
+ last.page === item.page &&
289
+ Math.abs(last.y - item.y) < 2 &&
290
+ Math.abs((last.x + last.width) - item.x) < 5) {
291
+ // Merge: extend text and width
292
+ last.text += item.text;
293
+ last.width = (item.x + item.width) - last.x;
294
+ last.height = Math.max(last.height, item.height);
295
+ last.fontSize = Math.max(last.fontSize, item.fontSize);
296
+ }
297
+ else {
298
+ grouped.push({ ...item });
299
+ }
300
+ }
301
+ results.push(...grouped);
302
+ }
303
+ return results;
304
+ }
58
305
  /**
59
306
  * Classify a PDF based on extractable text density.
60
307
  * Thresholds are empirical; can be tuned.
@@ -8,6 +8,15 @@ export declare const analyzeStaticPdfSchema: {
8
8
  type: string;
9
9
  description: string;
10
10
  };
11
+ _session_id: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ _mode: {
16
+ type: string;
17
+ enum: string[];
18
+ description: string;
19
+ };
11
20
  };
12
21
  required: string[];
13
22
  };
@@ -21,11 +21,18 @@ Si recibes este tipo, NO intentes procesar el PDF con los otros tools. En su lug
21
21
  3. Proporcionar coordenadas manualmente si ya las conoces
22
22
 
23
23
  ⚠️ CASO text_vectorial:
24
- - suggested_fields son SUGERENCIAS heuristicas (asumen layout "Label: ___" a la derecha).
25
- - Para layouts atipicos (certificados con campo DEBAJO del label, diplomas con layout centrado, etc.) las sugerencias pueden estar en posiciones incorrectas.
26
- - USA all_text_items para ver TODOS los textos del PDF con sus posiciones exactas y razona sobre el layout real antes de decidir las coordenadas finales.
27
- - Los campos "CERTIFICADO", "Firma 1/2" que se detectan como sugerencias probablemente NO son campos editables son etiquetas del certificado. Descarta sugerencias que claramente son texto decorativo.
28
- - Cuando no estes seguro, PREGUNTA al usuario donde va cada dato en lugar de adivinar.
24
+ - suggested_fields son SUGERENCIAS heuristicas que pueden estar en posiciones incorrectas para layouts atipicos (centrados, tabulares complejos).
25
+ - USA all_text_items para razonar sobre el layout real del PDF.
26
+ - Descarta sugerencias que son texto decorativo ("CERTIFICADO", "Firma 1/2").
27
+ - Cuando no estes seguro del layout, PREGUNTA al usuario o pidele que adjunte el PDF al chat para verlo visualmente.
28
+
29
+ ⚠️ FORMULARIOS COMPLEJOS:
30
+ - Si el formulario tiene muchos campos (>10), tablas densas, o un layout no obvio, RECOMIENDA al usuario adjuntar el PDF al chat para verificacion visual antes de generar. Dile algo como: "Este formulario es complejo. Para asegurar precision, te recomiendo adjuntarme el PDF al chat para que pueda verificar las posiciones visualmente."
31
+ - Si el usuario ya adjunto el PDF al chat Y tambien te dio la ruta, APROVECHA ambos: usa la imagen visual para verificar que tus coordenadas son correctas.
32
+
33
+ ⚠️ CHECKBOXES:
34
+ - Si detectas textos tipo opciones multiples en la misma linea Y (ej: "ALUMNO", "DOCENTE", "ADMINISTRATIVO"), es probable que haya casillas de verificacion.
35
+ - NO adivines la posicion de los cuadrados. Pide al usuario que adjunte el PDF para verlo visualmente.
29
36
 
30
37
  ⚠️ CASO mixed:
31
38
  Hay algo de texto pero poca densidad. Puede haber campos que NO se detectaron automaticamente porque el label es una imagen. Usa suggested_fields como punto de partida pero verifica visualmente si faltan campos.`,
@@ -35,6 +42,15 @@ Hay algo de texto pero poca densidad. Puede haber campos que NO se detectaron au
35
42
  pdf_path: {
36
43
  type: 'string',
37
44
  description: 'Ruta absoluta al PDF estatico a analizar'
45
+ },
46
+ _session_id: {
47
+ type: 'string',
48
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
49
+ },
50
+ _mode: {
51
+ type: 'string',
52
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
53
+ description: '(Opcional) Modo de autonomia experimental.'
38
54
  }
39
55
  },
40
56
  required: ['pdf_path']
@@ -127,7 +143,7 @@ export async function handleAnalyzeStaticPdf(args) {
127
143
  })));
128
144
  const summary = suggestedFields.length > 0
129
145
  ? `Se detectaron ${suggestedFields.length} campos candidatos automaticamente:\n${suggestedFields.map(f => `- "${f.inferredLabel}" en pag ${f.page}, pos (${f.suggestedX}, ${f.suggestedY}), ${f.suggestedWidth}x${f.suggestedHeight} [${f.confidence}]`).join('\n')}`
130
- : 'No se detectaron campos candidatos automaticamente. El PDF tiene texto pero no hay patrones claros de "Label: ___" reconocibles.';
146
+ : 'No se detectaron campos candidatos automaticamente. El PDF tiene texto pero no hay patrones claros de "Label: ___" reconocibles. Revisa all_text_items para ver todo el texto del PDF con posiciones y razona sobre el layout.';
131
147
  return {
132
148
  content: [{
133
149
  type: 'text',
@@ -143,14 +159,14 @@ export async function handleAnalyzeStaticPdf(args) {
143
159
  },
144
160
  total_text_items: allTextItems.length,
145
161
  suggested_fields: suggestedFields,
146
- all_text_items: allTextItems.slice(0, 100), // Limit to avoid huge responses
162
+ all_text_items: allTextItems.slice(0, 100),
147
163
  text_items_truncated: allTextItems.length > 100,
148
164
  summary,
149
165
  guidance: contentType === 'mixed'
150
- ? 'El PDF tiene densidad baja de texto — puede tener elementos visuales (titulos como imagen, logos) que no se extraen. Usa los suggested_fields como punto de partida pero verifica si faltan campos.'
166
+ ? 'El PDF tiene densidad baja de texto. Usa all_text_items como referencia y pregunta al usuario si faltan campos.'
151
167
  : suggestedFields.length > 0
152
- ? 'Usa los suggested_fields directamente con autoform_fill_at_coordinates (un documento) o autoform_fill_batch_at_coordinates (multiples documentos).'
153
- : 'No hay campos obvios detectables. Pregunta al usuario donde quiere colocar los datos o pidele que abra el PDF en la app web de AutoForm.'
168
+ ? 'Usa los suggested_fields como punto de partida. IMPORTANTE SOBRE CHECKBOXES: si detectas en all_text_items textos que parecen opciones multiples en la misma linea (ej: "ALUMNO", "DOCENTE", "ADMINISTRATIVO" con coordenadas Y similares), es MUY probable que el formulario tenga casillas de verificacion. En ese caso, NO adivines la posicion de los cuadrados — pide al usuario que adjunte el PDF al chat para que puedas verlo visualmente y determinar la posicion exacta de cada casilla antes de marcar con "X".'
169
+ : 'No hay campos obvios detectables por heuristicas. IMPORTANTE: si en all_text_items detectas textos que parecen opciones multiples (ej: varias opciones en la misma linea Y), probablemente hay casillas de verificacion cuya posicion exacta no puedo determinar solo con texto. Pide al usuario que adjunte el PDF al chat para verlo visualmente, o que te indique donde estan las casillas.'
154
170
  }, null, 2)
155
171
  }]
156
172
  };
@@ -8,6 +8,15 @@ export declare const detectFieldsSchema: {
8
8
  type: string;
9
9
  description: string;
10
10
  };
11
+ _session_id: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ _mode: {
16
+ type: string;
17
+ enum: string[];
18
+ description: string;
19
+ };
11
20
  };
12
21
  required: string[];
13
22
  };
@@ -36,6 +36,15 @@ FLUJO RECOMENDADO PARA MULTIPLES DOCUMENTOS:
36
36
  pdf_path: {
37
37
  type: 'string',
38
38
  description: 'Ruta absoluta al archivo PDF'
39
+ },
40
+ _session_id: {
41
+ type: 'string',
42
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
43
+ },
44
+ _mode: {
45
+ type: 'string',
46
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
47
+ description: '(Opcional) Modo de autonomia experimental.'
39
48
  }
40
49
  },
41
50
  required: ['pdf_path']
@@ -55,6 +55,15 @@ export declare const fillAtCoordinatesSchema: {
55
55
  type: string;
56
56
  description: string;
57
57
  };
58
+ _session_id: {
59
+ type: string;
60
+ description: string;
61
+ };
62
+ _mode: {
63
+ type: string;
64
+ enum: string[];
65
+ description: string;
66
+ };
58
67
  };
59
68
  required: string[];
60
69
  };
@@ -69,6 +78,15 @@ export declare const getPdfInfoSchema: {
69
78
  type: string;
70
79
  description: string;
71
80
  };
81
+ _session_id: {
82
+ type: string;
83
+ description: string;
84
+ };
85
+ _mode: {
86
+ type: string;
87
+ enum: string[];
88
+ description: string;
89
+ };
72
90
  };
73
91
  required: string[];
74
92
  };
@@ -1,31 +1,24 @@
1
1
  import * as path from 'path';
2
- import * as os from 'os';
3
2
  import { PdfService } from '../services/pdfService.js';
4
- const OUTPUT_DIR = path.join(os.homedir(), '.autoform-mcp', 'output');
5
3
  export const fillAtCoordinatesSchema = {
6
4
  name: 'autoform_fill_at_coordinates',
7
- description: `Escribe texto en posiciones exactas de UN PDF usando coordenadas en puntos PDF (origen: esquina inferior-izquierda).
8
- Ideal cuando Claude analiza visualmente un PDF y determina dónde colocar el texto.
5
+ description: `Escribe texto en posiciones exactas de UN PDF.
9
6
 
10
- ⚠️ ESTA TOOL ES PARA UN UNICO DOCUMENTO. Si necesitas generar multiples PDFs con los mismos campos pero datos diferentes, USA autoform_fill_batch_at_coordinates (mas eficiente, una sola llamada).
7
+ ⚠️ PARA MULTIPLES DOCUMENTOS usa autoform_fill_batch_at_coordinates (una sola llamada, mas eficiente).
11
8
 
12
- IMPORTANTE Codificación de texto:
13
- - El texto se pasa EXACTAMENTE como lo envías. NO simplifiques ni modifiques caracteres.
14
- - Acentos españoles (á é í ó ú ñ Ñ ü) están soportados completamente.
15
- - Nombres como "Sofía", "Benítez", "Peña" deben pasarse con sus tildes intactas.
16
- - Solo se filtran: emojis, caracteres de control invisibles, y patrones de script malicioso.
9
+ CODIFICACION: Acentos é í ó ú ñ Ñ ü) soportados. NO simplifiques caracteres.
17
10
 
18
- FLUJO RECOMENDADO:
19
- 1. Primero usa autoform_get_pdf_info para obtener las dimensiones de cada página
20
- 2. Basándote en el análisis visual del PDF, calcula las coordenadas en puntos PDF
21
- 3. Recuerda: Y=0 está ABAJO, Y aumenta hacia ARRIBA
11
+ CHECKBOXES: Solo marca casillas si puedes VER el PDF visualmente. Si no, pide al usuario que lo adjunte al chat.
22
12
 
23
- SISTEMA DE COORDENADAS:
24
- - Origen (0,0) = esquina inferior-izquierda de la página
25
- - X aumenta hacia la derecha
26
- - Y aumenta hacia arriba
27
- - Una página A4 típica mide ~595 x 842 puntos
28
- - Una página Letter típica mide ~612 x 792 puntos`,
13
+ VERIFICACION VISUAL: Si el usuario adjunto el PDF Y dio la ruta, aprovecha la imagen para verificar coordenadas antes de escribir.
14
+
15
+ DIRECTORIO DE SALIDA: Si no se especifica output_path, guarda en el MISMO directorio del pdf_path original.
16
+
17
+ NO GENERES PDF DE PRUEBA: Genera el resultado final directamente. No hagas "tests" individuales a menos que el usuario lo pida.
18
+
19
+ CUANDO TENGAS DUDAS: Si el formulario es complejo y no estas seguro de las posiciones, PREGUNTA al usuario o pidele que adjunte el PDF al chat. Mejor preguntar que generar mal.
20
+
21
+ COORDENADAS: Origen (0,0) = esquina inferior-izquierda. Y aumenta hacia arriba. Puntos PDF (A4 ≈ 595x842, Letter ≈ 612x792).`,
29
22
  inputSchema: {
30
23
  type: 'object',
31
24
  properties: {
@@ -54,6 +47,15 @@ SISTEMA DE COORDENADAS:
54
47
  output_path: {
55
48
  type: 'string',
56
49
  description: '(Opcional) Ruta donde guardar el PDF. Default: ~/.autoform-mcp/output/'
50
+ },
51
+ _session_id: {
52
+ type: 'string',
53
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl. Formato: <pdf_id>_<mode>_<rep>'
54
+ },
55
+ _mode: {
56
+ type: 'string',
57
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
58
+ description: '(Opcional) Modo de autonomia experimental. Se registra en metrics.jsonl.'
57
59
  }
58
60
  },
59
61
  required: ['pdf_path', 'fields']
@@ -68,6 +70,15 @@ export const getPdfInfoSchema = {
68
70
  pdf_path: {
69
71
  type: 'string',
70
72
  description: 'Ruta absoluta al archivo PDF'
73
+ },
74
+ _session_id: {
75
+ type: 'string',
76
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
77
+ },
78
+ _mode: {
79
+ type: 'string',
80
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
81
+ description: '(Opcional) Modo de autonomia experimental.'
71
82
  }
72
83
  },
73
84
  required: ['pdf_path']
@@ -89,7 +100,7 @@ export async function handleFillAtCoordinates(args) {
89
100
  isError: true
90
101
  };
91
102
  }
92
- const outputFile = output_path || path.join(OUTPUT_DIR, `coords_${path.basename(pdf_path, '.pdf')}_${Date.now()}.pdf`);
103
+ const outputFile = output_path || path.join(path.dirname(pdf_path), `${path.basename(pdf_path, '.pdf')}_filled_${Date.now()}.pdf`);
93
104
  const filled = await PdfService.fillAtCoordinates(pdf_path, fields, outputFile);
94
105
  return {
95
106
  content: [{
@@ -33,6 +33,15 @@ export declare const fillBatchAcroFormSchema: {
33
33
  type: string;
34
34
  description: string;
35
35
  };
36
+ _session_id: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ _mode: {
41
+ type: string;
42
+ enum: string[];
43
+ description: string;
44
+ };
36
45
  };
37
46
  required: string[];
38
47
  };
@@ -1,7 +1,5 @@
1
1
  import * as path from 'path';
2
- import * as os from 'os';
3
2
  import { PdfService } from '../services/pdfService.js';
4
- const OUTPUT_DIR = path.join(os.homedir(), '.autoform-mcp', 'output');
5
3
  export const fillBatchAcroFormSchema = {
6
4
  name: 'autoform_fill_batch_acroform',
7
5
  description: `Genera MULTIPLES PDFs a partir de un PDF con campos AcroForm (formulario interactivo) + array de datos. Una sola llamada genera N documentos.
@@ -17,9 +15,15 @@ IMPORTANTE sobre nombres de campos:
17
15
  - Si las claves de tus data_rows NO coinciden con los nombres tecnicos, usa el parametro field_map para traducir: { "nombre_logico": "nombre_tecnico" }
18
16
  - Ejemplo: si el campo tecnico es "Text1" pero visualmente corresponde al label "Nombre:", pasa field_map={"nombre": "Text1"} y data_rows=[{"nombre": "Juan"}]
19
17
 
20
- MERGE: Si merge_into_single=true, se genera SOLO el PDF unificado (los individuales son temporales y se borran automaticamente).
18
+ MERGE: Si merge_into_single=true, se genera SOLO el PDF unificado.
21
19
 
22
- CODIFICACION: Los datos se escriben EXACTAMENTE como los envias. Acentos espanoles (a e i o u n N u con tildes) estan completamente soportados. NO simplifiques caracteres.`,
20
+ CODIFICACION: Acentos (á é í ó ú ñ) soportados completamente. NO simplifiques caracteres.
21
+
22
+ DIRECTORIO DE SALIDA: Si no se especifica output_dir, usa el MISMO directorio donde esta el pdf_path original.
23
+
24
+ NO GENERES PDF DE PRUEBA: Genera el batch completo directamente. No hagas un "test" individual primero a menos que el usuario lo pida.
25
+
26
+ VERIFICACION: Si el usuario adjunto el PDF al chat Y dio la ruta, aprovecha la imagen visual para verificar que los inferred_labels son correctos.`,
23
27
  inputSchema: {
24
28
  type: 'object',
25
29
  properties: {
@@ -47,6 +51,15 @@ CODIFICACION: Los datos se escriben EXACTAMENTE como los envias. Acentos espanol
47
51
  output_dir: {
48
52
  type: 'string',
49
53
  description: '(Opcional) Directorio de salida. Default: ~/.autoform-mcp/output/'
54
+ },
55
+ _session_id: {
56
+ type: 'string',
57
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl. Formato: <pdf_id>_<mode>_<rep>'
58
+ },
59
+ _mode: {
60
+ type: 'string',
61
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
62
+ description: '(Opcional) Modo de autonomia experimental. Se registra en metrics.jsonl.'
50
63
  }
51
64
  },
52
65
  required: ['pdf_path', 'data_rows']
@@ -74,7 +87,7 @@ export async function handleFillBatchAcroForm(args) {
74
87
  isError: true
75
88
  };
76
89
  }
77
- const outDir = output_dir || OUTPUT_DIR;
90
+ const outDir = output_dir || path.dirname(pdf_path);
78
91
  const baseName = path.basename(pdf_path, '.pdf').replace(/[^a-zA-Z0-9_\-]/g, '_');
79
92
  const result = await PdfService.batchFillAcroForm(pdf_path, data_rows, outDir, baseName, merge_into_single, field_map);
80
93
  return {
@@ -69,6 +69,15 @@ export declare const fillBatchAtCoordinatesSchema: {
69
69
  type: string;
70
70
  description: string;
71
71
  };
72
+ _session_id: {
73
+ type: string;
74
+ description: string;
75
+ };
76
+ _mode: {
77
+ type: string;
78
+ enum: string[];
79
+ description: string;
80
+ };
72
81
  };
73
82
  required: string[];
74
83
  };
@@ -1,7 +1,5 @@
1
1
  import * as path from 'path';
2
- import * as os from 'os';
3
2
  import { PdfService } from '../services/pdfService.js';
4
- const OUTPUT_DIR = path.join(os.homedir(), '.autoform-mcp', 'output');
5
3
  export const fillBatchAtCoordinatesSchema = {
6
4
  name: 'autoform_fill_batch_at_coordinates',
7
5
  description: `Genera MULTIPLES PDFs a partir de un mismo PDF base, escribiendo datos diferentes en las mismas posiciones de cada copia.
@@ -11,16 +9,30 @@ CUANDO USAR: El usuario pasa un PDF (certificado, diploma, constancia) y una LIS
11
9
  IMPORTANTE — Codificación de texto:
12
10
  - Los datos se escriben EXACTAMENTE como los envías. NO simplifiques ni modifiques caracteres.
13
11
  - Acentos españoles (á é í ó ú ñ Ñ ü) están soportados completamente.
14
- - Nombres como "Sofía", "Benítez", "Peña" deben pasarse con sus tildes intactas.
12
+
13
+ IMPORTANTE — Casillas de verificación (checkboxes) en PDFs estaticos:
14
+ - Si detectas opciones tipo checkbox, SOLO marcalas si puedes VER el PDF visualmente (adjuntado al chat). Si no puedes verlo, pide al usuario que lo adjunte.
15
+ - Para marcar: text="X", fontSize=10, en las coordenadas exactas del cuadrado.
16
+
17
+ IMPORTANTE — Verificación visual:
18
+ - Si el usuario adjunto el PDF al chat Y tambien dio la ruta, APROVECHA la imagen visual para verificar que tus coordenadas son correctas antes de generar.
19
+ - Si tienes dudas sobre el layout de un formulario complejo (tablas densas, campos no claros), PREGUNTA al usuario antes de generar. Es mejor preguntar que generar mal.
20
+
21
+ DIRECTORIO DE SALIDA POR DEFECTO:
22
+ - Si el usuario NO especifica output_dir, usa el MISMO directorio donde esta el PDF base (pdf_path). No uses ~/.autoform-mcp/output/ por defecto — el usuario espera los archivos cerca de su PDF original.
23
+
24
+ NO GENERES PDFs DE PRUEBA/TEST:
25
+ - Genera directamente el batch completo. NO generes un "test" individual primero a menos que el usuario lo pida explicitamente. Generar tests innecesarios desperdicia tiempo y confunde al usuario con archivos extra.
15
26
 
16
27
  MERGE: Si merge_into_single=true, se genera SOLO el PDF unificado (los individuales son temporales y se borran automáticamente).
17
28
 
18
- FLUJO:
19
- 1. Usa autoform_get_pdf_info para obtener las dimensiones de la pagina
20
- 2. Analiza visualmente el PDF para determinar donde van los campos
21
- 3. Define los campos con label (nombre logico) + coordenadas
22
- 4. Pasa las filas de datos como array de objetos {label: valor}
23
- 5. Opcionalmente unifica todo en un solo PDF con merge_into_single
29
+ FLUJO CORRECTO:
30
+ 1. Usa autoform_get_pdf_info para dimensiones
31
+ 2. Si puedes ver el PDF (adjuntado al chat): analiza visualmente donde van los campos
32
+ 3. Si NO puedes verlo: usa autoform_analyze_static_pdf para obtener texto+posiciones, y si detectas posibles checkboxes o layout complejo, pide al usuario que adjunte el PDF
33
+ 4. Define los campos con coordenadas
34
+ 5. Genera el batch completo de una vez (NO hagas un test primero)
35
+ 6. Informa al usuario la ruta del resultado
24
36
 
25
37
  SISTEMA DE COORDENADAS: Origen (0,0) = esquina inferior-izquierda. Y aumenta hacia arriba. Unidades: puntos PDF.`,
26
38
  inputSchema: {
@@ -63,6 +75,15 @@ SISTEMA DE COORDENADAS: Origen (0,0) = esquina inferior-izquierda. Y aumenta hac
63
75
  output_dir: {
64
76
  type: 'string',
65
77
  description: '(Opcional) Directorio de salida. Default: ~/.autoform-mcp/output/'
78
+ },
79
+ _session_id: {
80
+ type: 'string',
81
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl. Formato: <pdf_id>_<mode>_<rep>'
82
+ },
83
+ _mode: {
84
+ type: 'string',
85
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
86
+ description: '(Opcional) Modo de autonomia experimental. Se registra en metrics.jsonl.'
66
87
  }
67
88
  },
68
89
  required: ['pdf_path', 'field_definitions', 'data_rows']
@@ -90,7 +111,7 @@ export async function handleFillBatchAtCoordinates(args) {
90
111
  if (!data_rows || !Array.isArray(data_rows) || data_rows.length === 0) {
91
112
  return { content: [{ type: 'text', text: JSON.stringify({ error: true, message: 'data_rows es requerido (array de objetos con datos)' }, null, 2) }], isError: true };
92
113
  }
93
- const outDir = output_dir || OUTPUT_DIR;
114
+ const outDir = output_dir || path.dirname(pdf_path);
94
115
  const baseName = path.basename(pdf_path, '.pdf').replace(/[^a-zA-Z0-9_\-]/g, '_');
95
116
  const result = await PdfService.batchFillAtCoordinates(pdf_path, field_definitions, data_rows, outDir, baseName, merge_into_single);
96
117
  return {
@@ -23,6 +23,15 @@ export declare const fillPdfSchema: {
23
23
  type: string;
24
24
  description: string;
25
25
  };
26
+ _session_id: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ _mode: {
31
+ type: string;
32
+ enum: string[];
33
+ description: string;
34
+ };
26
35
  };
27
36
  required: string[];
28
37
  };
@@ -1,8 +1,6 @@
1
1
  import * as path from 'path';
2
- import * as os from 'os';
3
2
  import { PdfService } from '../services/pdfService.js';
4
3
  import { TemplateStore } from '../services/templateStore.js';
5
- const OUTPUT_DIR = path.join(os.homedir(), '.autoform-mcp', 'output');
6
4
  export const fillPdfSchema = {
7
5
  name: 'autoform_fill_pdf',
8
6
  description: `Llena UN SOLO PDF con valores. Detecta automaticamente si tiene AcroForm o usa un template guardado.
@@ -13,8 +11,10 @@ export const fillPdfSchema = {
13
11
  - Template guardado + multiples filas → autoform_generate_batch
14
12
 
15
13
  CUANDO USAR: Solo cuando el usuario pide llenar UN documento especifico.
16
- NO USAR PARA BATCH: Nunca llames esta tool en loop para generar multiples PDFs — usa las tools batch que existen para eso.
17
- NO USAR SI: El PDF es estatico sin template → usa autoform_fill_at_coordinates.`,
14
+ NO USAR PARA BATCH: Nunca llames esta tool en loop — usa las tools batch.
15
+ NO USAR SI: El PDF es estatico sin template → usa autoform_fill_at_coordinates.
16
+
17
+ DIRECTORIO DE SALIDA: Si no se especifica output_path, guarda en el MISMO directorio del pdf_path original.`,
18
18
  inputSchema: {
19
19
  type: 'object',
20
20
  properties: {
@@ -34,6 +34,15 @@ NO USAR SI: El PDF es estatico sin template → usa autoform_fill_at_coordinates
34
34
  template_name: {
35
35
  type: 'string',
36
36
  description: '(Opcional) Nombre del template a usar para posicionar los campos. Solo necesario si el PDF no tiene AcroForm.'
37
+ },
38
+ _session_id: {
39
+ type: 'string',
40
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
41
+ },
42
+ _mode: {
43
+ type: 'string',
44
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
45
+ description: '(Opcional) Modo de autonomia experimental.'
37
46
  }
38
47
  },
39
48
  required: ['pdf_path', 'field_values']
@@ -48,7 +57,7 @@ export async function handleFillPdf(args) {
48
57
  }
49
58
  catch { /* keep as-is */ }
50
59
  }
51
- const outputFile = output_path || path.join(OUTPUT_DIR, `filled_${path.basename(pdf_path, '.pdf')}_${Date.now()}.pdf`);
60
+ const outputFile = output_path || path.join(path.dirname(pdf_path), `${path.basename(pdf_path, '.pdf')}_filled_${Date.now()}.pdf`);
52
61
  // Try AcroForm first
53
62
  const { fields, hasAcroform } = await PdfService.detectFields(pdf_path);
54
63
  if (hasAcroform && fields.length > 0) {
@@ -27,6 +27,15 @@ export declare const generateBatchSchema: {
27
27
  type: string;
28
28
  description: string;
29
29
  };
30
+ _session_id: {
31
+ type: string;
32
+ description: string;
33
+ };
34
+ _mode: {
35
+ type: string;
36
+ enum: string[];
37
+ description: string;
38
+ };
30
39
  };
31
40
  required: string[];
32
41
  };
@@ -35,6 +35,15 @@ Modos de distribucion:
35
35
  merge_into_single: {
36
36
  type: 'boolean',
37
37
  description: 'Si es true, une todos los PDFs en uno solo. Default: false.'
38
+ },
39
+ _session_id: {
40
+ type: 'string',
41
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
42
+ },
43
+ _mode: {
44
+ type: 'string',
45
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
46
+ description: '(Opcional) Modo de autonomia experimental.'
38
47
  }
39
48
  },
40
49
  required: ['template_name', 'data']
@@ -8,6 +8,15 @@ export declare const importTemplateSchema: {
8
8
  type: string;
9
9
  description: string;
10
10
  };
11
+ _session_id: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ _mode: {
16
+ type: string;
17
+ enum: string[];
18
+ description: string;
19
+ };
11
20
  };
12
21
  required: string[];
13
22
  };
@@ -11,6 +11,15 @@ ALTERNATIVA: Si no usas la app web, usa autoform_save_coordinates_as_template pa
11
11
  json_file_path: {
12
12
  type: 'string',
13
13
  description: 'Ruta absoluta al archivo JSON exportado desde la app web de AutoForm'
14
+ },
15
+ _session_id: {
16
+ type: 'string',
17
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
18
+ },
19
+ _mode: {
20
+ type: 'string',
21
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
22
+ description: '(Opcional) Modo de autonomia experimental.'
14
23
  }
15
24
  },
16
25
  required: ['json_file_path']
@@ -3,8 +3,18 @@ export declare const listTemplatesSchema: {
3
3
  description: string;
4
4
  inputSchema: {
5
5
  type: "object";
6
- properties: {};
7
- required: never[];
6
+ properties: {
7
+ _session_id: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ _mode: {
12
+ type: string;
13
+ enum: string[];
14
+ description: string;
15
+ };
16
+ };
17
+ required: string[];
8
18
  };
9
19
  };
10
20
  export declare function handleListTemplates(): Promise<{
@@ -6,7 +6,17 @@ export const listTemplatesSchema = {
6
6
  CUANDO USAR: Antes de autoform_generate_batch para verificar que el template existe y ver sus columnas requeridas.`,
7
7
  inputSchema: {
8
8
  type: 'object',
9
- properties: {},
9
+ properties: {
10
+ _session_id: {
11
+ type: 'string',
12
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
13
+ },
14
+ _mode: {
15
+ type: 'string',
16
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
17
+ description: '(Opcional) Modo de autonomia experimental.'
18
+ }
19
+ },
10
20
  required: []
11
21
  }
12
22
  };
@@ -59,6 +59,15 @@ export declare const saveCoordinatesAsTemplateSchema: {
59
59
  required: string[];
60
60
  };
61
61
  };
62
+ _session_id: {
63
+ type: string;
64
+ description: string;
65
+ };
66
+ _mode: {
67
+ type: string;
68
+ enum: string[];
69
+ description: string;
70
+ };
62
71
  };
63
72
  required: string[];
64
73
  };
@@ -45,6 +45,15 @@ FLUJO TIPICO:
45
45
  },
46
46
  required: ['label', 'page', 'x', 'y', 'width', 'height']
47
47
  }
48
+ },
49
+ _session_id: {
50
+ type: 'string',
51
+ description: '(Opcional) ID de sesion experimental para agrupar tool calls en metrics.jsonl.'
52
+ },
53
+ _mode: {
54
+ type: 'string',
55
+ enum: ['1_manual_web', '2_hybrid', '3a_acroform', '3b_vision'],
56
+ description: '(Opcional) Modo de autonomia experimental.'
48
57
  }
49
58
  },
50
59
  required: ['pdf_path', 'template_name', 'fields']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoform-mcp-server",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "MCP server for bulk PDF form filling. Detect fields, fill templates, and generate hundreds of PDFs from data — directly from Claude.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",