@amerged/ohmyhost-mcp 0.1.18 → 0.1.20
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 +1 -1
- package/dist/{chunk-X6IS5LX7.js → chunk-NESQZ3BS.js} +7 -7
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +1316 -165
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +86 -17
- package/src/apps/mcp/local-server.ts +280 -86
- package/src/apps/mcp/stdio-main.ts +11 -2
- package/src/apps/product-cli/cli.ts +337 -92
- package/src/apps/product-cli/command.ts +111 -6
- package/src/apps/product-cli/composition.ts +748 -120
- package/src/apps/product-cli/credential-store.ts +5 -3
- package/src/apps/product-cli/index.ts +2 -0
- package/src/apps/product-cli/product-api.ts +4 -4
- package/src/apps/product-cli/profile-store.ts +609 -0
- package/src/apps/product-cli/project-link-store.ts +169 -20
- package/src/apps/product-cli/runtime-contract.ts +15 -4
- package/src/apps/product-cli/workspace.ts +75 -13
- package/src/packages/agent-skills/generated-skill-resources.ts +6 -6
- package/src/packages/sdk-ts/generated/sdk.gen.ts +1 -1
package/package.json
CHANGED
|
@@ -101,12 +101,14 @@ import {
|
|
|
101
101
|
} from "@ohmyhost/sdk-ts";
|
|
102
102
|
|
|
103
103
|
import type {
|
|
104
|
+
IdentityReport,
|
|
104
105
|
LocalMcpProductClient,
|
|
105
106
|
WorkspaceReport,
|
|
106
107
|
WorkspaceSelectionReport,
|
|
107
108
|
} from "./local-server.js";
|
|
108
109
|
import {
|
|
109
110
|
createAuthenticatedProductTransport,
|
|
111
|
+
resolveProductCliEnvironment,
|
|
110
112
|
prepareUserTokenFile,
|
|
111
113
|
ProductApiAuthenticationError,
|
|
112
114
|
parseDevAccessTicket,
|
|
@@ -119,11 +121,13 @@ import {
|
|
|
119
121
|
parseAccountProfile,
|
|
120
122
|
parseCurrentIdentity,
|
|
121
123
|
chooseWorkspace,
|
|
124
|
+
publicProfile,
|
|
122
125
|
publicWorkspace,
|
|
123
126
|
selectWorkspace,
|
|
124
|
-
selectedWorkspaceId,
|
|
125
127
|
InteractiveSessionRequiredError,
|
|
126
128
|
type AuthenticatedProductApiOverrides,
|
|
129
|
+
type EffectiveContext,
|
|
130
|
+
type LocalProfile,
|
|
127
131
|
type WorkspaceSession,
|
|
128
132
|
} from "@ohmyhost/product-cli";
|
|
129
133
|
|
|
@@ -134,17 +138,26 @@ export class OperationEventsUnavailableError extends Error {
|
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
/**
|
|
142
|
+
* One client per tool call, for the login that call asked for. The process binding and the call's
|
|
143
|
+
* own arguments choose it, so another agent's choice never changes which account a call uses.
|
|
144
|
+
*/
|
|
137
145
|
export async function createAuthenticatedLocalMcpClient(
|
|
138
146
|
signal: AbortSignal,
|
|
139
|
-
overrides: Omit<AuthenticatedProductApiOverrides, "productApiOrigin"> = {},
|
|
147
|
+
overrides: Omit<AuthenticatedProductApiOverrides, "productApiOrigin" | "selector"> = {},
|
|
140
148
|
): Promise<LocalMcpProductClient> {
|
|
141
149
|
try {
|
|
142
|
-
const transport = await createAuthenticatedProductTransport(signal,
|
|
150
|
+
const transport = await createAuthenticatedProductTransport(signal, {
|
|
151
|
+
...overrides,
|
|
152
|
+
selector: "mcp",
|
|
153
|
+
});
|
|
143
154
|
return new GeneratedSdkLocalMcpClient(
|
|
144
155
|
transport.baseUrl,
|
|
145
156
|
transport.accessToken,
|
|
146
157
|
transport.fetch,
|
|
147
158
|
transport.workspace,
|
|
159
|
+
transport.context,
|
|
160
|
+
resolveProductCliEnvironment(overrides.environment).commandPrefix,
|
|
148
161
|
);
|
|
149
162
|
} catch (error) {
|
|
150
163
|
if (!(error instanceof ProductApiAuthenticationError)) throw error;
|
|
@@ -160,9 +173,13 @@ export async function createAuthenticatedLocalMcpClient(
|
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
163
|
-
|
|
176
|
+
#client: OhMyHostClient;
|
|
164
177
|
|
|
165
|
-
|
|
178
|
+
#workspace: WorkspaceSession | undefined;
|
|
179
|
+
|
|
180
|
+
readonly #context: EffectiveContext;
|
|
181
|
+
|
|
182
|
+
readonly #commandPrefix: string;
|
|
166
183
|
|
|
167
184
|
readonly #rescope: (accessToken: string) => OhMyHostClient;
|
|
168
185
|
|
|
@@ -171,10 +188,50 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
|
171
188
|
accessToken: string,
|
|
172
189
|
fetch: typeof globalThis.fetch,
|
|
173
190
|
workspace?: WorkspaceSession,
|
|
191
|
+
context?: EffectiveContext,
|
|
192
|
+
commandPrefix = "OHMYHOST_ENVIRONMENT=production ohmyhost",
|
|
174
193
|
) {
|
|
175
194
|
this.#rescope = (token) => createClient({ auth: token, baseUrl, fetch, throwOnError: true });
|
|
176
195
|
this.#client = this.#rescope(accessToken);
|
|
177
196
|
this.#workspace = workspace;
|
|
197
|
+
this.#context = context ?? {
|
|
198
|
+
credential: workspace === undefined ? "environment_token" : "profile",
|
|
199
|
+
profile: workspace?.profile ?? null,
|
|
200
|
+
};
|
|
201
|
+
this.#commandPrefix = commandPrefix;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public actingAs(): IdentityReport["context"] {
|
|
205
|
+
return this.#contextReport();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Which account this call acts as, without any credential. */
|
|
209
|
+
#contextReport(): IdentityReport["context"] {
|
|
210
|
+
const profile = this.#workspace?.profile ?? this.#context.profile;
|
|
211
|
+
return {
|
|
212
|
+
credential: this.#context.credential,
|
|
213
|
+
profile: profile === null ? null : publicProfile(profile),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Bind this call's login, which has no organization yet, and continue with its new credential.
|
|
219
|
+
* A login in another organization keeps it; the provider verifies the account first.
|
|
220
|
+
*/
|
|
221
|
+
async #bind(
|
|
222
|
+
organization: Parameters<typeof selectWorkspace>[1],
|
|
223
|
+
signal: AbortSignal,
|
|
224
|
+
): Promise<LocalProfile> {
|
|
225
|
+
const session = this.#interactiveSession();
|
|
226
|
+
const bound = await selectWorkspace(session, organization, signal, (token) =>
|
|
227
|
+
getCurrentIdentity({}, { client: this.#rescope(token) }),
|
|
228
|
+
);
|
|
229
|
+
const store = session.profiles.credential(bound);
|
|
230
|
+
const credential = await store.read();
|
|
231
|
+
if (credential === undefined) throw new InteractiveSessionRequiredError();
|
|
232
|
+
this.#workspace = { ...session, store, profile: bound };
|
|
233
|
+
this.#client = this.#rescope(credential.accessToken);
|
|
234
|
+
return bound;
|
|
178
235
|
}
|
|
179
236
|
|
|
180
237
|
public async changeDatabaseCompute(input: {
|
|
@@ -275,20 +332,20 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
|
275
332
|
{ client: this.#client },
|
|
276
333
|
);
|
|
277
334
|
}
|
|
278
|
-
public async getCurrentIdentity(signal: AbortSignal) {
|
|
335
|
+
public async getCurrentIdentity(signal: AbortSignal): Promise<IdentityReport> {
|
|
279
336
|
const identity = await getCurrentIdentity({}, { client: this.#client });
|
|
280
337
|
// A session signed in before a workspace could be selected carries none, and then every
|
|
281
338
|
// project call answers with an empty page that reads like an empty account.
|
|
282
339
|
if (this.#workspace === undefined || parseCurrentIdentity(identity).organization_ids.length > 0)
|
|
283
|
-
return identity;
|
|
340
|
+
return { ...identity, context: this.#contextReport() };
|
|
284
341
|
const selection = chooseWorkspace(await this.#readWorkspaces(), undefined);
|
|
285
|
-
if (selection.outcome !== "selected") return identity;
|
|
286
|
-
await selectWorkspace(this.#workspace, selection.organization, signal);
|
|
342
|
+
if (selection.outcome !== "selected") return { ...identity, context: this.#contextReport() };
|
|
287
343
|
// The workspace lives in the access token, so the repaired identity needs the new one.
|
|
288
|
-
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
:
|
|
344
|
+
await this.#bind(selection.organization, signal);
|
|
345
|
+
return {
|
|
346
|
+
...(await getCurrentIdentity({}, { client: this.#client })),
|
|
347
|
+
context: this.#contextReport(),
|
|
348
|
+
};
|
|
292
349
|
}
|
|
293
350
|
public async changeProjectHandle(input: {
|
|
294
351
|
projectId: string;
|
|
@@ -466,6 +523,16 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
|
466
523
|
},
|
|
467
524
|
{ client: this.#client },
|
|
468
525
|
);
|
|
526
|
+
const current = this.#interactiveSession().profile;
|
|
527
|
+
// A login in another organization keeps it, so no other agent using it moves along; the new
|
|
528
|
+
// workspace gets its own login instead.
|
|
529
|
+
if (current.organizationId !== null && current.organizationId !== created.id)
|
|
530
|
+
return {
|
|
531
|
+
organization: { id: created.id, name: created.name },
|
|
532
|
+
selected: current.organizationId,
|
|
533
|
+
profile: publicProfile(current),
|
|
534
|
+
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.`,
|
|
535
|
+
};
|
|
469
536
|
// A created workspace only becomes usable once the access token carries it.
|
|
470
537
|
return this.selectOrganization(created.id, signal);
|
|
471
538
|
}
|
|
@@ -474,7 +541,8 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
|
474
541
|
const session = this.#interactiveSession();
|
|
475
542
|
return {
|
|
476
543
|
organizations: (await this.#readWorkspaces()).map(publicWorkspace),
|
|
477
|
-
selected:
|
|
544
|
+
selected: session.profile.organizationId,
|
|
545
|
+
profile: publicProfile(session.profile),
|
|
478
546
|
};
|
|
479
547
|
}
|
|
480
548
|
|
|
@@ -482,20 +550,21 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
|
|
|
482
550
|
organizationId: string,
|
|
483
551
|
signal: AbortSignal,
|
|
484
552
|
): Promise<WorkspaceSelectionReport> {
|
|
485
|
-
|
|
553
|
+
this.#interactiveSession();
|
|
486
554
|
const selection = chooseWorkspace(await this.#readWorkspaces(), organizationId);
|
|
487
555
|
if (selection.outcome !== "selected")
|
|
488
556
|
throw new Error(`ORGANIZATION_NOT_FOUND:${organizationId}`);
|
|
489
|
-
await
|
|
557
|
+
const profile = await this.#bind(selection.organization, signal);
|
|
490
558
|
return {
|
|
491
559
|
organization: publicWorkspace(selection.organization),
|
|
492
560
|
selected: selection.organization.id,
|
|
561
|
+
profile: publicProfile(profile),
|
|
493
562
|
};
|
|
494
563
|
}
|
|
495
564
|
|
|
496
565
|
async #readWorkspaces() {
|
|
497
566
|
this.#interactiveSession();
|
|
498
|
-
return parseAccountProfile(await getAccountProfile({ client: this.#client }));
|
|
567
|
+
return parseAccountProfile(await getAccountProfile({ client: this.#client })).organizations;
|
|
499
568
|
}
|
|
500
569
|
|
|
501
570
|
#interactiveSession(): WorkspaceSession {
|