mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 17:31:51 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b180055aab |
No files matched your search
@@ -109,7 +109,25 @@ unfamiliar prefix. The unavailable fork reported in
|
||||
For a client that supports MCP catalog refresh, set
|
||||
`REA_MCP_TOOL_CATALOG_PROFILE=hot-cold` in REA's server environment and restart
|
||||
the connection. REA then advertises a small initial catalog and loads other
|
||||
named tools through `discover_tools`. Setup preserves this explicit setting.
|
||||
named tools through `discover_tools`. Setup can record the explicit choice:
|
||||
|
||||
```bash
|
||||
rea setup --client codex --tool-catalog-profile hot-cold --dry-run
|
||||
rea setup --client codex --tool-catalog-profile hot-cold
|
||||
```
|
||||
|
||||
The plan shows `REA_MCP_TOOL_CATALOG_PROFILE=hot-cold` before approval. This
|
||||
selects server catalog loading; enable and verify the client's catalog refresh
|
||||
separately. Setup preserves its existing feature flags and tool approval lists.
|
||||
To restore upfront advertisement, use `--tool-catalog-profile full`, then
|
||||
reconnect. Omitting the option preserves the saved profile and keeps `full` for
|
||||
a new registration. Existing explicit environment selections are also retained.
|
||||
|
||||
Setup does not infer refresh from a client name or enable experimental client
|
||||
features. The verified Codex trial below covers one version with an explicit
|
||||
flag; FastMCP was tested with explicit refresh calls. Neither establishes
|
||||
automatic refresh for every installed version or other client. Keep `full`
|
||||
until refresh is established for the selected client and configuration.
|
||||
See the [discovery contract](../reference/mcp-contracts.md#hotcold-catalog-discovery) for
|
||||
pagination, identity and family-scope semantics.
|
||||
|
||||
|
||||
@@ -754,3 +754,26 @@ describe("setup tool family scope", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("discloses the selected catalog profile before applying it with the family scope", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.doctorHealthy = true;
|
||||
host.skill = "unchanged";
|
||||
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
|
||||
const selection = {
|
||||
clientIds: ["cursor"],
|
||||
toolCatalogProfile: "hot-cold" as const,
|
||||
toolFamilies: "session,application",
|
||||
};
|
||||
const plan = await runSetup({ ...options(false), ...selection }, host);
|
||||
expect(plan.plannedActions[0]?.detail).toContain(
|
||||
"REA_MCP_TOOL_CATALOG_PROFILE=hot-cold",
|
||||
);
|
||||
expect(host.configuredProviderEnvironments).toEqual([]);
|
||||
const applied = await runSetup({ ...options(true), ...selection }, host);
|
||||
expect(applied.status).toBe("ready");
|
||||
expect(host.configuredProviderEnvironments).toContainEqual({
|
||||
REA_MCP_TOOL_FAMILIES: "session,application",
|
||||
REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold",
|
||||
});
|
||||
});
|
||||
@@ -131,14 +131,19 @@ const runSetupWorkflow = async (
|
||||
options.proposeHopper ?? (confirm !== undefined && !options.structured),
|
||||
initialDoctor: initialState.doctor,
|
||||
});
|
||||
// The advertised family scope is a registration setting, not a detected
|
||||
// provider fact; merge it after discovery so it cannot suppress the
|
||||
// Catalog loading and family scope are registration settings, not detected
|
||||
// provider facts; merge them after discovery so they cannot suppress the
|
||||
// Hopper installation offer on an otherwise unconfigured machine.
|
||||
if (options.toolFamilies !== undefined)
|
||||
providerEnvironment = {
|
||||
...providerEnvironment,
|
||||
REA_MCP_TOOL_FAMILIES: options.toolFamilies,
|
||||
};
|
||||
if (options.toolCatalogProfile !== undefined)
|
||||
providerEnvironment = {
|
||||
...providerEnvironment,
|
||||
REA_MCP_TOOL_CATALOG_PROFILE: options.toolCatalogProfile,
|
||||
};
|
||||
clientStates = discovery.clientStates;
|
||||
const clientSelectionAllowed =
|
||||
options.clientIds === undefined &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ClientConfigurationWriteProblem } from "./ClientConfigurationPublication.js";
|
||||
import type { McpToolCatalogProfile } from "../config/mcpToolCatalogProfile.js";
|
||||
import type { DoctorReport, DoctorScope, runDoctor } from "./Doctor.js";
|
||||
import type { LinuxDistribution } from "./LinuxHopper.js";
|
||||
import type { HopperRequestFailure } from "./HopperDownload.js";
|
||||
@@ -240,6 +241,8 @@ export interface SetupOptions {
|
||||
readonly installSkill?: boolean;
|
||||
/** Comma-separated tool families the MCP registration advertises. */
|
||||
readonly toolFamilies?: string;
|
||||
/** Explicit catalog loading policy; omitted preserves the saved selection. */
|
||||
readonly toolCatalogProfile?: McpToolCatalogProfile;
|
||||
readonly readinessScope?: DoctorScope;
|
||||
readonly dryRun?: boolean;
|
||||
readonly allDetectedClients?: boolean;
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
toolFamiliesConstraint,
|
||||
} from "../config/mcpToolFamilies.js";
|
||||
import { isUpdateFailure } from "../application/UpdateFailure.js";
|
||||
import {
|
||||
mcpToolCatalogProfileSchema,
|
||||
type McpToolCatalogProfile,
|
||||
} from "../config/mcpToolCatalogProfile.js";
|
||||
import {
|
||||
confirmInteractiveSetup,
|
||||
renderSetupDryRun,
|
||||
@@ -79,6 +83,11 @@ const registerSetupCommand = (
|
||||
.describe(
|
||||
"Comma-separated tool families this registration advertises; omit to preserve the saved selection, or advertise every tool if none is saved. To restore every tool, remove REA_MCP_TOOL_FAMILIES from the client's registration and restart the client.",
|
||||
),
|
||||
toolCatalogProfile: mcpToolCatalogProfileSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"MCP catalog: full advertises every in-scope tool; hot-cold loads tools on demand and requires client catalog refresh. Omit to preserve the saved profile, or use full if none is saved. Restart the client after changing it.",
|
||||
),
|
||||
allDetected: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
@@ -210,6 +219,7 @@ interface SetupCommandOptions {
|
||||
readonly client: readonly string[];
|
||||
readonly skillClient: readonly string[];
|
||||
readonly toolFamilies?: string | undefined;
|
||||
readonly toolCatalogProfile?: McpToolCatalogProfile | undefined;
|
||||
readonly allDetected: boolean;
|
||||
readonly skill?: boolean | undefined;
|
||||
readonly dryRun: boolean;
|
||||
@@ -285,6 +295,9 @@ const setupRunOptions = (
|
||||
...(options.toolFamilies === undefined
|
||||
? {}
|
||||
: { toolFamilies: options.toolFamilies }),
|
||||
...(options.toolCatalogProfile === undefined
|
||||
? {}
|
||||
: { toolCatalogProfile: options.toolCatalogProfile }),
|
||||
...(interactive ? { onProgress: renderSetupProgress } : {}),
|
||||
...(hasSelectedClients && !options.allDetected
|
||||
? {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
import { expect } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.for(["codex", "cursor"] as const)(
|
||||
"plans, preserves and reverses an explicit catalog selection for %s",
|
||||
async (client, { cli }) => {
|
||||
const home = await createTestTempDirectory("rea-setup-catalog-");
|
||||
const config = join(
|
||||
home,
|
||||
client === "codex" ? ".codex/config.toml" : ".cursor/mcp.json",
|
||||
);
|
||||
const original =
|
||||
client === "codex"
|
||||
? 'model = "caller-choice"\n[features]\nmcp_tool_list_changed = false\n'
|
||||
: JSON.stringify({ caller_choice: true });
|
||||
await mkdir(dirname(config), { recursive: true });
|
||||
await writeFile(config, original);
|
||||
const environment = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
XDG_DATA_HOME: join(home, ".local/share"),
|
||||
XDG_CACHE_HOME: join(home, ".cache"),
|
||||
REA_MCP_TOOL_CATALOG_PROFILE: undefined,
|
||||
};
|
||||
const setup = (approval: string, profile?: string) =>
|
||||
cli.run({
|
||||
arguments: [
|
||||
"setup",
|
||||
"--client",
|
||||
client,
|
||||
"--no-skill",
|
||||
approval,
|
||||
"--json",
|
||||
...(profile === undefined ? [] : ["--tool-catalog-profile", profile]),
|
||||
],
|
||||
cwd: home,
|
||||
environment,
|
||||
});
|
||||
const parse = (text: string) =>
|
||||
client === "codex" ? parseToml(text) : JSON.parse(text);
|
||||
const setting =
|
||||
client === "codex"
|
||||
? "mcp_servers.rea.env.REA_MCP_TOOL_CATALOG_PROFILE"
|
||||
: "mcpServers.rea.env.REA_MCP_TOOL_CATALOG_PROFILE";
|
||||
const backup = `${config}.rea.backup`;
|
||||
const planned = await setup("--dry-run", "hot-cold");
|
||||
expect(planned.exitCode, planned.stderr).toBe(0);
|
||||
expect(planned.json).toMatchObject({
|
||||
status: "planned",
|
||||
plannedActions: [
|
||||
expect.objectContaining({
|
||||
detail: expect.stringContaining(
|
||||
"REA_MCP_TOOL_CATALOG_PROFILE=hot-cold",
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(await readFile(config, "utf8")).toBe(original);
|
||||
await expect(access(backup)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const applied = await setup("--yes", "hot-cold");
|
||||
expect(applied.exitCode, applied.stderr).toBe(0);
|
||||
const saved = await readFile(config, "utf8");
|
||||
expect(parse(saved)).toHaveProperty(setting, "hot-cold");
|
||||
expect(parse(saved)).toMatchObject(parse(original));
|
||||
expect(await readFile(backup, "utf8")).toBe(original);
|
||||
for (const profile of ["hot-cold", undefined]) {
|
||||
const repeated = await setup("--yes", profile);
|
||||
expect(repeated.exitCode, repeated.stderr).toBe(0);
|
||||
expect(repeated.json).toMatchObject({
|
||||
status: "ready",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
});
|
||||
expect(await readFile(config, "utf8")).toBe(saved);
|
||||
}
|
||||
const restored = await setup("--yes", "full");
|
||||
expect(restored.exitCode, restored.stderr).toBe(0);
|
||||
const full = await readFile(config, "utf8");
|
||||
expect(parse(full)).toHaveProperty(setting, "full");
|
||||
expect(parse(full)).toMatchObject(parse(original));
|
||||
const files = await readdir(dirname(config));
|
||||
const invalid = await setup("--yes", "auto");
|
||||
expect(invalid.exitCode).not.toBe(0);
|
||||
expect(invalid.json).toMatchObject({ code: "VALIDATION_ERROR" });
|
||||
expect(await readFile(config, "utf8")).toBe(full);
|
||||
expect(await readdir(dirname(config))).toEqual(files);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["codex", "cursor"] as const)(
|
||||
"keeps the full catalog by default for %s without a verified refresh selection",
|
||||
async (client, { cli }) => {
|
||||
const home = await createTestTempDirectory("rea-setup-default-catalog-");
|
||||
const result = await cli.run({
|
||||
arguments: ["setup", "--client", client, "--no-skill", "--yes", "--json"],
|
||||
cwd: home,
|
||||
environment: {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
XDG_DATA_HOME: join(home, ".local/share"),
|
||||
XDG_CACHE_HOME: join(home, ".cache"),
|
||||
REA_MCP_TOOL_CATALOG_PROFILE: undefined,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const path = join(
|
||||
home,
|
||||
client === "codex" ? ".codex/config.toml" : ".cursor/mcp.json",
|
||||
);
|
||||
const text = await readFile(path, "utf8");
|
||||
const parsed = client === "codex" ? parseToml(text) : JSON.parse(text);
|
||||
expect(parsed).not.toHaveProperty(
|
||||
client === "codex"
|
||||
? "mcp_servers.rea.env.REA_MCP_TOOL_CATALOG_PROFILE"
|
||||
: "mcpServers.rea.env.REA_MCP_TOOL_CATALOG_PROFILE",
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user