diff --git a/node-version/src/handlers/platformHandler.ts b/node-version/src/handlers/platformHandler.ts index bbc4433..bbe1765 100644 --- a/node-version/src/handlers/platformHandler.ts +++ b/node-version/src/handlers/platformHandler.ts @@ -325,6 +325,16 @@ export class PlatformHandler { return this._extractResult(body); } + async extractFillableFormData(fileBytes: Buffer): Promise { + const file = createBytesFile(ContentType.PDF, fileBytes, 'document.pdf'); + const { body } = await this._client.run('extractions', file, { + method: 'extract-fillable-form-data', + params: {}, + acceptFormat: 'json', + }); + return this._extractResult(body); + } + async extractPiiBoundingBoxes(fileBytes: Buffer, language: 'en' | 'es'): Promise { const file = createBytesFile(ContentType.PDF, fileBytes, 'document.pdf'); const { body } = await this._client.run('extractions', file, { diff --git a/node-version/src/tools/extractions.ts b/node-version/src/tools/extractions.ts index 333ba96..3a9e7aa 100644 --- a/node-version/src/tools/extractions.ts +++ b/node-version/src/tools/extractions.ts @@ -152,6 +152,42 @@ export function register(server: McpServer, context: AppContext): void { }, ); + server.registerTool( + 'extract_fillable_form_data', + { + description: + 'Use this tool to extract the existing AcroForm field data — field names, values, types ' + + 'and positions — from a fillable PDF, e.g. to inspect or check a form or pull out the ' + + 'data someone entered (AcroForm only, not XFA). Returns a flat formFields list as JSON; ' + + 'the name/value pairs can be used directly as input to fill_forms.', + inputSchema: { + ...singleFileInputSchema.shape, + outputTarget: outputTargetSchema, + }, + annotations: READ_ONLY('Extract Fillable Form Data'), + }, + async (args) => { + try { + const filesHandler = getDep(context, 'filesHandler'); + const platformHandler = getDep(context, 'platformHandler'); + + const inputBytes = filesHandler.read(args.inputPath); + const result = await platformHandler.extractFillableFormData(inputBytes); + + return jsonResult({ + outputTarget: args.outputTarget, + data: JSON.parse(result.toString()), + bytes: result, + filesHandler, + inputPath: args.inputPath, + stemSuffix: 'form-data', + }); + } catch (err) { + return handleToolError('extract_fillable_form_data', err); + } + }, + ); + server.registerTool( 'extract_pdf_tables', { diff --git a/node-version/tests/conversions.test.ts b/node-version/tests/conversions.test.ts index 309d931..66d1705 100644 --- a/node-version/tests/conversions.test.ts +++ b/node-version/tests/conversions.test.ts @@ -63,6 +63,7 @@ describe('convert_file tool', () => { flattenPdf: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), redactPdf: vi.fn(), watermarkPdf: vi.fn(), ocrPdf: vi.fn(), diff --git a/node-version/tests/extractions.test.ts b/node-version/tests/extractions.test.ts index 52f5a56..d24362b 100644 --- a/node-version/tests/extractions.test.ts +++ b/node-version/tests/extractions.test.ts @@ -62,6 +62,7 @@ describe('extraction tools', () => { flattenPdf: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), redactPdf: vi.fn(), watermarkPdf: vi.fn(), ocrPdf: vi.fn(), @@ -294,6 +295,60 @@ describe('extraction tools', () => { }); }); + describe('extract_fillable_form_data', () => { + it('returns form data inline by default', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.extractFillableFormData.mockResolvedValue( + Buffer.from('{"formFields":[]}'), + ); + + await caller.call( + 'extract_fillable_form_data', + { inputPath: path.join(tmpDir, 'doc.pdf') }, + { expectedResult: { data: { formFields: [] } } }, + ); + + expect(filesHandlerMock.read).toHaveBeenCalledWith(path.join(tmpDir, 'doc.pdf')); + expect(platformHandlerMock.extractFillableFormData).toHaveBeenCalledWith( + Buffer.from('pdf-bytes'), + ); + expect(filesHandlerMock.write).not.toHaveBeenCalled(); + }); + + it('writes form data to file when outputTarget is file', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.extractFillableFormData.mockResolvedValue( + Buffer.from('{"formFields":[]}'), + ); + filesHandlerMock.write.mockReturnValue(path.join(tmpDir, 'doc-form-data.json')); + + await caller.call( + 'extract_fillable_form_data', + { inputPath: path.join(tmpDir, 'doc.pdf'), outputTarget: 'file' }, + { expectedResult: { outputFilename: 'doc-form-data.json' } }, + ); + + expect(filesHandlerMock.write).toHaveBeenCalledWith( + path.join(tmpDir, 'doc.pdf'), + Buffer.from('{"formFields":[]}'), + { stemSuffix: 'form-data', ext: 'json' }, + ); + }); + + it('returns error when platform handler throws', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.extractFillableFormData.mockRejectedValue(new GenericFailedError()); + + await caller.call( + 'extract_fillable_form_data', + { inputPath: path.join(tmpDir, 'doc.pdf') }, + { expectError: true }, + ); + + expect(filesHandlerMock.write).not.toHaveBeenCalled(); + }); + }); + describe('extract_pdf_tables', () => { it('returns tables JSON inline by default when outputFormat is json', async () => { filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); diff --git a/node-version/tests/generations.test.ts b/node-version/tests/generations.test.ts index a7009e1..7ebd932 100644 --- a/node-version/tests/generations.test.ts +++ b/node-version/tests/generations.test.ts @@ -54,6 +54,7 @@ describe('generation tools', () => { extractTextBoundingBoxes: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), mergePdfs: vi.fn(), splitPdf: vi.fn(), rotatePdf: vi.fn(), diff --git a/node-version/tests/helpers/fixtures.ts b/node-version/tests/helpers/fixtures.ts index d55d0be..6655f6f 100644 --- a/node-version/tests/helpers/fixtures.ts +++ b/node-version/tests/helpers/fixtures.ts @@ -10,6 +10,7 @@ export interface MockPlatformHandler { extractTextBoundingBoxes: ReturnType; extractPiiBoundingBoxes: ReturnType; smartDetectFormFields: ReturnType; + extractFillableFormData: ReturnType; mergePdfs: ReturnType; splitPdf: ReturnType; rotatePdf: ReturnType; @@ -42,6 +43,7 @@ export function createPlatformHandlerMock(): MockPlatformHandler { extractTextBoundingBoxes: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), mergePdfs: vi.fn(), splitPdf: vi.fn(), rotatePdf: vi.fn(), diff --git a/node-version/tests/pii.test.ts b/node-version/tests/pii.test.ts index 5685b9b..f739afb 100644 --- a/node-version/tests/pii.test.ts +++ b/node-version/tests/pii.test.ts @@ -54,6 +54,7 @@ describe('PII tools', () => { extractTextBoundingBoxes: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), mergePdfs: vi.fn(), splitPdf: vi.fn(), rotatePdf: vi.fn(), diff --git a/node-version/tests/platformHandler.test.ts b/node-version/tests/platformHandler.test.ts index 1e9804e..7ad2b3c 100644 --- a/node-version/tests/platformHandler.test.ts +++ b/node-version/tests/platformHandler.test.ts @@ -125,6 +125,32 @@ describe('PlatformHandler', () => { }); }); + describe('extractFillableFormData', () => { + it('calls extract-fillable-form-data and returns extracted result', async () => { + const formData = { + formFields: [{ pageIndex: 0, fieldType: 'TextBox', name: 'field-name', value: 'value' }], + }; + const rawResponse = JSON.stringify({ result: formData }); + runMock.mockResolvedValueOnce({ + body: Buffer.from(rawResponse), + contentType: 'application/json', + }); + + const result = await handler.extractFillableFormData(Buffer.from('pdf-bytes')); + + expect(result).toEqual(Buffer.from(JSON.stringify(formData, null, 2))); + expect(runMock).toHaveBeenCalledWith( + 'extractions', + expect.objectContaining({ + kind: 'bytes', + contentType: ContentType.PDF, + name: 'document.pdf', + }), + { method: 'extract-fillable-form-data', params: {}, acceptFormat: 'json' }, + ); + }); + }); + describe('extractPiiBoundingBoxes', () => { it('calls extract-pii-bounding-boxes with language param', async () => { const rawResponse = JSON.stringify({ result: { PIIBoxes: [] } }); diff --git a/node-version/tests/rendering.test.ts b/node-version/tests/rendering.test.ts index d191e58..34535e8 100644 --- a/node-version/tests/rendering.test.ts +++ b/node-version/tests/rendering.test.ts @@ -74,6 +74,7 @@ describe('render_pdf_page tool', () => { flattenPdf: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), redactPdf: vi.fn(), watermarkPdf: vi.fn(), ocrPdf: vi.fn(), diff --git a/node-version/tests/transformations.test.ts b/node-version/tests/transformations.test.ts index 86ce792..ff2b56c 100644 --- a/node-version/tests/transformations.test.ts +++ b/node-version/tests/transformations.test.ts @@ -63,6 +63,7 @@ describe('transformation tools', () => { flattenPdf: vi.fn(), extractPiiBoundingBoxes: vi.fn(), smartDetectFormFields: vi.fn(), + extractFillableFormData: vi.fn(), redactPdf: vi.fn(), watermarkPdf: vi.fn(), ocrPdf: vi.fn(),