@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/README.md +1 -1
- package/dist/{chunk-PXGLL6SR.js → chunk-4WY7AAPR.js} +11 -11
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +1575 -310
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +119 -17
- package/src/apps/mcp/local-server.ts +321 -88
- package/src/apps/mcp/stdio-main.ts +11 -2
- package/src/apps/product-cli/cli.ts +370 -92
- package/src/apps/product-cli/command.ts +163 -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 +4 -0
- package/src/apps/product-cli/product-api.ts +32 -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 +63 -7
- package/src/apps/product-cli/workspace.ts +75 -13
- package/src/packages/agent-skills/generated-skill-resources.ts +10 -10
- package/src/packages/contracts/operation-failure.ts +3 -3
- package/src/packages/sdk-ts/generated/index.ts +21 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +142 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +200 -2
|
@@ -9,9 +9,16 @@ import {
|
|
|
9
9
|
} from "@modelcontextprotocol/server";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import {
|
|
12
|
+
ACCOUNT_SENSITIVE_PROBLEMS,
|
|
13
|
+
CredentialStoreUnavailableError,
|
|
14
|
+
isProfileUserId,
|
|
15
|
+
parseCurrentIdentity,
|
|
16
|
+
profileLimitAction,
|
|
17
|
+
ProfileSelectionError,
|
|
12
18
|
resolveProductCliEnvironment,
|
|
13
19
|
parseSafeProblem,
|
|
14
20
|
type ProductCliEnvironment,
|
|
21
|
+
type ProfileRequestInput,
|
|
15
22
|
} from "@ohmyhost/product-cli";
|
|
16
23
|
import { createOhmyhostMcpServer } from "./index.js";
|
|
17
24
|
|
|
@@ -63,6 +70,12 @@ export interface LocalMcpProductClient {
|
|
|
63
70
|
projectId: string;
|
|
64
71
|
idempotencyKey: string;
|
|
65
72
|
}): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
|
|
73
|
+
getPoweredByFlag(projectId: string): Promise<import("@ohmyhost/sdk-ts").PoweredByFlag>;
|
|
74
|
+
setPoweredByFlag(input: {
|
|
75
|
+
projectId: string;
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
idempotencyKey: string;
|
|
78
|
+
}): Promise<import("@ohmyhost/sdk-ts").PoweredByFlag>;
|
|
66
79
|
revokeDevShareLink(input: {
|
|
67
80
|
projectId: string;
|
|
68
81
|
idempotencyKey: string;
|
|
@@ -149,6 +162,9 @@ export interface LocalMcpProductClient {
|
|
|
149
162
|
getOrganizationAccount(
|
|
150
163
|
organizationId: string,
|
|
151
164
|
): Promise<import("@ohmyhost/sdk-ts").OrganizationAccount>;
|
|
165
|
+
getOrganizationReferral(
|
|
166
|
+
organizationId: string,
|
|
167
|
+
): Promise<import("@ohmyhost/sdk-ts").OrganizationReferral>;
|
|
152
168
|
getOrganizationCredits(
|
|
153
169
|
organizationId: string,
|
|
154
170
|
): Promise<import("@ohmyhost/sdk-ts").OrganizationCredits>;
|
|
@@ -174,7 +190,9 @@ export interface LocalMcpProductClient {
|
|
|
174
190
|
organizationId: string,
|
|
175
191
|
signal: AbortSignal,
|
|
176
192
|
): Promise<WorkspaceSelectionReport>;
|
|
177
|
-
getCurrentIdentity(signal: AbortSignal): Promise<
|
|
193
|
+
getCurrentIdentity(signal: AbortSignal): Promise<IdentityReport>;
|
|
194
|
+
/** The account this client acts as, for errors whose meaning depends on it. */
|
|
195
|
+
actingAs?(): IdentityReport["context"];
|
|
178
196
|
listProjects(input: { readonly cursor?: string; readonly limit?: number }): Promise<ProjectPage>;
|
|
179
197
|
submitFeedback(input: {
|
|
180
198
|
report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
|
|
@@ -261,21 +279,53 @@ export interface PublicWorkspaceSummary {
|
|
|
261
279
|
readonly name: string;
|
|
262
280
|
}
|
|
263
281
|
|
|
264
|
-
/**
|
|
282
|
+
/** One saved login as `profile_list` shows it; it never carries a credential. */
|
|
283
|
+
export interface PublicProfileSummary {
|
|
284
|
+
readonly name: string;
|
|
285
|
+
readonly user_id: string;
|
|
286
|
+
readonly organization_id: string | null;
|
|
287
|
+
readonly organization_name: string | null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The identity a call acted as, and which saved login or OHMYHOST_TOKEN supplied it. */
|
|
291
|
+
export type IdentityReport = CurrentIdentity & {
|
|
292
|
+
readonly context: {
|
|
293
|
+
readonly credential: "profile" | "environment_token";
|
|
294
|
+
readonly profile: PublicProfileSummary | null;
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** The workspaces of the signed-in user and the one the chosen login is scoped to. */
|
|
265
299
|
export interface WorkspaceReport {
|
|
266
300
|
readonly organizations: readonly PublicWorkspaceSummary[];
|
|
267
301
|
readonly selected: string | null;
|
|
302
|
+
readonly profile: PublicProfileSummary;
|
|
268
303
|
}
|
|
269
304
|
|
|
270
305
|
/** One workspace that was just created or selected, with the resulting scope. */
|
|
271
306
|
export interface WorkspaceSelectionReport {
|
|
272
307
|
readonly organization: PublicWorkspaceSummary;
|
|
273
308
|
readonly selected: string;
|
|
309
|
+
readonly profile: PublicProfileSummary;
|
|
310
|
+
readonly next_action?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The saved logins of this computer and how this MCP process chooses among them. */
|
|
314
|
+
export interface ProfileListReport {
|
|
315
|
+
readonly profiles: readonly PublicProfileSummary[];
|
|
316
|
+
readonly bound: string | null;
|
|
317
|
+
readonly environment_token: boolean;
|
|
274
318
|
}
|
|
275
319
|
|
|
276
320
|
export interface LocalOhmyhostMcpDependencies {
|
|
277
321
|
readonly environment?: ProductCliEnvironment;
|
|
278
|
-
|
|
322
|
+
/** One client for one call; the request names the login and organization that call is about. */
|
|
323
|
+
authenticatedClient(
|
|
324
|
+
signal: AbortSignal,
|
|
325
|
+
request?: ProfileRequestInput,
|
|
326
|
+
): Promise<LocalMcpProductClient>;
|
|
327
|
+
/** Saved logins, read without choosing one; absent where no local store exists. */
|
|
328
|
+
listProfiles?(signal: AbortSignal): Promise<ProfileListReport>;
|
|
279
329
|
}
|
|
280
330
|
|
|
281
331
|
interface ProjectCreateInput {
|
|
@@ -351,6 +401,14 @@ interface ProjectDeleteInput {
|
|
|
351
401
|
}
|
|
352
402
|
|
|
353
403
|
const identifier = z.string().min(1).max(256);
|
|
404
|
+
/** Every product tool accepts the saved login to act as; it is an alias, never a credential. */
|
|
405
|
+
const profileName = z
|
|
406
|
+
.string()
|
|
407
|
+
.regex(/^[a-z0-9][a-z0-9_-]{0,62}$/u)
|
|
408
|
+
.optional()
|
|
409
|
+
.describe(
|
|
410
|
+
"Saved login to act as (profile_list shows them). Needed only when several logins could run this call; OHMYHOST_PROFILE of this server must match it.",
|
|
411
|
+
);
|
|
354
412
|
const idempotencyKey = z.string().min(1).max(256);
|
|
355
413
|
const commitSha = z.string().regex(/^[a-f0-9]{40}$/u);
|
|
356
414
|
const domainName = z
|
|
@@ -657,6 +715,14 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
657
715
|
z.object({ organization_id: identifier }),
|
|
658
716
|
(client, input) => client.getOrganizationAccount(input.organization_id),
|
|
659
717
|
);
|
|
718
|
+
registerProductTool(
|
|
719
|
+
server,
|
|
720
|
+
dependencies,
|
|
721
|
+
"referral_link_get",
|
|
722
|
+
"Read the workspace's referral link to share. A new user whose first workspace comes from it starts with a free Paid month and 1,000 credits; that workspace's first payment gives this workspace the same. Any member may read it.",
|
|
723
|
+
z.object({ organization_id: identifier }),
|
|
724
|
+
(client, input) => client.getOrganizationReferral(input.organization_id),
|
|
725
|
+
);
|
|
660
726
|
registerProductTool(
|
|
661
727
|
server,
|
|
662
728
|
dependencies,
|
|
@@ -700,7 +766,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
700
766
|
server,
|
|
701
767
|
dependencies,
|
|
702
768
|
"organization_create",
|
|
703
|
-
`Create an organization owned by the signed-in user
|
|
769
|
+
`Create an organization owned by the signed-in user. Signup is open. signup_source is optional attribution from a link's r parameter, never a secret; omit it when the customer arrived directly. The server decides any once-per-user bonus; do not promise credits for arbitrary sources. Repeat the same name/source/key after uncertainty. A login that has no organization yet is bound to the new one, so selected names the workspace later calls use; a login already in another organization keeps it and next_action names the login for the new one. Continue with identity_get, github_status and github_connect if needed, then projects. An OHMYHOST_TOKEN process cannot do this; it needs ${commandPrefix} login --json.`,
|
|
704
770
|
z.object({
|
|
705
771
|
name: z.string().min(1).max(128),
|
|
706
772
|
signup_source: z
|
|
@@ -724,7 +790,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
724
790
|
server,
|
|
725
791
|
dependencies,
|
|
726
792
|
"organization_list",
|
|
727
|
-
"List the workspaces the
|
|
793
|
+
"List the workspaces the chosen login's user belongs to and which one that login is scoped to. selected is null while none is chosen; every project and billing call then answers resource_not_found. Ask the customer which workspace to use instead of guessing, then call organization_use.",
|
|
728
794
|
z.object({}),
|
|
729
795
|
(client) => client.listOrganizations(),
|
|
730
796
|
);
|
|
@@ -732,10 +798,32 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
732
798
|
server,
|
|
733
799
|
dependencies,
|
|
734
800
|
"organization_use",
|
|
735
|
-
`
|
|
801
|
+
`Bind a saved login that has no organization yet to one workspace, so later calls act inside it. A login already in another organization keeps it (profile_organization_fixed): add a login for the other workspace as the same user with ${commandPrefix} login --organization ULID --user USER_ID --json. Read organization_list first, then github_status for the selected workspace and github_connect if needed; the identifier is the platform ULID, not the provider identifier. An OHMYHOST_TOKEN process cannot select anything and needs ${commandPrefix} login --json instead.`,
|
|
736
802
|
z.object({ organization_id: identifier }),
|
|
737
803
|
(client, input, signal) => client.selectOrganization(input.organization_id, signal),
|
|
738
804
|
mutationAnnotations(),
|
|
805
|
+
(input) => ({
|
|
806
|
+
...(input.profile_name === undefined ? {} : { name: input.profile_name }),
|
|
807
|
+
organizationId: input.organization_id,
|
|
808
|
+
bindOrganization: true,
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
server.registerTool(
|
|
813
|
+
"profile_list",
|
|
814
|
+
{
|
|
815
|
+
description: `List the saved ohmyho.st logins on this computer: each has a name, a user and an organization, never a token. When the customer named a user or organization, pass the matching name as profile_name on later calls; with several logins and no profile_name a call answers profile_selection_required. With no match, the customer adds one with ${commandPrefix} login --organization ULID --user USER_ID --json, which saves nothing if the browser signs in as another account. bound is this server's OHMYHOST_PROFILE; environment_token means OHMYHOST_TOKEN replaces saved logins here.`,
|
|
816
|
+
inputSchema: z.object({}),
|
|
817
|
+
annotations: readAnnotations(),
|
|
818
|
+
},
|
|
819
|
+
(async (_input: unknown, context: { mcpReq: { signal: AbortSignal } }) => {
|
|
820
|
+
try {
|
|
821
|
+
if (dependencies.listProfiles === undefined) throw new CredentialStoreUnavailableError();
|
|
822
|
+
return toolResult(await dependencies.listProfiles(context.mcpReq.signal));
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return toolError(error, dependencies);
|
|
825
|
+
}
|
|
826
|
+
}) as ToolCallback<z.ZodObject<Record<string, never>>>,
|
|
739
827
|
);
|
|
740
828
|
|
|
741
829
|
registerProductTool(
|
|
@@ -940,7 +1028,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
940
1028
|
server,
|
|
941
1029
|
dependencies,
|
|
942
1030
|
"identity_get",
|
|
943
|
-
"Get the
|
|
1031
|
+
"Get the ohmyho.st customer/agent identity this call acts as: user, organization and, in context, the saved login or OHMYHOST_TOKEN that supplied it. Compare it with the user and organization the customer named before any change. This does not authenticate the hosted application's end users or provision a customer AuthKit tenant; application authentication is the customer's chosen integration.",
|
|
944
1032
|
z.object({}),
|
|
945
1033
|
(client, _input, signal) => client.getCurrentIdentity(signal),
|
|
946
1034
|
);
|
|
@@ -1092,6 +1180,28 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1092
1180
|
}),
|
|
1093
1181
|
destructiveAnnotations(),
|
|
1094
1182
|
);
|
|
1183
|
+
registerProductTool(
|
|
1184
|
+
server,
|
|
1185
|
+
dependencies,
|
|
1186
|
+
"powered_by_flag_get",
|
|
1187
|
+
'Read whether the production site shows the opt-in "Powered by ohmyho.st" flag.',
|
|
1188
|
+
z.object({ project_id: identifier }),
|
|
1189
|
+
(client, input) => client.getPoweredByFlag(input.project_id),
|
|
1190
|
+
);
|
|
1191
|
+
registerProductTool(
|
|
1192
|
+
server,
|
|
1193
|
+
dependencies,
|
|
1194
|
+
"powered_by_flag_set",
|
|
1195
|
+
'Owner only, ask the human first: show or hide a small "Powered by ohmyho.st" flag on the right edge of the production site. While it is on, a Free workspace may connect its own domain to this project free of the domain fee, and each Paid period adds 250 credits. Hiding it is refused while a Free workspace\'s domain depends on it. Replay the same key and value after uncertainty to get the first answer without switching again; every new switch needs a new key.',
|
|
1196
|
+
z.object({ project_id: identifier, enabled: z.boolean(), idempotency_key: idempotencyKey }),
|
|
1197
|
+
(client, input) =>
|
|
1198
|
+
client.setPoweredByFlag({
|
|
1199
|
+
projectId: input.project_id,
|
|
1200
|
+
enabled: input.enabled,
|
|
1201
|
+
idempotencyKey: input.idempotency_key,
|
|
1202
|
+
}),
|
|
1203
|
+
mutationAnnotations(),
|
|
1204
|
+
);
|
|
1095
1205
|
registerProductTool(
|
|
1096
1206
|
server,
|
|
1097
1207
|
dependencies,
|
|
@@ -1144,7 +1254,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1144
1254
|
server,
|
|
1145
1255
|
dependencies,
|
|
1146
1256
|
"github_status",
|
|
1147
|
-
"Read this workspace's GitHub connection. If not_connected or revoked, an Owner or Admin must run github_connect. A connected installation covers only repositories selected in its settings.",
|
|
1257
|
+
"Read this workspace's GitHub connection. If not_connected or revoked, an Owner or Admin must run github_connect. A connected installation covers only repositories selected in its settings. Each workspace has its own connection, so the same repository can be connected and linked in the workspaces of separate accounts.",
|
|
1148
1258
|
z.object({ organization_id: identifier }),
|
|
1149
1259
|
(client, input) => client.getGithubConnection(input.organization_id),
|
|
1150
1260
|
);
|
|
@@ -1152,7 +1262,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1152
1262
|
server,
|
|
1153
1263
|
dependencies,
|
|
1154
1264
|
"source_link",
|
|
1155
|
-
"Link a repository covered by the workspace GitHub connection. Call github_status first and github_connect once if needed. This returns an operation without another browser consent and never starts a build. Wait for that operation before planning a deploy. If repository access is missing, update the installation using the returned settings action, then retry the same project and key. Never ask for an installation ID or provider token.",
|
|
1265
|
+
"Link a repository covered by the workspace GitHub connection. Call github_status first and github_connect once if needed. This returns an operation without another browser consent and never starts a build. Wait for that operation before planning a deploy. If repository access is missing, update the installation using the returned settings action, then retry the same project and key. Never ask for an installation ID or provider token. Errors name the account the call ran as (acting_as): resource_not_found means that account cannot see the project (use the login of the account that owns it), github_connection_required means that workspace has no GitHub connection yet, and repository_not_installed means its GitHub App installation does not cover the repository.",
|
|
1156
1266
|
z.object({
|
|
1157
1267
|
project_id: identifier,
|
|
1158
1268
|
repository_owner: identifier,
|
|
@@ -1237,7 +1347,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1237
1347
|
server,
|
|
1238
1348
|
dependencies,
|
|
1239
1349
|
"deployment_logs",
|
|
1240
|
-
"List the newest normalized diagnostics of one deployment (build, control, runtime and function failures with catalog codes). A BUILD_FAILED item carries excerpt: the sanitized tail of the customer's own install/build output, newest lines last, with excerpt_truncated when earlier output was omitted. Read
|
|
1350
|
+
"List the newest normalized diagnostics of one deployment (build, control, runtime and function failures with catalog codes). A failed operation_get names the deployment as deployment_id. A BUILD_FAILED item carries excerpt: the sanitized tail of the customer's own install/build output, newest lines last, with excerpt_truncated when earlier output was omitted. A HEALTH_CHECK_FAILED item carries the probed route and status_code and, only for a project owner, excerpt: the head (at most 4096 characters, control characters removed, never a header) of what the app answered. The probe is a cookieless GET that follows no redirect, 5 s per try and up to four tries, so a 3xx or an empty 503 is unhealthy too. Read the item before changing source; scheduled runs are in function_runs_list, not here.",
|
|
1241
1351
|
z.object({
|
|
1242
1352
|
project_id: identifier,
|
|
1243
1353
|
deployment_id: identifier,
|
|
@@ -1254,7 +1364,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1254
1364
|
server,
|
|
1255
1365
|
dependencies,
|
|
1256
1366
|
"operation_get",
|
|
1257
|
-
"Get durable operation status and current deployment progress/reconciliation guidance. Follow progress.phase and progress.suggested_action. waiting_for_mail requires mail_domain_status now for the current verification issue and DNS records; publishing means the build completed but runtime preparation/publication is unfinished. Poll this operation after progress.next_poll_after_seconds. A queued/running operation with reconciliation.state=required needs one confirmed operation_reconcile using the same operation; pending means poll this operation after 60 seconds. Never infer deployment success from a completed reconciliation attempt or start another build to recover it.",
|
|
1367
|
+
"Get durable operation status and current deployment progress/reconciliation guidance. Follow progress.phase and progress.suggested_action. waiting_for_mail requires mail_domain_status now for the current verification issue and DNS records; publishing means the build completed but runtime preparation/publication is unfinished. Poll this operation after progress.next_poll_after_seconds. A queued/running operation with reconciliation.state=required needs one confirmed operation_reconcile using the same operation; pending means poll this operation after 60 seconds. Never infer deployment success from a completed reconciliation attempt or start another build to recover it. A deploy, promotion or rollback names its deployment_id in every state; when state is failed, read that deployment with deployment_logs and follow error.suggested_action before changing source.",
|
|
1258
1368
|
z.object({ operation_id: identifier }),
|
|
1259
1369
|
(client, input) => client.getOperation(input.operation_id),
|
|
1260
1370
|
);
|
|
@@ -1543,21 +1653,22 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1543
1653
|
destructiveAnnotations(),
|
|
1544
1654
|
);
|
|
1545
1655
|
|
|
1546
|
-
|
|
1656
|
+
registerProductTool(
|
|
1657
|
+
server,
|
|
1658
|
+
dependencies,
|
|
1547
1659
|
"secret_set_command",
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
toolResult({
|
|
1660
|
+
"Return the stdin-only CLI command for setting a secret; the value never enters MCP. The call runs as the chosen login and first reads the project, so an account that cannot see it answers resource_not_found. The command names that login with --profile-name plus its user and organization (--profile-user, --profile-organization), so a login of the same name that belongs to anyone else is refused, for example on another computer; a server with OHMYHOST_TOKEN instead names its key's user and organization with --token-user and --token-organization, and the CLI then runs only with an OHMYHOST_TOKEN of that account. Either way the later write runs as exactly this account or not at all (acting_as shows which).",
|
|
1661
|
+
z.object({
|
|
1662
|
+
project_id: identifier,
|
|
1663
|
+
environment_id: identifier,
|
|
1664
|
+
name: secretName,
|
|
1665
|
+
idempotency_key: idempotencyKey,
|
|
1666
|
+
}),
|
|
1667
|
+
async (client, input, signal) => {
|
|
1668
|
+
// The project must be visible to the account this call runs as, and the command names it.
|
|
1669
|
+
await client.getProject(input.project_id);
|
|
1670
|
+
const account = await handoverAccount(client, signal);
|
|
1671
|
+
return {
|
|
1561
1672
|
command: [
|
|
1562
1673
|
`${commandPrefix} secret set`,
|
|
1563
1674
|
shellQuote(input.name),
|
|
@@ -1567,13 +1678,15 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1567
1678
|
shellQuote(input.environment_id),
|
|
1568
1679
|
"--idempotency-key",
|
|
1569
1680
|
shellQuote(input.idempotency_key),
|
|
1681
|
+
...account.flags,
|
|
1570
1682
|
"--stdin",
|
|
1571
1683
|
"--wait",
|
|
1572
1684
|
"--json",
|
|
1573
1685
|
].join(" "),
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1686
|
+
acting_as: client.actingAs?.() ?? null,
|
|
1687
|
+
instruction: `Pass the secret value on stdin in a local terminal; ${account.runs}. Never place the value or a key in MCP arguments, prompts, command-line arguments, source, or logs.`,
|
|
1688
|
+
};
|
|
1689
|
+
},
|
|
1577
1690
|
);
|
|
1578
1691
|
|
|
1579
1692
|
registerProductTool(
|
|
@@ -1647,24 +1760,33 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
|
|
|
1647
1760
|
dependencies: LocalOhmyhostMcpDependencies,
|
|
1648
1761
|
name: string,
|
|
1649
1762
|
description: string,
|
|
1650
|
-
|
|
1763
|
+
schema: Schema,
|
|
1651
1764
|
execute: (
|
|
1652
1765
|
client: LocalMcpProductClient,
|
|
1653
1766
|
input: z.output<Schema>,
|
|
1654
1767
|
signal: AbortSignal,
|
|
1655
1768
|
) => Promise<Result>,
|
|
1656
1769
|
annotations = readAnnotations(),
|
|
1770
|
+
selection: (
|
|
1771
|
+
input: z.output<Schema> & { readonly profile_name?: string | undefined },
|
|
1772
|
+
) => ProfileRequestInput = (input) => {
|
|
1773
|
+
// A call about one organization runs as a login of that organization.
|
|
1774
|
+
const organizationId = (input as Record<string, unknown>)["organization_id"];
|
|
1775
|
+
return {
|
|
1776
|
+
...(input.profile_name === undefined ? {} : { name: input.profile_name }),
|
|
1777
|
+
...(typeof organizationId === "string" ? { organizationId } : {}),
|
|
1778
|
+
};
|
|
1779
|
+
},
|
|
1657
1780
|
) {
|
|
1781
|
+
const inputSchema = schema.extend({ profile_name: profileName });
|
|
1658
1782
|
const callback = (async (input: unknown, context: { mcpReq: { signal: AbortSignal } }) => {
|
|
1659
|
-
const parsed = inputSchema.parse(input) as z.output<Schema
|
|
1783
|
+
const parsed = inputSchema.parse(input) as z.output<Schema> & {
|
|
1784
|
+
readonly profile_name?: string | undefined;
|
|
1785
|
+
};
|
|
1786
|
+
let client: LocalMcpProductClient | undefined;
|
|
1660
1787
|
try {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
await dependencies.authenticatedClient(context.mcpReq.signal),
|
|
1664
|
-
parsed,
|
|
1665
|
-
context.mcpReq.signal,
|
|
1666
|
-
),
|
|
1667
|
-
);
|
|
1788
|
+
client = await dependencies.authenticatedClient(context.mcpReq.signal, selection(parsed));
|
|
1789
|
+
const result = toolResult(await execute(client, parsed, context.mcpReq.signal));
|
|
1668
1790
|
const scope = parsed as Record<string, unknown>;
|
|
1669
1791
|
if (
|
|
1670
1792
|
typeof scope["project_id"] === "string" &&
|
|
@@ -1680,69 +1802,180 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
|
|
|
1680
1802
|
});
|
|
1681
1803
|
return result;
|
|
1682
1804
|
} catch (error) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1805
|
+
return toolError(error, dependencies, client?.actingAs?.());
|
|
1806
|
+
}
|
|
1807
|
+
}) as ToolCallback<typeof inputSchema>;
|
|
1808
|
+
server.registerTool(name, { description, inputSchema, annotations }, callback);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* The account a command handed to a terminal must run as: a saved login by its name, user and
|
|
1813
|
+
* organization, which the CLI compares with the login of that name wherever it runs, or this
|
|
1814
|
+
* server's OHMYHOST_TOKEN by its key's verified user and organization, which the CLI then demands
|
|
1815
|
+
* of the terminal's own OHMYHOST_TOKEN. No key or token value ever enters the command.
|
|
1816
|
+
*/
|
|
1817
|
+
async function handoverAccount(
|
|
1818
|
+
client: LocalMcpProductClient,
|
|
1819
|
+
signal: AbortSignal,
|
|
1820
|
+
): Promise<{ readonly flags: readonly string[]; readonly runs: string }> {
|
|
1821
|
+
const profile = client.actingAs?.()?.profile ?? null;
|
|
1822
|
+
if (profile !== null) {
|
|
1823
|
+
if (profile.organization_id === null)
|
|
1824
|
+
throw new ProfileSelectionError(
|
|
1825
|
+
"profile_context_mismatch",
|
|
1826
|
+
2,
|
|
1827
|
+
`The saved login ${profile.name} has no organization, so no command can be tied to its account. Nothing was sent.`,
|
|
1828
|
+
);
|
|
1829
|
+
return {
|
|
1830
|
+
flags: [
|
|
1831
|
+
"--profile-name",
|
|
1832
|
+
shellQuote(profile.name),
|
|
1833
|
+
"--profile-user",
|
|
1834
|
+
shellQuote(profile.user_id),
|
|
1835
|
+
"--profile-organization",
|
|
1836
|
+
shellQuote(profile.organization_id),
|
|
1837
|
+
],
|
|
1838
|
+
runs: `the command runs only as the saved login it names and only while that login belongs to user ${profile.user_id} in organization ${profile.organization_id}, so a login of the same name that belongs to anyone else, for example on another computer, is refused before anything is read or sent`,
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
const report = await client.getCurrentIdentity(signal);
|
|
1842
|
+
const identity = parseCurrentIdentity({
|
|
1843
|
+
actor_id: report.actor_id,
|
|
1844
|
+
organization_ids: report.organization_ids,
|
|
1845
|
+
});
|
|
1846
|
+
const userId = identity.actor_id.startsWith("user:") ? identity.actor_id.slice(5) : "";
|
|
1847
|
+
const [organizationId, ...others] = identity.organization_ids;
|
|
1848
|
+
if (!isProfileUserId(userId) || organizationId === undefined || others.length > 0)
|
|
1849
|
+
throw new ProfileSelectionError(
|
|
1850
|
+
"environment_token_context_mismatch",
|
|
1851
|
+
2,
|
|
1852
|
+
"This server's OHMYHOST_TOKEN does not belong to one user in one organization, so no command can be tied to its account. Nothing was sent.",
|
|
1853
|
+
);
|
|
1854
|
+
return {
|
|
1855
|
+
flags: ["--token-user", shellQuote(userId), "--token-organization", shellQuote(organizationId)],
|
|
1856
|
+
runs: `run it only where OHMYHOST_TOKEN holds a key of user ${userId} in organization ${organizationId}, loaded from its private env file; it refuses a saved login and any other account's key before anything is sent`,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/** How to add a login for another workspace as the same user; another browser account saves nothing. */
|
|
1861
|
+
function fixedOrganizationAction(
|
|
1862
|
+
error: {
|
|
1863
|
+
readonly profile: { readonly userId: string };
|
|
1864
|
+
readonly organization: { readonly id: string };
|
|
1865
|
+
},
|
|
1866
|
+
commandPrefix: string,
|
|
1867
|
+
): string {
|
|
1868
|
+
return `This saved login stays in its organization. Add a login for the other workspace with ${commandPrefix} login --organization ${error.organization.id} --user ${error.profile.userId} --json, then pass its profile_name.`;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/** The stable, credential-free error of one tool call. */
|
|
1872
|
+
function toolError(
|
|
1873
|
+
error: unknown,
|
|
1874
|
+
dependencies: LocalOhmyhostMcpDependencies,
|
|
1875
|
+
actingAs?: IdentityReport["context"],
|
|
1876
|
+
): CallToolResult {
|
|
1877
|
+
let safe: {
|
|
1878
|
+
code: string;
|
|
1879
|
+
retryable: boolean;
|
|
1880
|
+
suggestedAction: string;
|
|
1881
|
+
retryAfterSeconds?: number;
|
|
1882
|
+
requestId?: string;
|
|
1883
|
+
};
|
|
1884
|
+
try {
|
|
1885
|
+
safe =
|
|
1886
|
+
error instanceof Error && error.name === "ProfileSelectionError"
|
|
1887
|
+
? {
|
|
1888
|
+
code: (error as Error & { code: string }).code,
|
|
1889
|
+
retryable: false,
|
|
1890
|
+
suggestedAction: (error as Error & { suggestedAction: string }).suggestedAction,
|
|
1891
|
+
}
|
|
1892
|
+
: error instanceof Error && error.name === "ProfileLimitError"
|
|
1893
|
+
? {
|
|
1894
|
+
code: "profile_limit_reached",
|
|
1895
|
+
retryable: false,
|
|
1896
|
+
suggestedAction: profileLimitAction(
|
|
1897
|
+
resolveProductCliEnvironment(dependencies.environment).commandPrefix,
|
|
1898
|
+
),
|
|
1899
|
+
}
|
|
1900
|
+
: error instanceof Error && error.name === "ProfileOrganizationFixedError"
|
|
1693
1901
|
? {
|
|
1694
|
-
code: "
|
|
1902
|
+
code: "profile_organization_fixed",
|
|
1695
1903
|
retryable: false,
|
|
1696
|
-
suggestedAction:
|
|
1904
|
+
suggestedAction: fixedOrganizationAction(
|
|
1905
|
+
error as Error & {
|
|
1906
|
+
readonly profile: { readonly userId: string };
|
|
1907
|
+
readonly organization: { readonly id: string };
|
|
1908
|
+
},
|
|
1909
|
+
resolveProductCliEnvironment(dependencies.environment).commandPrefix,
|
|
1910
|
+
),
|
|
1697
1911
|
}
|
|
1698
|
-
: error instanceof Error && error.
|
|
1912
|
+
: error instanceof Error && error.name === "AccountVerificationError"
|
|
1699
1913
|
? {
|
|
1700
|
-
code: "
|
|
1701
|
-
retryable:
|
|
1702
|
-
suggestedAction:
|
|
1703
|
-
"Call organization_list and choose one of the returned workspace identifiers. Do not create another workspace to recover.",
|
|
1914
|
+
code: "login_verification_failed",
|
|
1915
|
+
retryable: true,
|
|
1916
|
+
suggestedAction: `The signed-in account could not be verified; nothing was saved. Run ${resolveProductCliEnvironment(dependencies.environment).commandPrefix} login --json again.`,
|
|
1704
1917
|
}
|
|
1705
|
-
: error instanceof
|
|
1918
|
+
: error instanceof Error && error.name === "CredentialStoreUnavailableError"
|
|
1706
1919
|
? {
|
|
1707
|
-
code: "
|
|
1920
|
+
code: "credential_store_unavailable",
|
|
1708
1921
|
retryable: true,
|
|
1709
1922
|
suggestedAction:
|
|
1710
|
-
"
|
|
1923
|
+
"Unlock or repair this computer's credential store and retry. Saved logins were not changed.",
|
|
1711
1924
|
}
|
|
1712
|
-
: error instanceof Error &&
|
|
1713
|
-
error.message.startsWith("EXPORT_PASSWORD_FILE_INVALID:")
|
|
1925
|
+
: error instanceof Error && error.name === "InteractiveSessionRequiredError"
|
|
1714
1926
|
? {
|
|
1715
|
-
code: "
|
|
1927
|
+
code: "interactive_login_required",
|
|
1716
1928
|
retryable: false,
|
|
1717
|
-
suggestedAction:
|
|
1718
|
-
"Choose an existing private UTF-8 password file of 1–1024 bytes. The user retains the file and password. Do not paste the password into MCP arguments.",
|
|
1929
|
+
suggestedAction: `A user API token cannot create, list or select a workspace. Run ${resolveProductCliEnvironment(dependencies.environment).commandPrefix} login --json in a process without OHMYHOST_TOKEN, then retry.`,
|
|
1719
1930
|
}
|
|
1720
|
-
:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1931
|
+
: error instanceof Error && error.message.startsWith("ORGANIZATION_NOT_FOUND:")
|
|
1932
|
+
? {
|
|
1933
|
+
code: "organization_not_found",
|
|
1934
|
+
retryable: false,
|
|
1935
|
+
suggestedAction:
|
|
1936
|
+
"Call organization_list and choose one of the returned workspace identifiers. Do not create another workspace to recover.",
|
|
1937
|
+
}
|
|
1938
|
+
: error instanceof OperationEventsUnavailableError
|
|
1939
|
+
? {
|
|
1940
|
+
code: "operation_events_unavailable",
|
|
1941
|
+
retryable: true,
|
|
1942
|
+
suggestedAction:
|
|
1943
|
+
"Read operation_get for current status, then retry operation_logs for this same operation. Do not submit another deployment to recover logs.",
|
|
1944
|
+
}
|
|
1945
|
+
: error instanceof Error &&
|
|
1946
|
+
error.message.startsWith("EXPORT_PASSWORD_FILE_INVALID:")
|
|
1947
|
+
? {
|
|
1948
|
+
code: "export_password_file_invalid",
|
|
1949
|
+
retryable: false,
|
|
1950
|
+
suggestedAction:
|
|
1951
|
+
"Choose an existing private UTF-8 password file of 1–1024 bytes. The user retains the file and password. Do not paste the password into MCP arguments.",
|
|
1952
|
+
}
|
|
1953
|
+
: parseSafeProblem(error);
|
|
1954
|
+
} catch {
|
|
1955
|
+
safe = {
|
|
1956
|
+
code: "client_request_failed",
|
|
1957
|
+
retryable: false,
|
|
1958
|
+
suggestedAction:
|
|
1959
|
+
"Check the connection and run 'ohmyhost login' if authentication expired. Inspect the existing operation before resubmitting any mutation.",
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
return {
|
|
1963
|
+
...toolResult({
|
|
1964
|
+
error: {
|
|
1965
|
+
code: safe.code,
|
|
1966
|
+
retryable: safe.retryable,
|
|
1967
|
+
suggested_action: safe.suggestedAction,
|
|
1968
|
+
...(safe.requestId === undefined ? {} : { request_id: safe.requestId }),
|
|
1969
|
+
...(safe.retryAfterSeconds === undefined
|
|
1970
|
+
? {}
|
|
1971
|
+
: { retry_after_seconds: safe.retryAfterSeconds }),
|
|
1972
|
+
...(actingAs !== undefined && ACCOUNT_SENSITIVE_PROBLEMS.has(safe.code)
|
|
1973
|
+
? { acting_as: actingAs }
|
|
1974
|
+
: {}),
|
|
1975
|
+
},
|
|
1976
|
+
}),
|
|
1977
|
+
isError: true,
|
|
1978
|
+
};
|
|
1746
1979
|
}
|
|
1747
1980
|
|
|
1748
1981
|
function toolResult<Result>(result: Result): CallToolResult {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
|
-
import { resolveProductCliEnvironment } from "@ohmyhost/product-cli";
|
|
4
|
+
import { listSavedProfiles, resolveProductCliEnvironment } from "@ohmyhost/product-cli";
|
|
5
5
|
|
|
6
6
|
import { createAuthenticatedLocalMcpClient } from "./local-client.js";
|
|
7
7
|
import { createLocalOhmyhostMcpServer } from "./local-server.js";
|
|
@@ -13,6 +13,9 @@ const environment = {
|
|
|
13
13
|
...(process.env["OHMYHOST_TOKEN"] === undefined
|
|
14
14
|
? {}
|
|
15
15
|
: { OHMYHOST_TOKEN: process.env["OHMYHOST_TOKEN"] }),
|
|
16
|
+
...(process.env["OHMYHOST_PROFILE"] === undefined
|
|
17
|
+
? {}
|
|
18
|
+
: { OHMYHOST_PROFILE: process.env["OHMYHOST_PROFILE"] }),
|
|
16
19
|
};
|
|
17
20
|
resolveProductCliEnvironment(environment);
|
|
18
21
|
|
|
@@ -20,7 +23,13 @@ serveStdio(
|
|
|
20
23
|
() =>
|
|
21
24
|
createLocalOhmyhostMcpServer({
|
|
22
25
|
environment,
|
|
23
|
-
|
|
26
|
+
// Every tool call chooses its own saved login; nothing this process remembers can retarget it.
|
|
27
|
+
authenticatedClient: (signal, request) =>
|
|
28
|
+
createAuthenticatedLocalMcpClient(signal, {
|
|
29
|
+
environment,
|
|
30
|
+
...(request === undefined ? {} : { request }),
|
|
31
|
+
}),
|
|
32
|
+
listProfiles: (signal) => listSavedProfiles(signal, { environment }),
|
|
24
33
|
}),
|
|
25
34
|
{ onerror: (error) => process.stderr.write(`${error.message}\n`) },
|
|
26
35
|
);
|