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
20 changes: 17 additions & 3 deletions src/mcp/server/miftah-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,9 +476,23 @@ export class MiftahServer {
async close(): Promise<void> {
this.profileTransitionSession += 1;
this.profileTransitionConfirmations = new WeakMap<object, ProfileTransitionConfirmationBinding>();
await this.unsubscribeResourceSubscriptions(() => true);
await this.server.close();
await this.upstreams.close();
let closeFailure: { readonly error: unknown } | undefined;
try {
await this.unsubscribeResourceSubscriptions(() => true);
} catch (error) {
closeFailure = { error };
}
try {
await this.server.close();
} catch (error) {
closeFailure ??= { error };
}
try {
await this.upstreams.close();
} catch (error) {
closeFailure ??= { error };
}
if (closeFailure !== undefined) throw closeFailure.error;
await this.auditTrail.writeLifecycle({
operation: "wrapper/shutdown",
name: this.config.name,
Expand Down
84 changes: 84 additions & 0 deletions tests/mcp-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,58 @@ class DropInitializedNotificationTransport implements Transport {
}
}

class RejectingCloseTransport implements Transport {
constructor(
private readonly delegate: Transport,
private readonly closeError: Error
) {}

get onclose(): Transport["onclose"] {
return this.delegate.onclose;
}

set onclose(handler: Transport["onclose"]) {
this.delegate.onclose = handler;
}

get onerror(): Transport["onerror"] {
return this.delegate.onerror;
}

set onerror(handler: Transport["onerror"]) {
this.delegate.onerror = handler;
}

get onmessage(): Transport["onmessage"] {
return this.delegate.onmessage;
}

set onmessage(handler: Transport["onmessage"]) {
this.delegate.onmessage = handler;
}

get sessionId(): string | undefined {
return this.delegate.sessionId;
}

get setProtocolVersion(): Transport["setProtocolVersion"] {
return this.delegate.setProtocolVersion;
}

async start(): Promise<void> {
await this.delegate.start();
}

async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
await this.delegate.send(message, options);
}

async close(): Promise<void> {
await this.delegate.close();
throw this.closeError;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function deferred(): { readonly promise: Promise<void>; resolve(): void } {
let resolvePromise: (() => void) | undefined;
const promise = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -216,6 +268,38 @@ interface ProfileManagementHost {
};
}

describe("Miftah server lifecycle", () => {
it("stops upstream processes when downstream transport close rejects", async () => {
const config = validateConfig({
version: "1",
name: "accounts",
defaultProfile: "work",
upstream: { transport: "stdio", command: process.execPath, args: [fixture] },
profiles: { work: { env: { TEST_ACCOUNT_NAME: "work" } } },
audit: { enabled: false }
});
const upstreams = new UpstreamProcessManager(config.upstream!, config.profiles, { startupTimeoutMs: 5_000 });
const wrapper = new MiftahServer(config, new ProfileManager(config), upstreams);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "rejecting-close-client", version: "1.0.0" });
const closeError = new Error("simulated downstream close failure");

try {
await Promise.all([
wrapper.connect(new RejectingCloseTransport(serverTransport, closeError)),
client.connect(clientTransport)
]);
await client.listTools();
expect(upstreams.listHealth()).toMatchObject([{ profile: "work", processState: "running" }]);

await expect(wrapper.close()).rejects.toBe(closeError);
expect(upstreams.listHealth()).toMatchObject([{ profile: "work", processState: "stopped", pid: null }]);
} finally {
await Promise.allSettled([client.close(), wrapper.close(), upstreams.close()]);
}
});
});

describe("Miftah MCP wrapper", () => {
it("uses explicitly trusted tool annotations and records risk provenance", async () => {
const directory = await mkdtemp(join(tmpdir(), "miftah-risk-annotations-"));
Expand Down