faxseal-mcp 1.0.0 → 2.0.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.
Files changed (2) hide show
  1. package/index.js +276 -99
  2. package/package.json +24 -3
package/index.js CHANGED
@@ -6,8 +6,9 @@ import {
6
6
  ListToolsRequestSchema,
7
7
  } from '@modelcontextprotocol/sdk/types.js';
8
8
 
9
- const BASE_URL = process.env.FAXSEAL_BASE_URL ?? 'https://faxseal.com';
9
+ const BASE_URL = (process.env.FAXSEAL_BASE_URL ?? 'https://faxseal.com').replace(/\/+$/, '');
10
10
  const API_KEY = process.env.FAXSEAL_API_KEY ?? '';
11
+ const FETCH_TIMEOUT_MS = 30_000;
11
12
 
12
13
  if (!API_KEY) {
13
14
  process.stderr.write('Error: FAXSEAL_API_KEY environment variable is required.\n');
@@ -16,123 +17,299 @@ if (!API_KEY) {
16
17
 
17
18
  function authHeaders() {
18
19
  return {
19
- 'Authorization': `Bearer ${API_KEY}`,
20
+ Authorization: `Bearer ${API_KEY}`,
20
21
  'Content-Type': 'application/json',
21
22
  };
22
23
  }
23
24
 
24
25
  async function apiFetch(path, options = {}) {
25
- const res = await fetch(`${BASE_URL}${path}`, options);
26
- const json = await res.json();
27
- if (!res.ok) throw new Error(json.error ?? `HTTP ${res.status}`);
28
- return json;
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
28
+ try {
29
+ const res = await fetch(`${BASE_URL}${path}`, { ...options, signal: controller.signal });
30
+ const contentType = res.headers.get('content-type') ?? '';
31
+ const body = contentType.includes('application/json')
32
+ ? await res.json().catch(() => null)
33
+ : await res.text().catch(() => '');
34
+ if (!res.ok) {
35
+ const message = body && typeof body === 'object' && 'error' in body
36
+ ? body.error
37
+ : typeof body === 'string' && body.trim()
38
+ ? body.trim()
39
+ : `HTTP ${res.status}`;
40
+ throw new Error(String(message));
41
+ }
42
+ const json = body && typeof body === 'object' ? body : {};
43
+ return json;
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
29
47
  }
30
48
 
31
- const server = new Server(
32
- { name: 'faxseal-mcp', version: '1.0.0' },
33
- { capabilities: { tools: {} } },
34
- );
49
+ // Filter out falsy values so tool handlers can use `condition && 'line'` inline.
50
+ function ok(lines) {
51
+ return { content: [{ type: 'text', text: lines.filter(Boolean).join('\n') }] };
52
+ }
35
53
 
36
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
- tools: [
38
- {
39
- name: 'send_fax',
40
- description: 'Send a fax from a publicly accessible PDF URL. Returns a job ID for tracking.',
41
- inputSchema: {
42
- type: 'object',
43
- required: ['to', 'file_url'],
44
- properties: {
45
- to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
46
- file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file' },
47
- from_name: { type: 'string', description: 'Sender name for the cover page (optional)' },
48
- from_company: { type: 'string', description: 'Sender company for the cover page (optional)' },
49
- subject: { type: 'string', description: 'Fax subject line (optional)' },
50
- notes: { type: 'string', description: 'Cover page notes (optional)' },
51
- },
54
+ function toolErr(message) {
55
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Tool handlers — each receives validated args, returns MCP content.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ const handlers = {
63
+ send_fax: async (args) => {
64
+ if (!args.to) throw new Error('to is required');
65
+ if (!args.file_url) throw new Error('file_url is required');
66
+
67
+ const data = await apiFetch('/api/zapier/send', {
68
+ method: 'POST',
69
+ headers: authHeaders(),
70
+ body: JSON.stringify({
71
+ to: args.to,
72
+ file_url: args.file_url,
73
+ from_name: args.from_name,
74
+ from_company: args.from_company,
75
+ subject: args.subject,
76
+ notes: args.notes,
77
+ }),
78
+ });
79
+
80
+ return ok([
81
+ 'Fax queued.',
82
+ `Job ID: ${data.id}`,
83
+ `To: ${data.to_number}`,
84
+ `Pages: ${data.pages}`,
85
+ `Credits used: ${data.credits_used} (${data.credits_remaining} remaining)`,
86
+ `Track: ${data.track_url}`,
87
+ ]);
88
+ },
89
+
90
+ send_fax_text: async (args) => {
91
+ if (!args.to) throw new Error('to is required');
92
+ if (!args.content) throw new Error('content is required');
93
+
94
+ const data = await apiFetch('/api/v1/fax/text', {
95
+ method: 'POST',
96
+ headers: authHeaders(),
97
+ body: JSON.stringify({
98
+ to: args.to,
99
+ content: args.content,
100
+ subject: args.subject,
101
+ from_name: args.from_name,
102
+ from_company: args.from_company,
103
+ }),
104
+ });
105
+
106
+ return ok([
107
+ 'Fax queued.',
108
+ `Job ID: ${data.id}`,
109
+ `To: ${data.to_number}`,
110
+ `Pages: ${data.pages}`,
111
+ `Credits used: ${data.credits_used} (${data.credits_remaining} remaining)`,
112
+ `Track: ${data.track_url}`,
113
+ ]);
114
+ },
115
+
116
+ get_fax_status: async (args) => {
117
+ if (!args.job_id) throw new Error('job_id is required');
118
+
119
+ const data = await apiFetch(`/api/v1/fax/${encodeURIComponent(args.job_id)}`, {
120
+ headers: authHeaders(),
121
+ });
122
+
123
+ return ok([
124
+ `Status: ${data.status}`,
125
+ `To: ${data.toNumber}`,
126
+ `Pages: ${data.pages}`,
127
+ data.creditsUsed != null && `Credits used: ${data.creditsUsed}`,
128
+ data.failureReason && `Failure reason: ${data.failureReason}`,
129
+ data.refundStatus && `Refund: ${data.refundStatus}`,
130
+ data.certReceiptUrl && `Receipt: ${data.certReceiptUrl}`,
131
+ `Sent: ${data.createdAt}`,
132
+ `Updated: ${data.updatedAt}`,
133
+ ]);
134
+ },
135
+
136
+ get_credits: async () => {
137
+ const data = await apiFetch('/api/user/credits', { headers: authHeaders() });
138
+ return ok([`Credit balance: ${data.credits}`]);
139
+ },
140
+
141
+ list_received_faxes: async (args) => {
142
+ const params = new URLSearchParams();
143
+ if (args.since) params.set('since', args.since);
144
+ const qs = params.size ? `?${params}` : '';
145
+
146
+ const faxes = await apiFetch(`/api/zapier/inbox${qs}`, { headers: authHeaders() });
147
+
148
+ if (!faxes.length) return ok(['No received faxes found.']);
149
+
150
+ const lines = [`${faxes.length} received fax(es):`, ''];
151
+ for (let i = 0; i < faxes.length; i++) {
152
+ const f = faxes[i];
153
+ if (i > 0) lines.push('─'.repeat(40));
154
+ lines.push(
155
+ `ID: ${f.id}`,
156
+ `From: ${f.from_number} To: ${f.to_number} Pages: ${f.pages}`,
157
+ `Received: ${f.received_at}`,
158
+ f.ocr_text
159
+ ? `Text preview: ${f.ocr_text.slice(0, 200)}${f.ocr_text.length > 200 ? '…' : ''}`
160
+ : 'OCR: not available',
161
+ );
162
+ }
163
+
164
+ return ok(lines);
165
+ },
166
+
167
+ verify_fax: async (args) => {
168
+ if (!args.job_id) throw new Error('job_id is required');
169
+
170
+ const data = await apiFetch(`/api/v1/fax/${encodeURIComponent(args.job_id)}/verify`, {
171
+ headers: authHeaders(),
172
+ });
173
+
174
+ if (!data.attested) {
175
+ return ok([`Job ${args.job_id}: not yet attested (status: ${data.status})`]);
176
+ }
177
+
178
+ const lines = [
179
+ `Job ID: ${data.jobId}`,
180
+ `Status: ${data.status}`,
181
+ data.documentHash && `Document hash (SHA-256): ${data.documentHash}`,
182
+ ];
183
+
184
+ if (data.send) {
185
+ lines.push(
186
+ '',
187
+ 'Send attestation:',
188
+ ` Verified: ${data.send.verified}`,
189
+ ` Rekor: ${data.send.rekorUrl}`,
190
+ data.send.error && ` Error: ${data.send.error}`,
191
+ );
192
+ }
193
+
194
+ if (data.delivery) {
195
+ lines.push(
196
+ '',
197
+ 'Delivery attestation:',
198
+ ` Verified: ${data.delivery.verified}`,
199
+ ` Rekor: ${data.delivery.rekorUrl}`,
200
+ data.delivery.error && ` Error: ${data.delivery.error}`,
201
+ );
202
+ }
203
+
204
+ return ok(lines);
205
+ },
206
+ };
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Tool definitions (schema exposed to MCP clients)
210
+ // ---------------------------------------------------------------------------
211
+
212
+ const TOOL_DEFS = [
213
+ {
214
+ name: 'send_fax',
215
+ description: 'Send a fax from a publicly accessible PDF URL. Returns a job ID for status tracking.',
216
+ inputSchema: {
217
+ type: 'object',
218
+ required: ['to', 'file_url'],
219
+ properties: {
220
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
221
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file' },
222
+ from_name: { type: 'string', description: 'Sender name for the cover page (optional)' },
223
+ from_company: { type: 'string', description: 'Sender company for the cover page (optional)' },
224
+ subject: { type: 'string', description: 'Fax subject line (optional)' },
225
+ notes: { type: 'string', description: 'Cover page notes (optional)' },
52
226
  },
53
227
  },
54
- {
55
- name: 'get_fax_status',
56
- description: 'Check the delivery status of a fax. Status: queued → sending → delivered or failed.',
57
- inputSchema: {
58
- type: 'object',
59
- required: ['job_id'],
60
- properties: {
61
- job_id: { type: 'string', description: 'Fax job ID returned by send_fax' },
228
+ },
229
+ {
230
+ name: 'send_fax_text',
231
+ description:
232
+ 'Convert plain text to a PDF and send it as a fax. Ideal when the content is generated at runtime rather than stored as a file.',
233
+ inputSchema: {
234
+ type: 'object',
235
+ required: ['to', 'content'],
236
+ properties: {
237
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
238
+ content: { type: 'string', description: 'Plain-text body of the fax (max 50,000 characters). Will be typeset into a PDF automatically.' },
239
+ subject: { type: 'string', description: 'Fax subject line, appears as a heading (optional)' },
240
+ from_name: { type: 'string', description: 'Sender name (optional)' },
241
+ from_company: { type: 'string', description: 'Sender company (optional)' },
242
+ },
243
+ },
244
+ },
245
+ {
246
+ name: 'get_fax_status',
247
+ description:
248
+ 'Check the delivery status of a sent fax. Status progresses: queued → sending → delivered or failed.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ required: ['job_id'],
252
+ properties: {
253
+ job_id: { type: 'string', description: 'Fax job ID returned by send_fax or send_fax_text' },
254
+ },
255
+ },
256
+ },
257
+ {
258
+ name: 'get_credits',
259
+ description: 'Check the current FaxSeal credit balance for this API token.',
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {},
263
+ },
264
+ },
265
+ {
266
+ name: 'list_received_faxes',
267
+ description:
268
+ 'List faxes received in the inbox, newest first (up to 50). Includes OCR text when available.',
269
+ inputSchema: {
270
+ type: 'object',
271
+ properties: {
272
+ since: {
273
+ type: 'string',
274
+ description:
275
+ 'ISO 8601 date-time to return only faxes received after this point (optional). Defaults to the last 7 days.',
62
276
  },
63
277
  },
64
278
  },
65
- {
66
- name: 'get_credits',
67
- description: 'Check the current FaxSeal credit balance for this API token.',
68
- inputSchema: {
69
- type: 'object',
70
- properties: {},
279
+ },
280
+ {
281
+ name: 'verify_fax',
282
+ description:
283
+ 'Verify the cryptographic attestation of a fax via Sigstore/Rekor. Confirms the document was transmitted without tampering and provides immutable audit proof.',
284
+ inputSchema: {
285
+ type: 'object',
286
+ required: ['job_id'],
287
+ properties: {
288
+ job_id: { type: 'string', description: 'Fax job ID to verify' },
71
289
  },
72
290
  },
73
- ],
74
- }));
291
+ },
292
+ ];
75
293
 
76
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
77
- const { name, arguments: args } = req.params;
294
+ // ---------------------------------------------------------------------------
295
+ // Server wiring
296
+ // ---------------------------------------------------------------------------
78
297
 
79
- try {
80
- if (name === 'send_fax') {
81
- const data = await apiFetch('/api/zapier/send', {
82
- method: 'POST',
83
- headers: authHeaders(),
84
- body: JSON.stringify({
85
- to: args.to,
86
- file_url: args.file_url,
87
- from_name: args.from_name,
88
- from_company: args.from_company,
89
- subject: args.subject,
90
- notes: args.notes,
91
- }),
92
- });
93
- return {
94
- content: [{
95
- type: 'text',
96
- text: [
97
- `✅ Fax queued successfully.`,
98
- `Job ID: ${data.id}`,
99
- `To: ${data.to_number}`,
100
- `Pages: ${data.pages}`,
101
- `Credits used: ${data.credits_used} (${data.credits_remaining} remaining)`,
102
- `Track: ${data.track_url}`,
103
- ].join('\n'),
104
- }],
105
- };
106
- }
107
-
108
- if (name === 'get_fax_status') {
109
- const data = await apiFetch(`/api/v1/fax/${args.job_id}`, {
110
- headers: authHeaders(),
111
- });
112
- const lines = [
113
- `Status: ${data.status}`,
114
- `To: ${data.toNumber}`,
115
- `Pages: ${data.pages}`,
116
- ];
117
- if (data.creditsUsed) lines.push(`Credits used: ${data.creditsUsed}`);
118
- if (data.failureReason) lines.push(`Failure reason: ${data.failureReason}`);
119
- if (data.certReceiptUrl) lines.push(`Receipt: ${data.certReceiptUrl}`);
120
- return { content: [{ type: 'text', text: lines.join('\n') }] };
121
- }
298
+ const server = new Server(
299
+ { name: 'faxseal-mcp', version: '2.0.0' },
300
+ { capabilities: { tools: {} } },
301
+ );
122
302
 
123
- if (name === 'get_credits') {
124
- const data = await apiFetch('/api/user/credits', { headers: authHeaders() });
125
- return {
126
- content: [{ type: 'text', text: `Credit balance: ${data.credits}` }],
127
- };
128
- }
303
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }));
129
304
 
130
- throw new Error(`Unknown tool: ${name}`);
131
- } catch (err) {
132
- return {
133
- content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
134
- isError: true,
135
- };
305
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
306
+ const { name, arguments: args = {} } = req.params;
307
+ const handler = handlers[name];
308
+ if (!handler) return toolErr(`Unknown tool: ${name}`);
309
+ try {
310
+ return await handler(args);
311
+ } catch (e) {
312
+ return toolErr(e instanceof Error ? e.message : String(e));
136
313
  }
137
314
  });
138
315
 
package/package.json CHANGED
@@ -1,17 +1,38 @@
1
1
  {
2
2
  "name": "faxseal-mcp",
3
- "version": "1.0.0",
4
- "description": "MCP server for FaxSeal — send faxes from Claude",
3
+ "version": "2.0.0",
4
+ "description": "MCP server for FaxSeal — send, receive, and verify faxes from AI assistants and agents",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "faxseal-mcp": "./index.js"
8
8
  },
9
9
  "type": "module",
10
+ "files": [
11
+ "index.js"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
10
16
  "scripts": {
11
17
  "start": "node index.js"
12
18
  },
13
- "keywords": ["mcp", "fax", "faxseal", "claude"],
19
+ "keywords": [
20
+ "mcp",
21
+ "fax",
22
+ "faxseal",
23
+ "claude",
24
+ "ai-agent",
25
+ "model-context-protocol",
26
+ "document",
27
+ "healthcare",
28
+ "hipaa"
29
+ ],
14
30
  "license": "MIT",
31
+ "homepage": "https://faxseal.com/api",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/faxseal/mcp-server"
35
+ },
15
36
  "dependencies": {
16
37
  "@modelcontextprotocol/sdk": "^1.29.0"
17
38
  }