Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion packages/mcp/src/lib/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."
);
Expand Down
18 changes: 17 additions & 1 deletion packages/mcp/src/lib/sessionStore.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}`;
Expand Down
35 changes: 35 additions & 0 deletions packages/mcp/test/sessionStore.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading