Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cli/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ async function loadContextManifestForCommand(
const { loadMcpConfig, mcpManager } = await import("../../mcp/index.js");
try {
await mcpManager.configure(loadMcpConfig(cwd, { includeEnvLimits: true }));
await mcpManager.connectAll();
return loadUnifiedContextManifest(cwd, {
mcpStatus: mcpManager.getStatus(),
});
Expand All @@ -182,13 +183,15 @@ async function loadContextManifestPairForCommand(
await mcpManager.configure(
loadMcpConfig(beforeCwd, { includeEnvLimits: true }),
);
await mcpManager.connectAll();
const before = loadUnifiedContextManifest(beforeCwd, {
mcpStatus: mcpManager.getStatus(),
});

await mcpManager.configure(
loadMcpConfig(afterCwd, { includeEnvLimits: true }),
);
await mcpManager.connectAll();
const after = loadUnifiedContextManifest(afterCwd, {
mcpStatus: mcpManager.getStatus(),
});
Expand Down
10 changes: 7 additions & 3 deletions src/mcp/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export class McpClientManager extends EventEmitter {
const toAdd = nextConfig.servers.filter(
(server) => !oldServerNames.has(server.name),
);
const toAddNames = new Set(toAdd.map((server) => server.name));

this.config = nextConfig;

Expand All @@ -498,12 +499,15 @@ export class McpClientManager extends EventEmitter {
}),
);

// Connect new servers (don't wait for success)
// Connect new servers and unchanged servers that were explicitly disconnected.
await Promise.allSettled(
toAdd
nextConfig.servers
.filter(
(server) =>
!reconnectNames.has(server.name) &&
(toAddNames.has(server.name) ||
(oldServerNames.has(server.name) &&
!reconnectNames.has(server.name) &&
!this.servers.has(server.name))) &&
canConnectServer(server, nextApprovalMap),
)
.map((server) => this.connectServer(server)),
Expand Down
23 changes: 23 additions & 0 deletions test/agent/mcp-manager-transports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ describe("MCP manager remote transports", () => {
expect(manager.isConnected("remote-http")).toBe(true);
});

it("reconnects unchanged configured servers after disconnectAll", async () => {
const config = {
servers: [
{
name: "remote-http",
transport: "http" as const,
url: "https://example.com/mcp",
},
],
};

await manager.configure(config);
expect(manager.isConnected("remote-http")).toBe(true);

await manager.disconnectAll();
expect(manager.isConnected("remote-http")).toBe(false);

await manager.configure(config);

expect(httpTransportCtor).toHaveBeenCalledTimes(2);
expect(manager.isConnected("remote-http")).toBe(true);
});

it("emits sparse MCP connection and tool usage beacons", async () => {
vi.stubEnv("MAESTRO_TELEMETRY", "1");
vi.stubEnv("MAESTRO_BEACON_FILE", join(tempDir, "beacon.jsonl"));
Expand Down
72 changes: 71 additions & 1 deletion test/cli/context-command.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
handleContextCommand,
renderContextManifestDiff,
Expand All @@ -11,10 +11,35 @@ import {
diffUnifiedContextManifests,
loadUnifiedContextManifest,
} from "../../src/context/manifest.js";
import { loadMcpConfig, mcpManager } from "../../src/mcp/index.js";

vi.mock("../../src/mcp/index.js", () => ({
loadMcpConfig: vi.fn(() => ({ authPresets: [], servers: [] })),
mcpManager: {
configure: vi.fn(),
connectAll: vi.fn(),
disconnectAll: vi.fn(),
getStatus: vi.fn(() => ({ authPresets: [], servers: [] })),
},
}));

describe("context command", () => {
const tempDirs: string[] = [];

beforeEach(() => {
vi.mocked(loadMcpConfig)
.mockReset()
.mockReturnValue({ authPresets: [], servers: [] });
vi.mocked(mcpManager.configure).mockReset().mockResolvedValue(undefined);
vi.mocked(mcpManager.connectAll).mockReset().mockResolvedValue(undefined);
vi.mocked(mcpManager.disconnectAll)
.mockReset()
.mockResolvedValue(undefined);
vi.mocked(mcpManager.getStatus)
.mockReset()
.mockReturnValue({ authPresets: [], servers: [] });
});

afterEach(() => {
vi.restoreAllMocks();
for (const dir of tempDirs.splice(0)) {
Expand Down Expand Up @@ -280,4 +305,49 @@ describe("context command", () => {
expect(payload.afterCwd).toBe(resolve(afterRoot));
expect(payload.changed).toHaveLength(1);
});

it("reconnects live MCP servers before reading runtime status", async () => {
const root = makeTempDir();
writeFileSync(join(root, "AGENTS.md"), "root rules");
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);

await handleContextCommand("explain", [root, "--live-mcp", "--json"]);

expect(mcpManager.configure).toHaveBeenCalledTimes(1);
expect(mcpManager.connectAll).toHaveBeenCalledTimes(1);
expect(mcpManager.getStatus).toHaveBeenCalledTimes(1);
expect(mcpManager.disconnectAll).toHaveBeenCalledTimes(1);
expect(
vi.mocked(mcpManager.connectAll).mock.invocationCallOrder[0],
).toBeLessThan(
vi.mocked(mcpManager.getStatus).mock.invocationCallOrder[0]!,
);
expect(JSON.parse(String(log.mock.calls[0]?.[0])).version).toBe(1);
});

it("reconnects live MCP servers for both diff snapshots", async () => {
const beforeRoot = makeTempDir();
const afterRoot = makeTempDir();
writeFileSync(join(beforeRoot, "AGENTS.md"), "root rules");
writeFileSync(join(afterRoot, "AGENTS.md"), "new root rules");
vi.spyOn(console, "log").mockImplementation(() => undefined);

await handleContextCommand("diff", [
beforeRoot,
afterRoot,
"--live-mcp",
"--json",
]);

expect(mcpManager.configure).toHaveBeenCalledTimes(2);
expect(mcpManager.connectAll).toHaveBeenCalledTimes(2);
expect(mcpManager.getStatus).toHaveBeenCalledTimes(2);
expect(mcpManager.disconnectAll).toHaveBeenCalledTimes(1);
const connectOrders = vi.mocked(mcpManager.connectAll).mock
.invocationCallOrder;
const statusOrders = vi.mocked(mcpManager.getStatus).mock
.invocationCallOrder;
expect(connectOrders[0]).toBeLessThan(statusOrders[0]!);
expect(connectOrders[1]).toBeLessThan(statusOrders[1]!);
});
});
Loading