Skip to content

Session Management

Seed implements two types of sessions: MCP Sessions for protocol communication and OAuth Client Sessions for registered clients. This page focuses on the session lifecycle and storage mechanisms.

Overview

Session management in Seed consists of:

  • MCP Sessions: UUID-based transport tracking for MCP protocol
  • OAuth Client Sessions: Redis-backed storage for dynamically registered clients
  • User Context: JWT-derived user information attached to requests

MCP Session Management

Session Lifecycle

MCP sessions track individual client connections and their associated transports.

mermaid
stateDiagram-v2
    [*] --> Created: Client sends initialize
    Created --> Active: Transport stored with UUID
    Active --> Active: Subsequent requests
    Active --> Expired: Connection closes
    Expired --> [*]

    note right of Created
        UUID generated
        Transport created
        Server metadata sent
    end note

    note right of Active
        Session ID in header
        Transport looked up
        Requests processed
    end note

Session Creation

When a client sends an initialize request:

  1. Request Received (no session ID):

    json
    {
      "jsonrpc": "2.0",
      "method": "initialize",
      "params": {
        "protocolVersion": "0.1.0",
        "capabilities": {}
      },
      "id": 1
    }
  2. UUID Generation:

    typescript
    const sessionId = randomUUID();
    // Example: "123e4567-e89b-12d3-a456-426614174000"
  3. Transport Creation:

    typescript
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => sessionId,
      enableJsonResponse: true,
      onsessioninitialized: (id) => {
        transports[id] = transport;
      },
    });
  4. Response with Session ID:

    http
    HTTP/1.1 200 OK
    mcp-session-id: 123e4567-e89b-12d3-a456-426614174000
    Content-Type: application/json
    
    {
      "jsonrpc": "2.0",
      "result": {
        "protocolVersion": "0.1.0",
        "capabilities": { "tools": {} },
        "serverInfo": {
          "name": "seed",
          "version": "0.1.3"
        }
      },
      "id": 1
    }

Session Storage

Storage Mechanism: In-memory JavaScript object

typescript
interface TransportStore {
  [sessionId: string]: StreamableHTTPServerTransport;
}

const transports: TransportStore = {};

Characteristics:

  • Ephemeral: Sessions lost on server restart
  • Fast: O(1) lookup by session ID
  • Automatic cleanup: Transports removed when connections close
  • No persistence: Not stored in Redis or database

Session Usage

After initialization, clients include the session ID in every request:

http
POST /mcp
Content-Type: application/json
mcp-session-id: 123e4567-e89b-12d3-a456-426614174000
Authorization: Bearer eyJhbGc...

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": { "name": "seed_ping", "arguments": {} },
  "id": 2
}

Lookup Process:

typescript
const sessionId = req.headers["mcp-session-id"];
const transport = transports[sessionId];

if (!transport) {
  return res.status(400).json({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Invalid session ID"
    },
    id: null
  });
}

await transport.handleRequest(req, res, req.body);

Session Expiration

Sessions expire when:

  1. Connection closes: HTTP connection terminated
  2. Server restarts: In-memory storage cleared
  3. Transport cleanup: MCP SDK garbage collection

No explicit timeout: Sessions persist as long as the connection is active.

OAuth Client Sessions

Client Registration Storage

Dynamically registered OAuth clients are stored in Redis with TTL.

Storage Format:

Key:   dcr:client:{client_id}
Value: JSON.stringify(RegisteredClient)
TTL:   2592000 seconds (30 days)

Example:

Key: dcr:client:seed-a1b2c3d4e5f6
Value: {
  "client_id": "seed-a1b2c3d4e5f6",
  "client_name": "My Application",
  "redirect_uris": ["https://app.example.com/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1702857600,
  "client_secret_expires_at": 0
}
TTL: 2592000

Client Lifecycle

mermaid
stateDiagram-v2
    [*] --> Registered: POST /oauth/register
    Registered --> Active: Used for OAuth flows
    Active --> Active: Token exchanges
    Active --> Expired: 30 days TTL
    Expired --> [*]

    note right of Registered
        Client ID generated
        Stored in Redis
        TTL: 30 days
    end note

    note right of Active
        Validated on authorize
        Validated on token
        Mapped to static IdP client
    end note

Client Lookup

Clients are retrieved from Redis during OAuth flows:

typescript
// Authorization endpoint
const client = await clientStore.get(clientId);
if (!client) {
  return res.redirect(`${redirectUri}?error=invalid_client`);
}

// Token endpoint
const client = await clientStore.get(params.client_id);
if (!client) {
  return res.status(401).json({
    error: "invalid_client",
    error_description: "Client not found or expired"
  });
}

Client Expiration

TTL-Based Expiration:

  • Default: 30 days (2,592,000 seconds)
  • Configurable via DCR_CLIENT_TTL environment variable
  • Redis automatically deletes expired keys
  • No renewal mechanism (clients must re-register)

Expiration Handling:

  1. Client not found in Redis
  2. Return invalid_client error
  3. Client must re-register via POST /oauth/register

User Context Sessions

JWT-Derived Context

Every authenticated request includes user context extracted from the JWT:

typescript
interface UserContext {
  sub: string;        // Subject (user ID) - REQUIRED
  email?: string;     // User email - OPTIONAL
  name?: string;      // Display name - OPTIONAL
  groups?: string[];  // Group memberships - OPTIONAL
  token: string;      // Original JWT token
}

Attachment to Request:

typescript
// src/middleware/auth.ts
const user: UserContext = {
  sub: payload.sub as string,
  token,
};

if (payload.email) user.email = payload.email as string;
if (payload.name) user.name = payload.name as string;
if (payload.groups) user.groups = payload.groups as string[];

req.user = user;

Context Availability

User context is available in:

  • Route handlers: via req.user
  • MCP tools: via request object (if passed to tool)
  • Middleware: for authorization checks

Example Usage in Tool:

typescript
export function registerAuthenticatedTool(server: McpServer): void {
  server.tool("get_user_info", "Get current user information", {}, async (params, req) => {
    const user = req.user;  // Access user context

    return {
      content: [
        {
          type: "text",
          text: `User: ${user.name} (${user.email})\nID: ${user.sub}`,
        },
      ],
    };
  });
}

Redis Connection Management

Connection Configuration

File: src/services/redis.ts

typescript
const redisClient = new Redis(config.dcr.redisUrl, {
  maxRetriesPerRequest: 3,
  retryStrategy(times: number) {
    // Exponential backoff: 100ms, 200ms, 300ms, ..., max 30s
    return Math.min(times * 100, 30000);
  },
  lazyConnect: true,
});

Configuration Details:

  • URL: redis://redis:6379 (default, configurable via REDIS_URL)
  • Max Retries: 3 attempts per request
  • Retry Strategy: Exponential backoff up to 30 seconds
  • Lazy Connect: Connection established on first command

Health Checking

typescript
export async function isRedisHealthy(): Promise<boolean> {
  try {
    const result = await redisClient.ping();
    return result.toUpperCase() === "PONG";
  } catch {
    return false;
  }
}

Used by: Health check endpoint (/health)

Error Handling

  • Connection errors logged but don't crash service
  • Exponential backoff prevents connection storms
  • Client operations fail gracefully if Redis unavailable
  • OAuth flows return server_error if storage unavailable

Session Security

MCP Sessions

  • Not Authenticated Separately: Rely on JWT middleware
  • UUID Unpredictability: 128-bit random IDs
  • Transport Isolation: Each session has its own transport
  • No Persistence: Can't be hijacked after server restart

OAuth Client Sessions

  • TTL-Based Expiration: Automatic cleanup after 30 days
  • Validation on Use: Clients validated on every OAuth flow
  • Redirect URI Matching: Strict validation of callback URLs
  • Public Clients: No client secrets (PKCE-protected)

User Context Security

  • JWT Validation: Every request validates JWT signature
  • Claim Verification: Issuer, audience, expiration checked
  • Stateless: No server-side user sessions
  • Token Refresh: Handled via OAuth refresh token flow

Monitoring and Observability

Session Metrics

MCP Sessions:

  • Count: Object.keys(transports).length
  • Active connections: Number of stored transports
  • Session creation rate: Monitor initialize requests

OAuth Clients:

  • Registered clients: Redis key count matching dcr:client:*
  • Active registrations: Clients used in last 30 days
  • Registration rate: Monitor /oauth/register requests

Health Checks

Redis Health:

bash
GET /health

Response:
{
  "status": "healthy",
  "redis": "connected"
}

MCP Session Count:

typescript
export function getSessionCount(): number {
  return Object.keys(transports).length;
}

Configuration

Environment Variables

bash
# Redis Configuration
REDIS_URL=redis://redis:6379

# OAuth Client TTL
DCR_CLIENT_TTL=2592000  # 30 days in seconds

# Rate Limiting (for client registration)
DCR_RATE_LIMIT_WINDOW_MS=3600000  # 1 hour
DCR_RATE_LIMIT_MAX=10             # 10 registrations per hour

Code Configuration

typescript
// src/config/dcr.ts
export const dcrConfig = {
  redisUrl: process.env.REDIS_URL ?? "redis://redis:6379",
  clientTtlSeconds: parseInt(process.env.DCR_CLIENT_TTL ?? "2592000", 10),
  keyPrefix: "dcr:client:",
  clientIdPrefix: "seed-",
  maxRedirectUris: 10,
  rateLimit: {
    windowMs: parseInt(process.env.DCR_RATE_LIMIT_WINDOW_MS ?? "3600000", 10),
    maxRequests: parseInt(process.env.DCR_RATE_LIMIT_MAX ?? "10", 10),
  },
};

Best Practices

MCP Sessions

  1. Generate session ID once: Don't regenerate on every request
  2. Store transport reference: Keep in session map for reuse
  3. Clean up on close: Remove from map when connection terminates
  4. Handle missing sessions: Return proper error for invalid session IDs

OAuth Clients

  1. Set appropriate TTL: Balance security and user experience
  2. Validate on every use: Don't trust cached client data
  3. Monitor Redis health: Implement alerting for connection issues
  4. Rate limit registration: Prevent abuse of registration endpoint

User Context

  1. Validate JWT on every request: Don't cache user context
  2. Extract only needed claims: Minimize data exposure
  3. Pass context to tools: Make user info available where needed
  4. Log access patterns: Monitor for suspicious activity

Implementation Files

  • MCP Sessions: src/mcp/mcp.ts, src/routes/mcp.ts
  • OAuth Clients: src/services/client-store.ts
  • Redis Client: src/services/redis.ts
  • User Context: src/middleware/auth.ts
  • Config: src/config/dcr.ts

Released under the MIT License.