autoform-mcp-server 1.5.0 → 1.6.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 +18 -14
- package/dist/services/metricsLogger.d.ts +28 -0
- package/dist/services/metricsLogger.js +267 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,7 +12,8 @@ import { handleSaveCoordinatesAsTemplate, saveCoordinatesAsTemplateSchema } from
|
|
|
12
12
|
import { handleGenerateBatch, generateBatchSchema } from './tools/generateBatch.js';
|
|
13
13
|
import { handleListTemplates, listTemplatesSchema } from './tools/manageTemplates.js';
|
|
14
14
|
import { handleImportTemplate, importTemplateSchema } from './tools/importTemplate.js';
|
|
15
|
-
|
|
15
|
+
import { withMetrics } from './services/metricsLogger.js';
|
|
16
|
+
const server = new Server({ name: 'autoform', version: '1.6.0' }, { capabilities: { tools: {} } });
|
|
16
17
|
// Register all tools
|
|
17
18
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
18
19
|
tools: [
|
|
@@ -33,33 +34,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
33
34
|
importTemplateSchema
|
|
34
35
|
]
|
|
35
36
|
}));
|
|
36
|
-
// Handle tool calls
|
|
37
|
+
// Handle tool calls — every handler is wrapped with metrics instrumentation.
|
|
38
|
+
// withMetrics() captures tool name, wall-clock duration, input/output stats,
|
|
39
|
+
// and success/failure — writing one JSONL line per call to ~/.autoform-mcp/metrics.jsonl
|
|
40
|
+
// It NEVER modifies handler behavior and NEVER throws from the logger.
|
|
37
41
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
38
42
|
const { name, arguments: args } = request.params;
|
|
39
43
|
try {
|
|
40
44
|
switch (name) {
|
|
41
45
|
case 'autoform_get_pdf_info':
|
|
42
|
-
return await
|
|
46
|
+
return await withMetrics(name, args, handleGetPdfInfo);
|
|
43
47
|
case 'autoform_detect_fields':
|
|
44
|
-
return await
|
|
48
|
+
return await withMetrics(name, args, handleDetectFields);
|
|
45
49
|
case 'autoform_analyze_static_pdf':
|
|
46
|
-
return await
|
|
50
|
+
return await withMetrics(name, args, handleAnalyzeStaticPdf);
|
|
47
51
|
case 'autoform_fill_pdf':
|
|
48
|
-
return await
|
|
52
|
+
return await withMetrics(name, args, handleFillPdf);
|
|
49
53
|
case 'autoform_fill_at_coordinates':
|
|
50
|
-
return await
|
|
54
|
+
return await withMetrics(name, args, handleFillAtCoordinates);
|
|
51
55
|
case 'autoform_fill_batch_at_coordinates':
|
|
52
|
-
return await
|
|
56
|
+
return await withMetrics(name, args, handleFillBatchAtCoordinates);
|
|
53
57
|
case 'autoform_fill_batch_acroform':
|
|
54
|
-
return await
|
|
58
|
+
return await withMetrics(name, args, handleFillBatchAcroForm);
|
|
55
59
|
case 'autoform_save_coordinates_as_template':
|
|
56
|
-
return await
|
|
60
|
+
return await withMetrics(name, args, handleSaveCoordinatesAsTemplate);
|
|
57
61
|
case 'autoform_generate_batch':
|
|
58
|
-
return await
|
|
62
|
+
return await withMetrics(name, args, handleGenerateBatch);
|
|
59
63
|
case 'autoform_list_templates':
|
|
60
|
-
return await handleListTemplates();
|
|
64
|
+
return await withMetrics(name, args, async () => handleListTemplates());
|
|
61
65
|
case 'autoform_import_template':
|
|
62
|
-
return await
|
|
66
|
+
return await withMetrics(name, args, handleImportTemplate);
|
|
63
67
|
default:
|
|
64
68
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
65
69
|
}
|
|
@@ -74,4 +78,4 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
74
78
|
// Start server
|
|
75
79
|
const transport = new StdioServerTransport();
|
|
76
80
|
await server.connect(transport);
|
|
77
|
-
console.error('[AutoForm MCP] Server v1.
|
|
81
|
+
console.error('[AutoForm MCP] Server v1.6.0 started — 11 tools registered (metrics enabled)');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface MetricsEvent {
|
|
2
|
+
ts: string;
|
|
3
|
+
tool: string;
|
|
4
|
+
duration_ms: number;
|
|
5
|
+
success: boolean;
|
|
6
|
+
error_message?: string;
|
|
7
|
+
session_id?: string;
|
|
8
|
+
input?: Record<string, number | string | boolean>;
|
|
9
|
+
output?: Record<string, number | string | boolean>;
|
|
10
|
+
}
|
|
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
|
+
export declare function withMetrics<T>(toolName: string, args: any, handler: (args: any) => Promise<T>): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Generate a new random session ID (not used automatically — exported for future use).
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateSessionId(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Read all metrics events from the log file (for analysis/debugging).
|
|
22
|
+
* Returns empty array if file doesn't exist or is unreadable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readAllEvents(): MetricsEvent[];
|
|
25
|
+
/**
|
|
26
|
+
* Get the path to the metrics file (for documentation / user info).
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMetricsFilePath(): string;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Append-only JSONL metrics logger for the AutoForm MCP server.
|
|
7
|
+
*
|
|
8
|
+
* Records one line per tool call with:
|
|
9
|
+
* - Timestamp (ISO 8601)
|
|
10
|
+
* - Tool name
|
|
11
|
+
* - Wall-clock duration in milliseconds
|
|
12
|
+
* - Input/output summary (sizes, key counts — no raw data for privacy)
|
|
13
|
+
* - Error flag
|
|
14
|
+
* - Optional session_id (Claude can pass it to group calls of the same task)
|
|
15
|
+
*
|
|
16
|
+
* Design principles:
|
|
17
|
+
* - NEVER throws: a failure to log must never break a tool call.
|
|
18
|
+
* - NEVER reads or modifies existing lines: pure append to preserve integrity.
|
|
19
|
+
* - NEVER records raw data (PDF contents, personal data, values): only structural counts.
|
|
20
|
+
* - Can be disabled via AUTOFORM_METRICS_DISABLED=1 environment variable.
|
|
21
|
+
*/
|
|
22
|
+
const METRICS_DIR = path.join(os.homedir(), '.autoform-mcp');
|
|
23
|
+
const METRICS_FILE = path.join(METRICS_DIR, 'metrics.jsonl');
|
|
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
|
+
function summarizeInput(toolName, args) {
|
|
30
|
+
const summary = {};
|
|
31
|
+
if (!args || typeof args !== 'object')
|
|
32
|
+
return summary;
|
|
33
|
+
// pdf_path: record only basename, not full path (privacy)
|
|
34
|
+
if (typeof args.pdf_path === 'string') {
|
|
35
|
+
summary.pdf_basename = path.basename(args.pdf_path);
|
|
36
|
+
}
|
|
37
|
+
if (typeof args.json_file_path === 'string') {
|
|
38
|
+
summary.json_basename = path.basename(args.json_file_path);
|
|
39
|
+
}
|
|
40
|
+
// Count arrays (core metric: how many fields/rows defined manually)
|
|
41
|
+
const fields = Array.isArray(args.fields) ? args.fields
|
|
42
|
+
: typeof args.fields === 'string' ? tryParseArray(args.fields) : null;
|
|
43
|
+
if (fields)
|
|
44
|
+
summary.fields_manual = fields.length;
|
|
45
|
+
const fieldDefs = Array.isArray(args.field_definitions) ? args.field_definitions
|
|
46
|
+
: typeof args.field_definitions === 'string' ? tryParseArray(args.field_definitions) : null;
|
|
47
|
+
if (fieldDefs)
|
|
48
|
+
summary.field_definitions_count = fieldDefs.length;
|
|
49
|
+
const dataRows = Array.isArray(args.data_rows) ? args.data_rows
|
|
50
|
+
: typeof args.data_rows === 'string' ? tryParseArray(args.data_rows) : null;
|
|
51
|
+
if (dataRows)
|
|
52
|
+
summary.data_rows_count = dataRows.length;
|
|
53
|
+
const data = Array.isArray(args.data) ? args.data
|
|
54
|
+
: typeof args.data === 'string' ? tryParseArray(args.data) : null;
|
|
55
|
+
if (data)
|
|
56
|
+
summary.data_rows_count = data.length;
|
|
57
|
+
// Field values (for fill_pdf)
|
|
58
|
+
if (args.field_values && typeof args.field_values === 'object') {
|
|
59
|
+
summary.field_values_count = Object.keys(args.field_values).length;
|
|
60
|
+
}
|
|
61
|
+
// field_map (for batch AcroForm)
|
|
62
|
+
if (args.field_map && typeof args.field_map === 'object') {
|
|
63
|
+
summary.field_map_count = Object.keys(args.field_map).length;
|
|
64
|
+
}
|
|
65
|
+
// Flags
|
|
66
|
+
if (typeof args.merge_into_single === 'boolean') {
|
|
67
|
+
summary.merge_into_single = args.merge_into_single;
|
|
68
|
+
}
|
|
69
|
+
if (typeof args.distribution_mode === 'string') {
|
|
70
|
+
summary.distribution_mode = args.distribution_mode;
|
|
71
|
+
}
|
|
72
|
+
if (typeof args.template_name === 'string') {
|
|
73
|
+
summary.template_name = args.template_name;
|
|
74
|
+
}
|
|
75
|
+
return summary;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Extract structural stats from the MCP tool response.
|
|
79
|
+
* Parses the returned JSON text to extract counts (documents_generated, total_fields, etc.).
|
|
80
|
+
*/
|
|
81
|
+
function summarizeOutput(result) {
|
|
82
|
+
const summary = {};
|
|
83
|
+
try {
|
|
84
|
+
const contentArray = result?.content;
|
|
85
|
+
if (!Array.isArray(contentArray) || contentArray.length === 0)
|
|
86
|
+
return summary;
|
|
87
|
+
const firstText = contentArray[0]?.text;
|
|
88
|
+
if (typeof firstText !== 'string')
|
|
89
|
+
return summary;
|
|
90
|
+
const parsed = JSON.parse(firstText);
|
|
91
|
+
// Common output fields
|
|
92
|
+
if (typeof parsed.documents_generated === 'number') {
|
|
93
|
+
summary.documents_generated = parsed.documents_generated;
|
|
94
|
+
}
|
|
95
|
+
if (typeof parsed.fields_filled === 'number') {
|
|
96
|
+
summary.fields_filled = parsed.fields_filled;
|
|
97
|
+
}
|
|
98
|
+
if (typeof parsed.total_fields === 'number') {
|
|
99
|
+
summary.total_fields = parsed.total_fields;
|
|
100
|
+
}
|
|
101
|
+
if (typeof parsed.has_acroform === 'boolean') {
|
|
102
|
+
summary.has_acroform = parsed.has_acroform;
|
|
103
|
+
}
|
|
104
|
+
if (typeof parsed.pdf_type === 'string') {
|
|
105
|
+
summary.pdf_type = parsed.pdf_type;
|
|
106
|
+
}
|
|
107
|
+
if (typeof parsed.total_pages === 'number') {
|
|
108
|
+
summary.total_pages = parsed.total_pages;
|
|
109
|
+
}
|
|
110
|
+
if (typeof parsed.total_text_items === 'number') {
|
|
111
|
+
summary.total_text_items = parsed.total_text_items;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(parsed.output_paths)) {
|
|
114
|
+
summary.output_paths_count = parsed.output_paths.length;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(parsed.suggested_fields)) {
|
|
117
|
+
summary.suggested_fields_count = parsed.suggested_fields.length;
|
|
118
|
+
}
|
|
119
|
+
if (typeof parsed.merged_path === 'string') {
|
|
120
|
+
summary.has_merged = true;
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(parsed.errors)) {
|
|
123
|
+
summary.errors_count = parsed.errors.length;
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(parsed.templates)) {
|
|
126
|
+
summary.templates_count = parsed.templates.length;
|
|
127
|
+
}
|
|
128
|
+
// Count fields in detect_fields response
|
|
129
|
+
if (Array.isArray(parsed.fields)) {
|
|
130
|
+
summary.fields_returned = parsed.fields.length;
|
|
131
|
+
// Count confidence levels for label inference
|
|
132
|
+
const high = parsed.fields.filter((f) => f.confidence === 'high').length;
|
|
133
|
+
const low = parsed.fields.filter((f) => f.confidence === 'low').length;
|
|
134
|
+
if (high > 0)
|
|
135
|
+
summary.fields_high_confidence = high;
|
|
136
|
+
if (low > 0)
|
|
137
|
+
summary.fields_low_confidence = low;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// If response is not JSON, just skip output summarization
|
|
142
|
+
}
|
|
143
|
+
return summary;
|
|
144
|
+
}
|
|
145
|
+
function tryParseArray(s) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(s);
|
|
148
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function ensureDirExists() {
|
|
155
|
+
try {
|
|
156
|
+
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function writeEvent(event) {
|
|
163
|
+
if (METRICS_DISABLED)
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
ensureDirExists();
|
|
167
|
+
const line = JSON.stringify(event) + '\n';
|
|
168
|
+
fs.appendFileSync(METRICS_FILE, line, 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// NEVER throw from the logger — a failed log must not break a tool call
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Wrap a tool handler to automatically measure and log metrics.
|
|
176
|
+
* The handler's behavior is unchanged; metrics are captured transparently.
|
|
177
|
+
*/
|
|
178
|
+
export async function withMetrics(toolName, args, handler) {
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const sessionId = typeof args?._session_id === 'string' ? args._session_id : undefined;
|
|
181
|
+
// Strip _session_id from args before passing to handler (internal only)
|
|
182
|
+
const cleanArgs = args && typeof args === 'object' && '_session_id' in args
|
|
183
|
+
? (() => { const { _session_id, ...rest } = args; return rest; })()
|
|
184
|
+
: args;
|
|
185
|
+
let result;
|
|
186
|
+
let success = true;
|
|
187
|
+
let errorMessage;
|
|
188
|
+
try {
|
|
189
|
+
result = await handler(cleanArgs);
|
|
190
|
+
// Check if the MCP result indicates an error
|
|
191
|
+
const isErr = result?.isError === true;
|
|
192
|
+
if (isErr) {
|
|
193
|
+
success = false;
|
|
194
|
+
const errText = result?.content?.[0]?.text;
|
|
195
|
+
if (typeof errText === 'string') {
|
|
196
|
+
errorMessage = errText.substring(0, 200);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
success = false;
|
|
202
|
+
errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 200);
|
|
203
|
+
// Still log the event, then rethrow
|
|
204
|
+
const event = {
|
|
205
|
+
ts: new Date().toISOString(),
|
|
206
|
+
tool: toolName,
|
|
207
|
+
duration_ms: Date.now() - startTime,
|
|
208
|
+
success: false,
|
|
209
|
+
error_message: errorMessage,
|
|
210
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
211
|
+
input: summarizeInput(toolName, cleanArgs)
|
|
212
|
+
};
|
|
213
|
+
writeEvent(event);
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
const event = {
|
|
217
|
+
ts: new Date().toISOString(),
|
|
218
|
+
tool: toolName,
|
|
219
|
+
duration_ms: Date.now() - startTime,
|
|
220
|
+
success,
|
|
221
|
+
...(errorMessage ? { error_message: errorMessage } : {}),
|
|
222
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
223
|
+
input: summarizeInput(toolName, cleanArgs),
|
|
224
|
+
output: summarizeOutput(result)
|
|
225
|
+
};
|
|
226
|
+
writeEvent(event);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Generate a new random session ID (not used automatically — exported for future use).
|
|
231
|
+
*/
|
|
232
|
+
export function generateSessionId() {
|
|
233
|
+
return crypto.randomBytes(8).toString('hex');
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Read all metrics events from the log file (for analysis/debugging).
|
|
237
|
+
* Returns empty array if file doesn't exist or is unreadable.
|
|
238
|
+
*/
|
|
239
|
+
export function readAllEvents() {
|
|
240
|
+
try {
|
|
241
|
+
if (!fs.existsSync(METRICS_FILE))
|
|
242
|
+
return [];
|
|
243
|
+
const content = fs.readFileSync(METRICS_FILE, 'utf-8');
|
|
244
|
+
const events = [];
|
|
245
|
+
for (const line of content.split('\n')) {
|
|
246
|
+
const trimmed = line.trim();
|
|
247
|
+
if (!trimmed)
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
events.push(JSON.parse(trimmed));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// skip malformed lines
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return events;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get the path to the metrics file (for documentation / user info).
|
|
264
|
+
*/
|
|
265
|
+
export function getMetricsFilePath() {
|
|
266
|
+
return METRICS_FILE;
|
|
267
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autoform-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|