@decocms/mesh-sdk 1.2.2 → 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.
- package/README.md +10 -10
- package/package.json +3 -2
- package/src/context/index.ts +0 -1
- package/src/context/project-context.tsx +0 -10
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-collections.ts +19 -12
- package/src/hooks/use-connection.ts +12 -1
- package/src/hooks/use-mcp-client.ts +27 -10
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +38 -2
- package/src/lib/bridge-transport.ts +6 -434
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +121 -67
- package/src/lib/default-model.ts +188 -3
- package/src/lib/mcp-oauth.ts +59 -8
- package/src/lib/query-keys.ts +19 -4
- package/src/lib/server-client-bridge.ts +4 -146
- package/src/lib/usage.test.ts +66 -0
- package/src/lib/usage.ts +26 -0
- package/src/types/ai-providers.ts +19 -1
- package/src/types/connection.ts +5 -0
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +51 -8
- package/src/types/index.ts +18 -0
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +416 -9
package/src/types/virtual-mcp.ts
CHANGED
|
@@ -57,6 +57,85 @@ const VirtualMcpPinnedViewSchema = z.object({
|
|
|
57
57
|
|
|
58
58
|
export type VirtualMcpPinnedView = z.infer<typeof VirtualMcpPinnedViewSchema>;
|
|
59
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
|
+
|
|
60
139
|
/**
|
|
61
140
|
* Virtual MCP UI customization schema
|
|
62
141
|
*/
|
|
@@ -66,10 +145,311 @@ const VirtualMcpUISchema = z.object({
|
|
|
66
145
|
icon: z.string().nullable().optional(),
|
|
67
146
|
themeColor: z.string().nullable().optional(),
|
|
68
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(),
|
|
69
170
|
});
|
|
70
171
|
|
|
71
172
|
export type VirtualMcpUI = z.infer<typeof VirtualMcpUISchema>;
|
|
72
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
|
|
230
|
+
.string()
|
|
231
|
+
.nullable()
|
|
232
|
+
.optional()
|
|
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
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe(
|
|
278
|
+
"ID of the mcp-github connection used for authentication. Absent for public repos cloned without credentials.",
|
|
279
|
+
),
|
|
280
|
+
});
|
|
281
|
+
|
|
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
|
|
306
|
+
.string()
|
|
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),
|
|
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
|
+
|
|
73
453
|
/**
|
|
74
454
|
* Virtual MCP entity schema - single source of truth
|
|
75
455
|
* Compliant with collections binding pattern
|
|
@@ -91,10 +471,7 @@ export const VirtualMCPEntitySchema = z.object({
|
|
|
91
471
|
// Entity-specific fields
|
|
92
472
|
organization_id: z.string().describe("Organization ID this item belongs to"),
|
|
93
473
|
status: z.enum(["active", "inactive"]).describe("Current status"),
|
|
94
|
-
|
|
95
|
-
.enum(["agent", "project"])
|
|
96
|
-
.nullable()
|
|
97
|
-
.describe("Virtual MCP subtype for UI presentation"),
|
|
474
|
+
pinned: z.boolean().describe("Whether this space is pinned to the sidebar"),
|
|
98
475
|
// Metadata (stored in connections.metadata)
|
|
99
476
|
// Normalize null/undefined to { instructions: null } for consistent form tracking
|
|
100
477
|
metadata: z
|
|
@@ -111,6 +488,17 @@ export const VirtualMCPEntitySchema = z.object({
|
|
|
111
488
|
ui: VirtualMcpUISchema.nullable()
|
|
112
489
|
.optional()
|
|
113
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
|
+
),
|
|
114
502
|
})
|
|
115
503
|
.loose()
|
|
116
504
|
.describe("Metadata"),
|
|
@@ -141,10 +529,7 @@ export const VirtualMCPCreateDataSchema = z.object({
|
|
|
141
529
|
.optional()
|
|
142
530
|
.default("active")
|
|
143
531
|
.describe("Initial status"),
|
|
144
|
-
|
|
145
|
-
.enum(["agent", "project"])
|
|
146
|
-
.optional()
|
|
147
|
-
.describe("Virtual MCP subtype"),
|
|
532
|
+
pinned: z.boolean().optional().default(false).describe("Pin to sidebar"),
|
|
148
533
|
metadata: z
|
|
149
534
|
.object({
|
|
150
535
|
instructions: z
|
|
@@ -160,6 +545,17 @@ export const VirtualMCPCreateDataSchema = z.object({
|
|
|
160
545
|
ui: VirtualMcpUISchema.nullable()
|
|
161
546
|
.optional()
|
|
162
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
|
+
),
|
|
163
559
|
})
|
|
164
560
|
.loose()
|
|
165
561
|
.nullable()
|
|
@@ -186,7 +582,7 @@ export const VirtualMCPUpdateDataSchema = z.object({
|
|
|
186
582
|
.describe("New description (null to clear)"),
|
|
187
583
|
icon: z.string().nullish().describe("New icon URL"),
|
|
188
584
|
status: z.enum(["active", "inactive"]).optional().describe("New status"),
|
|
189
|
-
|
|
585
|
+
pinned: z.boolean().optional().describe("Pin/unpin from sidebar"),
|
|
190
586
|
metadata: z
|
|
191
587
|
.object({
|
|
192
588
|
instructions: z
|
|
@@ -202,6 +598,17 @@ export const VirtualMCPUpdateDataSchema = z.object({
|
|
|
202
598
|
ui: VirtualMcpUISchema.nullable()
|
|
203
599
|
.optional()
|
|
204
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
|
+
),
|
|
205
612
|
})
|
|
206
613
|
.loose()
|
|
207
614
|
.nullable()
|