faxseal-mcp 2.0.1 → 2.2.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 CHANGED
@@ -8,6 +8,7 @@ Send, receive, and verify faxes from AI assistants and agents via the [Model Con
8
8
  |---|---|
9
9
  | `send_fax` | Send a fax from a publicly accessible PDF URL |
10
10
  | `send_fax_text` | Type content directly — FaxSeal converts it to a PDF and sends |
11
+ | `quote_fax` | Estimate pages and credits before sending |
11
12
  | `get_fax_status` | Check delivery status of a sent fax |
12
13
  | `get_credits` | Check your FaxSeal credit balance |
13
14
  | `list_received_faxes` | List faxes in your inbox with OCR text |
package/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  const BASE_URL = (process.env.FAXSEAL_BASE_URL ?? 'https://faxseal.com').replace(/\/+$/, '');
10
10
  const API_KEY = process.env.FAXSEAL_API_KEY ?? '';
11
11
  const FETCH_TIMEOUT_MS = 30_000;
12
+ const SERVER_VERSION = '2.2.0';
12
13
 
13
14
  if (!API_KEY) {
14
15
  process.stderr.write('Error: FAXSEAL_API_KEY environment variable is required.\n');
@@ -113,6 +114,34 @@ const handlers = {
113
114
  ]);
114
115
  },
115
116
 
117
+ quote_fax: async (args) => {
118
+ if (!args.to) throw new Error('to is required');
119
+ if (!args.pages && !args.content && !args.file_url) {
120
+ throw new Error('one of pages, content, or file_url is required');
121
+ }
122
+
123
+ const data = await apiFetch('/api/v1/fax/quote', {
124
+ method: 'POST',
125
+ headers: authHeaders(),
126
+ body: JSON.stringify({
127
+ to: args.to,
128
+ pages: args.pages,
129
+ content: args.content,
130
+ file_url: args.file_url,
131
+ subject: args.subject,
132
+ }),
133
+ });
134
+
135
+ return ok([
136
+ `To: ${data.to_number}`,
137
+ `Zone: ${data.zone}`,
138
+ `Pages: ${data.pages}`,
139
+ `Credits required: ${data.credits_required}`,
140
+ `Credits available: ${data.credits_available}`,
141
+ `Can send: ${data.can_send}`,
142
+ ]);
143
+ },
144
+
116
145
  get_fax_status: async (args) => {
117
146
  if (!args.job_id) throw new Error('job_id is required');
118
147
 
@@ -203,6 +232,37 @@ const handlers = {
203
232
 
204
233
  return ok(lines);
205
234
  },
235
+ open_pdf_editor: async (args) => {
236
+ if (!args.file_url) throw new Error('file_url is required');
237
+ const data = await apiFetch('/api/v1/pdf', {
238
+ method: 'POST',
239
+ headers: authHeaders(),
240
+ body: JSON.stringify({ file_url: args.file_url }),
241
+ });
242
+ return ok([
243
+ 'PDF opened in the FaxSeal editor. Nothing has been edited, signed or sent yet.',
244
+ `Document ID: ${data.document_id}`,
245
+ `Pages: ${data.pages}`,
246
+ `Editor: ${data.editor_url}`,
247
+ `Link expires: ${data.expires_at}`,
248
+ 'Give the user the editor link. They place their own text, dates and signature, click Apply and can download free.',
249
+ 'Then call get_edited_pdf with this document ID. Ready means edits were applied; it does not verify a signature was added. Never claim it is signed unless the user confirms that.',
250
+ ]);
251
+ },
252
+
253
+ get_edited_pdf: async (args) => {
254
+ if (!args.document_id) throw new Error('document_id is required');
255
+ const data = await apiFetch(`/api/v1/pdf/${encodeURIComponent(args.document_id)}`, { headers: authHeaders() });
256
+ if (!data.ready) return ok([`Document ${args.document_id}: not finished yet. The user must apply their edits in the editor first.`]);
257
+ return ok([
258
+ `Document ${data.document_id} is ready.`,
259
+ `Pages: ${data.pages}`,
260
+ data.filename && `File: ${data.filename}`,
261
+ `Download: ${data.download_url}`,
262
+ `Download link expires: ${data.expires_at}`,
263
+ 'Faxing is optional: only send this file with send_fax after the user explicitly approves the recipient and cost.',
264
+ ]);
265
+ },
206
266
  };
207
267
 
208
268
  // ---------------------------------------------------------------------------
@@ -242,6 +302,22 @@ const TOOL_DEFS = [
242
302
  },
243
303
  },
244
304
  },
305
+ {
306
+ name: 'quote_fax',
307
+ description:
308
+ 'Estimate pages and credits for a fax before sending. Accepts known page count, plain text content, or a PDF URL.',
309
+ inputSchema: {
310
+ type: 'object',
311
+ required: ['to'],
312
+ properties: {
313
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
314
+ pages: { type: 'number', description: 'Known page count, if already available' },
315
+ content: { type: 'string', description: 'Plain-text fax body to render and quote' },
316
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file to quote' },
317
+ subject: { type: 'string', description: 'Optional subject when quoting plain text content' },
318
+ },
319
+ },
320
+ },
245
321
  {
246
322
  name: 'get_fax_status',
247
323
  description:
@@ -289,6 +365,30 @@ const TOOL_DEFS = [
289
365
  },
290
366
  },
291
367
  },
368
+ {
369
+ name: 'open_pdf_editor',
370
+ description:
371
+ 'Open a PDF from an HTTPS URL in the free FaxSeal editor so the user can add text, dates, checkboxes and their own signature, rotate or remove pages, and download. Does not send a fax or spend credits. Requires an API token with the "Edit and sign PDFs" permission on a personal account. The user must apply edits themselves; never claim the document is signed because this returned a link.',
372
+ inputSchema: {
373
+ type: 'object',
374
+ required: ['file_url'],
375
+ properties: {
376
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file' },
377
+ },
378
+ },
379
+ },
380
+ {
381
+ name: 'get_edited_pdf',
382
+ description:
383
+ 'Check whether the user has applied their edits to a document opened with open_pdf_editor. Returns not ready until they do, then a short-lived download URL that can also be passed to send_fax with separate user approval.',
384
+ inputSchema: {
385
+ type: 'object',
386
+ required: ['document_id'],
387
+ properties: {
388
+ document_id: { type: 'string', description: 'Document ID returned by open_pdf_editor' },
389
+ },
390
+ },
391
+ },
292
392
  ];
293
393
 
294
394
  // ---------------------------------------------------------------------------
@@ -296,7 +396,7 @@ const TOOL_DEFS = [
296
396
  // ---------------------------------------------------------------------------
297
397
 
298
398
  const server = new Server(
299
- { name: 'faxseal-mcp', version: '2.0.0' },
399
+ { name: 'faxseal-mcp', version: SERVER_VERSION },
300
400
  { capabilities: { tools: {} } },
301
401
  );
302
402
 
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "faxseal-mcp",
3
- "version": "2.0.1",
4
- "description": "MCP server for FaxSeal — send, receive, and verify faxes from AI assistants and agents",
3
+ "version": "2.2.0",
4
+ "description": "MCP server for FaxSeal — send, receive, and verify faxes, and edit or sign PDFs free, 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
10
  "files": [
11
- "index.js"
11
+ "index.js",
12
+ "server-http.js",
13
+ "server.json",
14
+ "smithery.yaml"
12
15
  ],
13
16
  "engines": {
14
17
  "node": ">=20"
@@ -33,7 +36,7 @@
33
36
  "homepage": "https://faxseal.com/api",
34
37
  "repository": {
35
38
  "type": "git",
36
- "url": "git+https://github.com/faxseal/mcp-server.git"
39
+ "url": "git+https://github.com/grichardomi/mcp-server.git"
37
40
  },
38
41
  "dependencies": {
39
42
  "@modelcontextprotocol/sdk": "^1.29.0"
package/server-http.js ADDED
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FaxSeal MCP HTTP Server (Railway / cloud deployment)
4
+ * Implements MCP Streamable HTTP transport — one session per caller.
5
+ * Callers authenticate with their own FAXSEAL_API_KEY via Bearer token.
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { createServer } from 'node:http';
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
12
+
13
+ const PORT = parseInt(process.env.PORT ?? '3001', 10);
14
+ const BASE_URL = (process.env.FAXSEAL_BASE_URL ?? 'https://faxseal.com').replace(/\/+$/, '');
15
+ const FETCH_TIMEOUT_MS = 30_000;
16
+ const SERVER_VERSION = '2.2.0';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // HTTP helper — per-request API key, hard timeout, no timer leaks
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function makeApiFetch(apiKey) {
23
+ return async function apiFetch(path, options = {}) {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26
+ try {
27
+ const res = await fetch(`${BASE_URL}${path}`, {
28
+ ...options,
29
+ signal: controller.signal,
30
+ headers: {
31
+ Authorization: `Bearer ${apiKey}`,
32
+ 'Content-Type': 'application/json',
33
+ ...options.headers,
34
+ },
35
+ });
36
+ const contentType = res.headers.get('content-type') ?? '';
37
+ const body = contentType.includes('application/json')
38
+ ? await res.json().catch(() => null)
39
+ : await res.text().catch(() => '');
40
+ if (!res.ok) {
41
+ const message =
42
+ body && typeof body === 'object' && 'error' in body
43
+ ? body.error
44
+ : typeof body === 'string' && body.trim()
45
+ ? body.trim()
46
+ : `HTTP ${res.status}`;
47
+ throw new Error(String(message));
48
+ }
49
+ return body && typeof body === 'object' ? body : {};
50
+ } finally {
51
+ clearTimeout(timer);
52
+ }
53
+ };
54
+ }
55
+
56
+ function ok(lines) {
57
+ return { content: [{ type: 'text', text: lines.filter(Boolean).join('\n') }] };
58
+ }
59
+
60
+ function toolErr(message) {
61
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Tool handlers — factory bound to a single session's API key
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function makeHandlers(apiFetch) {
69
+ return {
70
+ send_fax: async (args) => {
71
+ if (!args.to) throw new Error('to is required');
72
+ if (!args.file_url) throw new Error('file_url is required');
73
+ const data = await apiFetch('/api/zapier/send', {
74
+ method: 'POST',
75
+ body: JSON.stringify({
76
+ to: args.to,
77
+ file_url: args.file_url,
78
+ from_name: args.from_name,
79
+ from_company: args.from_company,
80
+ subject: args.subject,
81
+ notes: args.notes,
82
+ }),
83
+ });
84
+ return ok([
85
+ 'Fax queued.',
86
+ `Job ID: ${data.id}`,
87
+ `To: ${data.to_number}`,
88
+ `Pages: ${data.pages}`,
89
+ `Credits used: ${data.credits_used} (${data.credits_remaining} remaining)`,
90
+ `Track: ${data.track_url}`,
91
+ ]);
92
+ },
93
+
94
+ send_fax_text: async (args) => {
95
+ if (!args.to) throw new Error('to is required');
96
+ if (!args.content) throw new Error('content is required');
97
+ const data = await apiFetch('/api/v1/fax/text', {
98
+ method: 'POST',
99
+ body: JSON.stringify({
100
+ to: args.to,
101
+ content: args.content,
102
+ subject: args.subject,
103
+ from_name: args.from_name,
104
+ from_company: args.from_company,
105
+ }),
106
+ });
107
+ return ok([
108
+ 'Fax queued.',
109
+ `Job ID: ${data.id}`,
110
+ `To: ${data.to_number}`,
111
+ `Pages: ${data.pages}`,
112
+ `Credits used: ${data.credits_used} (${data.credits_remaining} remaining)`,
113
+ `Track: ${data.track_url}`,
114
+ ]);
115
+ },
116
+
117
+ quote_fax: async (args) => {
118
+ if (!args.to) throw new Error('to is required');
119
+ if (!args.pages && !args.content && !args.file_url) {
120
+ throw new Error('one of pages, content, or file_url is required');
121
+ }
122
+ const data = await apiFetch('/api/v1/fax/quote', {
123
+ method: 'POST',
124
+ body: JSON.stringify({
125
+ to: args.to,
126
+ pages: args.pages,
127
+ content: args.content,
128
+ file_url: args.file_url,
129
+ subject: args.subject,
130
+ }),
131
+ });
132
+ return ok([
133
+ `To: ${data.to_number}`,
134
+ `Zone: ${data.zone}`,
135
+ `Pages: ${data.pages}`,
136
+ `Credits required: ${data.credits_required}`,
137
+ `Credits available: ${data.credits_available}`,
138
+ `Can send: ${data.can_send}`,
139
+ ]);
140
+ },
141
+
142
+ get_fax_status: async (args) => {
143
+ if (!args.job_id) throw new Error('job_id is required');
144
+ const data = await apiFetch(`/api/v1/fax/${encodeURIComponent(args.job_id)}`);
145
+ return ok([
146
+ `Status: ${data.status}`,
147
+ `To: ${data.toNumber}`,
148
+ `Pages: ${data.pages}`,
149
+ data.creditsUsed != null && `Credits used: ${data.creditsUsed}`,
150
+ data.failureReason && `Failure reason: ${data.failureReason}`,
151
+ data.refundStatus && `Refund: ${data.refundStatus}`,
152
+ data.certReceiptUrl && `Receipt: ${data.certReceiptUrl}`,
153
+ `Sent: ${data.createdAt}`,
154
+ `Updated: ${data.updatedAt}`,
155
+ ]);
156
+ },
157
+
158
+ get_credits: async () => {
159
+ const data = await apiFetch('/api/user/credits');
160
+ return ok([`Credit balance: ${data.credits}`]);
161
+ },
162
+
163
+ list_received_faxes: async (args) => {
164
+ const params = new URLSearchParams();
165
+ if (args.since) params.set('since', args.since);
166
+ const qs = params.size ? `?${params}` : '';
167
+ const faxes = await apiFetch(`/api/zapier/inbox${qs}`);
168
+ if (!faxes.length) return ok(['No received faxes found.']);
169
+ const lines = [`${faxes.length} received fax(es):`, ''];
170
+ for (let i = 0; i < faxes.length; i++) {
171
+ const f = faxes[i];
172
+ if (i > 0) lines.push('─'.repeat(40));
173
+ lines.push(
174
+ `ID: ${f.id}`,
175
+ `From: ${f.from_number} To: ${f.to_number} Pages: ${f.pages}`,
176
+ `Received: ${f.received_at}`,
177
+ f.ocr_text
178
+ ? `Text preview: ${f.ocr_text.slice(0, 200)}${f.ocr_text.length > 200 ? '…' : ''}`
179
+ : 'OCR: not available',
180
+ );
181
+ }
182
+ return ok(lines);
183
+ },
184
+
185
+ verify_fax: async (args) => {
186
+ if (!args.job_id) throw new Error('job_id is required');
187
+ const data = await apiFetch(`/api/v1/fax/${encodeURIComponent(args.job_id)}/verify`);
188
+ if (!data.attested) {
189
+ return ok([`Job ${args.job_id}: not yet attested (status: ${data.status})`]);
190
+ }
191
+ const lines = [
192
+ `Job ID: ${data.jobId}`,
193
+ `Status: ${data.status}`,
194
+ data.documentHash && `Document hash (SHA-256): ${data.documentHash}`,
195
+ ];
196
+ if (data.send) {
197
+ lines.push('', 'Send attestation:',
198
+ ` Verified: ${data.send.verified}`,
199
+ ` Rekor: ${data.send.rekorUrl}`,
200
+ data.send.error && ` Error: ${data.send.error}`);
201
+ }
202
+ if (data.delivery) {
203
+ lines.push('', 'Delivery attestation:',
204
+ ` Verified: ${data.delivery.verified}`,
205
+ ` Rekor: ${data.delivery.rekorUrl}`,
206
+ data.delivery.error && ` Error: ${data.delivery.error}`);
207
+ }
208
+ return ok(lines);
209
+ },
210
+ open_pdf_editor: async (args) => {
211
+ if (!args.file_url) throw new Error('file_url is required');
212
+ const data = await apiFetch('/api/v1/pdf', {
213
+ method: 'POST',
214
+ body: JSON.stringify({ file_url: args.file_url }),
215
+ });
216
+ return ok([
217
+ 'PDF opened in the FaxSeal editor. Nothing has been edited, signed or sent yet.',
218
+ `Document ID: ${data.document_id}`,
219
+ `Pages: ${data.pages}`,
220
+ `Editor: ${data.editor_url}`,
221
+ `Link expires: ${data.expires_at}`,
222
+ 'Give the user the editor link. They place their own text, dates and signature, click Apply and can download free.',
223
+ 'Then call get_edited_pdf with this document ID. Ready means edits were applied; it does not verify a signature was added. Never claim it is signed unless the user confirms that.',
224
+ ]);
225
+ },
226
+
227
+ get_edited_pdf: async (args) => {
228
+ if (!args.document_id) throw new Error('document_id is required');
229
+ const data = await apiFetch(`/api/v1/pdf/${encodeURIComponent(args.document_id)}`);
230
+ if (!data.ready) return ok([`Document ${args.document_id}: not finished yet. The user must apply their edits in the editor first.`]);
231
+ return ok([
232
+ `Document ${data.document_id} is ready.`,
233
+ `Pages: ${data.pages}`,
234
+ data.filename && `File: ${data.filename}`,
235
+ `Download: ${data.download_url}`,
236
+ `Download link expires: ${data.expires_at}`,
237
+ 'Faxing is optional: only send this file with send_fax after the user explicitly approves the recipient and cost.',
238
+ ]);
239
+ },
240
+ };
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Tool definitions
245
+ // ---------------------------------------------------------------------------
246
+
247
+ const TOOL_DEFS = [
248
+ {
249
+ name: 'send_fax',
250
+ description: 'Send a fax from a publicly accessible PDF URL. Returns a job ID for status tracking.',
251
+ inputSchema: {
252
+ type: 'object', required: ['to', 'file_url'],
253
+ properties: {
254
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
255
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file' },
256
+ from_name: { type: 'string', description: 'Sender name for the cover page (optional)' },
257
+ from_company: { type: 'string', description: 'Sender company for the cover page (optional)' },
258
+ subject: { type: 'string', description: 'Fax subject line (optional)' },
259
+ notes: { type: 'string', description: 'Cover page notes (optional)' },
260
+ },
261
+ },
262
+ },
263
+ {
264
+ name: 'send_fax_text',
265
+ description: 'Convert plain text to a PDF and send it as a fax. Ideal when the content is generated at runtime.',
266
+ inputSchema: {
267
+ type: 'object', required: ['to', 'content'],
268
+ properties: {
269
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
270
+ content: { type: 'string', description: 'Plain-text body of the fax (max 50,000 characters)' },
271
+ subject: { type: 'string', description: 'Fax subject line (optional)' },
272
+ from_name: { type: 'string', description: 'Sender name (optional)' },
273
+ from_company: { type: 'string', description: 'Sender company (optional)' },
274
+ },
275
+ },
276
+ },
277
+ {
278
+ name: 'quote_fax',
279
+ description: 'Estimate pages and credits for a fax before sending. Accepts known page count, plain text content, or a PDF URL.',
280
+ inputSchema: {
281
+ type: 'object', required: ['to'],
282
+ properties: {
283
+ to: { type: 'string', description: 'Destination fax number in E.164 format, e.g. +12025551234' },
284
+ pages: { type: 'number', description: 'Known page count, if already available' },
285
+ content: { type: 'string', description: 'Plain-text fax body to render and quote' },
286
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file to quote' },
287
+ subject: { type: 'string', description: 'Optional subject when quoting plain text content' },
288
+ },
289
+ },
290
+ },
291
+ {
292
+ name: 'get_fax_status',
293
+ description: 'Check the delivery status of a sent fax. Status: queued → sending → delivered or failed.',
294
+ inputSchema: {
295
+ type: 'object', required: ['job_id'],
296
+ properties: {
297
+ job_id: { type: 'string', description: 'Fax job ID returned by send_fax or send_fax_text' },
298
+ },
299
+ },
300
+ },
301
+ {
302
+ name: 'get_credits',
303
+ description: 'Check the current FaxSeal credit balance for this API token.',
304
+ inputSchema: { type: 'object', properties: {} },
305
+ },
306
+ {
307
+ name: 'list_received_faxes',
308
+ description: 'List faxes received in the inbox, newest first (up to 50). Includes OCR text when available.',
309
+ inputSchema: {
310
+ type: 'object',
311
+ properties: {
312
+ since: { type: 'string', description: 'ISO 8601 date-time — return only faxes received after this point. Defaults to last 7 days.' },
313
+ },
314
+ },
315
+ },
316
+ {
317
+ name: 'verify_fax',
318
+ description: 'Verify the cryptographic attestation of a fax via Sigstore/Rekor. Confirms delivery without tampering.',
319
+ inputSchema: {
320
+ type: 'object', required: ['job_id'],
321
+ properties: {
322
+ job_id: { type: 'string', description: 'Fax job ID to verify' },
323
+ },
324
+ },
325
+ },
326
+ {
327
+ name: 'open_pdf_editor',
328
+ description:
329
+ 'Open a PDF from an HTTPS URL in the free FaxSeal editor so the user can add text, dates, checkboxes and their own signature, rotate or remove pages, and download. Does not send a fax or spend credits. Requires an API token with the "Edit and sign PDFs" permission on a personal account. The user must apply edits themselves; never claim the document is signed because this returned a link.',
330
+ inputSchema: {
331
+ type: 'object',
332
+ required: ['file_url'],
333
+ properties: {
334
+ file_url: { type: 'string', description: 'Publicly accessible HTTPS URL to a PDF file' },
335
+ },
336
+ },
337
+ },
338
+ {
339
+ name: 'get_edited_pdf',
340
+ description:
341
+ 'Check whether the user has applied their edits to a document opened with open_pdf_editor. Returns not ready until they do, then a short-lived download URL that can also be passed to send_fax with separate user approval.',
342
+ inputSchema: {
343
+ type: 'object',
344
+ required: ['document_id'],
345
+ properties: {
346
+ document_id: { type: 'string', description: 'Document ID returned by open_pdf_editor' },
347
+ },
348
+ },
349
+ },
350
+ ];
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // MCP server factory — one instance per session
354
+ // ---------------------------------------------------------------------------
355
+
356
+ function buildMcpServer(apiKey) {
357
+ const apiFetch = makeApiFetch(apiKey);
358
+ const handlers = makeHandlers(apiFetch);
359
+
360
+ const server = new Server(
361
+ { name: 'faxseal-mcp', version: SERVER_VERSION },
362
+ { capabilities: { tools: {} } },
363
+ );
364
+
365
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }));
366
+
367
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
368
+ const { name, arguments: args = {} } = req.params;
369
+ const handler = handlers[name];
370
+ if (!handler) return toolErr(`Unknown tool: ${name}`);
371
+ try {
372
+ return await handler(args);
373
+ } catch (e) {
374
+ return toolErr(e instanceof Error ? e.message : String(e));
375
+ }
376
+ });
377
+
378
+ return server;
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // HTTP server — session registry with cleanup
383
+ // ---------------------------------------------------------------------------
384
+
385
+ // sessionId -> StreamableHTTPServerTransport
386
+ const sessions = new Map();
387
+
388
+ const httpServer = createServer(async (req, res) => {
389
+ const url = new URL(req.url ?? '/', `http://localhost:${PORT}`);
390
+
391
+ if (url.pathname === '/health') {
392
+ res.writeHead(200, { 'Content-Type': 'application/json' });
393
+ res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
394
+ return;
395
+ }
396
+
397
+ // OAuth Resource Server metadata (RFC 9728) — tells clients we use bearer tokens, not OAuth
398
+ if (url.pathname === '/.well-known/oauth-protected-resource') {
399
+ const resource = `https://${req.headers.host}`;
400
+ res.writeHead(200, { 'Content-Type': 'application/json' });
401
+ res.end(JSON.stringify({
402
+ resource,
403
+ bearer_methods_supported: ['header', 'query'],
404
+ resource_documentation: 'https://faxseal.com/docs/api',
405
+ }));
406
+ return;
407
+ }
408
+
409
+ if (url.pathname === '/mcp') {
410
+ const authHeader = req.headers['authorization'] ?? '';
411
+ const apiKey = authHeader.startsWith('Bearer ')
412
+ ? authHeader.slice(7)
413
+ : url.searchParams.get('apiKey') ?? '';
414
+
415
+ if (!apiKey) {
416
+ res.writeHead(401, { 'Content-Type': 'application/json' });
417
+ res.end(JSON.stringify({ error: 'Authorization: Bearer <FAXSEAL_API_KEY> required' }));
418
+ return;
419
+ }
420
+
421
+ // Re-use existing session if client sends Mcp-Session-Id
422
+ const existingId = req.headers['mcp-session-id'];
423
+ if (typeof existingId === 'string' && sessions.has(existingId)) {
424
+ await sessions.get(existingId).handleRequest(req, res);
425
+ return;
426
+ }
427
+
428
+ // New session
429
+ const transport = new StreamableHTTPServerTransport({
430
+ sessionIdGenerator: () => randomUUID(),
431
+ onsessioninitialized: (sessionId) => {
432
+ sessions.set(sessionId, transport);
433
+ },
434
+ });
435
+
436
+ transport.onclose = () => {
437
+ if (transport.sessionId) sessions.delete(transport.sessionId);
438
+ };
439
+
440
+ const server = buildMcpServer(apiKey);
441
+ await server.connect(transport);
442
+ await transport.handleRequest(req, res);
443
+ return;
444
+ }
445
+
446
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
447
+ res.end('Not found');
448
+ });
449
+
450
+ httpServer.listen(PORT, '0.0.0.0', () => {
451
+ process.stderr.write(`FaxSeal MCP HTTP server listening on port ${PORT}\n`);
452
+ });
package/server.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.grichardomi/faxseal",
4
+ "title": "FaxSeal",
5
+ "description": "Send, receive, and verify faxes from AI assistants and agents via FaxSeal.",
6
+ "version": "2.2.0",
7
+ "repository": {
8
+ "url": "https://github.com/grichardomi/mcp-server",
9
+ "source": "github"
10
+ },
11
+ "remotes": [
12
+ {
13
+ "type": "streamable-http",
14
+ "url": "https://mcp-server-production-8f66.up.railway.app/mcp"
15
+ }
16
+ ],
17
+ "packages": [
18
+ {
19
+ "registryType": "npm",
20
+ "name": "faxseal-mcp",
21
+ "identifier": "faxseal-mcp",
22
+ "version": "2.2.0",
23
+ "runtime": "node",
24
+ "transport": { "type": "stdio" },
25
+ "environmentVariables": [
26
+ {
27
+ "name": "FAXSEAL_API_KEY",
28
+ "description": "Your FaxSeal API key (fsx_...). Generate one at https://faxseal.com/dashboard/api-tokens",
29
+ "required": true
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,13 @@
1
+ startCommand:
2
+ type: http
3
+ url: https://mcp-server-production-8f66.up.railway.app/mcp
4
+ configSchema:
5
+ type: object
6
+ required:
7
+ - apiKey
8
+ properties:
9
+ apiKey:
10
+ type: string
11
+ description: Your FaxSeal API key (fsx_...). Generate one at https://faxseal.com/dashboard/api-tokens
12
+ exampleConfig:
13
+ apiKey: fsx_your_token_here