@amerged/ohmyhost-mcp 0.1.19 → 0.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-mcp",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -26,6 +26,8 @@ import {
26
26
  createProjectDevAccessTicket,
27
27
  ensureProjectDevShareLink,
28
28
  setProjectDevAccessMode,
29
+ getProjectPoweredByFlag,
30
+ setProjectPoweredByFlag,
29
31
  rotateProjectDevShareLink,
30
32
  revokeProjectDevShareLink,
31
33
  createCloudflareDnsAuthorization,
@@ -52,6 +54,7 @@ import {
52
54
  deletePaidProjectDomain,
53
55
  getOrganizationCredits,
54
56
  getOrganizationAccount,
57
+ getOrganizationReferral,
55
58
  getOrganizationCreditUsage,
56
59
  getProjectCreditBudget,
57
60
  setProjectCreditBudget,
@@ -93,6 +96,8 @@ import {
93
96
  streamOperationEvents,
94
97
  type DevAccessTicket,
95
98
  type DevAccessState,
99
+ type PoweredByFlag,
100
+ type OrganizationReferral,
96
101
  type CloudflareDnsAuthorization,
97
102
  type CloudflareDnsAuthorizationStatus,
98
103
  type OhMyHostClient,
@@ -101,16 +106,20 @@ import {
101
106
  } from "@ohmyhost/sdk-ts";
102
107
 
103
108
  import type {
109
+ IdentityReport,
104
110
  LocalMcpProductClient,
105
111
  WorkspaceReport,
106
112
  WorkspaceSelectionReport,
107
113
  } from "./local-server.js";
108
114
  import {
109
115
  createAuthenticatedProductTransport,
116
+ resolveProductCliEnvironment,
110
117
  prepareUserTokenFile,
111
118
  ProductApiAuthenticationError,
112
119
  parseDevAccessTicket,
113
120
  parseDevAccessState,
121
+ parsePoweredByFlag,
122
+ parseOrganizationReferral,
114
123
  parseCloudflareDnsAuthorization,
115
124
  parseCloudflareDnsAuthorizationStatus,
116
125
  parsePaidDomain,
@@ -119,11 +128,13 @@ import {
119
128
  parseAccountProfile,
120
129
  parseCurrentIdentity,
121
130
  chooseWorkspace,
131
+ publicProfile,
122
132
  publicWorkspace,
123
133
  selectWorkspace,
124
- selectedWorkspaceId,
125
134
  InteractiveSessionRequiredError,
126
135
  type AuthenticatedProductApiOverrides,
136
+ type EffectiveContext,
137
+ type LocalProfile,
127
138
  type WorkspaceSession,
128
139
  } from "@ohmyhost/product-cli";
129
140
 
@@ -134,17 +145,26 @@ export class OperationEventsUnavailableError extends Error {
134
145
  }
135
146
  }
136
147
 
148
+ /**
149
+ * One client per tool call, for the login that call asked for. The process binding and the call's
150
+ * own arguments choose it, so another agent's choice never changes which account a call uses.
151
+ */
137
152
  export async function createAuthenticatedLocalMcpClient(
138
153
  signal: AbortSignal,
139
- overrides: Omit<AuthenticatedProductApiOverrides, "productApiOrigin"> = {},
154
+ overrides: Omit<AuthenticatedProductApiOverrides, "productApiOrigin" | "selector"> = {},
140
155
  ): Promise<LocalMcpProductClient> {
141
156
  try {
142
- const transport = await createAuthenticatedProductTransport(signal, overrides);
157
+ const transport = await createAuthenticatedProductTransport(signal, {
158
+ ...overrides,
159
+ selector: "mcp",
160
+ });
143
161
  return new GeneratedSdkLocalMcpClient(
144
162
  transport.baseUrl,
145
163
  transport.accessToken,
146
164
  transport.fetch,
147
165
  transport.workspace,
166
+ transport.context,
167
+ resolveProductCliEnvironment(overrides.environment).commandPrefix,
148
168
  );
149
169
  } catch (error) {
150
170
  if (!(error instanceof ProductApiAuthenticationError)) throw error;
@@ -160,9 +180,13 @@ export async function createAuthenticatedLocalMcpClient(
160
180
  }
161
181
 
162
182
  export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
163
- readonly #client: OhMyHostClient;
183
+ #client: OhMyHostClient;
164
184
 
165
- readonly #workspace: WorkspaceSession | undefined;
185
+ #workspace: WorkspaceSession | undefined;
186
+
187
+ readonly #context: EffectiveContext;
188
+
189
+ readonly #commandPrefix: string;
166
190
 
167
191
  readonly #rescope: (accessToken: string) => OhMyHostClient;
168
192
 
@@ -171,10 +195,50 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
171
195
  accessToken: string,
172
196
  fetch: typeof globalThis.fetch,
173
197
  workspace?: WorkspaceSession,
198
+ context?: EffectiveContext,
199
+ commandPrefix = "OHMYHOST_ENVIRONMENT=production ohmyhost",
174
200
  ) {
175
201
  this.#rescope = (token) => createClient({ auth: token, baseUrl, fetch, throwOnError: true });
176
202
  this.#client = this.#rescope(accessToken);
177
203
  this.#workspace = workspace;
204
+ this.#context = context ?? {
205
+ credential: workspace === undefined ? "environment_token" : "profile",
206
+ profile: workspace?.profile ?? null,
207
+ };
208
+ this.#commandPrefix = commandPrefix;
209
+ }
210
+
211
+ public actingAs(): IdentityReport["context"] {
212
+ return this.#contextReport();
213
+ }
214
+
215
+ /** Which account this call acts as, without any credential. */
216
+ #contextReport(): IdentityReport["context"] {
217
+ const profile = this.#workspace?.profile ?? this.#context.profile;
218
+ return {
219
+ credential: this.#context.credential,
220
+ profile: profile === null ? null : publicProfile(profile),
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Bind this call's login, which has no organization yet, and continue with its new credential.
226
+ * A login in another organization keeps it; the provider verifies the account first.
227
+ */
228
+ async #bind(
229
+ organization: Parameters<typeof selectWorkspace>[1],
230
+ signal: AbortSignal,
231
+ ): Promise<LocalProfile> {
232
+ const session = this.#interactiveSession();
233
+ const bound = await selectWorkspace(session, organization, signal, (token) =>
234
+ getCurrentIdentity({}, { client: this.#rescope(token) }),
235
+ );
236
+ const store = session.profiles.credential(bound);
237
+ const credential = await store.read();
238
+ if (credential === undefined) throw new InteractiveSessionRequiredError();
239
+ this.#workspace = { ...session, store, profile: bound };
240
+ this.#client = this.#rescope(credential.accessToken);
241
+ return bound;
178
242
  }
179
243
 
180
244
  public async changeDatabaseCompute(input: {
@@ -275,20 +339,20 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
275
339
  { client: this.#client },
276
340
  );
277
341
  }
278
- public async getCurrentIdentity(signal: AbortSignal) {
342
+ public async getCurrentIdentity(signal: AbortSignal): Promise<IdentityReport> {
279
343
  const identity = await getCurrentIdentity({}, { client: this.#client });
280
344
  // A session signed in before a workspace could be selected carries none, and then every
281
345
  // project call answers with an empty page that reads like an empty account.
282
346
  if (this.#workspace === undefined || parseCurrentIdentity(identity).organization_ids.length > 0)
283
- return identity;
347
+ return { ...identity, context: this.#contextReport() };
284
348
  const selection = chooseWorkspace(await this.#readWorkspaces(), undefined);
285
- if (selection.outcome !== "selected") return identity;
286
- await selectWorkspace(this.#workspace, selection.organization, signal);
349
+ if (selection.outcome !== "selected") return { ...identity, context: this.#contextReport() };
287
350
  // The workspace lives in the access token, so the repaired identity needs the new one.
288
- const scoped = (await this.#workspace.store.read())?.accessToken;
289
- return scoped === undefined
290
- ? identity
291
- : getCurrentIdentity({}, { client: this.#rescope(scoped) });
351
+ await this.#bind(selection.organization, signal);
352
+ return {
353
+ ...(await getCurrentIdentity({}, { client: this.#client })),
354
+ context: this.#contextReport(),
355
+ };
292
356
  }
293
357
  public async changeProjectHandle(input: {
294
358
  projectId: string;
@@ -362,6 +426,11 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
362
426
  assertOrganizationAccount(account);
363
427
  return account;
364
428
  }
429
+ public async getOrganizationReferral(organizationId: string): Promise<OrganizationReferral> {
430
+ return parseOrganizationReferral(
431
+ await getOrganizationReferral({ organization_id: organizationId }, { client: this.#client }),
432
+ );
433
+ }
365
434
  public getOrganizationCredits(organizationId: string) {
366
435
  return getOrganizationCredits({ organization_id: organizationId }, { client: this.#client });
367
436
  }
@@ -466,6 +535,16 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
466
535
  },
467
536
  { client: this.#client },
468
537
  );
538
+ const current = this.#interactiveSession().profile;
539
+ // A login in another organization keeps it, so no other agent using it moves along; the new
540
+ // workspace gets its own login instead.
541
+ if (current.organizationId !== null && current.organizationId !== created.id)
542
+ return {
543
+ organization: { id: created.id, name: created.name },
544
+ selected: current.organizationId,
545
+ profile: publicProfile(current),
546
+ next_action: `Run '${this.#commandPrefix} login --organization ${created.id} --user ${current.userId} --json' to add a login for the new workspace as the same user, then pass its profile_name.`,
547
+ };
469
548
  // A created workspace only becomes usable once the access token carries it.
470
549
  return this.selectOrganization(created.id, signal);
471
550
  }
@@ -474,7 +553,8 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
474
553
  const session = this.#interactiveSession();
475
554
  return {
476
555
  organizations: (await this.#readWorkspaces()).map(publicWorkspace),
477
- selected: await selectedWorkspaceId(session),
556
+ selected: session.profile.organizationId,
557
+ profile: publicProfile(session.profile),
478
558
  };
479
559
  }
480
560
 
@@ -482,20 +562,21 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
482
562
  organizationId: string,
483
563
  signal: AbortSignal,
484
564
  ): Promise<WorkspaceSelectionReport> {
485
- const session = this.#interactiveSession();
565
+ this.#interactiveSession();
486
566
  const selection = chooseWorkspace(await this.#readWorkspaces(), organizationId);
487
567
  if (selection.outcome !== "selected")
488
568
  throw new Error(`ORGANIZATION_NOT_FOUND:${organizationId}`);
489
- await selectWorkspace(session, selection.organization, signal);
569
+ const profile = await this.#bind(selection.organization, signal);
490
570
  return {
491
571
  organization: publicWorkspace(selection.organization),
492
572
  selected: selection.organization.id,
573
+ profile: publicProfile(profile),
493
574
  };
494
575
  }
495
576
 
496
577
  async #readWorkspaces() {
497
578
  this.#interactiveSession();
498
- return parseAccountProfile(await getAccountProfile({ client: this.#client }));
579
+ return parseAccountProfile(await getAccountProfile({ client: this.#client })).organizations;
499
580
  }
500
581
 
501
582
  #interactiveSession(): WorkspaceSession {
@@ -537,6 +618,27 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
537
618
  ),
538
619
  );
539
620
  }
621
+ public async getPoweredByFlag(projectId: string): Promise<PoweredByFlag> {
622
+ return parsePoweredByFlag(
623
+ await getProjectPoweredByFlag({ project_id: projectId }, { client: this.#client }),
624
+ );
625
+ }
626
+ public async setPoweredByFlag(input: {
627
+ projectId: string;
628
+ enabled: boolean;
629
+ idempotencyKey: string;
630
+ }): Promise<PoweredByFlag> {
631
+ return parsePoweredByFlag(
632
+ await setProjectPoweredByFlag(
633
+ {
634
+ project_id: input.projectId,
635
+ "Idempotency-Key": input.idempotencyKey,
636
+ setPoweredByFlagRequest: { enabled: input.enabled },
637
+ },
638
+ { client: this.#client },
639
+ ),
640
+ );
641
+ }
540
642
  public async rotateDevShareLink(input: {
541
643
  projectId: string;
542
644
  idempotencyKey: string;