@amerged/ohmyhost-mcp 0.1.19 → 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-PXGLL6SR.js → chunk-NESQZ3BS.js} +6 -6
- 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 +5 -5
|
@@ -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
|
|
|
@@ -174,7 +181,9 @@ export interface LocalMcpProductClient {
|
|
|
174
181
|
organizationId: string,
|
|
175
182
|
signal: AbortSignal,
|
|
176
183
|
): Promise<WorkspaceSelectionReport>;
|
|
177
|
-
getCurrentIdentity(signal: AbortSignal): Promise<
|
|
184
|
+
getCurrentIdentity(signal: AbortSignal): Promise<IdentityReport>;
|
|
185
|
+
/** The account this client acts as, for errors whose meaning depends on it. */
|
|
186
|
+
actingAs?(): IdentityReport["context"];
|
|
178
187
|
listProjects(input: { readonly cursor?: string; readonly limit?: number }): Promise<ProjectPage>;
|
|
179
188
|
submitFeedback(input: {
|
|
180
189
|
report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
|
|
@@ -261,21 +270,53 @@ export interface PublicWorkspaceSummary {
|
|
|
261
270
|
readonly name: string;
|
|
262
271
|
}
|
|
263
272
|
|
|
264
|
-
/**
|
|
273
|
+
/** One saved login as `profile_list` shows it; it never carries a credential. */
|
|
274
|
+
export interface PublicProfileSummary {
|
|
275
|
+
readonly name: string;
|
|
276
|
+
readonly user_id: string;
|
|
277
|
+
readonly organization_id: string | null;
|
|
278
|
+
readonly organization_name: string | null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The identity a call acted as, and which saved login or OHMYHOST_TOKEN supplied it. */
|
|
282
|
+
export type IdentityReport = CurrentIdentity & {
|
|
283
|
+
readonly context: {
|
|
284
|
+
readonly credential: "profile" | "environment_token";
|
|
285
|
+
readonly profile: PublicProfileSummary | null;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/** The workspaces of the signed-in user and the one the chosen login is scoped to. */
|
|
265
290
|
export interface WorkspaceReport {
|
|
266
291
|
readonly organizations: readonly PublicWorkspaceSummary[];
|
|
267
292
|
readonly selected: string | null;
|
|
293
|
+
readonly profile: PublicProfileSummary;
|
|
268
294
|
}
|
|
269
295
|
|
|
270
296
|
/** One workspace that was just created or selected, with the resulting scope. */
|
|
271
297
|
export interface WorkspaceSelectionReport {
|
|
272
298
|
readonly organization: PublicWorkspaceSummary;
|
|
273
299
|
readonly selected: string;
|
|
300
|
+
readonly profile: PublicProfileSummary;
|
|
301
|
+
readonly next_action?: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The saved logins of this computer and how this MCP process chooses among them. */
|
|
305
|
+
export interface ProfileListReport {
|
|
306
|
+
readonly profiles: readonly PublicProfileSummary[];
|
|
307
|
+
readonly bound: string | null;
|
|
308
|
+
readonly environment_token: boolean;
|
|
274
309
|
}
|
|
275
310
|
|
|
276
311
|
export interface LocalOhmyhostMcpDependencies {
|
|
277
312
|
readonly environment?: ProductCliEnvironment;
|
|
278
|
-
|
|
313
|
+
/** One client for one call; the request names the login and organization that call is about. */
|
|
314
|
+
authenticatedClient(
|
|
315
|
+
signal: AbortSignal,
|
|
316
|
+
request?: ProfileRequestInput,
|
|
317
|
+
): Promise<LocalMcpProductClient>;
|
|
318
|
+
/** Saved logins, read without choosing one; absent where no local store exists. */
|
|
319
|
+
listProfiles?(signal: AbortSignal): Promise<ProfileListReport>;
|
|
279
320
|
}
|
|
280
321
|
|
|
281
322
|
interface ProjectCreateInput {
|
|
@@ -351,6 +392,14 @@ interface ProjectDeleteInput {
|
|
|
351
392
|
}
|
|
352
393
|
|
|
353
394
|
const identifier = z.string().min(1).max(256);
|
|
395
|
+
/** Every product tool accepts the saved login to act as; it is an alias, never a credential. */
|
|
396
|
+
const profileName = z
|
|
397
|
+
.string()
|
|
398
|
+
.regex(/^[a-z0-9][a-z0-9_-]{0,62}$/u)
|
|
399
|
+
.optional()
|
|
400
|
+
.describe(
|
|
401
|
+
"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.",
|
|
402
|
+
);
|
|
354
403
|
const idempotencyKey = z.string().min(1).max(256);
|
|
355
404
|
const commitSha = z.string().regex(/^[a-f0-9]{40}$/u);
|
|
356
405
|
const domainName = z
|
|
@@ -700,7 +749,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
700
749
|
server,
|
|
701
750
|
dependencies,
|
|
702
751
|
"organization_create",
|
|
703
|
-
`Create an organization owned by the signed-in user
|
|
752
|
+
`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
753
|
z.object({
|
|
705
754
|
name: z.string().min(1).max(128),
|
|
706
755
|
signup_source: z
|
|
@@ -724,7 +773,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
724
773
|
server,
|
|
725
774
|
dependencies,
|
|
726
775
|
"organization_list",
|
|
727
|
-
"List the workspaces the
|
|
776
|
+
"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
777
|
z.object({}),
|
|
729
778
|
(client) => client.listOrganizations(),
|
|
730
779
|
);
|
|
@@ -732,10 +781,32 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
732
781
|
server,
|
|
733
782
|
dependencies,
|
|
734
783
|
"organization_use",
|
|
735
|
-
`
|
|
784
|
+
`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
785
|
z.object({ organization_id: identifier }),
|
|
737
786
|
(client, input, signal) => client.selectOrganization(input.organization_id, signal),
|
|
738
787
|
mutationAnnotations(),
|
|
788
|
+
(input) => ({
|
|
789
|
+
...(input.profile_name === undefined ? {} : { name: input.profile_name }),
|
|
790
|
+
organizationId: input.organization_id,
|
|
791
|
+
bindOrganization: true,
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
server.registerTool(
|
|
796
|
+
"profile_list",
|
|
797
|
+
{
|
|
798
|
+
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.`,
|
|
799
|
+
inputSchema: z.object({}),
|
|
800
|
+
annotations: readAnnotations(),
|
|
801
|
+
},
|
|
802
|
+
(async (_input: unknown, context: { mcpReq: { signal: AbortSignal } }) => {
|
|
803
|
+
try {
|
|
804
|
+
if (dependencies.listProfiles === undefined) throw new CredentialStoreUnavailableError();
|
|
805
|
+
return toolResult(await dependencies.listProfiles(context.mcpReq.signal));
|
|
806
|
+
} catch (error) {
|
|
807
|
+
return toolError(error, dependencies);
|
|
808
|
+
}
|
|
809
|
+
}) as ToolCallback<z.ZodObject<Record<string, never>>>,
|
|
739
810
|
);
|
|
740
811
|
|
|
741
812
|
registerProductTool(
|
|
@@ -940,7 +1011,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
940
1011
|
server,
|
|
941
1012
|
dependencies,
|
|
942
1013
|
"identity_get",
|
|
943
|
-
"Get the
|
|
1014
|
+
"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
1015
|
z.object({}),
|
|
945
1016
|
(client, _input, signal) => client.getCurrentIdentity(signal),
|
|
946
1017
|
);
|
|
@@ -1144,7 +1215,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1144
1215
|
server,
|
|
1145
1216
|
dependencies,
|
|
1146
1217
|
"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.",
|
|
1218
|
+
"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
1219
|
z.object({ organization_id: identifier }),
|
|
1149
1220
|
(client, input) => client.getGithubConnection(input.organization_id),
|
|
1150
1221
|
);
|
|
@@ -1152,7 +1223,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1152
1223
|
server,
|
|
1153
1224
|
dependencies,
|
|
1154
1225
|
"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.",
|
|
1226
|
+
"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
1227
|
z.object({
|
|
1157
1228
|
project_id: identifier,
|
|
1158
1229
|
repository_owner: identifier,
|
|
@@ -1543,21 +1614,22 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1543
1614
|
destructiveAnnotations(),
|
|
1544
1615
|
);
|
|
1545
1616
|
|
|
1546
|
-
|
|
1617
|
+
registerProductTool(
|
|
1618
|
+
server,
|
|
1619
|
+
dependencies,
|
|
1547
1620
|
"secret_set_command",
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
toolResult({
|
|
1621
|
+
"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).",
|
|
1622
|
+
z.object({
|
|
1623
|
+
project_id: identifier,
|
|
1624
|
+
environment_id: identifier,
|
|
1625
|
+
name: secretName,
|
|
1626
|
+
idempotency_key: idempotencyKey,
|
|
1627
|
+
}),
|
|
1628
|
+
async (client, input, signal) => {
|
|
1629
|
+
// The project must be visible to the account this call runs as, and the command names it.
|
|
1630
|
+
await client.getProject(input.project_id);
|
|
1631
|
+
const account = await handoverAccount(client, signal);
|
|
1632
|
+
return {
|
|
1561
1633
|
command: [
|
|
1562
1634
|
`${commandPrefix} secret set`,
|
|
1563
1635
|
shellQuote(input.name),
|
|
@@ -1567,13 +1639,15 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
|
|
|
1567
1639
|
shellQuote(input.environment_id),
|
|
1568
1640
|
"--idempotency-key",
|
|
1569
1641
|
shellQuote(input.idempotency_key),
|
|
1642
|
+
...account.flags,
|
|
1570
1643
|
"--stdin",
|
|
1571
1644
|
"--wait",
|
|
1572
1645
|
"--json",
|
|
1573
1646
|
].join(" "),
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1647
|
+
acting_as: client.actingAs?.() ?? null,
|
|
1648
|
+
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.`,
|
|
1649
|
+
};
|
|
1650
|
+
},
|
|
1577
1651
|
);
|
|
1578
1652
|
|
|
1579
1653
|
registerProductTool(
|
|
@@ -1647,24 +1721,33 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
|
|
|
1647
1721
|
dependencies: LocalOhmyhostMcpDependencies,
|
|
1648
1722
|
name: string,
|
|
1649
1723
|
description: string,
|
|
1650
|
-
|
|
1724
|
+
schema: Schema,
|
|
1651
1725
|
execute: (
|
|
1652
1726
|
client: LocalMcpProductClient,
|
|
1653
1727
|
input: z.output<Schema>,
|
|
1654
1728
|
signal: AbortSignal,
|
|
1655
1729
|
) => Promise<Result>,
|
|
1656
1730
|
annotations = readAnnotations(),
|
|
1731
|
+
selection: (
|
|
1732
|
+
input: z.output<Schema> & { readonly profile_name?: string | undefined },
|
|
1733
|
+
) => ProfileRequestInput = (input) => {
|
|
1734
|
+
// A call about one organization runs as a login of that organization.
|
|
1735
|
+
const organizationId = (input as Record<string, unknown>)["organization_id"];
|
|
1736
|
+
return {
|
|
1737
|
+
...(input.profile_name === undefined ? {} : { name: input.profile_name }),
|
|
1738
|
+
...(typeof organizationId === "string" ? { organizationId } : {}),
|
|
1739
|
+
};
|
|
1740
|
+
},
|
|
1657
1741
|
) {
|
|
1742
|
+
const inputSchema = schema.extend({ profile_name: profileName });
|
|
1658
1743
|
const callback = (async (input: unknown, context: { mcpReq: { signal: AbortSignal } }) => {
|
|
1659
|
-
const parsed = inputSchema.parse(input) as z.output<Schema
|
|
1744
|
+
const parsed = inputSchema.parse(input) as z.output<Schema> & {
|
|
1745
|
+
readonly profile_name?: string | undefined;
|
|
1746
|
+
};
|
|
1747
|
+
let client: LocalMcpProductClient | undefined;
|
|
1660
1748
|
try {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
await dependencies.authenticatedClient(context.mcpReq.signal),
|
|
1664
|
-
parsed,
|
|
1665
|
-
context.mcpReq.signal,
|
|
1666
|
-
),
|
|
1667
|
-
);
|
|
1749
|
+
client = await dependencies.authenticatedClient(context.mcpReq.signal, selection(parsed));
|
|
1750
|
+
const result = toolResult(await execute(client, parsed, context.mcpReq.signal));
|
|
1668
1751
|
const scope = parsed as Record<string, unknown>;
|
|
1669
1752
|
if (
|
|
1670
1753
|
typeof scope["project_id"] === "string" &&
|
|
@@ -1680,69 +1763,180 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
|
|
|
1680
1763
|
});
|
|
1681
1764
|
return result;
|
|
1682
1765
|
} catch (error) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1766
|
+
return toolError(error, dependencies, client?.actingAs?.());
|
|
1767
|
+
}
|
|
1768
|
+
}) as ToolCallback<typeof inputSchema>;
|
|
1769
|
+
server.registerTool(name, { description, inputSchema, annotations }, callback);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* The account a command handed to a terminal must run as: a saved login by its name, user and
|
|
1774
|
+
* organization, which the CLI compares with the login of that name wherever it runs, or this
|
|
1775
|
+
* server's OHMYHOST_TOKEN by its key's verified user and organization, which the CLI then demands
|
|
1776
|
+
* of the terminal's own OHMYHOST_TOKEN. No key or token value ever enters the command.
|
|
1777
|
+
*/
|
|
1778
|
+
async function handoverAccount(
|
|
1779
|
+
client: LocalMcpProductClient,
|
|
1780
|
+
signal: AbortSignal,
|
|
1781
|
+
): Promise<{ readonly flags: readonly string[]; readonly runs: string }> {
|
|
1782
|
+
const profile = client.actingAs?.()?.profile ?? null;
|
|
1783
|
+
if (profile !== null) {
|
|
1784
|
+
if (profile.organization_id === null)
|
|
1785
|
+
throw new ProfileSelectionError(
|
|
1786
|
+
"profile_context_mismatch",
|
|
1787
|
+
2,
|
|
1788
|
+
`The saved login ${profile.name} has no organization, so no command can be tied to its account. Nothing was sent.`,
|
|
1789
|
+
);
|
|
1790
|
+
return {
|
|
1791
|
+
flags: [
|
|
1792
|
+
"--profile-name",
|
|
1793
|
+
shellQuote(profile.name),
|
|
1794
|
+
"--profile-user",
|
|
1795
|
+
shellQuote(profile.user_id),
|
|
1796
|
+
"--profile-organization",
|
|
1797
|
+
shellQuote(profile.organization_id),
|
|
1798
|
+
],
|
|
1799
|
+
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`,
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
const report = await client.getCurrentIdentity(signal);
|
|
1803
|
+
const identity = parseCurrentIdentity({
|
|
1804
|
+
actor_id: report.actor_id,
|
|
1805
|
+
organization_ids: report.organization_ids,
|
|
1806
|
+
});
|
|
1807
|
+
const userId = identity.actor_id.startsWith("user:") ? identity.actor_id.slice(5) : "";
|
|
1808
|
+
const [organizationId, ...others] = identity.organization_ids;
|
|
1809
|
+
if (!isProfileUserId(userId) || organizationId === undefined || others.length > 0)
|
|
1810
|
+
throw new ProfileSelectionError(
|
|
1811
|
+
"environment_token_context_mismatch",
|
|
1812
|
+
2,
|
|
1813
|
+
"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.",
|
|
1814
|
+
);
|
|
1815
|
+
return {
|
|
1816
|
+
flags: ["--token-user", shellQuote(userId), "--token-organization", shellQuote(organizationId)],
|
|
1817
|
+
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`,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/** How to add a login for another workspace as the same user; another browser account saves nothing. */
|
|
1822
|
+
function fixedOrganizationAction(
|
|
1823
|
+
error: {
|
|
1824
|
+
readonly profile: { readonly userId: string };
|
|
1825
|
+
readonly organization: { readonly id: string };
|
|
1826
|
+
},
|
|
1827
|
+
commandPrefix: string,
|
|
1828
|
+
): string {
|
|
1829
|
+
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.`;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/** The stable, credential-free error of one tool call. */
|
|
1833
|
+
function toolError(
|
|
1834
|
+
error: unknown,
|
|
1835
|
+
dependencies: LocalOhmyhostMcpDependencies,
|
|
1836
|
+
actingAs?: IdentityReport["context"],
|
|
1837
|
+
): CallToolResult {
|
|
1838
|
+
let safe: {
|
|
1839
|
+
code: string;
|
|
1840
|
+
retryable: boolean;
|
|
1841
|
+
suggestedAction: string;
|
|
1842
|
+
retryAfterSeconds?: number;
|
|
1843
|
+
requestId?: string;
|
|
1844
|
+
};
|
|
1845
|
+
try {
|
|
1846
|
+
safe =
|
|
1847
|
+
error instanceof Error && error.name === "ProfileSelectionError"
|
|
1848
|
+
? {
|
|
1849
|
+
code: (error as Error & { code: string }).code,
|
|
1850
|
+
retryable: false,
|
|
1851
|
+
suggestedAction: (error as Error & { suggestedAction: string }).suggestedAction,
|
|
1852
|
+
}
|
|
1853
|
+
: error instanceof Error && error.name === "ProfileLimitError"
|
|
1854
|
+
? {
|
|
1855
|
+
code: "profile_limit_reached",
|
|
1856
|
+
retryable: false,
|
|
1857
|
+
suggestedAction: profileLimitAction(
|
|
1858
|
+
resolveProductCliEnvironment(dependencies.environment).commandPrefix,
|
|
1859
|
+
),
|
|
1860
|
+
}
|
|
1861
|
+
: error instanceof Error && error.name === "ProfileOrganizationFixedError"
|
|
1693
1862
|
? {
|
|
1694
|
-
code: "
|
|
1863
|
+
code: "profile_organization_fixed",
|
|
1695
1864
|
retryable: false,
|
|
1696
|
-
suggestedAction:
|
|
1865
|
+
suggestedAction: fixedOrganizationAction(
|
|
1866
|
+
error as Error & {
|
|
1867
|
+
readonly profile: { readonly userId: string };
|
|
1868
|
+
readonly organization: { readonly id: string };
|
|
1869
|
+
},
|
|
1870
|
+
resolveProductCliEnvironment(dependencies.environment).commandPrefix,
|
|
1871
|
+
),
|
|
1697
1872
|
}
|
|
1698
|
-
: error instanceof Error && error.
|
|
1873
|
+
: error instanceof Error && error.name === "AccountVerificationError"
|
|
1699
1874
|
? {
|
|
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.",
|
|
1875
|
+
code: "login_verification_failed",
|
|
1876
|
+
retryable: true,
|
|
1877
|
+
suggestedAction: `The signed-in account could not be verified; nothing was saved. Run ${resolveProductCliEnvironment(dependencies.environment).commandPrefix} login --json again.`,
|
|
1704
1878
|
}
|
|
1705
|
-
: error instanceof
|
|
1879
|
+
: error instanceof Error && error.name === "CredentialStoreUnavailableError"
|
|
1706
1880
|
? {
|
|
1707
|
-
code: "
|
|
1881
|
+
code: "credential_store_unavailable",
|
|
1708
1882
|
retryable: true,
|
|
1709
1883
|
suggestedAction:
|
|
1710
|
-
"
|
|
1884
|
+
"Unlock or repair this computer's credential store and retry. Saved logins were not changed.",
|
|
1711
1885
|
}
|
|
1712
|
-
: error instanceof Error &&
|
|
1713
|
-
error.message.startsWith("EXPORT_PASSWORD_FILE_INVALID:")
|
|
1886
|
+
: error instanceof Error && error.name === "InteractiveSessionRequiredError"
|
|
1714
1887
|
? {
|
|
1715
|
-
code: "
|
|
1888
|
+
code: "interactive_login_required",
|
|
1716
1889
|
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.",
|
|
1890
|
+
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
1891
|
}
|
|
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
|
-
|
|
1892
|
+
: error instanceof Error && error.message.startsWith("ORGANIZATION_NOT_FOUND:")
|
|
1893
|
+
? {
|
|
1894
|
+
code: "organization_not_found",
|
|
1895
|
+
retryable: false,
|
|
1896
|
+
suggestedAction:
|
|
1897
|
+
"Call organization_list and choose one of the returned workspace identifiers. Do not create another workspace to recover.",
|
|
1898
|
+
}
|
|
1899
|
+
: error instanceof OperationEventsUnavailableError
|
|
1900
|
+
? {
|
|
1901
|
+
code: "operation_events_unavailable",
|
|
1902
|
+
retryable: true,
|
|
1903
|
+
suggestedAction:
|
|
1904
|
+
"Read operation_get for current status, then retry operation_logs for this same operation. Do not submit another deployment to recover logs.",
|
|
1905
|
+
}
|
|
1906
|
+
: error instanceof Error &&
|
|
1907
|
+
error.message.startsWith("EXPORT_PASSWORD_FILE_INVALID:")
|
|
1908
|
+
? {
|
|
1909
|
+
code: "export_password_file_invalid",
|
|
1910
|
+
retryable: false,
|
|
1911
|
+
suggestedAction:
|
|
1912
|
+
"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.",
|
|
1913
|
+
}
|
|
1914
|
+
: parseSafeProblem(error);
|
|
1915
|
+
} catch {
|
|
1916
|
+
safe = {
|
|
1917
|
+
code: "client_request_failed",
|
|
1918
|
+
retryable: false,
|
|
1919
|
+
suggestedAction:
|
|
1920
|
+
"Check the connection and run 'ohmyhost login' if authentication expired. Inspect the existing operation before resubmitting any mutation.",
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
return {
|
|
1924
|
+
...toolResult({
|
|
1925
|
+
error: {
|
|
1926
|
+
code: safe.code,
|
|
1927
|
+
retryable: safe.retryable,
|
|
1928
|
+
suggested_action: safe.suggestedAction,
|
|
1929
|
+
...(safe.requestId === undefined ? {} : { request_id: safe.requestId }),
|
|
1930
|
+
...(safe.retryAfterSeconds === undefined
|
|
1931
|
+
? {}
|
|
1932
|
+
: { retry_after_seconds: safe.retryAfterSeconds }),
|
|
1933
|
+
...(actingAs !== undefined && ACCOUNT_SENSITIVE_PROBLEMS.has(safe.code)
|
|
1934
|
+
? { acting_as: actingAs }
|
|
1935
|
+
: {}),
|
|
1936
|
+
},
|
|
1937
|
+
}),
|
|
1938
|
+
isError: true,
|
|
1939
|
+
};
|
|
1746
1940
|
}
|
|
1747
1941
|
|
|
1748
1942
|
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
|
);
|