siglio-mcp 1.0.4 → 1.1.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 +44 -6
- package/index.js +225 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It runs on your own machine, so your API key never leaves it.
|
|
|
10
10
|
Get a key from [your Siglio account](https://esigndev.com/app). Signing up takes
|
|
11
11
|
an email address, no card, and comes with 25 free envelopes.
|
|
12
12
|
|
|
13
|
-
**Claude Desktop
|
|
13
|
+
**Claude Desktop:** add this to your config file:
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
@@ -30,14 +30,29 @@ Other MCP clients take the same command and environment.
|
|
|
30
30
|
|
|
31
31
|
| Tool | What it does |
|
|
32
32
|
|---|---|
|
|
33
|
-
| `send_for_signature` | Sends a PDF to one or two people, by email, text, or both |
|
|
34
|
-
| `check_envelope` | Whether it has been delivered, opened, signed or cancelled |
|
|
33
|
+
| `send_for_signature` | Sends a PDF to one or two people, by email, text, or both. Can also ask for a payment, files, or answers |
|
|
34
|
+
| `check_envelope` | Whether it has been delivered, opened, signed or cancelled, plus any payment, files and answers |
|
|
35
35
|
| `download_signed_document` | Saves the completed PDF once every signature is in |
|
|
36
|
+
| `download_attachment` | Saves a file the signer uploaded, such as a photo ID |
|
|
36
37
|
| `void_envelope` | Cancels an unsigned envelope; the link stops working |
|
|
37
38
|
|
|
38
39
|
Then you can just ask: *"Send the NDA on my desktop to Dana at 813 555 0142 by
|
|
39
40
|
text."*
|
|
40
41
|
|
|
42
|
+
When the last signer signs, every signer is sent the signed PDF automatically.
|
|
43
|
+
|
|
44
|
+
## Options on a send
|
|
45
|
+
|
|
46
|
+
All optional. Leave them out and you get a plain signature request.
|
|
47
|
+
|
|
48
|
+
| Input | What it does |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `delivery` | `auto` (the default) sends by email and text together when the signer has both, otherwise by whichever one they have. `email`, `sms` and `both` are strict |
|
|
51
|
+
| `payment` | Asks the signer to pay, in whole cents: `{ "amount": 15000, "description": "Deposit", "when": "after" }`. Paid straight into your own Stripe account, which you connect once in the document studio (choose Connect Stripe). `before` holds the document until it is paid |
|
|
52
|
+
| `attachments` | Asks the signer to upload 1 to 5 files from their phone: `{ "when": "before", "items": [{ "label": "Photo ID" }] }`. `before` holds the document until the files are in |
|
|
53
|
+
| `form_fields` | **Enterprise.** Labels, types, options, formats and show or require rules for the answer fields in your PDF |
|
|
54
|
+
| `constraints` | **Enterprise.** Rules across answer fields: exactly one, at least N, at most N, one of, requires |
|
|
55
|
+
|
|
41
56
|
## The one thing to know about your PDF
|
|
42
57
|
|
|
43
58
|
Siglio does not take coordinates for signature fields. Placement comes from
|
|
@@ -55,6 +70,29 @@ instructions, not content, and black ones stay visible on the signed document.
|
|
|
55
70
|
If the tags are missing the send is rejected and nothing goes out, so you cannot
|
|
56
71
|
accidentally mail someone a document they have no way to sign.
|
|
57
72
|
|
|
73
|
+
## Collecting answers (Enterprise)
|
|
74
|
+
|
|
75
|
+
On Enterprise accounts the PDF can also ask the signer questions, which they fill
|
|
76
|
+
in on a short form before signing:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
^M1 required text ^T1 optional text
|
|
80
|
+
^C1 a checkbox ^R1_G1 one option of pick-one group 1
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Each can carry a name, and boxes a value: `^M1:policy_number`,
|
|
84
|
+
`^C1:coverage=liability`, `^R1_G1:plan=monthly`. `check_envelope` then shows the
|
|
85
|
+
answers by their labels. Social Security numbers are always shown as the last
|
|
86
|
+
four digits only.
|
|
87
|
+
|
|
88
|
+
On other accounts these tags are refused and nothing is sent.
|
|
89
|
+
[Contact Siglio](https://esigndev.com/contact?topic=enterprise) to turn it on.
|
|
90
|
+
|
|
91
|
+
## Keep your own copy
|
|
92
|
+
|
|
93
|
+
Signed PDFs, uploaded files and answers are deleted 30 days after the envelope
|
|
94
|
+
closes. Download what you need before then.
|
|
95
|
+
|
|
58
96
|
## Two safety decisions worth knowing
|
|
59
97
|
|
|
60
98
|
**Live keys are refused by default.** A key starting with `sig_live_` will not
|
|
@@ -63,8 +101,8 @@ confused assistant can do is send a free sandbox envelope. Sandbox envelopes
|
|
|
63
101
|
deliver for real and produce real signatures; they just cost nothing.
|
|
64
102
|
|
|
65
103
|
**Sending is idempotent by content.** If an assistant retries the same document
|
|
66
|
-
to the same people, it gets the first envelope back
|
|
67
|
-
copy of a contract in someone's inbox.
|
|
104
|
+
to the same people with the same options, it gets the first envelope back
|
|
105
|
+
instead of putting a second copy of a contract in someone's inbox.
|
|
68
106
|
|
|
69
107
|
Cancelling additionally requires an explicit confirmation, so it cannot happen as
|
|
70
108
|
a side effect of a vague instruction.
|
|
@@ -81,4 +119,4 @@ a side effect of a vague instruction.
|
|
|
81
119
|
|
|
82
120
|
- Full API reference: <https://esigndev.com/llms-full.txt>
|
|
83
121
|
- OpenAPI spec: <https://esigndev.com/openapi.json>
|
|
84
|
-
- Docs: <https://esigndev.com/docs>
|
|
122
|
+
- Docs: <https://esigndev.com/docs>
|
package/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Siglio MCP server.
|
|
3
3
|
//
|
|
4
4
|
// Lets an AI assistant send a document for signature, check on it, fetch the
|
|
5
|
-
// signed file, and cancel one. It runs on the
|
|
6
|
-
// key never leaves it.
|
|
5
|
+
// signed file and any files the signer uploaded, and cancel one. It runs on the
|
|
6
|
+
// user's own machine, so their API key never leaves it.
|
|
7
7
|
//
|
|
8
8
|
// Two deliberate safety positions, because these tools send legally binding
|
|
9
9
|
// documents to real people:
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
// 1. LIVE KEYS ARE REFUSED unless SIGLIO_ALLOW_LIVE=true is set explicitly.
|
|
12
12
|
// The default blast radius of a confused model is a sandbox envelope.
|
|
13
13
|
// 2. Sending is IDEMPOTENT BY CONTENT. If no key is supplied we derive one
|
|
14
|
-
// from the document and
|
|
15
|
-
// same call does not put a second copy of a contract in
|
|
16
|
-
// Models retry. This makes that harmless
|
|
14
|
+
// from the document, the signers and every option sent, so an assistant
|
|
15
|
+
// that retries the same call does not put a second copy of a contract in
|
|
16
|
+
// someone's inbox. Models retry. This makes that harmless.
|
|
17
17
|
//
|
|
18
18
|
// Voiding additionally requires confirm:true, so it cannot happen as a casual
|
|
19
19
|
// side effect of a vague instruction.
|
|
20
20
|
//
|
|
21
|
+
// Answers collected from signers are personal data. This server prints them
|
|
22
|
+
// for the user who owns the key and never writes them anywhere else. Social
|
|
23
|
+
// Security numbers are always masked to the last four digits.
|
|
24
|
+
//
|
|
21
25
|
// Environment:
|
|
22
26
|
// SIGLIO_API_KEY required. sig_sandbox_... or sig_live_...
|
|
23
27
|
// SIGLIO_ALLOW_LIVE set to "true" to permit a live key
|
|
@@ -30,6 +34,7 @@ import { readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
|
30
34
|
import { basename, resolve } from 'node:path';
|
|
31
35
|
import { createHash } from 'node:crypto';
|
|
32
36
|
|
|
37
|
+
const VERSION = '1.1.0';
|
|
33
38
|
const API_BASE = process.env.SIGLIO_API_BASE || 'https://api.esigndev.com';
|
|
34
39
|
const KEY = (process.env.SIGLIO_API_KEY || '').trim();
|
|
35
40
|
const INLINE_LIMIT = 3 * 1024 * 1024; // the API's inline document ceiling
|
|
@@ -54,7 +59,7 @@ const IS_LIVE = /^sig_live_/.test(KEY);
|
|
|
54
59
|
// --- talking to the API --------------------------------------------------
|
|
55
60
|
|
|
56
61
|
async function api(method, path, { body, idempotencyKey, raw } = {}) {
|
|
57
|
-
const headers = { Authorization: 'Bearer ' + KEY, 'User-Agent': 'siglio-mcp/
|
|
62
|
+
const headers = { Authorization: 'Bearer ' + KEY, 'User-Agent': 'siglio-mcp/' + VERSION };
|
|
58
63
|
if (body) headers['Content-Type'] = 'application/json';
|
|
59
64
|
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
|
|
60
65
|
|
|
@@ -74,33 +79,78 @@ async function api(method, path, { body, idempotencyKey, raw } = {}) {
|
|
|
74
79
|
throw new Error(explain(res.status, parsed, res.headers.get('retry-after')));
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
const KEEP_A_COPY = 'Signed PDFs, uploaded files and answers are deleted 30 days after the envelope ' +
|
|
83
|
+
'closes, so the sender should keep their own copy.';
|
|
84
|
+
|
|
77
85
|
// Turn an API error into something an assistant can act on rather than repeat.
|
|
86
|
+
// The specific reason is in error.code; error.type is the broad category
|
|
87
|
+
// (feature_not_enabled, for example, arrives as type authorization_error).
|
|
78
88
|
function explain(status, parsed, retryAfter) {
|
|
79
89
|
const e = (parsed && parsed.error) || {};
|
|
80
90
|
const rid = e.request_id ? ' (request_id ' + e.request_id + ')' : '';
|
|
81
|
-
|
|
91
|
+
const msg = e.message ? ' ' + e.message : '';
|
|
92
|
+
switch (e.code || e.type) {
|
|
82
93
|
case 'missing_required_tag':
|
|
94
|
+
case 'missing_required_signature_tag':
|
|
95
|
+
case 'unsupported_signer_tag':
|
|
83
96
|
return 'The PDF does not carry the signature tags the request needs' + rid + '. ' +
|
|
84
97
|
'Placement comes from literal text inside the document: put ^S1 where signer one signs, ' +
|
|
85
98
|
'^I1 for initials, ^D1 for a self-filling date, and ^S2 / ^I2 / ^D2 for a second signer. ' +
|
|
86
99
|
'A document tagged for two signers must be sent with two, and one tagged for one with one. ' +
|
|
87
100
|
'Set that tag text to WHITE so it does not print on the finished document. ' +
|
|
88
|
-
'Siglio message:
|
|
101
|
+
'Siglio message:' + msg;
|
|
89
102
|
case 'invalid_document':
|
|
90
|
-
|
|
103
|
+
case 'unreadable_pdf':
|
|
104
|
+
return 'Siglio could not use that PDF' + rid + ':' + msg +
|
|
91
105
|
' Check it opens normally and is a real PDF rather than a renamed file.';
|
|
106
|
+
case 'file_too_large':
|
|
107
|
+
return 'That PDF is too large for this server' + rid + '.' + msg +
|
|
108
|
+
' Compress it, or use the upload endpoint directly (see https://esigndev.com/llms-full.txt).';
|
|
109
|
+
case 'feature_not_enabled':
|
|
110
|
+
return 'Collecting answers from the signer (^M, ^T, ^C or ^R tags, form_fields or constraints) is an ' +
|
|
111
|
+
'Enterprise feature that is not turned on for this account' + rid + '. Nothing was sent and nothing ' +
|
|
112
|
+
'was billed. Tell the user to contact Siglio at https://esigndev.com/contact?topic=enterprise, or ' +
|
|
113
|
+
'remove those tags and fields and send again. Do not retry as is.';
|
|
114
|
+
case 'invalid_form_fields':
|
|
115
|
+
return 'The form_fields do not fit the document' + rid + '.' + msg +
|
|
116
|
+
' Fix the entry the message names and send again.';
|
|
117
|
+
case 'invalid_constraints':
|
|
118
|
+
return 'The constraints do not fit the document' + rid + '.' + msg +
|
|
119
|
+
' Fix the entry the message names and send again.';
|
|
120
|
+
case 'payment_not_configured':
|
|
121
|
+
return 'This account cannot take payments yet' + rid + '. Connect a Stripe account in the Siglio ' +
|
|
122
|
+
'document studio first (choose Connect Stripe), then send again. Nothing was sent.';
|
|
123
|
+
case 'invalid_signer':
|
|
124
|
+
return 'Siglio rejected a signer' + rid + ':' + msg + ' Check the name, email and phone.';
|
|
125
|
+
case 'document_deleted':
|
|
126
|
+
return 'That file was deleted after 30 days, the sender should keep their own copy' + rid + '. Siglio ' +
|
|
127
|
+
'deletes signed PDFs, uploaded files and answers 30 days after the envelope closes. It cannot be recovered.';
|
|
128
|
+
case 'not_found':
|
|
129
|
+
return 'Siglio has no envelope or file with that id for this key' + rid + '. Envelope ids come from the ' +
|
|
130
|
+
'response to send_for_signature and look like env_7mmyc.... Check the id for a typo; retrying the ' +
|
|
131
|
+
'same id will not help. A sandbox key cannot see live envelopes, and the reverse.';
|
|
92
132
|
case 'authentication_error':
|
|
93
133
|
return 'The API key was rejected' + rid + '. Get a current key from https://esigndev.com/app.';
|
|
134
|
+
case 'authorization_error':
|
|
135
|
+
return 'This key is not allowed to do that' + rid + '.' + msg;
|
|
94
136
|
case 'payment_required':
|
|
95
137
|
return 'This account needs a card on file before it can send' + rid +
|
|
96
138
|
'. Add one at https://esigndev.com/app, or use a sandbox key, which is free.';
|
|
97
139
|
case 'usage_limit_reached':
|
|
98
140
|
return 'This account is out of its free allowance' + rid +
|
|
99
141
|
'. Add a card at https://esigndev.com/app to keep sending.';
|
|
142
|
+
case 'self_imposed_cap_reached':
|
|
143
|
+
return 'This account has hit the spending cap its owner set' + rid +
|
|
144
|
+
'. Raise or remove it at https://esigndev.com/app.';
|
|
145
|
+
case 'new_envelopes_paused':
|
|
146
|
+
case 'account_paused':
|
|
147
|
+
return 'Sending is paused on this account' + rid + '.' + msg;
|
|
100
148
|
case 'rate_limited':
|
|
101
149
|
return 'Rate limited' + rid + '. Wait ' + (retryAfter || 'a few') + ' seconds and retry with the same idempotency key.';
|
|
102
150
|
case 'service_unavailable':
|
|
103
|
-
return 'Siglio is temporarily unavailable' + rid + '. Retrying with the same idempotency key is safe.';
|
|
151
|
+
return 'Siglio is temporarily unavailable' + rid + '.' + msg + ' Retrying with the same idempotency key is safe.';
|
|
152
|
+
case 'invalid_request':
|
|
153
|
+
return 'Siglio rejected the request' + rid + ':' + msg;
|
|
104
154
|
default:
|
|
105
155
|
return 'Siglio returned ' + status + rid + ': ' + (e.message || JSON.stringify(parsed) || 'no detail');
|
|
106
156
|
}
|
|
@@ -123,6 +173,9 @@ function loadPdf(p) {
|
|
|
123
173
|
return { base64: buf.toString('base64'), bytes: buf.length, name: basename(full) };
|
|
124
174
|
}
|
|
125
175
|
|
|
176
|
+
const dollars = (cents) => '$' + (cents / 100).toFixed(2);
|
|
177
|
+
const size = (b) => b >= 1048576 ? (b / 1048576).toFixed(1) + ' MB' : Math.max(1, Math.round(b / 1024)) + ' KB';
|
|
178
|
+
|
|
126
179
|
function describe(env) {
|
|
127
180
|
const lines = [
|
|
128
181
|
'Envelope ' + env.id,
|
|
@@ -133,27 +186,94 @@ function describe(env) {
|
|
|
133
186
|
];
|
|
134
187
|
const people = env.signers || (env.signer ? [env.signer] : []);
|
|
135
188
|
const done = env.state === 'completed';
|
|
189
|
+
const partial = env.state === 'partially_signed';
|
|
136
190
|
people.forEach((s, i) => {
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
: '
|
|
191
|
+
// signed_at null means "no recorded time", never "unsigned" on a completed
|
|
192
|
+
// envelope. On partially_signed, signer one has signed and signer two has
|
|
193
|
+
// not, whatever the timestamps say.
|
|
194
|
+
const signed = !!s.signed_at || done || (partial && i === 0);
|
|
195
|
+
const status = s.signed_at ? 'signed ' + s.signed_at
|
|
196
|
+
: signed ? (done ? 'signed, time not recorded' : 'signed, time not yet recorded')
|
|
197
|
+
: 'not yet signed';
|
|
143
198
|
lines.push('Signer ' + (i + 1) + ': ' + s.name +
|
|
144
|
-
[s.email, s.phone].filter(Boolean).map((x) => ' <' + x + '>').join('') +
|
|
199
|
+
[s.email, s.phone].filter(Boolean).map((x) => ' <' + x + '>').join('') +
|
|
200
|
+
(s.delivery ? ' via ' + s.delivery : '') + ', ' + status);
|
|
145
201
|
if (s.signing_url) lines.push(' link: ' + s.signing_url);
|
|
146
202
|
});
|
|
147
203
|
if (!people.length && env.signing_url) lines.push('Signing link: ' + env.signing_url);
|
|
204
|
+
|
|
205
|
+
const p = env.payment;
|
|
206
|
+
if (p) {
|
|
207
|
+
lines.push('Payment: ' + dollars(p.amount) + (p.description ? ' for ' + p.description : '') +
|
|
208
|
+
', ' + (p.when === 'before' ? 'before signing' : 'after signing') + ', ' + p.status +
|
|
209
|
+
(p.paid_at ? ' on ' + p.paid_at : ''));
|
|
210
|
+
if (p.pay_url) lines.push(' pay link: ' + p.pay_url);
|
|
211
|
+
if (p.when === 'before' && p.status === 'pending')
|
|
212
|
+
lines.push(' The document is held until this is paid; the signing link is the payment page for now.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const a = env.attachments;
|
|
216
|
+
if (a) {
|
|
217
|
+
lines.push('Files from the signer (' + (a.when === 'before' ? 'before signing' : 'after signing') + '): ' +
|
|
218
|
+
a.status + (a.received_at ? ' on ' + a.received_at : ''));
|
|
219
|
+
for (const it of a.items || []) {
|
|
220
|
+
const files = it.files || [];
|
|
221
|
+
lines.push(' ' + it.label + (it.required === false ? ' (optional)' : '') + ': ' +
|
|
222
|
+
(files.length ? files.map((f) => f.name + ' (' + size(f.size) + ', id ' + f.id + ')').join('; ') : 'nothing yet'));
|
|
223
|
+
}
|
|
224
|
+
if (a.upload_url) lines.push(' upload link: ' + a.upload_url);
|
|
225
|
+
if (a.when === 'before' && a.status === 'pending')
|
|
226
|
+
lines.push(' The document is held until the required files are in.');
|
|
227
|
+
if ((a.items || []).some((it) => (it.files || []).length))
|
|
228
|
+
lines.push(' Save one with download_attachment and its id.');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const answers = describeAnswers(env);
|
|
232
|
+
if (answers.length) lines.push(...answers);
|
|
233
|
+
|
|
148
234
|
if (env.completed_at) lines.push('Completed: ' + env.completed_at);
|
|
149
235
|
return lines.join('\n');
|
|
150
236
|
}
|
|
151
237
|
|
|
238
|
+
// One line per signer: "Answers (signer 1): Policy number: A-1234; I agree: yes".
|
|
239
|
+
// A choice shows its option label, not the stored value.
|
|
240
|
+
function describeAnswers(env) {
|
|
241
|
+
const values = env.field_values;
|
|
242
|
+
if (!Array.isArray(values) || !values.length) return [];
|
|
243
|
+
const defs = new Map(((env.form && env.form.fields) || []).map((f) => [f.name, f]));
|
|
244
|
+
const optionLabel = (name, v) => {
|
|
245
|
+
const o = ((defs.get(name) || {}).options || []).find((x) => x.value === v);
|
|
246
|
+
return o && o.label ? o.label : v;
|
|
247
|
+
};
|
|
248
|
+
const render = (fv) => {
|
|
249
|
+
if (fv.deleted) return 'deleted after 30 days';
|
|
250
|
+
if (fv.value === null || fv.value === undefined) return fv.submitted_at ? 'left blank' : 'not signed yet';
|
|
251
|
+
if (fv.type === 'checkbox') return fv.value === true ? 'yes' : 'no';
|
|
252
|
+
if (fv.type === 'checkbox_group')
|
|
253
|
+
return Array.isArray(fv.value) && fv.value.length ? fv.value.map((v) => optionLabel(fv.name, v)).join(', ') : 'none';
|
|
254
|
+
if (fv.type === 'select' || fv.type === 'radio') return optionLabel(fv.name, fv.value);
|
|
255
|
+
// Social Security numbers never appear in full: chat transcripts are kept.
|
|
256
|
+
const fmt = fv.format || (defs.get(fv.name) || {}).format;
|
|
257
|
+
if (fmt === 'ssn') { const d = String(fv.value).replace(/\D/g, ''); return '***-**-' + (d.slice(-4) || '****'); }
|
|
258
|
+
return String(fv.value);
|
|
259
|
+
};
|
|
260
|
+
const out = [];
|
|
261
|
+
for (const n of [1, 2]) {
|
|
262
|
+
const mine = values.filter((fv) => (fv.signer || 1) === n);
|
|
263
|
+
if (mine.length)
|
|
264
|
+
out.push('Answers (signer ' + n + '): ' + mine.map((fv) => (fv.label || fv.name) + ': ' + render(fv)).join('; '));
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
152
269
|
function stateNote(s) {
|
|
270
|
+
if (s === 'created') return ' <- held: waiting for a payment or files before the document goes out.';
|
|
153
271
|
if (s === 'partially_signed') return ' <- one of two signers is done. NOT finished.';
|
|
154
272
|
if (s === 'completed') return ' <- every signature is in.';
|
|
155
273
|
if (s === 'delivered') return ' <- sent, not opened yet.';
|
|
274
|
+
if (s === 'viewed') return ' <- opened, not signed yet.';
|
|
156
275
|
if (s === 'voided') return ' <- cancelled, the link no longer works.';
|
|
276
|
+
if (s === 'failed') return ' <- could not be delivered.';
|
|
157
277
|
return '';
|
|
158
278
|
}
|
|
159
279
|
|
|
@@ -162,13 +282,34 @@ const fail = (e) => ({ content: [{ type: 'text', text: 'Failed: ' + (e && e.mess
|
|
|
162
282
|
|
|
163
283
|
// --- the server ----------------------------------------------------------
|
|
164
284
|
|
|
165
|
-
const server = new McpServer({ name: 'siglio', version:
|
|
285
|
+
const server = new McpServer({ name: 'siglio', version: VERSION });
|
|
286
|
+
|
|
287
|
+
const deliveryEnum = z.enum(['auto', 'email', 'sms', 'both']);
|
|
166
288
|
|
|
167
289
|
const signerShape = z.object({
|
|
168
290
|
name: z.string().describe("The signer's full name."),
|
|
169
|
-
email: z.string().optional().describe('
|
|
170
|
-
phone: z.string().optional().describe('E.164 preferred, e.g. +18135550142.
|
|
171
|
-
delivery:
|
|
291
|
+
email: z.string().optional().describe('Their email address. Give both email and phone when you have them.'),
|
|
292
|
+
phone: z.string().optional().describe('Their mobile number, E.164 preferred, e.g. +18135550142.'),
|
|
293
|
+
delivery: deliveryEnum.optional().describe("Overrides the envelope's delivery for this signer."),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const paymentShape = z.object({
|
|
297
|
+
amount: z.number().int().min(50).max(99999900).describe('Whole cents. 15000 is $150.00.'),
|
|
298
|
+
currency: z.enum(['usd']).optional().describe('usd only.'),
|
|
299
|
+
description: z.string().max(120).optional().describe('What the payment is for, as the signer will read it.'),
|
|
300
|
+
when: z.enum(['before', 'after']).optional().describe(
|
|
301
|
+
'after (the default): the payment link goes out when the document is signed. before: the document is held ' +
|
|
302
|
+
'and the signing link goes out once the payment lands.'),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const attachmentsShape = z.object({
|
|
306
|
+
when: z.enum(['before', 'after']).describe(
|
|
307
|
+
'No default, ask the user. before: the document is held until the files are in. after: the upload link ' +
|
|
308
|
+
'goes out once it is signed.'),
|
|
309
|
+
items: z.array(z.object({
|
|
310
|
+
label: z.string().max(60).describe('What to upload, as the signer reads it: "Photo ID", "Proof of insurance".'),
|
|
311
|
+
required: z.boolean().optional().describe('Defaults to true.'),
|
|
312
|
+
})).min(1).max(5),
|
|
172
313
|
});
|
|
173
314
|
|
|
174
315
|
server.registerTool('send_for_signature', {
|
|
@@ -180,36 +321,62 @@ server.registerTool('send_for_signature', {
|
|
|
180
321
|
'The PDF must already contain signature tags: ^S1 where signer one signs, ^I1 for initials, ^D1 for a ' +
|
|
181
322
|
'date that fills itself, and ^S2 / ^I2 / ^D2 for a second signer. Those tags must be WHITE font or they ' +
|
|
182
323
|
'print on the finished document. If the document has no tags this call is rejected and nothing is sent.\n\n' +
|
|
183
|
-
'
|
|
184
|
-
'
|
|
324
|
+
'On Enterprise accounts the PDF can also ask the signer questions before they sign: ^M1 (required text), ' +
|
|
325
|
+
'^T1 (optional text), ^C1 (a checkbox), ^R1_G1 (one option of pick-one group 1). Each may carry a name, ' +
|
|
326
|
+
'and boxes a value: ^M1:policy_number, ^C1:coverage=liability, ^R1_G1:plan=monthly. form_fields sets ' +
|
|
327
|
+
'labels, types, options, formats and show/require rules; constraints sets rules across fields. Accounts ' +
|
|
328
|
+
'without the feature are refused with feature_not_enabled and nothing is sent.\n\n' +
|
|
329
|
+
'Optional extras: payment asks the signer to pay (a deposit, an invoice) through the sender\'s connected ' +
|
|
330
|
+
'Stripe account; attachments asks the signer to upload files (a photo ID, a W-9).\n\n' +
|
|
331
|
+
'When the last signer signs, every signer is sent the signed PDF automatically. ' + KEEP_A_COPY + '\n\n' +
|
|
332
|
+
'Calling twice with the same document, signers and options returns the first envelope rather than sending ' +
|
|
333
|
+
'a second copy, so a retry is safe.',
|
|
185
334
|
inputSchema: {
|
|
186
335
|
document_path: z.string().describe('Full path to the tagged PDF on this machine.'),
|
|
187
336
|
signers: z.array(signerShape).min(1).max(2)
|
|
188
|
-
.describe('One or two signers. With two,
|
|
189
|
-
delivery:
|
|
190
|
-
.describe('
|
|
337
|
+
.describe('One or two signers. With two, signing is sequential: the second is not notified until the first finishes.'),
|
|
338
|
+
delivery: deliveryEnum.default('auto')
|
|
339
|
+
.describe('auto (the default) sends by email and text together when both are known, which is what Siglio ' +
|
|
340
|
+
'is for, and otherwise by whichever one the signer has. email, sms and both are strict.'),
|
|
191
341
|
document_name: z.string().optional().describe('What the signer sees it called. Defaults to the filename.'),
|
|
192
|
-
|
|
342
|
+
payment: paymentShape.optional().describe('Ask the signer for a payment. Needs Stripe connected in the document studio.'),
|
|
343
|
+
attachments: attachmentsShape.optional().describe('Ask the signer to upload 1 to 5 files.'),
|
|
344
|
+
form_fields: z.array(z.object({ name: z.string() }).passthrough()).max(100).optional()
|
|
345
|
+
.describe('Enterprise. One entry per ^M / ^T / ^C / ^R field to label or configure: { name, signer?, type?, ' +
|
|
346
|
+
'label?, required?, format?, max_length?, width?, options?, show_if?, required_if? }. Passed to Siglio as is.'),
|
|
347
|
+
constraints: z.array(z.object({ type: z.string() }).passthrough()).max(50).optional()
|
|
348
|
+
.describe('Enterprise. Rules across fields: { type: exactly_one | at_least | at_most | one_of, fields, n? } ' +
|
|
349
|
+
'or { type: requires, field, when }. Passed to Siglio as is.'),
|
|
350
|
+
idempotency_key: z.string().optional().describe('Rarely needed. One is derived from the document, signers and options if omitted.'),
|
|
193
351
|
},
|
|
194
|
-
}, async ({ document_path, signers, delivery, document_name, idempotency_key }) => {
|
|
352
|
+
}, async ({ document_path, signers, delivery, document_name, payment, attachments, form_fields, constraints, idempotency_key }) => {
|
|
195
353
|
try {
|
|
196
354
|
const pdf = loadPdf(document_path);
|
|
197
355
|
const name = document_name || pdf.name;
|
|
198
|
-
|
|
356
|
+
// email, sms and both are strict, so catch a missing address before spending a call.
|
|
357
|
+
// auto is resolved by Siglio, which answers 400 when a signer has neither.
|
|
199
358
|
for (const s of signers) {
|
|
200
359
|
const d = s.delivery || delivery;
|
|
201
|
-
if (
|
|
360
|
+
if ((d === 'sms' || d === 'both') && !s.phone) throw new Error(s.name + ' is set to receive a text but has no phone number.');
|
|
202
361
|
if ((d === 'email' || d === 'both') && !s.email) throw new Error(s.name + ' is set to receive an email but has no address.');
|
|
203
362
|
}
|
|
204
|
-
const
|
|
205
|
-
|
|
363
|
+
const extras = {};
|
|
364
|
+
if (payment) extras.payment = payment;
|
|
365
|
+
if (attachments) extras.attachments = attachments;
|
|
366
|
+
if (form_fields) extras.form_fields = form_fields;
|
|
367
|
+
if (constraints) extras.constraints = constraints;
|
|
368
|
+
|
|
369
|
+
const key = idempotency_key || 'mcp-' + createHash('sha256')
|
|
370
|
+
.update(pdf.base64 + '|' + name + '|' + delivery + '|' + JSON.stringify(signers) + '|' + JSON.stringify(extras))
|
|
371
|
+
.digest('hex').slice(0, 32);
|
|
206
372
|
|
|
207
|
-
const body = { document_name: name, document_base64: pdf.base64, delivery };
|
|
373
|
+
const body = { document_name: name, document_base64: pdf.base64, delivery, ...extras };
|
|
208
374
|
if (signers.length === 1) body.signer = signers[0]; else body.signers = signers;
|
|
209
375
|
|
|
210
376
|
const env = await api('POST', '/v1/envelopes', { body, idempotencyKey: key });
|
|
211
377
|
return text('Sent.\n\n' + describe(env) +
|
|
212
378
|
'\n\nCheck on it later with check_envelope and this id: ' + env.id +
|
|
379
|
+
'\nWhen the last signer signs, every signer is sent the signed PDF automatically.' +
|
|
213
380
|
(IS_LIVE ? '\n\nThis was a LIVE envelope and will be billed.' : ''));
|
|
214
381
|
} catch (e) { return fail(e); }
|
|
215
382
|
});
|
|
@@ -218,7 +385,8 @@ server.registerTool('check_envelope', {
|
|
|
218
385
|
title: 'Check an envelope',
|
|
219
386
|
description:
|
|
220
387
|
'Returns the current state of an envelope: whether it was delivered, opened, signed, or cancelled, ' +
|
|
221
|
-
'
|
|
388
|
+
'when each signer signed, and any payment, uploaded files and answers. Use this to answer ' +
|
|
389
|
+
'"has it been signed yet?".\n\n' +
|
|
222
390
|
'Read the STATE for whether it is signed, not the signer timestamp. On a completed envelope every ' +
|
|
223
391
|
'signer has signed; a missing signature time only means the time was not recorded.',
|
|
224
392
|
inputSchema: { envelope_id: z.string().describe('The id returned when it was sent, e.g. env_7mmyc...') },
|
|
@@ -231,7 +399,7 @@ server.registerTool('download_signed_document', {
|
|
|
231
399
|
title: 'Download the signed PDF',
|
|
232
400
|
description:
|
|
233
401
|
'Saves the completed, signed PDF to this machine. Only works once every signature is in; an envelope ' +
|
|
234
|
-
'that is still out, or only partially signed, has no signed document yet.',
|
|
402
|
+
'that is still out, or only partially signed, has no signed document yet. ' + KEEP_A_COPY,
|
|
235
403
|
inputSchema: {
|
|
236
404
|
envelope_id: z.string().describe('The envelope id.'),
|
|
237
405
|
save_path: z.string().describe('Full path to write the PDF to, e.g. C:\\Users\\me\\Documents\\signed.pdf'),
|
|
@@ -241,7 +409,27 @@ server.registerTool('download_signed_document', {
|
|
|
241
409
|
const buf = await api('GET', '/v1/envelopes/' + encodeURIComponent(envelope_id) + '/document', { raw: true });
|
|
242
410
|
const out = resolve(save_path);
|
|
243
411
|
writeFileSync(out, buf);
|
|
244
|
-
return text('Saved the signed document to ' + out + ' (' + (buf.length
|
|
412
|
+
return text('Saved the signed document to ' + out + ' (' + size(buf.length) + ').');
|
|
413
|
+
} catch (e) { return fail(e); }
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
server.registerTool('download_attachment', {
|
|
417
|
+
title: 'Download a file the signer uploaded',
|
|
418
|
+
description:
|
|
419
|
+
'Saves one file the signer uploaded (a photo ID, a W-9) to this machine. Get the file id from ' +
|
|
420
|
+
'check_envelope, where each uploaded file is listed with its id (att_...). ' + KEEP_A_COPY,
|
|
421
|
+
inputSchema: {
|
|
422
|
+
envelope_id: z.string().describe('The envelope id.'),
|
|
423
|
+
file_id: z.string().describe('The file id from check_envelope, e.g. att_0123456789abcdef.'),
|
|
424
|
+
save_path: z.string().describe('Full path to write the file to, keeping its extension, e.g. C:\\Users\\me\\Documents\\id.jpg'),
|
|
425
|
+
},
|
|
426
|
+
}, async ({ envelope_id, file_id, save_path }) => {
|
|
427
|
+
try {
|
|
428
|
+
const buf = await api('GET', '/v1/envelopes/' + encodeURIComponent(envelope_id) + '/attachments/' +
|
|
429
|
+
encodeURIComponent(file_id), { raw: true });
|
|
430
|
+
const out = resolve(save_path);
|
|
431
|
+
writeFileSync(out, buf);
|
|
432
|
+
return text('Saved the file to ' + out + ' (' + size(buf.length) + ').');
|
|
245
433
|
} catch (e) { return fail(e); }
|
|
246
434
|
});
|
|
247
435
|
|
|
@@ -268,4 +456,4 @@ server.registerTool('void_envelope', {
|
|
|
268
456
|
|
|
269
457
|
const transport = new StdioServerTransport();
|
|
270
458
|
await server.connect(transport);
|
|
271
|
-
process.stderr.write('siglio-mcp ready (' + (IS_LIVE ? 'LIVE' : 'sandbox') + ' key, ' + API_BASE + ')\n');
|
|
459
|
+
process.stderr.write('siglio-mcp ' + VERSION + ' ready (' + (IS_LIVE ? 'LIVE' : 'sandbox') + ' key, ' + API_BASE + ')\n');
|
package/package.json
CHANGED