pull/636/head
Pete Matsyburka 4 weeks ago
parent c95a8616ac
commit 888f1ec6df

@ -4,6 +4,7 @@ module Mcp
module HandleRequest module HandleRequest
TOOLS = [ TOOLS = [
Mcp::Tools::SearchTemplates, Mcp::Tools::SearchTemplates,
Mcp::Tools::LoadTemplate,
Mcp::Tools::CreateTemplate, Mcp::Tools::CreateTemplate,
Mcp::Tools::SendDocuments, Mcp::Tools::SendDocuments,
Mcp::Tools::SearchDocuments Mcp::Tools::SearchDocuments

@ -6,27 +6,22 @@ module Mcp
SCHEMA = { SCHEMA = {
name: 'create_template', name: 'create_template',
title: '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: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
url: { name: {
type: 'string',
description: 'URL of the document file to upload'
},
file: {
type: 'string',
description: 'Base64-encoded file content'
},
filename: {
type: 'string', 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', 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: { annotations: {
readOnlyHint: false, readOnlyHint: false,
@ -44,48 +39,44 @@ module Mcp
account = current_user.account account = current_user.account
if arguments['file'].present? template = Template.new(
tempfile = Tempfile.new account:,
tempfile.binmode author: current_user,
tempfile.write(Base64.decode64(arguments['file'])) folder: account.default_template_folder,
tempfile.rewind name: arguments['name'].to_s.presence || 'New Template',
fields: [],
schema: []
)
filename = arguments['filename'] || 'document.pdf' if arguments['url'].present?
elsif arguments['url'].present?
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
tempfile.write(DownloadUtils.call(arguments['url'], validate: true).body) tempfile.write(DownloadUtils.call(arguments['url'], validate: true).body)
tempfile.rewind tempfile.rewind
filename = File.basename(URI.decode_www_form_component(arguments['url'])) 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( file = ActionDispatch::Http::UploadedFile.new(
tempfile:, tempfile:,
filename:, filename:,
type: Marcel::MimeType.for(tempfile) type: Marcel::MimeType.for(tempfile)
) )
template = Template.new( template.name = arguments['name'].presence || File.basename(filename, '.*')
account:, template.save!
author: current_user,
folder: account.default_template_folder,
name: arguments['name'].presence || File.basename(filename, '.*')
)
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) if template.fields.blank?
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents)
end
if template.fields.blank? template.update!(schema:)
template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents) else
template.save!
end end
template.update!(schema:)
WebhookUrls.enqueue_events(template, 'template.created') WebhookUrls.enqueue_events(template, 'template.created')
SearchEntries.enqueue_reindex(template) SearchEntries.enqueue_reindex(template)
@ -104,7 +95,7 @@ module Mcp
] ]
} }
end end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end end
end end

@ -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

@ -31,6 +31,27 @@ module Mcp
phone: { phone: {
type: 'string', type: 'string',
description: 'Submitter phone number in E.164 format' 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? return { content: [{ type: 'text', text: 'Template has no fields' }], isError: true } if template.fields.blank?
submitters = (arguments['submitters'] || []).map do |s| submitters = (arguments['submitters'] || []).map do |s|
s.slice('email', 'name', 'role', 'phone') attrs = s.slice('email', 'name', 'role', 'phone').compact_blank
.compact_blank
.with_indifferent_access 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 end
submissions = Submissions.create_from_submitters( submissions = Submissions.create_from_submitters(

Loading…
Cancel
Save