diff --git a/lib/mcp/handle_request.rb b/lib/mcp/handle_request.rb index 7149fd06..83d6725f 100644 --- a/lib/mcp/handle_request.rb +++ b/lib/mcp/handle_request.rb @@ -4,6 +4,7 @@ module Mcp module HandleRequest TOOLS = [ Mcp::Tools::SearchTemplates, + Mcp::Tools::LoadTemplate, Mcp::Tools::CreateTemplate, Mcp::Tools::SendDocuments, Mcp::Tools::SearchDocuments diff --git a/lib/mcp/tools/create_template.rb b/lib/mcp/tools/create_template.rb index 0a945264..e45ee66e 100644 --- a/lib/mcp/tools/create_template.rb +++ b/lib/mcp/tools/create_template.rb @@ -6,27 +6,22 @@ module Mcp SCHEMA = { name: 'create_template', title: 'Create Template', - description: 'Create a template from a PDF. Provide a URL or base64-encoded file content.', + description: 'Create a document template. Provide a URL to upload a PDF/DOCX file, or provide only a name ' \ + 'to create an empty template and receive an edit URL where the file can be uploaded via the UI.', inputSchema: { type: 'object', properties: { - url: { - type: 'string', - description: 'URL of the document file to upload' - }, - file: { - type: 'string', - description: 'Base64-encoded file content' - }, - filename: { + name: { type: 'string', - description: 'Filename with extension (required when using file)' + description: 'Template name (used as the template name and required when url is not provided)' }, - name: { + url: { type: 'string', - description: 'Template name (defaults to filename)' + description: 'Optional URL of a PDF or DOCX file to upload. If omitted, an empty template is ' \ + 'created and the returned edit_url can be used to upload a file via the UI.' } - } + }, + required: %w[name] }, annotations: { readOnlyHint: false, @@ -44,48 +39,44 @@ module Mcp account = current_user.account - if arguments['file'].present? - tempfile = Tempfile.new - tempfile.binmode - tempfile.write(Base64.decode64(arguments['file'])) - tempfile.rewind + template = Template.new( + account:, + author: current_user, + folder: account.default_template_folder, + name: arguments['name'].to_s.presence || 'New Template', + fields: [], + schema: [] + ) - filename = arguments['filename'] || 'document.pdf' - elsif arguments['url'].present? + if arguments['url'].present? tempfile = Tempfile.new tempfile.binmode tempfile.write(DownloadUtils.call(arguments['url'], validate: true).body) tempfile.rewind filename = File.basename(URI.decode_www_form_component(arguments['url'])) - else - return { content: [{ type: 'text', text: 'Provide either url or file' }], isError: true } - end - file = ActionDispatch::Http::UploadedFile.new( - tempfile:, - filename:, - type: Marcel::MimeType.for(tempfile) - ) + file = ActionDispatch::Http::UploadedFile.new( + tempfile:, + filename:, + type: Marcel::MimeType.for(tempfile) + ) - template = Template.new( - account:, - author: current_user, - folder: account.default_template_folder, - name: arguments['name'].presence || File.basename(filename, '.*') - ) + template.name = arguments['name'].presence || File.basename(filename, '.*') + template.save! - template.save! + documents, = Templates::CreateAttachments.call(template, { files: [file] }, extract_fields: true) + schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } - documents, = Templates::CreateAttachments.call(template, { files: [file] }, extract_fields: true) - schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } + if template.fields.blank? + template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents) + end - if template.fields.blank? - template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents) + template.update!(schema:) + else + template.save! end - template.update!(schema:) - WebhookUrls.enqueue_events(template, 'template.created') SearchEntries.enqueue_reindex(template) @@ -104,7 +95,7 @@ module Mcp ] } end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end end diff --git a/lib/mcp/tools/load_template.rb b/lib/mcp/tools/load_template.rb new file mode 100644 index 00000000..7cfcc3ff --- /dev/null +++ b/lib/mcp/tools/load_template.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Mcp + module Tools + module LoadTemplate + SCHEMA = { + name: 'load_template', + title: 'Load Template', + description: 'Load a template with its fields. Each field includes name, type, and the signing role name.', + inputSchema: { + type: 'object', + properties: { + template_id: { + type: 'integer', + description: 'Template identifier' + } + }, + required: %w[template_id] + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } + }.freeze + + module_function + + def call(arguments, _current_user, current_ability) + template = Template.accessible_by(current_ability).find_by(id: arguments['template_id']) + + return { content: [{ type: 'text', text: 'Template not found' }], isError: true } unless template + + current_ability.authorize!(:read, template) + + submitters_index = template.submitters.index_by { |s| s['uuid'] } + + roles = template.submitters.pluck('name') + + fields = template.fields.filter_map do |field| + next if field['name'].blank? + + { + name: field['name'], + type: field['type'], + role: submitters_index[field['submitter_uuid']]&.dig('name') + } + end + + { + content: [ + { + type: 'text', + text: { + id: template.id, + name: template.name, + roles: roles, + fields: fields + }.to_json + } + ] + } + end + end + end +end diff --git a/lib/mcp/tools/send_documents.rb b/lib/mcp/tools/send_documents.rb index 25b1be72..461728e6 100644 --- a/lib/mcp/tools/send_documents.rb +++ b/lib/mcp/tools/send_documents.rb @@ -31,6 +31,27 @@ module Mcp phone: { type: 'string', description: 'Submitter phone number in E.164 format' + }, + role: { + type: 'string', + description: 'Signing role name from the template' + }, + fields: { + type: 'array', + description: 'Prefill field values for this submitter (fields become readonly)', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Field name' + }, + value: { + description: 'Prefilled value for the field' + } + }, + required: %w[name value] + } } } } @@ -59,9 +80,17 @@ module Mcp return { content: [{ type: 'text', text: 'Template has no fields' }], isError: true } if template.fields.blank? submitters = (arguments['submitters'] || []).map do |s| - s.slice('email', 'name', 'role', 'phone') - .compact_blank - .with_indifferent_access + attrs = s.slice('email', 'name', 'role', 'phone').compact_blank + + fields = Array.wrap(s['fields']).filter_map do |f| + next if f['name'].blank? + + { 'name' => f['name'], 'default_value' => f['value'], 'readonly' => true } + end + + attrs['fields'] = fields if fields.present? + + attrs.with_indifferent_access end submissions = Submissions.create_from_submitters(