pi-profile-switch 0.4.2 → 0.4.4

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 CHANGED
@@ -59,7 +59,6 @@ In the TUI, the `/profile` command family manages everything in-session:
59
59
  | `/profile use <name>` / `/profile reload` | Switch / reload without restarting (rollback on failure) |
60
60
  | `/profile create\|edit\|delete\|duplicate` | Guided profile CRUD (TUI only) |
61
61
  | `/profile customize` / `/profile reset` | Narrow the active profile for this session only |
62
- | `/mcp enable\|disable <server>` | Toggle MCP servers in the active profile |
63
62
 
64
63
  All commands work in non-interactive modes (`--mode rpc|print|json`); CRUD wizards are TUI-only.
65
64
 
package/README.zh-CN.md CHANGED
@@ -59,7 +59,6 @@ Profile 只**引用**资源,从不复制资源。已安装的包和标准目
59
59
  | `/profile use <name>` / `/profile reload` | 会话内切换 / 重载(失败自动回滚) |
60
60
  | `/profile create\|edit\|delete\|duplicate` | 向导式 profile 增删改(仅 TUI) |
61
61
  | `/profile customize` / `/profile reset` | 仅本次会话收窄活动 profile |
62
- | `/mcp enable\|disable <server>` | 在活动 profile 中开关 MCP server |
63
62
 
64
63
  非交互模式(`--mode rpc|print|json`)下命令同样生效;CRUD 向导仅 TUI 可用。
65
64
 
package/bin/pi-profile.ts CHANGED
@@ -29,7 +29,7 @@ try {
29
29
  const agentDir = getAgentDir();
30
30
  // Fails before spawning when the profile is unknown or cannot activate.
31
31
  // --approve/--no-approve are consumed here as a one-run trust input.
32
- const { plan, discovery, projectSettings, warnings } = await resolveInitialProfile(args.profile, {
32
+ const { plan, discovery, projectSettings, projectDir, warnings } = await resolveInitialProfile(args.profile, {
33
33
  agentDir,
34
34
  cwd: process.cwd(),
35
35
  trustOverride: args.trustOverride,
@@ -41,7 +41,7 @@ try {
41
41
  // window) are swept before this launch materializes its own. Best-effort:
42
42
  // sweep errors never block the launch.
43
43
  await sweepStaleRuntimeDirs(agentDir);
44
- const generated = await generateRuntimeDir(plan, { agentDir, discovery, projectSettings });
44
+ const generated = await generateRuntimeDir(plan, { agentDir, discovery, projectSettings, projectDir });
45
45
  process.exitCode = await spawnPi({
46
46
  generated,
47
47
  piArgs: args.piArgs,
@@ -21,8 +21,6 @@ import {
21
21
  runProfileDuplicateWizard,
22
22
  runProfileEditWizard,
23
23
  } from "../../src/switching/profile-wizard.ts";
24
- import { probeAdapterPresence } from "../../src/mcp-coordination.ts";
25
- import { setMcpServerEnabled } from "../../src/switching/mcp-toggle.ts";
26
24
  import { buildStatusReport, formatStatusMarkdown } from "../../src/switching/status.ts";
27
25
  import { switchProfile, type SwitchDeps } from "../../src/switching/switch-profile.ts";
28
26
  import { getGlobalStateDir } from "../../src/workspace.ts";
@@ -53,9 +51,6 @@ import { getGlobalStateDir } from "../../src/workspace.ts";
53
51
  * is stale afterwards.
54
52
  * - list/status ship structured `details` payloads (`{kind, profiles}` /
55
53
  * `{kind, report}`) for RPC consumers (ticket 11).
56
- * - `/mcp enable|disable <server>`: edit the active profile's mcp array in
57
- * its owning catalog, then reload (ticket 10). Fails clearly with the
58
- * adapter absent; notifies before the reload (stale context after).
59
54
  *
60
55
  * Pi re-executes this module on reload, so post-reload state is established
61
56
  * exclusively through `session_start` — nothing stale survives.
@@ -383,69 +378,4 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
383
378
  }
384
379
  },
385
380
  });
386
-
387
- // Persistent profile-scoped MCP toggles (ticket 10): edit the active
388
- // profile's mcp array in its owning catalog, then reload so the runtime
389
- // and the republished allowlist match. The adapter's own configuration
390
- // (mcp.json files) is never written.
391
- pi.registerCommand("mcp", {
392
- description: "pi-profile: /mcp enable <server> | /mcp disable <server>",
393
- handler: async (args, ctx) => {
394
- const notify = (message: string, level: "info" | "warning" | "error") => {
395
- try {
396
- ctx.ui?.notify(message, level);
397
- } catch {
398
- // stale context after reload — see /profile
399
- }
400
- };
401
- const [action, server] = args.trim().split(/\s+/).filter(Boolean);
402
- if (!["enable", "disable"].includes(action ?? "") || server === undefined) {
403
- notify("usage: /mcp enable <server> | /mcp disable <server>", "error");
404
- return;
405
- }
406
- try {
407
- const plan = await readLaunchPlanFile(runtimeDir);
408
- if (plan?.agentDir === undefined) {
409
- notify("pi-profile: launch plan is missing agentDir — cannot toggle MCP servers", "error");
410
- return;
411
- }
412
- if (!probeAdapterPresence(pi.events)) {
413
- throw new Error(
414
- "pi-mcp-adapter is not active in this session — /mcp enable|disable requires it " +
415
- "(select the adapter in the profile's extensions).",
416
- );
417
- }
418
- const result = await setMcpServerEnabled(
419
- { realAgentDir: plan.agentDir, cwd: ctx.cwd, profile: { name: plan.profile, source: plan.source } },
420
- server,
421
- action === "enable",
422
- );
423
- if (!result.changed) {
424
- notify(`MCP server "${server}" is already ${action}d in profile "${plan.profile}"`, "info");
425
- return;
426
- }
427
- // Notify BEFORE the reload: this context is stale afterwards.
428
- notify(
429
- `${action}d MCP server "${server}" in profile "${plan.profile}" (mcps: [${result.mcps.join(", ")}]); reloading`,
430
- "info",
431
- );
432
- const switched = await switchProfile(plan.profile, {
433
- runtimeDir,
434
- realAgentDir: plan.agentDir,
435
- cwd: ctx.cwd,
436
- waitForIdle: () => ctx.waitForIdle(),
437
- reload: () => ctx.reload(),
438
- // Same staleness probe as /profile: interactive Pi
439
- // swallows reload refusals; a live context afterwards
440
- // means the reload never ran.
441
- assertStale: () => {
442
- void ctx.cwd;
443
- },
444
- }, { reloadCurrent: true });
445
- for (const warning of switched.warnings) notify(warning, "warning");
446
- } catch (error) {
447
- notify(error instanceof Error ? error.message : String(error), "error");
448
- }
449
- },
450
- });
451
381
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-profile-switch",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -56,6 +56,8 @@ export interface InitialProfile {
56
56
  /** The trusted project's `.pi/settings.json` content, when trusted and
57
57
  * present. The generator merges it into the base for selection plans. */
58
58
  projectSettings?: Record<string, unknown>;
59
+ /** The trusted project directory, when trusted. */
60
+ projectDir?: string;
59
61
  /** Non-fatal notices for the user (e.g. a dangling restored profile that
60
62
  * fell back to default). The launcher prints them. */
61
63
  warnings: string[];
@@ -154,7 +156,7 @@ export async function resolveInitialProfile(
154
156
  overlay,
155
157
  });
156
158
  warnings.push(...discovery.extensions.warnings(), ...unmatchedWarnings(plan));
157
- return { plan, discovery, projectSettings, warnings };
159
+ return { plan, discovery, projectSettings, projectDir, warnings };
158
160
  }
159
161
 
160
162
  const discovery = await discoverLauncherResources({ ...context, projectTrusted });
@@ -175,5 +177,5 @@ export async function resolveInitialProfile(
175
177
  // silently do nothing or leak through unfiltered.
176
178
  throw new MissingMcpAdapterError(plan.profile);
177
179
  }
178
- return { plan, discovery, projectSettings, warnings };
180
+ return { plan, discovery, projectSettings, projectDir, warnings };
179
181
  }
package/src/mcp-config.ts CHANGED
@@ -1,25 +1,26 @@
1
1
  /**
2
- * AdapterConfigDiscovery: reads the MCP server NAMES pi-mcp-adapter would
3
- * discover from its pi-native config files, without ever managing them.
2
+ * AdapterConfigDiscovery: reads MCP server names and configurations
3
+ * pi-mcp-adapter would discover from its standard configuration files,
4
+ * without ever managing them.
4
5
  *
5
6
  * pi-profile never stores MCP connection parameters or credentials
6
- * (ADR-0002); this module reads only the `mcpServers` key names so the
7
- * launcher can validate a profile's `mcp` references before spawn.
7
+ * (ADR-0002); this module reads config definitions to validate references
8
+ * before spawn and filter instance mcp.json files.
8
9
  *
9
- * Discovery scope (documented limitation): the pi-native files only — the
10
- * global `<agentDir>/mcp.json` and, when trusted, the project's
10
+ * Discovery scope: standard user-global configs (~/.config/mcp/mcp.json,
11
+ * ~/.agents/mcp.json, ~/.agents/mcp/mcp.json), the Pi-global
12
+ * `<agentDir>/mcp.json`, and, when trusted, the project's `.mcp.json` and
11
13
  * `.pi/mcp.json`. Servers defined solely in the adapter's editor-specific
12
- * legacy locations (~/.claude/mcp.json et al.) are invisible here; profiles
13
- * referencing them fail launch validation. The pi-native files are the
14
- * adapter's documented default, and no in-session re-validation exists (the
15
- * adapter's status snapshots arrive too late and only post-init), so the
16
- * launch check is the only name validation — keep configs in the pi-native
17
- * files.
14
+ * legacy locations (~/.claude/mcp.json et al.) are invisible here unless
15
+ * imported.
18
16
  *
19
17
  * Malformed config files fail loudly — a broken mcp.json must not silently
20
18
  * read as "no servers" and reject every reference.
21
19
  */
22
20
 
21
+ import { homedir } from "node:os";
22
+ import path from "node:path";
23
+
23
24
  import { isRecord, readJsonFile } from "./json-file.ts";
24
25
 
25
26
  export class McpConfigError extends Error {
@@ -32,35 +33,104 @@ export class McpConfigError extends Error {
32
33
  }
33
34
  }
34
35
 
35
- async function readServerNames(filePath: string): Promise<string[]> {
36
- const result = await readJsonFile(filePath);
37
- if (!result.ok) {
38
- if (result.reason === "missing") {
39
- return [];
40
- }
41
- throw new McpConfigError(`MCP config is not valid JSON: ${filePath}`, filePath);
42
- }
43
- if (!isRecord(result.value)) {
44
- throw new McpConfigError(`MCP config must be a JSON object: ${filePath}`, filePath);
45
- }
46
- if (result.value.mcpServers === undefined) {
47
- return [];
48
- }
49
- if (!isRecord(result.value.mcpServers)) {
50
- throw new McpConfigError(`"mcpServers" must be a JSON object: ${filePath}`, filePath);
51
- }
52
- return Object.keys(result.value.mcpServers);
36
+ export interface McpDiscoveryOptions {
37
+ homeDir?: string;
53
38
  }
54
39
 
55
- /** Server names the adapter would discover: global agentDir config plus the
56
- * trusted project's config. Pass `projectDir` only when the trust check
57
- * passed — an untrusted project's config is never read. */
58
- export async function discoverAdapterServerNames(agentDir: string, projectDir?: string): Promise<string[]> {
59
- const names = new Set(await readServerNames(`${agentDir}/mcp.json`));
40
+ export interface MergedMcpResult {
41
+ servers: Record<string, Record<string, unknown>>;
42
+ sharedServers: Set<string>;
43
+ baseConfig?: Record<string, unknown>;
44
+ }
45
+
46
+ export interface McpConfigSource {
47
+ path: string;
48
+ isShared: boolean;
49
+ isAgentDir?: boolean;
50
+ }
51
+
52
+ /**
53
+ * Standard MCP configuration sources recognized by pi-mcp-adapter in precedence order:
54
+ * 1. ~/.config/mcp/mcp.json (user-global standard MCP)
55
+ * 2. ~/.agents/mcp.json (user-global .agents MCP)
56
+ * 3. ~/.agents/mcp/mcp.json (user-global .agents nested MCP)
57
+ * 4. <agentDir>/mcp.json (Pi global override)
58
+ * 5. <projectDir>/.mcp.json (project standard MCP, when project is trusted)
59
+ * 6. <projectDir>/.pi/mcp.json (project Pi override, when project is trusted)
60
+ */
61
+ export function getStandardMcpConfigSources(
62
+ agentDir: string,
63
+ projectDir?: string,
64
+ options?: McpDiscoveryOptions,
65
+ ): McpConfigSource[] {
66
+ const home = options?.homeDir ?? process.env.HOME ?? homedir();
67
+ const sources: McpConfigSource[] = [
68
+ { path: path.join(home, ".config", "mcp", "mcp.json"), isShared: true },
69
+ { path: path.join(home, ".agents", "mcp.json"), isShared: true },
70
+ { path: path.join(home, ".agents", "mcp", "mcp.json"), isShared: true },
71
+ { path: path.join(agentDir, "mcp.json"), isShared: false, isAgentDir: true },
72
+ ];
60
73
  if (projectDir !== undefined) {
61
- for (const name of await readServerNames(`${projectDir}/.pi/mcp.json`)) {
62
- names.add(name);
74
+ sources.push({ path: path.join(projectDir, ".mcp.json"), isShared: true });
75
+ sources.push({ path: path.join(projectDir, ".pi", "mcp.json"), isShared: false });
76
+ }
77
+ return sources;
78
+ }
79
+
80
+ export async function loadMergedMcpServers(
81
+ agentDir: string,
82
+ projectDir?: string,
83
+ options?: McpDiscoveryOptions,
84
+ ): Promise<MergedMcpResult> {
85
+ const sources = getStandardMcpConfigSources(agentDir, projectDir, options);
86
+ const seenPaths = new Set<string>();
87
+ const servers: Record<string, Record<string, unknown>> = {};
88
+ const sharedServers = new Set<string>();
89
+ let baseConfig: Record<string, unknown> | undefined;
90
+
91
+ for (const source of sources) {
92
+ const resolvedPath = path.resolve(source.path);
93
+ if (seenPaths.has(resolvedPath)) continue;
94
+ seenPaths.add(resolvedPath);
95
+
96
+ const result = await readJsonFile(resolvedPath);
97
+ if (!result.ok) {
98
+ if (result.reason === "missing") continue;
99
+ throw new McpConfigError(`MCP config is not valid JSON: ${resolvedPath}`, resolvedPath);
100
+ }
101
+ if (!isRecord(result.value)) {
102
+ throw new McpConfigError(`MCP config must be a JSON object: ${resolvedPath}`, resolvedPath);
103
+ }
104
+ if (source.isAgentDir) {
105
+ baseConfig = result.value;
106
+ }
107
+ if (result.value.mcpServers === undefined) continue;
108
+ if (!isRecord(result.value.mcpServers)) {
109
+ throw new McpConfigError(`"mcpServers" must be a JSON object: ${resolvedPath}`, resolvedPath);
110
+ }
111
+ for (const [name, def] of Object.entries(result.value.mcpServers)) {
112
+ if (source.isShared) {
113
+ sharedServers.add(name);
114
+ }
115
+ if (isRecord(def)) {
116
+ servers[name] = { ...(servers[name] ?? {}), ...def };
117
+ } else {
118
+ servers[name] = { ...(servers[name] ?? {}) };
119
+ }
63
120
  }
64
121
  }
65
- return [...names].sort();
122
+
123
+ return { servers, sharedServers, baseConfig };
124
+ }
125
+
126
+ /** Server names the adapter would discover: standard global MCP configs,
127
+ * global agentDir config, plus the trusted project's configs. Pass `projectDir`
128
+ * only when the trust check passed — an untrusted project's config is never read. */
129
+ export async function discoverAdapterServerNames(
130
+ agentDir: string,
131
+ projectDir?: string,
132
+ options?: McpDiscoveryOptions,
133
+ ): Promise<string[]> {
134
+ const { servers } = await loadMergedMcpServers(agentDir, projectDir, options);
135
+ return Object.keys(servers).sort();
66
136
  }
@@ -45,6 +45,7 @@ import path from "node:path";
45
45
  import type { ActivationPlan } from "./profile-resolver.ts";
46
46
  import { getInstancesRootDir } from "./workspace.ts";
47
47
  import { isRecord } from "./json-file.ts";
48
+ import { loadMergedMcpServers } from "./mcp-config.ts";
48
49
  import type { SkillEntry } from "./skill-registry.ts";
49
50
 
50
51
  /** A configured global package and its resolved install/local root. */
@@ -66,6 +67,10 @@ export interface DiscoveryContext {
66
67
  export interface GenerateOptions {
67
68
  /** The user's real agent dir (e.g. ~/.pi/agent). */
68
69
  agentDir: string;
70
+ /** Optional home dir override (useful for testing). */
71
+ homeDir?: string;
72
+ /** Optional trusted project dir. */
73
+ projectDir?: string;
69
74
  /** Required for selection plans; unused for the default profile. */
70
75
  discovery?: DiscoveryContext;
71
76
  /** The trusted project's `.pi/settings.json` content (already parsed).
@@ -276,6 +281,10 @@ function buildSelectionSettings(
276
281
  export interface RuntimeFileOptions {
277
282
  /** The user's real agent dir (e.g. ~/.pi/agent). */
278
283
  agentDir: string;
284
+ /** Optional home dir override (useful for testing). */
285
+ homeDir?: string;
286
+ /** Optional trusted project dir. */
287
+ projectDir?: string;
279
288
  /** Required for selection plans; unused for the default profile. */
280
289
  discovery?: DiscoveryContext;
281
290
  /** The trusted project's `.pi/settings.json` content (already parsed). */
@@ -418,29 +427,34 @@ export async function writeRuntimeFiles(
418
427
  } else {
419
428
  // Filter MCP servers
420
429
  try { await rm(mcpInstancePath); } catch {}
421
- if (await exists(mcpTarget)) {
422
- try {
423
- const mcpContent = await readFile(mcpTarget, "utf8");
424
- let mcpParsed = JSON.parse(mcpContent);
425
- if (isRecord(mcpParsed) && isRecord(mcpParsed.mcpServers)) {
426
- const filteredServers: Record<string, unknown> = {};
427
- for (const serverName of plan.mcps) {
428
- if (mcpParsed.mcpServers[serverName] !== undefined) {
429
- filteredServers[serverName] = mcpParsed.mcpServers[serverName];
430
- }
431
- }
432
- mcpParsed.mcpServers = filteredServers;
433
- await writeFile(mcpInstancePath, JSON.stringify(mcpParsed, null, 2));
434
- } else {
435
- // Malformed or empty, write empty
436
- await writeFile(mcpInstancePath, JSON.stringify({ mcpServers: {} }, null, 2));
437
- }
438
- } catch {
439
- await writeFile(mcpInstancePath, JSON.stringify({ mcpServers: {} }, null, 2));
430
+ const { servers, sharedServers, baseConfig } = await loadMergedMcpServers(
431
+ options.agentDir,
432
+ options.projectDir,
433
+ options.homeDir !== undefined ? { homeDir: options.homeDir } : undefined,
434
+ );
435
+
436
+ const allowedSet = new Set(plan.mcps);
437
+ const filteredServers: Record<string, unknown> = {};
438
+
439
+ for (const serverName of plan.mcps) {
440
+ if (servers[serverName] !== undefined) {
441
+ const def = { ...servers[serverName] };
442
+ delete def.disabled;
443
+ filteredServers[serverName] = def;
444
+ }
445
+ }
446
+
447
+ for (const sharedName of sharedServers) {
448
+ if (!allowedSet.has(sharedName)) {
449
+ filteredServers[sharedName] = { disabled: true };
440
450
  }
441
- } else {
442
- await writeFile(mcpInstancePath, JSON.stringify({ mcpServers: {} }, null, 2));
443
451
  }
452
+
453
+ const outputConfig: Record<string, unknown> = isRecord(baseConfig)
454
+ ? { ...baseConfig, mcpServers: filteredServers }
455
+ : { mcpServers: filteredServers };
456
+
457
+ await writeFile(mcpInstancePath, JSON.stringify(outputConfig, null, 2));
444
458
  }
445
459
 
446
460
  // Instructions generation (Ticket 04)
@@ -164,6 +164,7 @@ export async function switchProfile(
164
164
  : undefined;
165
165
  await writeRuntimeFiles(deps.runtimeDir, resolved.plan, {
166
166
  agentDir: deps.realAgentDir,
167
+ projectDir: resolved.projectDir,
167
168
  discovery: resolved.discovery,
168
169
  projectSettings: resolved.projectSettings,
169
170
  planExtras: {
@@ -1,71 +0,0 @@
1
- /**
2
- * McpToggle: persistent, profile-scoped `/mcp enable|disable` (ticket 10).
3
- *
4
- * The profile's `mcps` array in its OWNING catalog is the profile-scoped
5
- * state store (ticket 04 established that pi-mcp-adapter@2.33.0 has no
6
- * allowlist/profile-state API — ADR-0002's assumed store does not exist;
7
- * pi-profile owns the contract). Runtime effect flows through the standard
8
- * rewrite-settings-and-reload path: the post-reload session_start
9
- * republishes the allowlist over the coordination channel.
10
- *
11
- * Invariants:
12
- * - Enable accepts only adapter-discovered names (fail fast on typos);
13
- * disable also removes names the adapter no longer reports (stale
14
- * cleanup).
15
- * - The built-in default profile has no catalog entry — toggling it is a
16
- * clear error, not a synthetic write.
17
- * - Only the ACTIVE profile's array changes; other profiles and the
18
- * adapter's own configuration (global mcp.json, project .pi/mcp.json)
19
- * are never modified.
20
- */
21
-
22
- import { discoverAdapterServerNames } from "../mcp-config.ts";
23
- import { CatalogError, DEFAULT_PROFILE_NAME, type ProfileDefinition } from "../profile-catalog.ts";
24
- import { readTrustInputs as readTrust } from "../launcher/initial-profile.ts";
25
- import { catalogStore, readCatalogScope, type CatalogScope } from "./profile-crud.ts";
26
-
27
- export async function setMcpServerEnabled(
28
- input: { realAgentDir: string; cwd: string; profile: { name: string; source: string } },
29
- server: string,
30
- enabled: boolean,
31
- ): Promise<{ mcps: string[]; changed: boolean }> {
32
- if (input.profile.name === DEFAULT_PROFILE_NAME || input.profile.source === "builtin") {
33
- throw new CatalogError(
34
- `the built-in default profile has no catalog entry — create a named profile (/profile create) to toggle MCP servers`,
35
- );
36
- }
37
- const scope = input.profile.source as CatalogScope;
38
- if (scope !== "global" && scope !== "project") {
39
- throw new CatalogError(`profile "${input.profile.name}" has no writable owning catalog (source: ${input.profile.source})`);
40
- }
41
-
42
- const { projectTrusted } = await readTrust({ agentDir: input.realAgentDir, cwd: input.cwd });
43
- if (scope === "project" && !projectTrusted) {
44
- throw new CatalogError(`project catalog is unavailable: ${input.cwd} is not trusted`);
45
- }
46
- const discovered = await discoverAdapterServerNames(input.realAgentDir, projectTrusted ? input.cwd : undefined);
47
- if (enabled && !discovered.includes(server)) {
48
- throw new CatalogError(
49
- `unknown MCP server "${server}" — adapter discovered: [${discovered.join(", ") || "(none)"}]`,
50
- );
51
- }
52
-
53
- const definitions = await readCatalogScope(input, scope);
54
- const definition = definitions.get(input.profile.name);
55
- if (definition === undefined) {
56
- throw new CatalogError(`profile "${input.profile.name}" not found in the ${scope} catalog`);
57
- }
58
-
59
- const current = definition.mcps ?? [];
60
- if (enabled === current.includes(server)) {
61
- return { mcps: current, changed: false }; // already in the requested state
62
- }
63
- const next = enabled ? [...current, server] : current.filter((name) => name !== server);
64
- // Drop the key entirely when empty (exactOptionalPropertyTypes; a
65
- // written `mcps: undefined` would also misrepresent the definition).
66
- const rest = { ...definition };
67
- delete rest.mcps;
68
- const updated: ProfileDefinition = next.length > 0 ? { ...rest, mcps: next } : rest;
69
- await catalogStore(input, scope).upsert(input.profile.name, updated);
70
- return { mcps: next, changed: true };
71
- }