@mesh-tech/mesh-cli 0.38.1 → 0.40.0

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.
Files changed (104) hide show
  1. package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-AUFQ2FGD.js} +1 -1
  2. package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-2ASXJ5DO.js} +235 -581
  3. package/dist/bin/mesh.chunk-2ASXJ5DO.js.map +6 -0
  4. package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-6AIG5OJM.js} +3 -3
  5. package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-6SSOVZOI.js} +1 -1
  6. package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-7HTKSUWR.js} +1900 -577
  7. package/dist/bin/mesh.chunk-7HTKSUWR.js.map +6 -0
  8. package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-F44A3ACW.js} +4 -4
  9. package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-F44A3ACW.js.map} +1 -1
  10. package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-HJGMOEUG.js} +1 -1
  11. package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-NSQJEX36.js} +1 -1
  12. package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-PZVI7V3J.js} +1 -1
  13. package/dist/bin/mesh.chunk-TDCH4SFQ.js +419 -0
  14. package/dist/bin/mesh.chunk-TDCH4SFQ.js.map +6 -0
  15. package/dist/bin/mesh.chunk-YE2WIY3Y.js +473 -0
  16. package/dist/bin/mesh.chunk-YE2WIY3Y.js.map +6 -0
  17. package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-Z3RKSTBO.js} +3 -3
  18. package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-XG26C6IQ.js} +2 -2
  19. package/dist/bin/mesh.generate-XSM4C6FQ.js +10 -0
  20. package/dist/bin/mesh.js +9 -8
  21. package/dist/bin/mesh.js.map +1 -1
  22. package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-3AWVGPCM.js} +2 -2
  23. package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-2ZDMHRCI.js} +1 -1
  24. package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-K5JQXEHX.js} +9 -8
  25. package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-JLQYFUNN.js} +4 -4
  26. package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-3AR27UGO.js} +3 -3
  27. package/dist/bin/mesh.registry-identity-3AR27UGO.js.map +6 -0
  28. package/dist/bin/mesh.review-command-LUVZQN2T.js +238 -0
  29. package/dist/bin/mesh.review-command-LUVZQN2T.js.map +6 -0
  30. package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-KZ5L6SOJ.js} +2 -2
  31. package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FCTT55KI.js} +7 -7
  32. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js +65 -0
  33. package/dist/bin/mesh.workspace-membership-DI3YQGEK.js.map +6 -0
  34. package/dist/build-info.json +2 -2
  35. package/dist/src/commands/app-check.js +2 -2
  36. package/dist/src/commands/dev-doctor.d.ts +22 -0
  37. package/dist/src/commands/dev-doctor.js +149 -0
  38. package/dist/src/commands/dev-env-leaks.d.ts +24 -0
  39. package/dist/src/commands/dev-env-leaks.js +178 -0
  40. package/dist/src/commands/dev-launch.d.ts +17 -1
  41. package/dist/src/commands/dev-launch.js +130 -5
  42. package/dist/src/commands/dev-readiness.d.ts +49 -0
  43. package/dist/src/commands/dev-readiness.js +78 -0
  44. package/dist/src/commands/dev.d.ts +60 -0
  45. package/dist/src/commands/dev.js +574 -140
  46. package/dist/src/commands/hub/index.js +10 -7
  47. package/dist/src/commands/local/app-authz.d.ts +118 -0
  48. package/dist/src/commands/local/app-authz.js +295 -0
  49. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  50. package/dist/src/commands/local/auth-provision.js +12 -1
  51. package/dist/src/commands/local/index.js +2 -0
  52. package/dist/src/commands/mcp/generate.d.ts +11 -0
  53. package/dist/src/commands/mcp/generate.js +78 -0
  54. package/dist/src/commands/mcp/index.d.ts +2 -0
  55. package/dist/src/commands/mcp/index.js +70 -0
  56. package/dist/src/commands/mcp/manifest-file.d.ts +5 -0
  57. package/dist/src/commands/mcp/manifest-file.js +24 -0
  58. package/dist/src/commands/mcp/review-command.d.ts +24 -0
  59. package/dist/src/commands/mcp/review-command.js +66 -0
  60. package/dist/src/commands/mcp/review.d.ts +41 -0
  61. package/dist/src/commands/mcp/review.js +140 -0
  62. package/dist/src/commands/mcp/workspace-membership.d.ts +7 -0
  63. package/dist/src/commands/mcp/workspace-membership.js +77 -0
  64. package/dist/src/commands/secrets/reindex.js +2 -7
  65. package/dist/src/commands/secrets/set.d.ts +22 -1
  66. package/dist/src/commands/secrets/set.js +31 -8
  67. package/dist/src/commands/temporal.js +51 -5
  68. package/dist/src/commands/temporal.js.map +2 -2
  69. package/dist/src/program.js +2 -0
  70. package/dist/src/utils/aws-auth.d.ts +4 -0
  71. package/dist/src/utils/aws-auth.js +7 -3
  72. package/dist/src/utils/dev-sessions-root.d.ts +3 -0
  73. package/dist/src/utils/dev-sessions-root.js +45 -0
  74. package/dist/src/utils/kubeconfig.js +2 -2
  75. package/dist/src/utils/tailscale.js +7 -1
  76. package/dist/src/utils/temporal-auth.js +2 -1
  77. package/dist/src/utils/workflow-fingerprint.js +23 -12
  78. package/fragments/base/index.ts.hbs +4 -1
  79. package/package.json +7 -5
  80. package/skills/mesh/SKILL.md +10 -6
  81. package/templates/api-auth/api/src/schema.ts.hbs +17 -0
  82. package/templates/api-auth/index.ts.hbs +1 -0
  83. package/templates/api-auth/package.json.hbs +3 -0
  84. package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
  85. package/templates/api-role-gating/index.ts.hbs +1 -0
  86. package/templates/external-service/index.ts.hbs +1 -0
  87. package/templates/workflow/index.ts.hbs +2 -0
  88. package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
  89. package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
  90. /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-AUFQ2FGD.js.map} +0 -0
  91. /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-6AIG5OJM.js.map} +0 -0
  92. /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-6SSOVZOI.js.map} +0 -0
  93. /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-HJGMOEUG.js.map} +0 -0
  94. /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-NSQJEX36.js.map} +0 -0
  95. /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-PZVI7V3J.js.map} +0 -0
  96. /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-Z3RKSTBO.js.map} +0 -0
  97. /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-XG26C6IQ.js.map} +0 -0
  98. /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.generate-XSM4C6FQ.js.map} +0 -0
  99. /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.login-3AWVGPCM.js.map} +0 -0
  100. /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.platform-setup-2ZDMHRCI.js.map} +0 -0
  101. /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.program-K5JQXEHX.js.map} +0 -0
  102. /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-JLQYFUNN.js.map} +0 -0
  103. /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-KZ5L6SOJ.js.map} +0 -0
  104. /package/dist/bin/{mesh.wizard-ZJE4S6PC.js.map → mesh.wizard-FCTT55KI.js.map} +0 -0
@@ -0,0 +1,70 @@
1
+ import * as path from "node:path";
2
+ import { logError, logInfo, logWarn } from "../../utils/log.js";
3
+ export function registerMcpCommands(program) {
4
+ const mcp = program
5
+ .command("mcp")
6
+ .description("Manifest-driven MCP Integrations (reviewed tool catalogs)");
7
+ mcp
8
+ .command("review <manifest>")
9
+ .description("Fetch a manifest's MCP server tools/list with the Integration's credential, diff it against the checked-in snapshot, and write the snapshot on confirmation")
10
+ .option("--yes", "Write without prompting")
11
+ .option("--dry-run", "Report the diff and write nothing")
12
+ .option("--stack <stack>", "Pulumi stack name (override detection)")
13
+ .option("-t, --tenant <tenant>", "Tenant to address (override detection)")
14
+ .option("--region <region>", "AWS region (default: us-east-2)")
15
+ .option("--secret <id>", "Secrets Manager secret holding the Integration credential (default: the deployed ExternalService's own secret)")
16
+ .action(async (manifest, opts) => {
17
+ const { runMcpReviewCommand } = await import("./review-command.js");
18
+ try {
19
+ await runMcpReviewCommand(manifest, opts);
20
+ }
21
+ catch (error) {
22
+ logError(error instanceof Error ? error.message : String(error));
23
+ process.exit(1);
24
+ }
25
+ });
26
+ mcp
27
+ .command("generate <manifest>")
28
+ .description("Write the worker package mesh.apps.McpIntegration deploys (package.json, main.ts, openapi.json) from a manifest and its reviewed snapshot; commit it with the lockfile")
29
+ .option("--out <dir>", "Package directory (default: <manifest dir>/.mesh-mcp/<name>)")
30
+ .option("--refresh-versions", "Re-resolve every dependency version instead of keeping the committed ones")
31
+ .action(async (manifest, opts) => {
32
+ const { generateMcpServeDir } = await import("./generate.js");
33
+ const { workspaceMembership } = await import("./workspace-membership.js");
34
+ try {
35
+ const { serveDir, written } = generateMcpServeDir(manifest, {
36
+ ...(opts.out ? { out: opts.out } : {}),
37
+ ...(opts.refreshVersions ? { refreshVersions: true } : {}),
38
+ });
39
+ if (written.length === 0) {
40
+ logInfo(`${serveDir} is current.`);
41
+ }
42
+ else {
43
+ logInfo(`Wrote ${written.join(", ")} in ${serveDir}.`);
44
+ if (written.includes("package.json")) {
45
+ logInfo("Run `pnpm install` so the lockfile records it, then commit both.");
46
+ }
47
+ }
48
+ const membership = workspaceMembership(serveDir);
49
+ if (!membership.workspaceFile) {
50
+ logWarn(`No pnpm-workspace.yaml above ${serveDir}: the worker image installs this package from a pnpm workspace, so it must be in one.`);
51
+ }
52
+ else if (!membership.member && membership.suggestedGlob) {
53
+ const { ensureWorkspaceGlobs } = await import("../create-app.js");
54
+ const added = ensureWorkspaceGlobs(path.dirname(membership.workspaceFile), [
55
+ membership.suggestedGlob,
56
+ ]);
57
+ if (added.length > 0) {
58
+ logInfo(`${membership.workspaceFile}: added ${added.join(", ")} so \`pnpm install\` records the package.`);
59
+ }
60
+ else {
61
+ logWarn(`${serveDir} is not a member of the workspace in ${membership.workspaceFile}, so \`pnpm install\` will not record it. Add "${membership.suggestedGlob}" to its packages.`);
62
+ }
63
+ }
64
+ }
65
+ catch (error) {
66
+ logError(error instanceof Error ? error.message : String(error));
67
+ process.exit(1);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,5 @@
1
+ import { type LoadedMcpManifest } from "@mesh-tech/api-registry/mcp-manifest";
2
+ export type LoadedMcpManifestFile = LoadedMcpManifest & {
3
+ toolsPath: string;
4
+ };
5
+ export declare function readMcpManifestFile(manifestPath: string): LoadedMcpManifestFile;
@@ -0,0 +1,24 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { parseMcpManifest, resolveMcpManifest, } from "@mesh-tech/api-registry/mcp-manifest";
4
+ function readJson(file) {
5
+ let text;
6
+ try {
7
+ text = readFileSync(file, "utf8");
8
+ }
9
+ catch (error) {
10
+ throw new Error(`MCP manifest: cannot read ${file} (${error instanceof Error ? error.message : String(error)})`);
11
+ }
12
+ try {
13
+ return JSON.parse(text);
14
+ }
15
+ catch (error) {
16
+ throw new Error(`MCP manifest: ${file} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
17
+ }
18
+ }
19
+ export function readMcpManifestFile(manifestPath) {
20
+ const absolute = path.resolve(manifestPath);
21
+ const manifest = parseMcpManifest(readJson(absolute));
22
+ const toolsPath = path.resolve(path.dirname(absolute), manifest.tools);
23
+ return { ...resolveMcpManifest(manifest, readJson(toolsPath), { toolsPath }), toolsPath };
24
+ }
@@ -0,0 +1,24 @@
1
+ import { SSMClient } from "@aws-sdk/client-ssm";
2
+ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3
+ import { type FetchMcpTools } from "./review.js";
4
+ export declare function resolveIntegrationCredentials(name: string, opts: {
5
+ stack?: string;
6
+ stage?: string;
7
+ tenant?: string;
8
+ region?: string;
9
+ secret?: string;
10
+ }, clients?: {
11
+ ssm?: SSMClient;
12
+ secretsManager?: Pick<SecretsManagerClient, "send">;
13
+ ensureCredentials?: (stack: string | undefined) => Promise<void>;
14
+ }): Promise<Record<string, string>>;
15
+ export declare const fetchMcpToolsOverHttp: FetchMcpTools;
16
+ export declare function runMcpReviewCommand(manifestPath: string, opts: {
17
+ yes?: boolean;
18
+ dryRun?: boolean;
19
+ stack?: string;
20
+ stage?: string;
21
+ tenant?: string;
22
+ region?: string;
23
+ secret?: string;
24
+ }): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import { SSMClient } from "@aws-sdk/client-ssm";
2
+ import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3
+ import confirm from "@inquirer/confirm";
4
+ import { logInfo } from "../../utils/log.js";
5
+ import { discoverStackExternalServices, ensureAwsCredentialsForStack, resolveExternalServiceScope, } from "../secrets/set.js";
6
+ import { readMcpManifestFile } from "./manifest-file.js";
7
+ import { reviewMcpCatalog } from "./review.js";
8
+ export async function resolveIntegrationCredentials(name, opts, clients = {}) {
9
+ const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
10
+ let secretId = opts.secret;
11
+ if (secretId) {
12
+ const ensure = clients.ensureCredentials ?? ensureAwsCredentialsForStack;
13
+ await ensure(resolveExternalServiceScope(opts).stack);
14
+ }
15
+ else {
16
+ const { scope, services } = await discoverStackExternalServices(opts, clients.ssm ?? new SSMClient({ region }), clients.ensureCredentials ? { ensureCredentials: clients.ensureCredentials } : {});
17
+ const service = services.get(name);
18
+ if (!service?.meta?.secretPrefix) {
19
+ throw new Error(`No deployed ExternalService named "${name}" in ${scope.context.tenant}/${scope.platformEnv} (stack ${scope.stack ?? "unknown"}). Deploy the Integration first, or pass --secret <id>.`);
20
+ }
21
+ secretId = service.meta.secretPrefix;
22
+ }
23
+ const response = await (clients.secretsManager ?? new SecretsManagerClient({ region })).send(new GetSecretValueCommand({ SecretId: secretId }));
24
+ if (!response.SecretString) {
25
+ throw new Error(`The Integration credential at "${secretId}" is empty — populate it with \`mesh secrets set external/${name}\``);
26
+ }
27
+ return JSON.parse(response.SecretString);
28
+ }
29
+ export const fetchMcpToolsOverHttp = async ({ serverUrl, headers, timeoutMs }) => {
30
+ const { Client, StreamableHTTPClientTransport } = await import("@modelcontextprotocol/client");
31
+ const client = new Client({ name: "mesh-mcp-review", version: "1.0.0" }, { capabilities: {}, versionNegotiation: { mode: "auto" } });
32
+ const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
33
+ requestInit: { headers, redirect: "error" },
34
+ });
35
+ try {
36
+ await client.connect(transport, { timeout: timeoutMs });
37
+ const result = await client.listTools(undefined, { timeout: timeoutMs });
38
+ return result.tools;
39
+ }
40
+ finally {
41
+ if (transport.sessionId)
42
+ await transport.terminateSession().catch(() => { });
43
+ await client.close().catch(() => { });
44
+ await transport.close().catch(() => { });
45
+ }
46
+ };
47
+ export async function runMcpReviewCommand(manifestPath, opts) {
48
+ const { manifest } = readMcpManifestFile(manifestPath);
49
+ const credentials = manifest.auth.kind === "shared-header"
50
+ ? await resolveIntegrationCredentials(manifest.name, opts)
51
+ : {};
52
+ if (manifest.auth.kind === "none") {
53
+ logInfo(`${manifest.name} declares no authentication; connecting unauthenticated.`);
54
+ }
55
+ const result = await reviewMcpCatalog({
56
+ manifestPath,
57
+ credentials,
58
+ fetchTools: fetchMcpToolsOverHttp,
59
+ ...(opts.yes ? { yes: true } : {}),
60
+ ...(opts.dryRun ? { dryRun: true } : {}),
61
+ confirm: (question) => confirm({ message: question, default: false }),
62
+ });
63
+ if (result.written) {
64
+ logInfo("Commit the snapshot with the regenerated worker package; McpIntegration refuses a package that does not match its manifest.");
65
+ }
66
+ }
@@ -0,0 +1,41 @@
1
+ import { type McpManifest, type McpTool, type McpToolAccess } from "@mesh-tech/api-registry/mcp-manifest";
2
+ export interface ToolDiffEntry {
3
+ name: string;
4
+ status: "added" | "removed" | "changed" | "unchanged";
5
+ access: McpToolAccess;
6
+ suggested?: "read";
7
+ detail?: string;
8
+ }
9
+ export interface SnapshotDiff {
10
+ entries: ToolDiffEntry[];
11
+ added: string[];
12
+ removed: string[];
13
+ changed: string[];
14
+ removedEnabled: string[];
15
+ clean: boolean;
16
+ next: McpTool[];
17
+ }
18
+ export declare function diffSnapshot(current: readonly McpTool[], fetched: readonly McpTool[], enabledOps?: readonly string[], classification?: Record<string, McpToolAccess>): SnapshotDiff;
19
+ export type FetchMcpTools = (input: {
20
+ serverUrl: string;
21
+ headers: Record<string, string>;
22
+ timeoutMs: number;
23
+ }) => Promise<unknown>;
24
+ export type ConfirmFn = (question: string) => Promise<boolean>;
25
+ export interface ReviewMcpCatalogOptions {
26
+ manifestPath: string;
27
+ credentials: Readonly<Record<string, string>>;
28
+ fetchTools: FetchMcpTools;
29
+ confirm?: ConfirmFn;
30
+ report?: (line: string) => void;
31
+ yes?: boolean;
32
+ dryRun?: boolean;
33
+ }
34
+ export interface ReviewMcpCatalogResult {
35
+ diff: SnapshotDiff;
36
+ manifest: McpManifest;
37
+ snapshotPath: string;
38
+ written: boolean;
39
+ }
40
+ export declare function reviewMcpCatalog(opts: ReviewMcpCatalogOptions): Promise<ReviewMcpCatalogResult>;
41
+ export declare function assertEnabledPresent(manifestName: string, enabledOps: readonly string[], catalog: readonly McpTool[]): void;
@@ -0,0 +1,140 @@
1
+ import * as fs from "node:fs";
2
+ import { classifyMcpTools, mcpAuthHeaders, parseMcpToolSnapshot, } from "@mesh-tech/api-registry/mcp-manifest";
3
+ import { defaultServeDir, generateMcpServeDir } from "./generate.js";
4
+ import { readMcpManifestFile } from "./manifest-file.js";
5
+ function canonical(tool) {
6
+ return JSON.stringify(tool, Object.keys(tool).sort());
7
+ }
8
+ function describeChange(before, after) {
9
+ const parts = [];
10
+ if (before.description !== after.description)
11
+ parts.push("description");
12
+ if (JSON.stringify(before.inputSchema) !== JSON.stringify(after.inputSchema))
13
+ parts.push("input schema");
14
+ if (JSON.stringify(before.outputSchema) !== JSON.stringify(after.outputSchema))
15
+ parts.push("output schema");
16
+ if (JSON.stringify(before.annotations) !== JSON.stringify(after.annotations))
17
+ parts.push("annotations (read/write classification)");
18
+ return parts.length > 0 ? parts.join(", ") : "other fields";
19
+ }
20
+ export function diffSnapshot(current, fetched, enabledOps = [], classification = {}) {
21
+ const before = new Map(current.map((t) => [t.name, t]));
22
+ const after = new Map(fetched.map((t) => [t.name, t]));
23
+ const classified = {
24
+ ...classifyMcpTools(current, classification),
25
+ ...classifyMcpTools(fetched, classification),
26
+ };
27
+ const accessOf = (name) => {
28
+ const entry = classified[name];
29
+ return { access: entry.access, ...(entry.suggested ? { suggested: entry.suggested } : {}) };
30
+ };
31
+ const entries = [];
32
+ for (const tool of fetched) {
33
+ const prior = before.get(tool.name);
34
+ if (!prior) {
35
+ entries.push({ name: tool.name, status: "added", ...accessOf(tool.name) });
36
+ }
37
+ else if (canonical(prior) !== canonical(tool)) {
38
+ entries.push({
39
+ name: tool.name,
40
+ status: "changed",
41
+ ...accessOf(tool.name),
42
+ detail: describeChange(prior, tool),
43
+ });
44
+ }
45
+ else {
46
+ entries.push({ name: tool.name, status: "unchanged", ...accessOf(tool.name) });
47
+ }
48
+ }
49
+ for (const tool of current) {
50
+ if (!after.has(tool.name)) {
51
+ entries.push({ name: tool.name, status: "removed", ...accessOf(tool.name) });
52
+ }
53
+ }
54
+ const pick = (status) => entries.filter((e) => e.status === status).map((e) => e.name);
55
+ const removed = pick("removed");
56
+ return {
57
+ entries,
58
+ added: pick("added"),
59
+ changed: pick("changed"),
60
+ removed,
61
+ removedEnabled: enabledOps.filter((op) => removed.includes(op)),
62
+ clean: pick("added").length === 0 && pick("changed").length === 0 && removed.length === 0,
63
+ next: [...fetched],
64
+ };
65
+ }
66
+ export async function reviewMcpCatalog(opts) {
67
+ const loaded = readMcpManifestFile(opts.manifestPath);
68
+ const { manifest } = loaded;
69
+ const report = opts.report ?? ((line) => console.log(line));
70
+ const snapshotPath = loaded.toolsPath;
71
+ const headers = mcpAuthHeaders(manifest, opts.credentials);
72
+ const fetched = parseMcpToolSnapshot(await opts.fetchTools({
73
+ serverUrl: manifest.serverUrl,
74
+ headers,
75
+ timeoutMs: manifest.timeoutMs ?? 30_000,
76
+ }), `${manifest.serverUrl} tools/list`);
77
+ if (fetched.length === 0) {
78
+ throw new Error(`${manifest.name}: the server answered tools/list with an empty catalog — refusing to replace a reviewed snapshot with nothing`);
79
+ }
80
+ const enabled = manifest.enabledOps ?? loaded.tools.map((t) => t.name);
81
+ const diff = diffSnapshot(loaded.tools, fetched, enabled, manifest.classification ?? {});
82
+ report(`${manifest.name} — ${manifest.serverUrl}`);
83
+ report(` snapshot: ${snapshotPath}`);
84
+ for (const entry of diff.entries) {
85
+ const mark = entry.status === "added"
86
+ ? "+"
87
+ : entry.status === "removed"
88
+ ? "-"
89
+ : entry.status === "changed"
90
+ ? "~"
91
+ : " ";
92
+ const enabledMark = enabled.includes(entry.name) ? "enabled" : "not enabled";
93
+ const suggestion = entry.suggested ? ", vendor suggests read" : "";
94
+ report(` ${mark} ${entry.name} [${entry.access}${suggestion}, ${enabledMark}]${entry.detail ? ` — ${entry.detail}` : ""}`);
95
+ }
96
+ const suggestedReads = diff.entries
97
+ .filter((entry) => entry.suggested === "read" && entry.status !== "removed")
98
+ .map((entry) => entry.name);
99
+ if (suggestedReads.length > 0) {
100
+ report(` the vendor marks ${suggestedReads.join(", ")} read-only (readOnlyHint). They stay unknown and need approval until the manifest's "classification" names them "read".`);
101
+ }
102
+ if (diff.added.length > 0) {
103
+ report(` ${diff.added.length} new upstream tool(s) will be recorded but NOT enabled: add them to enabledOps deliberately.`);
104
+ }
105
+ if (diff.removedEnabled.length > 0) {
106
+ throw new Error(`${manifest.name}: the server no longer offers enabled operation(s) ${diff.removedEnabled.join(", ")} — remove them from enabledOps first, then re-review`);
107
+ }
108
+ if (diff.clean) {
109
+ report(" no changes — the reviewed snapshot still matches the server.");
110
+ return { diff, manifest, snapshotPath, written: false };
111
+ }
112
+ if (opts.dryRun) {
113
+ report(" --dry-run: nothing written.");
114
+ return { diff, manifest, snapshotPath, written: false };
115
+ }
116
+ const approved = opts.yes === true ||
117
+ (await (opts.confirm ?? (async () => false))(`Write the new snapshot to ${snapshotPath}?`));
118
+ if (!approved) {
119
+ report(" not written.");
120
+ return { diff, manifest, snapshotPath, written: false };
121
+ }
122
+ fs.writeFileSync(snapshotPath, `${JSON.stringify(diff.next, null, 2)}\n`);
123
+ report(` wrote ${diff.next.length} reviewed tool(s).`);
124
+ const serveDir = defaultServeDir(opts.manifestPath, manifest.name);
125
+ if (fs.existsSync(serveDir)) {
126
+ const generated = generateMcpServeDir(opts.manifestPath);
127
+ report(` regenerated ${generated.written.join(", ") || "nothing"} in ${generated.serveDir}.`);
128
+ }
129
+ else {
130
+ report(` run \`mesh mcp generate ${opts.manifestPath}\` to write the worker package.`);
131
+ }
132
+ return { diff, manifest, snapshotPath, written: true };
133
+ }
134
+ export function assertEnabledPresent(manifestName, enabledOps, catalog) {
135
+ const names = new Set(catalog.map((t) => t.name));
136
+ const missing = enabledOps.filter((op) => !names.has(op));
137
+ if (missing.length > 0) {
138
+ throw new Error(`${manifestName}: cannot enable ${missing.join(", ")} — absent from the reviewed snapshot`);
139
+ }
140
+ }
@@ -0,0 +1,7 @@
1
+ export interface WorkspaceMembership {
2
+ workspaceFile?: string;
3
+ member: boolean;
4
+ suggestedGlob?: string;
5
+ }
6
+ export declare function matchesWorkspaceGlob(pattern: string, relDir: string): boolean;
7
+ export declare function workspaceMembership(dir: string): WorkspaceMembership;
@@ -0,0 +1,77 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { parse } from "yaml";
4
+ function findWorkspaceFile(from) {
5
+ let dir = from;
6
+ for (;;) {
7
+ const file = path.join(dir, "pnpm-workspace.yaml");
8
+ if (fs.existsSync(file))
9
+ return file;
10
+ const parent = path.dirname(dir);
11
+ if (parent === dir)
12
+ return undefined;
13
+ dir = parent;
14
+ }
15
+ }
16
+ function segmentPattern(segment) {
17
+ const body = segment
18
+ .split("")
19
+ .map((ch) => ch === "*" ? "[^/]*" : ch === "?" ? "[^/]" : ch.replace(/[.+^${}()|[\]\\]/g, "\\$&"))
20
+ .join("");
21
+ return new RegExp(segment.startsWith(".") ? `^${body}$` : `^(?!\\.)${body}$`);
22
+ }
23
+ function matchSegments(pattern, target) {
24
+ if (pattern.length === 0)
25
+ return target.length === 0;
26
+ const [head, ...rest] = pattern;
27
+ if (head === "**") {
28
+ for (let i = 0; i <= target.length; i++) {
29
+ if (target.slice(0, i).some((s) => s.startsWith(".")))
30
+ break;
31
+ if (matchSegments(rest, target.slice(i)))
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ if (target.length === 0)
37
+ return false;
38
+ return segmentPattern(head).test(target[0]) && matchSegments(rest, target.slice(1));
39
+ }
40
+ export function matchesWorkspaceGlob(pattern, relDir) {
41
+ const clean = (p) => p
42
+ .replace(/^\.\//, "")
43
+ .replace(/\/+$/, "")
44
+ .split("/")
45
+ .filter((s) => s.length > 0 && s !== ".");
46
+ return matchSegments(clean(pattern), clean(relDir.split(path.sep).join("/")));
47
+ }
48
+ export function workspaceMembership(dir) {
49
+ const abs = path.resolve(dir);
50
+ const workspaceFile = findWorkspaceFile(abs);
51
+ if (!workspaceFile)
52
+ return { member: false };
53
+ const root = path.dirname(workspaceFile);
54
+ const rel = path.relative(root, abs).split(path.sep).join("/");
55
+ const suggestedGlob = `${path.posix.dirname(rel)}/*`;
56
+ let globs;
57
+ try {
58
+ globs = parse(fs.readFileSync(workspaceFile, "utf8"))
59
+ ?.packages;
60
+ }
61
+ catch {
62
+ return { workspaceFile, member: false, suggestedGlob };
63
+ }
64
+ let member = false;
65
+ for (const glob of Array.isArray(globs) ? globs : []) {
66
+ if (typeof glob !== "string")
67
+ continue;
68
+ if (glob.startsWith("!")) {
69
+ if (matchesWorkspaceGlob(glob.slice(1), rel))
70
+ member = false;
71
+ }
72
+ else if (matchesWorkspaceGlob(glob, rel)) {
73
+ member = true;
74
+ }
75
+ }
76
+ return { workspaceFile, member, suggestedGlob };
77
+ }
@@ -2,9 +2,7 @@ import { SSMClient } from "@aws-sdk/client-ssm";
2
2
  import { SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, } from "@aws-sdk/client-secrets-manager";
3
3
  import { buildInstanceIndexValue, instanceIndexSecretId, instanceKeyFromSecretName, parseInstanceIndex, } from "@mesh-tech/secrets";
4
4
  import { logInfo, logSuccess, logError } from "../../utils/log.js";
5
- import { resolveStackOption } from "../../utils/stack-flag.js";
6
- import { resolveSecretsContext } from "./context.js";
7
- import { discoverExternalServices, ensureAwsCredentialsForStack, isResourceNotFound, } from "./set.js";
5
+ import { discoverStackExternalServices, isResourceNotFound } from "./set.js";
8
6
  export async function scanInstanceKeys(client, secretPrefix) {
9
7
  const keys = [];
10
8
  let nextToken;
@@ -26,13 +24,10 @@ export function indexAlreadyMatches(missing, stale, indexExists, malformed) {
26
24
  return missing.length === 0 && stale.length === 0 && indexExists && !malformed;
27
25
  }
28
26
  export async function reindexCommand(servicePath, opts) {
29
- const stackOpt = resolveStackOption(opts);
30
- const context = resolveSecretsContext(opts, stackOpt);
31
27
  const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
32
- await ensureAwsCredentialsForStack(stackOpt ?? context.stage);
33
28
  const ssmClient = new SSMClient({ region });
29
+ const { services } = await discoverStackExternalServices(opts, ssmClient);
34
30
  const smClient = new SecretsManagerClient({ region });
35
- const services = await discoverExternalServices(ssmClient, context.tenant, context.platformEnv);
36
31
  const serviceName = servicePath?.replace(/^external\//, "");
37
32
  if (!serviceName || !services.has(serviceName)) {
38
33
  if (serviceName) {
@@ -1,5 +1,7 @@
1
1
  import { SSMClient } from "@aws-sdk/client-ssm";
2
2
  import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3
+ import { type StackSelector } from "../../utils/stack-flag.js";
4
+ import type { MeshContext } from "../../utils/context.js";
3
5
  export declare function ensureAwsCredentialsForStack(stack: string | undefined): Promise<void>;
4
6
  interface SchemaField {
5
7
  type: "field";
@@ -25,10 +27,29 @@ export interface ExternalServiceMeta {
25
27
  description: string;
26
28
  secretPrefix: string;
27
29
  }
28
- export declare function discoverExternalServices(ssmClient: SSMClient, tenant: string, platformEnv: string): Promise<Map<string, {
30
+ export declare function discoverExternalServices(ssmClient: SSMClient, tenant: string, platformEnv: string, stack?: string): Promise<Map<string, {
29
31
  meta: ExternalServiceMeta;
30
32
  schema?: CredentialSchema;
31
33
  }>>;
34
+ export interface ExternalServiceScope {
35
+ context: MeshContext;
36
+ stack: string | undefined;
37
+ platformEnv: string;
38
+ }
39
+ export declare function resolveExternalServiceScope(opts: StackSelector & {
40
+ tenant?: string;
41
+ }): ExternalServiceScope;
42
+ export declare function discoverStackExternalServices(opts: StackSelector & {
43
+ tenant?: string;
44
+ }, ssmClient: SSMClient, deps?: {
45
+ ensureCredentials?: (stack: string | undefined) => Promise<void>;
46
+ }): Promise<{
47
+ scope: ExternalServiceScope;
48
+ services: Map<string, {
49
+ meta: ExternalServiceMeta;
50
+ schema?: CredentialSchema;
51
+ }>;
52
+ }>;
32
53
  export declare function setCommand(servicePath: string | undefined, opts: {
33
54
  key?: string;
34
55
  all?: boolean;
@@ -7,7 +7,7 @@ import { logInfo, logSuccess, logError } from "../../utils/log.js";
7
7
  import { resolveStackOption } from "../../utils/stack-flag.js";
8
8
  import { resolveSecretsContext } from "./context.js";
9
9
  import { findAppRoot, readStackConfig } from "../../utils/pulumi.js";
10
- import { resolveAwsCredentials } from "../../utils/aws-auth.js";
10
+ import { resolveAwsCredentials, derivePlatformRef } from "../../utils/aws-auth.js";
11
11
  import { isAwsErrorCode } from "../../utils/aws-errors.js";
12
12
  export async function ensureAwsCredentialsForStack(stack) {
13
13
  if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SESSION_TOKEN)
@@ -46,9 +46,11 @@ function normalizeEntry(raw) {
46
46
  function normalizeFields(raw) {
47
47
  return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, normalizeEntry(value)]));
48
48
  }
49
- export async function discoverExternalServices(ssmClient, tenant, platformEnv) {
49
+ export async function discoverExternalServices(ssmClient, tenant, platformEnv, stack) {
50
50
  const services = new Map();
51
51
  const basePath = `/mesh-platform/${tenant}/${platformEnv}`;
52
+ const stackSegment = stack ? `/stacks/${stack}/` : undefined;
53
+ const fromStack = new Set();
52
54
  let nextToken;
53
55
  do {
54
56
  const response = await ssmClient.send(new GetParametersByPathCommand({
@@ -59,9 +61,16 @@ export async function discoverExternalServices(ssmClient, tenant, platformEnv) {
59
61
  for (const param of response.Parameters ?? []) {
60
62
  if (!param.Name || !param.Value)
61
63
  continue;
64
+ const scoped = param.Name.includes("/stacks/");
65
+ if (stackSegment && scoped && !param.Name.includes(stackSegment))
66
+ continue;
62
67
  const metaMatch = param.Name.match(/\/external-services\/([^/]+)\/meta$/);
63
68
  if (metaMatch?.[1]) {
64
69
  const serviceName = metaMatch[1];
70
+ if (stackSegment && !scoped && fromStack.has(`meta:${serviceName}`))
71
+ continue;
72
+ if (stackSegment && scoped)
73
+ fromStack.add(`meta:${serviceName}`);
65
74
  const meta = JSON.parse(param.Value);
66
75
  const existing = services.get(serviceName);
67
76
  services.set(serviceName, { ...existing, meta });
@@ -69,6 +78,10 @@ export async function discoverExternalServices(ssmClient, tenant, platformEnv) {
69
78
  const credMatch = param.Name.match(/\/external-services\/([^/]+)\/credentials$/);
70
79
  if (credMatch?.[1]) {
71
80
  const serviceName = credMatch[1];
81
+ if (stackSegment && !scoped && fromStack.has(`schema:${serviceName}`))
82
+ continue;
83
+ if (stackSegment && scoped)
84
+ fromStack.add(`schema:${serviceName}`);
72
85
  const raw = JSON.parse(param.Value);
73
86
  const schema = {
74
87
  fields: normalizeFields(raw.fields),
@@ -146,17 +159,27 @@ function displaySummary(fields, values, depth = 0) {
146
159
  }
147
160
  }
148
161
  }
149
- export async function setCommand(servicePath, opts) {
162
+ export function resolveExternalServiceScope(opts) {
150
163
  const stackOpt = resolveStackOption(opts);
151
164
  const context = resolveSecretsContext(opts, stackOpt);
165
+ const stack = stackOpt ?? context.stage;
166
+ const appRoot = findAppRoot(process.cwd());
167
+ const platformEnv = (appRoot && stack ? derivePlatformRef(appRoot, stack)?.env : undefined) ?? context.platformEnv;
168
+ return { context, stack, platformEnv };
169
+ }
170
+ export async function discoverStackExternalServices(opts, ssmClient, deps = {}) {
171
+ const scope = resolveExternalServiceScope(opts);
172
+ await (deps.ensureCredentials ?? ensureAwsCredentialsForStack)(scope.stack);
173
+ const services = await discoverExternalServices(ssmClient, scope.context.tenant, scope.platformEnv, scope.stack);
174
+ return { scope, services };
175
+ }
176
+ export async function setCommand(servicePath, opts) {
152
177
  const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
153
- await ensureAwsCredentialsForStack(stackOpt ?? context.stage);
154
- const ssmClient = new SSMClient({ region });
178
+ const { services } = await discoverStackExternalServices(opts, new SSMClient({ region }));
155
179
  const smClient = new SecretsManagerClient({ region });
156
- const services = await discoverExternalServices(ssmClient, context.tenant, context.platformEnv);
157
180
  if (services.size === 0) {
158
181
  logError("No external services found. Deploy an ExternalService first:\n" +
159
- " new mesh.apps.ExternalService(\"symitar\", { env, credentials: { ... } })");
182
+ ' new mesh.apps.ExternalService("symitar", { env, credentials: { ... } })');
160
183
  process.exit(1);
161
184
  }
162
185
  let serviceName;
@@ -212,7 +235,7 @@ export async function setCommand(servicePath, opts) {
212
235
  values = JSON.parse(opts.json);
213
236
  }
214
237
  catch {
215
- logError("Invalid JSON input. Provide valid JSON, e.g.: --json='{\"key\": \"value\"}'");
238
+ logError('Invalid JSON input. Provide valid JSON, e.g.: --json=\'{"key": "value"}\'');
216
239
  return;
217
240
  }
218
241
  const merged = deepMerge(existing, values);