@formio/uag 2.0.0-rc.5 → 2.0.0-rc.7

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 (75) hide show
  1. package/bin/uag.js +6 -2
  2. package/lib/UAGFormInterface.d.ts +12 -0
  3. package/lib/UAGFormInterface.js +30 -3
  4. package/lib/UAGProjectInterface.d.ts +9 -1
  5. package/lib/UAGProjectInterface.js +14 -3
  6. package/lib/agent/anonymous.js +1 -1
  7. package/lib/agent/engines.d.ts +10 -0
  8. package/lib/agent/engines.js +15 -1
  9. package/lib/agent/errors.d.ts +9 -0
  10. package/lib/agent/errors.js +20 -0
  11. package/lib/agent/index.d.ts +1 -1
  12. package/lib/agent/index.js +2 -1
  13. package/lib/agent/remote.d.ts +5 -1
  14. package/lib/agent/remote.js +25 -2
  15. package/lib/agent/router.d.ts +32 -2
  16. package/lib/agent/router.js +17 -11
  17. package/lib/agent/stubs.d.ts +13 -2
  18. package/lib/agent/stubs.js +18 -5
  19. package/lib/agent/tools.d.ts +24 -3
  20. package/lib/agent/tools.js +47 -4
  21. package/lib/config.d.ts +31 -2
  22. package/lib/documents/convert.d.ts +38 -0
  23. package/lib/documents/convert.js +151 -0
  24. package/lib/documents/converters.d.ts +26 -0
  25. package/lib/documents/converters.js +139 -0
  26. package/lib/documents/detect.d.ts +31 -0
  27. package/lib/documents/detect.js +169 -0
  28. package/lib/documents/index.d.ts +9 -0
  29. package/lib/documents/index.js +43 -0
  30. package/lib/documents/mime.d.ts +39 -0
  31. package/lib/documents/mime.js +100 -0
  32. package/lib/documents/plugins.d.ts +18 -0
  33. package/lib/documents/plugins.js +65 -0
  34. package/lib/documents/readers.d.ts +19 -0
  35. package/lib/documents/readers.js +64 -0
  36. package/lib/documents/routes.d.ts +52 -0
  37. package/lib/documents/routes.js +99 -0
  38. package/lib/documents/types.d.ts +126 -0
  39. package/lib/documents/types.js +2 -0
  40. package/lib/documents/uagJs.d.ts +17 -0
  41. package/lib/documents/uagJs.js +157 -0
  42. package/lib/formflow.d.ts +4 -1
  43. package/lib/formflow.js +2 -2
  44. package/lib/index.d.ts +50 -6
  45. package/lib/index.js +150 -15
  46. package/lib/markdown.d.ts +1 -0
  47. package/lib/markdown.js +166 -0
  48. package/lib/modules/api.d.ts +22 -0
  49. package/lib/modules/api.js +100 -0
  50. package/lib/modules/index.d.ts +4 -0
  51. package/lib/modules/index.js +24 -0
  52. package/lib/modules/routes.d.ts +14 -0
  53. package/lib/modules/routes.js +21 -0
  54. package/lib/modules/types.d.ts +68 -0
  55. package/lib/modules/types.js +5 -0
  56. package/lib/pluginFiles.d.ts +16 -0
  57. package/lib/pluginFiles.js +62 -0
  58. package/lib/plugins.d.ts +17 -0
  59. package/lib/plugins.js +93 -0
  60. package/lib/richText.d.ts +20 -0
  61. package/lib/richText.js +99 -0
  62. package/lib/router.d.ts +9 -0
  63. package/lib/router.js +19 -0
  64. package/lib/testing.d.ts +74 -0
  65. package/lib/testing.js +165 -0
  66. package/lib/tools/index.d.ts +2 -1
  67. package/lib/tools/index.js +5 -2
  68. package/lib/tools/readDocument.d.ts +17 -0
  69. package/lib/tools/readDocument.js +99 -0
  70. package/lib/tools/submissionUpdate.js +3 -2
  71. package/lib/tools/utils.d.ts +40 -0
  72. package/lib/tools/utils.js +16 -1
  73. package/package.json +10 -5
  74. package/testing.d.ts +1 -0
  75. package/testing.js +2 -0
package/lib/testing.js ADDED
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.testForm = testForm;
7
+ exports.testAuth = testAuth;
8
+ exports.createTestModuleApi = createTestModuleApi;
9
+ exports.mountModuleRoutes = mountModuleRoutes;
10
+ /**
11
+ * @formio/uag/testing: test a module without a Form.io project or a running UAG.
12
+ *
13
+ * const { createTestModuleApi, testForm, testAuth, mountModuleRoutes } = require('@formio/uag/testing');
14
+ *
15
+ * const api = await createTestModuleApi(MyModule, { config: { myModule: { max: 2 } } });
16
+ * const app = await mountModuleRoutes(MyModule, { form: testForm(definition), auth: testAuth() });
17
+ * await supertest(app).post('/notes/my-module/check').send({ ... });
18
+ *
19
+ * The API is the UAG's own (errors, stream, forms, converters, engine handles,
20
+ * Markdown, callerKey), with stand-ins only for what needs a project: the
21
+ * caller's access (from `testAuth`), the license (`licensed`), engines
22
+ * (`engines`) and stored files (`testForm` reads base64 values; `files` for
23
+ * others). Settings go through the module's own `settings`, with the server's rules.
24
+ */
25
+ const express_1 = __importDefault(require("express"));
26
+ const crypto_1 = require("crypto");
27
+ const appserver_1 = require("@formio/appserver");
28
+ const formflow_1 = require("./formflow");
29
+ const plugins_1 = require("./documents/plugins");
30
+ const errors_1 = require("./agent/errors");
31
+ const version_1 = require("./version");
32
+ const api_1 = require("./modules/api");
33
+ const routes_1 = require("./modules/routes");
34
+ /** A form for tests, as module routes get it: `definition` is the form's JSON. */
35
+ function testForm(definition, options = {}) {
36
+ const form = {
37
+ id: options.id || String(definition._id || 'test-form'),
38
+ name: String(definition.name || definition.path || 'test'),
39
+ path: String(definition.path || definition.name || 'test'),
40
+ title: String(definition.title || definition.name || ''),
41
+ definition,
42
+ async readFile(value, _auth, readOptions = {}) {
43
+ const url = String(value?.url || '');
44
+ if (url.startsWith('data:')) {
45
+ const bytes = Buffer.from(url.slice(url.indexOf(',') + 1), url.includes(';base64,') ? 'base64' : 'utf8');
46
+ if (readOptions.maxBytes !== undefined && bytes.length > readOptions.maxBytes) {
47
+ throw new Error(`The file "${value?.originalName || value?.name || 'unnamed'}" is larger than the ${readOptions.maxBytes} bytes that may be read.`);
48
+ }
49
+ return bytes;
50
+ }
51
+ if (options.files)
52
+ return options.files(value, readOptions);
53
+ throw new Error('testForm reads base64 (data: URL) files only: give createTestModuleApi or mountModuleRoutes a "files" function for others.');
54
+ }
55
+ };
56
+ return form;
57
+ }
58
+ /**
59
+ * A caller for tests: a user with these permissions on every form (create and
60
+ * update by default), as a list (`['read']`) or as `formPermissions()` gives
61
+ * them (`{ read: true }`).
62
+ */
63
+ function testAuth(options = {}) {
64
+ const given = options.permissions || ['create', 'update'];
65
+ const granted = { create: false, read: false, update: false, delete: false };
66
+ if (Array.isArray(given)) {
67
+ for (const method of given)
68
+ granted[method] = true;
69
+ }
70
+ else if (given && typeof given === 'object') {
71
+ for (const method of Object.keys(granted))
72
+ granted[method] = given[method] === true;
73
+ }
74
+ else {
75
+ throw new Error('testAuth: "permissions" is a list of create, read, update and delete, or { read: true, ... }');
76
+ }
77
+ return {
78
+ user: options.user || { _id: 'test-user' },
79
+ admin: !!options.admin,
80
+ formPermissions: () => (options.admin ? { create: true, read: true, update: true, delete: true } : granted)
81
+ };
82
+ }
83
+ /** The module, checked for what a test needs: a name and an apiVersion. */
84
+ function loadedOf(module, settings) {
85
+ if (!module || typeof module.name !== 'string' || module.apiVersion === undefined) {
86
+ throw new Error('The module needs a "name" and an "apiVersion" to be tested as a module.');
87
+ }
88
+ return { definition: module, name: module.name, source: 'test', settings };
89
+ }
90
+ /** The module's settings, as the server resolves them. */
91
+ function settingsOf(module, options) {
92
+ if (options.settings)
93
+ return Object.freeze({ ...options.settings });
94
+ const settings = module.settings ? module.settings(options.config || {}, options.env || {}) : {};
95
+ if (settings && typeof settings.then === 'function')
96
+ throw new Error(`Module ${module.name}: "settings" returned a promise; it must return the settings themselves (no async)`);
97
+ return Object.freeze({ ...(settings || {}) });
98
+ }
99
+ /** The UAG's module API for a module, in a test. */
100
+ async function createTestModuleApi(module, options = {}) {
101
+ const loaded = loadedOf(module, settingsOf(module, options));
102
+ const registry = new appserver_1.PluginRegistry();
103
+ (0, plugins_1.registerDocuments)(registry);
104
+ const licensed = options.licensed !== false;
105
+ const requireLicense = async () => {
106
+ if (!licensed)
107
+ throw new formflow_1.FormflowError(403, 'Agentic Formflow requires a license (the test API was created with licensed: false).', 'license_required');
108
+ };
109
+ const engines = (options.engines || []).map((engine) => ({ kind: 'agent', model: engine.name, accepts: ['text'], ...engine }));
110
+ const base = (0, appserver_1.createBaseModuleApi)(loaded, registry, { name: () => 'uag', version: () => version_1.UAG_VERSION, errorBody: routes_1.uagErrorBody });
111
+ const api = (0, api_1.uagModuleApi)(base, {
112
+ registry: () => registry,
113
+ engines: () => ({ get: (name) => engines.find((engine) => engine.name === name), agent: engines.filter((engine) => engine.kind === 'agent') }),
114
+ requireLicense
115
+ });
116
+ const pluginTypes = typeof module.pluginTypes === 'function' ? module.pluginTypes(api) : (module.pluginTypes || []);
117
+ for (const type of pluginTypes)
118
+ registry.addType(type, module.name);
119
+ if (module.plugins)
120
+ registry.add(module.plugins, module.name);
121
+ if (options.plugins?.length)
122
+ registry.add(options.plugins, 'test');
123
+ await registry.initialize({ server: {} });
124
+ return api;
125
+ }
126
+ /**
127
+ * The module's form routes on an Express app, as the UAG mounts them on a
128
+ * form (by its path, and by `form/<id>`), for supertest: with the form, the
129
+ * caller, a request id, the access check and the UAG's error shape. No rate
130
+ * limits. `api` is created with `options` unless given.
131
+ */
132
+ async function mountModuleRoutes(module, options) {
133
+ const api = options.api || await createTestModuleApi(module, options);
134
+ const loaded = loadedOf(module, api.settings);
135
+ const app = (0, express_1.default)();
136
+ app.use(express_1.default.json({ limit: '50mb' }));
137
+ const host = {
138
+ errorBody: routes_1.uagErrorBody,
139
+ routeHandlers: (_project, route) => ({
140
+ before: [(req, res, next) => {
141
+ req.requestId = req.get('x-request-id') || (0, crypto_1.randomUUID)();
142
+ res.set('X-Request-Id', req.requestId);
143
+ next();
144
+ }],
145
+ wrap: (run) => async (req, res) => {
146
+ try {
147
+ await run(req, res);
148
+ }
149
+ catch (err) {
150
+ const { status, body } = (0, errors_1.agentError)(req.requestId, err);
151
+ res.status(status).json(body);
152
+ }
153
+ }
154
+ })
155
+ };
156
+ const router = (0, appserver_1.moduleFormRouter)({}, loaded, api, host);
157
+ const withForm = (req, res, next) => {
158
+ req.form = options.form;
159
+ req.auth = options.auth || testAuth();
160
+ next();
161
+ };
162
+ app.use(`/${options.form.path}`, withForm, router);
163
+ app.use(`/form/${options.form.id}`, withForm, router);
164
+ return app;
165
+ }
@@ -11,7 +11,8 @@ import { UAGProjectInterface } from "../UAGProjectInterface";
11
11
  import { agentProvide } from "./agentProvide";
12
12
  import { agentDecide } from "./agentDecide";
13
13
  import { fetchExternalData } from "./fetchExternalData";
14
+ import { readDocument } from "./readDocument";
14
15
  export * from './utils';
15
16
  export * from './annotations';
16
- export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide, agentDecide };
17
+ export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide, agentDecide, readDocument };
17
18
  export declare const getTools: (project: UAGProjectInterface) => Promise<ToolInfo[]>;
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getTools = exports.agentDecide = exports.agentProvide = exports.fetchExternalData = exports.submissionUpdate = exports.findSubmission = exports.submitCompletedForm = exports.confirmSubmission = exports.collectData = exports.getFieldInfo = exports.getFormFields = exports.getForms = void 0;
17
+ exports.getTools = exports.readDocument = exports.agentDecide = exports.agentProvide = exports.fetchExternalData = exports.submissionUpdate = exports.findSubmission = exports.submitCompletedForm = exports.confirmSubmission = exports.collectData = exports.getFieldInfo = exports.getFormFields = exports.getForms = void 0;
18
18
  const getForms_1 = require("./getForms");
19
19
  Object.defineProperty(exports, "getForms", { enumerable: true, get: function () { return getForms_1.getForms; } });
20
20
  const getFormFields_1 = require("./getFormFields");
@@ -37,6 +37,8 @@ const agentDecide_1 = require("./agentDecide");
37
37
  Object.defineProperty(exports, "agentDecide", { enumerable: true, get: function () { return agentDecide_1.agentDecide; } });
38
38
  const fetchExternalData_1 = require("./fetchExternalData");
39
39
  Object.defineProperty(exports, "fetchExternalData", { enumerable: true, get: function () { return fetchExternalData_1.fetchExternalData; } });
40
+ const readDocument_1 = require("./readDocument");
41
+ Object.defineProperty(exports, "readDocument", { enumerable: true, get: function () { return readDocument_1.readDocument; } });
40
42
  __exportStar(require("./utils"), exports);
41
43
  __exportStar(require("./annotations"), exports);
42
44
  const getTools = async (project) => {
@@ -51,7 +53,8 @@ const getTools = async (project) => {
51
53
  (0, submissionUpdate_1.submissionUpdate)(project),
52
54
  (0, agentProvide_1.agentProvide)(project),
53
55
  (0, agentDecide_1.agentDecide)(project),
54
- (0, fetchExternalData_1.fetchExternalData)(project)
56
+ (0, fetchExternalData_1.fetchExternalData)(project),
57
+ (0, readDocument_1.readDocument)(project)
55
58
  ]);
56
59
  };
57
60
  exports.getTools = getTools;
@@ -0,0 +1,17 @@
1
+ import type { UAGProjectInterface } from '../UAGProjectInterface';
2
+ import { ToolInfo } from './utils';
3
+ /**
4
+ * read_document's limits: the largest file it reads (`documents.maxFileBytes`,
5
+ * else DOCUMENTS_MAX_FILE_BYTES, 25 MB), and the most characters of text it
6
+ * returns (`agentApi.files.maxChars`, as task inputs are cut, 200000).
7
+ */
8
+ export declare function readDocumentLimits(config: any, env?: NodeJS.ProcessEnv): {
9
+ maxBytes: number;
10
+ maxChars: number;
11
+ };
12
+ /**
13
+ * read_document: the text of an uploaded document in a submission (a file in a
14
+ * file component, or in an AI Document's files), read through the installed
15
+ * converters with the caller's identity. Read-only.
16
+ */
17
+ export declare const readDocument: (project: UAGProjectInterface) => Promise<ToolInfo>;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readDocument = void 0;
7
+ exports.readDocumentLimits = readDocumentLimits;
8
+ const zod_1 = __importDefault(require("zod"));
9
+ const lodash_1 = require("lodash");
10
+ const core_1 = require("@formio/core");
11
+ const template_1 = require("../template");
12
+ const annotations_1 = require("./annotations");
13
+ const readers_1 = require("../documents/readers");
14
+ const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024;
15
+ const DEFAULT_MAX_CHARS = 200000;
16
+ const positive = (value) => (Number(value) > 0 ? Number(value) : undefined);
17
+ /**
18
+ * read_document's limits: the largest file it reads (`documents.maxFileBytes`,
19
+ * else DOCUMENTS_MAX_FILE_BYTES, 25 MB), and the most characters of text it
20
+ * returns (`agentApi.files.maxChars`, as task inputs are cut, 200000).
21
+ */
22
+ function readDocumentLimits(config, env = process.env) {
23
+ return {
24
+ maxBytes: positive(config?.documents?.maxFileBytes) || positive(env.DOCUMENTS_MAX_FILE_BYTES) || DEFAULT_MAX_FILE_BYTES,
25
+ maxChars: positive(config?.agentApi?.files?.maxChars) || DEFAULT_MAX_CHARS
26
+ };
27
+ }
28
+ /** The component at a data path, row indices removed (`rows[2].contract` is `rows.contract`). */
29
+ function componentAt(components, dataPath) {
30
+ const path = dataPath.replace(/\[\d+\]/g, '');
31
+ let found;
32
+ // The data path: with every component included, the second argument also names layout components (panels, columns).
33
+ core_1.Utils.eachComponent(components, (component, componentPath, _components, _parent, paths) => {
34
+ if (!found && (paths?.path ?? componentPath) === path)
35
+ found = component;
36
+ }, true);
37
+ return found;
38
+ }
39
+ /**
40
+ * read_document: the text of an uploaded document in a submission (a file in a
41
+ * file component, or in an AI Document's files), read through the installed
42
+ * converters with the caller's identity. Read-only.
43
+ */
44
+ const readDocument = async (project) => {
45
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.read_document, {
46
+ name: 'read_document',
47
+ title: 'Read Uploaded Document',
48
+ description: 'Read the text of a document uploaded to a submission: a file in a file field, or in an AI Document field. ' +
49
+ 'Give the form, the submission id (from `find_submissions`) and the data path of the field (such as "contract" or "rows[0].attachment"; ' +
50
+ 'add an index, such as "contract[1]", for another file than the first). Returns the file name, its type, its page count when known, and its text; ' +
51
+ 'a long text is cut, and `truncated` is true.',
52
+ annotations: (0, annotations_1.reads)('Read an uploaded document'),
53
+ inputSchema: {
54
+ form_name: zod_1.default.string().describe('The name of the form, as `get_forms` lists it.'),
55
+ submission_id: zod_1.default.string().describe('The full ID of the submission, as `find_submissions` gives it.'),
56
+ data_path: zod_1.default.string().describe('The data path of the file field or AI Document field.')
57
+ },
58
+ execute: async ({ form_name, submission_id, data_path }, extra) => {
59
+ const form = await project.getForm(form_name);
60
+ if (!form) {
61
+ return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
62
+ }
63
+ const submission = await form.loadSubmission(submission_id, extra.authInfo);
64
+ if (!submission) {
65
+ return project.mcpResponse(template_1.ResponseTemplate.submissionNotFound, { submissionId: submission_id, form: form.form }, true);
66
+ }
67
+ // An index on the last segment picks a file of a multiple field: contract[1].
68
+ const index = /\[(\d+)\]$/.exec(data_path);
69
+ const fieldPath = index ? data_path.slice(0, index.index) : data_path;
70
+ const value = (0, lodash_1.get)(submission.data, fieldPath);
71
+ const isDocument = value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.files);
72
+ const files = isDocument ? value.files : Array.isArray(value) ? value : (value && typeof value === 'object' ? [value] : []);
73
+ const file = files[index ? Number(index[1]) : 0];
74
+ const component = componentAt(form.form?.components || [], isDocument ? `${fieldPath}.files` : fieldPath);
75
+ if (!file || !component || component.type !== 'file') {
76
+ return project.mcpJSONResponse({ error: `No uploaded file at "${data_path}" in that submission. Give the data path of a file field or an AI Document field.` }, true);
77
+ }
78
+ if (!project.plugins) {
79
+ return project.mcpJSONResponse({ error: 'Documents cannot be read until the server has started.' }, true);
80
+ }
81
+ const limits = readDocumentLimits(project.config);
82
+ try {
83
+ const buffer = await form.readFile(file, extra.authInfo, { maxBytes: limits.maxBytes, component });
84
+ if (!buffer) {
85
+ return project.mcpJSONResponse({ error: `The file is stored as ${file.storage || 'unknown'}, which cannot be read here.` }, true);
86
+ }
87
+ const name = String(file.originalName || file.name || 'file');
88
+ const read = await (0, readers_1.readAsText)(project.plugins, buffer, { name, type: typeof file.type === 'string' ? file.type : undefined }, { signal: extra?.signal });
89
+ const truncated = read.text.length > limits.maxChars;
90
+ return project.mcpJSONResponse({ file_name: name, type: read.type, pages: read.pages ?? null, text: truncated ? read.text.slice(0, limits.maxChars) : read.text, truncated });
91
+ }
92
+ catch (err) {
93
+ // The message names the file and the reason, never its content.
94
+ return project.mcpJSONResponse({ error: err?.message || 'The file could not be read.' }, true);
95
+ }
96
+ }
97
+ });
98
+ };
99
+ exports.readDocument = readDocument;
@@ -35,10 +35,11 @@ const submissionUpdate = async (project) => {
35
35
  const updateSummary = [];
36
36
  for (const update of updates) {
37
37
  const previousValue = (0, lodash_1.get)(updatedData, update.data_path);
38
- (0, lodash_1.set)(updatedData, update.data_path, update.new_value);
38
+ const newValue = form.normalizeWrittenValue(update.data_path, update.new_value);
39
+ (0, lodash_1.set)(updatedData, update.data_path, newValue);
39
40
  updateSummary.push({
40
41
  data_path: update.data_path,
41
- new_value: update.new_value,
42
+ new_value: newValue,
42
43
  previous_value: previousValue || ''
43
44
  });
44
45
  }
@@ -1,5 +1,7 @@
1
1
  import { ZodRawShape } from "zod";
2
2
  import { ToolAnnotations } from "./annotations";
3
+ import type { AuthRequest } from "@formio/appserver";
4
+ import type { UAGProjectInterface } from "../UAGProjectInterface";
3
5
  /**
4
6
  * Escape a literal value so it can be carried in a `__regex` query parameter.
5
7
  *
@@ -20,14 +22,52 @@ import { ToolAnnotations } from "./annotations";
20
22
  * @returns The value as a regular expression pattern that matches it literally.
21
23
  */
22
24
  export declare const escapeSearchPattern: (value: string) => string;
25
+ /**
26
+ * What a tool's `execute` receives besides its arguments, on every path that
27
+ * runs it: MCP clients on `/mcp`, and Agent API research in process.
28
+ */
29
+ export type ToolExtra = {
30
+ /** The caller's identity: who the work runs as. Pass it to form methods so Form.io applies its permissions. */
31
+ authInfo: AuthRequest;
32
+ /** The project serving the request. */
33
+ project: UAGProjectInterface;
34
+ /** For a tool from a module with a name and an apiVersion: the module's API (its settings, engines, ...). */
35
+ module?: import('../modules').UAGModuleApi;
36
+ /** On `/mcp`, the rest of the MCP SDK's request context (signal, request id, ...). */
37
+ [key: string]: any;
38
+ };
23
39
  export type ToolInfo = {
24
40
  name?: string;
25
41
  title?: string;
26
42
  description?: string;
27
43
  inputSchema?: ZodRawShape | undefined;
28
44
  annotations?: ToolAnnotations;
45
+ /**
46
+ * Runs the tool: always called with its arguments (an empty object when it
47
+ * takes none), then a `ToolExtra`. Typed loosely so existing tools and their
48
+ * callers keep compiling; annotate `extra` as `ToolExtra` in your own tools.
49
+ */
29
50
  execute?: any;
51
+ /**
52
+ * Builds the project-specific part of the tool (`inputSchema`, `annotations`,
53
+ * `execute`, ...) for a project, as the built-in tools are built. The static
54
+ * `name`, `title` and `description` are used unless `bind` returns its own,
55
+ * so the tool's name is known without a project. Called whenever the tool
56
+ * list is built (every `/mcp` request and every research plan): keep it
57
+ * cheap, and load libraries and other slow state once, outside it.
58
+ */
59
+ bind?: (project: UAGProjectInterface) => Partial<ToolInfo> | Promise<Partial<ToolInfo>>;
30
60
  };
61
+ /** An entry of `config.tools`: a tool, optionally bound to the project with `bind`. */
62
+ export type ToolEntry = ToolInfo;
63
+ /** The shape a `config.tools` entry takes, shown when one is a bare function. */
64
+ export declare const BOUND_TOOL_SHAPE = "{ name: 'my_tool', title: 'My Tool', description: '...', bind(project) { return { inputSchema, annotations, execute }; } }";
65
+ /**
66
+ * Refuse `config.tools` entries that are bare functions (the unreleased factory
67
+ * form): a tool is an object, whose `bind(project)` builds its project-specific
68
+ * part. Throws naming the entry and showing the shape.
69
+ */
70
+ export declare function assertToolEntries(entries: unknown[] | undefined): void;
31
71
  export type ParentInfo = {
32
72
  type: string;
33
73
  label?: string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.escapeSearchPattern = void 0;
3
+ exports.BOUND_TOOL_SHAPE = exports.escapeSearchPattern = void 0;
4
+ exports.assertToolEntries = assertToolEntries;
4
5
  const lodash_1 = require("lodash");
5
6
  /**
6
7
  * Escape a literal value so it can be carried in a `__regex` query parameter.
@@ -23,3 +24,17 @@ const lodash_1 = require("lodash");
23
24
  */
24
25
  const escapeSearchPattern = (value) => (0, lodash_1.escapeRegExp)(value).replace(/\//g, '\\x2f');
25
26
  exports.escapeSearchPattern = escapeSearchPattern;
27
+ /** The shape a `config.tools` entry takes, shown when one is a bare function. */
28
+ exports.BOUND_TOOL_SHAPE = "{ name: 'my_tool', title: 'My Tool', description: '...', bind(project) { return { inputSchema, annotations, execute }; } }";
29
+ /**
30
+ * Refuse `config.tools` entries that are bare functions (the unreleased factory
31
+ * form): a tool is an object, whose `bind(project)` builds its project-specific
32
+ * part. Throws naming the entry and showing the shape.
33
+ */
34
+ function assertToolEntries(entries) {
35
+ (entries || []).forEach((entry, index) => {
36
+ if (typeof entry === 'function') {
37
+ throw new Error(`config.tools[${index}] is a function. A custom tool is an object; build its project-specific part with bind(project): ${exports.BOUND_TOOL_SHAPE}`);
38
+ }
39
+ });
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formio/uag",
3
- "version": "2.0.0-rc.5",
3
+ "version": "2.0.0-rc.7",
4
4
  "mcpName": "io.form/formio-uag",
5
5
  "description": "The Form.io Universal Agent Gateway (UAG).",
6
6
  "main": "lib/index.js",
@@ -10,7 +10,9 @@
10
10
  },
11
11
  "files": [
12
12
  "lib",
13
- "bin"
13
+ "bin",
14
+ "testing.js",
15
+ "testing.d.ts"
14
16
  ],
15
17
  "workspaces": [
16
18
  "engines/*",
@@ -43,9 +45,9 @@
43
45
  "author": "Form.io Developers",
44
46
  "license": "MIT",
45
47
  "dependencies": {
46
- "@formio/appserver": "^2.25.2",
48
+ "@formio/appserver": "^2.26.0",
47
49
  "@formio/core": "2.9.0",
48
- "@formio/uag-sdk": "^2.0.0-rc.5",
50
+ "@formio/uag-sdk": "^2.0.0-rc.7",
49
51
  "@modelcontextprotocol/sdk": "1.32.2",
50
52
  "cors": "^2.8.6",
51
53
  "debug": "^4.4.3",
@@ -53,9 +55,13 @@
53
55
  "express": "^5.3.0",
54
56
  "jsonwebtoken": "^9.0.3",
55
57
  "lodash": "^4.18.1",
58
+ "markdown-it": "^15.0.3",
56
59
  "mongodb": "^7.7.0",
57
60
  "node-cache": "^5.1.2",
58
61
  "node-html-markdown": "^2.0.0",
62
+ "node-html-parser": "^9.0.5",
63
+ "pdf-lib": "^1.17.1",
64
+ "unpdf": "^2.1.0",
59
65
  "zod": "^4.6.7"
60
66
  },
61
67
  "resolutions": {
@@ -72,7 +78,6 @@
72
78
  "@types/chai": "^5.2.3",
73
79
  "@types/cors": "^2.8.19",
74
80
  "@types/debug": "^4.1.13",
75
- "@types/dotenv": "^8.2.3",
76
81
  "@types/express": "^5.0.6",
77
82
  "@types/jsdom": "^30.0.0",
78
83
  "@types/jsonwebtoken": "^9.0.10",
package/testing.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/testing';
package/testing.js ADDED
@@ -0,0 +1,2 @@
1
+ // @formio/uag/testing: test a module without a Form.io project or a running UAG.
2
+ module.exports = require('./lib/testing');