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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules
!.yarn/plugins
!.yarn/releases
!.yarn/versions
package-lock.json

# testing
/coverage
Expand Down
30 changes: 30 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,36 @@ Agents can filter results by project(s) using optional HTTP headers:
}
```

#### Session Management

MCP sessions use **sliding window expiration** with activity tracking:

**Mechanism**:
- Each session tracks `lastActivity` timestamp
- **30-minute timeout**: Sessions expire after 30 minutes of **inactivity**
- **Auto-renewal**: Every request automatically renews the session (updates `lastActivity`)
- **Periodic cleanup**: Expired sessions are cleaned up every 5 minutes
- **Memory storage**: Sessions are stored in-memory (cleared on server restart)

**Example**:
```
Time 0:00 - Session created (lastActivity = 0:00)
Time 0:15 - Request made (lastActivity updated to 0:15)
Time 0:30 - Request made (lastActivity updated to 0:30)
Time 0:55 - No activity since 0:30 → Session expires (25 minutes inactive)
Time 1:00 - Cleanup runs, session deleted
```

**Client handling**:
- When a session expires, the client receives HTTP 404: `Session not found. Please reinitialize.`
- The client should automatically reinitialize by creating a new session
- This is transparent in MCP clients that support auto-reconnect

**Why this approach?**
- ✅ **No fixed timeout**: Active sessions don't expire mid-work
- ✅ **Resource efficiency**: Inactive sessions are cleaned up automatically
- ⚠️ **Server restart**: All sessions are lost on restart (mitigated by auto-reconnect)

#### Public Tools (All Agents)

| Tool | Description |
Expand Down
44 changes: 44 additions & 0 deletions docs/MCP_TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,50 @@ The following tools respect project filtering:

---

## Session Management

MCP sessions implement **sliding window expiration** with activity tracking to balance resource efficiency with user experience.

### Mechanism

- **Activity tracking**: Each session records `lastActivity` timestamp
- **30-minute timeout**: Sessions expire after 30 minutes of **inactivity** (not from creation time)
- **Auto-renewal**: Every MCP request automatically renews the session by updating `lastActivity`
- **Periodic cleanup**: Server checks for expired sessions every 5 minutes and cleans them up
- **Memory storage**: Sessions are stored in-memory and lost on server restart

### Example Timeline

```
Time 0:00 - Session created (lastActivity = 0:00)
Time 0:15 - API call made (lastActivity updated to 0:15)
Time 0:30 - API call made (lastActivity updated to 0:30)
Time 0:55 - No activity since 0:30 → Session expires (25 minutes inactive)
Time 1:00 - Cleanup runs, session deleted from memory
Time 1:05 - Client tries to use session → HTTP 404: "Session not found"
```

### Client Behavior

When a session expires:
1. Server returns HTTP 404: `{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found. Please reinitialize."},"id":null}`
2. MCP client should automatically reinitialize by creating a new session
3. This reconnection is transparent in clients that support auto-reconnect

### Why Sliding Expiration?

✅ **No mid-work expiration**: Active agents can work for hours without timeout
✅ **Resource efficient**: Inactive sessions are cleaned up automatically
⚠️ **Server restart impact**: All sessions lost on restart (mitigated by auto-reconnect)

### Best Practices

- **Implement auto-reconnect**: Handle HTTP 404 by reinitializing the session
- **Keep sessions alive**: Regular tool calls automatically prevent timeout
- **Clean shutdown**: Call DELETE `/api/mcp` when done to free resources

---

## Public Tools

Tools available to all Agents.
Expand Down
136 changes: 85 additions & 51 deletions public/chorus-plugin/bin/chorus-api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,64 +225,98 @@ cmd_mcp_tool() {

local mcp_url="${CHORUS_URL}/api/mcp"
local auth_header="Authorization: Bearer ${CHORUS_API_KEY}"

# Step 1: Initialize MCP session
local init_payload
init_payload=$(cat <<JSONEOF
local max_retries=3
local retry_count=0
local session_id=""
local tool_response=""

# Retry loop for auto-reconnection on 404 (session expired)
while [ $retry_count -le $max_retries ]; do
# Step 1: Initialize MCP session
local init_payload
init_payload=$(cat <<JSONEOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"chorus-hook","version":"0.1.1"}}}
JSONEOF
)

# Use a unique temp file for headers to avoid concurrent hooks overwriting each other
local headers_file
headers_file=$(mktemp "${STATE_DIR}/.mcp_headers.XXXXXX")

local init_response
init_response=$(curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-D "$headers_file" \
-d "$init_payload" \
"$mcp_url" 2>/dev/null) || { rm -f "$headers_file"; die "MCP initialize failed"; }

# Extract session ID from response headers
local session_id
session_id=$(grep -i "^mcp-session-id:" "$headers_file" | tr -d '\r' | awk '{print $2}')
rm -f "$headers_file"

if [ -z "$session_id" ]; then
die "No MCP session ID returned"
fi
# Use a unique temp file for headers to avoid concurrent hooks overwriting each other
local headers_file
headers_file=$(mktemp "${STATE_DIR}/.mcp_headers.XXXXXX")

local init_response
init_response=$(curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-D "$headers_file" \
-d "$init_payload" \
"$mcp_url" 2>/dev/null) || { rm -f "$headers_file"; die "MCP initialize failed"; }

# Extract session ID from response headers
session_id=$(grep -i "^mcp-session-id:" "$headers_file" | tr -d '\r' | awk '{print $2}')
rm -f "$headers_file"

if [ -z "$session_id" ]; then
die "No MCP session ID returned"
fi

# Step 2: Send initialized notification
local notif_payload='{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $session_id" \
-d "$notif_payload" \
"$mcp_url" >/dev/null 2>&1 || true

# Step 3: Call the tool
local call_payload
call_payload=$(printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$tool_name" "$arguments")

local tool_response
tool_response=$(curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $session_id" \
-d "$call_payload" \
"$mcp_url") || die "MCP tool call failed"
# Step 2: Send initialized notification
local notif_payload='{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $session_id" \
-d "$notif_payload" \
"$mcp_url" >/dev/null 2>&1 || true

# Step 3: Call the tool
local call_payload
call_payload=$(printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$tool_name" "$arguments")

# Capture HTTP status code and response separately
local http_code
local response_file
response_file=$(mktemp "${STATE_DIR}/.mcp_response.XXXXXX")

http_code=$(curl -s -S -X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $session_id" \
-d "$call_payload" \
-w "%{http_code}" \
-o "$response_file" \
"$mcp_url" 2>/dev/null) || http_code="000"

# Check if session expired (404)
if [ "$http_code" = "404" ]; then
retry_count=$((retry_count + 1))
rm -f "$response_file"

if [ $retry_count -le $max_retries ]; then
# Session expired - retry with new session
continue
else
die "MCP session expired after retry. Please reinitialize."
fi
fi

# Read the response
tool_response=$(cat "$response_file")
rm -f "$response_file"

# Break the retry loop - request succeeded
break
done

# Step 4: Close session (best effort)
curl -s -S -X DELETE \
-H "$auth_header" \
-H "mcp-session-id: $session_id" \
"$mcp_url" >/dev/null 2>&1 || true
if [ -n "$session_id" ]; then
curl -s -S -X DELETE \
-H "$auth_header" \
-H "mcp-session-id: $session_id" \
"$mcp_url" >/dev/null 2>&1 || true
fi

# Response may be SSE format (event: message\ndata: {...}) or plain JSON
# Strip SSE framing to get the JSON payload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ Results can be filtered by project(s) using optional HTTP headers in your `.mcp.

Sessions track which agent is working on which task, powering UI features (Kanban worker badges, Task Detail active workers, Settings page). The Chorus Plugin **fully automates** session lifecycle — sessions are created, heartbeated, and closed automatically. See [05-session-sub-agent.md](05-session-sub-agent.md) for details.

**MCP Session Lifecycle** (connection level):
- Sessions expire after 30 minutes of **inactivity** (sliding window)
- Each MCP request automatically renews the session
- Server restart clears all sessions (plugin auto-reconnects)

**What you do manually (the plugin handles everything else):**

| Tool | Purpose |
Expand Down
17 changes: 17 additions & 0 deletions public/skill/references/00-common-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ Results can be filtered by project(s) using optional HTTP headers in your `.mcp.

---

## Session Lifecycle

MCP sessions implement **sliding window expiration** — sessions expire after 30 minutes of **inactivity**, not from creation time.

**Key points**:
- Each request automatically renews the session (updates `lastActivity`)
- Active agents can work indefinitely without timeout
- Inactive sessions are cleaned up every 5 minutes
- Server restart clears all sessions (clients should auto-reconnect)

**When you see "Session not found" (HTTP 404)**:
- The session expired due to inactivity or server restart
- Your MCP client should automatically reinitialize
- This is transparent in clients with auto-reconnect support

---

## Project Groups

Projects can be organized into **Project Groups** — a single-level grouping for categorizing related projects together (e.g., all projects for the same product). A project belongs to at most one group, or can be ungrouped.
Expand Down
Loading
Loading