(function() { var REQUEST_TARGETS = ['path', 'query', 'headers', 'body']; var SCALAR_TYPES = ['string', 'number', 'integer', 'boolean', 'null']; var suspendSync = false; function field(id) { return document.getElementById(id); } function readText(id) { var element = field(id); return element ? element.value.trim() : ''; } function writeText(id, value) { var element = field(id); if (element) element.value = value || ''; } function parseText(id) { if (typeof parseStructuredText === 'function') { return parseStructuredText(readText(id)); } return JSON.parse(readText(id)); } function dumpYaml(object) { return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1, noRefs: true }) : JSON.stringify(object, null, 2); } function safeJson(id) { try { return parseText(id); } catch (_error) { return null; } } function scalarType(value) { if (value === null) return 'null'; if (Array.isArray(value)) return 'array'; var typeName = typeof value; if (typeName === 'number') return Number.isInteger(value) ? 'integer' : 'number'; if (typeName === 'boolean') return 'boolean'; if (typeName === 'object') return 'object'; return 'string'; } function inferJsonSchema(value) { var typeName = scalarType(value); if (typeName === 'object') { var properties = {}; var required = []; Object.keys(value || {}).forEach(function(name) { properties[name] = inferJsonSchema(value[name]); if (value[name] !== null && value[name] !== undefined) required.push(name); }); var schema = { type: 'object', properties: properties, additionalProperties: false, }; if (required.length) schema.required = required; return schema; } if (typeName === 'array') { return { type: 'array', items: value.length ? inferJsonSchema(value[0]) : { type: 'string' }, }; } return { type: typeName }; } function collectSchemaFields(schema, prefix) { var result = []; var node = schema || {}; if (node.type === 'object' && node.properties) { Object.keys(node.properties).forEach(function(name) { var child = node.properties[name] || {}; var path = prefix ? prefix + '.' + name : name; if (child.type === 'object' && child.properties) { result = result.concat(collectSchemaFields(child, path)); } else { result.push({ name: path, required: Array.isArray(node.required) && node.required.indexOf(name) >= 0, }); } }); } return result; } function requiredSchemaFields(schema) { return collectSchemaFields(schema, '').filter(function(item) { return item.required; }).map(function(item) { return item.name; }); } function collectJsonLeaves(value, prefix) { var result = []; if (Array.isArray(value)) { if (!value.length) { if (prefix) result.push({ name: prefix, required: true, array: true }); return result; } var samplePath = prefix ? prefix + '[0]' : '[0]'; return collectJsonLeaves(value[0], samplePath).map(function(item) { item.array = true; return item; }); } if (value && typeof value === 'object') { Object.keys(value).forEach(function(name) { var path = prefix ? prefix + '.' + name : name; var child = value[name]; if (child && typeof child === 'object') { result = result.concat(collectJsonLeaves(child, path)); } else { result.push({ name: path, required: child !== null && child !== undefined }); } }); return result; } if (prefix) result.push({ name: prefix, required: true }); return result; } function pathParams() { var path = readText('endpoint-path') || ''; var params = []; path.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, function(_match, name) { params.push(name); return _match; }); path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, function(_match, name) { params.push(name); return _match; }); return Array.from(new Set(params)); } function currentMethod() { var active = document.querySelector('.method-card.active'); return active ? active.dataset.method || 'GET' : 'GET'; } function defaultTargetKind() { var method = currentMethod().toUpperCase(); return method === 'GET' || method === 'DELETE' ? 'query' : 'body'; } function splitRequestTarget(target) { var value = String(target || '').replace(/^\$\.request\./, '').replace(/^request\./, ''); var parts = value.split('.'); var kind = parts.shift() || defaultTargetKind(); if (REQUEST_TARGETS.indexOf(kind) < 0) kind = defaultTargetKind(); return { kind: kind, name: parts.join('.') }; } function sourceToField(source) { return String(source || '') .replace(/^\$\.mcp\.?/, '') .replace(/^\$\.input\.?/, ''); } function responseSourceToPath(source) { return String(source || '') .replace(/^\$\.response\.body\.?/, '') .replace(/^\$\.response\.data\.?/, ''); } function outputTargetToField(target) { return String(target || '').replace(/^\$\.output\.?/, ''); } function mappingEntrySource(entry) { return entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.source : entry; } function mappingEntryDefault(entry) { return entry && typeof entry === 'object' && !Array.isArray(entry) && Object.prototype.hasOwnProperty.call(entry, 'default_value') ? entry.default_value : undefined; } function mappingEntryTransform(entry) { if (!entry || typeof entry !== 'object' || Array.isArray(entry) || entry.transform === undefined || entry.transform === null) { return ''; } var value = typeof entry.transform === 'object' ? entry.transform.kind : entry.transform; value = String(value || '').trim(); return transformKinds().indexOf(value) >= 0 ? value : ''; } function defaultValueToText(value) { if (value === undefined) return ''; if (typeof value === 'string') return value; return JSON.stringify(value); } function parseDefaultValue(text) { var value = String(text || '').trim(); if (!value) return undefined; if (/^(true|false|null)$/i.test(value) || /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(value) || /^[\[{]/.test(value)) { try { return JSON.parse(value); } catch (_error) { return value; } } return value; } function transformKinds() { return [ 'to_string', 'to_number', 'to_boolean', 'join', 'split', 'wrap_array', 'unwrap_singleton', ]; } function requestRowsFromEditor() { var parsed = safeJson('tool-input-mapping') || {}; return Object.keys(parsed).map(function(target) { var entry = parsed[target]; var source = mappingEntrySource(entry); var split = splitRequestTarget(target); return { input: sourceToField(source) || split.name, target: split.kind, apiName: split.name || sourceToField(source), defaultValue: defaultValueToText(mappingEntryDefault(entry)), transform: mappingEntryTransform(entry), }; }); } function responseRowsFromEditor() { var parsed = safeJson('tool-output-mapping') || {}; return Object.keys(parsed).map(function(outputField) { var source = mappingEntrySource(parsed[outputField]); return { output: outputField, responsePath: responseSourceToPath(source) || outputField, }; }); } function suggestedRequestRows() { var rows = []; pathParams().forEach(function(name) { rows.push({ input: name, target: 'path', apiName: name }); }); var sample = safeJson('wizard-input-sample'); var fields = []; if (sample && typeof sample === 'object') { fields = collectJsonLeaves(sample, ''); } if (!fields.length) { fields = collectSchemaFields(safeJson('tool-input-schema'), ''); } var pathSet = new Set(rows.map(function(row) { return row.input; })); fields.forEach(function(item) { if (pathSet.has(item.name)) return; rows.push({ input: item.name, target: defaultTargetKind(), apiName: item.name, }); }); return rows; } function suggestedResponseRows() { var sample = safeJson('wizard-output-sample'); var fields = sample && typeof sample === 'object' ? collectJsonLeaves(sample, '') : collectSchemaFields(safeJson('tool-output-schema'), ''); return fields.map(function(item) { return { output: outputNameFromPath(item.name), responsePath: item.name, }; }); } function outputNameFromPath(path) { return String(path || 'value') .replace(/^\[0\]\.?/, 'item_') .replace(/\[([0-9]+)\]/g, '_$1') .replace(/[^A-Za-z0-9_]+/g, '_') .replace(/^_+|_+$/g, '') || 'value'; } function joinJsonPath(root, relativePath) { var path = String(relativePath || '').trim(); if (!path) return root; return path.charAt(0) === '[' ? root + path : root + '.' + path; } function makeOption(value, label, selected) { var option = document.createElement('option'); option.value = value; option.textContent = label; option.selected = selected; return option; } function makeInput(value, className, placeholder) { var input = document.createElement('input'); input.className = className || 'form-input input-mono'; input.value = value === undefined || value === null ? '' : String(value); input.placeholder = placeholder || ''; input.autocomplete = 'off'; input.spellcheck = false; input.addEventListener('input', syncVisualMappingsToYaml); input.addEventListener('change', syncVisualMappingsToYaml); return input; } function makeRemoveButton(row) { var button = document.createElement('button'); button.type = 'button'; button.className = 'row-btn row-btn-delete mapping-row-remove'; button.title = 'Удалить строку'; button.textContent = '×'; button.addEventListener('click', function() { row.remove(); syncVisualMappingsToYaml(); }); return button; } function wrapMappingControl(labelText, control) { var wrapper = document.createElement('label'); wrapper.className = 'mapping-field'; var label = document.createElement('span'); label.className = 'mapping-field-label'; label.textContent = labelText; wrapper.appendChild(label); wrapper.appendChild(control); return wrapper; } function makeMappingArrow() { var arrow = document.createElement('span'); arrow.className = 'mapping-arrow'; arrow.setAttribute('aria-hidden', 'true'); arrow.textContent = '→'; return arrow; } function renderRequestRows(rows) { var root = field('wizard-request-mapping-rows'); if (!root) return; root.replaceChildren(); if (!rows.length) rows = suggestedRequestRows(); rows.forEach(function(row) { root.appendChild(createRequestRow(row)); }); if (!suspendSync) syncVisualMappingsToYaml(); } function createRequestRow(row) { var item = document.createElement('div'); item.className = 'mapping-row request-mapping-row'; item.dataset.mappingRow = 'request'; var input = makeInput(row.input, 'form-input input-mono mapping-source', 'base'); input.dataset.role = 'input'; input.title = 'Поле, которое MCP клиент передает инструменту'; item.appendChild(wrapMappingControl('Из инструмента', input)); item.appendChild(makeMappingArrow()); var select = document.createElement('select'); select.className = 'form-select mapping-target'; select.dataset.role = 'target'; [ ['path', 'Path'], ['query', 'Query'], ['headers', 'Header'], ['body', 'Body'], ].forEach(function(entry) { select.appendChild(makeOption(entry[0], entry[1], entry[0] === row.target)); }); select.addEventListener('change', syncVisualMappingsToYaml); item.appendChild(wrapMappingControl('Куда в API', select)); var apiName = makeInput(row.apiName || row.input, 'form-input input-mono mapping-api-name', 'base'); apiName.dataset.role = 'apiName'; apiName.title = 'Имя path, query, header или body-поля в API-запросе'; item.appendChild(wrapMappingControl('Имя в API', apiName)); var defaultValue = makeInput(row.defaultValue, 'form-input input-mono mapping-default-value', 'если не передано'); defaultValue.dataset.role = 'defaultValue'; defaultValue.title = 'Необязательно. Это значение уйдет в API, если агент не передал поле инструмента.'; item.appendChild(wrapMappingControl('Если пусто', defaultValue)); var transform = document.createElement('select'); transform.className = 'form-select mapping-transform'; transform.dataset.role = 'transform'; transform.appendChild(makeOption('', 'Без преобразования', !row.transform)); transformKinds().forEach(function(kind) { transform.appendChild(makeOption(kind, kind, kind === row.transform)); }); transform.title = 'Простое преобразование перед отправкой в API'; transform.addEventListener('change', syncVisualMappingsToYaml); item.appendChild(wrapMappingControl('Преобразование', transform)); item.appendChild(makeRemoveButton(item)); return item; } function renderResponseRows(rows) { var root = field('wizard-response-mapping-rows'); if (!root) return; root.replaceChildren(); if (!rows.length) rows = suggestedResponseRows(); rows.forEach(function(row) { root.appendChild(createResponseRow(row)); }); if (!suspendSync) syncVisualMappingsToYaml(); } function createResponseRow(row) { var item = document.createElement('div'); item.className = 'mapping-row response-mapping-row'; item.dataset.mappingRow = 'response'; var responsePath = makeInput(row.responsePath, 'form-input input-mono mapping-response-path', 'rates.EUR'); responsePath.dataset.role = 'responsePath'; responsePath.title = 'Поле из ответа API'; item.appendChild(wrapMappingControl('Из ответа API', responsePath)); item.appendChild(makeMappingArrow()); var output = makeInput(row.output, 'form-input input-mono mapping-output-field', 'rate'); output.dataset.role = 'output'; output.title = 'Поле результата, которое получит MCP клиент'; item.appendChild(wrapMappingControl('В результат инструмента', output)); item.appendChild(makeRemoveButton(item)); return item; } function valueIn(row, role) { var input = row.querySelector('[data-role="' + role + '"]'); return input ? input.value.trim() : ''; } function syncVisualMappingsToYaml() { var requestRows = document.querySelectorAll('[data-mapping-row="request"]'); if (requestRows.length) { var requestMapping = {}; requestRows.forEach(function(row) { var input = valueIn(row, 'input'); var target = valueIn(row, 'target'); var apiName = valueIn(row, 'apiName') || input; var defaultText = valueIn(row, 'defaultValue'); var transform = valueIn(row, 'transform'); if (!input || !target || !apiName) return; if (defaultText || transform) { var entry = { source: '$.input.' + input }; if (defaultText) entry.default_value = parseDefaultValue(defaultText); if (transform) entry.transform = transform; requestMapping[target + '.' + apiName] = entry; return; } requestMapping[target + '.' + apiName] = '$.input.' + input; }); writeText('tool-input-mapping', dumpYaml(requestMapping)); } var responseRows = document.querySelectorAll('[data-mapping-row="response"]'); if (responseRows.length) { var responseMapping = {}; responseRows.forEach(function(row) { var responsePath = valueIn(row, 'responsePath'); var output = valueIn(row, 'output') || outputNameFromPath(responsePath); if (!responsePath || !output) return; responseMapping[output] = joinJsonPath('$.response.body', responsePath); }); writeText('tool-output-mapping', dumpYaml(responseMapping)); } renderMappingWarnings(); } function currentRequestMappedInputs() { return Array.from(document.querySelectorAll('[data-mapping-row="request"]')).map(function(row) { return valueIn(row, 'input'); }).filter(Boolean); } function currentResponseMappedOutputs() { return Array.from(document.querySelectorAll('[data-mapping-row="response"]')).map(function(row) { return valueIn(row, 'output'); }).filter(Boolean); } function jsonParseWarning(id, label) { var raw = readText(id); if (!raw) return null; try { parseText(id); return null; } catch (error) { return label + ': JSON не разобран. ' + error.message; } } function buildMappingWarnings() { var warnings = []; var inputJsonWarning = jsonParseWarning('wizard-input-sample', 'Пример входных данных'); var outputJsonWarning = jsonParseWarning('wizard-output-sample', 'Пример ответа'); if (inputJsonWarning) warnings.push(inputJsonWarning); if (outputJsonWarning) warnings.push(outputJsonWarning); var required = requiredSchemaFields(safeJson('tool-input-schema')); var mapped = new Set(currentRequestMappedInputs()); var missing = required.filter(function(name) { return !mapped.has(name); }); if (missing.length) { warnings.push('Обязательные поля входной схемы не отправляются в API: ' + missing.join(', ') + '.'); } var outputRows = currentResponseMappedOutputs(); if (!outputRows.length) { warnings.push('Ответ инструмента пустой. Выберите хотя бы одно поле из ответа API.'); } if (outputRows.length > 12) { warnings.push('В ответ инструмента выбрано много полей: ' + outputRows.length + '. Лучше вернуть только данные, нужные агенту.'); } var hasArrayIndex = Array.from(document.querySelectorAll('[data-mapping-row="response"]')).some(function(row) { return /\[[0-9]+\]/.test(valueIn(row, 'responsePath')); }); if (hasArrayIndex) { warnings.push('В ответе выбрано поле из одного элемента массива. Если агенту нужен весь список, верните массив целиком через расширенный YAML-маппинг.'); } return warnings; } function renderMappingWarnings() { var root = field('wizard-mapping-warnings'); if (!root) return; var warnings = buildMappingWarnings(); root.replaceChildren(); root.hidden = warnings.length === 0; warnings.forEach(function(message) { var item = document.createElement('div'); item.className = 'mapping-warning'; var title = document.createElement('strong'); title.textContent = 'Проверьте маппинг. '; item.appendChild(title); item.appendChild(document.createTextNode(message)); root.appendChild(item); }); } function renderResponseTree() { var root = field('wizard-response-json-tree'); if (!root) return; root.replaceChildren(); var sample = safeJson('wizard-output-sample'); var leaves = sample && typeof sample === 'object' ? collectJsonLeaves(sample, '') : []; if (!leaves.length) { var empty = document.createElement('div'); empty.className = 'form-hint'; empty.textContent = 'Добавьте пример ответа JSON, чтобы выбрать поля кликом.'; root.appendChild(empty); return; } leaves.forEach(function(leaf) { var button = document.createElement('button'); button.type = 'button'; button.className = 'json-tree-node'; button.textContent = leaf.name; button.addEventListener('click', function() { var output = outputNameFromPath(leaf.name); field('wizard-response-mapping-rows').appendChild(createResponseRow({ responsePath: leaf.name, output: output, })); syncVisualMappingsToYaml(); }); root.appendChild(button); }); } function formatSamplesAndInferSchemas() { var input = parseText('wizard-input-sample'); var output = parseText('wizard-output-sample'); writeText('wizard-input-sample', JSON.stringify(input, null, 2)); writeText('wizard-test-input', JSON.stringify(input, null, 2)); writeText('wizard-output-sample', JSON.stringify(output, null, 2)); writeText('tool-input-schema', JSON.stringify(inferJsonSchema(input), null, 2)); writeText('tool-output-schema', JSON.stringify(inferJsonSchema(output), null, 2)); renderRequestRows(suggestedRequestRows()); renderResponseRows(suggestedResponseRows()); renderResponseTree(); if (window.CrankWizardLive && typeof window.CrankWizardLive.renderAgentFacingPreview === 'function') { window.CrankWizardLive.renderAgentFacingPreview(); } } function loadJsonFile(inputId, targetTextareaId, afterLoad) { var input = field(inputId); if (!input) return; var file = input.files && input.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(event) { try { var parsed = JSON.parse(event.target.result || '{}'); writeText(targetTextareaId, JSON.stringify(parsed, null, 2)); if (afterLoad) afterLoad(); } catch (error) { if (window.CrankUi) { window.CrankUi.error(error.message, 'JSON не прочитан'); } } finally { input.value = ''; } }; reader.readAsText(file); } function bindFileButton(buttonId, inputId) { var button = field(buttonId); var input = field(inputId); if (!button || !input || button.dataset.mappingBound === 'true') return; button.dataset.mappingBound = 'true'; button.addEventListener('click', function(event) { event.preventDefault(); input.click(); }); } function bindOnce(id, eventName, handler) { var element = field(id); if (!element || element.dataset.mappingBound === 'true') return; element.dataset.mappingBound = 'true'; element.addEventListener(eventName, handler); } function initializeMappingBuilder() { bindOnce('wizard-add-request-mapping-row', 'click', function(event) { event.preventDefault(); field('wizard-request-mapping-rows').appendChild(createRequestRow({ input: '', target: defaultTargetKind(), apiName: '', })); }); bindOnce('wizard-add-response-mapping-row', 'click', function(event) { event.preventDefault(); field('wizard-response-mapping-rows').appendChild(createResponseRow({ responsePath: '', output: '', })); }); bindOnce('wizard-auto-request-mapping', 'click', function(event) { event.preventDefault(); renderRequestRows(suggestedRequestRows()); }); bindOnce('wizard-auto-response-mapping', 'click', function(event) { event.preventDefault(); renderResponseRows(suggestedResponseRows()); renderResponseTree(); }); bindOnce('wizard-sync-request-mapping', 'click', function(event) { event.preventDefault(); syncVisualMappingsToYaml(); }); bindOnce('wizard-sync-response-mapping', 'click', function(event) { event.preventDefault(); syncVisualMappingsToYaml(); }); bindOnce('wizard-format-samples', 'click', function(event) { event.preventDefault(); formatSamplesAndInferSchemas(); }); bindFileButton('wizard-load-input-sample-file', 'wizard-input-sample-file'); bindFileButton('wizard-load-output-sample-file', 'wizard-output-sample-file'); bindOnce('wizard-input-sample-file', 'change', function() { loadJsonFile('wizard-input-sample-file', 'wizard-input-sample', function() { renderRequestRows(suggestedRequestRows()); }); }); bindOnce('wizard-output-sample-file', 'change', function() { loadJsonFile('wizard-output-sample-file', 'wizard-output-sample', function() { renderResponseRows(suggestedResponseRows()); renderResponseTree(); }); }); ['wizard-input-sample', 'tool-input-schema', 'endpoint-path'].forEach(function(id) { var element = field(id); if (!element || element.dataset.mappingRefreshBound === 'true') return; element.dataset.mappingRefreshBound = 'true'; element.addEventListener('change', function() { renderRequestRows(requestRowsFromEditor()); renderMappingWarnings(); }); }); ['wizard-output-sample', 'tool-output-schema'].forEach(function(id) { var element = field(id); if (!element || element.dataset.mappingRefreshBound === 'true') return; element.dataset.mappingRefreshBound = 'true'; element.addEventListener('change', function() { renderResponseRows(responseRowsFromEditor()); renderResponseTree(); renderMappingWarnings(); }); }); renderRequestRows(requestRowsFromEditor()); renderResponseRows(responseRowsFromEditor()); renderResponseTree(); renderMappingWarnings(); } window.CrankWizardMapping = { initialize: initializeMappingBuilder, sync: syncVisualMappingsToYaml, renderFromEditors: function() { suspendSync = true; renderRequestRows(requestRowsFromEditor()); renderResponseRows(responseRowsFromEditor()); suspendSync = false; syncVisualMappingsToYaml(); renderResponseTree(); renderMappingWarnings(); }, formatSamplesAndInferSchemas: formatSamplesAndInferSchemas, renderWarnings: renderMappingWarnings, }; })();