@decocms/mesh-sdk 1.2.1 → 1.2.3

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.
@@ -8,20 +8,7 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  /**
11
- * Tool selection mode schema
12
- * - "inclusion": Include selected tools/connections (default behavior)
13
- * - "exclusion": Exclude selected tools/connections (inverse filter)
14
- */
15
- const ToolSelectionModeSchema = z
16
- .enum(["inclusion", "exclusion"])
17
- .describe(
18
- "Tool selection mode: 'inclusion' = include selected (default), 'exclusion' = exclude selected",
19
- );
20
-
21
- export type ToolSelectionMode = z.infer<typeof ToolSelectionModeSchema>;
22
-
23
- /**
24
- * Virtual MCP connection schema - defines which connection and tools/resources/prompts are included/excluded
11
+ * Virtual MCP connection schema - defines which connection and tools/resources/prompts are included
25
12
  */
26
13
  const VirtualMCPConnectionSchema = z.object({
27
14
  connection_id: z.string().describe("Connection ID"),
@@ -29,68 +16,496 @@ const VirtualMCPConnectionSchema = z.object({
29
16
  .array(z.string())
30
17
  .nullable()
31
18
  .describe(
32
- "Selected tool names. With 'inclusion' mode: null = all tools included. With 'exclusion' mode: null = entire connection excluded",
19
+ "Selected tool names. null = all tools included, array = only these tools included",
33
20
  ),
34
21
  selected_resources: z
35
22
  .array(z.string())
36
23
  .nullable()
37
24
  .describe(
38
- "Selected resource URIs or patterns. Supports * and ** wildcards for pattern matching. With 'inclusion' mode: null = all resources included.",
25
+ "Selected resource URIs or patterns. Supports * and ** wildcards for pattern matching. null = all resources included, array = only these resources included",
39
26
  ),
40
27
  selected_prompts: z
41
28
  .array(z.string())
42
29
  .nullable()
43
30
  .describe(
44
- "Selected prompt names. With 'inclusion' mode: null = all prompts included. With 'exclusion' mode: null = entire connection excluded.",
31
+ "Selected prompt names. null = all prompts included, array = only these prompts included",
45
32
  ),
46
33
  });
47
34
 
48
35
  export type VirtualMCPConnection = z.infer<typeof VirtualMCPConnectionSchema>;
49
36
 
50
37
  /**
51
- * Virtual MCP entity schema - single source of truth
52
- * Compliant with collections binding pattern
38
+ * Virtual MCP connection schema for input (Create/Update) - fields can be optional
53
39
  */
54
- export const VirtualMCPEntitySchema = z.object({
55
- // Base collection entity fields
56
- id: z.string().describe("Unique identifier for the virtual MCP"),
57
- title: z.string().describe("Human-readable name for the virtual MCP"),
58
- description: z.string().nullable().describe("Description of the virtual MCP"),
59
- icon: z
40
+ const VirtualMCPConnectionInputSchema = VirtualMCPConnectionSchema.extend({
41
+ selected_tools: VirtualMCPConnectionSchema.shape.selected_tools.optional(),
42
+ selected_resources:
43
+ VirtualMCPConnectionSchema.shape.selected_resources.optional(),
44
+ selected_prompts:
45
+ VirtualMCPConnectionSchema.shape.selected_prompts.optional(),
46
+ });
47
+
48
+ /**
49
+ * Pinned view schema - a tool view pinned to a virtual MCP
50
+ */
51
+ const VirtualMcpPinnedViewSchema = z.object({
52
+ connectionId: z.string(),
53
+ toolName: z.string(),
54
+ label: z.string(),
55
+ icon: z.string().nullable().optional(),
56
+ });
57
+
58
+ export type VirtualMcpPinnedView = z.infer<typeof VirtualMcpPinnedViewSchema>;
59
+
60
+ /**
61
+ * A single tab declared by an agent in `metadata.ui.layout.tabs`. Rendered
62
+ * after the fixed system tabs (Instructions / Connections / Layout / Env)
63
+ * in the unified chat layout's right panel.
64
+ */
65
+ export const VirtualMcpUILayoutTabSchema = z.object({
66
+ id: z.string().describe("Stable id; used as React key and ?tab= value"),
67
+ title: z.string().describe("Tab label"),
68
+ icon: z.string().optional().describe("Optional lucide icon name"),
69
+ view: z.object({
70
+ type: z.literal("ext-app"),
71
+ appId: z.string(),
72
+ args: z.record(z.string(), z.unknown()).optional(),
73
+ }),
74
+ });
75
+
76
+ export type VirtualMcpUILayoutTab = z.infer<typeof VirtualMcpUILayoutTabSchema>;
77
+
78
+ /**
79
+ * Layout-specific settings stored under `metadata.ui.layout`. Controls which
80
+ * main view opens by default and which additional right-panel tabs are
81
+ * permanently available for the agent.
82
+ */
83
+ export const VirtualMcpUILayoutSchema = z.object({
84
+ defaultMainView: z
85
+ .object({
86
+ type: z.string(),
87
+ id: z.string().optional(),
88
+ toolName: z.string().optional(),
89
+ })
90
+ .nullable()
91
+ .optional(),
92
+ /**
93
+ * When true, the chat panel is open alongside the main view on first
94
+ * load. Ignored when `defaultMainView.type === "chat"` (chat is always
95
+ * open in that case). Absent / null / false → chat is closed unless the
96
+ * default view is chat.
97
+ */
98
+ chatDefaultOpen: z.boolean().nullable().optional(),
99
+ tabs: z.array(VirtualMcpUILayoutTabSchema).optional(),
100
+ });
101
+
102
+ export type VirtualMcpUILayout = z.infer<typeof VirtualMcpUILayoutSchema>;
103
+
104
+ /**
105
+ * Tile UI declared by a home agent. When present, the `/$org` home page
106
+ * renders the resource as an iframe inside the agent's tile (same MCP UI
107
+ * iframe pattern used for tool results in chat).
108
+ *
109
+ * The resource lives on a specific underlying connection — not the virtual
110
+ * MCP gateway — so we store the source `connectionId` here. The host opens
111
+ * an MCP client to that connection directly so tool calls from inside the
112
+ * iframe hit bare tool names (the gateway would otherwise reject calls that
113
+ * don't carry its namespace prefix).
114
+ */
115
+ const VirtualMcpHomeTileSchema = z.object({
116
+ /**
117
+ * Optional for backward compatibility with tiles saved before this field
118
+ * existed. The home API drops tiles that don't carry a connectionId (they
119
+ * can't render correctly without it), but parsing must still succeed so
120
+ * existing virtual MCPs don't fail output validation on COLLECTION_*_LIST.
121
+ */
122
+ connectionId: z
123
+ .string()
124
+ .optional()
125
+ .describe(
126
+ "Connection that owns the resource — the host opens a direct MCP client to this connection so the iframe can call tools by their bare names.",
127
+ ),
128
+ resourceUri: z
129
+ .string()
130
+ .describe(
131
+ "ui:// resource URI exposed by `connectionId`. Read on the home page and rendered via MCPAppRenderer.",
132
+ ),
133
+ minHeight: z.number().int().positive().optional(),
134
+ maxHeight: z.number().int().positive().optional(),
135
+ });
136
+
137
+ export type VirtualMcpHomeTile = z.infer<typeof VirtualMcpHomeTileSchema>;
138
+
139
+ /**
140
+ * Virtual MCP UI customization schema
141
+ */
142
+ const VirtualMcpUISchema = z.object({
143
+ banner: z.string().nullable().optional(),
144
+ bannerColor: z.string().nullable().optional(),
145
+ icon: z.string().nullable().optional(),
146
+ themeColor: z.string().nullable().optional(),
147
+ pinnedViews: z.array(VirtualMcpPinnedViewSchema).nullable().optional(),
148
+ layout: VirtualMcpUILayoutSchema.nullable().optional(),
149
+ /**
150
+ * Legacy single-tile slot. Still honored by the home-next-actions
151
+ * endpoint when `homeTiles` is empty/absent. New writes go to
152
+ * `homeTiles` so agents can surface more than one UI on the home
153
+ * board.
154
+ */
155
+ homeTile: VirtualMcpHomeTileSchema.nullable().optional(),
156
+ /**
157
+ * Multiple home tiles per agent. Each entry becomes its own tile on
158
+ * the org home board, rendered via MCPAppRenderer against the
159
+ * resource's owning connection.
160
+ */
161
+ homeTiles: z.array(VirtualMcpHomeTileSchema).nullable().optional(),
162
+ /**
163
+ * Curated list of prompt names to surface on the home board. When
164
+ * absent / null, the BE falls back to listing every prompt the
165
+ * agent's gateway exposes (today's behavior). An explicit empty
166
+ * array means "no prompts" — useful for an agent that only wants to
167
+ * surface its UI tiles.
168
+ */
169
+ homePrompts: z.array(z.string()).nullable().optional(),
170
+ });
171
+
172
+ export type VirtualMcpUI = z.infer<typeof VirtualMcpUISchema>;
173
+
174
+ /**
175
+ * Canonical reader for an agent's pinned home tiles. Prefers the
176
+ * `homeTiles` array; falls back to the legacy single `homeTile` slot so
177
+ * agents written before the multi-tile migration still surface their
178
+ * tile. Returning `[]` (rather than null) keeps callers branchless.
179
+ */
180
+ export function getHomeTiles(
181
+ ui: VirtualMcpUI | null | undefined,
182
+ ): VirtualMcpHomeTile[] {
183
+ const arr = ui?.homeTiles;
184
+ if (Array.isArray(arr) && arr.length > 0) return arr;
185
+ const legacy = ui?.homeTile;
186
+ return legacy ? [legacy] : [];
187
+ }
188
+
189
+ /**
190
+ * Shell-portable env var name: must start with a letter or underscore and
191
+ * contain only letters, digits, and underscores. Same shape parse-dotenv
192
+ * enforces on imports. Single source of truth — the form editor and the
193
+ * paste flow both validate against this.
194
+ */
195
+ export const ENV_VAR_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
196
+
197
+ const envVarKey = z.string().min(1).regex(ENV_VAR_KEY_RE, {
198
+ message:
199
+ "Env var key must start with a letter or underscore and contain only letters, digits, and underscores.",
200
+ });
201
+
202
+ /**
203
+ * One env var declaration on a virtual MCP. Literal values live inline in
204
+ * metadata; secret values store a stable secretId that mesh resolves against
205
+ * the credential vault on every SANDBOX_START. The env var KEY is independent of
206
+ * the secret's NAME — a single secret can back multiple env keys across
207
+ * different agents.
208
+ */
209
+ const RuntimeEnvEntrySchema = z.discriminatedUnion("kind", [
210
+ z.object({
211
+ key: envVarKey,
212
+ kind: z.literal("literal"),
213
+ value: z.string(),
214
+ }),
215
+ z.object({
216
+ key: envVarKey,
217
+ kind: z.literal("secret"),
218
+ secretId: z.string().min(1),
219
+ }),
220
+ ]);
221
+
222
+ export type RuntimeEnvEntry = z.infer<typeof RuntimeEnvEntrySchema>;
223
+
224
+ /**
225
+ * User-pinned runtime configuration stored under `metadata.runtime`. Empty
226
+ * fields fall back to autodetect on the next SANDBOX_START.
227
+ */
228
+ const RuntimeMetadataSchema = z.object({
229
+ selected: z
60
230
  .string()
61
231
  .nullable()
62
232
  .optional()
63
- .describe("Icon URL for the virtual MCP"),
64
- created_at: z.string().describe("When the virtual MCP was created"),
65
- updated_at: z.string().describe("When the virtual MCP was last updated"),
66
- created_by: z.string().describe("User ID who created the virtual MCP"),
67
- updated_by: z
233
+ .describe(
234
+ "User-selected package manager (npm | pnpm | yarn | bun | deno). Null/absent means autodetect on next SANDBOX_START.",
235
+ ),
236
+ port: z
237
+ .string()
238
+ .nullable()
239
+ .optional()
240
+ .describe(
241
+ "User-selected dev server port as a string (allows '' / null for unset). Null/absent means autodetect.",
242
+ ),
243
+ path: z
244
+ .string()
245
+ .nullable()
246
+ .optional()
247
+ .describe(
248
+ "Optional path (relative to repo root) to the directory containing package.json. Null/absent means repo root. Forwarded as `application.packageManager.path` to the daemon config.",
249
+ ),
250
+ env: z
251
+ .array(RuntimeEnvEntrySchema)
252
+ .nullable()
253
+ .optional()
254
+ .describe(
255
+ "Env vars injected on every SANDBOX_START. Literal entries inline their value; secret entries store a secretId that mesh resolves via the credential vault before posting /_sandbox/config.",
256
+ ),
257
+ });
258
+
259
+ export type RuntimeMetadata = z.infer<typeof RuntimeMetadataSchema>;
260
+
261
+ /**
262
+ * GitHub repository linked to a virtual MCP
263
+ */
264
+ const GithubRepoSchema = z.object({
265
+ url: z.string().describe("GitHub repository URL"),
266
+ owner: z.string().describe("Repository owner"),
267
+ name: z.string().describe("Repository name"),
268
+ installationId: z
269
+ .number()
270
+ .optional()
271
+ .describe(
272
+ "GitHub App installation ID. Absent when the repo was linked without a GitHub connection (public-clone mode).",
273
+ ),
274
+ connectionId: z
68
275
  .string()
69
276
  .optional()
70
- .describe("User ID who last updated the virtual MCP"),
277
+ .describe(
278
+ "ID of the mcp-github connection used for authentication. Absent for public repos cloned without credentials.",
279
+ ),
280
+ });
71
281
 
72
- // Virtual MCP-specific fields
73
- organization_id: z
282
+ export type GithubRepo = z.infer<typeof GithubRepoSchema>;
283
+
284
+ /**
285
+ * A single sandbox record in the per-(user, branch, kind) sandbox map — the
286
+ * provider-issued handle plus the preview URL the UI renders.
287
+ *
288
+ * `sandboxProviderKind` lets the UI construct daemon URLs correctly:
289
+ * - cluster: daemon is reached via the mesh proxy; preview URL is the
290
+ * per-claim HTTPRoute host (in-cluster) or a local port-forward (kind dev).
291
+ * - user-desktop: daemon is reached directly via the user's link binary.
292
+ *
293
+ * `previewUrl` is nullable: blank / tool sandboxes (no `workload`, no dev
294
+ * server) have nothing to render. UI code MUST check before constructing
295
+ * an iframe URL.
296
+ */
297
+ export const SandboxRecordSchema = z.object({
298
+ sandboxHandle: z.string().describe("Provider-specific handle"),
299
+ previewUrl: z
300
+ .string()
301
+ .nullable()
302
+ .describe(
303
+ "URL where the sandbox's iframe-proxied UI is served, or null when the sandbox has no dev server (blank / tool sandboxes).",
304
+ ),
305
+ sandboxApiUrl: z
74
306
  .string()
75
- .describe("Organization ID this virtual MCP belongs to"),
76
- tool_selection_mode: ToolSelectionModeSchema.describe(
77
- "Tool selection mode: 'inclusion' = include selected, 'exclusion' = exclude selected",
307
+ .nullable()
308
+ .optional()
309
+ .describe(
310
+ "Daemon's public URL — what cluster→daemon RPCs target. Equal to previewUrl for user-desktop; null/absent for the cluster provider (routes through cluster ingress).",
311
+ ),
312
+ sandboxProviderKind: z
313
+ // Canonical set. Migration 092 rewrote every persisted legacy value
314
+ // ("docker", "agent-sandbox", "desktop", "remote-user", "host",
315
+ // "freestyle") to a canonical kind, and migration 097 dropped the
316
+ // retired "local-docker" kind; readers no longer accept those strings —
317
+ // Zod will reject them at parse time.
318
+ .enum(["cluster", "user-desktop"])
319
+ .optional(),
320
+ createdAt: z
321
+ .number()
322
+ .optional()
323
+ .describe(
324
+ "Epoch ms the entry was first written by SANDBOX_START. Used by the booting overlay to show a stable elapsed timer that survives browser reloads. Optional for backward compatibility with entries written before this field existed.",
325
+ ),
326
+ startedWith: z
327
+ .object({
328
+ packageManager: z
329
+ .string()
330
+ .nullable()
331
+ .optional()
332
+ .describe("metadata.runtime.selected at the time of SANDBOX_START"),
333
+ port: z
334
+ .string()
335
+ .nullable()
336
+ .optional()
337
+ .describe("metadata.runtime.port at the time of SANDBOX_START"),
338
+ path: z
339
+ .string()
340
+ .nullable()
341
+ .optional()
342
+ .describe("metadata.runtime.path at the time of SANDBOX_START"),
343
+ })
344
+ .optional()
345
+ .describe(
346
+ "Snapshot of metadata.runtime fields (selected/port/path) used at SANDBOX_START. The Preview tab compares the live metadata.runtime against this to decide if a restart is required to apply changes.",
347
+ ),
348
+ });
349
+
350
+ export type SandboxRecord = z.infer<typeof SandboxRecordSchema>;
351
+
352
+ /**
353
+ * Strict parser for a single sandbox record. Migration 091 rewrote every
354
+ * persisted legacy value (`runnerKind`, `vmId`, legacy kind names), so
355
+ * readers no longer accept those shapes — Zod throws on any leftover.
356
+ */
357
+ export function parseSandboxRecord(raw: unknown): SandboxRecord {
358
+ return SandboxRecordSchema.parse(raw);
359
+ }
360
+
361
+ /** The active sandbox provider kinds. */
362
+ export type SandboxProviderKind = "cluster" | "user-desktop";
363
+
364
+ /**
365
+ * Parse a `sandboxMap[user][branch]` cell into the kind-keyed v2 shape.
366
+ *
367
+ * Migration 087 rewrote every cell to the 3-level layout
368
+ * (`sandboxProviderKind → SandboxRecord`) and migration 091 rewrote every
369
+ * legacy kind value; this reader is now strict — entries that fail to parse
370
+ * (e.g. a stray cell missing required fields) are skipped, but no legacy
371
+ * key/value normalization happens.
372
+ */
373
+ export function parseBranchMap(
374
+ raw: unknown,
375
+ ): Partial<Record<SandboxProviderKind, SandboxRecord>> {
376
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
377
+ const obj = raw as Record<string, unknown>;
378
+
379
+ const out: Partial<Record<SandboxProviderKind, SandboxRecord>> = {};
380
+ for (const [k, v] of Object.entries(obj)) {
381
+ if (!v || typeof v !== "object") continue;
382
+ if (k !== "cluster" && k !== "user-desktop") {
383
+ continue;
384
+ }
385
+ try {
386
+ out[k] = SandboxRecordSchema.parse(v);
387
+ } catch {
388
+ // Skip malformed entries rather than throw — readers stay forgiving
389
+ // about unexpected shapes within a known-key cell.
390
+ }
391
+ }
392
+ return out;
393
+ }
394
+
395
+ /**
396
+ * Maps a user to their sandbox records per (branch, sandboxProviderKind).
397
+ * Lookup: sandboxMap[userId][branch][sandboxProviderKind] -> SandboxRecord
398
+ *
399
+ * Multiple threads on the same (userId, branch, kind) share one sandbox.
400
+ * Cloud and local sandboxes can coexist on the same branch as siblings.
401
+ *
402
+ * The schema is strict v2. Strict input/output types here are load-bearing
403
+ * for `useForm<…>(zodResolver(…))` callers, whose generic depends on
404
+ * `z.input` being identical to `z.output`. A `z.preprocess` here widens
405
+ * `z.input` to `unknown` and breaks the form.
406
+ */
407
+ export const SandboxMapSchema = z.record(
408
+ z.string().describe("userId"),
409
+ z.record(
410
+ z.string().describe("branch"),
411
+ z.record(z.string().describe("sandboxProviderKind"), SandboxRecordSchema),
78
412
  ),
413
+ );
414
+
415
+ export type SandboxMap = z.infer<typeof SandboxMapSchema>;
416
+
417
+ /**
418
+ * Normalize a raw `metadata.sandboxMap` value into v2 shape on read. Use this
419
+ * in storage adapters BEFORE returning data that will be Zod-validated against
420
+ * `VirtualMCPEntitySchema` (or any schema embedding `SandboxMapSchema`).
421
+ *
422
+ * After migration 091, the stored shape is the strict 3-level
423
+ * `userId → branch → sandboxProviderKind → SandboxRecord`. Returns `{}` for
424
+ * missing / malformed input rather than throwing — readers stay forgiving
425
+ * about unexpected per-row corruption; the strict schema catches any
426
+ * residual issues at validation time.
427
+ */
428
+ export function normalizeSandboxMap(raw: unknown): SandboxMap {
429
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
430
+ const out: SandboxMap = {};
431
+ for (const [userId, userVal] of Object.entries(
432
+ raw as Record<string, unknown>,
433
+ )) {
434
+ if (!userVal || typeof userVal !== "object" || Array.isArray(userVal)) {
435
+ continue;
436
+ }
437
+ const userOut: SandboxMap[string] = {};
438
+ for (const [branch, branchVal] of Object.entries(
439
+ userVal as Record<string, unknown>,
440
+ )) {
441
+ const normalized = parseBranchMap(branchVal);
442
+ if (Object.keys(normalized).length > 0) {
443
+ userOut[branch] = normalized as SandboxMap[string][string];
444
+ }
445
+ }
446
+ if (Object.keys(userOut).length > 0) {
447
+ out[userId] = userOut;
448
+ }
449
+ }
450
+ return out;
451
+ }
452
+
453
+ /**
454
+ * Virtual MCP entity schema - single source of truth
455
+ * Compliant with collections binding pattern
456
+ */
457
+ export const VirtualMCPEntitySchema = z.object({
458
+ // Base collection entity fields
459
+ id: z.string().describe("Unique identifier"),
460
+ title: z.string().describe("Human-readable name"),
461
+ description: z.string().nullable().describe("Description"),
462
+ icon: z.string().nullable().describe("Icon URL"),
463
+ created_at: z.string().describe("Creation timestamp"),
464
+ updated_at: z.string().describe("Last update timestamp"),
465
+ created_by: z.string().describe("User ID who created this item"),
466
+ updated_by: z
467
+ .string()
468
+ .optional()
469
+ .describe("User ID who last updated this item"),
470
+
471
+ // Entity-specific fields
472
+ organization_id: z.string().describe("Organization ID this item belongs to"),
79
473
  status: z.enum(["active", "inactive"]).describe("Current status"),
474
+ pinned: z.boolean().describe("Whether this space is pinned to the sidebar"),
80
475
  // Metadata (stored in connections.metadata)
476
+ // Normalize null/undefined to { instructions: null } for consistent form tracking
81
477
  metadata: z
82
478
  .object({
83
- instructions: z.string().optional().describe("MCP server instructions"),
479
+ instructions: z
480
+ .string()
481
+ .nullable()
482
+ .describe("Instructions also used as system prompt"),
483
+ enabled_plugins: z
484
+ .array(z.string())
485
+ .nullable()
486
+ .optional()
487
+ .describe("List of enabled plugin IDs"),
488
+ ui: VirtualMcpUISchema.nullable()
489
+ .optional()
490
+ .describe("UI customization settings"),
491
+ githubRepo: GithubRepoSchema.nullable()
492
+ .optional()
493
+ .describe("Linked GitHub repository"),
494
+ runtime: RuntimeMetadataSchema.nullable()
495
+ .optional()
496
+ .describe(
497
+ "User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
498
+ ),
499
+ sandboxMap: SandboxMapSchema.optional().describe(
500
+ "Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
501
+ ),
84
502
  })
85
- .nullable()
86
- .optional()
87
- .describe("Additional metadata including MCP server instructions"),
503
+ .loose()
504
+ .describe("Metadata"),
88
505
  // Nested connections
89
506
  connections: z
90
507
  .array(VirtualMCPConnectionSchema)
91
- .describe(
92
- "Connections with their selected tools (behavior depends on tool_selection_mode)",
93
- ),
508
+ .describe("Connections with their selected tools, resources, and prompts"),
94
509
  });
95
510
 
96
511
  /**
@@ -108,49 +523,46 @@ export const VirtualMCPCreateDataSchema = z.object({
108
523
  .nullable()
109
524
  .optional()
110
525
  .describe("Optional description"),
111
- tool_selection_mode: ToolSelectionModeSchema.optional()
112
- .default("inclusion")
113
- .describe("Tool selection mode (defaults to 'inclusion')"),
114
- icon: z.string().nullable().optional().describe("Optional icon URL"),
526
+ icon: z.string().nullish().describe("Optional icon URL"),
115
527
  status: z
116
528
  .enum(["active", "inactive"])
117
529
  .optional()
118
530
  .default("active")
119
531
  .describe("Initial status"),
532
+ pinned: z.boolean().optional().default(false).describe("Pin to sidebar"),
120
533
  metadata: z
121
534
  .object({
122
- instructions: z.string().optional().describe("MCP server instructions"),
535
+ instructions: z
536
+ .string()
537
+ .nullable()
538
+ .optional()
539
+ .describe("MCP server instructions"),
540
+ enabled_plugins: z
541
+ .array(z.string())
542
+ .nullable()
543
+ .optional()
544
+ .describe("List of enabled plugin IDs"),
545
+ ui: VirtualMcpUISchema.nullable()
546
+ .optional()
547
+ .describe("UI customization settings"),
548
+ githubRepo: GithubRepoSchema.nullable()
549
+ .optional()
550
+ .describe("Linked GitHub repository"),
551
+ runtime: RuntimeMetadataSchema.nullable()
552
+ .optional()
553
+ .describe(
554
+ "User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
555
+ ),
556
+ sandboxMap: SandboxMapSchema.optional().describe(
557
+ "Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
558
+ ),
123
559
  })
560
+ .loose()
124
561
  .nullable()
125
562
  .optional()
126
563
  .describe("Additional metadata including MCP server instructions"),
127
564
  connections: z
128
- .array(
129
- z.object({
130
- connection_id: z.string().describe("Connection ID"),
131
- selected_tools: z
132
- .array(z.string())
133
- .nullable()
134
- .optional()
135
- .describe(
136
- "Selected tool names (null/undefined = all tools or full exclusion)",
137
- ),
138
- selected_resources: z
139
- .array(z.string())
140
- .nullable()
141
- .optional()
142
- .describe(
143
- "Selected resource URIs or patterns with * and ** wildcards (null/undefined = all resources)",
144
- ),
145
- selected_prompts: z
146
- .array(z.string())
147
- .nullable()
148
- .optional()
149
- .describe(
150
- "Selected prompt names (null/undefined = all prompts or full exclusion)",
151
- ),
152
- }),
153
- )
565
+ .array(VirtualMCPConnectionInputSchema)
154
566
  .describe(
155
567
  "Connections to include/exclude (can be empty for exclusion mode)",
156
568
  ),
@@ -168,49 +580,42 @@ export const VirtualMCPUpdateDataSchema = z.object({
168
580
  .nullable()
169
581
  .optional()
170
582
  .describe("New description (null to clear)"),
171
- tool_selection_mode: ToolSelectionModeSchema.optional().describe(
172
- "New tool selection mode",
173
- ),
174
- icon: z
175
- .string()
176
- .nullable()
177
- .optional()
178
- .describe("New icon URL (null to clear)"),
583
+ icon: z.string().nullish().describe("New icon URL"),
179
584
  status: z.enum(["active", "inactive"]).optional().describe("New status"),
585
+ pinned: z.boolean().optional().describe("Pin/unpin from sidebar"),
180
586
  metadata: z
181
587
  .object({
182
- instructions: z.string().optional().describe("MCP server instructions"),
588
+ instructions: z
589
+ .string()
590
+ .nullable()
591
+ .optional()
592
+ .describe("MCP server instructions"),
593
+ enabled_plugins: z
594
+ .array(z.string())
595
+ .nullable()
596
+ .optional()
597
+ .describe("List of enabled plugin IDs"),
598
+ ui: VirtualMcpUISchema.nullable()
599
+ .optional()
600
+ .describe("UI customization settings"),
601
+ githubRepo: GithubRepoSchema.nullable()
602
+ .optional()
603
+ .describe("Linked GitHub repository"),
604
+ runtime: RuntimeMetadataSchema.nullable()
605
+ .optional()
606
+ .describe(
607
+ "User-pinned runtime config (package manager, dev port). Empty fields = autodetect.",
608
+ ),
609
+ sandboxMap: SandboxMapSchema.optional().describe(
610
+ "Per-user, per-branch sandbox mapping: sandboxMap[userId][branch] -> { sandboxHandle, previewUrl }",
611
+ ),
183
612
  })
613
+ .loose()
184
614
  .nullable()
185
615
  .optional()
186
616
  .describe("Additional metadata including MCP server instructions"),
187
617
  connections: z
188
- .array(
189
- z.object({
190
- connection_id: z.string().describe("Connection ID"),
191
- selected_tools: z
192
- .array(z.string())
193
- .nullable()
194
- .optional()
195
- .describe(
196
- "Selected tool names (null/undefined = all tools or full exclusion)",
197
- ),
198
- selected_resources: z
199
- .array(z.string())
200
- .nullable()
201
- .optional()
202
- .describe(
203
- "Selected resource URIs or patterns with * and ** wildcards (null/undefined = all resources)",
204
- ),
205
- selected_prompts: z
206
- .array(z.string())
207
- .nullable()
208
- .optional()
209
- .describe(
210
- "Selected prompt names (null/undefined = all prompts or full exclusion)",
211
- ),
212
- }),
213
- )
618
+ .array(VirtualMCPConnectionInputSchema)
214
619
  .optional()
215
620
  .describe("New connections (replaces existing)"),
216
621
  });