@cat-factory/app 0.222.0 → 0.223.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@
16
16
  import { computed, onMounted, reactive, ref } from 'vue'
17
17
  import type { CapabilityCredentialStatus } from '~/types/capabilityCredentials'
18
18
  import SecretInput from '~/components/common/SecretInput.vue'
19
+ import ToolServerChecklist from '~/components/settings/ToolServerChecklist.vue'
19
20
 
20
21
  const { t, d } = useI18n()
21
22
  const store = useCapabilityCredentialsStore()
@@ -63,6 +64,19 @@ onMounted(async () => {
63
64
  }
64
65
  })
65
66
 
67
+ // The tool-server inventory that renders above the checklist. Its own read (and its own failure
68
+ // report, for the same reason): the two surfaces answer different questions off different endpoints,
69
+ // so one failing must not blank the other. Both resolve the same 403, so this is only reached by a
70
+ // caller the backend has already admitted.
71
+ const toolServers = useToolServersStore()
72
+ onMounted(async () => {
73
+ try {
74
+ await toolServers.load()
75
+ } catch (e) {
76
+ present(e, 'settings.toolServers.toast.loadFailed')
77
+ }
78
+ })
79
+
66
80
  async function saveKey(key: string) {
67
81
  const value = (drafts[key] ?? '').trim()
68
82
  if (!value) return
@@ -99,6 +113,11 @@ async function removeKey(key: string) {
99
113
 
100
114
  <template>
101
115
  <div class="space-y-4" data-testid="capability-credentials-panel">
116
+ <!-- The servers FIRST, because they are what the keys below authenticate: a bare list of
117
+ variable names does not tell an operator which of them matters, and the Test button is the
118
+ only thing on either surface that can say whether the value they typed works. -->
119
+ <ToolServerChecklist />
120
+
102
121
  <p class="text-sm text-slate-400">
103
122
  {{ t('settings.capabilityCredentials.intro') }}
104
123
  </p>
@@ -15,10 +15,11 @@
15
15
  // - "Package registries" — the private npm registries a checkout installs from (formerly an
16
16
  // Integrations-hub row). What a container can resolve its dependencies from is part of the
17
17
  // execution environment, not an optional external system a workspace links in.
18
- // - "Capability credentials" — the sealed per-workspace values behind the secrets a registered
19
- // tool server (MCP) or generative binary integration declares. What an agent's tools
20
- // authenticate as belongs beside where those agents run, and it is `secrets.manage`-only, so
21
- // the tab is HIDDEN rather than disabled for anyone without that permission.
18
+ // - "Capability credentials" — the deployment's tool servers (MCP) with a Test button each, above
19
+ // the sealed per-workspace values behind the secrets a registered tool server or generative
20
+ // binary integration declares. What an agent's tools authenticate as belongs beside where those
21
+ // agents run, and it is `secrets.manage`-only (the READ included, since both halves name the
22
+ // deployment's credential keys), so the tab is HIDDEN rather than disabled for anyone else.
22
23
  // Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
23
24
  // backend integration is disabled (503) simply doesn't render.
24
25
  import { computed, ref, watch } from 'vue'
@@ -43,6 +44,7 @@ const store = useProviderConnectionsStore()
43
44
  const auth = useAuthStore()
44
45
  const packageRegistries = usePackageRegistriesStore()
45
46
  const capabilityCredentials = useCapabilityCredentialsStore()
47
+ const toolServers = useToolServersStore()
46
48
  const { canManageSecrets } = useWorkspaceAccess()
47
49
 
48
50
  const open = computed({
@@ -93,7 +95,14 @@ const tabs = computed(() =>
93
95
  // the backend gates the read too), and `hasSurface` hides a tab with nothing in it — the
94
96
  // panel is a checklist projected from the deployment's registered capabilities, so a build
95
97
  // that registers none has no credential to type.
96
- capabilityCredentials: canManageSecrets.value && capabilityCredentials.hasSurface,
98
+ //
99
+ // EITHER surface earns the tab. A tool server that declares no credential has nothing on the
100
+ // checklist, and gating the tab on the checklist alone would leave the one server an operator
101
+ // most wants to test unreachable — while a credential whose capability is a generative
102
+ // integration has no tool-server row. Two questions, one tab, and neither is a subset of the
103
+ // other.
104
+ capabilityCredentials:
105
+ canManageSecrets.value && (capabilityCredentials.hasSurface || toolServers.hasSurface),
97
106
  }).map((value) => ({
98
107
  value,
99
108
  label: TAB_LABELS.value[value],
@@ -121,7 +130,10 @@ watch(
121
130
  // window must still open), reported by the panel, which can only do that once its tab exists.
122
131
  // Not probed at all without the permission — the backend would refuse it, and asking would
123
132
  // put a 403 in every member's console on every open.
124
- if (canManageSecrets.value) void capabilityCredentials.ensureLoaded().catch(() => {})
133
+ if (canManageSecrets.value) {
134
+ void capabilityCredentials.ensureLoaded().catch(() => {})
135
+ void toolServers.ensureLoaded().catch(() => {})
136
+ }
125
137
  activeTab.value = openInfrastructureTab(tabValues.value, ui.infrastructureTab)
126
138
  },
127
139
  { immediate: true },
@@ -0,0 +1,264 @@
1
+ <script setup lang="ts">
2
+ // Tool servers (MCP) — the deployment's registered servers, and a Test button that speaks the
3
+ // protocol to one.
4
+ //
5
+ // It sits ABOVE the credential checklist in the same tab, because it is what the credentials on that
6
+ // list are FOR: the checklist answers "which keys does this deployment want", and this answers "and
7
+ // does the server they authenticate actually work". Both are `secrets.manage`-only, which is why the
8
+ // tab is hidden rather than disabled for anyone else.
9
+ //
10
+ // A row states four independent reasons a declared server may never reach a run — no kind declares
11
+ // it, no harness can serve its transport, its credentials do not resolve, the endpoint is dead — and
12
+ // each is rendered rather than implied. Before this panel the first two lived in a boot log and the
13
+ // deployment's own source, and the last two were only visible by starting a run and reading the
14
+ // agent's prompt.
15
+ import { computed, ref } from 'vue'
16
+ import type {
17
+ ToolServerNotProbeableReason,
18
+ ToolServerProbeStatus,
19
+ ToolServerTransport,
20
+ ToolServerView,
21
+ } from '~/types/toolServers'
22
+
23
+ const { t } = useI18n()
24
+ const store = useToolServersStore()
25
+ const { present } = usePipelineErrorToast()
26
+
27
+ // Which failure DETAILS are expanded, by server id. Collapsed by default: the translated status line
28
+ // is what an operator acts on, and the raw backend prose is a disclosure behind it (never the
29
+ // primary description) per the i18n rule.
30
+ const expanded = ref<Record<string, boolean>>({})
31
+
32
+ // Exhaustive Records over the wire vocabularies, so a member added in `@cat-factory/contracts` fails
33
+ // to compile here until it has translated copy. The sanctioned guard for an enum-keyed lookup the
34
+ // typed-message-key check cannot see.
35
+ const TRANSPORT_LABELS = computed<Record<ToolServerTransport, string>>(() => ({
36
+ stdio: t('settings.toolServers.transport.stdio'),
37
+ http: t('settings.toolServers.transport.http'),
38
+ }))
39
+ const STATUS_LABELS = computed<Record<ToolServerProbeStatus, string>>(() => ({
40
+ ok: t('settings.toolServers.status.ok'),
41
+ credentials_missing: t('settings.toolServers.status.credentialsMissing'),
42
+ credential_refused: t('settings.toolServers.status.credentialRefused'),
43
+ unreachable: t('settings.toolServers.status.unreachable'),
44
+ http_error: t('settings.toolServers.status.httpError'),
45
+ protocol_error: t('settings.toolServers.status.protocolError'),
46
+ not_probeable: t('settings.toolServers.status.notProbeable'),
47
+ }))
48
+ const NOT_PROBEABLE_LABELS = computed<Record<ToolServerNotProbeableReason, string>>(() => ({
49
+ stdio_transport: t('settings.toolServers.notProbeable.stdio'),
50
+ container_local_url: t('settings.toolServers.notProbeable.containerLocal'),
51
+ url_not_allowed: t('settings.toolServers.notProbeable.urlNotAllowed'),
52
+ }))
53
+
54
+ const servers = computed<ToolServerView[]>(() => store.view?.servers ?? [])
55
+
56
+ function resultFor(id: string) {
57
+ return store.results[id]
58
+ }
59
+
60
+ /** Success is the only green; every other verdict names something for the operator to fix. */
61
+ function statusColor(status: ToolServerProbeStatus): 'success' | 'warning' | 'error' {
62
+ if (status === 'ok') return 'success'
63
+ return status === 'not_probeable' ? 'warning' : 'error'
64
+ }
65
+
66
+ async function runProbe(id: string) {
67
+ try {
68
+ await store.probe(id)
69
+ } catch (e) {
70
+ // A THROWN failure is not a verdict — a 404 for a server the deployment has since dropped, or a
71
+ // transient 5xx. Presented through the shared status-class funnel rather than stored as a
72
+ // result, so the row does not claim the probe answered.
73
+ present(e, 'settings.toolServers.toast.probeFailed')
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <template>
79
+ <section v-if="servers.length" class="space-y-3" data-testid="tool-servers-section">
80
+ <div>
81
+ <h3 class="text-sm font-semibold text-slate-200">
82
+ {{ t('settings.toolServers.heading') }}
83
+ </h3>
84
+ <p class="text-xs text-slate-400">{{ t('settings.toolServers.intro') }}</p>
85
+ </div>
86
+
87
+ <article
88
+ v-for="server in servers"
89
+ :key="server.id"
90
+ class="space-y-2 rounded-lg border border-slate-700 p-3"
91
+ :data-testid="`tool-server-${server.id}`"
92
+ >
93
+ <div class="flex flex-wrap items-center gap-2">
94
+ <span class="text-sm font-medium text-slate-200">{{ server.label }}</span>
95
+ <UBadge color="neutral" variant="soft" size="sm">
96
+ {{ TRANSPORT_LABELS[server.transport] }}
97
+ </UBadge>
98
+ <code class="font-mono text-[11px] text-slate-500">{{ server.id }}</code>
99
+ </div>
100
+
101
+ <p class="truncate font-mono text-[11px] text-slate-500" :title="server.target">
102
+ {{ server.target }}
103
+ </p>
104
+ <p v-if="server.guidance" class="text-xs text-slate-400">{{ server.guidance }}</p>
105
+
106
+ <!-- Which agents get it. An EMPTY list is a registration attached to nothing: it never reaches
107
+ a dispatch, so the credentials it asks for are keys an operator fills in for no run. Said
108
+ out loud, because no other surface in the platform can see that state. -->
109
+ <p v-if="server.declaredBy.length" class="text-[11px] text-slate-400">
110
+ {{ t('settings.toolServers.declaredBy', { kinds: server.declaredBy.join(', ') }) }}
111
+ </p>
112
+ <p v-else class="text-[11px] text-amber-400" :data-testid="`tool-server-orphan-${server.id}`">
113
+ {{ t('settings.toolServers.declaredByNone') }}
114
+ </p>
115
+
116
+ <!-- Which harnesses could serve it. EMPTY means the declaration can never run anywhere (an
117
+ `http` server narrowed to Codex, whose MCP client is stdio-only): it is never dropped FOR
118
+ A REASON on any run, so no prompt and no log line ever mentions it. -->
119
+ <p v-if="server.servableHarnesses.length" class="text-[11px] text-slate-400">
120
+ {{
121
+ t('settings.toolServers.servableHarnesses', {
122
+ harnesses: server.servableHarnesses.join(', '),
123
+ })
124
+ }}
125
+ </p>
126
+ <p v-else class="text-[11px] text-amber-400">
127
+ {{ t('settings.toolServers.servableHarnessesNone') }}
128
+ </p>
129
+
130
+ <p v-if="server.allowedTools?.length" class="text-[11px] text-slate-400">
131
+ {{ t('settings.toolServers.allowedTools', { tools: server.allowedTools.join(', ') }) }}
132
+ </p>
133
+ <p v-if="server.credentials.length" class="text-[11px] text-slate-400">
134
+ {{
135
+ t('settings.toolServers.credentials', {
136
+ keys: server.credentials.map((c) => c.key).join(', '),
137
+ })
138
+ }}
139
+ </p>
140
+
141
+ <div class="flex flex-wrap items-center gap-2 pt-1">
142
+ <UButton
143
+ v-if="server.probeable"
144
+ size="xs"
145
+ variant="subtle"
146
+ icon="i-lucide-plug"
147
+ :loading="store.probing === server.id"
148
+ :disabled="store.probing !== null"
149
+ :data-testid="`tool-server-test-${server.id}`"
150
+ @click="runProbe(server.id)"
151
+ >
152
+ {{ t('settings.toolServers.test') }}
153
+ </UButton>
154
+ <!-- Not a disabled button: nothing the operator can do here makes it clickable, so the row
155
+ states the reason instead. Each reason needs a different response — nothing to fix,
156
+ verify it from a run, or change the declaration. -->
157
+ <p
158
+ v-else-if="server.notProbeableReason"
159
+ class="text-[11px] text-slate-500"
160
+ :data-testid="`tool-server-unprobeable-${server.id}`"
161
+ >
162
+ {{ NOT_PROBEABLE_LABELS[server.notProbeableReason] }}
163
+ </p>
164
+ </div>
165
+
166
+ <div
167
+ v-if="resultFor(server.id)"
168
+ class="space-y-1 rounded-md border border-slate-800 bg-slate-900/40 p-2"
169
+ :data-testid="`tool-server-result-${server.id}`"
170
+ >
171
+ <div class="flex flex-wrap items-center gap-2">
172
+ <UBadge
173
+ :color="statusColor(resultFor(server.id)!.status)"
174
+ variant="soft"
175
+ size="sm"
176
+ :data-testid="`tool-server-status-${server.id}`"
177
+ >
178
+ {{ STATUS_LABELS[resultFor(server.id)!.status] }}
179
+ </UBadge>
180
+ <span v-if="resultFor(server.id)!.httpStatus" class="text-[11px] text-slate-400">
181
+ {{ t('settings.toolServers.httpStatus', { status: resultFor(server.id)!.httpStatus }) }}
182
+ </span>
183
+ </div>
184
+
185
+ <p v-if="resultFor(server.id)!.status === 'ok'" class="text-[11px] text-slate-300">
186
+ {{
187
+ t('settings.toolServers.okDetail', {
188
+ name: resultFor(server.id)!.serverName || server.id,
189
+ version: resultFor(server.id)!.serverVersion || '?',
190
+ protocol: resultFor(server.id)!.protocolVersion ?? '?',
191
+ count: resultFor(server.id)!.toolCount ?? 0,
192
+ })
193
+ }}
194
+ </p>
195
+ <!-- A count off a truncated read is a FLOOR, not a total, and the difference decides whether
196
+ the allowedTools verdict below means anything. -->
197
+ <p v-if="resultFor(server.id)!.toolsComplete === false" class="text-[11px] text-slate-500">
198
+ {{ t('settings.toolServers.toolsIncomplete') }}
199
+ </p>
200
+
201
+ <!-- The reconciliation nothing else in the platform can do: a well-formed tool name that
202
+ matches nothing narrows the CLI's allow-list to a dead pattern while the prompt keeps
203
+ advertising the tool. Withheld entirely when the tool list was a prefix. -->
204
+ <p
205
+ v-if="resultFor(server.id)!.allowedTools?.unmatched?.length"
206
+ class="text-[11px] text-amber-400"
207
+ :data-testid="`tool-server-unmatched-${server.id}`"
208
+ >
209
+ {{
210
+ t('settings.toolServers.unmatchedTools', {
211
+ tools: resultFor(server.id)!.allowedTools!.unmatched.join(', '),
212
+ })
213
+ }}
214
+ </p>
215
+ <p
216
+ v-else-if="resultFor(server.id)!.allowedTools?.checked === false"
217
+ class="text-[11px] text-slate-500"
218
+ >
219
+ {{ t('settings.toolServers.allowedToolsUnchecked') }}
220
+ </p>
221
+
222
+ <p
223
+ v-if="resultFor(server.id)!.unresolvedCredentials?.length"
224
+ class="text-[11px] text-amber-400"
225
+ >
226
+ {{
227
+ t('settings.toolServers.unresolvedCredentials', {
228
+ keys: resultFor(server.id)!.unresolvedCredentials!.join(', '),
229
+ })
230
+ }}
231
+ </p>
232
+ <p v-if="resultFor(server.id)!.refusedCredentials?.length" class="text-[11px] text-red-400">
233
+ {{
234
+ t('settings.toolServers.refusedCredentials', {
235
+ keys: resultFor(server.id)!.refusedCredentials!.join(', '),
236
+ })
237
+ }}
238
+ </p>
239
+
240
+ <!-- Raw backend prose is DETAIL behind a disclosure, never the primary description. It is
241
+ already scrubbed through `redactSecrets` at the emit site. -->
242
+ <template v-if="resultFor(server.id)!.error">
243
+ <UButton
244
+ size="xs"
245
+ variant="link"
246
+ class="px-0"
247
+ :data-testid="`tool-server-details-${server.id}`"
248
+ @click="expanded[server.id] = !expanded[server.id]"
249
+ >
250
+ {{
251
+ expanded[server.id]
252
+ ? t('settings.toolServers.hideDetails')
253
+ : t('settings.toolServers.showDetails')
254
+ }}
255
+ </UButton>
256
+ <pre
257
+ v-if="expanded[server.id]"
258
+ class="overflow-x-auto rounded bg-slate-950 p-2 font-mono text-[10px] text-slate-400"
259
+ >{{ resultFor(server.id)!.error }}</pre>
260
+ </template>
261
+ </div>
262
+ </article>
263
+ </section>
264
+ </template>
@@ -0,0 +1,21 @@
1
+ import { listToolServersContract, probeToolServerContract } from '@cat-factory/contracts'
2
+ import type { ApiContext } from './context'
3
+
4
+ /**
5
+ * Per-workspace tool-server (MCP) operability: the inventory of what this deployment declared, and
6
+ * a probe that speaks the protocol to one of them.
7
+ *
8
+ * `secrets.manage`-gated end to end, the READ included — the inventory names the credential keys the
9
+ * deployment's capabilities want and the endpoints those credentials are sent to. The probe is a
10
+ * POST because it SPENDS an outbound request under the deployment's own credential, so it must not
11
+ * be safe to retry from a cache or a prefetch. See ToolServerController.
12
+ */
13
+ export function toolServersApi({ send, ws }: ApiContext) {
14
+ return {
15
+ listToolServers: (workspaceId: string) =>
16
+ send(listToolServersContract, { pathPrefix: ws(workspaceId) }),
17
+
18
+ probeToolServer: (workspaceId: string, id: string) =>
19
+ send(probeToolServerContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
20
+ }
21
+ }
@@ -33,6 +33,7 @@ import { modelsApi } from './api/models'
33
33
  import { notificationsApi } from './api/notifications'
34
34
  import { packageRegistriesApi } from './api/packageRegistries'
35
35
  import { capabilityCredentialsApi } from './api/capabilityCredentials'
36
+ import { toolServersApi } from './api/toolServers'
36
37
  import { preflightsApi } from './api/preflights'
37
38
  import { presetsApi } from './api/presets'
38
39
  import { publicApiKeysApi } from './api/publicApiKeys'
@@ -156,6 +157,7 @@ export function useApi() {
156
157
  ...testSecretsApi(ctx),
157
158
  ...packageRegistriesApi(ctx),
158
159
  ...capabilityCredentialsApi(ctx),
160
+ ...toolServersApi(ctx),
159
161
  ...previewApi(ctx),
160
162
  ...environmentsApi(ctx),
161
163
  ...recurringApi(ctx),
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { useToolServersStore } from '~/stores/toolServers'
3
+ import { useWorkspaceStore } from '~/stores/workspace'
4
+ import type { ToolServerProbeResult, ToolServerView } from '~/types/toolServers'
5
+
6
+ /**
7
+ * Three behaviours carry this store, each about not losing an answer:
8
+ *
9
+ * - the availability probe. A 403 ("you may not manage secrets") is an ANSWER and resolves
10
+ * normally, hiding the surface; anything else propagates, because the panel is what can tell a
11
+ * reader the list could not be fetched. There is deliberately no 503 case, unlike the
12
+ * credential store: projecting a registry needs no encryption key.
13
+ * - a probe result is kept for every VERDICT, failures included. A failure is exactly the answer
14
+ * the operator asked for, and a store that only kept successes would leave the row looking
15
+ * untouched after a dead endpoint was reported.
16
+ * - results survive a re-read of the inventory, because a result describes the SERVER rather than
17
+ * the list it arrived in.
18
+ */
19
+ function server(over: Partial<ToolServerView> = {}): ToolServerView {
20
+ return {
21
+ id: 'issues',
22
+ label: 'Issue tracker',
23
+ transport: 'http',
24
+ target: 'https://mcp.example/rpc',
25
+ declaredBy: ['coder'],
26
+ servableHarnesses: ['claude-code'],
27
+ credentials: [],
28
+ probeable: true,
29
+ ...over,
30
+ }
31
+ }
32
+
33
+ describe('toolServers store', () => {
34
+ beforeEach(() => {
35
+ useWorkspaceStore().workspaceId = 'ws1'
36
+ })
37
+
38
+ it('load stores the inventory and marks the surface available', async () => {
39
+ vi.stubGlobal('useApi', () => ({
40
+ listToolServers: () => Promise.resolve({ servers: [server()] }),
41
+ }))
42
+
43
+ const store = useToolServersStore()
44
+ await store.load()
45
+
46
+ expect(store.available).toBe(true)
47
+ expect(store.hasSurface).toBe(true)
48
+ expect(store.loading).toBe(false)
49
+ })
50
+
51
+ it('shares one read between concurrent callers', async () => {
52
+ let reads = 0
53
+ vi.stubGlobal('useApi', () => ({
54
+ listToolServers: () => {
55
+ reads++
56
+ return Promise.resolve({ servers: [server()] })
57
+ },
58
+ }))
59
+
60
+ const store = useToolServersStore()
61
+ // Both callers fire on the same interaction: the Infrastructure window asks `ensureLoaded` whether
62
+ // the tab exists at all, and the panel refreshes on mount so a redeploy shows up without a reload.
63
+ // A read that started microseconds ago IS that refresh, so it is shared rather than duplicated.
64
+ await Promise.all([store.ensureLoaded(), store.load()])
65
+
66
+ expect(reads).toBe(1)
67
+ expect(store.hasSurface).toBe(true)
68
+ })
69
+
70
+ it('a 403 latches the surface unavailable without throwing', async () => {
71
+ vi.stubGlobal('useApi', () => ({
72
+ listToolServers: () => Promise.reject({ statusCode: 403 }),
73
+ }))
74
+
75
+ const store = useToolServersStore()
76
+ await expect(store.load()).resolves.toBeUndefined()
77
+
78
+ expect(store.available).toBe(false)
79
+ expect(store.hasSurface).toBe(false)
80
+ })
81
+
82
+ it('a transient failure propagates and latches nothing', async () => {
83
+ vi.stubGlobal('useApi', () => ({
84
+ listToolServers: () => Promise.reject({ statusCode: 500 }),
85
+ }))
86
+
87
+ const store = useToolServersStore()
88
+ await expect(store.load()).rejects.toBeDefined()
89
+
90
+ // `available` stays null so `ensureLoaded` remains retryable, and an already-loaded surface is
91
+ // not hidden by one bad read.
92
+ expect(store.available).toBeNull()
93
+ })
94
+
95
+ it('reports no surface for a deployment that registers no tool server', async () => {
96
+ vi.stubGlobal('useApi', () => ({ listToolServers: () => Promise.resolve({ servers: [] }) }))
97
+
98
+ const store = useToolServersStore()
99
+ await store.load()
100
+
101
+ // An answer rather than possibly an outage: the inventory is read off this process's own
102
+ // registry, which is why there is no `declarationsIncomplete` counterpart here.
103
+ expect(store.available).toBe(true)
104
+ expect(store.hasSurface).toBe(false)
105
+ })
106
+
107
+ it('keeps a FAILING probe result, and keeps it across a re-read of the inventory', async () => {
108
+ const failure: ToolServerProbeResult = {
109
+ serverId: 'issues',
110
+ status: 'unreachable',
111
+ error: 'TypeError: fetch failed',
112
+ }
113
+ vi.stubGlobal('useApi', () => ({
114
+ listToolServers: () => Promise.resolve({ servers: [server()] }),
115
+ probeToolServer: () => Promise.resolve(failure),
116
+ }))
117
+
118
+ const store = useToolServersStore()
119
+ await store.load()
120
+ await store.probe('issues')
121
+
122
+ expect(store.results.issues).toEqual(failure)
123
+ expect(store.probing).toBeNull()
124
+
125
+ await store.load()
126
+ expect(store.results.issues).toEqual(failure)
127
+ })
128
+
129
+ it('propagates a THROWN probe failure without storing a verdict', async () => {
130
+ // A 404 for a server the deployment has since dropped, or a transient 5xx: not a verdict, so the
131
+ // row must not claim the probe answered.
132
+ vi.stubGlobal('useApi', () => ({
133
+ probeToolServer: () => Promise.reject({ statusCode: 404 }),
134
+ }))
135
+
136
+ const store = useToolServersStore()
137
+ await expect(store.probe('ghost')).rejects.toBeDefined()
138
+
139
+ expect(store.results.ghost).toBeUndefined()
140
+ expect(store.probing).toBeNull()
141
+ })
142
+ })
@@ -0,0 +1,112 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { ToolServerProbeResult, ToolServersView } from '~/types/toolServers'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+ import { apiErrorStatus } from '~/composables/api/errors'
6
+
7
+ /**
8
+ * The deployment's tool servers (MCP) and the results of probing them.
9
+ *
10
+ * Two halves with different lifetimes, which is why they are separate refs. The INVENTORY is
11
+ * deployment code: it changes when the deployment redeploys, so it is loaded on demand and re-read
12
+ * rather than patched. A PROBE RESULT is a moment in time and belongs to the operator who asked for
13
+ * it, so results are kept per server id and never fetched eagerly — a probe spends an outbound
14
+ * request under the deployment's own credential, so opening a panel must not fire one.
15
+ *
16
+ * Mirrors the capability-credential store's availability handling deliberately: the two surfaces sit
17
+ * in one tab, gate on the same permission, and a member without it must see neither.
18
+ */
19
+ export const useToolServersStore = defineStore('toolServers', () => {
20
+ const api = useApi()
21
+
22
+ const view = ref<ToolServersView | null>(null)
23
+ // Probe results by server id. Kept after a re-read of the inventory: a result describes the server
24
+ // rather than the list it arrived in, and dropping it on refresh would erase the answer the
25
+ // operator just asked for.
26
+ const results = ref<Record<string, ToolServerProbeResult>>({})
27
+ const probing = ref<string | null>(null)
28
+ const loading = ref(false)
29
+ // The backend's two definitive refusals: no `secrets.manage` (403), and — unlike the credential
30
+ // store — never a 503, since the inventory needs no encryption key to project a registry. `null`
31
+ // until first probed. A 403 HIDES the surface rather than disabling it, because the inventory
32
+ // names the deployment's credential keys and its endpoints.
33
+ const available = ref<boolean | null>(null)
34
+ let inFlight: Promise<void> | null = null
35
+
36
+ /**
37
+ * Whether there is anything to show. A deployment that registers no tool server has no row to
38
+ * render, and the panel section is hidden rather than rendering an empty heading.
39
+ *
40
+ * No `declarationsIncomplete` counterpart here, unlike the credential checklist: the inventory is
41
+ * read straight off this process's own registry, so an empty answer is an answer rather than
42
+ * possibly an outage.
43
+ */
44
+ const hasSurface = computed(() => (view.value?.servers.length ?? 0) > 0)
45
+
46
+ /**
47
+ * Refresh the inventory, sharing a read that is already in flight.
48
+ *
49
+ * Coalescing belongs on `load` and not only on `ensureLoaded` because both callers fire on the
50
+ * same interaction: the Infrastructure window calls `ensureLoaded` to decide whether the tab
51
+ * exists at all, and the panel refreshes on mount so a redeploy shows up without a reload. Two
52
+ * identical GETs per open is what a plain "force" would have cost, and a read that started
53
+ * microseconds ago IS the refresh.
54
+ */
55
+ async function load() {
56
+ if (inFlight) return inFlight
57
+ inFlight = readInventory().finally(() => (inFlight = null))
58
+ return inFlight
59
+ }
60
+
61
+ async function readInventory() {
62
+ const ws = useWorkspaceStore()
63
+ loading.value = true
64
+ try {
65
+ view.value = await api.listToolServers(ws.requireId())
66
+ available.value = true
67
+ } catch (err) {
68
+ if (apiErrorStatus(err) === 403) {
69
+ // A definitive answer, not a failure: this caller may not manage secrets. Hide the surface
70
+ // and stop probing; resolve normally.
71
+ available.value = false
72
+ view.value = null
73
+ return
74
+ }
75
+ // Any other failure (transient 5xx / network) leaves the state untouched, so it neither hides
76
+ // an available panel nor caches a false "unavailable", and PROPAGATES: the panel is the one
77
+ // surface that can tell a reader it is looking at a list we could not fetch. Same split as the
78
+ // capability-credential store.
79
+ throw err
80
+ } finally {
81
+ loading.value = false
82
+ }
83
+ }
84
+
85
+ /** Load once and stay loaded; `load()` re-reads (both share whatever is in flight). */
86
+ async function ensureLoaded() {
87
+ if (available.value !== null) return
88
+ return load()
89
+ }
90
+
91
+ /**
92
+ * Probe ONE server and keep its result.
93
+ *
94
+ * The result is stored for every outcome, failures included: a failure IS the answer the operator
95
+ * asked for, and a store that only kept successes would leave the row looking untouched after the
96
+ * probe reported a dead endpoint. A thrown error (a 404 for a server the deployment has since
97
+ * dropped, a transient 5xx) propagates instead, because those are not probe verdicts.
98
+ */
99
+ async function probe(id: string) {
100
+ const ws = useWorkspaceStore()
101
+ probing.value = id
102
+ try {
103
+ const result = await api.probeToolServer(ws.requireId(), id)
104
+ results.value = { ...results.value, [id]: result }
105
+ return result
106
+ } finally {
107
+ probing.value = null
108
+ }
109
+ }
110
+
111
+ return { view, results, probing, loading, available, hasSurface, load, ensureLoaded, probe }
112
+ })
@@ -0,0 +1,17 @@
1
+ // Tool server (MCP) operability shapes: what this deployment declared, and what a probe answered.
2
+ //
3
+ // All wire shapes are sourced from @cat-factory/contracts (single source of truth). The probe
4
+ // STATUS and the not-probeable REASON in particular are vocabularies both sides must agree about —
5
+ // the backend decides them, this app maps each member to translated copy plus a remedy — so a
6
+ // member added on one side only renders as a blank chip rather than failing to compile.
7
+
8
+ export type {
9
+ ToolServerAllowedToolsCheck,
10
+ ToolServerCredential,
11
+ ToolServerNotProbeableReason,
12
+ ToolServerProbeResult,
13
+ ToolServerProbeStatus,
14
+ ToolServerTransport,
15
+ ToolServerView,
16
+ ToolServersView,
17
+ } from '@cat-factory/contracts'
@@ -606,6 +606,48 @@
606
606
  "removeFailed": "Der Registry-Eintrag konnte nicht entfernt werden"
607
607
  }
608
608
  },
609
+ "toolServers": {
610
+ "heading": "Werkzeugserver (MCP)",
611
+ "intro": "Die MCP-Server, die diese Installation für ihre Agenten registriert. Ein Test löst die Zugangsdaten dieses Boards auf und spricht das Protokoll mit dem Server, das Ergebnis entspricht also dem, was ein Lauf erhält.",
612
+ "transport": {
613
+ "stdio": "Im Container",
614
+ "http": "Extern"
615
+ },
616
+ "declaredBy": "Zugewiesen an: {kinds}",
617
+ "declaredByNone": "Kein Agent erhält diesen Server, daher startet ihn kein Lauf.",
618
+ "servableHarnesses": "Läuft mit: {harnesses}",
619
+ "servableHarnessesNone": "Keine Agenten-CLI kann diesen Transport bedienen, daher greift dieser Server in keinem Lauf.",
620
+ "allowedTools": "Eingeschränkt auf: {tools}",
621
+ "credentials": "Zugangsdaten: {keys}",
622
+ "test": "Testen",
623
+ "notProbeable": {
624
+ "stdio": "Läuft im Container des Agenten und kann von hier aus nicht getestet werden.",
625
+ "containerLocal": "Lauscht neben dem Agenten in dessen eigenem Container, der von hier aus nicht erreichbar ist.",
626
+ "urlNotAllowed": "Unter dieser Adresse darf ein Werkzeugserver nicht erreicht werden (https oder einfaches http nur auf localhost)."
627
+ },
628
+ "status": {
629
+ "ok": "Hat geantwortet",
630
+ "credentialsMissing": "Keine Zugangsdaten",
631
+ "credentialRefused": "Zugangsdaten abgelehnt",
632
+ "unreachable": "Keine Antwort",
633
+ "httpError": "Anfrage abgewiesen",
634
+ "protocolError": "Kein MCP-Server",
635
+ "notProbeable": "Von hier nicht testbar"
636
+ },
637
+ "okDetail": "{name} {version}, Protokoll {protocol}, {count} Werkzeuge.",
638
+ "toolsIncomplete": "Der Server hat mehr Werkzeuge, als ein Test liest, diese Zahl ist also ein Mindestwert.",
639
+ "unmatchedTools": "Dieser Server bietet kein Werkzeug namens {tools}, dem Agenten wird also ein Werkzeug angekündigt, das er nicht aufrufen kann.",
640
+ "allowedToolsUnchecked": "Die Werkzeugliste war zu lang, um sie vollständig zu lesen, daher konnten die eingeschränkten Namen nicht geprüft werden.",
641
+ "unresolvedCredentials": "Für {keys} wurde nichts aufgelöst. Trage den Wert unten ein oder setze ihn in der Umgebung der Installation.",
642
+ "refusedCredentials": "{keys} benennt eine Variable, die zur Konfiguration der Plattform selbst gehört, und wird daher nie aufgelöst. Ändere die Deklaration im Code der Installation.",
643
+ "httpStatus": "HTTP {status}",
644
+ "showDetails": "Details anzeigen",
645
+ "hideDetails": "Details verbergen",
646
+ "toast": {
647
+ "loadFailed": "Die Werkzeugserver konnten nicht geladen werden",
648
+ "probeFailed": "Der Werkzeugserver konnte nicht getestet werden"
649
+ }
650
+ },
609
651
  "capabilityCredentials": {
610
652
  "tab": "Zugangsdaten für Fähigkeiten",
611
653
  "intro": "Die Secrets, die die Tool-Server und generativen Integrationen dieser Installation namentlich anfordern. Werte gelten nur für dieses Board, werden verschlüsselt gespeichert und direkt an den Prozess des Agenten übergeben: Sie erscheinen weder in einem Prompt noch in einem Log. Werte lassen sich nur schreiben, nie auslesen, ein gespeicherter Wert wird also durch Eingabe eines neuen ersetzt.",
@@ -3086,6 +3086,48 @@
3086
3086
  "removeFailed": "Could not remove the registry entry"
3087
3087
  }
3088
3088
  },
3089
+ "toolServers": {
3090
+ "heading": "Tool servers (MCP)",
3091
+ "intro": "The MCP servers this deployment registers for its agents. Testing one resolves this board's credentials and speaks the protocol to the server, so the result is what a run would get.",
3092
+ "transport": {
3093
+ "stdio": "In the container",
3094
+ "http": "Remote"
3095
+ },
3096
+ "declaredBy": "Given to: {kinds}",
3097
+ "declaredByNone": "No agent gets this server, so no run will ever start it.",
3098
+ "servableHarnesses": "Works on: {harnesses}",
3099
+ "servableHarnessesNone": "No agent CLI can serve this transport, so this server never applies to any run.",
3100
+ "allowedTools": "Narrowed to: {tools}",
3101
+ "credentials": "Credentials: {keys}",
3102
+ "test": "Test",
3103
+ "notProbeable": {
3104
+ "stdio": "Runs inside the agent's container, so it cannot be tested from here.",
3105
+ "containerLocal": "Listens beside the agent in its own container, which is not reachable from here.",
3106
+ "urlNotAllowed": "The address is not one a tool server may be reached at (https, or plain http on localhost)."
3107
+ },
3108
+ "status": {
3109
+ "ok": "Answered",
3110
+ "credentialsMissing": "No credential",
3111
+ "credentialRefused": "Credential refused",
3112
+ "unreachable": "No answer",
3113
+ "httpError": "Rejected the request",
3114
+ "protocolError": "Not an MCP server",
3115
+ "notProbeable": "Cannot be tested from here"
3116
+ },
3117
+ "okDetail": "{name} {version}, protocol {protocol}, {count} tools.",
3118
+ "toolsIncomplete": "The server has more tools than one test reads, so this count is a minimum.",
3119
+ "unmatchedTools": "This server exposes no tool called {tools}, so the agent is told about a tool it cannot call.",
3120
+ "allowedToolsUnchecked": "The tool list was too long to read in full, so the narrowed names could not be checked.",
3121
+ "unresolvedCredentials": "Nothing resolved for {keys}. Fill it in below, or set it in the deployment's environment.",
3122
+ "refusedCredentials": "{keys} names a variable the platform's own configuration owns, so it is never resolved. Change the declaration in the deployment's code.",
3123
+ "httpStatus": "HTTP {status}",
3124
+ "showDetails": "Show details",
3125
+ "hideDetails": "Hide details",
3126
+ "toast": {
3127
+ "loadFailed": "Could not load the tool servers",
3128
+ "probeFailed": "Could not test the tool server"
3129
+ }
3130
+ },
3089
3131
  "capabilityCredentials": {
3090
3132
  "tab": "Capability credentials",
3091
3133
  "intro": "The secrets this deployment's tool servers and generative integrations ask for by name. Values are stored for this board only, sealed at rest, and handed straight to the agent's process: they never reach a prompt or a log. Values are write-only, so a stored one is replaced by typing a new one and never read back.",
@@ -2838,6 +2838,48 @@
2838
2838
  "removeFailed": "No se pudo eliminar la entrada del registro"
2839
2839
  }
2840
2840
  },
2841
+ "toolServers": {
2842
+ "heading": "Servidores de herramientas (MCP)",
2843
+ "intro": "Los servidores MCP que esta instalación registra para sus agentes. Al probar uno se resuelven las credenciales de este tablero y se habla el protocolo con el servidor, así que el resultado es el que obtendría una ejecución.",
2844
+ "transport": {
2845
+ "stdio": "En el contenedor",
2846
+ "http": "Remoto"
2847
+ },
2848
+ "declaredBy": "Asignado a: {kinds}",
2849
+ "declaredByNone": "Ningún agente recibe este servidor, así que ninguna ejecución lo iniciará.",
2850
+ "servableHarnesses": "Funciona con: {harnesses}",
2851
+ "servableHarnessesNone": "Ninguna CLI de agente puede servir este transporte, así que este servidor nunca se aplica a una ejecución.",
2852
+ "allowedTools": "Limitado a: {tools}",
2853
+ "credentials": "Credenciales: {keys}",
2854
+ "test": "Probar",
2855
+ "notProbeable": {
2856
+ "stdio": "Se ejecuta dentro del contenedor del agente, así que no puede probarse desde aquí.",
2857
+ "containerLocal": "Escucha junto al agente en su propio contenedor, que no es accesible desde aquí.",
2858
+ "urlNotAllowed": "Esta dirección no es válida para un servidor de herramientas (https, o http simple solo en localhost)."
2859
+ },
2860
+ "status": {
2861
+ "ok": "Respondió",
2862
+ "credentialsMissing": "Sin credencial",
2863
+ "credentialRefused": "Credencial rechazada",
2864
+ "unreachable": "Sin respuesta",
2865
+ "httpError": "Rechazó la solicitud",
2866
+ "protocolError": "No es un servidor MCP",
2867
+ "notProbeable": "No se puede probar desde aquí"
2868
+ },
2869
+ "okDetail": "{name} {version}, protocolo {protocol}, {count} herramientas.",
2870
+ "toolsIncomplete": "El servidor tiene más herramientas de las que lee una prueba, así que este número es un mínimo.",
2871
+ "unmatchedTools": "Este servidor no expone ninguna herramienta llamada {tools}, así que al agente se le anuncia una herramienta que no puede invocar.",
2872
+ "allowedToolsUnchecked": "La lista de herramientas era demasiado larga para leerla completa, así que no se pudieron comprobar los nombres limitados.",
2873
+ "unresolvedCredentials": "No se resolvió nada para {keys}. Rellénalo abajo o defínelo en el entorno de la instalación.",
2874
+ "refusedCredentials": "{keys} nombra una variable que pertenece a la configuración de la propia plataforma, así que nunca se resuelve. Cambia la declaración en el código de la instalación.",
2875
+ "httpStatus": "HTTP {status}",
2876
+ "showDetails": "Ver detalles",
2877
+ "hideDetails": "Ocultar detalles",
2878
+ "toast": {
2879
+ "loadFailed": "No se pudieron cargar los servidores de herramientas",
2880
+ "probeFailed": "No se pudo probar el servidor de herramientas"
2881
+ }
2882
+ },
2841
2883
  "capabilityCredentials": {
2842
2884
  "tab": "Credenciales de capacidades",
2843
2885
  "intro": "Los secretos que los servidores de herramientas y las integraciones generativas de esta instalación piden por nombre. Los valores se guardan solo para este tablero, cifrados en reposo, y se entregan directamente al proceso del agente: nunca llegan a un prompt ni a un registro. Los valores son de solo escritura, así que uno guardado se sustituye escribiendo otro y nunca se vuelve a leer.",
@@ -2838,6 +2838,48 @@
2838
2838
  "removeFailed": "Impossible de supprimer l'entrée du registre"
2839
2839
  }
2840
2840
  },
2841
+ "toolServers": {
2842
+ "heading": "Serveurs d’outils (MCP)",
2843
+ "intro": "Les serveurs MCP que ce déploiement enregistre pour ses agents. Tester un serveur résout les identifiants de ce tableau et parle le protocole avec lui : le résultat est donc celui qu’obtiendrait une exécution.",
2844
+ "transport": {
2845
+ "stdio": "Dans le conteneur",
2846
+ "http": "Distant"
2847
+ },
2848
+ "declaredBy": "Attribué à : {kinds}",
2849
+ "declaredByNone": "Aucun agent ne reçoit ce serveur, donc aucune exécution ne le démarrera.",
2850
+ "servableHarnesses": "Fonctionne avec : {harnesses}",
2851
+ "servableHarnessesNone": "Aucune CLI d’agent ne peut servir ce transport, donc ce serveur ne s’applique à aucune exécution.",
2852
+ "allowedTools": "Restreint à : {tools}",
2853
+ "credentials": "Identifiants : {keys}",
2854
+ "test": "Tester",
2855
+ "notProbeable": {
2856
+ "stdio": "Il s’exécute dans le conteneur de l’agent et ne peut donc pas être testé d’ici.",
2857
+ "containerLocal": "Il écoute à côté de l’agent, dans son propre conteneur, qui n’est pas joignable d’ici.",
2858
+ "urlNotAllowed": "Cette adresse n’est pas autorisée pour un serveur d’outils (https, ou http simple sur localhost uniquement)."
2859
+ },
2860
+ "status": {
2861
+ "ok": "A répondu",
2862
+ "credentialsMissing": "Aucun identifiant",
2863
+ "credentialRefused": "Identifiant refusé",
2864
+ "unreachable": "Aucune réponse",
2865
+ "httpError": "Requête rejetée",
2866
+ "protocolError": "Pas un serveur MCP",
2867
+ "notProbeable": "Non testable d’ici"
2868
+ },
2869
+ "okDetail": "{name} {version}, protocole {protocol}, {count} outils.",
2870
+ "toolsIncomplete": "Le serveur expose plus d’outils qu’un test n’en lit : ce nombre est donc un minimum.",
2871
+ "unmatchedTools": "Ce serveur n’expose aucun outil nommé {tools} : l’agent se voit donc annoncer un outil qu’il ne peut pas appeler.",
2872
+ "allowedToolsUnchecked": "La liste d’outils était trop longue pour être lue en entier, les noms restreints n’ont donc pas pu être vérifiés.",
2873
+ "unresolvedCredentials": "Rien n’a été résolu pour {keys}. Renseignez la valeur ci-dessous, ou définissez-la dans l’environnement du déploiement.",
2874
+ "refusedCredentials": "{keys} désigne une variable qui appartient à la configuration de la plateforme elle-même : elle n’est jamais résolue. Modifiez la déclaration dans le code du déploiement.",
2875
+ "httpStatus": "HTTP {status}",
2876
+ "showDetails": "Afficher les détails",
2877
+ "hideDetails": "Masquer les détails",
2878
+ "toast": {
2879
+ "loadFailed": "Impossible de charger les serveurs d’outils",
2880
+ "probeFailed": "Impossible de tester le serveur d’outils"
2881
+ }
2882
+ },
2841
2883
  "capabilityCredentials": {
2842
2884
  "tab": "Identifiants des capacités",
2843
2885
  "intro": "Les secrets que les serveurs d'outils et les intégrations génératives de ce déploiement réclament par leur nom. Les valeurs ne sont enregistrées que pour ce tableau, chiffrées au repos, et remises directement au processus de l'agent : elles n'apparaissent ni dans un prompt ni dans un journal. Les valeurs sont en écriture seule, une valeur enregistrée se remplace donc en en saisissant une nouvelle et n'est jamais relue.",
@@ -2979,6 +2979,48 @@
2979
2979
  "removeFailed": "הסרת רשומת המאגר נכשלה"
2980
2980
  }
2981
2981
  },
2982
+ "toolServers": {
2983
+ "heading": "שרתי כלים (MCP)",
2984
+ "intro": "שרתי ה-MCP שהתקנה זו רושמת עבור הסוכנים שלה. בדיקה מאתרת את פרטי ההזדהות של לוח זה ומדברת עם השרת בפרוטוקול, כך שהתוצאה היא מה שהרצה תקבל.",
2985
+ "transport": {
2986
+ "stdio": "בתוך המכולה",
2987
+ "http": "מרוחק"
2988
+ },
2989
+ "declaredBy": "משויך אל: {kinds}",
2990
+ "declaredByNone": "אף סוכן אינו מקבל שרת זה, ולכן אף הרצה לא תפעיל אותו.",
2991
+ "servableHarnesses": "עובד עם: {harnesses}",
2992
+ "servableHarnessesNone": "אף CLI של סוכן אינו יכול לשרת תעבורה זו, ולכן שרת זה לא חל על שום הרצה.",
2993
+ "allowedTools": "מוגבל אל: {tools}",
2994
+ "credentials": "פרטי הזדהות: {keys}",
2995
+ "test": "בדיקה",
2996
+ "notProbeable": {
2997
+ "stdio": "רץ בתוך המכולה של הסוכן, ולכן לא ניתן לבדוק אותו מכאן.",
2998
+ "containerLocal": "מאזין לצד הסוכן במכולה שלו, שאינה נגישה מכאן.",
2999
+ "urlNotAllowed": "אין זו כתובת שבה מותר להגיע לשרת כלים (https, או http פשוט רק ב-localhost)."
3000
+ },
3001
+ "status": {
3002
+ "ok": "השיב",
3003
+ "credentialsMissing": "אין פרטי הזדהות",
3004
+ "credentialRefused": "פרטי ההזדהות נדחו",
3005
+ "unreachable": "אין תשובה",
3006
+ "httpError": "דחה את הבקשה",
3007
+ "protocolError": "אינו שרת MCP",
3008
+ "notProbeable": "לא ניתן לבדוק מכאן"
3009
+ },
3010
+ "okDetail": "{name} {version}, פרוטוקול {protocol}, {count} כלים.",
3011
+ "toolsIncomplete": "לשרת יש יותר כלים ממה שבדיקה אחת קוראת, ולכן מספר זה הוא מינימום.",
3012
+ "unmatchedTools": "שרת זה אינו חושף כלי בשם {tools}, ולכן מסופר לסוכן על כלי שאינו יכול לקרוא לו.",
3013
+ "allowedToolsUnchecked": "רשימת הכלים הייתה ארוכה מכדי לקרוא אותה במלואה, ולכן לא ניתן היה לבדוק את השמות המוגבלים.",
3014
+ "unresolvedCredentials": "לא אותר דבר עבור {keys}. מלאו את הערך למטה, או הגדירו אותו בסביבת ההתקנה.",
3015
+ "refusedCredentials": "{keys} מציין משתנה שהוא חלק מהתצורה של הפלטפורמה עצמה, ולכן הוא לעולם אינו מאותר. שנו את ההצהרה בקוד ההתקנה.",
3016
+ "httpStatus": "HTTP {status}",
3017
+ "showDetails": "הצגת פרטים",
3018
+ "hideDetails": "הסתרת פרטים",
3019
+ "toast": {
3020
+ "loadFailed": "לא ניתן לטעון את שרתי הכלים",
3021
+ "probeFailed": "לא ניתן לבדוק את שרת הכלים"
3022
+ }
3023
+ },
2982
3024
  "capabilityCredentials": {
2983
3025
  "tab": "אישורי גישה ליכולות",
2984
3026
  "intro": "הסודות ששרתי הכלים והאינטגרציות הגנרטיביות של הפריסה הזו מבקשים לפי שם. הערכים נשמרים ללוח הזה בלבד, מוצפנים במנוחה ומועברים ישירות לתהליך של הסוכן: הם לעולם לא מגיעים להנחיה או ליומן. הערכים ניתנים לכתיבה בלבד, ולכן ערך שמור מוחלף בהקלדת ערך חדש ולעולם אינו נקרא בחזרה.",
@@ -606,6 +606,48 @@
606
606
  "removeFailed": "Impossibile rimuovere la voce del registry"
607
607
  }
608
608
  },
609
+ "toolServers": {
610
+ "heading": "Server di strumenti (MCP)",
611
+ "intro": "I server MCP che questa installazione registra per i suoi agenti. Provarne uno risolve le credenziali di questa bacheca e parla il protocollo con il server, quindi il risultato è quello che otterrebbe un’esecuzione.",
612
+ "transport": {
613
+ "stdio": "Nel container",
614
+ "http": "Remoto"
615
+ },
616
+ "declaredBy": "Assegnato a: {kinds}",
617
+ "declaredByNone": "Nessun agente riceve questo server, quindi nessuna esecuzione lo avvierà.",
618
+ "servableHarnesses": "Funziona con: {harnesses}",
619
+ "servableHarnessesNone": "Nessuna CLI di agente può servire questo trasporto, quindi questo server non si applica a nessuna esecuzione.",
620
+ "allowedTools": "Limitato a: {tools}",
621
+ "credentials": "Credenziali: {keys}",
622
+ "test": "Prova",
623
+ "notProbeable": {
624
+ "stdio": "Gira dentro il container dell’agente, quindi non può essere provato da qui.",
625
+ "containerLocal": "Ascolta accanto all’agente nel suo container, che non è raggiungibile da qui.",
626
+ "urlNotAllowed": "Questo indirizzo non è consentito per un server di strumenti (https, oppure http semplice solo su localhost)."
627
+ },
628
+ "status": {
629
+ "ok": "Ha risposto",
630
+ "credentialsMissing": "Nessuna credenziale",
631
+ "credentialRefused": "Credenziale rifiutata",
632
+ "unreachable": "Nessuna risposta",
633
+ "httpError": "Richiesta respinta",
634
+ "protocolError": "Non è un server MCP",
635
+ "notProbeable": "Non provabile da qui"
636
+ },
637
+ "okDetail": "{name} {version}, protocollo {protocol}, {count} strumenti.",
638
+ "toolsIncomplete": "Il server ha più strumenti di quanti una prova ne legga, quindi questo numero è un minimo.",
639
+ "unmatchedTools": "Questo server non espone alcuno strumento chiamato {tools}, quindi all’agente viene annunciato uno strumento che non può invocare.",
640
+ "allowedToolsUnchecked": "L’elenco degli strumenti era troppo lungo per leggerlo tutto, quindi i nomi limitati non hanno potuto essere verificati.",
641
+ "unresolvedCredentials": "Nulla è stato risolto per {keys}. Inseriscilo qui sotto, oppure impostalo nell’ambiente dell’installazione.",
642
+ "refusedCredentials": "{keys} nomina una variabile che appartiene alla configurazione della piattaforma stessa, quindi non viene mai risolta. Modifica la dichiarazione nel codice dell’installazione.",
643
+ "httpStatus": "HTTP {status}",
644
+ "showDetails": "Mostra dettagli",
645
+ "hideDetails": "Nascondi dettagli",
646
+ "toast": {
647
+ "loadFailed": "Impossibile caricare i server di strumenti",
648
+ "probeFailed": "Impossibile provare il server di strumenti"
649
+ }
650
+ },
609
651
  "capabilityCredentials": {
610
652
  "tab": "Credenziali delle capacità",
611
653
  "intro": "I segreti che i server di strumenti e le integrazioni generative di questo deployment richiedono per nome. I valori vengono salvati solo per questa board, cifrati a riposo, e consegnati direttamente al processo dell'agente: non finiscono mai in un prompt né in un log. I valori sono di sola scrittura, quindi uno salvato si sostituisce digitandone uno nuovo e non viene mai riletto.",
@@ -2979,6 +2979,48 @@
2979
2979
  "removeFailed": "レジストリエントリを削除できませんでした"
2980
2980
  }
2981
2981
  },
2982
+ "toolServers": {
2983
+ "heading": "ツールサーバー (MCP)",
2984
+ "intro": "このデプロイがエージェント向けに登録している MCP サーバーです。テストするとこのボードの認証情報を解決してサーバーとプロトコルで通信するため、結果は実行時に得られるものと同じです。",
2985
+ "transport": {
2986
+ "stdio": "コンテナ内",
2987
+ "http": "リモート"
2988
+ },
2989
+ "declaredBy": "割り当て先: {kinds}",
2990
+ "declaredByNone": "どのエージェントにも渡されていないため、どの実行でも起動されません。",
2991
+ "servableHarnesses": "対応 CLI: {harnesses}",
2992
+ "servableHarnessesNone": "このトランスポートを扱えるエージェント CLI がないため、このサーバーはどの実行にも適用されません。",
2993
+ "allowedTools": "許可されたツール: {tools}",
2994
+ "credentials": "認証情報: {keys}",
2995
+ "test": "テスト",
2996
+ "notProbeable": {
2997
+ "stdio": "エージェントのコンテナ内で動作するため、ここからはテストできません。",
2998
+ "containerLocal": "エージェントと同じコンテナ内で待ち受けているため、ここからは到達できません。",
2999
+ "urlNotAllowed": "ツールサーバーの接続先として許可されていないアドレスです (https、または localhost の平文 http のみ)。"
3000
+ },
3001
+ "status": {
3002
+ "ok": "応答あり",
3003
+ "credentialsMissing": "認証情報なし",
3004
+ "credentialRefused": "認証情報を拒否",
3005
+ "unreachable": "応答なし",
3006
+ "httpError": "リクエストを拒否",
3007
+ "protocolError": "MCP サーバーではない",
3008
+ "notProbeable": "ここからテストできない"
3009
+ },
3010
+ "okDetail": "{name} {version}、プロトコル {protocol}、ツール {count} 個。",
3011
+ "toolsIncomplete": "サーバーは 1 回のテストで読み取れる数を超えるツールを持つため、この数は最小値です。",
3012
+ "unmatchedTools": "このサーバーに {tools} というツールは存在しないため、エージェントは呼び出せないツールを知らされています。",
3013
+ "allowedToolsUnchecked": "ツール一覧が長すぎて全部を読み取れなかったため、絞り込んだ名前は確認できませんでした。",
3014
+ "unresolvedCredentials": "{keys} の値が解決できませんでした。下で入力するか、デプロイの環境変数に設定してください。",
3015
+ "refusedCredentials": "{keys} はプラットフォーム自身の設定に属する変数名なので、決して解決されません。デプロイのコード側の宣言を変更してください。",
3016
+ "httpStatus": "HTTP {status}",
3017
+ "showDetails": "詳細を表示",
3018
+ "hideDetails": "詳細を隠す",
3019
+ "toast": {
3020
+ "loadFailed": "ツールサーバーを読み込めませんでした",
3021
+ "probeFailed": "ツールサーバーをテストできませんでした"
3022
+ }
3023
+ },
2982
3024
  "capabilityCredentials": {
2983
3025
  "tab": "機能の認証情報",
2984
3026
  "intro": "このデプロイのツールサーバーと生成系インテグレーションが名前で要求するシークレットです。値はこのボードにのみ保存され、保管時は暗号化され、エージェントのプロセスへ直接渡されます。プロンプトにもログにも現れません。値は書き込み専用なので、保存済みの値は新しい値を入力して置き換えるだけで、読み出すことはできません。",
@@ -2838,6 +2838,48 @@
2838
2838
  "removeFailed": "Nie udało się usunąć wpisu rejestru"
2839
2839
  }
2840
2840
  },
2841
+ "toolServers": {
2842
+ "heading": "Serwery narzędzi (MCP)",
2843
+ "intro": "Serwery MCP, które ta instalacja rejestruje dla swoich agentów. Test rozwiązuje dane uwierzytelniające tej tablicy i rozmawia z serwerem protokołem, więc wynik jest taki, jaki otrzymałby przebieg.",
2844
+ "transport": {
2845
+ "stdio": "W kontenerze",
2846
+ "http": "Zdalny"
2847
+ },
2848
+ "declaredBy": "Przypisany do: {kinds}",
2849
+ "declaredByNone": "Żaden agent nie otrzymuje tego serwera, więc żaden przebieg go nie uruchomi.",
2850
+ "servableHarnesses": "Działa z: {harnesses}",
2851
+ "servableHarnessesNone": "Żadne CLI agenta nie obsługuje tego transportu, więc ten serwer nie dotyczy żadnego przebiegu.",
2852
+ "allowedTools": "Zawężone do: {tools}",
2853
+ "credentials": "Dane uwierzytelniające: {keys}",
2854
+ "test": "Testuj",
2855
+ "notProbeable": {
2856
+ "stdio": "Działa w kontenerze agenta, więc nie można go przetestować z tego miejsca.",
2857
+ "containerLocal": "Nasłuchuje obok agenta w jego własnym kontenerze, który jest tu nieosiągalny.",
2858
+ "urlNotAllowed": "Pod tym adresem serwer narzędzi nie może być osiągany (https albo zwykłe http tylko na localhost)."
2859
+ },
2860
+ "status": {
2861
+ "ok": "Odpowiedział",
2862
+ "credentialsMissing": "Brak danych uwierzytelniających",
2863
+ "credentialRefused": "Dane uwierzytelniające odrzucone",
2864
+ "unreachable": "Brak odpowiedzi",
2865
+ "httpError": "Odrzucił żądanie",
2866
+ "protocolError": "To nie serwer MCP",
2867
+ "notProbeable": "Nie można testować z tego miejsca"
2868
+ },
2869
+ "okDetail": "{name} {version}, protokół {protocol}, {count} narzędzi.",
2870
+ "toolsIncomplete": "Serwer ma więcej narzędzi, niż jeden test odczytuje, więc ta liczba to minimum.",
2871
+ "unmatchedTools": "Ten serwer nie udostępnia narzędzia o nazwie {tools}, więc agent dowiaduje się o narzędziu, którego nie może wywołać.",
2872
+ "allowedToolsUnchecked": "Lista narzędzi była zbyt długa, by odczytać ją w całości, więc zawężonych nazw nie dało się sprawdzić.",
2873
+ "unresolvedCredentials": "Nic nie rozwiązano dla {keys}. Wpisz wartość poniżej albo ustaw ją w środowisku instalacji.",
2874
+ "refusedCredentials": "{keys} nazywa zmienną należącą do konfiguracji samej platformy, więc nigdy nie jest rozwiązywana. Zmień deklarację w kodzie instalacji.",
2875
+ "httpStatus": "HTTP {status}",
2876
+ "showDetails": "Pokaż szczegóły",
2877
+ "hideDetails": "Ukryj szczegóły",
2878
+ "toast": {
2879
+ "loadFailed": "Nie udało się wczytać serwerów narzędzi",
2880
+ "probeFailed": "Nie udało się przetestować serwera narzędzi"
2881
+ }
2882
+ },
2841
2883
  "capabilityCredentials": {
2842
2884
  "tab": "Poświadczenia funkcji",
2843
2885
  "intro": "Sekrety, o które serwery narzędzi i integracje generatywne tego wdrożenia proszą po nazwie. Wartości są zapisywane tylko dla tej tablicy, szyfrowane w spoczynku i przekazywane wprost do procesu agenta: nigdy nie trafiają do promptu ani do logu. Wartości można tylko zapisywać, więc zapisaną zastępuje się, wpisując nową, i nigdy nie jest odczytywana.",
@@ -2979,6 +2979,48 @@
2979
2979
  "removeFailed": "Kayıt defteri girdisi kaldırılamadı"
2980
2980
  }
2981
2981
  },
2982
+ "toolServers": {
2983
+ "heading": "Araç sunucuları (MCP)",
2984
+ "intro": "Bu kurulumun ajanları için kaydettiği MCP sunucuları. Bir sunucuyu test etmek bu panonun kimlik bilgilerini çözer ve sunucuyla protokol konuşur; sonuç, bir çalıştırmanın alacağı sonuçtur.",
2985
+ "transport": {
2986
+ "stdio": "Konteyner içinde",
2987
+ "http": "Uzak"
2988
+ },
2989
+ "declaredBy": "Şu ajanlara verili: {kinds}",
2990
+ "declaredByNone": "Hiçbir ajan bu sunucuyu almıyor, dolayısıyla hiçbir çalıştırma onu başlatmaz.",
2991
+ "servableHarnesses": "Şunlarla çalışır: {harnesses}",
2992
+ "servableHarnessesNone": "Hiçbir ajan CLI’si bu taşımayı sunamaz, dolayısıyla bu sunucu hiçbir çalıştırmada geçerli olmaz.",
2993
+ "allowedTools": "Şunlarla sınırlı: {tools}",
2994
+ "credentials": "Kimlik bilgileri: {keys}",
2995
+ "test": "Test et",
2996
+ "notProbeable": {
2997
+ "stdio": "Ajanın konteynerinin içinde çalışır, bu yüzden buradan test edilemez.",
2998
+ "containerLocal": "Ajanın yanında, onun kendi konteynerinde dinliyor; buradan erişilemez.",
2999
+ "urlNotAllowed": "Bu adres bir araç sunucusuna erişim için uygun değil (https ya da yalnızca localhost üzerinde düz http)."
3000
+ },
3001
+ "status": {
3002
+ "ok": "Yanıt verdi",
3003
+ "credentialsMissing": "Kimlik bilgisi yok",
3004
+ "credentialRefused": "Kimlik bilgisi reddedildi",
3005
+ "unreachable": "Yanıt yok",
3006
+ "httpError": "İsteği reddetti",
3007
+ "protocolError": "MCP sunucusu değil",
3008
+ "notProbeable": "Buradan test edilemez"
3009
+ },
3010
+ "okDetail": "{name} {version}, protokol {protocol}, {count} araç.",
3011
+ "toolsIncomplete": "Sunucunun araç sayısı tek bir testin okuduğundan fazla, bu yüzden bu sayı bir alt sınırdır.",
3012
+ "unmatchedTools": "Bu sunucu {tools} adlı bir araç sunmuyor; dolayısıyla ajana çağıramayacağı bir araç bildiriliyor.",
3013
+ "allowedToolsUnchecked": "Araç listesi tümüyle okunacak kadar kısa değildi, bu yüzden sınırlanan adlar denetlenemedi.",
3014
+ "unresolvedCredentials": "{keys} için hiçbir değer çözülmedi. Aşağıda doldurun ya da kurulumun ortamında tanımlayın.",
3015
+ "refusedCredentials": "{keys}, platformun kendi yapılandırmasına ait bir değişkeni adlandırıyor, bu yüzden asla çözülmez. Kurulumun kodundaki bildirimi değiştirin.",
3016
+ "httpStatus": "HTTP {status}",
3017
+ "showDetails": "Ayrıntıları göster",
3018
+ "hideDetails": "Ayrıntıları gizle",
3019
+ "toast": {
3020
+ "loadFailed": "Araç sunucuları yüklenemedi",
3021
+ "probeFailed": "Araç sunucusu test edilemedi"
3022
+ }
3023
+ },
2982
3024
  "capabilityCredentials": {
2983
3025
  "tab": "Yetenek kimlik bilgileri",
2984
3026
  "intro": "Bu kurulumdaki araç sunucularının ve üretken entegrasyonların adıyla istediği sırlar. Değerler yalnızca bu pano için saklanır, beklerken şifrelenir ve doğrudan ajanın sürecine verilir: ne bir isteme ne de bir günlüğe düşer. Değerler yalnızca yazılabilir, dolayısıyla saklanan bir değer yenisi yazılarak değiştirilir ve hiçbir zaman geri okunmaz.",
@@ -2838,6 +2838,48 @@
2838
2838
  "removeFailed": "Не вдалося видалити запис реєстру"
2839
2839
  }
2840
2840
  },
2841
+ "toolServers": {
2842
+ "heading": "Сервери інструментів (MCP)",
2843
+ "intro": "Сервери MCP, які ця інсталяція реєструє для своїх агентів. Перевірка розв’язує облікові дані цієї дошки і спілкується із сервером за протоколом, тож результат такий самий, який отримає запуск.",
2844
+ "transport": {
2845
+ "stdio": "У контейнері",
2846
+ "http": "Віддалений"
2847
+ },
2848
+ "declaredBy": "Призначено: {kinds}",
2849
+ "declaredByNone": "Жоден агент не отримує цей сервер, тож жоден запуск його не запустить.",
2850
+ "servableHarnesses": "Працює з: {harnesses}",
2851
+ "servableHarnessesNone": "Жоден CLI агента не може обслуговувати цей транспорт, тож цей сервер не застосовується ні до якого запуску.",
2852
+ "allowedTools": "Звужено до: {tools}",
2853
+ "credentials": "Облікові дані: {keys}",
2854
+ "test": "Перевірити",
2855
+ "notProbeable": {
2856
+ "stdio": "Працює всередині контейнера агента, тож звідси його перевірити неможливо.",
2857
+ "containerLocal": "Слухає поруч з агентом у його власному контейнері, який звідси недосяжний.",
2858
+ "urlNotAllowed": "За цією адресою сервер інструментів не може бути досяжним (https або звичайний http лише на localhost)."
2859
+ },
2860
+ "status": {
2861
+ "ok": "Відповів",
2862
+ "credentialsMissing": "Немає облікових даних",
2863
+ "credentialRefused": "Облікові дані відхилено",
2864
+ "unreachable": "Немає відповіді",
2865
+ "httpError": "Відхилив запит",
2866
+ "protocolError": "Це не сервер MCP",
2867
+ "notProbeable": "Звідси перевірити неможливо"
2868
+ },
2869
+ "okDetail": "{name} {version}, протокол {protocol}, інструментів: {count}.",
2870
+ "toolsIncomplete": "Сервер має більше інструментів, ніж читає одна перевірка, тож це число є мінімумом.",
2871
+ "unmatchedTools": "Цей сервер не надає інструмента з назвою {tools}, тож агентові повідомляють про інструмент, який він не може викликати.",
2872
+ "allowedToolsUnchecked": "Список інструментів був завеликий, щоб прочитати його повністю, тож звужені назви перевірити не вдалося.",
2873
+ "unresolvedCredentials": "Для {keys} нічого не розв’язано. Введіть значення нижче або задайте його в середовищі інсталяції.",
2874
+ "refusedCredentials": "{keys} називає змінну, що належить власній конфігурації платформи, тож вона ніколи не розв’язується. Змініть оголошення в коді інсталяції.",
2875
+ "httpStatus": "HTTP {status}",
2876
+ "showDetails": "Показати деталі",
2877
+ "hideDetails": "Сховати деталі",
2878
+ "toast": {
2879
+ "loadFailed": "Не вдалося завантажити сервери інструментів",
2880
+ "probeFailed": "Не вдалося перевірити сервер інструментів"
2881
+ }
2882
+ },
2841
2883
  "capabilityCredentials": {
2842
2884
  "tab": "Облікові дані можливостей",
2843
2885
  "intro": "Секрети, які сервери інструментів і генеративні інтеграції цього розгортання запитують за іменем. Значення зберігаються лише для цієї дошки, зашифрованими, і передаються просто в процес агента: вони ніколи не потрапляють ані в підказку, ані в журнал. Значення доступні лише для запису, тож збережене замінюють, ввівши нове, і ніколи не читають назад.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.222.0",
3
+ "version": "0.223.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.238.0"
43
+ "@cat-factory/contracts": "0.239.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",