Newer
Older
gnexus-tasks / frontend / src / views / McpView.vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from 'gnexus-ui-kit/vue'

// Страница-инструкция по MCP (ТЗ 3.10): что реализует, как подключить агента,
// как сервер держит контекст проекта. Ссылка — из «Настроек».

const { t } = useI18n()
const toast = useToast()

// Адрес MCP совпадает с адресом приложения: /mcp монтируется в backend,
// в dev проксируется vite (как /api)
const mcpUrl = computed(() => `${window.location.origin}/mcp/`)

const copied = ref(false)

async function copyUrl() {
  try {
    await navigator.clipboard.writeText(mcpUrl.value)
    copied.value = true
    toast.success({ title: t('mcp.copied') })
    setTimeout(() => (copied.value = false), 1500)
  } catch {
    toast.error({ title: t('mcp.copyFailed') })
  }
}

// Инструменты сервера: имя + короткое описание (детали агент получает сам —
// из описаний тулов и серверных instructions)
const tools = computed(() => [
  { name: 'list_projects', desc: t('mcp.toolListProjects') },
  { name: 'get_project', desc: t('mcp.toolGetProject') },
  { name: 'create_task', desc: t('mcp.toolCreateTask') },
  { name: 'update_task', desc: t('mcp.toolUpdateTask') },
  { name: 'complete_task', desc: t('mcp.toolCompleteTask') },
  { name: 'list_tasks', desc: t('mcp.toolListTasks') },
  { name: 'get_task', desc: t('mcp.toolGetTask') },
  { name: 'list_tags', desc: t('mcp.toolListTags') },
])

const claudeCommand = computed(
  () =>
    `claude mcp add --transport http gntodo ${mcpUrl.value} --header "Authorization: Bearer <MCP_TOKEN>"`,
)

const jsonConfig = computed(
  () =>
    JSON.stringify(
      {
        mcpServers: {
          gntodo: {
            type: 'http',
            url: mcpUrl.value,
            headers: { Authorization: 'Bearer <MCP_TOKEN>' },
          },
        },
      },
      null,
      2,
    ),
)
</script>

<template>
  <section>
    <GnPageHeader kicker="GNexus Tasks" :title="t('mcp.title')" />

    <GnCard>
      <template #title>{{ t('mcp.whatTitle') }}</template>
      <p class="hint">{{ t('mcp.whatText') }}</p>
    </GnCard>

    <GnCard>
      <template #title>{{ t('mcp.toolsTitle') }}</template>
      <p class="hint">{{ t('mcp.toolsHint') }}</p>
      <div class="tools">
        <div v-for="tool in tools" :key="tool.name" class="tool">
          <code>{{ tool.name }}</code>
          <span>{{ tool.desc }}</span>
        </div>
      </div>
    </GnCard>

    <GnCard>
      <template #title>{{ t('mcp.connectTitle') }}</template>
      <p class="hint">{{ t('mcp.connectText') }}</p>

      <div class="field">
        <div class="field-label">{{ t('mcp.connectUrlLabel') }}</div>
        <div class="code-line">
          <code>{{ mcpUrl }}</code>
          <GnButton
            size="sm"
            variant="secondary"
            :icon="copied ? 'ph-check' : 'ph-copy'"
            @click="copyUrl"
          >
            {{ t('mcp.copy') }}
          </GnButton>
        </div>
      </div>

      <div class="field">
        <div class="field-label">{{ t('mcp.connectTokenLabel') }}</div>
        <p class="hint">{{ t('mcp.connectTokenText') }}</p>
      </div>

      <div class="field">
        <div class="field-label">{{ t('mcp.connectClaudeLabel') }}</div>
        <pre class="code-block"><code>{{ claudeCommand }}</code></pre>
      </div>

      <div class="field">
        <div class="field-label">{{ t('mcp.connectJsonLabel') }}</div>
        <pre class="code-block"><code>{{ jsonConfig }}</code></pre>
      </div>
    </GnCard>

    <GnCard>
      <template #title>{{ t('mcp.projectTitle') }}</template>
      <p class="hint">{{ t('mcp.projectText') }}</p>
    </GnCard>
  </section>
</template>

<style scoped>
.card + .card,
.card {
  margin-top: 1.5rem;
}
.hint {
  color: var(--muted, #888);
  margin: 0 0 0.75rem;
}
.tools {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.tool {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}
.tool code {
  flex: 0 0 9.5rem;
  font-size: 0.85rem;
}
.tool span {
  color: var(--muted, #888);
  font-size: 0.9rem;
}
.field {
  margin-top: 1rem;
}
.field-label {
  font-weight: 600;
  margin-bottom: 0.35rem;
}
.code-line {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.code-line code,
.code-block code {
  background: var(--surface-alt, rgba(128, 128, 128, 0.12));
  border-radius: 6px;
  padding: 0.25rem 0.5rem;
  font-size: 0.85rem;
  word-break: break-all;
}
.code-block {
  margin: 0;
  overflow-x: auto;
}
.code-block code {
  display: block;
  padding: 0.6rem 0.75rem;
  white-space: pre;
}
</style>