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
95 changes: 91 additions & 4 deletions apps/server/src/plugin-transport.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createServer } from "node:http";
import { once } from "node:events";
import { expect, it } from "vitest";
import { PluginTransport } from "./plugin-transport.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { expect, it, vi } from "vitest";
import { contractPluginFixture } from "../../../scripts/contract-plugin-fixture.ts";
import { PluginTransport, readPluginContent, withPlugin } from "./plugin-transport.js";
it("pins one explicit local endpoint, strips ambient authority, bounds bytes and terminates the session", async () => {
let count = 0,
mode = "normal",
Expand Down Expand Up @@ -73,10 +75,10 @@ it("pins one explicit local endpoint, strips ambient authority, bounds bytes and
}
});
it("refuses a mixed public/private DNS answer before opening a socket", async () => {
const resolve = (async () => [
const resolve = async () => [
{ address: "8.8.8.8", family: 4 },
{ address: "127.0.0.1", family: 4 },
]);
];
const transport = new PluginTransport(
"https://example.com/mcp",
undefined,
Expand Down Expand Up @@ -108,3 +110,88 @@ it("cancellation settles an unresolved resolver without sending a request later"
await refused;
finish?.([{ address: "8.8.8.8", family: 4 }]);
});

it.each([
["tool", false],
["resource", false],
["tool", true],
["resource", true],
] as const)(
"retains an observed %s result when SDK close fails (deadline expired: %s)",
async (kind, expired) => {
const fixture = await contractPluginFixture();
const abort = new AbortController();
const close = Client.prototype.close;
const closing = vi.spyOn(Client.prototype, "close").mockImplementation(async function (
this: Client,
) {
if (expired) abort.abort(new Error("Synthetic operation deadline."));
await close.call(this);
throw new Error("Synthetic SDK cleanup failure.");
});
try {
const result = await withPlugin(
fixture.endpoint,
fixture.token,
[fixture.endpoint],
abort.signal,
async (client) =>
kind === "tool"
? client.callTool({ name: "read_value", arguments: {} }, undefined, {
signal: abort.signal,
})
: readPluginContent(client, "resource", "notes://public/info", {}, abort.signal),
);
expect(result).toEqual(
kind === "tool"
? { content: [{ type: "text", text: "42" }] }
: {
contents: [
{ uri: "notes://public/info", mimeType: "text/plain", text: "Untrusted 文档 🧪" },
],
},
);
const stats = await fetch(fixture.endpoint.replace("/mcp", "/fixture-control"), {
method: "POST",
headers: { Authorization: "Bearer " + fixture.controllerToken },
body: "{}",
}).then((response) => response.json());
expect(stats).toMatchObject({
toolCalls: kind === "tool" ? 1 : 0,
resourceReads: kind === "resource" ? 1 : 0,
deletedSessions: 1,
});
expect(closing).toHaveBeenCalledTimes(1);
} finally {
closing.mockRestore();
await fixture.close();
}
},
);

it.each(["operation failure", "caller cancellation"])(
"preserves %s when SDK close also fails",
async (mode) => {
const fixture = await contractPluginFixture();
const abort = new AbortController();
const original = new Error("Synthetic " + mode);
const close = Client.prototype.close;
const closing = vi.spyOn(Client.prototype, "close").mockImplementation(async function (
this: Client,
) {
await close.call(this);
throw new Error("Synthetic SDK cleanup failure.");
});
if (mode === "caller cancellation") abort.abort(original);
try {
await expect(
withPlugin(fixture.endpoint, fixture.token, [fixture.endpoint], abort.signal, async () => {
throw original;
}),
).rejects.toBe(original);
} finally {
closing.mockRestore();
await fixture.close();
}
},
);
2 changes: 2 additions & 0 deletions apps/server/src/plugin-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export async function withPlugin<T>(
signal.removeEventListener("abort", abort);
try {
await client.close();
} catch {
// Cleanup cannot replace an observed result or the operation's original failure.
} finally {
await transport.finish();
}
Expand Down
5 changes: 3 additions & 2 deletions scripts/check-affected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ else {
`${name}: no package-local tests; consumer coverage only, not a zero-test pass.`,
);
}
// Existing Turbo graph builds dependencies. Sequential phases prevent duplicate writers.
for (const task of ["typecheck", "test", "build"])
// Tests import their own compiled entry too; ^build alone only prepares dependencies.
// Sequential phases prevent duplicate writers and make cold checkouts reproducible.
for (const task of ["build", "typecheck", "test"])
run([
"exec",
"--",
Expand Down
Loading