@formio/uag 2.0.0-rc.6 → 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 -4
  74. package/testing.d.ts +1 -0
  75. package/testing.js +2 -0
@@ -0,0 +1,68 @@
1
+ import type { Request } from 'express';
2
+ import type { ModuleApi, ModuleRequest, ModuleRoute } from '@formio/appserver';
3
+ import type { Converter, ConverterOutput, Representation } from '../documents/types';
4
+ import type { FileRoute, Pins, AcceptEntry } from '../documents/routes';
5
+ /** A module route's request in the UAG: also its request id. */
6
+ export type UAGModuleRequest = ModuleRequest & {
7
+ requestId: string;
8
+ };
9
+ /** The names of the UAG's own rate-limited operations, which module routes may not take. */
10
+ export declare const UAG_OPERATIONS: string[];
11
+ /** An agent engine a module may use: its name and model, and generate, which checks the license first. */
12
+ export type GeneratingEngine = {
13
+ name: string;
14
+ model: string;
15
+ accepts: Representation[];
16
+ generate(request: any): Promise<any>;
17
+ };
18
+ /**
19
+ * The module API the UAG gives a module (version 1): appserver's, with the
20
+ * license check, engines, converters and the UAG's helpers. The project and
21
+ * the engines themselves are never handed out.
22
+ */
23
+ export type UAGModuleApi<Settings extends Record<string, any> = Record<string, any>> = ModuleApi<Settings> & {
24
+ /** Throws 403 license_required unless Agentic Formflow is allowed. */
25
+ requireLicense(): Promise<void>;
26
+ converters: {
27
+ list(): Converter[];
28
+ identify(buffer: Buffer, hint: {
29
+ name: string;
30
+ type?: string;
31
+ }): string;
32
+ route(type: string, inputs: Representation[][], pins?: Pins): FileRoute | null;
33
+ accept(inputs: Representation[][]): AcceptEntry[];
34
+ run(steps: Converter[], input: {
35
+ buffer: Buffer;
36
+ type: string;
37
+ name: string;
38
+ }, to: Representation, options: {
39
+ signal: AbortSignal;
40
+ }): Promise<ConverterOutput>;
41
+ stepName(converter: {
42
+ name: string;
43
+ version?: string;
44
+ }): string;
45
+ };
46
+ engines: {
47
+ /**
48
+ * The named engine, or the first agent engine that generates and reads every
49
+ * representation in `needs`; throws 503 no_engine. Its `generate` checks the
50
+ * license first, so no module calls a model outside it.
51
+ */
52
+ select(options: {
53
+ name?: string;
54
+ needs?: Representation[];
55
+ }): GeneratingEngine;
56
+ };
57
+ markdown: {
58
+ sanitize(text: string): string;
59
+ fromHtml(html: string): string;
60
+ };
61
+ pdf: {
62
+ pageCount(pdf: Buffer): Promise<number | undefined>;
63
+ };
64
+ /** Who the request is from, for limits: the user or token, or, for an anonymous caller, `anonymous:<ip>`. */
65
+ callerKey(req: Request): string;
66
+ };
67
+ /** A form route of a UAG module. */
68
+ export type UAGModuleRoute = ModuleRoute<UAGModuleApi>;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UAG_OPERATIONS = void 0;
4
+ /** The names of the UAG's own rate-limited operations, which module routes may not take. */
5
+ exports.UAG_OPERATIONS = ['extract', 'decide', 'generate', 'chat', 'capabilities'];
@@ -0,0 +1,16 @@
1
+ import type { UAGServer } from './index';
2
+ /** Where the curated image keeps its plugins file. */
3
+ export declare const DEFAULT_PLUGINS_FILE = "/app/plugins/index.mjs";
4
+ /**
5
+ * The plugins file to load: UAG_PLUGINS (a path, or "off"), or else the
6
+ * image's default when it exists. Undefined when there is none.
7
+ */
8
+ export declare function pluginsFile(env?: Record<string, string | undefined>, defaultFile?: string): string | undefined;
9
+ /**
10
+ * Load the plugins file, if any, and use it, named by its path in error
11
+ * messages. Its default export is a plugin or a list of plugins; or a module
12
+ * (an extra, with its plugins), or a list mixing modules and
13
+ * plugins, each module used in turn. Plugins are trusted code: they run in
14
+ * the UAG process.
15
+ */
16
+ export declare function usePluginFiles(server: Pick<UAGServer, 'use'>, env?: Record<string, string | undefined>, defaultFile?: string): Promise<string | undefined>;
@@ -0,0 +1,62 @@
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.DEFAULT_PLUGINS_FILE = void 0;
7
+ exports.pluginsFile = pluginsFile;
8
+ exports.usePluginFiles = usePluginFiles;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const url_1 = require("url");
12
+ const appserver_1 = require("@formio/appserver");
13
+ /** Where the curated image keeps its plugins file. */
14
+ exports.DEFAULT_PLUGINS_FILE = '/app/plugins/index.mjs';
15
+ /**
16
+ * The plugins file to load: UAG_PLUGINS (a path, or "off"), or else the
17
+ * image's default when it exists. Undefined when there is none.
18
+ */
19
+ function pluginsFile(env = process.env, defaultFile = exports.DEFAULT_PLUGINS_FILE) {
20
+ const setting = String(env.UAG_PLUGINS || '').trim();
21
+ if (setting.toLowerCase() === 'off')
22
+ return undefined;
23
+ if (setting)
24
+ return path_1.default.resolve(setting);
25
+ return fs_1.default.existsSync(defaultFile) ? defaultFile : undefined;
26
+ }
27
+ /** A real dynamic import(), kept one after compilation to CommonJS. */
28
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
29
+ /**
30
+ * The keys only a module has: an entry with one of them (and no plugin `type`)
31
+ * is a module, anything else a plugin. Plugins have a `name` and `settings`
32
+ * too, so a name says nothing, and `settings` counts only as a function.
33
+ * (`uag` too, so a module written for the old shape is refused saying what to
34
+ * change, not read as a plugin.)
35
+ */
36
+ const MODULE_KEYS = ['uag', 'routes', 'components', 'initialize', 'plugins', 'pluginTypes', 'config', 'formRoutes', 'actions', 'hooks', 'providers', 'processors'];
37
+ const isModule = (entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.type !== 'string'
38
+ && (MODULE_KEYS.some((key) => key in entry) || typeof entry.settings === 'function');
39
+ /**
40
+ * Load the plugins file, if any, and use it, named by its path in error
41
+ * messages. Its default export is a plugin or a list of plugins; or a module
42
+ * (an extra, with its plugins), or a list mixing modules and
43
+ * plugins, each module used in turn. Plugins are trusted code: they run in
44
+ * the UAG process.
45
+ */
46
+ async function usePluginFiles(server, env = process.env, defaultFile = exports.DEFAULT_PLUGINS_FILE) {
47
+ const file = pluginsFile(env, defaultFile);
48
+ if (!file)
49
+ return undefined;
50
+ const exported = (await dynamicImport((0, url_1.pathToFileURL)(file).href))?.default;
51
+ const entries = [].concat(exported ?? []);
52
+ if (entries.some(isModule)) {
53
+ const plugins = entries.filter((entry) => !isModule(entry));
54
+ server.use([...entries.filter(isModule), ...(plugins.length ? [{ plugins }] : [])], file);
55
+ console.log(`[formio-uag] Plugins and modules loaded from ${file}.`);
56
+ return file;
57
+ }
58
+ const plugins = await (0, appserver_1.loadPluginFile)(file);
59
+ server.use({ plugins }, file);
60
+ console.log(`[formio-uag] Plugins loaded from ${file}.`);
61
+ return file;
62
+ }
@@ -0,0 +1,17 @@
1
+ import type { PluginLog, PluginRegistry, PluginType } from '@formio/appserver';
2
+ import type { UAGProjectInterface } from './UAGProjectInterface';
3
+ /**
4
+ * Engines and MCP tools, listed in the plugin registry (phase 1). The UAG keeps
5
+ * loading them its own way (UAG_ENGINES, config.tools); the registry only lists
6
+ * them, so they appear in GET {project}/plugins. Settings values are never listed.
7
+ */
8
+ export declare const enginePluginType: PluginType;
9
+ export declare const toolPluginType: PluginType;
10
+ /** List each loaded engine type: its instances, kinds, what agent instances read, and whether they generate. */
11
+ export declare function listEngines(registry: PluginRegistry, project: UAGProjectInterface): void;
12
+ /**
13
+ * List every MCP tool the project serves (built-in, then config.tools). A name
14
+ * served twice is listed once, with a warning: listing never refuses what the
15
+ * tool loader allows.
16
+ */
17
+ export declare function listTools(registry: PluginRegistry, project: UAGProjectInterface, log?: PluginLog): Promise<void>;
package/lib/plugins.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolPluginType = exports.enginePluginType = void 0;
4
+ exports.listEngines = listEngines;
5
+ exports.listTools = listTools;
6
+ const uag_sdk_1 = require("@formio/uag-sdk");
7
+ const tools_1 = require("./agent/tools");
8
+ const engines_1 = require("./agent/engines");
9
+ const version_1 = require("./version");
10
+ /**
11
+ * Engines and MCP tools, listed in the plugin registry (phase 1). The UAG keeps
12
+ * loading them its own way (UAG_ENGINES, config.tools); the registry only lists
13
+ * them, so they appear in GET {project}/plugins. Settings values are never listed.
14
+ */
15
+ exports.enginePluginType = {
16
+ type: 'engine',
17
+ title: 'Engine',
18
+ apiVersions: [1],
19
+ editForm: 'none',
20
+ // A misconfigured engine stops startup.
21
+ unavailable: 'error',
22
+ registration: 'internal',
23
+ stability: 'stable',
24
+ validate: (plugin) => uag_sdk_1.ENGINE_NAME.test(plugin.name) ? [] : [`engine name "${plugin.name}" must be lowercase letters, digits and "-", starting with a letter`],
25
+ manifest: (plugin) => ({
26
+ kinds: plugin.kinds,
27
+ accepts: plugin.accepts,
28
+ generate: plugin.generate,
29
+ instances: plugin.instances
30
+ })
31
+ };
32
+ exports.toolPluginType = {
33
+ type: 'tool',
34
+ title: 'MCP tool',
35
+ apiVersions: [1],
36
+ editForm: 'none',
37
+ unavailable: 'error',
38
+ registration: 'internal',
39
+ stability: 'stable',
40
+ validate: (plugin) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(plugin.name) ? [] : [`tool name "${plugin.name}" must start with a letter and hold only letters, digits, "_" and "-"`],
41
+ manifest: (plugin) => ({ annotations: plugin.annotations ?? null })
42
+ };
43
+ const semver = (version) => (version && /^\d+\.\d+\.\d+/.test(version)) ? version : version_1.UAG_VERSION;
44
+ /** List each loaded engine type: its instances, kinds, what agent instances read, and whether they generate. */
45
+ function listEngines(registry, project) {
46
+ const engines = project.formflowEngines();
47
+ const entries = (0, engines_1.engineTypesOf)(engines).map((type) => {
48
+ const instances = type.instances.map((name) => engines.get(name)).filter(Boolean);
49
+ const agents = instances.filter((engine) => engine.kind === 'agent');
50
+ const accepts = [...new Set(agents.flatMap((engine) => Array.isArray(engine.accepts) ? engine.accepts : ['text']))];
51
+ return {
52
+ type: 'engine',
53
+ name: type.name,
54
+ title: type.title,
55
+ version: semver(type.version),
56
+ apiVersion: 1,
57
+ kinds: type.kinds,
58
+ accepts,
59
+ generate: agents.some((engine) => typeof engine.generate === 'function'),
60
+ instances: type.instances
61
+ };
62
+ });
63
+ registry.addType(exports.enginePluginType, '@formio/uag');
64
+ registry.add(entries, 'UAG_ENGINES', { server: true });
65
+ }
66
+ /**
67
+ * List every MCP tool the project serves (built-in, then config.tools). A name
68
+ * served twice is listed once, with a warning: listing never refuses what the
69
+ * tool loader allows.
70
+ */
71
+ async function listTools(registry, project, log = console) {
72
+ const seen = new Set();
73
+ const entries = [];
74
+ for (const tool of await (0, tools_1.allTools)(project)) {
75
+ const name = String(tool.name);
76
+ if (seen.has(name)) {
77
+ log.warn(`[plugins] tool:${name} is defined more than once; it is listed once`);
78
+ continue;
79
+ }
80
+ seen.add(name);
81
+ entries.push({
82
+ type: 'tool',
83
+ name,
84
+ title: tool.title || name,
85
+ description: tool.description,
86
+ version: version_1.UAG_VERSION,
87
+ apiVersion: 1,
88
+ annotations: tool.annotations
89
+ });
90
+ }
91
+ registry.addType(exports.toolPluginType, '@formio/uag');
92
+ registry.add(entries, 'config.tools', { server: true });
93
+ }
@@ -0,0 +1,20 @@
1
+ import type { Component } from '@formio/core';
2
+ /**
3
+ * HTML for rich text fields (textareas with the CKEditor or Quill editor).
4
+ *
5
+ * What an agent engine writes into such a field is reduced to the markup the
6
+ * editors keep, because the engine's input can be steered (a document it reads
7
+ * is someone else's text), and the field is later rendered for reviewers. The
8
+ * HTML is parsed and written out again: allowed elements with their allowed
9
+ * attributes, text escaped, everything else dropped. An element that is not
10
+ * allowed is dropped and its children kept, except for elements whose content
11
+ * is never text to show (scripts, styles, frames, embedded documents).
12
+ */
13
+ /** Allowed elements, each with the attributes it may keep. */
14
+ export declare const RICH_TEXT_TAGS: Record<string, string[]>;
15
+ /** Whether a component is a rich text field, whose value is HTML. */
16
+ export declare function isRichText(component: Component | undefined): boolean;
17
+ /** Reduce HTML to the markup rich text editors keep (RICH_TEXT_TAGS). */
18
+ export declare function sanitizeRichText(html: string): string;
19
+ /** The rule agents are given for a rich text field's value. */
20
+ export declare const RICH_TEXT_RULE: string;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RICH_TEXT_RULE = exports.RICH_TEXT_TAGS = void 0;
4
+ exports.isRichText = isRichText;
5
+ exports.sanitizeRichText = sanitizeRichText;
6
+ const node_html_parser_1 = require("node-html-parser");
7
+ /**
8
+ * HTML for rich text fields (textareas with the CKEditor or Quill editor).
9
+ *
10
+ * What an agent engine writes into such a field is reduced to the markup the
11
+ * editors keep, because the engine's input can be steered (a document it reads
12
+ * is someone else's text), and the field is later rendered for reviewers. The
13
+ * HTML is parsed and written out again: allowed elements with their allowed
14
+ * attributes, text escaped, everything else dropped. An element that is not
15
+ * allowed is dropped and its children kept, except for elements whose content
16
+ * is never text to show (scripts, styles, frames, embedded documents).
17
+ */
18
+ /** Allowed elements, each with the attributes it may keep. */
19
+ exports.RICH_TEXT_TAGS = {
20
+ p: [], br: [], hr: [],
21
+ strong: [], b: [], em: [], i: [], u: [], s: [], sub: [], sup: [], code: [], pre: [],
22
+ h1: [], h2: [], h3: [], h4: [], h5: [], h6: [],
23
+ ul: [], ol: [], li: [],
24
+ blockquote: [],
25
+ a: ['href'],
26
+ figure: [], table: [], thead: [], tbody: [], tfoot: [], tr: [],
27
+ th: ['colspan', 'rowspan'], td: ['colspan', 'rowspan']
28
+ };
29
+ /** Elements dropped with everything inside them. */
30
+ const DROPPED_WITH_CONTENT = new Set(['script', 'style', 'noscript', 'template', 'iframe', 'frame', 'frameset', 'object', 'embed', 'svg', 'math', 'head', 'title', 'textarea', 'select']);
31
+ const VOID = new Set(['br', 'hr']);
32
+ /** Whether a component is a rich text field, whose value is HTML. */
33
+ function isRichText(component) {
34
+ const editor = component?.editor;
35
+ return component?.type === 'textarea' && (editor === 'ckeditor' || editor === 'quill');
36
+ }
37
+ function escapeText(text) {
38
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
39
+ }
40
+ function escapeAttribute(value) {
41
+ return escapeText(value).replace(/"/g, '&quot;');
42
+ }
43
+ /** Only links to web pages and mail addresses: never `javascript:`, `data:` or the like. */
44
+ function safeHref(value) {
45
+ const href = String(value || '').replace(/[\u0000-\u001F\u007F\s]+/g, '');
46
+ return /^(https?:\/\/|mailto:)/i.test(href) ? String(value).trim() : null;
47
+ }
48
+ function attributesOf(element, allowed) {
49
+ let out = '';
50
+ for (const name of allowed) {
51
+ const raw = element.getAttribute(name);
52
+ if (raw === undefined || raw === null)
53
+ continue;
54
+ let value = raw;
55
+ if (name === 'href')
56
+ value = safeHref(raw);
57
+ if ((name === 'colspan' || name === 'rowspan'))
58
+ value = /^\d{1,3}$/.test(raw.trim()) ? raw.trim() : null;
59
+ if (value === null)
60
+ continue;
61
+ out += ` ${name}="${escapeAttribute(value)}"`;
62
+ }
63
+ return out;
64
+ }
65
+ function write(node) {
66
+ if (node.nodeType === node_html_parser_1.NodeType.TEXT_NODE) {
67
+ // `text` is the decoded text; it is escaped once, here.
68
+ return escapeText(node.text);
69
+ }
70
+ if (node.nodeType !== node_html_parser_1.NodeType.ELEMENT_NODE)
71
+ return '';
72
+ const element = node;
73
+ const children = () => element.childNodes.map(write).join('');
74
+ const tag = String(element.rawTagName || '').toLowerCase();
75
+ if (!tag)
76
+ return children();
77
+ if (DROPPED_WITH_CONTENT.has(tag))
78
+ return '';
79
+ const allowed = exports.RICH_TEXT_TAGS[tag];
80
+ if (!allowed)
81
+ return children();
82
+ const open = `<${tag}${attributesOf(element, allowed)}>`;
83
+ return VOID.has(tag) ? open : `${open}${children()}</${tag}>`;
84
+ }
85
+ /** Reduce HTML to the markup rich text editors keep (RICH_TEXT_TAGS). */
86
+ function sanitizeRichText(html) {
87
+ const root = (0, node_html_parser_1.parse)(String(html ?? ''), {
88
+ comment: false,
89
+ lowerCaseTagName: true,
90
+ // Read as raw text, so their content is never parsed as markup; they are dropped.
91
+ blockTextElements: { script: true, style: true, noscript: true }
92
+ });
93
+ return root.childNodes.map(write).join('').trim();
94
+ }
95
+ /** The rule agents are given for a rich text field's value. */
96
+ exports.RICH_TEXT_RULE = 'The value is HTML for a rich text editor. Use only these elements: ' +
97
+ Object.keys(exports.RICH_TEXT_TAGS).join(', ') +
98
+ ', with no attributes except href on a (http, https or mailto links only) and colspan and rowspan on td and th. ' +
99
+ 'No other elements, styles, classes, images or scripts, which are removed. Write HTML, not Markdown, and do not wrap it in a code fence.';
package/lib/router.d.ts CHANGED
@@ -1,3 +1,12 @@
1
1
  import { Router } from 'express';
2
2
  import { UAGProjectInterface } from './UAGProjectInterface';
3
+ import type { ScopedTokenClaims } from './formflow';
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ /**
6
+ * Hold a scoped token to its tools whatever built the server: a project
7
+ * interface a module replaces may override buildMcpServer() and register tools
8
+ * without the scope it was given. Every tool the token does not list, and every
9
+ * tool not annotated read-only, is disabled, so it is neither listed nor run.
10
+ */
11
+ export declare function restrictToScope(server: McpServer, scope: Pick<ScopedTokenClaims, 'tools'>): void;
3
12
  export declare function UAGRouter(project: UAGProjectInterface): Router;
package/lib/router.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.restrictToScope = restrictToScope;
3
4
  exports.UAGRouter = UAGRouter;
4
5
  const express_1 = require("express");
5
6
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -30,6 +31,22 @@ const handleResponseError = (error, res) => {
30
31
  res.status(500).json(createJsonRpcErrorResponse(-32603, "Internal Server Error"));
31
32
  return false;
32
33
  };
34
+ /**
35
+ * Hold a scoped token to its tools whatever built the server: a project
36
+ * interface a module replaces may override buildMcpServer() and register tools
37
+ * without the scope it was given. Every tool the token does not list, and every
38
+ * tool not annotated read-only, is disabled, so it is neither listed nor run.
39
+ */
40
+ function restrictToScope(server, scope) {
41
+ const allowed = new Set(scope.tools || []);
42
+ // The SDK keeps registered tools here; it has no public list of them.
43
+ const registered = server._registeredTools || {};
44
+ for (const [name, tool] of Object.entries(registered)) {
45
+ if (!allowed.has(name) || tool?.annotations?.readOnlyHint !== true) {
46
+ tool.disable?.();
47
+ }
48
+ }
49
+ }
33
50
  /**
34
51
  * Build a per-request McpServer + transport (stateless mode) and tear both down
35
52
  * when the response closes. A fresh instance per request keeps concurrent requests
@@ -37,6 +54,8 @@ const handleResponseError = (error, res) => {
37
54
  */
38
55
  const connectMcpTransport = async (project, res, scope) => {
39
56
  const server = await project.buildMcpServer(scope);
57
+ if (scope)
58
+ restrictToScope(server, scope);
40
59
  const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
41
60
  sessionIdGenerator: undefined, // Use "undefined" to trigger stateless mode.
42
61
  });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @formio/uag/testing: test a module without a Form.io project or a running UAG.
3
+ *
4
+ * const { createTestModuleApi, testForm, testAuth, mountModuleRoutes } = require('@formio/uag/testing');
5
+ *
6
+ * const api = await createTestModuleApi(MyModule, { config: { myModule: { max: 2 } } });
7
+ * const app = await mountModuleRoutes(MyModule, { form: testForm(definition), auth: testAuth() });
8
+ * await supertest(app).post('/notes/my-module/check').send({ ... });
9
+ *
10
+ * The API is the UAG's own (errors, stream, forms, converters, engine handles,
11
+ * Markdown, callerKey), with stand-ins only for what needs a project: the
12
+ * caller's access (from `testAuth`), the license (`licensed`), engines
13
+ * (`engines`) and stored files (`testForm` reads base64 values; `files` for
14
+ * others). Settings go through the module's own `settings`, with the server's rules.
15
+ */
16
+ import express from 'express';
17
+ import type { AuthRequest, ModuleForm, ServerModule } from '@formio/appserver';
18
+ import type { UAGModuleApi } from './modules/types';
19
+ /** An engine for tests: an agent engine that generates. */
20
+ export type TestEngine = {
21
+ name: string;
22
+ model?: string;
23
+ accepts?: Array<'text' | 'pdf'>;
24
+ generate(request: any): any;
25
+ kind?: 'agent';
26
+ };
27
+ export type TestModuleOptions = {
28
+ /** The settings as given, instead of running the module's `settings`. */
29
+ settings?: Record<string, any>;
30
+ /** The configuration and environment the module's `settings` gets ({} by default). */
31
+ config?: Record<string, any>;
32
+ env?: Record<string, string | undefined>;
33
+ /** Plugins to register beside the module's own (document plugins, converters). */
34
+ plugins?: any[];
35
+ /** The engines `api.engines.select()` chooses from. */
36
+ engines?: TestEngine[];
37
+ /** Whether Agentic Formflow is licensed (true): false makes `requireLicense` and engine handles refuse with 403. */
38
+ licensed?: boolean;
39
+ /** Reads a stored file value that is not base64 (the test form reads those). */
40
+ files?: (value: any, options: {
41
+ component?: any;
42
+ maxBytes?: number;
43
+ }) => Buffer | null | Promise<Buffer | null>;
44
+ };
45
+ /** A form for tests, as module routes get it: `definition` is the form's JSON. */
46
+ export declare function testForm(definition: Record<string, any>, options?: {
47
+ id?: string;
48
+ files?: TestModuleOptions['files'];
49
+ }): ModuleForm & {
50
+ readFile(value: any, auth: any, options?: any): Promise<Buffer | null>;
51
+ };
52
+ /**
53
+ * A caller for tests: a user with these permissions on every form (create and
54
+ * update by default), as a list (`['read']`) or as `formPermissions()` gives
55
+ * them (`{ read: true }`).
56
+ */
57
+ export declare function testAuth(options?: {
58
+ user?: Record<string, any>;
59
+ permissions?: Array<'create' | 'read' | 'update' | 'delete'> | Partial<Record<'create' | 'read' | 'update' | 'delete', boolean>>;
60
+ admin?: boolean;
61
+ }): AuthRequest;
62
+ /** The UAG's module API for a module, in a test. */
63
+ export declare function createTestModuleApi<Settings extends Record<string, any> = Record<string, any>>(module: ServerModule<any>, options?: TestModuleOptions): Promise<UAGModuleApi<Settings>>;
64
+ /**
65
+ * The module's form routes on an Express app, as the UAG mounts them on a
66
+ * form (by its path, and by `form/<id>`), for supertest: with the form, the
67
+ * caller, a request id, the access check and the UAG's error shape. No rate
68
+ * limits. `api` is created with `options` unless given.
69
+ */
70
+ export declare function mountModuleRoutes(module: ServerModule<any>, options: TestModuleOptions & {
71
+ form: ReturnType<typeof testForm>;
72
+ auth?: AuthRequest;
73
+ api?: UAGModuleApi;
74
+ }): Promise<express.Express>;