Skip to content

everything: add a test that requestState expires after its 600-second TTL #5542

Open

Description

@reservoir-cmyk

Which server?

everything (@modelcontextprotocol/server-everything)

Protocol era

Modern (2026-07-28)

MCP client

No response

The problem

The multi-step tools' continuation token (requestState) is constructed with a declared TTL of 600 seconds (src/everything/server/index.ts:83-85). The existing tests in modern.test.ts pin the tamper case (a modified state is rejected with -32602, :319) and the happy-path round trip (:330) — but not expiry: that a correctly signed, untampered state is refused once its time has run out. Expiry is the security-relevant half of the TTL; if validation only ever checked the signature, an old state would work forever.

Case

Start trigger-multi-step-elicitation over a raw session and capture the requestState. Advance only the clock — vi.useFakeTimers({ toFake: ["Date"] }) plus setSystemTime +601 seconds, the same fake-Date pattern the minted-resource TTL test in the same file uses (:643) — then continue with a valid answer and assert the error is -32602, consistent with the tamper rejection. The state is untampered; only time has passed.

Solution you have in mind (optional)

Proposed test

Beside the existing requestState cases in src/everything/__tests__/modern.test.ts, using the raw-session harness:

import { afterEach, describe, expect, it, vi } from "vitest";
import { connectRaw, type RawSession } from "./harness.js";

let raw: RawSession | undefined;

afterEach(async () => {
  await raw?.close();
  raw = undefined;
  vi.useRealTimers();
});

describe("requestState expiry", () => {
  it("refuses a requestState continued after its 600-second TTL with -32602", async () => {
    raw = await connectRaw({ elicitation: {} });
    const first = await raw.call("trigger-multi-step-elicitation");
    expect(first).toMatchObject({
      resultType: "input_required",
      requestState: expect.any(String),
    });
    const state = (first as { requestState: string }).requestState;

    vi.useFakeTimers({ toFake: ["Date"] });
    vi.setSystemTime(Date.now() + 601_000);

    const response = await raw.request("tools/call", {
      name: "trigger-multi-step-elicitation",
      arguments: {},
      requestState: state,
      inputResponses: {
        name: { action: "accept", content: { name: "Ada" } },
      },
    });
    expect(response.error).toMatchObject({ code: -32602 });
  });
});

Which MCP feature does it demonstrate? (optional)

No response

Alternatives or workarounds you have tried

The only near hit, #4852, is a spec-refactor tracking issue, not a duplicate of this test proposal. No workaround covers this — expiry of a correctly signed, untampered requestState is not pinned by any existing test.

Already built it locally?

Verification

Ran against a byte-identical copy of v2/main @ 6cfff62: 1 passed — the TTL is enforced as declared. Full everything suite with it included: 27 files, 475 passed.

Prompt used

"In src/everything's modern tests, the requestState cases pin tamper rejection (-32602) and the happy-path round trip, but not expiry. Add one: start trigger-multi-step-elicitation over a raw session, capture the requestState, advance the clock 601 seconds with vi.useFakeTimers({ toFake: ["Date"] }) + setSystemTime (the pattern the minted-resource TTL test in the same file uses), then continue with a valid answer and assert the error is -32602. The state is untampered — only time has passed."

Before you submit

  • I searched existing issues and this is not a duplicate.
  • This is a request for this repository or one of its servers, not a new server, the MCP specification or an SDK.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions