@mintmcp/hosted-cli 0.0.16 → 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/auth.d.ts +95 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +659 -0
- package/dist/auth.js.map +1 -0
- 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 +506 -135
- 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 +4 -3
- package/src/api.ts +87 -4
- package/src/auth.ts +945 -0
- package/src/containerBuilder.ts +602 -0
- package/src/index.ts +789 -174
- package/src/updateBuilder.ts +10 -3
package/dist/index.js
CHANGED
|
@@ -4,20 +4,21 @@ import { createTRPCClient, httpLink } from "@trpc/client";
|
|
|
4
4
|
import archiver from "archiver";
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import { readFileSync } from "fs";
|
|
7
|
-
import keytar from "keytar";
|
|
8
7
|
import * as path from "path";
|
|
9
8
|
import { dirname, join } from "path";
|
|
10
9
|
import superjson from "superjson";
|
|
11
10
|
import { fileURLToPath } from "url";
|
|
12
11
|
import z, { ZodSchema } from "zod";
|
|
13
12
|
import { HostedIdSchema, UserConfigUpdateSchema, } from "./api.js";
|
|
13
|
+
import { clearAllAuth, clearSelectedAuth, EnvSchema, formatAuthSummary, getAuth, getCurrentAuthAccount, getSelectedAuth, getTokenClaims, listAuthRecords, requestAndStoreAuth, sameStoredAuthAccount, useAuth, } from "./auth.js";
|
|
14
|
+
import { assertImageSupportsPlatform, buildImage, defaultBuildPlatform, defaultLocalTestImageRef, ensureDockerAvailable, ensureDockerBuildxAvailable, ensureImageAvailableLocally, loginToRegistry, pushImage, smokeTestImage, } from "./containerBuilder.js";
|
|
14
15
|
import { buildPartialUpdate, toHostedTransport, } from "./updateBuilder.js";
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
17
18
|
const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
18
19
|
const version = packageJson.version;
|
|
19
|
-
const EnvSchema = z.enum(["staging", "production"]);
|
|
20
20
|
const TransportSchema = z.enum(["http", "stdio"]);
|
|
21
|
+
const parseTransport = argParser(TransportSchema);
|
|
21
22
|
const TRPC_API_URL = {
|
|
22
23
|
staging: "http://localhost:3000/api.trpc",
|
|
23
24
|
production: "https://app.mintmcp.com/api.trpc",
|
|
@@ -26,116 +27,43 @@ const WEB_CLIENT_URL = {
|
|
|
26
27
|
staging: "http://localhost:3000",
|
|
27
28
|
production: "https://app.mintmcp.com",
|
|
28
29
|
};
|
|
29
|
-
const CLIENT_IDS = {
|
|
30
|
-
staging: "client_01K0WB8ABW72JSBZ62V98AZMPS",
|
|
31
|
-
production: "client_01K0WB8AHKS2J39SFKAPTHTR90",
|
|
32
|
-
};
|
|
33
30
|
// Where to look for the hosted config, relative to "--base".
|
|
34
31
|
const HOSTED_CONFIG_PATHS = {
|
|
35
32
|
staging: ".mintmcp/hosted-staging.json",
|
|
36
33
|
production: ".mintmcp/hosted.json",
|
|
37
34
|
};
|
|
38
|
-
const
|
|
35
|
+
const HostedConfigBaseSchema = z.object({
|
|
39
36
|
hostedId: HostedIdSchema,
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
const AuthSchema = z.object({
|
|
44
|
-
accessToken: z.string(),
|
|
45
|
-
email: z.string(),
|
|
46
|
-
organizationId: z.string(),
|
|
37
|
+
// Organization that owns this hosted server. Older config files may omit it.
|
|
38
|
+
organizationId: z.string().optional(),
|
|
47
39
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const { device_code: deviceCode, verification_uri_complete: verificationUriComplete, interval = 5, } = deviceData;
|
|
64
|
-
console.log(`To authenticate, visit ${verificationUriComplete}`);
|
|
65
|
-
// Poll for access token.
|
|
66
|
-
const maxAttempts = 60;
|
|
67
|
-
for (let attempts = 0; attempts < maxAttempts; attempts += 1) {
|
|
68
|
-
await new Promise((resolve) => {
|
|
69
|
-
setTimeout(resolve, interval * 1000);
|
|
70
|
-
});
|
|
71
|
-
const tokenResponse = await fetch("https://api.workos.com/user_management/authenticate", {
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: {
|
|
74
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
-
},
|
|
76
|
-
body: new URLSearchParams({
|
|
77
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
78
|
-
device_code: deviceCode,
|
|
79
|
-
client_id: clientId,
|
|
80
|
-
}),
|
|
81
|
-
});
|
|
82
|
-
if (!tokenResponse.ok) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const tokenData = (await tokenResponse.json());
|
|
86
|
-
if (tokenData.error) {
|
|
87
|
-
if (tokenData.error === "authorization_pending") {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
else if (tokenData.error === "slow_down") {
|
|
91
|
-
// Increase interval as requested
|
|
92
|
-
await new Promise((resolve) => {
|
|
93
|
-
setTimeout(resolve, 5000);
|
|
94
|
-
});
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
throw new Error(`Authentication failed: ${tokenData.error}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (tokenData.access_token) {
|
|
102
|
-
if (!tokenData.user?.email) {
|
|
103
|
-
throw new Error("Authentication response missing user email");
|
|
104
|
-
}
|
|
105
|
-
if (!tokenData.organization_id) {
|
|
106
|
-
throw new Error("Authentication response missing organization ID");
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
accessToken: tokenData.access_token,
|
|
110
|
-
email: tokenData.user.email,
|
|
111
|
-
organizationId: tokenData.organization_id,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
throw new Error("Authentication timeout");
|
|
40
|
+
const HostedConfigSchema = z.union([
|
|
41
|
+
HostedConfigBaseSchema.extend({
|
|
42
|
+
mode: z.literal("container"),
|
|
43
|
+
image: z.string().optional(),
|
|
44
|
+
transport: TransportSchema.optional(),
|
|
45
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
46
|
+
mntDataDir: z.string().optional(),
|
|
47
|
+
}),
|
|
48
|
+
HostedConfigBaseSchema.extend({
|
|
49
|
+
// Directory containing data to send to hosted server, relative to "--base".
|
|
50
|
+
mntDataDir: z.string(),
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
function storedTransport(config) {
|
|
54
|
+
return "mode" in config ? config.transport : undefined;
|
|
116
55
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const stored = await keytar.getPassword(keychainServiceName, env);
|
|
120
|
-
if (stored) {
|
|
121
|
-
try {
|
|
122
|
-
const parsedData = JSON.parse(stored);
|
|
123
|
-
const validatedAuth = AuthSchema.parse(parsedData);
|
|
124
|
-
return validatedAuth;
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
console.warn("Invalid stored credentials, re-authenticating:", error);
|
|
128
|
-
// If validation fails, delete the invalid stored credentials and re-authenticate
|
|
129
|
-
await keytar.deletePassword(keychainServiceName, env);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const auth = await requestAuth(env);
|
|
133
|
-
await keytar.setPassword(keychainServiceName, env, JSON.stringify(auth));
|
|
134
|
-
return auth;
|
|
56
|
+
function canUseDirectImageStartup(params) {
|
|
57
|
+
return params.transport === "http" && params.mntDataDir == undefined;
|
|
135
58
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
59
|
+
function isTRPCUnauthorized(error) {
|
|
60
|
+
return (error instanceof Error &&
|
|
61
|
+
"data" in error &&
|
|
62
|
+
typeof error.data === "object" &&
|
|
63
|
+
error.data?.code === "UNAUTHORIZED");
|
|
64
|
+
}
|
|
65
|
+
async function makeAuthenticatedClient(env, options) {
|
|
66
|
+
const auth = await getAuth(env, options);
|
|
139
67
|
console.log(`Authenticated as ${auth.email} in ${auth.organizationId}.`);
|
|
140
68
|
return createTRPCClient({
|
|
141
69
|
links: [
|
|
@@ -149,6 +77,22 @@ async function makeAuthenticatedClient(env) {
|
|
|
149
77
|
],
|
|
150
78
|
});
|
|
151
79
|
}
|
|
80
|
+
async function withAuthRetry(env, fn) {
|
|
81
|
+
const client = await makeAuthenticatedClient(env);
|
|
82
|
+
try {
|
|
83
|
+
return await fn(client);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (isTRPCUnauthorized(error)) {
|
|
87
|
+
console.warn("Session expired or invalid. Refreshing credentials and retrying...");
|
|
88
|
+
const newClient = await makeAuthenticatedClient(env, {
|
|
89
|
+
forceRefresh: true,
|
|
90
|
+
});
|
|
91
|
+
return await fn(newClient);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
152
96
|
function argParser(schema) {
|
|
153
97
|
return (value) => {
|
|
154
98
|
const result = schema.safeParse(value);
|
|
@@ -211,47 +155,246 @@ async function uploadData(client, mntDataDir) {
|
|
|
211
155
|
console.log("File uploaded.");
|
|
212
156
|
return { secretDataZipGcsPath: upload.secretDataZipGcsPath };
|
|
213
157
|
}
|
|
214
|
-
const
|
|
158
|
+
const DEPLOY_OPTIONS_LEGACY_DEFAULTS = {
|
|
215
159
|
transport: "stdio",
|
|
216
160
|
// Skip install/build if already done (e.g., by startup probe or previous session).
|
|
217
161
|
startupCommand: "([ -d node_modules ] || npm install) && ([ -d dist ] || npm run build) && npm run start",
|
|
218
162
|
mntDataDir: ".",
|
|
219
163
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
164
|
+
const DEPLOY_OPTIONS_CONTAINER_DEFAULTS = {
|
|
165
|
+
transport: "http",
|
|
166
|
+
};
|
|
167
|
+
async function createNew(client, base, organizationId, options) {
|
|
168
|
+
const defaults = options.image
|
|
169
|
+
? DEPLOY_OPTIONS_CONTAINER_DEFAULTS
|
|
170
|
+
: DEPLOY_OPTIONS_LEGACY_DEFAULTS;
|
|
171
|
+
const { name, image, transport, startupCommand, mntDataDir } = {
|
|
172
|
+
...defaults,
|
|
223
173
|
...options,
|
|
224
174
|
};
|
|
225
175
|
if (name == undefined) {
|
|
226
176
|
throw new Error("--name is a required option when creating a new server");
|
|
227
177
|
}
|
|
228
|
-
|
|
178
|
+
if (image != undefined &&
|
|
179
|
+
startupCommand == undefined &&
|
|
180
|
+
!canUseDirectImageStartup({ transport, mntDataDir })) {
|
|
181
|
+
throw new Error("--startup-command is required unless using --transport http without --mnt-data-dir");
|
|
182
|
+
}
|
|
183
|
+
if (image == undefined && startupCommand == undefined) {
|
|
184
|
+
throw new Error("Expected --startup-command to be defined after applying defaults");
|
|
185
|
+
}
|
|
186
|
+
const upload = mntDataDir != undefined || image == undefined
|
|
187
|
+
? await uploadData(client, path.join(base, mntDataDir ?? "."))
|
|
188
|
+
: undefined;
|
|
229
189
|
const config = UserConfigUpdateSchema.parse({
|
|
230
190
|
userGivenName: name,
|
|
231
|
-
|
|
191
|
+
...(image != undefined ? { image } : {}),
|
|
192
|
+
...(startupCommand != undefined ? { command: startupCommand } : {}),
|
|
232
193
|
transport: toHostedTransport(transport),
|
|
233
194
|
replaceEnv: [],
|
|
234
|
-
secretDataZipGcsPath: upload.secretDataZipGcsPath,
|
|
195
|
+
...(upload ? { secretDataZipGcsPath: upload.secretDataZipGcsPath } : {}),
|
|
235
196
|
});
|
|
236
197
|
const createdServer = await client.mcpHost.createServer.mutate({
|
|
237
198
|
config,
|
|
238
199
|
});
|
|
239
200
|
return {
|
|
240
|
-
|
|
241
|
-
|
|
201
|
+
config: HostedConfigSchema.parse({
|
|
202
|
+
hostedId: createdServer.hostedId,
|
|
203
|
+
...(image != undefined
|
|
204
|
+
? { mode: "container", image, transport }
|
|
205
|
+
: {}),
|
|
206
|
+
...(mntDataDir != undefined ? { mntDataDir } : {}),
|
|
207
|
+
organizationId,
|
|
208
|
+
}),
|
|
209
|
+
gatewayId: createdServer.gatewayId,
|
|
242
210
|
};
|
|
243
211
|
}
|
|
244
|
-
async function updateExisting(client, base, config, options) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
212
|
+
async function updateExisting(client, base, config, organizationId, options) {
|
|
213
|
+
if (config.organizationId != undefined &&
|
|
214
|
+
config.organizationId !== organizationId) {
|
|
215
|
+
throw new Error(`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.`);
|
|
248
216
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
217
|
+
const nextTransport = options.transport ?? storedTransport(config);
|
|
218
|
+
const clearCommand = options.image != undefined &&
|
|
219
|
+
options.startupCommand == undefined &&
|
|
220
|
+
options.mntDataDir == undefined &&
|
|
221
|
+
nextTransport === "http";
|
|
222
|
+
if (options.image != undefined &&
|
|
223
|
+
options.startupCommand == undefined &&
|
|
224
|
+
!clearCommand) {
|
|
225
|
+
if (options.mntDataDir != undefined) {
|
|
226
|
+
throw new Error("--startup-command is required when using --mnt-data-dir with --image");
|
|
227
|
+
}
|
|
228
|
+
if (nextTransport == undefined) {
|
|
229
|
+
throw new Error("--startup-command is required unless the resulting transport is known to be http. Pass --transport http to switch to direct image startup.");
|
|
230
|
+
}
|
|
231
|
+
throw new Error("--startup-command is required unless using --transport http without --mnt-data-dir");
|
|
232
|
+
}
|
|
233
|
+
const newConfig = HostedConfigSchema.parse({
|
|
234
|
+
...config,
|
|
235
|
+
...(options.mntDataDir != undefined
|
|
236
|
+
? { mntDataDir: options.mntDataDir }
|
|
237
|
+
: {}),
|
|
238
|
+
...(options.image != undefined
|
|
239
|
+
? {
|
|
240
|
+
mode: "container",
|
|
241
|
+
image: options.image,
|
|
242
|
+
...(nextTransport != undefined ? { transport: nextTransport } : {}),
|
|
243
|
+
}
|
|
244
|
+
: {}),
|
|
245
|
+
...("mode" in config && nextTransport != undefined
|
|
246
|
+
? { transport: nextTransport }
|
|
247
|
+
: {}),
|
|
248
|
+
organizationId,
|
|
249
|
+
});
|
|
250
|
+
const upload = (options.image == undefined || options.mntDataDir != undefined) &&
|
|
251
|
+
newConfig.mntDataDir != undefined
|
|
252
|
+
? await uploadData(client, path.join(base, newConfig.mntDataDir))
|
|
253
|
+
: undefined;
|
|
254
|
+
const updatedServer = await client.mcpHost.partialUpdateServer.mutate({
|
|
251
255
|
hostedId: config.hostedId,
|
|
252
|
-
update: buildPartialUpdate(options, upload
|
|
256
|
+
update: buildPartialUpdate(options, upload?.secretDataZipGcsPath, {
|
|
257
|
+
clearCommand,
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
config: newConfig,
|
|
262
|
+
...(updatedServer.gatewayId != undefined
|
|
263
|
+
? { gatewayId: updatedServer.gatewayId }
|
|
264
|
+
: {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function connectorSettingsUrl(env, gatewayId) {
|
|
268
|
+
const url = new URL("/vmcps", WEB_CLIENT_URL[env]);
|
|
269
|
+
url.searchParams.set("id", gatewayId);
|
|
270
|
+
url.searchParams.set("serverTab", "connector-settings");
|
|
271
|
+
return url.toString();
|
|
272
|
+
}
|
|
273
|
+
function logDeployResult(env, action, result) {
|
|
274
|
+
if (result.gatewayId == undefined) {
|
|
275
|
+
console.log(`${action}.`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(`${action} ${connectorSettingsUrl(env, result.gatewayId)}`);
|
|
279
|
+
}
|
|
280
|
+
function updateConfigAfterManagedPush(config, organizationId, managedImageRef) {
|
|
281
|
+
return HostedConfigSchema.parse({
|
|
282
|
+
...config,
|
|
283
|
+
mode: "container",
|
|
284
|
+
image: managedImageRef,
|
|
285
|
+
organizationId,
|
|
286
|
+
...("mode" in config && config.transport != undefined
|
|
287
|
+
? { transport: config.transport }
|
|
288
|
+
: {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function managedRegistryImageRef(session) {
|
|
292
|
+
return `${session.registryHost}/${session.repository}:${session.imageTag}`;
|
|
293
|
+
}
|
|
294
|
+
async function buildAndPushManagedImage(params) {
|
|
295
|
+
if (params.config.organizationId != undefined &&
|
|
296
|
+
params.config.organizationId !== params.organizationId) {
|
|
297
|
+
throw new Error(`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.`);
|
|
298
|
+
}
|
|
299
|
+
const session = await params.client.mcpHost.createRegistryPushSession.mutate({
|
|
300
|
+
hostedId: params.config.hostedId,
|
|
301
|
+
});
|
|
302
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
303
|
+
const buildResult = buildImage({
|
|
304
|
+
dockerfile: params.dockerfile,
|
|
305
|
+
context: params.context,
|
|
306
|
+
buildArgs: params.buildArg,
|
|
307
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
308
|
+
platform: params.platform,
|
|
309
|
+
imageRef: deployImageRef,
|
|
310
|
+
});
|
|
311
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
312
|
+
pushImage(buildResult.imageRef);
|
|
313
|
+
const finalized = await params.client.mcpHost.finalizeRegistryPush.mutate({
|
|
314
|
+
hostedId: params.config.hostedId,
|
|
315
|
+
finalizeToken: session.finalizeToken,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
config: updateConfigAfterManagedPush(params.config, params.organizationId, deployImageRef),
|
|
319
|
+
...(finalized.gatewayId != undefined
|
|
320
|
+
? { gatewayId: finalized.gatewayId }
|
|
321
|
+
: {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function buildManagedImageCreateConfig(params) {
|
|
325
|
+
const transport = params.transport ?? "http";
|
|
326
|
+
if (params.name == undefined) {
|
|
327
|
+
throw new Error("--name is a required option when building and pushing a new managed image connector");
|
|
328
|
+
}
|
|
329
|
+
if (params.startupCommand == undefined &&
|
|
330
|
+
!canUseDirectImageStartup({ transport, mntDataDir: undefined })) {
|
|
331
|
+
throw new Error("--startup-command is required unless using --transport http");
|
|
332
|
+
}
|
|
333
|
+
return UserConfigUpdateSchema.omit({ image: true }).parse({
|
|
334
|
+
userGivenName: params.name,
|
|
335
|
+
...(params.startupCommand != undefined
|
|
336
|
+
? { command: params.startupCommand }
|
|
337
|
+
: {}),
|
|
338
|
+
transport: toHostedTransport(transport),
|
|
339
|
+
replaceEnv: [],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async function buildAndPushManagedImageForCreate(params) {
|
|
343
|
+
const config = buildManagedImageCreateConfig({
|
|
344
|
+
...(params.name != undefined ? { name: params.name } : {}),
|
|
345
|
+
...(params.transport != undefined ? { transport: params.transport } : {}),
|
|
346
|
+
...(params.startupCommand != undefined
|
|
347
|
+
? { startupCommand: params.startupCommand }
|
|
348
|
+
: {}),
|
|
349
|
+
});
|
|
350
|
+
const session = await params.client.mcpHost.createRegistryPushSessionForCreate.mutate({});
|
|
351
|
+
const deployImageRef = managedRegistryImageRef(session);
|
|
352
|
+
const buildResult = buildImage({
|
|
353
|
+
dockerfile: params.dockerfile,
|
|
354
|
+
context: params.context,
|
|
355
|
+
buildArgs: params.buildArg,
|
|
356
|
+
...(params.target != undefined ? { target: params.target } : {}),
|
|
357
|
+
platform: params.platform,
|
|
358
|
+
imageRef: deployImageRef,
|
|
359
|
+
});
|
|
360
|
+
loginToRegistry(session.registryHost, session.username, session.password);
|
|
361
|
+
pushImage(buildResult.imageRef);
|
|
362
|
+
const finalized = await params.client.mcpHost.finalizeRegistryPushCreate.mutate({
|
|
363
|
+
finalizeToken: session.finalizeToken,
|
|
364
|
+
config,
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
config: HostedConfigSchema.parse({
|
|
368
|
+
hostedId: finalized.hostedId,
|
|
369
|
+
organizationId: params.organizationId,
|
|
370
|
+
mode: "container",
|
|
371
|
+
image: deployImageRef,
|
|
372
|
+
...(params.transport != undefined
|
|
373
|
+
? { transport: params.transport }
|
|
374
|
+
: { transport: "http" }),
|
|
375
|
+
}),
|
|
376
|
+
gatewayId: finalized.gatewayId,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function deployWithOptions(env, base, options) {
|
|
380
|
+
const auth = await getAuth(env);
|
|
381
|
+
await withAuthRetry(env, async (client) => {
|
|
382
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
383
|
+
let result;
|
|
384
|
+
if (fs.existsSync(configPath)) {
|
|
385
|
+
console.log("Updating existing server...");
|
|
386
|
+
const configData = fs.readFileSync(configPath, "utf8");
|
|
387
|
+
result = await updateExisting(client, base, HostedConfigSchema.parse(JSON.parse(configData)), auth.organizationId, options);
|
|
388
|
+
logDeployResult(env, "Updated", result);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
console.log("Creating new server...");
|
|
392
|
+
result = await createNew(client, base, auth.organizationId, options);
|
|
393
|
+
logDeployResult(env, "Created", result);
|
|
394
|
+
}
|
|
395
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
396
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
253
397
|
});
|
|
254
|
-
return newConfig;
|
|
255
398
|
}
|
|
256
399
|
const program = new Command()
|
|
257
400
|
.name("hosted")
|
|
@@ -260,33 +403,261 @@ const program = new Command()
|
|
|
260
403
|
.option("-e, --env <environment>", "Target environment.", argParser(EnvSchema), "production")
|
|
261
404
|
.option("-b, --base <base>", "base directory for config and data.", ".");
|
|
262
405
|
function makeDefaultMessage(optionName) {
|
|
263
|
-
return `
|
|
406
|
+
return `For new zip-based servers, defaults to '${DEPLOY_OPTIONS_LEGACY_DEFAULTS[optionName]}'.`;
|
|
407
|
+
}
|
|
408
|
+
function collect(value, previous) {
|
|
409
|
+
return previous.concat([value]);
|
|
410
|
+
}
|
|
411
|
+
function parsePositiveInt(value) {
|
|
412
|
+
const parsed = Number.parseInt(value, 10);
|
|
413
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
414
|
+
throw new InvalidArgumentError("Expected a positive integer.");
|
|
415
|
+
}
|
|
416
|
+
return parsed;
|
|
417
|
+
}
|
|
418
|
+
function shouldBuildTestImage(options) {
|
|
419
|
+
if (options.build) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (options.image == undefined) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
if (options.dockerfile !== undefined && options.dockerfile !== "Dockerfile") {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (options.context !== undefined && options.context !== ".") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
if ((options.buildArg?.length ?? 0) > 0) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
return options.target != undefined;
|
|
264
435
|
}
|
|
436
|
+
const authCommand = program
|
|
437
|
+
.command("auth")
|
|
438
|
+
.description("Manage cached hosted-cli authentication credentials.");
|
|
439
|
+
authCommand
|
|
440
|
+
.command("login")
|
|
441
|
+
.description("Authenticate and cache credentials for the current environment.")
|
|
442
|
+
.action(async () => {
|
|
443
|
+
const { env } = program.opts();
|
|
444
|
+
const auth = await requestAndStoreAuth(env);
|
|
445
|
+
console.log(`Cached ${formatAuthSummary(auth)}.`);
|
|
446
|
+
});
|
|
447
|
+
authCommand
|
|
448
|
+
.command("list")
|
|
449
|
+
.description("List cached credentials for the current environment.")
|
|
450
|
+
.action(async () => {
|
|
451
|
+
const { env } = program.opts();
|
|
452
|
+
const currentAccount = await getCurrentAuthAccount(env);
|
|
453
|
+
const records = await listAuthRecords(env);
|
|
454
|
+
if (records.length === 0) {
|
|
455
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
for (const record of records) {
|
|
459
|
+
const marker = currentAccount && sameStoredAuthAccount(record.account, currentAccount)
|
|
460
|
+
? "*"
|
|
461
|
+
: " ";
|
|
462
|
+
console.log(`${marker} ${formatAuthSummary(record.auth)}`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
authCommand
|
|
466
|
+
.command("use")
|
|
467
|
+
.description("Select which cached credentials the CLI should use.")
|
|
468
|
+
.option("--user-id <id>", "WorkOS user ID to select.")
|
|
469
|
+
.option("--email <email>", "Email address to select.")
|
|
470
|
+
.option("--organization-id <id>", "Organization ID to select.")
|
|
471
|
+
.action(async (options) => {
|
|
472
|
+
const { env } = program.opts();
|
|
473
|
+
if (!options.userId && !options.email && !options.organizationId) {
|
|
474
|
+
throw new Error('Specify at least one filter, for example "hosted auth use --organization-id <id>".');
|
|
475
|
+
}
|
|
476
|
+
const result = await useAuth(env, options);
|
|
477
|
+
const message = result.mode === "switched" ? "Cached and selected" : "Selected";
|
|
478
|
+
console.log(`${message} ${formatAuthSummary(result.auth)}.`);
|
|
479
|
+
});
|
|
265
480
|
program
|
|
266
481
|
.command("deploy")
|
|
267
482
|
.description("Create or update a hosted server.")
|
|
268
483
|
.option("-n, --name <name>", "Name of the hosted server. Required for new servers.")
|
|
269
|
-
.option("
|
|
270
|
-
.option("
|
|
484
|
+
.option("--image <ref>", "Container image reference (e.g., ghcr.io/org/repo:tag). Skips zip upload when --mnt-data-dir is not also supplied.")
|
|
485
|
+
.option("-t, --transport <transport>", `Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`, argParser(TransportSchema))
|
|
486
|
+
.option("--startup-command <command>", `Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`)
|
|
271
487
|
.option("--mnt-data-dir <directory>", `Directory to copy to /mnt on the server. ${makeDefaultMessage("mntDataDir")}`)
|
|
272
488
|
.action(async (options) => {
|
|
273
489
|
const { env, base } = program.opts();
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
490
|
+
await deployWithOptions(env, base, options);
|
|
491
|
+
});
|
|
492
|
+
program
|
|
493
|
+
.command("test-image")
|
|
494
|
+
.description("Run a local MCP smoke test against a container image. Builds the current Dockerfile by default; with --image, tests that existing image directly.")
|
|
495
|
+
.option("--image <ref>", "Existing image reference to test directly, or the output tag to build when --build or other build options are supplied.")
|
|
496
|
+
.option("--build", "Force a local build before testing, even when --image is supplied.")
|
|
497
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
498
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
499
|
+
.option("--build-arg <arg>", "Build arguments (KEY=VALUE), repeatable", collect, [])
|
|
500
|
+
.option("--target <target>", "Multi-stage build target")
|
|
501
|
+
.option("--probe-path <path>", "HTTP path to probe with MCP initialize", "/mcp")
|
|
502
|
+
.option("--probe-timeout-ms <ms>", "How long to wait for the local MCP probe to succeed", parsePositiveInt, 30_000)
|
|
503
|
+
.option("--env <entry>", "Container runtime env vars for the local smoke test (KEY=VALUE), repeatable", collect, [])
|
|
504
|
+
.option("--env-file <path>", "Path to a Docker env file for the local smoke test")
|
|
505
|
+
.action(async (options) => {
|
|
506
|
+
ensureDockerAvailable();
|
|
507
|
+
ensureDockerBuildxAvailable();
|
|
508
|
+
const shouldBuild = shouldBuildTestImage(options);
|
|
509
|
+
const imageRef = options.image ?? defaultLocalTestImageRef();
|
|
510
|
+
if (shouldBuild) {
|
|
511
|
+
buildImage({
|
|
512
|
+
dockerfile: options.dockerfile,
|
|
513
|
+
context: options.context,
|
|
514
|
+
buildArgs: options.buildArg,
|
|
515
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
516
|
+
imageRef,
|
|
517
|
+
});
|
|
282
518
|
}
|
|
283
519
|
else {
|
|
284
|
-
|
|
285
|
-
newOrUpdatedConfig = await createNew(client, base, options);
|
|
286
|
-
console.log(`Created ${WEB_CLIENT_URL[env]}/hosted/${newOrUpdatedConfig.hostedId}`);
|
|
520
|
+
ensureImageAvailableLocally(imageRef);
|
|
287
521
|
}
|
|
522
|
+
await smokeTestImage({
|
|
523
|
+
imageRef,
|
|
524
|
+
probePath: options.probePath,
|
|
525
|
+
probeTimeoutMs: options.probeTimeoutMs,
|
|
526
|
+
env: options.env,
|
|
527
|
+
...(options.envFile != undefined ? { envFile: options.envFile } : {}),
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
program
|
|
531
|
+
.command("build-and-push")
|
|
532
|
+
.description("Build a Docker image, push it through the managed registry, and finalize the hosted connector image update or creation.")
|
|
533
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
534
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
535
|
+
.option("--build-arg <arg>", "Build arguments (KEY=VALUE), repeatable", collect, [])
|
|
536
|
+
.option("--target <target>", "Multi-stage build target")
|
|
537
|
+
.option("--platform <platform>", `Target platform to build for before push. Defaults to '${defaultBuildPlatform()}'.`, defaultBuildPlatform())
|
|
538
|
+
.option("--name <name>", "Connector name when creating a new hosted connector")
|
|
539
|
+
.option("--transport <transport>", "Transport to configure when creating a new managed connector", parseTransport)
|
|
540
|
+
.option("--startup-command <command>", "Startup command to configure when creating a new managed connector")
|
|
541
|
+
.action(async (options) => {
|
|
542
|
+
const { env, base } = program.opts();
|
|
543
|
+
ensureDockerAvailable();
|
|
544
|
+
ensureDockerBuildxAvailable();
|
|
545
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
546
|
+
console.warn(`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`);
|
|
547
|
+
}
|
|
548
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
549
|
+
const auth = await getAuth(env);
|
|
550
|
+
const hasExistingConfig = fs.existsSync(configPath);
|
|
551
|
+
const result = await withAuthRetry(env, async (client) => {
|
|
552
|
+
if (hasExistingConfig) {
|
|
553
|
+
const config = HostedConfigSchema.parse(JSON.parse(fs.readFileSync(configPath, "utf8")));
|
|
554
|
+
return await buildAndPushManagedImage({
|
|
555
|
+
client,
|
|
556
|
+
organizationId: auth.organizationId,
|
|
557
|
+
config,
|
|
558
|
+
dockerfile: options.dockerfile,
|
|
559
|
+
context: options.context,
|
|
560
|
+
buildArg: options.buildArg,
|
|
561
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
562
|
+
platform: options.platform,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return await buildAndPushManagedImageForCreate({
|
|
566
|
+
client,
|
|
567
|
+
organizationId: auth.organizationId,
|
|
568
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
569
|
+
...(options.transport != undefined
|
|
570
|
+
? { transport: options.transport }
|
|
571
|
+
: {}),
|
|
572
|
+
...(options.startupCommand != undefined
|
|
573
|
+
? { startupCommand: options.startupCommand }
|
|
574
|
+
: {}),
|
|
575
|
+
dockerfile: options.dockerfile,
|
|
576
|
+
context: options.context,
|
|
577
|
+
buildArg: options.buildArg,
|
|
578
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
579
|
+
platform: options.platform,
|
|
580
|
+
});
|
|
581
|
+
});
|
|
288
582
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
289
|
-
fs.writeFileSync(configPath, JSON.stringify(
|
|
583
|
+
fs.writeFileSync(configPath, JSON.stringify(result.config, null, 2));
|
|
584
|
+
logDeployResult(env, hasExistingConfig ? "Updated" : "Created", result);
|
|
585
|
+
});
|
|
586
|
+
program
|
|
587
|
+
.command("build-and-deploy")
|
|
588
|
+
.description("Build a Docker image for hosted deployment, push it, verify the target platform, and deploy it.")
|
|
589
|
+
.requiredOption("--image <ref>", "Image reference to build, push, and deploy (e.g., ghcr.io/org/repo:tag).")
|
|
590
|
+
.option("--dockerfile <path>", "Path to Dockerfile", "Dockerfile")
|
|
591
|
+
.option("--context <dir>", "Docker build context directory", ".")
|
|
592
|
+
.option("--build-arg <arg>", "Build arguments (KEY=VALUE), repeatable", collect, [])
|
|
593
|
+
.option("--target <target>", "Multi-stage build target")
|
|
594
|
+
.option("--platform <platform>", `Target platform to build for before deploy. Defaults to '${defaultBuildPlatform()}'.`, defaultBuildPlatform())
|
|
595
|
+
.option("-n, --name <name>", "Name of the hosted server. Required for new servers.")
|
|
596
|
+
.option("-t, --transport <transport>", `Transport (stdio or http). Defaults to 'http' for image-based creates. ${makeDefaultMessage("transport")}`, argParser(TransportSchema))
|
|
597
|
+
.option("--startup-command <command>", `Command that runs on hosted container to start the server (runs in bash). Required for stdio and zip-backed image deploys. ${makeDefaultMessage("startupCommand")}`)
|
|
598
|
+
.option("--mnt-data-dir <directory>", `Directory to copy to /mnt on the server. ${makeDefaultMessage("mntDataDir")}`)
|
|
599
|
+
.action(async (options) => {
|
|
600
|
+
const { env, base } = program.opts();
|
|
601
|
+
ensureDockerAvailable();
|
|
602
|
+
ensureDockerBuildxAvailable();
|
|
603
|
+
if (options.platform !== defaultBuildPlatform()) {
|
|
604
|
+
console.warn(`Building for ${options.platform}. Hosted deployments typically target ${defaultBuildPlatform()}; choose a different platform only when you know the runtime is compatible.`);
|
|
605
|
+
}
|
|
606
|
+
const buildResult = buildImage({
|
|
607
|
+
dockerfile: options.dockerfile,
|
|
608
|
+
context: options.context,
|
|
609
|
+
buildArgs: options.buildArg,
|
|
610
|
+
...(options.target != undefined ? { target: options.target } : {}),
|
|
611
|
+
platform: options.platform,
|
|
612
|
+
imageRef: options.image,
|
|
613
|
+
});
|
|
614
|
+
pushImage(buildResult.imageRef);
|
|
615
|
+
assertImageSupportsPlatform(buildResult.imageRef, options.platform);
|
|
616
|
+
await deployWithOptions(env, base, {
|
|
617
|
+
image: buildResult.imageRef,
|
|
618
|
+
...(options.name != undefined ? { name: options.name } : {}),
|
|
619
|
+
...(options.transport != undefined
|
|
620
|
+
? { transport: options.transport }
|
|
621
|
+
: {}),
|
|
622
|
+
...(options.startupCommand != undefined
|
|
623
|
+
? { startupCommand: options.startupCommand }
|
|
624
|
+
: {}),
|
|
625
|
+
...(options.mntDataDir != undefined
|
|
626
|
+
? { mntDataDir: options.mntDataDir }
|
|
627
|
+
: {}),
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
program
|
|
631
|
+
.command("logout")
|
|
632
|
+
.description("Clear stored credentials and sign out of the WorkOS browser session.")
|
|
633
|
+
.option("--all", "Clear all cached credentials for the current environment.")
|
|
634
|
+
.action(async (options) => {
|
|
635
|
+
const { env } = program.opts();
|
|
636
|
+
let logoutUrl;
|
|
637
|
+
const selectedAuth = await getSelectedAuth(env);
|
|
638
|
+
if (selectedAuth) {
|
|
639
|
+
const { sid } = getTokenClaims(selectedAuth.accessToken);
|
|
640
|
+
if (sid) {
|
|
641
|
+
logoutUrl = `https://api.workos.com/user_management/sessions/logout?session_id=${sid}`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (options.all) {
|
|
645
|
+
await clearAllAuth(env);
|
|
646
|
+
console.log(`All cached credentials cleared for ${env}.`);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const clearedAuth = await clearSelectedAuth(env);
|
|
650
|
+
if (clearedAuth) {
|
|
651
|
+
console.log(`Cleared cached credentials for ${formatAuthSummary(clearedAuth)}.`);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (logoutUrl) {
|
|
658
|
+
console.log("To fully sign out of the WorkOS browser session, visit:");
|
|
659
|
+
console.log(logoutUrl);
|
|
660
|
+
}
|
|
290
661
|
});
|
|
291
662
|
program.parse();
|
|
292
663
|
//# sourceMappingURL=index.js.map
|