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 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
- const server = new Server({ name: 'autoform', version: '1.5.0' }, { capabilities: { tools: {} } });
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 handleGetPdfInfo(args);
46
+ return await withMetrics(name, args, handleGetPdfInfo);
43
47
  case 'autoform_detect_fields':
44
- return await handleDetectFields(args);
48
+ return await withMetrics(name, args, handleDetectFields);
45
49
  case 'autoform_analyze_static_pdf':
46
- return await handleAnalyzeStaticPdf(args);
50
+ return await withMetrics(name, args, handleAnalyzeStaticPdf);
47
51
  case 'autoform_fill_pdf':
48
- return await handleFillPdf(args);
52
+ return await withMetrics(name, args, handleFillPdf);
49
53
  case 'autoform_fill_at_coordinates':
50
- return await handleFillAtCoordinates(args);
54
+ return await withMetrics(name, args, handleFillAtCoordinates);
51
55
  case 'autoform_fill_batch_at_coordinates':
52
- return await handleFillBatchAtCoordinates(args);
56
+ return await withMetrics(name, args, handleFillBatchAtCoordinates);
53
57
  case 'autoform_fill_batch_acroform':
54
- return await handleFillBatchAcroForm(args);
58
+ return await withMetrics(name, args, handleFillBatchAcroForm);
55
59
  case 'autoform_save_coordinates_as_template':
56
- return await handleSaveCoordinatesAsTemplate(args);
60
+ return await withMetrics(name, args, handleSaveCoordinatesAsTemplate);
57
61
  case 'autoform_generate_batch':
58
- return await handleGenerateBatch(args);
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 handleImportTemplate(args);
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.5.0 started — 11 tools registered');
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.5.0",
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",