@vumasign/mcp 0.1.0 → 0.3.2
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 +8 -3
- package/dist/client.js +12 -6
- package/dist/config.js +7 -1
- package/dist/meta.js +1 -1
- package/dist/server.js +9 -6
- package/dist/tools.js +329 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# @vumasign/mcp
|
|
2
2
|
|
|
3
|
-
An MCP server for a Vumasign account. An agent can read your templates and
|
|
3
|
+
An MCP server for a Vumasign account. An agent can read your templates, their pages and the core
|
|
4
|
+
vocabulary with your organisation’s own keys,
|
|
4
5
|
check the values it means to fill in, and create **draft** envelopes. A person reviews each draft in
|
|
5
6
|
Vumasign and sends it.
|
|
6
7
|
|
|
7
|
-
It never sends an envelope, never mints a signing URL, never
|
|
8
|
-
and never touches a webhook. There is no tool for any of them.
|
|
8
|
+
It never sends an envelope, never mints a signing URL, never names a field freely, never creates a
|
|
9
|
+
template and never touches a webhook. There is no tool for any of them.
|
|
10
|
+
|
|
11
|
+
Three tools change a template, and exist only when `VUMASIGN_MCP_ALLOW_TEMPLATE_WRITES=1` is set:
|
|
12
|
+
`request_naming`, `accept_naming` and `accept_person`. They accept Vumasign’s own naming of a
|
|
13
|
+
template and nothing of the agent’s, and need a live key that also names `templates:name`.
|
|
9
14
|
|
|
10
15
|
## Use
|
|
11
16
|
|
package/dist/client.js
CHANGED
|
@@ -44,6 +44,10 @@ export async function callTool(tool, args, config, fetchImpl) {
|
|
|
44
44
|
let request;
|
|
45
45
|
let url;
|
|
46
46
|
try {
|
|
47
|
+
// The flag is the operator's, not the model's: a write tool is refused without it, however it was reached.
|
|
48
|
+
if (tool.templateWrite === true && config.templateWrites !== true) {
|
|
49
|
+
throw new ToolRefusal('refused_by_mcp_server', `${tool.name} writes to a template, and this server was not started with VUMASIGN_MCP_ALLOW_TEMPLATE_WRITES=1.`);
|
|
50
|
+
}
|
|
47
51
|
request = tool.build(args);
|
|
48
52
|
url = requestUrl(tool, request, config.apiOrigin);
|
|
49
53
|
}
|
|
@@ -76,12 +80,14 @@ export async function callTool(tool, args, config, fetchImpl) {
|
|
|
76
80
|
// committed the request. A repeat with the same arguments replays it.
|
|
77
81
|
return failure('network_error',
|
|
78
82
|
// Keyed on the tool, not the method: validate_values is a POST that writes nothing.
|
|
79
|
-
tool.
|
|
80
|
-
?
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
tool.uncertainOutcome !== undefined
|
|
84
|
+
? tool.uncertainOutcome
|
|
85
|
+
: tool.name === 'create_envelope_draft'
|
|
86
|
+
? `The request to the Vumasign API at ${config.apiOrigin} failed before an answer came back, so the ` +
|
|
87
|
+
'draft may or may not have been made. Call again with exactly the same arguments: a draft that was ' +
|
|
88
|
+
'made is then returned rather than made twice.'
|
|
89
|
+
: `Could not reach the Vumasign API at ${config.apiOrigin}. This tool writes nothing, so call it again ` +
|
|
90
|
+
'when the connection is back.', config.apiKey);
|
|
85
91
|
}
|
|
86
92
|
if (!response.ok)
|
|
87
93
|
return refusal(response, config.apiKey);
|
package/dist/config.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
export const API_KEY_ENV = 'VUMASIGN_API_KEY';
|
|
13
13
|
/** The opt-in a live key needs. Exactly `1`; nothing else counts. */
|
|
14
14
|
export const ALLOW_LIVE_ENV = 'VUMASIGN_MCP_ALLOW_LIVE';
|
|
15
|
+
/**
|
|
16
|
+
* The opt-in for the tools that write to a template: `request_naming`,
|
|
17
|
+
* `accept_naming` and `accept_person` (spec 2026-10-11, 15.3). Exactly `1`;
|
|
18
|
+
* nothing else counts. Without it they are not offered at all.
|
|
19
|
+
*/
|
|
20
|
+
export const ALLOW_TEMPLATE_WRITES_ENV = 'VUMASIGN_MCP_ALLOW_TEMPLATE_WRITES';
|
|
15
21
|
/** Where the API is. Optional; for pointing the server at a local instance. */
|
|
16
22
|
export const API_URL_ENV = 'VUMASIGN_API_URL';
|
|
17
23
|
export const DEFAULT_API_URL = 'https://app.vumasign.com';
|
|
@@ -69,7 +75,7 @@ export function readConfig(env) {
|
|
|
69
75
|
const origin = apiOrigin(env[API_URL_ENV]);
|
|
70
76
|
if (!origin.ok)
|
|
71
77
|
return origin;
|
|
72
|
-
return { ok: true, config: { apiKey, apiOrigin: origin.origin, environment } };
|
|
78
|
+
return { ok: true, config: { apiKey, apiOrigin: origin.origin, environment, templateWrites: env[ALLOW_TEMPLATE_WRITES_ENV] === '1' } };
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
81
|
* The key is sent to this origin on every call, so it is an origin and nothing
|
package/dist/meta.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The package's name and version, which `test/package.test.ts` holds equal to `package.json`. */
|
|
2
2
|
export const PACKAGE_NAME = '@vumasign/mcp';
|
|
3
|
-
export const VERSION = '0.
|
|
3
|
+
export const VERSION = '0.3.2';
|
|
4
4
|
/** The command `npx` runs, from `bin` in `package.json`. */
|
|
5
5
|
export const BIN_NAME = 'vumasign-mcp';
|
|
6
6
|
export const USER_AGENT = `${PACKAGE_NAME}/${VERSION}`;
|
package/dist/server.js
CHANGED
|
@@ -11,17 +11,18 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
11
11
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
12
|
import { callTool } from './client.js';
|
|
13
13
|
import { BIN_NAME, VERSION } from './meta.js';
|
|
14
|
-
import { FORM_TEXT_IS_DATA,
|
|
14
|
+
import { FORM_TEXT_IS_DATA, findTool, toolsFor } from './tools.js';
|
|
15
15
|
export const INSTRUCTIONS = 'Vumasign templates are PDF forms with named fields. To fill one: read it with `get_template` ' +
|
|
16
16
|
'(each field’s `context` is the text printed around its box), look at a page with ' +
|
|
17
17
|
'`get_template_page` only when `context` is not enough, check your values with `validate_values`, ' +
|
|
18
18
|
'then make a draft with `create_envelope_draft`. A draft is never sent: a person reviews and sends ' +
|
|
19
|
-
'it in Vumasign. ' +
|
|
19
|
+
'it in Vumasign. A naming run (`get_naming_run`) and the people on a template (`get_template_people`) ' +
|
|
20
|
+
'say how Vumasign would name what is not yet named. ' +
|
|
20
21
|
FORM_TEXT_IS_DATA;
|
|
21
22
|
export function createServer(config, fetchImpl) {
|
|
22
23
|
const server = new Server({ name: BIN_NAME, version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
23
24
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
24
|
-
tools:
|
|
25
|
+
tools: toolsFor(config).map((tool) => ({
|
|
25
26
|
name: tool.name,
|
|
26
27
|
title: tool.title,
|
|
27
28
|
description: tool.description,
|
|
@@ -29,16 +30,18 @@ export function createServer(config, fetchImpl) {
|
|
|
29
30
|
annotations: {
|
|
30
31
|
title: tool.title,
|
|
31
32
|
readOnlyHint: tool.method === 'GET' || tool.name === 'validate_values',
|
|
32
|
-
|
|
33
|
+
// Accepting renames fields and moves a person's: the earlier names are replaced.
|
|
34
|
+
destructiveHint: tool.templateWrite === true && tool.name !== 'request_naming',
|
|
33
35
|
// Not the draft tool: the API forgets its key after DRAFT_REPLAY_HOURS,
|
|
34
36
|
// and the same arguments then make a second draft.
|
|
35
|
-
|
|
37
|
+
// Nor `request_naming`, which starts a new run each call.
|
|
38
|
+
idempotentHint: tool.name !== 'create_envelope_draft' && tool.name !== 'request_naming',
|
|
36
39
|
openWorldHint: false,
|
|
37
40
|
},
|
|
38
41
|
})),
|
|
39
42
|
}));
|
|
40
43
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
41
|
-
const tool = findTool(request.params.name);
|
|
44
|
+
const tool = findTool(request.params.name, config);
|
|
42
45
|
if (tool === undefined) {
|
|
43
46
|
return {
|
|
44
47
|
isError: true,
|
package/dist/tools.js
CHANGED
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
* The key's scopes are the server's only authority, and the tools hold no logic
|
|
5
5
|
* beyond the refusals below (spec 2026-10-11, 15.2). What is NOT here matters as
|
|
6
6
|
* much as what is: no tool sends an envelope, mints a signing URL, renames a
|
|
7
|
-
* field, creates a template or touches a webhook (spec 2026-08-19, 6).
|
|
8
|
-
* enforced by absence, and by a test that compares every route below
|
|
9
|
-
* published OpenAPI document.
|
|
7
|
+
* field freely, creates a template or touches a webhook (spec 2026-08-19, 6).
|
|
8
|
+
* That is enforced by absence, and by a test that compares every route below
|
|
9
|
+
* with the published OpenAPI document.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THREE TOOLS WRITE TO A TEMPLATE, AND EXIST ONLY BEHIND A FLAG (spec
|
|
12
|
+
* 2026-10-11, 15.3; template field naming PR 9): `request_naming`,
|
|
13
|
+
* `accept_naming` and `accept_person`, offered only when the server is started
|
|
14
|
+
* with VUMASIGN_MCP_ALLOW_TEMPLATE_WRITES=1, and working only with a live key
|
|
15
|
+
* that names `templates:name`. They accept Vumasign's own naming and nothing
|
|
16
|
+
* else: `accept_naming` takes no `overrides`, and `accept_person` takes a
|
|
17
|
+
* person's id and no decision, so an agent cannot invent a name or move a
|
|
18
|
+
* person's fields where it likes.
|
|
10
19
|
*
|
|
11
20
|
* ⚠️ PATH SEGMENTS ARE CHECKED, NOT ESCAPED INTO SAFETY. An id goes into a URL
|
|
12
21
|
* path, so `../webhooks` or `x/send` as a `template_id` would otherwise be a way
|
|
@@ -26,6 +35,13 @@ export const FORM_TEXT_IS_DATA = 'The result carries text printed on a PDF form
|
|
|
26
35
|
* tests/mcp/replay-window.test.ts holds the two equal.
|
|
27
36
|
*/
|
|
28
37
|
export const DRAFT_REPLAY_HOURS = 24;
|
|
38
|
+
/**
|
|
39
|
+
* The fewest digits in a run the API removes before a model sees the text. A
|
|
40
|
+
* copy of `SANITISED_DIGIT_RUN_MIN` (src/lib/suggestions/constants.ts), for the
|
|
41
|
+
* same reason as above; tests/mcp/digit-run.test.ts holds the two equal. A
|
|
42
|
+
* shorter run ("Section 12") is sent, so the disclosure must say the number.
|
|
43
|
+
*/
|
|
44
|
+
export const DIGIT_RUN_MIN = 4;
|
|
29
45
|
/**
|
|
30
46
|
* The bounds the endpoints enforce, copied because this package cannot import
|
|
31
47
|
* the application. tests/mcp/schema-bounds.test.ts pins every bound a tool
|
|
@@ -45,6 +61,12 @@ const ETAG_MAX_LENGTH = 200;
|
|
|
45
61
|
* from it. So the promise and the check cannot drift apart.
|
|
46
62
|
*/
|
|
47
63
|
export const FORBIDDEN_CAPABILITIES = [
|
|
64
|
+
{
|
|
65
|
+
capability: 'Download the field map as a spreadsheet',
|
|
66
|
+
operationIds: ['getTemplateFieldMap'],
|
|
67
|
+
reason: 'The spreadsheet is the template\u2019s fields laid out as CSV for a person. A model reads the same facts ' +
|
|
68
|
+
'as JSON from `get_template`, and sees the numbered boxes with `get_template_field_map`.',
|
|
69
|
+
},
|
|
48
70
|
{
|
|
49
71
|
capability: 'Send an envelope, alone or in a batch',
|
|
50
72
|
operationIds: ['sendEnvelope', 'createEnvelopeBatch'],
|
|
@@ -65,6 +87,14 @@ export const FORBIDDEN_CAPABILITIES = [
|
|
|
65
87
|
reason: 'Naming a template\u2019s fields is for a person or for Vumasign\u2019s own naming, not for an agent ' +
|
|
66
88
|
'inventing names.',
|
|
67
89
|
},
|
|
90
|
+
{
|
|
91
|
+
capability: 'Accept a naming run with names of its own, or decide a person freely',
|
|
92
|
+
operationIds: ['acceptNamingRun', 'decideTemplatePerson'],
|
|
93
|
+
reason: 'Names an agent typed would pass every naming rule and could still swap two people\u2019s details, such as ' +
|
|
94
|
+
'the main member\u2019s ID number and the spouse\u2019s, and a free decision could move a person\u2019s ' +
|
|
95
|
+
'signature boxes to another role. The template-write tools accept only Vumasign\u2019s own proposals, through ' +
|
|
96
|
+
'`accept-current` and `accept-proposal`.',
|
|
97
|
+
},
|
|
68
98
|
{
|
|
69
99
|
capability: 'Create a template, or an envelope from a document of its own',
|
|
70
100
|
operationIds: ['createTemplate', 'createOneOffEnvelope'],
|
|
@@ -161,6 +191,41 @@ function optionalObject(args, name) {
|
|
|
161
191
|
}
|
|
162
192
|
return value;
|
|
163
193
|
}
|
|
194
|
+
/** A person's id, as `GET …/people` gives it: 32 lower-case hexadecimal digits. */
|
|
195
|
+
function personId(args, name) {
|
|
196
|
+
const value = args[name];
|
|
197
|
+
if (typeof value !== 'string' || !/^[0-9a-f]{32}$/.test(value)) {
|
|
198
|
+
throw new ToolRefusal('invalid_request', `\`${name}\` must be a person’s id from \`get_template_people\`: 32 lower-case hexadecimal digits.`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
/** Refuse any argument but these: a write tool takes nothing it does not name. */
|
|
203
|
+
function only(args, names, why) {
|
|
204
|
+
const extra = Object.keys(args).filter((name) => !names.includes(name) && !absent(args[name]));
|
|
205
|
+
if (extra.length > 0) {
|
|
206
|
+
throw new ToolRefusal('refused_by_mcp_server', `This tool takes no \`${extra.join('`, `')}\`. ${why}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** `accept`: `"all"`, `"high"`, or lists of the run's own ids. */
|
|
210
|
+
function acceptSelection(args) {
|
|
211
|
+
const value = args.accept;
|
|
212
|
+
if (value === 'all' || value === 'high')
|
|
213
|
+
return value;
|
|
214
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
215
|
+
throw new ToolRefusal('invalid_request', '`accept` must be "all", "high", or an object of `fields`, `questions` and `people` id lists.');
|
|
216
|
+
}
|
|
217
|
+
const selection = {};
|
|
218
|
+
for (const [key, list] of Object.entries(value)) {
|
|
219
|
+
if (!['fields', 'questions', 'people'].includes(key)) {
|
|
220
|
+
throw new ToolRefusal('invalid_request', `\`accept.${key}\` is not a list this tool takes: \`fields\`, \`questions\` or \`people\`.`);
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(list) || list.some((id) => typeof id !== 'string')) {
|
|
223
|
+
throw new ToolRefusal('invalid_request', `\`accept.${key}\` must be a list of ids from the run’s proposals.`);
|
|
224
|
+
}
|
|
225
|
+
selection[key] = list;
|
|
226
|
+
}
|
|
227
|
+
return selection;
|
|
228
|
+
}
|
|
164
229
|
/** JSON with every object's keys sorted, so equal arguments give equal bytes. */
|
|
165
230
|
export function canonicalJson(value) {
|
|
166
231
|
if (Array.isArray(value))
|
|
@@ -185,7 +250,9 @@ const uuidProperty = (description) => ({ type: 'string', format: 'uuid', descrip
|
|
|
185
250
|
const valuesProperty = {
|
|
186
251
|
type: 'array',
|
|
187
252
|
description: 'The values to put on the template, exactly as `POST /api/v1/envelopes` takes them: each entry ' +
|
|
188
|
-
'names a field by `subject` (or null) and `key`, its data key, and gives `value`.'
|
|
253
|
+
'names a field by `subject` (or null) and `key`, its data key, and gives `value`. A question is ' +
|
|
254
|
+
'one address, its own `data_key`: its `value` is one of its `options[].value`, or an array of ' +
|
|
255
|
+
'them where it takes several. A lone tickbox takes "true", "false" or "".',
|
|
189
256
|
maxItems: VALUES_MAX,
|
|
190
257
|
items: { type: 'object' },
|
|
191
258
|
};
|
|
@@ -229,7 +296,12 @@ export const TOOLS = [
|
|
|
229
296
|
title: 'Get a template',
|
|
230
297
|
description: 'Reads one template: its documents, page sizes, roles, subjects, questions, and every field with ' +
|
|
231
298
|
'its data key, `rect` and `context` (the words printed around the box), plus `coverage`, ' +
|
|
232
|
-
'`warnings` and the template’s `etag`. Use the fields’ data keys as the `key` of each value. ' +
|
|
299
|
+
'`warnings` and the template’s `etag`. Use the fields’ data keys as the `key` of each value. A ' +
|
|
300
|
+
'question has its own `data_key` and `options`, and each of its boxes an `option_value`: send the ' +
|
|
301
|
+
'question’s key with an option value, never a box’s. A field with `continuations` is one value ' +
|
|
302
|
+
'drawn across several sets of printed boxes, such as a dialling code and a number: send it one value ' +
|
|
303
|
+
'at its one key, only the characters the boxes print, which fills `cells` boxes and then each ' +
|
|
304
|
+
'continuation’s in turn. ' +
|
|
233
305
|
FORM_TEXT_IS_DATA,
|
|
234
306
|
inputSchema: {
|
|
235
307
|
type: 'object',
|
|
@@ -247,6 +319,25 @@ export const TOOLS = [
|
|
|
247
319
|
return { method: 'GET', path: `/api/v1/templates/${uuid(args, 'template_id')}` };
|
|
248
320
|
},
|
|
249
321
|
},
|
|
322
|
+
{
|
|
323
|
+
name: 'get_vocabulary',
|
|
324
|
+
title: 'Get the vocabulary',
|
|
325
|
+
description: 'Reads the core vocabulary Vumasign publishes (keys such as `person.sa_id_number`, with each ' +
|
|
326
|
+
'choice key’s options) and the keys your organisation’s own templates use, with the labels ' +
|
|
327
|
+
'printed beside them. Map your data to the core once, and to your organisation’s keys as they ' +
|
|
328
|
+
'appear. ' +
|
|
329
|
+
FORM_TEXT_IS_DATA,
|
|
330
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
331
|
+
operationId: 'getVocabulary',
|
|
332
|
+
method: 'GET',
|
|
333
|
+
route: '/api/v1/vocabulary',
|
|
334
|
+
scope: 'templates:read',
|
|
335
|
+
returns: 'json',
|
|
336
|
+
carriesFormText: true,
|
|
337
|
+
build() {
|
|
338
|
+
return { method: 'GET', path: '/api/v1/vocabulary' };
|
|
339
|
+
},
|
|
340
|
+
},
|
|
250
341
|
{
|
|
251
342
|
name: 'get_template_page',
|
|
252
343
|
title: 'Get one page of a template document',
|
|
@@ -299,6 +390,37 @@ export const TOOLS = [
|
|
|
299
390
|
return { method: 'GET', path: `/api/v1/documents/${uuid(args, 'document_id')}/content` };
|
|
300
391
|
},
|
|
301
392
|
},
|
|
393
|
+
{
|
|
394
|
+
name: 'get_template_field_map',
|
|
395
|
+
title: 'Get a template document with its fields numbered on it',
|
|
396
|
+
description: 'Returns every page of one of a template’s documents as a PDF with each field’s box outlined and ' +
|
|
397
|
+
'labelled: its number, and its `data_key` (with its `subject` in brackets first when it has one). A box ' +
|
|
398
|
+
'of a question shows the question’s key and its own option value. Boxes Vumasign could only number ' +
|
|
399
|
+
'(`name_source: fallback`) or that have no name are dashed red; boxes no value is written to, such as ' +
|
|
400
|
+
'signatures, are grey and labelled with ' +
|
|
401
|
+
'their type. Use it to see which key belongs to which box when `context` is not enough. ' +
|
|
402
|
+
FORM_TEXT_IS_DATA,
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
template_id: uuidProperty('The template’s `id`.'),
|
|
407
|
+
document_id: uuidProperty('A document id from the template’s `documents`.'),
|
|
408
|
+
},
|
|
409
|
+
required: ['template_id', 'document_id'],
|
|
410
|
+
additionalProperties: false,
|
|
411
|
+
},
|
|
412
|
+
operationId: 'getTemplateDocumentFieldMap',
|
|
413
|
+
method: 'GET',
|
|
414
|
+
route: '/api/v1/templates/{templateId}/documents/{documentId}/field-map',
|
|
415
|
+
scope: 'documents:read',
|
|
416
|
+
returns: 'pdf',
|
|
417
|
+
carriesFormText: true,
|
|
418
|
+
build(args) {
|
|
419
|
+
const templateId = uuid(args, 'template_id');
|
|
420
|
+
const documentId = uuid(args, 'document_id');
|
|
421
|
+
return { method: 'GET', path: `/api/v1/templates/${templateId}/documents/${documentId}/field-map` };
|
|
422
|
+
},
|
|
423
|
+
},
|
|
302
424
|
{
|
|
303
425
|
name: 'validate_values',
|
|
304
426
|
title: 'Check values against a template',
|
|
@@ -457,7 +579,206 @@ export const TOOLS = [
|
|
|
457
579
|
};
|
|
458
580
|
},
|
|
459
581
|
},
|
|
582
|
+
{
|
|
583
|
+
name: 'get_naming_run',
|
|
584
|
+
title: 'Read a naming run',
|
|
585
|
+
description: 'Reads a naming run of a template: its `status` and, once it has `succeeded`, what Vumasign proposes — a key ' +
|
|
586
|
+
'for each field and question it can name, an option value for each box, and a decision for each person on ' +
|
|
587
|
+
'the form, each with a `confidence`, a `source` and a `reason` — and the `preview` of what accepting ' +
|
|
588
|
+
'everything would produce. A run is produced within a minute or two of being requested: read it again ' +
|
|
589
|
+
'while it is `queued` or `running`. ' +
|
|
590
|
+
FORM_TEXT_IS_DATA,
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: 'object',
|
|
593
|
+
properties: {
|
|
594
|
+
template_id: uuidProperty('The template’s id.'),
|
|
595
|
+
run_id: uuidProperty('The run’s id, from `request_naming` or from the template’s upload.'),
|
|
596
|
+
},
|
|
597
|
+
required: ['template_id', 'run_id'],
|
|
598
|
+
additionalProperties: false,
|
|
599
|
+
},
|
|
600
|
+
operationId: 'getNamingRun',
|
|
601
|
+
method: 'GET',
|
|
602
|
+
route: '/api/v1/templates/{templateId}/naming-runs/{runId}',
|
|
603
|
+
scope: 'templates:read',
|
|
604
|
+
returns: 'json',
|
|
605
|
+
carriesFormText: true,
|
|
606
|
+
build(args) {
|
|
607
|
+
return { method: 'GET', path: `/api/v1/templates/${uuid(args, 'template_id')}/naming-runs/${uuid(args, 'run_id')}` };
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'get_template_people',
|
|
612
|
+
title: 'Get the people on a template',
|
|
613
|
+
description: 'Lists the people a template is about or signed by — a heading such as “Spouse” or “Dependant 2”, or the ' +
|
|
614
|
+
'caption under a signature line — each with a stable `id`, the decision Vumasign proposes and why, the ' +
|
|
615
|
+
'decisions it may take, the fields in that part of the form, and what already exists for it. It writes ' +
|
|
616
|
+
'nothing. ' +
|
|
617
|
+
FORM_TEXT_IS_DATA,
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: { template_id: uuidProperty('The template’s id.') },
|
|
621
|
+
required: ['template_id'],
|
|
622
|
+
additionalProperties: false,
|
|
623
|
+
},
|
|
624
|
+
operationId: 'listTemplatePeople',
|
|
625
|
+
method: 'GET',
|
|
626
|
+
route: '/api/v1/templates/{templateId}/people',
|
|
627
|
+
scope: 'templates:read',
|
|
628
|
+
returns: 'json',
|
|
629
|
+
carriesFormText: true,
|
|
630
|
+
build(args) {
|
|
631
|
+
return { method: 'GET', path: `/api/v1/templates/${uuid(args, 'template_id')}/people` };
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'request_naming',
|
|
636
|
+
title: 'Ask Vumasign to name a template',
|
|
637
|
+
description: 'Asks Vumasign to propose names for a template’s fields, questions and people: a naming run. It changes ' +
|
|
638
|
+
'nothing on the template. Read the run with `get_naming_run` until it has `succeeded`, show the person ' +
|
|
639
|
+
'its `preview`, then accept it with `accept_naming`. `use_ai` asks for a model as well: text inside the ' +
|
|
640
|
+
'form’s boxes is removed before the model sees it, and text outside every box is still sent after runs of ' +
|
|
641
|
+
`${DIGIT_RUN_MIN} or more digits, ID and passport numbers, email addresses, phone numbers and dates are removed.`,
|
|
642
|
+
inputSchema: {
|
|
643
|
+
type: 'object',
|
|
644
|
+
properties: {
|
|
645
|
+
template_id: uuidProperty('The template’s id.'),
|
|
646
|
+
use_ai: { type: 'boolean', description: 'Ask for a model as well as the vocabulary. Default false.' },
|
|
647
|
+
blank_form: {
|
|
648
|
+
type: 'boolean',
|
|
649
|
+
deprecated: true,
|
|
650
|
+
description: 'Deprecated: nothing needs it, and it never decides whether a model is asked. Accepted, and passed on as sent, for callers that still send it.',
|
|
651
|
+
},
|
|
652
|
+
request_id: {
|
|
653
|
+
type: 'string',
|
|
654
|
+
description: 'Leave it out normally: calling again with the same arguments within ' +
|
|
655
|
+
`${DRAFT_REPLAY_HOURS} hours returns the run the first call started. To ask for a fresh run on purpose (the ` +
|
|
656
|
+
'template changed, so accepting the first was refused), pass any new value here.',
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
required: ['template_id'],
|
|
660
|
+
additionalProperties: false,
|
|
661
|
+
},
|
|
662
|
+
operationId: 'requestNamingRun',
|
|
663
|
+
method: 'POST',
|
|
664
|
+
route: '/api/v1/templates/{templateId}/naming-runs',
|
|
665
|
+
scope: 'templates:name',
|
|
666
|
+
returns: 'json',
|
|
667
|
+
carriesFormText: false,
|
|
668
|
+
templateWrite: true,
|
|
669
|
+
uncertainOutcome: 'The request to the Vumasign API failed before an answer came back, so a naming run may or may not have been started. Call again with exactly the same arguments: a run that was started is then returned rather than started twice.',
|
|
670
|
+
build(args) {
|
|
671
|
+
const templateId = uuid(args, 'template_id');
|
|
672
|
+
const body = {};
|
|
673
|
+
for (const name of ['use_ai', 'blank_form']) {
|
|
674
|
+
if (absent(args[name]))
|
|
675
|
+
continue;
|
|
676
|
+
if (typeof args[name] !== 'boolean')
|
|
677
|
+
throw new ToolRefusal('invalid_request', `\`${name}\` must be true or false.`);
|
|
678
|
+
body[name] = args[name];
|
|
679
|
+
}
|
|
680
|
+
// `blank_form` is deprecated and required by nothing (set up this form, spec D14): passed on as sent, never refused.
|
|
681
|
+
/*
|
|
682
|
+
ONE KEY PER LOGICAL REQUEST (Codex on PR 206): a digest of the template
|
|
683
|
+
and the body, and of `request_id` when given, so a retry after a lost
|
|
684
|
+
answer replays the run the first call started rather than starting a
|
|
685
|
+
second. A fresh run is asked for with a new `request_id`.
|
|
686
|
+
*/
|
|
687
|
+
const requestId = optionalString(args, 'request_id');
|
|
688
|
+
const digest = createHash('sha256')
|
|
689
|
+
.update(canonicalJson({ template_id: templateId, body, ...(requestId === undefined ? {} : { request_id: requestId }) }))
|
|
690
|
+
.digest('hex');
|
|
691
|
+
return { method: 'POST', path: `/api/v1/templates/${templateId}/naming-runs`, body, headers: { 'Idempotency-Key': `vumasign-mcp-naming-${digest}` } };
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'accept_naming',
|
|
696
|
+
title: 'Accept a naming run',
|
|
697
|
+
description: 'Applies a naming run’s own proposals to the template, all of them or some, in one change — if the template ' +
|
|
698
|
+
'has not changed since the run was made; if it has, request a new run. Show the person the run’s `preview` ' +
|
|
699
|
+
'first. `accept` is "all", "high" (only high-confidence proposals, never a model’s, and no person), or lists ' +
|
|
700
|
+
'of the run’s own ids. It takes no names of its own: to name a field differently, the person does it in ' +
|
|
701
|
+
'Vumasign.',
|
|
702
|
+
inputSchema: {
|
|
703
|
+
type: 'object',
|
|
704
|
+
properties: {
|
|
705
|
+
template_id: uuidProperty('The template’s id.'),
|
|
706
|
+
run_id: uuidProperty('The run’s id.'),
|
|
707
|
+
accept: {
|
|
708
|
+
description: '"all", "high", or `{ "fields": [...], "questions": [...], "people": [...] }` with ids from the run’s proposals.',
|
|
709
|
+
anyOf: [
|
|
710
|
+
{ type: 'string', enum: ['all', 'high'] },
|
|
711
|
+
{
|
|
712
|
+
type: 'object',
|
|
713
|
+
properties: {
|
|
714
|
+
fields: { type: 'array', items: { type: 'string' } },
|
|
715
|
+
questions: { type: 'array', items: { type: 'string' } },
|
|
716
|
+
people: { type: 'array', items: { type: 'string' } },
|
|
717
|
+
},
|
|
718
|
+
additionalProperties: false,
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
required: ['template_id', 'run_id', 'accept'],
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
},
|
|
726
|
+
operationId: 'acceptNamingRunCurrent',
|
|
727
|
+
method: 'POST',
|
|
728
|
+
route: '/api/v1/templates/{templateId}/naming-runs/{runId}/accept-current',
|
|
729
|
+
scope: 'templates:name',
|
|
730
|
+
returns: 'json',
|
|
731
|
+
carriesFormText: true,
|
|
732
|
+
templateWrite: true,
|
|
733
|
+
uncertainOutcome: 'The request to the Vumasign API failed before an answer came back, so the run’s names may or may not have been applied to the template. Do not simply retry: read the template with `get_template` (or the run with `get_naming_run`) first. If they were applied, the template has changed and accepting again is refused as `template_changed`.',
|
|
734
|
+
build(args) {
|
|
735
|
+
// ⚠️ NO OVERRIDES, WHATEVER IT IS ASKED (spec 2026-10-11, 15.3): only Vumasign's own proposals.
|
|
736
|
+
only(args, ['template_id', 'run_id', 'accept'], 'It accepts only the run’s own proposals; a person names a field differently in Vumasign.');
|
|
737
|
+
const templateId = uuid(args, 'template_id');
|
|
738
|
+
const runId = uuid(args, 'run_id');
|
|
739
|
+
return {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
path: `/api/v1/templates/${templateId}/naming-runs/${runId}/accept-current`,
|
|
742
|
+
body: { accept: acceptSelection(args) },
|
|
743
|
+
};
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: 'accept_person',
|
|
748
|
+
title: 'Accept Vumasign’s proposal for one person',
|
|
749
|
+
description: 'Applies Vumasign’s own current proposal for one person on a template — the decision `get_template_people` ' +
|
|
750
|
+
'shows as `proposed` — which sets up the subject and signing role it names and moves that person’s fields ' +
|
|
751
|
+
'to them. It takes the person’s id and nothing else: it cannot choose a different decision. If the proposal ' +
|
|
752
|
+
'has changed or would change nothing, it is refused; read the people again.',
|
|
753
|
+
inputSchema: {
|
|
754
|
+
type: 'object',
|
|
755
|
+
properties: {
|
|
756
|
+
template_id: uuidProperty('The template’s id.'),
|
|
757
|
+
person_id: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'The person’s `id`, from `get_template_people`.' },
|
|
758
|
+
},
|
|
759
|
+
required: ['template_id', 'person_id'],
|
|
760
|
+
additionalProperties: false,
|
|
761
|
+
},
|
|
762
|
+
operationId: 'acceptTemplatePersonProposal',
|
|
763
|
+
method: 'POST',
|
|
764
|
+
route: '/api/v1/templates/{templateId}/people/{personId}/accept-proposal',
|
|
765
|
+
scope: 'templates:name',
|
|
766
|
+
returns: 'json',
|
|
767
|
+
carriesFormText: true,
|
|
768
|
+
templateWrite: true,
|
|
769
|
+
uncertainOutcome: 'The request to the Vumasign API failed before an answer came back, so the decision for this person may or may not have been applied. Do not simply retry: read the people with `get_template_people` and look at this person’s `current` first. If it was applied, accepting again is refused as `proposal_changed`.',
|
|
770
|
+
build(args) {
|
|
771
|
+
// ⚠️ NO DECISION OF THE AGENT'S OWN (spec 2026-10-11, 15.3): the server applies its own proposal.
|
|
772
|
+
only(args, ['template_id', 'person_id'], 'It applies Vumasign’s own proposal for the person; a person decides otherwise in Vumasign.');
|
|
773
|
+
const templateId = uuid(args, 'template_id');
|
|
774
|
+
return { method: 'POST', path: `/api/v1/templates/${templateId}/people/${personId(args, 'person_id')}/accept-proposal` };
|
|
775
|
+
},
|
|
776
|
+
},
|
|
460
777
|
];
|
|
778
|
+
/** The tools a server started with `config` offers: the template-write tools only behind their flag. */
|
|
779
|
+
export function toolsFor(config) {
|
|
780
|
+
return TOOLS.filter((tool) => tool.templateWrite !== true || config.templateWrites === true);
|
|
781
|
+
}
|
|
461
782
|
/** The route as a pattern: each `{placeholder}` is one path segment and nothing else. */
|
|
462
783
|
export function routePattern(route) {
|
|
463
784
|
const escaped = route
|
|
@@ -466,6 +787,7 @@ export function routePattern(route) {
|
|
|
466
787
|
.join('[^/]+');
|
|
467
788
|
return new RegExp(`^${escaped}$`);
|
|
468
789
|
}
|
|
469
|
-
|
|
470
|
-
|
|
790
|
+
/** A tool this server offers, by name: a template-write tool only when `config` allows them. */
|
|
791
|
+
export function findTool(name, config = {}) {
|
|
792
|
+
return toolsFor(config).find((tool) => tool.name === name);
|
|
471
793
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vumasign/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "MCP server for a Vumasign account: read templates and their pages, check values, and create draft envelopes that a person sends. It never sends and never mints a signing URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|