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 +17 -1
- package/dist/index.js +2 -2
- package/dist/services/metricsLogger.d.ts +2 -5
- package/dist/services/metricsLogger.js +96 -8
- package/dist/services/pdfTextExtractor.d.ts +50 -3
- package/dist/services/pdfTextExtractor.js +247 -0
- package/dist/tools/analyzeStaticPdf.d.ts +9 -0
- package/dist/tools/analyzeStaticPdf.js +26 -10
- package/dist/tools/detectFields.d.ts +9 -0
- package/dist/tools/detectFields.js +9 -0
- package/dist/tools/fillAtCoordinates.d.ts +18 -0
- package/dist/tools/fillAtCoordinates.js +32 -21
- package/dist/tools/fillBatchAcroForm.d.ts +9 -0
- package/dist/tools/fillBatchAcroForm.js +18 -5
- package/dist/tools/fillBatchAtCoordinates.d.ts +9 -0
- package/dist/tools/fillBatchAtCoordinates.js +31 -10
- package/dist/tools/fillPdf.d.ts +9 -0
- package/dist/tools/fillPdf.js +14 -5
- package/dist/tools/generateBatch.d.ts +9 -0
- package/dist/tools/generateBatch.js +9 -0
- package/dist/tools/importTemplate.d.ts +9 -0
- package/dist/tools/importTemplate.js +9 -0
- package/dist/tools/manageTemplates.d.ts +12 -2
- package/dist/tools/manageTemplates.js +11 -1
- package/dist/tools/saveCoordinatesAsTemplate.d.ts +9 -0
- package/dist/tools/saveCoordinatesAsTemplate.js +9 -0
- package/package.json +1 -1
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 (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
*
|
|
14
|
+
* A graphic element detected from PDF drawing operators.
|
|
15
15
|
*/
|
|
16
|
-
export interface
|
|
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
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
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),
|
|
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
|
|
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
|
|
153
|
-
: 'No hay campos obvios detectables.
|
|
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
|
|
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
|
-
⚠️
|
|
7
|
+
⚠️ PARA MULTIPLES DOCUMENTOS usa autoform_fill_batch_at_coordinates (una sola llamada, mas eficiente).
|
|
11
8
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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
|
|
18
|
+
MERGE: Si merge_into_single=true, se genera SOLO el PDF unificado.
|
|
21
19
|
|
|
22
|
-
CODIFICACION:
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
20
|
-
2.
|
|
21
|
-
3.
|
|
22
|
-
4.
|
|
23
|
-
5.
|
|
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 ||
|
|
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 {
|
package/dist/tools/fillPdf.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/tools/fillPdf.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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",
|