diff --git a/apps/ui/css/wizard.css b/apps/ui/css/wizard.css index d26e1ea..7a4d284 100644 --- a/apps/ui/css/wizard.css +++ b/apps/ui/css/wizard.css @@ -1446,6 +1446,44 @@ } .upstream-preview-change:hover { border-color: var(--text-secondary); color: var(--text-primary); } +.agent-preview-summary { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); + gap: 12px; +} + +.agent-preview-label { + color: var(--text-muted); + font-size: 11px; + font-weight: 700; + letter-spacing: .04em; + margin-bottom: 6px; + text-transform: uppercase; +} + +.agent-preview-value, +.agent-preview-description { + background: var(--surface-subtle); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 13px; + line-height: 1.5; + min-height: 38px; + padding: 9px 10px; +} + +.agent-preview-description { + color: var(--text-secondary); + min-height: 54px; +} + +@media (max-width: 760px) { + .agent-preview-summary { + grid-template-columns: 1fr; + } +} + /* ── Register new upstream inline form ── */ .upstream-new-form { background: var(--bg-canvas); diff --git a/apps/ui/html/wizard/step5.html b/apps/ui/html/wizard/step5.html index 05c4d4b..73fa2f9 100644 --- a/apps/ui/html/wizard/step5.html +++ b/apps/ui/html/wizard/step5.html @@ -103,6 +103,57 @@ tls: +
+
+
+ + + +
+
+
Как инструмент увидит MCP клиент
+
Проверьте имя, описание, входную схему и примеры до публикации.
+
+ +
+
+
+
+
Имя инструмента
+
+
+
+
Короткое описание
+
+
+
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+
diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index 1992661..3712b04 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -566,6 +566,16 @@ var TRANSLATIONS = { 'wizard.step5.draft_actions': 'Action result', 'wizard.step5.live_save_title': 'Saving before action', 'wizard.step5.live_save_body': 'Crank saves the current settings before running checks or publishing.', + 'wizard.step5.agent_preview_title': 'How the MCP client sees this tool', + 'wizard.step5.agent_preview_subtitle': 'Check the name, description, input schema and examples before publication.', + 'wizard.step5.copy_agent_preview': 'Copy schema', + 'wizard.step5.preview_tool_name': 'Tool name', + 'wizard.step5.preview_tool_title': 'Short description', + 'wizard.step5.preview_tool_description': 'Description for the model', + 'wizard.step5.preview_input_schema': 'Input schema', + 'wizard.step5.preview_tool_call': 'Example tools/call', + 'wizard.step5.preview_success_response': 'Successful response example', + 'wizard.step5.preview_error_response': 'Error example', 'wizard.step5.samples_title': 'Examples and automatic setup', 'wizard.step5.samples_subtitle': 'Save example input and output data, then let Crank rebuild schemas and field bindings from them.', 'wizard.step5.input_sample': 'Input example', @@ -652,6 +662,8 @@ var TRANSLATIONS = { 'wizard.test.no_response_body': 'Run a test first, then copy the response into the output example.', 'wizard.test.response_copied': 'Response copied', 'wizard.test.response_copied_body': 'The latest test response was copied into the output example.', + 'wizard.agent_preview.copied': 'Schema copied', + 'wizard.agent_preview.copied_body': 'The MCP tool schema was copied to the clipboard.', 'wizard.boolean.yes': 'yes', 'wizard.boolean.no': 'no', 'wizard.yaml.exported': 'YAML exported', @@ -1352,6 +1364,16 @@ var TRANSLATIONS = { 'wizard.step5.draft_actions': 'Результат действия', 'wizard.step5.live_save_title': 'Сохранение перед действием', 'wizard.step5.live_save_body': 'Перед проверкой или публикацией Crank сохраняет текущие настройки.', + 'wizard.step5.agent_preview_title': 'Как инструмент увидит MCP клиент', + 'wizard.step5.agent_preview_subtitle': 'Проверьте имя, описание, входную схему и примеры до публикации.', + 'wizard.step5.copy_agent_preview': 'Копировать схему', + 'wizard.step5.preview_tool_name': 'Имя инструмента', + 'wizard.step5.preview_tool_title': 'Короткое описание', + 'wizard.step5.preview_tool_description': 'Описание для модели', + 'wizard.step5.preview_input_schema': 'Входная схема', + 'wizard.step5.preview_tool_call': 'Пример вызова tools/call', + 'wizard.step5.preview_success_response': 'Пример успешного ответа', + 'wizard.step5.preview_error_response': 'Пример ошибки', 'wizard.step5.samples_title': 'Примеры и автоматическая настройка', 'wizard.step5.samples_subtitle': 'Сохраните пример входных и выходных данных, затем Crank сможет пересобрать схемы и связи полей на их основе.', 'wizard.step5.input_sample': 'Пример входных данных', @@ -1438,6 +1460,8 @@ var TRANSLATIONS = { 'wizard.test.no_response_body': 'Сначала выполните тест, затем скопируйте ответ в выходной пример.', 'wizard.test.response_copied': 'Ответ скопирован', 'wizard.test.response_copied_body': 'Последний тестовый ответ скопирован в выходной пример.', + 'wizard.agent_preview.copied': 'Схема скопирована', + 'wizard.agent_preview.copied_body': 'Схема MCP инструмента скопирована в буфер обмена.', 'wizard.boolean.yes': 'да', 'wizard.boolean.no': 'нет', 'wizard.yaml.exported': 'YAML экспортирован', diff --git a/apps/ui/js/wizard-live.js b/apps/ui/js/wizard-live.js index 708e5dc..717507c 100644 --- a/apps/ui/js/wizard-live.js +++ b/apps/ui/js/wizard-live.js @@ -68,6 +68,7 @@ async function loadOperationForEdit() { wizardCurrentOperation = detail; wizardCurrentVersion = draftVersion; prefillWizardFromEdit(detail, draftVersion); + renderAgentFacingPreview(); } function setEditModePresentation() { @@ -98,6 +99,7 @@ function bindWizardLiveActions() { bindLiveAction('wizard-generate-draft', tKey('wizard.busy.generate'), generateDraftFromWizard); bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest); bindClick('wizard-copy-test-response', copyTestResponseToOutputSample); + bindClick('wizard-copy-agent-preview', copyAgentFacingPreview); bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml); bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml); bindLiveAction('wizard-publish-operation', tKey('wizard.busy.publish'), publishWizardOperation); @@ -108,6 +110,7 @@ function bindWizardLiveActions() { var yamlInput = document.getElementById('wizard-import-yaml-file'); if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection); + bindAgentFacingPreview(); } function bindClick(id, handler) { @@ -302,6 +305,99 @@ function setTextareaValue(id, value) { element.value = safeStringify(value); } +function setTextContent(id, value) { + var element = document.getElementById(id); + if (!element) return; + element.textContent = value || ''; +} + +function structuredTextOrFallback(id, fallback) { + try { + var value = parseStructuredText(textValue(id)); + if (value === null || value === undefined || value === '') return fallback; + return value; + } catch (_error) { + return fallback; + } +} + +function buildAgentFacingPreview() { + var inputSchema = structuredTextOrFallback('tool-input-schema', {}); + var inputSample = structuredTextOrFallback('wizard-input-sample', {}); + var outputSample = structuredTextOrFallback('wizard-output-sample', {}); + var toolName = textValue('tool-name') || 'unnamed_tool'; + var title = textValue('tool-title') || textValue('tool-display-name') || toolName; + var description = textValue('tool-description'); + + return { + manifest: { + name: toolName, + title: title, + description: description, + inputSchema: inputSchema, + }, + toolCall: { + name: toolName, + arguments: inputSample, + }, + successResponse: outputSample, + errorResponse: { + isError: true, + content: [ + { + type: 'text', + text: 'Инструмент вернул ошибку. Проверьте входные параметры или состояние внешнего API.', + }, + ], + }, + }; +} + +function renderAgentFacingPreview() { + var root = document.getElementById('agent-facing-preview-card'); + if (!root) return; + + var preview = buildAgentFacingPreview(); + setTextContent('agent-preview-tool-name', preview.manifest.name); + setTextContent('agent-preview-tool-title', preview.manifest.title); + setTextContent('agent-preview-tool-description', preview.manifest.description); + setTextareaValue('agent-preview-input-schema', preview.manifest.inputSchema); + setTextareaValue('agent-preview-tool-call', preview.toolCall); + setTextareaValue('agent-preview-success-response', preview.successResponse); + setTextareaValue('agent-preview-error-response', preview.errorResponse); +} + +function bindAgentFacingPreview() { + [ + 'tool-name', + 'tool-title', + 'tool-display-name', + 'tool-description', + 'tool-input-schema', + 'wizard-input-sample', + 'wizard-output-sample', + ].forEach(function(id) { + var element = document.getElementById(id); + if (!element || element.dataset.agentPreviewBound === 'true') return; + element.dataset.agentPreviewBound = 'true'; + element.addEventListener('input', renderAgentFacingPreview); + element.addEventListener('change', renderAgentFacingPreview); + }); + renderAgentFacingPreview(); +} + +function copyAgentFacingPreview() { + var preview = buildAgentFacingPreview(); + var content = JSON.stringify(preview.manifest, null, 2); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(content).then(function() { + showWizardLiveStatus(tKey('wizard.agent_preview.copied'), tKey('wizard.agent_preview.copied_body')); + }).catch(function() { + setTextareaValue('agent-preview-input-schema', preview.manifest.inputSchema); + }); + } +} + async function exportWizardYaml() { await persistCurrentDraft(true); var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, { @@ -386,6 +482,7 @@ async function generateDraftFromWizard() { setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2)); setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol)); setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol)); + renderAgentFacingPreview(); showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body')); } @@ -413,6 +510,7 @@ function copyTestResponseToOutputSample() { return; } setTextareaValue('wizard-output-sample', wizardTestResponsePreview); + renderAgentFacingPreview(); showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body')); } @@ -421,4 +519,5 @@ window.CrankWizardLive = { loadOperationForEdit: loadOperationForEdit, bindWizardLiveActions: bindWizardLiveActions, updateWizardProtocolVisibility: updateWizardProtocolVisibility, + renderAgentFacingPreview: renderAgentFacingPreview, }; diff --git a/apps/ui/tests/e2e/wizard.spec.js b/apps/ui/tests/e2e/wizard.spec.js index 3bbb513..ba39fe6 100644 --- a/apps/ui/tests/e2e/wizard.spec.js +++ b/apps/ui/tests/e2e/wizard.spec.js @@ -339,6 +339,171 @@ test('wizard edit mode hydrates fields from operation version snapshot', async ( await expect(page.locator('#wizard-test-input')).not.toHaveValue(/Ada/); }); +test('wizard shows agent-facing MCP preview from current draft fields', async ({ page }) => { + await login(page); + const workspace = await getCurrentWorkspace(page); + const operationId = 'frankfurter_monthly_rates'; + const version = 7; + + await page.route( + `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`, + async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + id: operationId, + workspace_id: workspace.id, + name: operationId, + display_name: 'Frankfurter monthly rates', + category: 'finance', + protocol: 'rest', + security_level: 'standard', + status: 'draft', + current_draft_version: version, + latest_published_version: null, + created_at: '2026-06-19T00:00:00Z', + updated_at: '2026-06-19T00:00:00Z', + published_at: null, + draft_version_ref: { version, status: 'draft' }, + published_version_ref: null, + agent_refs: [], + }), + }); + }, + ); + + await page.route( + `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`, + async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + operation_id: operationId, + workspace_id: workspace.id, + version, + status: 'draft', + change_note: null, + created_at: '2026-06-19T00:00:00Z', + created_by: null, + snapshot: { + id: operationId, + name: operationId, + display_name: 'Frankfurter monthly rates', + category: 'finance', + protocol: 'rest', + security_level: 'standard', + status: 'draft', + version, + target: { + kind: 'rest', + base_url: 'https://api.frankfurter.app', + method: 'GET', + path_template: '/2024-01..2024-03', + static_headers: { Accept: 'application/json' }, + }, + input_schema: { + type: 'object', + description: null, + required: false, + nullable: false, + default_value: null, + fields: { + from: { + type: 'string', + description: 'Base currency', + required: true, + nullable: false, + default_value: 'USD', + fields: {}, + items: null, + enum_values: [], + variants: [], + }, + to: { + type: 'string', + description: 'Quote currency', + required: false, + nullable: false, + default_value: 'EUR', + fields: {}, + items: null, + enum_values: [], + variants: [], + }, + }, + items: null, + enum_values: [], + variants: [], + }, + output_schema: { + type: 'object', + description: null, + required: false, + nullable: false, + default_value: null, + fields: { + rates: { + type: 'object', + description: 'Rates by date', + required: true, + nullable: false, + default_value: null, + fields: {}, + items: null, + enum_values: [], + variants: [], + }, + }, + items: null, + enum_values: [], + variants: [], + }, + input_mapping: { rules: [] }, + output_mapping: { + rules: [{ source: '$.response.body.rates', target: '$.output.rates' }], + }, + execution_config: { + timeout_ms: 9000, + retry_policy: null, + auth_profile_ref: null, + headers: {}, + protocol_options: null, + streaming: null, + }, + tool_description: { + title: 'Получить историю курсов за месяц', + description: 'Возвращает историю курсов валют Frankfurter за заданный период.', + tags: [], + examples: [], + }, + wizard_state: { + input_sample: { from: 'USD', to: 'EUR' }, + output_sample: { rates: { '2024-01': { EUR: 0.91 } } }, + test_input: { from: 'GBP', to: 'USD' }, + }, + samples: [], + generated_draft: null, + config_export: null, + }, + }), + }); + }, + ); + + await page.goto(`/wizard/?mode=edit&operationId=${operationId}`); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + + await expect(page.locator('#agent-preview-tool-name')).toHaveText(operationId); + await expect(page.locator('#agent-preview-tool-description')).toContainText('Frankfurter'); + await expect(page.locator('#agent-preview-input-schema')).toHaveValue(/"from"/); + await expect(page.locator('#agent-preview-tool-call')).toHaveValue(/"name": "frankfurter_monthly_rates"/); + await expect(page.locator('#agent-preview-tool-call')).toHaveValue(/"from": "USD"/); + await expect(page.locator('#agent-preview-success-response')).toHaveValue(/0\.91/); +}); + test('wizard edit mode preserves explicit request mapping targets on save', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page);