autoform-mcp-server 1.4.0 → 1.5.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/dist/index.js CHANGED
@@ -7,17 +7,19 @@ import { handleFillPdf, fillPdfSchema } from './tools/fillPdf.js';
7
7
  import { handleFillAtCoordinates, fillAtCoordinatesSchema, handleGetPdfInfo, getPdfInfoSchema } from './tools/fillAtCoordinates.js';
8
8
  import { handleFillBatchAtCoordinates, fillBatchAtCoordinatesSchema } from './tools/fillBatchAtCoordinates.js';
9
9
  import { handleFillBatchAcroForm, fillBatchAcroFormSchema } from './tools/fillBatchAcroForm.js';
10
+ import { handleAnalyzeStaticPdf, analyzeStaticPdfSchema } from './tools/analyzeStaticPdf.js';
10
11
  import { handleSaveCoordinatesAsTemplate, saveCoordinatesAsTemplateSchema } from './tools/saveCoordinatesAsTemplate.js';
11
12
  import { handleGenerateBatch, generateBatchSchema } from './tools/generateBatch.js';
12
13
  import { handleListTemplates, listTemplatesSchema } from './tools/manageTemplates.js';
13
14
  import { handleImportTemplate, importTemplateSchema } from './tools/importTemplate.js';
14
- const server = new Server({ name: 'autoform', version: '1.4.0' }, { capabilities: { tools: {} } });
15
+ const server = new Server({ name: 'autoform', version: '1.5.0' }, { capabilities: { tools: {} } });
15
16
  // Register all tools
16
17
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
18
  tools: [
18
19
  // Info & detection
19
20
  getPdfInfoSchema,
20
21
  detectFieldsSchema,
22
+ analyzeStaticPdfSchema,
21
23
  // Single fill
22
24
  fillPdfSchema,
23
25
  fillAtCoordinatesSchema,
@@ -40,6 +42,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
40
42
  return await handleGetPdfInfo(args);
41
43
  case 'autoform_detect_fields':
42
44
  return await handleDetectFields(args);
45
+ case 'autoform_analyze_static_pdf':
46
+ return await handleAnalyzeStaticPdf(args);
43
47
  case 'autoform_fill_pdf':
44
48
  return await handleFillPdf(args);
45
49
  case 'autoform_fill_at_coordinates':
@@ -70,4 +74,4 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
70
74
  // Start server
71
75
  const transport = new StdioServerTransport();
72
76
  await server.connect(transport);
73
- console.error('[AutoForm MCP] Server v1.4.0 started — 10 tools registered');
77
+ console.error('[AutoForm MCP] Server v1.5.0 started — 11 tools registered');
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Position of a text element in PDF coordinates (origin: bottom-left).
3
+ */
4
+ export interface TextItem {
5
+ text: string;
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ fontSize: number;
11
+ page: number;
12
+ }
13
+ /**
14
+ * Horizontal line detected as a drawing operator.
15
+ */
16
+ export interface HorizontalLine {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ page: number;
21
+ }
22
+ /**
23
+ * A page's extracted information.
24
+ */
25
+ export interface PageTextInfo {
26
+ page: number;
27
+ width: number;
28
+ height: number;
29
+ textItems: TextItem[];
30
+ totalCharacters: number;
31
+ }
32
+ /**
33
+ * Classifies a PDF based on its extractable content.
34
+ */
35
+ export type PdfContentType = 'acroform' | 'text_vectorial' | 'image_only' | 'mixed';
36
+ /**
37
+ * Extracts text content from a PDF using pdfjs-dist in Node.js-compatible mode.
38
+ * No native dependencies, works entirely in pure JavaScript.
39
+ */
40
+ export declare class PdfTextExtractor {
41
+ /** Extract all text items from all pages of a PDF */
42
+ static extractAllText(pdfPath: string): Promise<PageTextInfo[]>;
43
+ /**
44
+ * Classify a PDF based on extractable text density.
45
+ * Thresholds are empirical; can be tuned.
46
+ */
47
+ static classifyContent(pages: PageTextInfo[]): PdfContentType;
48
+ /**
49
+ * For a given point (x, y) on a page, find the text items within a certain distance.
50
+ * Distance is computed in PDF points (not pixels).
51
+ * Returns items sorted by distance (closest first).
52
+ */
53
+ static findNearbyText(pageInfo: PageTextInfo, targetX: number, targetY: number, maxDistance?: number): Array<TextItem & {
54
+ distance: number;
55
+ relativePosition: 'left' | 'right' | 'above' | 'below' | 'inside';
56
+ }>;
57
+ /**
58
+ * Find the most likely label for a field at a given position.
59
+ * Prefers text that:
60
+ * - Is to the LEFT of the field (same Y level, typical "Label: ___")
61
+ * - Is ABOVE the field (stacked layouts)
62
+ * - Ends with ':' (explicit label)
63
+ * - Is short (labels are usually 1-3 words)
64
+ */
65
+ static inferLabelForField(pageInfo: PageTextInfo, fieldX: number, fieldY: number, fieldWidth: number, fieldHeight: number): {
66
+ label: string | null;
67
+ confidence: 'high' | 'medium' | 'low';
68
+ nearbyText: string[];
69
+ };
70
+ /**
71
+ * Detect probable field locations in a static PDF (no AcroForm).
72
+ * Heuristics:
73
+ * 1. Text ending in ':' followed by horizontal space → likely label + input area
74
+ * 2. Text that looks like a label (short, ends with ':') positioned above a gap
75
+ */
76
+ static detectStaticFieldCandidates(pages: PageTextInfo[]): Array<{
77
+ page: number;
78
+ inferredLabel: string;
79
+ hintFromText: string;
80
+ suggestedX: number;
81
+ suggestedY: number;
82
+ suggestedWidth: number;
83
+ suggestedHeight: number;
84
+ confidence: 'high' | 'medium' | 'low';
85
+ reason: string;
86
+ }>;
87
+ }
@@ -0,0 +1,224 @@
1
+ import * as fs from 'fs';
2
+ // Use legacy build — works in Node without DOM/canvas
3
+ // @ts-ignore — pdfjs-dist types are incomplete for this path
4
+ import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
5
+ /**
6
+ * Extracts text content from a PDF using pdfjs-dist in Node.js-compatible mode.
7
+ * No native dependencies, works entirely in pure JavaScript.
8
+ */
9
+ export class PdfTextExtractor {
10
+ /** Extract all text items from all pages of a PDF */
11
+ static async extractAllText(pdfPath) {
12
+ const bytes = fs.readFileSync(pdfPath);
13
+ const pdf = await pdfjsLib.getDocument({
14
+ data: new Uint8Array(bytes),
15
+ useSystemFonts: true,
16
+ // Silence pdfjs warnings on stderr when running in Node
17
+ verbosity: 0
18
+ }).promise;
19
+ const pages = [];
20
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
21
+ const page = await pdf.getPage(pageNum);
22
+ const viewport = page.getViewport({ scale: 1 });
23
+ const textContent = await page.getTextContent();
24
+ const items = [];
25
+ let totalChars = 0;
26
+ for (const raw of textContent.items) {
27
+ const text = String(raw.str ?? '').trim();
28
+ if (text === '')
29
+ continue;
30
+ // transform = [scaleX, skewX, skewY, scaleY, translateX, translateY]
31
+ const [, , , scaleY, tx, ty] = raw.transform;
32
+ const fontSize = Math.abs(scaleY);
33
+ // pdfjs gives us the baseline; approximate the bounding box
34
+ // Width is already computed by pdfjs; height ≈ fontSize
35
+ const width = Math.abs(raw.width ?? 0);
36
+ const height = Math.abs(raw.height ?? fontSize);
37
+ items.push({
38
+ text,
39
+ x: tx,
40
+ y: ty, // already in PDF coordinate system (bottom-left origin)
41
+ width,
42
+ height,
43
+ fontSize,
44
+ page: pageNum
45
+ });
46
+ totalChars += text.length;
47
+ }
48
+ pages.push({
49
+ page: pageNum,
50
+ width: viewport.width,
51
+ height: viewport.height,
52
+ textItems: items,
53
+ totalCharacters: totalChars
54
+ });
55
+ }
56
+ return pages;
57
+ }
58
+ /**
59
+ * Classify a PDF based on extractable text density.
60
+ * Thresholds are empirical; can be tuned.
61
+ */
62
+ static classifyContent(pages) {
63
+ if (pages.length === 0)
64
+ return 'image_only';
65
+ const totalChars = pages.reduce((sum, p) => sum + p.totalCharacters, 0);
66
+ const totalItems = pages.reduce((sum, p) => sum + p.textItems.length, 0);
67
+ const avgCharsPerPage = totalChars / pages.length;
68
+ // Empty or near-empty: scanned image
69
+ if (totalChars < 10 || totalItems < 3)
70
+ return 'image_only';
71
+ // Low density: possibly mixed (image + some labels)
72
+ if (avgCharsPerPage < 50)
73
+ return 'mixed';
74
+ return 'text_vectorial';
75
+ }
76
+ /**
77
+ * For a given point (x, y) on a page, find the text items within a certain distance.
78
+ * Distance is computed in PDF points (not pixels).
79
+ * Returns items sorted by distance (closest first).
80
+ */
81
+ static findNearbyText(pageInfo, targetX, targetY, maxDistance = 100) {
82
+ const results = [];
83
+ for (const item of pageInfo.textItems) {
84
+ // Use the center of the text item
85
+ const itemCenterX = item.x + item.width / 2;
86
+ const itemCenterY = item.y + item.height / 2;
87
+ const dx = targetX - itemCenterX;
88
+ const dy = targetY - itemCenterY;
89
+ const distance = Math.sqrt(dx * dx + dy * dy);
90
+ if (distance > maxDistance)
91
+ continue;
92
+ // Determine relative position
93
+ let relativePosition;
94
+ const absDx = Math.abs(dx);
95
+ const absDy = Math.abs(dy);
96
+ if (absDx < item.width / 2 && absDy < item.height / 2) {
97
+ relativePosition = 'inside';
98
+ }
99
+ else if (absDx > absDy) {
100
+ relativePosition = dx > 0 ? 'left' : 'right';
101
+ }
102
+ else {
103
+ relativePosition = dy > 0 ? 'below' : 'above';
104
+ }
105
+ results.push({ ...item, distance, relativePosition });
106
+ }
107
+ return results.sort((a, b) => a.distance - b.distance);
108
+ }
109
+ /**
110
+ * Find the most likely label for a field at a given position.
111
+ * Prefers text that:
112
+ * - Is to the LEFT of the field (same Y level, typical "Label: ___")
113
+ * - Is ABOVE the field (stacked layouts)
114
+ * - Ends with ':' (explicit label)
115
+ * - Is short (labels are usually 1-3 words)
116
+ */
117
+ static inferLabelForField(pageInfo, fieldX, fieldY, fieldWidth, fieldHeight) {
118
+ // Target point: slightly to the LEFT of the field's center Y
119
+ const targetX = fieldX; // left edge of field
120
+ const targetY = fieldY + fieldHeight / 2;
121
+ const nearby = this.findNearbyText(pageInfo, targetX, targetY, 150);
122
+ if (nearby.length === 0) {
123
+ return { label: null, confidence: 'low', nearbyText: [] };
124
+ }
125
+ // Score each candidate
126
+ const scored = nearby.map(item => {
127
+ let score = 0;
128
+ // Prefer text to the LEFT
129
+ if (item.relativePosition === 'left')
130
+ score += 50;
131
+ // Secondary: above the field
132
+ else if (item.relativePosition === 'above')
133
+ score += 30;
134
+ // Penalize right/below/inside
135
+ else
136
+ score += 10;
137
+ // Closer is better (inversely proportional)
138
+ score += Math.max(0, 100 - item.distance);
139
+ // Ends with ':' → almost certainly a label
140
+ if (item.text.trim().endsWith(':'))
141
+ score += 40;
142
+ // Short text is more label-like (1-4 words)
143
+ const wordCount = item.text.trim().split(/\s+/).length;
144
+ if (wordCount >= 1 && wordCount <= 4)
145
+ score += 20;
146
+ else if (wordCount > 10)
147
+ score -= 20;
148
+ // Penalize very generic text
149
+ if (/^(página|page|de|the|and|or)$/i.test(item.text.trim()))
150
+ score -= 30;
151
+ return { item, score };
152
+ });
153
+ scored.sort((a, b) => b.score - a.score);
154
+ const best = scored[0];
155
+ const cleanLabel = best.item.text.trim().replace(/:\s*$/, '');
156
+ // Confidence based on score
157
+ let confidence;
158
+ if (best.score >= 100)
159
+ confidence = 'high';
160
+ else if (best.score >= 60)
161
+ confidence = 'medium';
162
+ else
163
+ confidence = 'low';
164
+ // Return top 5 nearby text items for context
165
+ const nearbyText = scored.slice(0, 5).map(s => s.item.text.trim());
166
+ return { label: cleanLabel, confidence, nearbyText };
167
+ }
168
+ /**
169
+ * Detect probable field locations in a static PDF (no AcroForm).
170
+ * Heuristics:
171
+ * 1. Text ending in ':' followed by horizontal space → likely label + input area
172
+ * 2. Text that looks like a label (short, ends with ':') positioned above a gap
173
+ */
174
+ static detectStaticFieldCandidates(pages) {
175
+ const candidates = [];
176
+ for (const pageInfo of pages) {
177
+ // Find all "label-like" text items (end with ':' or are short and capitalized)
178
+ const labels = pageInfo.textItems.filter(item => {
179
+ const t = item.text.trim();
180
+ if (t.length === 0 || t.length > 40)
181
+ return false;
182
+ return t.endsWith(':') || /^[A-ZÁÉÍÓÚÑ][\wáéíóúñ\s]*$/.test(t);
183
+ });
184
+ for (const label of labels) {
185
+ const labelText = label.text.trim().replace(/:\s*$/, '');
186
+ if (labelText.length < 2)
187
+ continue;
188
+ // Assume the input goes to the RIGHT of the label
189
+ // (most common "Label: ___" pattern)
190
+ const fieldX = label.x + label.width + 5;
191
+ const fieldY = label.y;
192
+ const fieldWidth = Math.max(150, (pageInfo.width - fieldX - 20) * 0.4);
193
+ const fieldHeight = label.fontSize * 1.5;
194
+ // Only suggest if there's space to the right
195
+ if (fieldX + fieldWidth > pageInfo.width)
196
+ continue;
197
+ // Check: is there text already occupying that space?
198
+ const occupied = pageInfo.textItems.some(other => {
199
+ if (other === label)
200
+ return false;
201
+ const otherCenterX = other.x + other.width / 2;
202
+ const otherCenterY = other.y + other.height / 2;
203
+ return (otherCenterX >= fieldX &&
204
+ otherCenterX <= fieldX + fieldWidth &&
205
+ Math.abs(otherCenterY - (fieldY + fieldHeight / 2)) < fieldHeight);
206
+ });
207
+ if (occupied)
208
+ continue;
209
+ candidates.push({
210
+ page: pageInfo.page,
211
+ inferredLabel: labelText,
212
+ hintFromText: label.text.trim(),
213
+ suggestedX: Math.round(fieldX),
214
+ suggestedY: Math.round(fieldY),
215
+ suggestedWidth: Math.round(fieldWidth),
216
+ suggestedHeight: Math.round(fieldHeight),
217
+ confidence: labelText.endsWith(':') ? 'high' : 'medium',
218
+ reason: `Label text "${label.text.trim()}" detected at (${Math.round(label.x)}, ${Math.round(label.y)}); suggested input area to its right.`
219
+ });
220
+ }
221
+ }
222
+ return candidates;
223
+ }
224
+ }
@@ -0,0 +1,27 @@
1
+ export declare const analyzeStaticPdfSchema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ pdf_path: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ };
12
+ required: string[];
13
+ };
14
+ };
15
+ export declare function handleAnalyzeStaticPdf(args: any): Promise<{
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ isError: boolean;
21
+ } | {
22
+ content: {
23
+ type: "text";
24
+ text: string;
25
+ }[];
26
+ isError?: undefined;
27
+ }>;
@@ -0,0 +1,157 @@
1
+ import { PdfTextExtractor } from '../services/pdfTextExtractor.js';
2
+ export const analyzeStaticPdfSchema = {
3
+ name: 'autoform_analyze_static_pdf',
4
+ description: `Analiza un PDF estatico (sin AcroForm) para sugerir donde colocar los campos automaticamente. Extrae texto con posiciones y usa heuristicas para detectar labels y areas de input probables.
5
+
6
+ CUANDO USAR:
7
+ - El PDF no tiene AcroForm (autoform_detect_fields devolvio has_acroform=false).
8
+ - Es un certificado, diploma, constancia, formulario diseñado en Canva/Word/Figma.
9
+ - Quieres sugerencias automaticas de donde van los campos sin tener que calcular coordenadas visualmente.
10
+
11
+ QUE DEVUELVE:
12
+ - Clasificacion del PDF: "text_vectorial" (texto extraible), "mixed" (texto + imagenes), "image_only" (escaneado).
13
+ - Para text_vectorial: "suggested_fields" con labels inferidos y coordenadas sugeridas.
14
+ - Para image_only: "suggested_approaches" con opciones claras para el usuario (NO rechaza el PDF).
15
+ - Texto completo con posiciones por pagina ("all_text_items").
16
+
17
+ ⚠️ CASO PDF-IMAGEN (pdf_type: "image_only"):
18
+ Si recibes este tipo, NO intentes procesar el PDF con los otros tools. En su lugar, PRESENTA AL USUARIO las suggested_approaches de forma clara y amigable, y espera su eleccion. Las opciones tipicas son:
19
+ 1. Adjuntar el PDF al chat de Claude Desktop (Claude puede verlo visualmente)
20
+ 2. Usar la app web de AutoForm para definir campos manualmente
21
+ 3. Proporcionar coordenadas manualmente si ya las conoces
22
+
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.
29
+
30
+ ⚠️ CASO mixed:
31
+ 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.`,
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ pdf_path: {
36
+ type: 'string',
37
+ description: 'Ruta absoluta al PDF estatico a analizar'
38
+ }
39
+ },
40
+ required: ['pdf_path']
41
+ }
42
+ };
43
+ export async function handleAnalyzeStaticPdf(args) {
44
+ const { pdf_path } = args;
45
+ let pages;
46
+ try {
47
+ pages = await PdfTextExtractor.extractAllText(pdf_path);
48
+ }
49
+ catch (err) {
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: JSON.stringify({
54
+ error: true,
55
+ message: `No se pudo leer el PDF: ${err instanceof Error ? err.message : String(err)}`
56
+ }, null, 2)
57
+ }],
58
+ isError: true
59
+ };
60
+ }
61
+ if (pages.length === 0) {
62
+ return {
63
+ content: [{
64
+ type: 'text',
65
+ text: JSON.stringify({
66
+ error: true,
67
+ message: 'El PDF no contiene paginas procesables.'
68
+ }, null, 2)
69
+ }],
70
+ isError: true
71
+ };
72
+ }
73
+ const contentType = PdfTextExtractor.classifyContent(pages);
74
+ // Case: image-only PDF — return actionable options, never reject
75
+ if (contentType === 'image_only') {
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ pdf_type: 'image_only',
81
+ pdf_info: {
82
+ pages: pages.length,
83
+ dimensions_pt: {
84
+ width: Math.round(pages[0].width),
85
+ height: Math.round(pages[0].height)
86
+ }
87
+ },
88
+ reason: 'Este PDF no contiene texto vectorial extraible. Probablemente es una imagen escaneada, un PDF aplanado, o un diseño donde todos los textos fueron convertidos a curvas.',
89
+ message_for_user: 'Este PDF es tipo imagen — no puedo extraer texto automaticamente para detectar los campos. Sin embargo, tenemos varias opciones para procesarlo. Elige la que prefieras:',
90
+ suggested_approaches: [
91
+ {
92
+ id: 'attach_to_chat',
93
+ title: 'Adjuntar el PDF directamente al chat',
94
+ description: 'Arrastra el PDF al chat de Claude Desktop. Podre verlo visualmente como imagen y estimar las coordenadas de los campos con buena precision. Luego usaremos autoform_fill_at_coordinates o autoform_fill_batch_at_coordinates para llenar los datos.',
95
+ effort: 'bajo',
96
+ precision: 'media-alta',
97
+ recommended: true
98
+ },
99
+ {
100
+ id: 'web_app',
101
+ title: 'Usar la app web de AutoForm',
102
+ description: 'Abre la aplicacion web de AutoForm en tu navegador, carga el PDF, dibuja los campos visualmente donde correspondan, exporta como JSON y luego usa autoform_import_template aqui. Es la opcion mas precisa para PDFs complejos.',
103
+ effort: 'medio',
104
+ precision: 'alta'
105
+ },
106
+ {
107
+ id: 'manual_coordinates',
108
+ title: 'Proporcionar coordenadas manualmente',
109
+ description: 'Si ya conoces las coordenadas exactas de los campos (por ejemplo porque trabajaste con el PDF antes), puedes pasarlas directamente a autoform_fill_at_coordinates o autoform_fill_batch_at_coordinates.',
110
+ effort: 'alto',
111
+ precision: 'exacta'
112
+ }
113
+ ],
114
+ next_instruction: 'Presenta estas opciones al usuario de forma clara (no copies el JSON literal, reformulalo conversacionalmente). Espera su eleccion antes de continuar. NO intentes procesar el PDF con otros tools hasta tener instrucciones del usuario.'
115
+ }, null, 2)
116
+ }]
117
+ };
118
+ }
119
+ // Case: text_vectorial or mixed — extract field candidates
120
+ const suggestedFields = PdfTextExtractor.detectStaticFieldCandidates(pages);
121
+ const allTextItems = pages.flatMap(p => p.textItems.map(item => ({
122
+ text: item.text,
123
+ page: item.page,
124
+ x: Math.round(item.x),
125
+ y: Math.round(item.y),
126
+ fontSize: Math.round(item.fontSize * 10) / 10
127
+ })));
128
+ const summary = suggestedFields.length > 0
129
+ ? `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.';
131
+ return {
132
+ content: [{
133
+ type: 'text',
134
+ text: JSON.stringify({
135
+ pdf_type: contentType,
136
+ pdf_info: {
137
+ pages: pages.length,
138
+ dimensions_pt: pages.map(p => ({
139
+ page: p.page,
140
+ width: Math.round(p.width),
141
+ height: Math.round(p.height)
142
+ }))
143
+ },
144
+ total_text_items: allTextItems.length,
145
+ suggested_fields: suggestedFields,
146
+ all_text_items: allTextItems.slice(0, 100), // Limit to avoid huge responses
147
+ text_items_truncated: allTextItems.length > 100,
148
+ summary,
149
+ 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.'
151
+ : 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.'
154
+ }, null, 2)
155
+ }]
156
+ };
157
+ }
@@ -1,22 +1,35 @@
1
1
  import { PdfService } from '../services/pdfService.js';
2
+ import { PdfTextExtractor } from '../services/pdfTextExtractor.js';
2
3
  export const detectFieldsSchema = {
3
4
  name: 'autoform_detect_fields',
4
- description: `Detecta campos de formulario AcroForm en un PDF interactivo (creado con Adobe Acrobat, LibreOffice, etc).
5
+ description: `Detecta campos de formulario AcroForm en un PDF interactivo Y correlaciona cada campo con su label visible extraido del texto del PDF.
5
6
 
6
- CUANDO USAR: El PDF es un formulario donde puedes hacer click y escribir. Si el PDF es una imagen/diseño estatico (certificado, diploma), esta tool dira "sin campos" — en ese caso usa autoform_get_pdf_info + analisis visual.
7
+ CUANDO USAR:
8
+ - El PDF es un formulario donde puedes hacer click y escribir (AcroForm).
9
+ - Quieres saber que campos tiene + como se llaman visualmente.
7
10
 
8
- ⚠️ IMPORTANTE — Los nombres tecnicos NO son los labels visibles:
9
- - Esta tool devuelve nombres TECNICOS internos del PDF (ej: "Text1", "fld_001", "untitled_field"), que pueden NO coincidir con los labels visibles ("Nombre:", "Apellido:", "DNI:").
10
- - NO asumas el significado de cada campo por su nombre tecnico ni por su posicion.
11
- - DESPUES de detectar los campos, DEBES mirar el PDF visualmente (ya puedes ver PDFs como imagenes) y correlacionar cada campo tecnico con su label visible usando las coordenadas (x, y, page) que devuelve esta tool.
12
- - Solo DESPUES de esa correlacion visual procede a llenar con los datos correctos.
11
+ QUE DEVUELVE:
12
+ - Lista de campos AcroForm con nombre tecnico, tipo, pagina y coordenadas.
13
+ - Para CADA campo, un "inferred_label" calculado automaticamente correlacionando la posicion del campo con el texto circundante del PDF.
14
+ - "nearby_text": hasta 5 textos cercanos al campo, ordenados por relevancia.
15
+ - "confidence": high/medium/low segun que tan claro es el label correlacionado.
13
16
 
14
- FLUJO RECOMENDADO:
15
- 1. autoform_detect_fields obtienes lista de campos tecnicos con coordenadas
16
- 2. MIRAS EL PDF VISUALMENTE asocias cada coordenada con el label visible cercano
17
- 3. Decides que dato va en que campo tecnico
18
- 4. Para UN documento: autoform_fill_pdf con field_values={"nombre_tecnico": "valor"}
19
- 5. Para MULTIPLES documentos (batch): autoform_fill_batch_acroform con data_rows y field_map si las claves son logicas.`,
17
+ ⚠️ COMO USAR EL RESULTADO:
18
+ - Si confidence es "high": confia en inferred_label y usalo directamente.
19
+ - Si confidence es "medium": el label es probable pero verifica con nearby_text si tiene sentido.
20
+ - Si confidence es "low": NO asumas pregunta al usuario o mira el PDF visualmente si es posible.
21
+ - NUNCA inventes el significado de un campo por su posicion o numero.
22
+
23
+ SI EL PDF NO TIENE ACROFORM: esta tool devolvera has_acroform=false. En ese caso usa autoform_analyze_static_pdf.
24
+
25
+ FLUJO RECOMENDADO PARA UN DOCUMENTO:
26
+ 1. autoform_detect_fields → obtienes campos + inferred_labels
27
+ 2. Usa autoform_fill_pdf con field_values={nombre_tecnico: valor}
28
+ O usa autoform_fill_pdf con field_map si prefieres claves logicas
29
+
30
+ FLUJO RECOMENDADO PARA MULTIPLES DOCUMENTOS:
31
+ 1. autoform_detect_fields → obtienes campos + inferred_labels
32
+ 2. Usa autoform_fill_batch_acroform con data_rows y field_map si las claves son logicas.`,
20
33
  inputSchema: {
21
34
  type: 'object',
22
35
  properties: {
@@ -31,21 +44,85 @@ FLUJO RECOMENDADO:
31
44
  export async function handleDetectFields(args) {
32
45
  const { pdf_path } = args;
33
46
  const { fields, hasAcroform } = await PdfService.detectFields(pdf_path);
34
- let summary;
35
- if (fields.length === 0) {
36
- summary = `El PDF "${pdf_path}" no contiene campos AcroForm.\n\nPara usar este PDF, defina los campos manualmente en la app web de AutoForm y exporte la plantilla como JSON. Luego impórtela con autoform_import_template.`;
47
+ // If no AcroForm, recommend the static analysis tool
48
+ if (!hasAcroform || fields.length === 0) {
49
+ return {
50
+ content: [{
51
+ type: 'text',
52
+ text: JSON.stringify({
53
+ has_acroform: false,
54
+ total_fields: 0,
55
+ fields: [],
56
+ message: `El PDF no contiene campos AcroForm interactivos. Use autoform_analyze_static_pdf para analizar PDFs estaticos (certificados, diplomas, formularios diseñados en Canva/Word).`,
57
+ next_action: 'autoform_analyze_static_pdf'
58
+ }, null, 2)
59
+ }]
60
+ };
61
+ }
62
+ // Extract text from the PDF to correlate with AcroForm fields
63
+ let pagesText;
64
+ try {
65
+ pagesText = await PdfTextExtractor.extractAllText(pdf_path);
66
+ }
67
+ catch (err) {
68
+ // If extraction fails, return fields without label inference
69
+ return {
70
+ content: [{
71
+ type: 'text',
72
+ text: JSON.stringify({
73
+ has_acroform: true,
74
+ total_fields: fields.length,
75
+ fields,
76
+ text_extraction_error: err instanceof Error ? err.message : String(err),
77
+ warning: 'No se pudo extraer texto del PDF para inferir labels. Los nombres tecnicos se devuelven sin correlacion visual.'
78
+ }, null, 2)
79
+ }]
80
+ };
81
+ }
82
+ // Enrich each field with inferred label and nearby text
83
+ const enrichedFields = fields.map(field => {
84
+ const pageInfo = pagesText.find(p => p.page === field.page);
85
+ if (!pageInfo) {
86
+ return { ...field, inferred_label: null, confidence: 'low', nearby_text: [] };
87
+ }
88
+ const { label, confidence, nearbyText } = PdfTextExtractor.inferLabelForField(pageInfo, field.x, field.y, field.width, field.height);
89
+ return {
90
+ ...field,
91
+ inferred_label: label,
92
+ confidence,
93
+ nearby_text: nearbyText
94
+ };
95
+ });
96
+ // Build a summary that emphasizes inferred labels
97
+ const labelSummary = enrichedFields
98
+ .map(f => {
99
+ const labelDisplay = f.inferred_label
100
+ ? `"${f.inferred_label}" (${f.confidence})`
101
+ : '[sin label inferido]';
102
+ return `- ${f.name}: ${labelDisplay} — pag ${f.page}, pos (${Math.round(f.x)}, ${Math.round(f.y)}), ${Math.round(f.width)}x${Math.round(f.height)}`;
103
+ })
104
+ .join('\n');
105
+ const highConfidenceCount = enrichedFields.filter(f => f.confidence === 'high').length;
106
+ const lowConfidenceCount = enrichedFields.filter(f => f.confidence === 'low').length;
107
+ let guidance;
108
+ if (lowConfidenceCount === 0) {
109
+ guidance = 'Todos los campos tienen labels correlacionados con alta confianza. Puedes proceder a llenar usando los inferred_label directamente.';
110
+ }
111
+ else if (lowConfidenceCount < enrichedFields.length) {
112
+ guidance = `${highConfidenceCount} campos tienen labels claros, pero ${lowConfidenceCount} tienen baja confianza. Verifica los nearby_text antes de llenar.`;
37
113
  }
38
114
  else {
39
- summary = `Encontrados ${fields.length} campos AcroForm:\n\n${fields.map(f => `- ${f.name} (${f.type}) Pag ${f.page}, pos: ${Math.round(f.x)},${Math.round(f.y)} ${Math.round(f.width)}x${Math.round(f.height)}${f.value ? ` = "${f.value}"` : ''}`).join('\n')}`;
115
+ guidance = 'Los labels inferidos tienen baja confianza. Revisa el nearby_text de cada campo o pregunta al usuario que represente cada uno antes de llenar.';
40
116
  }
41
117
  return {
42
118
  content: [{
43
119
  type: 'text',
44
120
  text: JSON.stringify({
45
- has_acroform: hasAcroform,
121
+ has_acroform: true,
46
122
  total_fields: fields.length,
47
- fields,
48
- summary
123
+ fields: enrichedFields,
124
+ summary: labelSummary,
125
+ guidance
49
126
  }, null, 2)
50
127
  }]
51
128
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoform-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -41,10 +41,11 @@
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.12.1",
43
43
  "jszip": "^3.10.1",
44
- "pdf-lib": "^1.17.1"
44
+ "pdf-lib": "^1.17.1",
45
+ "pdfjs-dist": "^4.10.38"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/node": "^20.0.0",
48
49
  "typescript": "^5.2.2"
49
50
  }
50
- }
51
+ }