@mintmcp/hosted-cli 0.0.17 → 0.0.18
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/dist/api.d.ts +87 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +65 -4
- package/dist/api.js.map +1 -1
- package/dist/containerBuilder.d.ts +48 -0
- package/dist/containerBuilder.d.ts.map +1 -0
- package/dist/containerBuilder.js +364 -0
- package/dist/containerBuilder.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +400 -38
- package/dist/index.js.map +1 -1
- package/dist/updateBuilder.d.ts +28 -3
- package/dist/updateBuilder.d.ts.map +1 -1
- package/dist/updateBuilder.js +8 -3
- package/dist/updateBuilder.js.map +1 -1
- package/package.json +2 -2
- package/src/api.ts +87 -4
- package/src/containerBuilder.ts +602 -0
- package/src/index.ts +633 -63
- package/src/updateBuilder.ts +10 -3
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from "url";
|
|
|
13
13
|
import z, { ZodSchema } from "zod";
|
|
14
14
|
import {
|
|
15
15
|
type AppRouter,
|
|
16
|
+
type GatewayId,
|
|
16
17
|
HostedIdSchema,
|
|
17
18
|
type HostedTransport,
|
|
18
19
|
UserConfigUpdateSchema,
|
|
@@ -34,6 +35,18 @@ import {
|
|
|
34
35
|
sameStoredAuthAccount,
|
|
35
36
|
useAuth,
|
|
36
37
|
} from "./auth.js";
|
|
38
|
+
import {
|
|
39
|
+
assertImageSupportsPlatform,
|
|
40
|
+
buildImage,
|
|
41
|
+
defaultBuildPlatform,
|
|
42
|
+
defaultLocalTestImageRef,
|
|
43
|
+
ensureDockerAvailable,
|
|
44
|
+
ensureDockerBuildxAvailable,
|
|
45
|
+
ensureImageAvailableLocally,
|
|
46
|
+
loginToRegistry,
|
|
47
|
+
pushImage,
|
|
48
|
+
smokeTestImage,
|
|
49
|
+
} from "./containerBuilder.js";
|
|
37
50
|
import {
|
|
38
51
|
buildPartialUpdate,
|
|
39
52
|
type DeployOptions,
|
|
@@ -52,6 +65,7 @@ export type CliClientAppRouterInputs = inferRouterInputs<AppRouter>;
|
|
|
52
65
|
export type CliClientAppRouterOutputs = inferRouterOutputs<AppRouter>;
|
|
53
66
|
|
|
54
67
|
const TransportSchema = z.enum(["http", "stdio"]);
|
|
68
|
+
const parseTransport = argParser(TransportSchema);
|
|
55
69
|
|
|
56
70
|
const TRPC_API_URL: Record<Env, string> = {
|
|
57
71
|
staging: "http://localhost:3000/api.trpc",
|
|
@@ -69,16 +83,46 @@ const HOSTED_CONFIG_PATHS: Record<Env, string> = {
|
|
|
69
83
|
production: ".mintmcp/hosted.json",
|
|
70
84
|
};
|
|
71
85
|
|
|
72
|
-
const
|
|
86
|
+
const HostedConfigBaseSchema = z.object({
|
|
73
87
|
hostedId: HostedIdSchema,
|
|
74
88
|
|
|
75
|
-
// Directory containing data to send to hosted server, relative to "--base".
|
|
76
|
-
mntDataDir: z.string(),
|
|
77
|
-
|
|
78
89
|
// Organization that owns this hosted server. Older config files may omit it.
|
|
79
90
|
organizationId: z.string().optional(),
|
|
80
91
|
});
|
|
92
|
+
|
|
93
|
+
const HostedConfigSchema = z.union([
|
|
94
|
+
HostedConfigBaseSchema.extend({
|
|
95
|
+
mode: z.literal("container"),
|
|
96
|
+
image: z.string().optional(),
|
|
97
|
+
transport: TransportSchema.optional(),
|
|
98
|
+
|
|
99
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
100
|
+
mntDataDir: z.string().optional(),
|
|
101
|
+
}),
|
|
102
|
+
HostedConfigBaseSchema.extend({
|
|
103
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
104
|
+
mntDataDir: z.string(),
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
81
107
|
type HostedConfig = z.infer<typeof HostedConfigSchema>;
|
|
108
|
+
type DeployResult = {
|
|
109
|
+
config: HostedConfig;
|
|
110
|
+
gatewayId?: GatewayId;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type RegistryPushSession =
|
|
114
|
+
CliClientAppRouterOutputs["mcpHost"]["createRegistryPushSession"];
|
|
115
|
+
|
|
116
|
+
function storedTransport(config: HostedConfig): Transport | undefined {
|
|
117
|
+
return "mode" in config ? config.transport : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function canUseDirectImageStartup(params: {
|
|
121
|
+
transport: Transport | undefined;
|
|
122
|
+
mntDataDir: string | undefined;
|
|
123
|
+
}): boolean {
|
|
124
|
+
return params.transport === "http" && params.mntDataDir == undefined;
|
|
125
|
+
}
|
|
82
126
|
|
|
83
127
|
function isTRPCUnauthorized(error: unknown): boolean {
|
|
84
128
|
return (
|
|
@@ -208,7 +252,7 @@ async function uploadData(
|
|
|
208
252
|
return { secretDataZipGcsPath: upload.secretDataZipGcsPath };
|
|
209
253
|
}
|
|
210
254
|
|
|
211
|
-
const
|
|
255
|
+
const DEPLOY_OPTIONS_LEGACY_DEFAULTS = {
|
|
212
256
|
transport: "stdio",
|
|
213
257
|
// Skip install/build if already done (e.g., by startup probe or previous session).
|
|
214
258
|
startupCommand:
|
|
@@ -216,33 +260,53 @@ const DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS = {
|
|
|
216
260
|
mntDataDir: ".",
|
|
217
261
|
} as const;
|
|
218
262
|
|
|
263
|
+
const DEPLOY_OPTIONS_CONTAINER_DEFAULTS = {
|
|
264
|
+
transport: "http",
|
|
265
|
+
} as const;
|
|
266
|
+
|
|
219
267
|
async function createNew(
|
|
220
268
|
client: Awaited<ReturnType<typeof makeAuthenticatedClient>>,
|
|
221
269
|
base: string,
|
|
222
270
|
organizationId: string,
|
|
223
271
|
options: DeployOptions,
|
|
224
|
-
): Promise<
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} = {
|
|
231
|
-
...DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS,
|
|
272
|
+
): Promise<DeployResult> {
|
|
273
|
+
const defaults = options.image
|
|
274
|
+
? DEPLOY_OPTIONS_CONTAINER_DEFAULTS
|
|
275
|
+
: DEPLOY_OPTIONS_LEGACY_DEFAULTS;
|
|
276
|
+
const { name, image, transport, startupCommand, mntDataDir } = {
|
|
277
|
+
...defaults,
|
|
232
278
|
...options,
|
|
233
279
|
};
|
|
234
280
|
if (name == undefined) {
|
|
235
281
|
throw new Error("--name is a required option when creating a new server");
|
|
236
282
|
}
|
|
283
|
+
if (
|
|
284
|
+
image != undefined &&
|
|
285
|
+
startupCommand == undefined &&
|
|
286
|
+
!canUseDirectImageStartup({ transport, mntDataDir })
|
|
287
|
+
) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
"--startup-command is required unless using --transport http without --mnt-data-dir",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (image == undefined && startupCommand == undefined) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
"Expected --startup-command to be defined after applying defaults",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
237
297
|
|
|
238
|
-
const upload =
|
|
298
|
+
const upload =
|
|
299
|
+
mntDataDir != undefined || image == undefined
|
|
300
|
+
? await uploadData(client, path.join(base, mntDataDir ?? "."))
|
|
301
|
+
: undefined;
|
|
239
302
|
|
|
240
303
|
const config = UserConfigUpdateSchema.parse({
|
|
241
304
|
userGivenName: name,
|
|
242
|
-
|
|
305
|
+
...(image != undefined ? { image } : {}),
|
|
306
|
+
...(startupCommand != undefined ? { command: startupCommand } : {}),
|
|
243
307
|
transport: toHostedTransport(transport),
|
|
244
308
|
replaceEnv: [],
|
|
245
|
-
secretDataZipGcsPath: upload.secretDataZipGcsPath,
|
|
309
|
+
...(upload ? { secretDataZipGcsPath: upload.secretDataZipGcsPath } : {}),
|
|
246
310
|
} satisfies z.input<typeof UserConfigUpdateSchema>);
|
|
247
311
|
|
|
248
312
|
const createdServer = await client.mcpHost.createServer.mutate({
|
|
@@ -250,9 +314,15 @@ async function createNew(
|
|
|
250
314
|
});
|
|
251
315
|
|
|
252
316
|
return {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
317
|
+
config: HostedConfigSchema.parse({
|
|
318
|
+
hostedId: createdServer.hostedId,
|
|
319
|
+
...(image != undefined
|
|
320
|
+
? { mode: "container" as const, image, transport }
|
|
321
|
+
: {}),
|
|
322
|
+
...(mntDataDir != undefined ? { mntDataDir } : {}),
|
|
323
|
+
organizationId,
|
|
324
|
+
}),
|
|
325
|
+
gatewayId: createdServer.gatewayId,
|
|
256
326
|
};
|
|
257
327
|
}
|
|
258
328
|
|
|
@@ -262,7 +332,7 @@ async function updateExisting(
|
|
|
262
332
|
config: HostedConfig,
|
|
263
333
|
organizationId: string,
|
|
264
334
|
options: DeployOptions,
|
|
265
|
-
): Promise<
|
|
335
|
+
): Promise<DeployResult> {
|
|
266
336
|
if (
|
|
267
337
|
config.organizationId != undefined &&
|
|
268
338
|
config.organizationId !== organizationId
|
|
@@ -271,20 +341,273 @@ async function updateExisting(
|
|
|
271
341
|
`Hosted config belongs to organization ${config.organizationId}, but the selected credentials are for ${organizationId}. Switch organizations with "hosted auth use --organization-id ${config.organizationId}" before deploying.`,
|
|
272
342
|
);
|
|
273
343
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
344
|
+
const nextTransport = options.transport ?? storedTransport(config);
|
|
345
|
+
const clearCommand =
|
|
346
|
+
options.image != undefined &&
|
|
347
|
+
options.startupCommand == undefined &&
|
|
348
|
+
options.mntDataDir == undefined &&
|
|
349
|
+
nextTransport === "http";
|
|
350
|
+
if (
|
|
351
|
+
options.image != undefined &&
|
|
352
|
+
options.startupCommand == undefined &&
|
|
353
|
+
!clearCommand
|
|
354
|
+
) {
|
|
355
|
+
if (options.mntDataDir != undefined) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"--startup-command is required when using --mnt-data-dir with --image",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (nextTransport == undefined) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"--startup-command is required unless the resulting transport is known to be http. Pass --transport http to switch to direct image startup.",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
throw new Error(
|
|
366
|
+
"--startup-command is required unless using --transport http without --mnt-data-dir",
|
|
367
|
+
);
|
|
277
368
|
}
|
|
278
|
-
newConfig
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
369
|
+
const newConfig = HostedConfigSchema.parse({
|
|
370
|
+
...config,
|
|
371
|
+
...(options.mntDataDir != undefined
|
|
372
|
+
? { mntDataDir: options.mntDataDir }
|
|
373
|
+
: {}),
|
|
374
|
+
...(options.image != undefined
|
|
375
|
+
? {
|
|
376
|
+
mode: "container" as const,
|
|
377
|
+
image: options.image,
|
|
378
|
+
...(nextTransport != undefined ? { transport: nextTransport } : {}),
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
...("mode" in config && nextTransport != undefined
|
|
382
|
+
? { transport: nextTransport }
|
|
383
|
+
: {}),
|
|
384
|
+
organizationId,
|
|
385
|
+
});
|
|
386
|
+
const upload =
|
|
387
|
+
(options.image == undefined || options.mntDataDir != undefined) &&
|
|
388
|
+
newConfig.mntDataDir != undefined
|
|
389
|
+
? await uploadData(client, path.join(base, newConfig.mntDataDir))
|
|
390
|
+
: undefined;
|
|
391
|
+
const updatedServer = await client.mcpHost.partialUpdateServer.mutate({
|
|
284
392
|
hostedId: config.hostedId,
|
|
285
|
-
update: buildPartialUpdate(options, upload
|
|
393
|
+
update: buildPartialUpdate(options, upload?.secretDataZipGcsPath, {
|
|
394
|
+
clearCommand,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
config: newConfig,
|
|
399
|
+
...(updatedServer.gatewayId != undefined
|
|
400
|
+
? { gatewayId: updatedServer.gatewayId }
|
|
401
|
+
: {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function connectorSettingsUrl(env: Env, gatewayId: GatewayId): string {
|
|
406
|
+
const url = new URL("/vmcps", WEB_CLIENT_URL[env]);
|
|
407
|
+
url.searchParams.set("id", gatewayId);
|
|
408
|
+
url.searchParams.set("serverTab", "connector-settings");
|
|
409
|
+
return url.toString();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function logDeployResult(
|
|
413
|
+
env: Env,
|
|
414
|
+
action: "Created" | "Updated",
|
|
415
|
+
result: DeployResult,
|
|
416
|
+
): void {
|
|
417
|
+
if (result.gatewayId == undefined) {
|
|
418
|
+
console.log(`${action}.`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
console.log(`${action} ${connectorSettingsUrl(env, result.gatewayId)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function updateConfigAfterManagedPush(
|
|
425
|
+
config: HostedConfig,
|
|
426
|
+
organizationId: string,
|
|
427
|
+
managedImageRef: string,
|
|
428
|
+
): HostedConfig {
|
|
429
|
+
return HostedConfigSchema.parse({
|
|
430
|
+
...config,
|
|
431
|
+
mode: "container" as const,
|
|
432
|
+
image: managedImageRef,
|
|
433
|
+
organizationId,
|
|
434
|
+
...("mode" in config && config.transport != undefined
|
|
435
|
+
? { transport: config.transport }
|
|
436
|
+
: {}),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function managedRegistryImageRef(session: RegistryPushSession): string {
|
|
441
|
+
return `${session.registryHost}/${session.repository}:${session.imageTag}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function buildAndPushManagedImage(params: {
|
|
445
|
+
client: AuthenticatedClient;
|
|
446
|
+
organizationId: string;
|
|
447
|
+
config: HostedConfig;
|
|
448
|
+
dockerfile: string;
|
|
449
|
+
context: string;
|
|
450
|
+
buildArg: string[];
|
|
451
|
+
target?: string;
|
|
452
|
+
platform: string;
|
|
453
|
+
}): Promise<DeployResult> {
|
|
454
|
+
if (
|
|
455
|
+
params.config.organizationId != undefined &&
|
|
456
|
+
params.config.organizationId !== params.organizationId
|
|
457
|
+
) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`Hosted config belongs to organization ${params.config.organizationId}, but the selected credentials are for ${params.organizationId}. Switch organizations with "hosted auth use --organization-id ${params.config.organizationId}" before building and pushing.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const session = await params.client.mcpHost.createRegistryPushSession.mutate({
|
|
464
|
+
hostedId: params.config.hostedId,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
468
|
+
const buildResult = buildImage({
|
|
469
|
+
dockerfile: params.dockerfile,
|
|
470
|
+
context: params.context,
|
|
471
|
+
buildArgs: params.buildArg,
|
|
472
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
473
|
+
platform: params.platform,
|
|
474
|
+
imageRef: deployImageRef,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
478
|
+
pushImage(buildResult.imageRef);
|
|
479
|
+
|
|
480
|
+
const finalized = await params.client.mcpHost.finalizeRegistryPush.mutate({
|
|
481
|
+
hostedId: params.config.hostedId,
|
|
482
|
+
finalizeToken: session.finalizeToken,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
config: updateConfigAfterManagedPush(
|
|
487
|
+
params.config,
|
|
488
|
+
params.organizationId,
|
|
489
|
+
deployImageRef,
|
|
490
|
+
),
|
|
491
|
+
...(finalized.gatewayId != undefined
|
|
492
|
+
? { gatewayId: finalized.gatewayId }
|
|
493
|
+
: {}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function buildManagedImageCreateConfig(params: {
|
|
498
|
+
name?: string;
|
|
499
|
+
transport?: Transport;
|
|
500
|
+
startupCommand?: string;
|
|
501
|
+
}) {
|
|
502
|
+
const transport = params.transport ?? "http";
|
|
503
|
+
if (params.name == undefined) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"--name is a required option when building and pushing a new managed image connector",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (
|
|
509
|
+
params.startupCommand == undefined &&
|
|
510
|
+
!canUseDirectImageStartup({ transport, mntDataDir: undefined })
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"--startup-command is required unless using --transport http",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return UserConfigUpdateSchema.omit({ image: true }).parse({
|
|
517
|
+
userGivenName: params.name,
|
|
518
|
+
...(params.startupCommand != undefined
|
|
519
|
+
? { command: params.startupCommand }
|
|
520
|
+
: {}),
|
|
521
|
+
transport: toHostedTransport(transport),
|
|
522
|
+
replaceEnv: [],
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function buildAndPushManagedImageForCreate(params: {
|
|
527
|
+
client: AuthenticatedClient;
|
|
528
|
+
organizationId: string;
|
|
529
|
+
name?: string;
|
|
530
|
+
transport?: Transport;
|
|
531
|
+
startupCommand?: string;
|
|
532
|
+
dockerfile: string;
|
|
533
|
+
context: string;
|
|
534
|
+
buildArg: string[];
|
|
535
|
+
target?: string;
|
|
536
|
+
platform: string;
|
|
537
|
+
}): Promise<DeployResult> {
|
|
538
|
+
const config = buildManagedImageCreateConfig({
|
|
539
|
+
...(params.name != undefined ? { name: params.name } : {}),
|
|
540
|
+
...(params.transport != undefined ? { transport: params.transport } : {}),
|
|
541
|
+
...(params.startupCommand != undefined
|
|
542
|
+
? { startupCommand: params.startupCommand }
|
|
543
|
+
: {}),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const session =
|
|
547
|
+
await params.client.mcpHost.createRegistryPushSessionForCreate.mutate({});
|
|
548
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
549
|
+
const buildResult = buildImage({
|
|
550
|
+
dockerfile: params.dockerfile,
|
|
551
|
+
context: params.context,
|
|
552
|
+
buildArgs: params.buildArg,
|
|
553
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
554
|
+
platform: params.platform,
|
|
555
|
+
imageRef: deployImageRef,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
559
|
+
pushImage(buildResult.imageRef);
|
|
560
|
+
|
|
561
|
+
const finalized =
|
|
562
|
+
await params.client.mcpHost.finalizeRegistryPushCreate.mutate({
|
|
563
|
+
finalizeToken: session.finalizeToken,
|
|
564
|
+
config,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
config: HostedConfigSchema.parse({
|
|
569
|
+
hostedId: finalized.hostedId,
|
|
570
|
+
organizationId: params.organizationId,
|
|
571
|
+
mode: "container" as const,
|
|
572
|
+
image: deployImageRef,
|
|
573
|
+
...(params.transport != undefined
|
|
574
|
+
? { transport: params.transport }
|
|
575
|
+
: { transport: "http" as const }),
|
|
576
|
+
}),
|
|
577
|
+
gatewayId: finalized.gatewayId,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function deployWithOptions(
|
|
582
|
+
env: Env,
|
|
583
|
+
base: string,
|
|
584
|
+
options: DeployOptions,
|
|
585
|
+
): Promise<void> {
|
|
586
|
+
const auth = await getAuth(env);
|
|
587
|
+
|
|
588
|
+
await withAuthRetry(env, async (client) => {
|
|
589
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
590
|
+
|
|
591
|
+
let result: DeployResult;
|
|
592
|
+
if (fs.existsSync(configPath)) {
|
|
593
|
+
console.log("Updating existing server...");
|
|
594
|
+
const configData = fs.readFileSync(configPath, "utf8");
|
|
595
|
+
result = await updateExisting(
|
|
596
|
+
client,
|
|
597
|
+
base,
|
|
598
|
+
HostedConfigSchema.parse(JSON.parse(configData)),
|
|
599
|
+
auth.organizationId,
|
|
600
|
+
options,
|
|
601
|
+
);
|
|
602
|
+
logDeployResult(env, "Updated", result);
|
|
603
|
+
} else {
|
|
604
|
+
console.log("Creating new server...");
|
|
605
|
+
result = await createNew(client, base, auth.organizationId, options);
|
|
606
|
+
logDeployResult(env, "Created", result);
|
|
607
|
+
}
|
|
608
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
609
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
286
610
|
});
|
|
287
|
-
return newConfig;
|
|
288
611
|
}
|
|
289
612
|
|
|
290
613
|
const program = new Command()
|
|
@@ -300,9 +623,47 @@ const program = new Command()
|
|
|
300
623
|
.option("-b, --base <base>", "base directory for config and data.", ".");
|
|
301
624
|
|
|
302
625
|
function makeDefaultMessage(
|
|
303
|
-
optionName: keyof typeof
|
|
626
|
+
optionName: keyof typeof DEPLOY_OPTIONS_LEGACY_DEFAULTS,
|
|
304
627
|
): string {
|
|
305
|
-
return `
|
|
628
|
+
return `For new zip-based servers, defaults to '${DEPLOY_OPTIONS_LEGACY_DEFAULTS[optionName]}'.`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function collect(value: string, previous: string[]): string[] {
|
|
632
|
+
return previous.concat([value]);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parsePositiveInt(value: string): number {
|
|
636
|
+
const parsed = Number.parseInt(value, 10);
|
|
637
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
638
|
+
throw new InvalidArgumentError("Expected a positive integer.");
|
|
639
|
+
}
|
|
640
|
+
return parsed;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function shouldBuildTestImage(options: {
|
|
644
|
+
build?: boolean;
|
|
645
|
+
image?: string;
|
|
646
|
+
dockerfile?: string;
|
|
647
|
+
context?: string;
|
|
648
|
+
buildArg?: string[];
|
|
649
|
+
target?: string;
|
|
650
|
+
}): boolean {
|
|
651
|
+
if (options.build) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
if (options.image == undefined) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
if (options.dockerfile !== undefined && options.dockerfile !== "Dockerfile") {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
if (options.context !== undefined && options.context !== ".") {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
if ((options.buildArg?.length ?? 0) > 0) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return options.target != undefined;
|
|
306
667
|
}
|
|
307
668
|
|
|
308
669
|
const authCommand = program
|
|
@@ -372,14 +733,18 @@ program
|
|
|
372
733
|
"-n, --name <name>",
|
|
373
734
|
"Name of the hosted server. Required for new servers.",
|
|
374
735
|
)
|
|
736
|
+
.option(
|
|
737
|
+
"--image <ref>",
|
|
738
|
+
"Container image reference (e.g., ghcr.io/org/repo:tag). Skips zip upload when --mnt-data-dir is not also supplied.",
|
|
739
|
+
)
|
|
375
740
|
.option(
|
|
376
741
|
"-t, --transport <transport>",
|
|
377
|
-
`Transport (stdio or http). ${makeDefaultMessage("transport")}`,
|
|
742
|
+
`Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`,
|
|
378
743
|
argParser(TransportSchema),
|
|
379
744
|
)
|
|
380
745
|
.option(
|
|
381
746
|
"--startup-command <command>",
|
|
382
|
-
`Command that runs on hosted container to start the server (runs in bash). ${makeDefaultMessage("startupCommand")}`,
|
|
747
|
+
`Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`,
|
|
383
748
|
)
|
|
384
749
|
.option(
|
|
385
750
|
"--mnt-data-dir <directory>",
|
|
@@ -387,39 +752,244 @@ program
|
|
|
387
752
|
)
|
|
388
753
|
.action(async (options) => {
|
|
389
754
|
const { env, base } = program.opts();
|
|
390
|
-
|
|
755
|
+
await deployWithOptions(env, base, options);
|
|
756
|
+
});
|
|
391
757
|
|
|
392
|
-
|
|
393
|
-
|
|
758
|
+
program
|
|
759
|
+
.command("test-image")
|
|
760
|
+
.description(
|
|
761
|
+
"Run a local MCP smoke test against a container image. Builds the current Dockerfile by default; with --image, tests that existing image directly.",
|
|
762
|
+
)
|
|
763
|
+
.option(
|
|
764
|
+
"--image <ref>",
|
|
765
|
+
"Existing image reference to test directly, or the output tag to build when --build or other build options are supplied.",
|
|
766
|
+
)
|
|
767
|
+
.option(
|
|
768
|
+
"--build",
|
|
769
|
+
"Force a local build before testing, even when --image is supplied.",
|
|
770
|
+
)
|
|
771
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
772
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
773
|
+
.option(
|
|
774
|
+
"--build-arg <arg>",
|
|
775
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
776
|
+
collect,
|
|
777
|
+
[],
|
|
778
|
+
)
|
|
779
|
+
.option("--target <target>", "Multi-stage build target")
|
|
780
|
+
.option(
|
|
781
|
+
"--probe-path <path>",
|
|
782
|
+
"HTTP path to probe with MCP initialize",
|
|
783
|
+
"/mcp",
|
|
784
|
+
)
|
|
785
|
+
.option(
|
|
786
|
+
"--probe-timeout-ms <ms>",
|
|
787
|
+
"How long to wait for the local MCP probe to succeed",
|
|
788
|
+
parsePositiveInt,
|
|
789
|
+
30_000,
|
|
790
|
+
)
|
|
791
|
+
.option(
|
|
792
|
+
"--env <entry>",
|
|
793
|
+
"Container runtime env vars for the local smoke test (KEY=VALUE), repeatable",
|
|
794
|
+
collect,
|
|
795
|
+
[],
|
|
796
|
+
)
|
|
797
|
+
.option(
|
|
798
|
+
"--env-file <path>",
|
|
799
|
+
"Path to a Docker env file for the local smoke test",
|
|
800
|
+
)
|
|
801
|
+
.action(async (options) => {
|
|
802
|
+
ensureDockerAvailable();
|
|
803
|
+
ensureDockerBuildxAvailable();
|
|
394
804
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
805
|
+
const shouldBuild = shouldBuildTestImage(options);
|
|
806
|
+
const imageRef = options.image ?? defaultLocalTestImageRef();
|
|
807
|
+
|
|
808
|
+
if (shouldBuild) {
|
|
809
|
+
buildImage({
|
|
810
|
+
dockerfile: options.dockerfile,
|
|
811
|
+
context: options.context,
|
|
812
|
+
buildArgs: options.buildArg,
|
|
813
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
814
|
+
imageRef,
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
ensureImageAvailableLocally(imageRef);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await smokeTestImage({
|
|
821
|
+
imageRef,
|
|
822
|
+
probePath: options.probePath,
|
|
823
|
+
probeTimeoutMs: options.probeTimeoutMs,
|
|
824
|
+
env: options.env,
|
|
825
|
+
...(options.envFile != undefined ? { envFile: options.envFile } : {}),
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
program
|
|
830
|
+
.command("build-and-push")
|
|
831
|
+
.description(
|
|
832
|
+
"Build a Docker image, push it through the managed registry, and finalize the hosted connector image update or creation.",
|
|
833
|
+
)
|
|
834
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
835
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
836
|
+
.option(
|
|
837
|
+
"--build-arg <arg>",
|
|
838
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
839
|
+
collect,
|
|
840
|
+
[],
|
|
841
|
+
)
|
|
842
|
+
.option("--target <target>", "Multi-stage build target")
|
|
843
|
+
.option(
|
|
844
|
+
"--platform <platform>",
|
|
845
|
+
`Target platform to build for before push. Defaults to '${defaultBuildPlatform()}'.`,
|
|
846
|
+
defaultBuildPlatform(),
|
|
847
|
+
)
|
|
848
|
+
.option(
|
|
849
|
+
"--name <name>",
|
|
850
|
+
"Connector name when creating a new hosted connector",
|
|
851
|
+
)
|
|
852
|
+
.option(
|
|
853
|
+
"--transport <transport>",
|
|
854
|
+
"Transport to configure when creating a new managed connector",
|
|
855
|
+
parseTransport,
|
|
856
|
+
)
|
|
857
|
+
.option(
|
|
858
|
+
"--startup-command <command>",
|
|
859
|
+
"Startup command to configure when creating a new managed connector",
|
|
860
|
+
)
|
|
861
|
+
.action(async (options) => {
|
|
862
|
+
const { env, base } = program.opts();
|
|
863
|
+
|
|
864
|
+
ensureDockerAvailable();
|
|
865
|
+
ensureDockerBuildxAvailable();
|
|
866
|
+
|
|
867
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
868
|
+
console.warn(
|
|
869
|
+
`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
874
|
+
const auth = await getAuth(env);
|
|
875
|
+
const hasExistingConfig = fs.existsSync(configPath);
|
|
876
|
+
|
|
877
|
+
const result = await withAuthRetry(env, async (client) => {
|
|
878
|
+
if (hasExistingConfig) {
|
|
879
|
+
const config = HostedConfigSchema.parse(
|
|
880
|
+
JSON.parse(fs.readFileSync(configPath, "utf8")),
|
|
408
881
|
);
|
|
409
|
-
|
|
410
|
-
console.log("Creating new server...");
|
|
411
|
-
newOrUpdatedConfig = await createNew(
|
|
882
|
+
return await buildAndPushManagedImage({
|
|
412
883
|
client,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
options,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
884
|
+
organizationId: auth.organizationId,
|
|
885
|
+
config,
|
|
886
|
+
dockerfile: options.dockerfile,
|
|
887
|
+
context: options.context,
|
|
888
|
+
buildArg: options.buildArg,
|
|
889
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
890
|
+
platform: options.platform,
|
|
891
|
+
});
|
|
420
892
|
}
|
|
421
|
-
|
|
422
|
-
|
|
893
|
+
|
|
894
|
+
return await buildAndPushManagedImageForCreate({
|
|
895
|
+
client,
|
|
896
|
+
organizationId: auth.organizationId,
|
|
897
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
898
|
+
...(options.transport != undefined
|
|
899
|
+
? { transport: options.transport }
|
|
900
|
+
: {}),
|
|
901
|
+
...(options.startupCommand != undefined
|
|
902
|
+
? { startupCommand: options.startupCommand }
|
|
903
|
+
: {}),
|
|
904
|
+
dockerfile: options.dockerfile,
|
|
905
|
+
context: options.context,
|
|
906
|
+
buildArg: options.buildArg,
|
|
907
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
908
|
+
platform: options.platform,
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
913
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
914
|
+
logDeployResult(env, hasExistingConfig ? "Updated" : "Created", result);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
program
|
|
918
|
+
.command("build-and-deploy")
|
|
919
|
+
.description(
|
|
920
|
+
"Build a Docker image for hosted deployment, push it, verify the target platform, and deploy it.",
|
|
921
|
+
)
|
|
922
|
+
.requiredOption(
|
|
923
|
+
"--image <ref>",
|
|
924
|
+
"Image reference to build, push, and deploy (e.g., ghcr.io/org/repo:tag).",
|
|
925
|
+
)
|
|
926
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
927
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
928
|
+
.option(
|
|
929
|
+
"--build-arg <arg>",
|
|
930
|
+
"Build arguments (KEY=VALUE), repeatable",
|
|
931
|
+
collect,
|
|
932
|
+
[],
|
|
933
|
+
)
|
|
934
|
+
.option("--target <target>", "Multi-stage build target")
|
|
935
|
+
.option(
|
|
936
|
+
"--platform <platform>",
|
|
937
|
+
`Target platform to build for before deploy. Defaults to '${defaultBuildPlatform()}'.`,
|
|
938
|
+
defaultBuildPlatform(),
|
|
939
|
+
)
|
|
940
|
+
.option(
|
|
941
|
+
"-n, --name <name>",
|
|
942
|
+
"Name of the hosted server. Required for new servers.",
|
|
943
|
+
)
|
|
944
|
+
.option(
|
|
945
|
+
"-t, --transport <transport>",
|
|
946
|
+
`Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`,
|
|
947
|
+
argParser(TransportSchema),
|
|
948
|
+
)
|
|
949
|
+
.option(
|
|
950
|
+
"--startup-command <command>",
|
|
951
|
+
`Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`,
|
|
952
|
+
)
|
|
953
|
+
.option(
|
|
954
|
+
"--mnt-data-dir <directory>",
|
|
955
|
+
`Directory to copy to /mnt on the server. ${makeDefaultMessage("mntDataDir")}`,
|
|
956
|
+
)
|
|
957
|
+
.action(async (options) => {
|
|
958
|
+
const { env, base } = program.opts();
|
|
959
|
+
|
|
960
|
+
ensureDockerAvailable();
|
|
961
|
+
ensureDockerBuildxAvailable();
|
|
962
|
+
|
|
963
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
964
|
+
console.warn(
|
|
965
|
+
`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const buildResult = buildImage({
|
|
970
|
+
dockerfile: options.dockerfile,
|
|
971
|
+
context: options.context,
|
|
972
|
+
buildArgs: options.buildArg,
|
|
973
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
974
|
+
platform: options.platform,
|
|
975
|
+
imageRef: options.image,
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
pushImage(buildResult.imageRef);
|
|
979
|
+
assertImageSupportsPlatform(buildResult.imageRef, options.platform);
|
|
980
|
+
|
|
981
|
+
await deployWithOptions(env, base, {
|
|
982
|
+
image: buildResult.imageRef,
|
|
983
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
984
|
+
...(options.transport != undefined
|
|
985
|
+
? { transport: options.transport }
|
|
986
|
+
: {}),
|
|
987
|
+
...(options.startupCommand != undefined
|
|
988
|
+
? { startupCommand: options.startupCommand }
|
|
989
|
+
: {}),
|
|
990
|
+
...(options.mntDataDir != undefined
|
|
991
|
+
? { mntDataDir: options.mntDataDir }
|
|
992
|
+
: {}),
|
|
423
993
|
});
|
|
424
994
|
});
|
|
425
995
|
|