diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 3c53ca53..c3ba0355 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -1448,6 +1448,8 @@ You can use the `CONTEXT7_API_KEY` environment variable instead of passing the ` **Note:** The `--api-key` CLI flag takes precedence over the environment variable when both are provided. +You can optionally set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` to persist MCP session IDs in [Upstash Redis](https://upstash.com/). This lets multiple server instances share session state and expires idle sessions after 7 days. Without them, the server still works normally — sessions are simply not persisted or expired. + **Example with .env file:** ```bash diff --git a/packages/mcp/src/lib/redis.ts b/packages/mcp/src/lib/redis.ts index c71bb0cb..929dd20d 100644 --- a/packages/mcp/src/lib/redis.ts +++ b/packages/mcp/src/lib/redis.ts @@ -2,12 +2,19 @@ import { Redis } from "@upstash/redis"; let cached: Redis | undefined; +/** + * Whether Upstash Redis credentials are configured. + */ +export function hasRedisCredentials(): boolean { + return Boolean(process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN); +} + /** * Returns the shared Upstash Redis client. Throws if credentials are missing. */ export function getRedis(): Redis { if (cached) return cached; - if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) { + if (!hasRedisCredentials()) { throw new Error( "Upstash Redis credentials are required. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN." ); diff --git a/packages/mcp/src/lib/sessionStore.ts b/packages/mcp/src/lib/sessionStore.ts index e9a7cf56..7fe35e51 100644 --- a/packages/mcp/src/lib/sessionStore.ts +++ b/packages/mcp/src/lib/sessionStore.ts @@ -1,4 +1,4 @@ -import { getRedis } from "./redis.js"; +import { getRedis, hasRedisCredentials } from "./redis.js"; const SESSION_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days const REFRESH_THRESHOLD_SECONDS = 24 * 60 * 60; // 1 day — only extend TTL when below this @@ -8,7 +8,23 @@ const SESSION_KEY_PREFIX = "#mcp#session#"; // primitive — only an opaque identifier for log correlation and spec compliance — // so an unreachable Redis shouldn't block clients. Ghost sessions self-heal on // the next refresh (returns false → client gets 404 → re-inits). +// +// Redis itself is optional: without credentials, sessions are minted but not +// persisted or expired — refresh always succeeds. export function createSessionStore() { + if (!hasRedisCredentials()) { + console.warn( + "Upstash Redis credentials not set — MCP sessions will not be persisted or expired." + ); + return { + async create(_sessionId: string) {}, + async refresh(_sessionId: string) { + return true; + }, + async delete(_sessionId: string) {}, + }; + } + const redis = getRedis(); const getSessionKey = (sessionId: string) => `${SESSION_KEY_PREFIX}${sessionId}`; diff --git a/packages/mcp/test/sessionStore.test.ts b/packages/mcp/test/sessionStore.test.ts new file mode 100644 index 00000000..6f4107e0 --- /dev/null +++ b/packages/mcp/test/sessionStore.test.ts @@ -0,0 +1,35 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { createSessionStore } from "../src/lib/sessionStore.js"; + +beforeEach(() => { + vi.stubEnv("UPSTASH_REDIS_REST_URL", undefined); + vi.stubEnv("UPSTASH_REDIS_REST_TOKEN", undefined); + vi.spyOn(console, "warn").mockImplementation(() => {}); +}); + +afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +describe("createSessionStore without Redis credentials", () => { + test("returns a no-op store", async () => { + const store = createSessionStore(); + await expect(store.create("some-session-id")).resolves.toBeUndefined(); + await expect(store.refresh("some-session-id")).resolves.toBe(true); + await expect(store.delete("some-session-id")).resolves.toBeUndefined(); + }); + + test("warns about missing credentials", () => { + createSessionStore(); + expect(console.warn).toHaveBeenCalledTimes(1); + }); +}); + +describe("createSessionStore with Redis credentials", () => { + test("constructs the Redis-backed store without throwing", () => { + vi.stubEnv("UPSTASH_REDIS_REST_URL", "https://example.upstash.io"); + vi.stubEnv("UPSTASH_REDIS_REST_TOKEN", "fake-token"); + expect(() => createSessionStore()).not.toThrow(); + }); +});