-
Notifications
You must be signed in to change notification settings - Fork 41
feat: Add comprehensive Pages API support with session authentication #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Conversation
- Implement session-based authentication for Pages endpoints - Add 18 complete page management tools covering all API operations - Support dual authentication: session cookies for /api/, API key for /api/v1/ - Add cookie jar persistence using tough-cookie and axios-cookiejar-support Pages Tools Added: - Core CRUD: list, get, create, update, delete - Lifecycle: archive, unarchive, lock, unlock - Organization: favorite, unfavorite, duplicate, set_page_access, get_pages_summary - Content: get_page_description, update_page_description - History: get_page_versions, get_page_version Authentication: - plane_login: Email/password authentication with session cookies - plane_auth_status: Check current authentication state - plane_logout: Clear session Technical Details: - Form-based authentication with CSRF token handling - Automatic API prefix routing (/api/ vs /api/v1/) - Cookie persistence across MCP tool invocations - Comprehensive debug logging for troubleshooting
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds cookie-jar-backed session authentication with CSRF handling, dynamic routing between session-based pages endpoints and API-key endpoints, a new Page Zod schema/type, MCP tools for authentication and page management, debug logging, and two runtime cookie dependencies. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant MCP as MCP Server
participant Auth as auth.ts
participant Req as request-helper.ts
participant Axios as Axios+CookieJar
participant Plane as Plane API
User->>MCP: plane_login(email,password)
MCP->>Auth: authenticateWithPassword(email,password,host)
Auth->>Axios: getAxiosInstance()
Auth->>Plane: GET /auth/get-csrf-token/
Plane-->>Axios: csrftoken cookie
Auth->>Plane: POST /auth/sign-in/ (form + X-CSRFToken)
Plane-->>Axios: session cookies
Auth-->>MCP: AuthResult (success/error)
User->>MCP: invoke page tool (e.g., list_pages)
MCP->>Req: makePlaneRequest(endpoint,...)
Req->>Auth: isSessionAuthenticated()
Auth-->>Req: true
Req->>Axios: request with cookie-jar (+ X-CSRFToken if non-GET)
Axios->>Plane: GET/POST /api/.../pages
Plane-->>Req: response
Req-->>MCP: formatted content block
User->>MCP: plane_logout
MCP->>Auth: resetAuthentication()
Auth-->>MCP: cleared session
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
…cessful Addresses code review feedback - authentication now properly validates that: - Set-Cookie headers are present in login response - session-id cookie is stored in the cookie jar - Only marks isAuthenticated=true after validation succeeds This prevents false positive authentication when login request succeeds but session cookies are not received, which would cause subsequent authenticated requests to fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (8)
src/schemas.ts (1)
253-279: Page schema looks good; consider a couple of consistency tweaksThe Page schema matches the expected Plane fields and fits how you use
Pageinsrc/tools/pages.ts. Two small follow-ups to consider:
- Other entities tend to cap
nameat 255 chars (z.string().max(255)); if the backend has the same limit for pages, mirroring that here would keep things consistent.- Most
archived_atfields elsewhere usedatetime({ offset: true }), while Page uses.date(). If the API returns full timestamps for page archives,.datetime({ offset: true })would be safer.If these differences are intentional per the Pages API, no change needed; otherwise aligning them would avoid subtle validation/type mismatches in the future.
src/tools/auth.ts (1)
5-83: Clarify what session auth actually unlocks in login/status messagesThe tools’ behavior looks good, but the messaging may be a bit misleading:
plane_logindescription and success note say “enable full API access including Pages” / “all endpoints available”. In practice, only Pages (and other/api/paths) use the session cookies;/api/v1/endpoints still depend onPLANE_API_KEYinmakePlaneRequest.plane_auth_status.current_mode+notealso imply that a session alone gives “full access” even if no API key is configured, which may not be true for/api/v1/tools.It would be clearer to phrase this as: session auth enables Pages and other
/api/endpoints, while/api/v1/endpoints still rely on an API key if required by the backend.src/tools/pages.ts (2)
7-191: Pages CRUD and lifecycle tools look consistent with the API layoutThe core tools (
list_page*,get_page,create_page,update_page,delete_page,archive/unarchive) are implemented consistently:
- All paths share the
workspaces/${PLANe_WORKSPACE_SLUG}/projects/${project_id}/...pattern that matches the routing logic inmakePlaneRequest.create_page/update_pageonly include optional fields when provided, which is friendly to partial updates.list_pagesreturns a simplified shape that’s more usable for the model.The only thing I’d consider is failing fast with a clearer error if
PLANe_WORKSPACE_SLUGis unset, instead of sending requests toworkspaces/undefined/...(same applies to other tools using this pattern).
337-507: Description and version tools are straightforward; type inference can stay looseThe description and version-history tools (
get_page_description,update_page_description,get_page_versions,get_page_version) all follow the same pattern as the other tools and delegate shapes to the backend (noPagetyping). That’s acceptable here, since the responses are just serialized back out for the client, and you’re not manipulating their structure locally.If you want stronger type safety later, you could introduce lightweight types for description and version payloads and use them as generics on
makePlaneRequest, but it’s not necessary for correctness.src/common/request-helper.ts (1)
18-27: Regex for detecting “pages endpoints” may be too broad for future APIs
isPagesEndpointuses:const isPagesEndpoint = /\/pages\/|\/pages$|\/pages-summary\/|\/favorite-pages\/|\/description\/|\/versions\//.test(path);Right now it matches the paths used by
src/tools/pages.ts, but the generic/description/and/versions/fragments could misclassify future non-page endpoints (e.g., project or module description/version endpoints) as “pages”, forcing them onto/api/+ session auth instead of/api/v1/+ API key.To make this more future-proof, consider tightening the pattern (e.g., anchoring
description/versionsto/pages/.../description/and/pages/.../versions/) or driving the decision from the caller (e.g., arequiresSessionflag from the tools).src/common/auth.ts (3)
15-18: Consider dependency injection for improved testability.The module-level singleton pattern works for the current MCP server use case where session state should persist across tool invocations. However, it makes unit testing difficult and prevents multiple independent instances.
Consider refactoring to a class-based approach that allows both singleton usage and independent instances:
class AuthManager { private axiosInstance: AxiosInstance | null = null; private isAuthenticated = false; getAxiosInstance(): AxiosInstance { /* ... */ } authenticateWithPassword(...): Promise<boolean> { /* ... */ } // ... other methods } // Export singleton for production use export const authManager = new AuthManager(); // Also export class for testing export { AuthManager };This allows tests to create isolated instances while maintaining the singleton pattern for production.
103-106: Improve error handling to distinguish between failure modes.The catch block returns
falsefor all errors, making it impossible for callers to distinguish between different failure scenarios (network errors, invalid credentials, CSRF issues, etc.).Consider returning a more detailed result:
export interface AuthResult { success: boolean; error?: 'network' | 'csrf' | 'credentials' | 'unknown'; message?: string; } export async function authenticateWithPassword( email: string, password: string, hostUrl: string ): Promise<AuthResult> { try { // ... existing logic ... return { success: true }; } catch (error) { if (axios.isAxiosError(error)) { if (!error.response) { return { success: false, error: 'network', message: 'Network error' }; } if (error.response.status === 401 || error.response.status === 403) { return { success: false, error: 'credentials', message: 'Invalid credentials' }; } } debugLog(`[AUTH] Authentication failed: ${error}`); return { success: false, error: 'unknown', message: String(error) }; } }This allows callers (like
src/tools/auth.ts) to provide more helpful error messages to users.
114-117: Explicitly clear cookies when resetting authentication.The current implementation sets
axiosInstance = nullbut doesn't explicitly clear the cookie jar. While setting the instance to null will eventually garbage-collect the cookies, explicitly clearing them ensures immediate cleanup and prevents potential memory leaks.Apply this diff:
-export function resetAuthentication(): void { +export async function resetAuthentication(): Promise<void> { + if (axiosInstance) { + const jar = (axiosInstance.defaults as any).jar as CookieJar | undefined; + if (jar) { + await jar.removeAllCookies(); + debugLog("[AUTH] Cookie jar cleared"); + } + } axiosInstance = null; isAuthenticated = false; + debugLog("[AUTH] Authentication reset"); }Note: This makes the function
async, so callers insrc/tools/auth.tswill need toawaitthe call.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
package.json(1 hunks)src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)src/schemas.ts(1 hunks)src/tools/auth.ts(1 hunks)src/tools/index.ts(1 hunks)src/tools/pages.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/common/request-helper.ts (1)
src/common/auth.ts (2)
isSessionAuthenticated(109-112)getAxiosInstance(20-29)
src/tools/pages.ts (2)
src/schemas.ts (2)
Page(254-277)Page(279-279)src/common/request-helper.ts (1)
makePlaneRequest(14-103)
src/tools/auth.ts (1)
src/common/auth.ts (3)
authenticateWithPassword(31-107)isSessionAuthenticated(109-112)resetAuthentication(114-117)
src/tools/index.ts (2)
src/tools/auth.ts (1)
registerAuthTools(5-104)src/tools/pages.ts (1)
registerPageTools(7-508)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
package.json (1)
33-40: New cookie-jar dependencies align with session auth usageThe added dependencies are consistent with the new cookie-jar–based Axios auth flow and look appropriate for the session-based implementation.
src/tools/index.ts (1)
3-29: Auth and pages tools are wired in cleanlyThe new
registerAuthToolsandregisterPageToolsimports and registrations integrate neatly with the existing tool setup and the ordering (auth first, then project/pages) makes sense.src/tools/auth.ts (1)
85-103: Logout tool is straightforward and correct
plane_logoutcorrectly delegates toresetAuthentication()and returns a simple JSON confirmation; no issues here.
src/common/request-helper.ts
Outdated
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { getAxiosInstance, isSessionAuthenticated } from "./auth.js"; | ||
|
|
||
| const logFile = path.join("/tmp", "plane-mcp-debug.log"); | ||
| function debugLog(message: string) { | ||
| const timestamp = new Date().toISOString(); | ||
| const logMessage = `[${timestamp}] ${message}\n`; | ||
| fs.appendFileSync(logFile, logMessage); | ||
| console.error(message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unconditional debug logging risks PII leakage and adds sync I/O overhead
debugLog currently:
- Writes every message (including full error responses) to
/tmp/plane-mcp-debug.logusingfs.appendFileSync, and - Logs to
stderron every request and error.
This has a few implications:
- Response bodies and error payloads may contain sensitive project/workspace data, which will be persisted to disk by default.
- Synchronous file writes on every request can block the event loop and impact latency under load.
- Hard-coding
/tmpmay not be portable or writable in all environments.
I’d recommend gating this behind an env flag and making it more robust, e.g.:
- Use
os.tmpdir()for the directory. - Wrap the file write in a try/catch to avoid failing a request due to logging issues.
- Consider either async logging or reducing verbosity by default (e.g., only log status codes and minimal metadata unless debug is explicitly enabled).
Also applies to: 28-29, 93-99
🤖 Prompt for AI Agents
In src/common/request-helper.ts around lines 2-12 (and also apply same changes
to occurrences at 28-29 and 93-99): the current debugLog unconditionally writes
full messages synchronously to a hard-coded /tmp file and stderr, risking PII
leakage, blocking the event loop, and failing in non-writable envs; change it to
be gated by a DEBUG or PLANE_MCP_DEBUG env flag, use os.tmpdir() to build the
log path, perform file writes asynchronously (or use non-blocking append with
fs.promises) and wrap writes/console calls in try/catch so logging failures
don’t affect requests, and reduce default verbosity by avoiding full response
bodies (log status, URL, and minimal metadata unless debug flag is set).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/common/auth.ts (4)
7-13: [Duplicate] Debug logging needs cross-platform and reliability fixes.This issue has already been flagged by a previous reviewer: the hardcoded
/tmppath breaks on Windows,fs.appendFileSyncblocks the event loop, and missing error handling will crash the application on write failures.
48-48: [Duplicate] Unsafe cookie jar access needs runtime validation.This unsafe type cast has already been flagged by a previous reviewer with a suggestion to add runtime validation before accessing the jar property.
50-50: [Duplicate] Cookie value logging poses security risk.This has already been flagged by a previous reviewer: logging even truncated cookie values could expose sensitive session tokens if debug logs are compromised. The suggestion is to log only cookie names (keys).
Also applies to: 92-92
108-110: [Duplicate] Authentication verification and session expiry already flagged.This critical issue has been comprehensively addressed by a previous reviewer, including a solution to verify the session with a test API request (using the axios instance directly to avoid circular dependencies) and implement session expiry tracking.
🧹 Nitpick comments (1)
src/common/auth.ts (1)
20-29: Configure request timeouts for production reliability.The axios instance has no timeout configured, meaning requests can hang indefinitely on network issues or unresponsive servers. This affects the reliability of authentication operations.
Apply this diff to add sensible timeout defaults:
export function getAxiosInstance(): AxiosInstance { if (!axiosInstance) { debugLog("[AUTH] Creating new axios instance with cookie jar"); const jar = new CookieJar(); - axiosInstance = wrapper(axios.create({ jar, withCredentials: true })); + axiosInstance = wrapper(axios.create({ + jar, + withCredentials: true, + timeout: 30000, // 30 second timeout + })); } else { debugLog("[AUTH] Reusing existing axios instance"); } return axiosInstance; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/common/auth.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
| maxRedirects: 0, // Don't follow redirects, we just need the cookies | ||
| validateStatus: (status) => status >= 200 && status < 400, // Accept redirects as success | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accepting redirects as success may cause false positives.
The validateStatus accepts 3xx redirects as successful responses, but a redirect might indicate failed authentication rather than success. While the code does validate Set-Cookie headers and session cookies afterward, relying on redirect status codes is fragile.
This is related to the authentication verification issue already flagged at lines 108-110—making a test API request after login would definitively verify the session works regardless of the HTTP status code.
🤖 Prompt for AI Agents
In src/common/auth.ts around lines 73 to 75, the validateStatus currently treats
3xx redirects as success which can produce false positives for authentication;
change validateStatus to accept only 2xx (status >= 200 && status < 300) while
still keeping maxRedirects: 0 so cookies are captured, and after the login
request make an explicit follow-up test API request (using the same cookie
jar/session) to a protected endpoint and assert a 2xx response and expected
session behavior — fail the login flow if that verification request does not
return a successful authenticated response.
This commit comprehensively addresses all feedback from CodeRabbit's code review:
1. **Regex Pattern Specificity** (request-helper.ts:21)
- Changed generic `/description/` and `/versions/` patterns to pages-specific
- Now uses `/pages\/[^/]+\/description\/` and `/pages\/[^/]+\/versions\/`
- Prevents false positives with future non-page endpoints
2. **Schema Consistency** (schemas.ts:256,264)
- Page.name now uses `.max(255)` like other entity schemas
- Page.archived_at changed from `.date()` to `.datetime({ offset: true })`
- Aligns with Issue, Cycle, Module, and other entity schemas
3. **Error Handling** (auth.ts:15-25, 40-158)
- Created `AuthResult` interface with discriminated error types
- Changed return type from `Promise<boolean>` to `Promise<AuthResult>`
- Error categories: 'network', 'csrf', 'credentials', 'cookies', 'unknown'
- Each error path returns specific type and user-friendly message
- Enables better error reporting in plane_login tool
4. **Cookie Cleanup** (auth.ts:169-181)
- Made `resetAuthentication()` async for proper cleanup
- Explicitly calls `jar.removeAllCookies()` before nulling instance
- Added debug logging for cleanup verification
- Updated plane_logout to await the async function
5. **JSDoc Documentation** (all modified files)
- Added comprehensive JSDoc to all exported functions
- Documented auth.ts: getAxiosInstance, authenticateWithPassword,
isSessionAuthenticated, resetAuthentication
- Documented request-helper.ts: makePlaneRequest with routing logic
- Documented registration functions: registerAuthTools, registerPageTools
- Includes @param, @returns, @throws annotations
- Achieves 80%+ docstring coverage requirement
6. **Auth Messaging Clarity** (auth.ts:6-86)
- plane_login description: "session-based access to Pages and /api/ endpoints"
- Success note: "Other endpoints (/api/v1/) use API key if configured"
- plane_auth_status: Updated current_mode to show endpoint scope
- Note clarifies: "Pages + /api/" for session, "/api/v1/" for API key
- Removes misleading "full access" language
7. **Environment Validation** (pages.ts:7-18, all tool implementations)
- Added validateWorkspaceSlug() helper function
- Validates PLANE_WORKSPACE_SLUG is set before use
- Throws clear error message with configuration guidance
- Called in all 18 page tools before making requests
- Prevents cryptic "workspaces/undefined/..." errors
All changes maintain backward compatibility and follow TypeScript + Zod best practices.
Build verified successful with no type errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/common/auth.ts (3)
4-13: Prevent debugLog from crashing on non‑Unix hostsHard-coding
/tmpand doing an unguarded synchronous append will throw on Windows (no/tmp) and on any host without write permission, taking down the auth flow. Wrap the write, useos.tmpdir(), and keep logging benign if the file write fails.-import fs from "fs"; -import path from "path"; +import fs from "fs"; +import os from "os"; +import path from "path"; -const logFile = path.join("/tmp", "plane-mcp-debug.log"); -function debugLog(message: string) { +const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log"); +function debugLog(message: string) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; - fs.appendFileSync(logFile, logMessage); - console.error(message); + try { + fs.appendFileSync(logFile, logMessage); + } catch (error) { + console.error(`[AUTH] debugLog write failed: ${error}`); + } + console.error(message); }
80-83: Guard access to the axios cookie jar
(instance.defaults as any).jarcan beundefined(library update, misconfiguration) and will crash when you callgetCookies. Fail fast with an explicit guard so we return a structured error instead of throwing.- const jar = (instance.defaults as any).jar as CookieJar; - const cookies = await jar.getCookies(host); + const maybeJar = (instance.defaults as Record<string, unknown>).jar; + if (!(maybeJar instanceof CookieJar)) { + debugLog("[AUTH] ERROR: Cookie jar not found on axios instance"); + return { success: false, error: "cookies", message: "Cookie jar not available for session authentication" }; + } + const jar = maybeJar; + const cookies = await jar.getCookies(host);
82-83: Remove cookie value loggingEven truncated values expose session/CSRF tokens once logs leak. Log only cookie names (or counts) to avoid credential disclosure.
- debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => `${c.key}=${c.value.substring(0, 10)}...`).join(", ")}`); + debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => c.key).join(", ")}`); ... - debugLog(`[AUTH] Cookies after login: ${loginCookies.map(c => `${c.key}=${c.value.substring(0, 10)}...`).join(", ")}`); + debugLog(`[AUTH] Cookies after login: ${loginCookies.map(c => c.key).join(", ")}`);Also applies to: 125-126
src/common/request-helper.ts (1)
2-12: Harden request debugLog pathSame problem as in auth.ts:
/tmpplus unguardedappendFileSyncwill explode on Windows or restricted hosts, killing every request. Switch toos.tmpdir()and catch write failures.-import fs from "fs"; -import path from "path"; +import fs from "fs"; +import os from "os"; +import path from "path"; -const logFile = path.join("/tmp", "plane-mcp-debug.log"); -function debugLog(message: string) { +const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log"); +function debugLog(message: string) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; - fs.appendFileSync(logFile, logMessage); - console.error(message); + try { + fs.appendFileSync(logFile, logMessage); + } catch (error) { + console.error(`[REQUEST] debugLog write failed: ${error}`); + } + console.error(message); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)src/schemas.ts(1 hunks)src/tools/auth.ts(1 hunks)src/tools/pages.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/tools/auth.ts
- src/schemas.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/common/request-helper.ts (1)
src/common/auth.ts (2)
isSessionAuthenticated(164-167)getAxiosInstance(36-45)
src/tools/pages.ts (2)
src/schemas.ts (2)
Page(254-277)Page(279-279)src/common/request-helper.ts (1)
makePlaneRequest(31-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
This commit fixes critical security and robustness issues identified in code review: 1. **Cross-platform debugLog (auth.ts, request-helper.ts)** - Changed from hardcoded `/tmp` to `os.tmpdir()` for Windows compatibility - Added try-catch around `fs.appendFileSync` to prevent crashes on write failures - Continues execution gracefully if log file is not writable 2. **Cookie jar validation (auth.ts:85-90)** - Added runtime guard to verify cookie jar exists before access - Checks `instanceof CookieJar` instead of unsafe type cast - Returns structured error if jar unavailable instead of throwing - Prevents crashes if axios-cookiejar-support misconfigured 3. **Security: Remove cookie value logging (auth.ts:92,135)** - Changed from logging truncated cookie values to names only - Prevents session/CSRF token exposure in debug logs - Logs: `csrftoken, session-id` instead of `csrftoken=abc123..., session-id=def456...` All changes maintain existing functionality while improving reliability and security. No breaking changes to API or behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (6)
src/common/request-helper.ts (3)
8-17: This is a duplicate of an existing concern already flagged in past reviews regarding unconditional debug logging that risks PII leakage and uses synchronous I/O. While the try/catch adds some robustness, the core issues remain: no environment flag gating, synchronous file writes on every request, and potential logging of sensitive data (full error responses, cookies, etc.).Based on past review comments.
69-70: This duplicates a concern already flagged in past reviews about logging cookie values. Line 69 logs the first 10 characters of cookie values which could leak session tokens or CSRF tokens. Only cookie names (keys) should be logged.Based on past review comments.
78-81: Remove Content-Type header from GET requests.The session authentication path sets
Content-Type: application/jsonunconditionally (line 79), including for GET requests. This is inconsistent with the API key path (lines 98-100) which correctly only sets Content-Type for non-GET requests. Some HTTP servers may reject GET requests with a Content-Type header since GET requests shouldn't have a body.Apply this diff to fix:
const config: AxiosRequestConfig = { url, method, - headers: { - "Content-Type": "application/json", - }, + headers: {}, }; + // Only add Content-Type for non-GET requests + if (method.toUpperCase() !== "GET") { + config.headers["Content-Type"] = "application/json"; + } + // Include body for non-GET requestssrc/common/auth.ts (3)
8-18: This duplicates an existing concern about the debugLog implementation using synchronous file I/O without environment flag gating and potential PII exposure. The try/catch improves robustness but doesn't address the core security and performance concerns.Based on past review comments.
150-152: Verify authentication with a test API call before marking as authenticated.Setting
isAuthenticated = truebased solely on the presence of a session-id cookie doesn't verify the session actually works. The server could return cookies but reject the credentials, or the cookies might not be properly formatted for subsequent requests.After line 149, add a verification request using the existing axios instance (avoid importing
makePlaneRequestto prevent circular dependency):loginCookies.forEach(c => { debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`); }); + // Verify the session works with a test API call + try { + const verifyResponse = await instance.get(`${host}api/v1/users/me/`); + if (verifyResponse.status !== 200) { + debugLog(`[AUTH] Session verification failed with status: ${verifyResponse.status}`); + return { success: false, error: 'credentials', message: 'Session verification failed' }; + } + debugLog("[AUTH] Session verified successfully"); + } catch (verifyError) { + debugLog(`[AUTH] Session verification request failed: ${verifyError}`); + return { success: false, error: 'credentials', message: 'Could not verify session validity' }; + } + isAuthenticated = true;
191-202: Fix error handling and cleanup order in resetAuthentication.If
jar.removeAllCookies()throws at line 195, the function exits before resettingaxiosInstanceandisAuthenticated(lines 199-201), leaving authentication state inconsistent. Additionally, line 193 uses an unsafe type cast.Apply this diff to ensure cleanup always completes:
export async function resetAuthentication(): Promise<void> { + try { if (axiosInstance) { - const jar = (axiosInstance.defaults as any).jar as CookieJar | undefined; - if (jar) { + const maybeJar = (axiosInstance.defaults as Record<string, unknown>).jar; + if (maybeJar instanceof CookieJar) { + const jar = maybeJar; await jar.removeAllCookies(); debugLog("[AUTH] Cookie jar cleared"); } } + } catch (error) { + debugLog(`[AUTH] Error clearing cookies: ${error}`); + // Continue with cleanup even if cookie removal fails + } finally { axiosInstance = null; isAuthenticated = false; debugLog("[AUTH] Authentication reset"); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/request-helper.ts (1)
src/common/auth.ts (2)
isSessionAuthenticated(174-177)getAxiosInstance(41-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
src/common/auth.ts (2)
85-90: LGTM - Cookie jar validation implemented.Good defensive programming! The runtime validation checks that the jar exists and is a CookieJar instance before use, preventing TypeScript's type safety from being bypassed.
92-92: LGTM - Cookie logging fixed.Correctly logs only cookie names without values, addressing the security concern from past reviews.
src/common/request-helper.ts (1)
43-43: All review concerns are unfounded—the regex is correctly designed and safe.The verification confirms:
All pages endpoints are properly matched: Every endpoint in the codebase (
/pages/,/pages-summary/,/favorite-pages/,/pages/{id}/description/,/pages/{id}/versions/, etc.) is correctly matched by the regex.Concern about
/pages$: While this pattern is technically redundant (no actual endpoint uses paths without trailing slashes), it causes no harm and doesn't introduce bugs.Concern about false positives: Invalid. The regex uses leading slash boundaries (
/\/pages-summary\/,/\/favorite-pages\/) that prevent matching unintended paths like "old-pages-summary/" or "my-favorite-pages/".Boundary checks already present: The regex includes leading-slash boundaries in all patterns, properly constraining matches to intended pages endpoints.
Likely an incorrect or invalid review comment.
…d request handling This commit addresses all remaining CodeRabbit feedback: 1. **Session Verification (auth.ts:150-161)** - Added test API call to /api/v1/users/me/ after login - Verifies session actually works before marking authenticated - Prevents false positives from cookies that don't grant access - Returns 'credentials' error if verification fails 2. **Content-Type Header Fix (request-helper.ts:75-86)** - Session auth path now only sets Content-Type for non-GET requests - Matches API key path behavior (lines 97-100) - Prevents potential HTTP server rejections of GET with Content-Type - Properly initializes headers as Record<string, string> 3. **Cookie Logging Security (request-helper.ts:69)** - Changed from logging cookie values to names only - Prevents exposure of session tokens in debug logs - Consistent with auth.ts cookie logging fix 4. **resetAuthentication Robustness (auth.ts:191-209)** - Wrapped cookie cleanup in try-catch-finally - Ensures axiosInstance and isAuthenticated always reset - Prevents inconsistent state if jar.removeAllCookies() throws - Uses safe jar access with instanceof check All changes maintain backward compatibility and improve production reliability. Build verified successful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/common/auth.ts (1)
120-131: CRITICAL: Stop logging session cookies in plaintext.Lines 122 and 127 still use
JSON.stringify()on response headers and Set-Cookie values, which exposes complete session cookie values in plaintext logs. This is sufficient for session hijacking if logs are compromised.This issue was flagged in previous reviews and marked as addressed, but the code still contains the vulnerability. Apply this diff to redact sensitive values:
// Log response details debugLog(`[AUTH] Login response status: ${loginResponse.status}`); - debugLog(`[AUTH] Login response headers: ${JSON.stringify(loginResponse.headers)}`); + const headerNames = Object.keys(loginResponse.headers ?? {}); + debugLog(`[AUTH] Login response headers present: ${headerNames.join(", ")}`); // Check if Set-Cookie headers are present const setCookieHeader = loginResponse.headers['set-cookie']; if (setCookieHeader) { - debugLog(`[AUTH] Set-Cookie headers received: ${JSON.stringify(setCookieHeader)}`); + debugLog(`[AUTH] Set-Cookie headers received: ${Array.isArray(setCookieHeader) ? setCookieHeader.length : 1} cookie(s)`); } else { debugLog(`[AUTH] ERROR: No Set-Cookie headers in login response!`);
🧹 Nitpick comments (5)
src/common/auth.ts (4)
8-18: Consider async logging and environment flag to reduce production overhead.The synchronous
fs.appendFileSyncblocks the event loop on every request, which can impact latency under load. Additionally, unconditional logging of all requests and responses may expose sensitive data and degrade performance in production.Consider these improvements:
+const DEBUG_ENABLED = process.env.PLANE_MCP_DEBUG === 'true'; + const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log"); -function debugLog(message: string) { +async function debugLog(message: string) { + if (!DEBUG_ENABLED) return; + const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; try { - fs.appendFileSync(logFile, logMessage); + await fs.promises.appendFile(logFile, logMessage); } catch (error) { console.error(`[AUTH] debugLog write failed: ${error}`); } - console.error(message); + if (DEBUG_ENABLED) { + console.error(message); + } }Note: Since this changes
debugLogto async, all callsites would need to be updated toawait debugLog(...)or the function should not be awaited (fire-and-forget), which is acceptable for debug logs.
115-118: Consider restricting validateStatus to 2xx responses only.While the session verification step (lines 151-161) ensures the session is valid, accepting 3xx redirects as successful login responses is fragile and could be confusing. Different server configurations might redirect for various reasons unrelated to successful authentication.
Apply this diff to be more explicit:
maxRedirects: 0, // Don't follow redirects, we just need the cookies - validateStatus: (status) => status >= 200 && status < 400, // Accept redirects as success + validateStatus: (status) => status >= 200 && status < 300, // Accept only 2xx as success }The session verification request that follows will catch any authentication failures, making this change safe.
145-148: Consider reducing cookie metadata logging verbosity.While not logging cookie values (which would be a security issue), logging detailed cookie attributes (domain, path, httpOnly, secure) for every cookie is quite verbose and may not be necessary in production. This information could be useful during development but should ideally be gated behind a debug flag.
Consider removing this detailed logging or gating it behind the same debug flag suggested earlier:
// Log full cookie details for debugging - loginCookies.forEach(c => { - debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`); - }); + if (process.env.PLANE_MCP_DEBUG === 'verbose') { + loginCookies.forEach(c => { + debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`); + }); + }
187-190: Consider implementing session expiry detection.The current implementation only checks a boolean flag without considering session age. Sessions can expire on the server side, and this could lead to using stale credentials that will fail on actual requests.
Consider tracking authentication time and implementing expiry detection:
let authenticationTime: number | null = null; const SESSION_TIMEOUT_MS = 3600000; // 1 hour, adjust based on server session timeout // In authenticateWithPassword, after successful verification: authenticationTime = Date.now(); isAuthenticated = true; // Update isSessionAuthenticated: export function isSessionAuthenticated(): boolean { if (!isAuthenticated || !authenticationTime) { debugLog(`[AUTH] isSessionAuthenticated() - not authenticated`); return false; } const isStale = Date.now() - authenticationTime > SESSION_TIMEOUT_MS; if (isStale) { debugLog(`[AUTH] Session expired, resetting authentication`); isAuthenticated = false; authenticationTime = null; return false; } debugLog(`[AUTH] isSessionAuthenticated() - authenticated and valid`); return true; }Also update
resetAuthenticationto clearauthenticationTime.src/common/request-helper.ts (1)
40-46: Consider a more maintainable approach to endpoint routing.The regex-based detection of pages endpoints works but is brittle and could miss edge cases or require updates as the API evolves. A false positive (non-pages endpoint matched as pages) would route to session auth when API key is needed, and vice versa.
Consider these alternatives:
- Explicit endpoint registry:
const PAGES_ENDPOINTS = [ '/pages/', '/pages$', '/pages-summary/', '/favorite-pages/', '/description/', '/versions/' ]; const isPagesEndpoint = PAGES_ENDPOINTS.some(pattern => path.includes(pattern));
- Caller-specified authentication:
export async function makePlaneRequest<T>( method: string, path: string, body: any = null, options?: { authMode?: 'session' | 'api_key' } ): Promise<T>This would make the routing logic more explicit and easier to maintain as the API evolves.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/request-helper.ts (1)
src/common/auth.ts (2)
isSessionAuthenticated(187-190)getAxiosInstance(41-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
This commit addresses a CRITICAL security vulnerability identified in code review:
**Security Issue (auth.ts:120-131):**
- Lines 122 and 127 were logging full HTTP headers and Set-Cookie values
- This exposed complete session cookie values in plaintext debug logs
- Session cookies include authentication tokens sufficient for session hijacking
- If debug logs are compromised, attackers could steal valid sessions
**Fix Applied:**
- Line 122: Changed from logging full headers JSON to just header names
- Before: `JSON.stringify(loginResponse.headers)`
- After: `Object.keys(loginResponse.headers).join(", ")`
- Line 127: Changed from logging full Set-Cookie values to cookie count
- Before: `JSON.stringify(setCookieHeader)`
- After: `${Array.isArray(setCookieHeader) ? setCookieHeader.length : 1} cookie(s)`
**Security Impact:**
- Debug logs now show only metadata (header names, cookie counts)
- No sensitive cookie values or tokens are logged
- Session hijacking via log compromise is no longer possible
- Maintains debugging utility without security risk
This fix completes the security hardening of the authentication flow.
Build verified successful.
| "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", | ||
| "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", | ||
| "license": "MIT", | ||
| "peer": true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Incorrect Peer Dependency Resolution Breaks Installs
The axios package is marked with "peer": true in package-lock.json despite being listed as a direct dependency in package.json. This causes axios not to be installed when running npm install or npm ci, leading to runtime errors when the authentication and request modules try to import it. The same issue affects tough-cookie at line 2952. This occurs because axios-cookiejar-support declares both as peer dependencies, and npm incorrectly resolved them as peer-only rather than installing them as actual dependencies. The package-lock file needs regeneration after adding both packages to ensure they're installed as direct dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "peer": true flag in package-lock.json is not a bug - it's correct npm v7+ behavior.
Why Cursor is Incorrect
The "peer": true flag is metadata that indicates a package serves dual purposes:
- Direct dependency (listed in
package.json) - Satisfies peer dependency requirements from other packages
In our case, axios is both:
- A direct dependency of our project
- A peer dependency required by
axios-cookiejar-support
npm deduplicates these into a single installation and marks it with "peer": true in the lockfile.
Evidence the Package IS Installed
✅ Listed in package.json dependencies:
"axios": "1.12.0",
"tough-cookie": "^5.1.2"✅ Present in node_modules:
$ ls node_modules/ | grep -E "axios|tough-cookie"
axios
axios-cookiejar-support
tough-cookie✅ Build succeeds:
$ npm run build
> rm -rf build && tsc && chmod 755 build/index.js
# ✓ No errors✅ Imports resolve correctly in compiled output:
import axios from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";Conclusion
The "peer": true flag does NOT prevent installation. It's correct npm lockfile v3 behavior for deduplicated dependencies. No changes needed.
References:
- npm RFC: Package Manifest and Lockfile Metadata
- npm v7+ automatically installs peer dependencies and marks them in the lockfile
|
I tried this and received an error: |
- Accept multiple session cookie names: 'session-id', 'sessionid', 'plane_session' - Accept multiple CSRF token names: 'csrftoken', 'csrf', 'XSRF-TOKEN' - Make Set-Cookie header check non-blocking (log warning instead of error) - Fixes 'No Session cookies received' error when Set-Cookie header is missing from response object but cookies are captured
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (4)
src/common/auth.ts (4)
8-18: Duplicate: debugLog should be extracted to shared module.This is the same
debugLogimplementation that appears inrequest-helper.ts. See the review comment onrequest-helper.tslines 7-17 for the detailed refactoring suggestion.
127-133: Critical: Full response headers expose session cookies.Line 133 logs the complete login response headers using
JSON.stringify(loginResponse.headers), which includes theSet-Cookieheader with full session cookie values. This is sufficient for session hijacking if logs are compromised.The redacted logging at lines 129-130 is correct, but line 133 undermines it completely. Remove line 133:
const headerNames = Object.keys(loginResponse.headers ?? {}); debugLog(`[AUTH] Login response headers present: ${headerNames.join(", ")}`); - - // Log ALL headers for debugging - debugLog(`[AUTH] Login response headers FULL: ${JSON.stringify(loginResponse.headers)}`);If detailed debugging is absolutely required, gate it behind a separate verbose flag and add a security warning in documentation.
Based on learnings, previous reviews flagged similar issues that were marked as addressed, but this line was added afterward.
191-205: Reset authentication state on login failure.When
authenticateWithPasswordfails (lines 191-205), theisAuthenticatedflag is not reset tofalse. If a user successfully authenticates once then re-authenticates with invalid credentials, the failure leavesisAuthenticated = truewhile session cookies may be invalid or cleared, causing subsequent requests to use stale authentication state.Add at the start of the catch block:
} catch (error) { + isAuthenticated = false; debugLog(`[AUTH] Authentication failed: ${error}`);This ensures authentication state is always consistent with the actual session validity.
Based on learnings, previous review flagged this exact issue.
109-112: Critical: Remove plaintext password and CSRF token from debug logs.Lines 110-112 log sensitive authentication credentials:
- Line 111 logs the password in plaintext - this is a critical security vulnerability
- Line 112 logs a substring of the CSRF token
If these logs are persisted, backed up, or accessed by unauthorized parties, user credentials are completely exposed.
Apply this diff:
debugLog(`[AUTH] Sending login request to: ${host}auth/sign-in/`); debugLog(`[AUTH] Login email: ${email}`); - debugLog(`[AUTH] Login password: ${password}`); - debugLog(`[AUTH] CSRF token: ${csrfCookie.value.substring(0, 10)}...`); + debugLog(`[AUTH] Attempting login with provided credentials`); + debugLog(`[AUTH] CSRF token cookie: ${csrfCookie.key}`);
🧹 Nitpick comments (2)
src/common/auth.ts (2)
80-101: Reduce header verbosity in CSRF token retrieval logs.Line 83 logs the complete CSRF response headers using
JSON.stringify. While this may not contain session cookies yet, it's unnecessarily verbose and could leak server configuration details.Consider logging only essential metadata:
- debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`); + debugLog(`[AUTH] CSRF response status: ${csrfResponse.status}`);The
instanceof CookieJarcheck at line 88 correctly addresses the unsafe cast concern.
162-190: Session verification addresses previous concern, but missing expiry tracking.The session verification flow (lines 162-186) correctly validates that the session works by calling
/api/users/me/before settingisAuthenticated = true(line 188). This addresses the previous critical review concern about false positive authentication.However, the implementation still lacks session expiry detection. Add timestamp tracking:
let authenticationTime: number | null = null; const SESSION_TIMEOUT_MS = 3600000; // 1 hour // In authenticateWithPassword after line 188: authenticationTime = Date.now(); // Update isSessionAuthenticated: export function isSessionAuthenticated(): boolean { if (!isAuthenticated || !authenticationTime) { return false; } const isStale = Date.now() - authenticationTime > SESSION_TIMEOUT_MS; if (isStale) { debugLog("[AUTH] Session expired, resetting authentication"); isAuthenticated = false; authenticationTime = null; } return isAuthenticated; }Additionally, line 181 logs the full error response data which may contain PII:
- debugLog(`[AUTH] Verification error response data: ${JSON.stringify(verifyError.response?.data)}`); + debugLog(`[AUTH] Verification error status: ${verifyError.response?.status}`);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/request-helper.ts (1)
src/common/auth.ts (2)
isSessionAuthenticated(212-215)getAxiosInstance(41-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (4)
src/common/request-helper.ts (2)
40-52: LGTM on routing logic.The conditional API versioning logic correctly routes pages endpoints to
/api/(session auth) and other endpoints to/api/v1/(API key auth). The regex pattern is sufficiently specific to avoid false positives.
106-131: LGTM on API key authentication path.The API key authentication logic correctly:
- Only sets
Content-Typefor non-GET requests- Only includes request body for non-GET methods
- Conditionally applies the API key header
src/common/auth.ts (2)
41-50: LGTM on axios instance creation.The lazy initialization of the axios instance with cookie jar support is correctly implemented. The jar is created here and attached to the instance, ensuring it exists for subsequent access.
229-247: LGTM on resetAuthentication error handling.The
try-finallypattern correctly ensures thataxiosInstanceandisAuthenticatedare always reset (lines 243-245) even if cookie removal fails. Theinstanceofcheck at line 233 safely validates the jar before use.
src/common/request-helper.ts
Outdated
| const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log"); | ||
| function debugLog(message: string) { | ||
| const timestamp = new Date().toISOString(); | ||
| const logMessage = `[${timestamp}] ${message}\n`; | ||
| try { | ||
| fs.appendFileSync(logFile, logMessage); | ||
| } catch (error) { | ||
| console.error(`[REQUEST] debugLog write failed: ${error}`); | ||
| } | ||
| console.error(message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Extract debugLog to a shared module and gate behind environment flag.
This debugLog function is duplicated between request-helper.ts and auth.ts (lines 8-18), violating DRY principles. Additionally:
- Synchronous
fs.appendFileSyncblocks the event loop on every call, impacting latency under load - No environment flag to disable debug logging in production
- Logs unconditionally write to disk and stderr
Create src/common/debug.ts:
import fs from "fs";
import os from "os";
import path from "path";
const DEBUG_ENABLED = process.env.PLANE_MCP_DEBUG === 'true';
const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log");
export async function debugLog(message: string): Promise<void> {
if (!DEBUG_ENABLED) return;
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
try {
await fs.promises.appendFile(logFile, logMessage);
} catch (error) {
console.error(`debugLog write failed: ${error}`);
}
console.error(message);
}Then import in both files:
-import fs from "fs";
-import os from "os";
-import path from "path";
+import { debugLog } from "./debug.js";And remove the local debugLog implementations. Update all call sites to handle the Promise (await or .catch()).
Based on learnings, this addresses the code duplication and synchronous I/O concerns raised in previous reviews.
🤖 Prompt for AI Agents
In src/common/request-helper.ts lines 7-17 and corresponding auth.ts duplicate,
extract the local debugLog into a new shared module src/common/debug.ts that
exports an async debugLog(message: string): Promise<void>; implement it to gate
logging with an environment flag (const DEBUG_ENABLED =
process.env.PLANE_MCP_DEBUG === 'true'), use path.join(os.tmpdir(),
"plane-mcp-debug.log") and fs.promises.appendFile to avoid synchronous I/O,
catch and console.error write failures, and still write message to stderr when
enabled; then remove the local debugLog implementations in request-helper.ts and
auth.ts and replace them with imports from src/common/debug.ts, and update all
call sites to await the returned Promise or append .catch() to handle
rejections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
- Extracted debug logging to a shared module with async file I/O and environment gating. - Removed sensitive data logging (cookie values, CSRF tokens, full error responses) from and . - Gated full error response logging behind to prevent accidental PII leaks. - Standardized debug log output format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/common/auth.ts(1 hunks)src/common/debug.ts(1 hunks)src/common/request-helper.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/request-helper.ts (2)
src/common/debug.ts (1)
debugLog(8-20)src/common/auth.ts (2)
isSessionAuthenticated(200-203)getAxiosInstance(27-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
src/common/debug.ts (1)
5-20: Solid: env-gated, async, portable debug logging
This fixes the earlier “always-on + /tmp + sync I/O” issues nicely. Only minor nits: stderr output currently drops the timestamp (file has it), and the temp log can grow unbounded over time.src/common/auth.ts (1)
72-88: Consider using the CSRF endpoint path when retrieving the cookieThe
tough-cookielibrary filters cookies by path according to RFC 6265. If the Plane server sets the CSRF cookie with a path scope of/auth/, callingjar.getCookies(host)at the root level may not return it. Query cookies using the CSRF endpoint path instead:- const cookies = await jar.getCookies(host); + const cookies = await jar.getCookies(`${host}auth/get-csrf-token/`); await debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => c.key).join(", ")}`);Alternatively,
jar.getCookies(${host}auth/sign-in/)would also match if the cookie's path scope is/auth/.src/common/request-helper.ts (1)
63-78: CSRF cookie lookup should use full URL for path-scoped cookie filtering
jar.getCookies(host)using only the hostname loses the request path, which may exclude cookies with specificPathattributes. CSRF cookies are often scoped to specific paths. Consider passing the full URL or a URL object tojar.getCookies()for proper cookie filtering instead of just the hostname.
| export async function authenticateWithPassword( | ||
| email: string, | ||
| password: string, | ||
| hostUrl: string | ||
| ): Promise<AuthResult> { | ||
| try { | ||
| const instance = getAxiosInstance(); | ||
| const host = hostUrl.endsWith("/") ? hostUrl : `${hostUrl}/`; | ||
|
|
||
| await debugLog("[AUTH] Starting authentication flow..."); | ||
| await debugLog(`[AUTH] Host URL: ${host}`); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid stale isAuthenticated=true after a failed re-login
If a user was previously authenticated and a later authenticateWithPassword() fails, isAuthenticated is never reset in the failure paths. Set it false at the start (and ideally clear cookies) before attempting login.
export async function authenticateWithPassword(
email: string,
password: string,
hostUrl: string
): Promise<AuthResult> {
+ // Prevent stale auth state from prior successful logins
+ isAuthenticated = false;
try {
const instance = getAxiosInstance();Also applies to: 176-193
🤖 Prompt for AI Agents
In src/common/auth.ts around lines 54 to 65, the function
authenticateWithPassword can leave isAuthenticated=true after a failed re-login;
set isAuthenticated = false at the start of the method (before any network/login
attempt) and clear any stored auth cookies/tokens/session state so we don't
retain stale auth on failure; also update the failure/catch branches to ensure
isAuthenticated stays false and any partial credentials are removed; apply the
same changes to the other similar login routine at lines 176-193.
| // Step 1: Get CSRF token (stored in cookie jar automatically) | ||
| const csrfResponse = await instance.get(`${host}auth/get-csrf-token/`); | ||
| await debugLog(`[AUTH] CSRF response status: ${csrfResponse.status}`); | ||
| await debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`); | ||
| await debugLog("[AUTH] CSRF token requested"); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CRITICAL: remove header dumps (they can include Set-Cookie / session material)
JSON.stringify(csrfResponse.headers) and especially JSON.stringify(loginResponse.headers) can write session/CSRF cookies to the temp log + stderr (session hijack risk). Keep only safe metadata (status + header names + cookie count).
const csrfResponse = await instance.get(`${host}auth/get-csrf-token/`);
await debugLog(`[AUTH] CSRF response status: ${csrfResponse.status}`);
- await debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`);
+ await debugLog(`[AUTH] CSRF response headers present: ${Object.keys(csrfResponse.headers ?? {}).join(", ")}`);
await debugLog("[AUTH] CSRF token requested");
...
const headerNames = Object.keys(loginResponse.headers ?? {});
await debugLog(`[AUTH] Login response headers present: ${headerNames.join(", ")}`);
-
- // Log ALL headers for debugging
- await debugLog(`[AUTH] Login response headers FULL: ${JSON.stringify(loginResponse.headers)}`);Also applies to: 113-125
🤖 Prompt for AI Agents
In src/common/auth.ts around lines 66 to 71 (and similarly for lines 113 to
125), remove logging that dumps full response headers (e.g.,
JSON.stringify(csrfResponse.headers) and JSON.stringify(loginResponse.headers))
because they may contain Set-Cookie/session tokens; instead log only safe
metadata: the response status, the list of header names (Object.keys) and the
number of cookies present (if any) or a boolean cookiePresent flag. Update
debugLog calls to output only those sanitized values and ensure no raw header
objects or cookie values are ever written to logs or stderr.
| // Verify the session works with a test API call | ||
| // Note: Use /api/ endpoint (not /api/v1/) since session cookies work with /api/ endpoints | ||
| try { | ||
| const verifyUrl = `${host}api/users/me/`; | ||
| await debugLog(`[AUTH] Attempting to verify session with: ${verifyUrl}`); | ||
| await debugLog(`[AUTH] Cookies being sent: ${loginCookies.map(c => c.key).join(", ")}`); | ||
|
|
||
| const verifyResponse = await instance.get(verifyUrl); | ||
| await debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`); | ||
| // Log only non-sensitive data if possible, or truncate heavily | ||
| await debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 50)}...`); | ||
|
|
||
| if (verifyResponse.status !== 200) { | ||
| await debugLog(`[AUTH] Session verification failed with status: ${verifyResponse.status}`); | ||
| return { success: false, error: 'credentials', message: 'Session verification failed' }; | ||
| } | ||
| await debugLog("[AUTH] Session verified successfully"); | ||
| } catch (verifyError) { | ||
| if (axios.isAxiosError(verifyError)) { | ||
| await debugLog(`[AUTH] Session verification axios error - status: ${verifyError.response?.status}, message: ${verifyError.message}`); | ||
| // Avoid logging full sensitive data in error responses | ||
| await debugLog(`[AUTH] Verification error response status: ${verifyError.response?.status}`); | ||
| } | ||
| await debugLog(`[AUTH] Session verification request failed: ${verifyError}`); | ||
| return { success: false, error: 'credentials', message: 'Could not verify session validity' }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t log verification response payloads (even truncated) outside verbose mode
verifyResponse.data can contain user/profile data. If you keep it, gate behind PLANE_MCP_DEBUG === "verbose" and consider logging only shape/keys.
const verifyResponse = await instance.get(verifyUrl);
await debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`);
- // Log only non-sensitive data if possible, or truncate heavily
- await debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 50)}...`);
+ if (process.env.PLANE_MCP_DEBUG === "verbose") {
+ await debugLog(`[AUTH] Verification response keys: ${Object.keys(verifyResponse.data ?? {}).join(", ")}`);
+ }🤖 Prompt for AI Agents
In src/common/auth.ts around lines 149 to 174, the code logs verifyResponse.data
(even truncated) which may contain sensitive user/profile info; change this to
only log when PLANE_MCP_DEBUG === "verbose" and log non-sensitive metadata
instead (e.g., Object.keys(verifyResponse.data) or a small shape/keys summary
and status) — remove or gate the JSON.stringify(...).substring(0,50) call, wrap
the detailed payload logging behind the verbose check, and ensure default logs
only include status and a safe summary.
| const isPagesEndpoint = /\/pages\/|\/pages$|\/pages-summary\/|\/favorite-pages\/|\/pages\/[^/]+\/description\/|\/pages\/[^/]+\/versions\//.test(path); | ||
| const usesV1 = !isPagesEndpoint; | ||
| const apiPrefix = usesV1 ? 'api/v1/' : 'api/'; | ||
| const url = `${host}${apiPrefix}${path}`; | ||
|
|
||
| // Pages endpoints require session authentication, others use API key | ||
| const requiresSession = isPagesEndpoint; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enforce API-key presence for /api/v1/ requests (currently “required” only in docs)
If PLANE_API_KEY is missing, we should fail fast with a clear message (mirrors the session branch behavior) instead of sending unauthenticated requests and surfacing a 401/403 later.
} else {
// Use API key authentication for /api/v1/ endpoints
const headers: Record<string, string> = {};
- if (process.env.PLANE_API_KEY) {
- headers["X-API-Key"] = process.env.PLANE_API_KEY;
- }
+ if (!process.env.PLANE_API_KEY) {
+ throw new Error("API key authentication required. Please set PLANE_API_KEY.");
+ }
+ headers["X-API-Key"] = process.env.PLANE_API_KEY;Also applies to: 92-99
🤖 Prompt for AI Agents
In src/common/request-helper.ts around lines 29 to 36 (and similarly apply to
lines 92 to 99), the code constructs V1 API requests without enforcing the
presence of PLANE_API_KEY; update the V1 branch to fail fast when the env var is
missing by checking process.env.PLANE_API_KEY (or the config value) whenever
usesV1 is true and, if absent, log a clear error message and abort (throw an
Error or call processLogger.error with process.exit(1)) so unauthenticated
requests are never sent.
| await debugLog(`[REQUEST] CSRF token found`); | ||
| } else { | ||
| await debugLog(`[REQUEST] WARNING: No CSRF token found in cookies!`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing CSRF token allows requests to proceed and fail
When making non-GET requests to pages endpoints, if the CSRF token is not found in cookies, the code only logs a warning but proceeds with the request anyway. This will likely result in a 403 Forbidden response from the server, but the resulting error message won't indicate the actual root cause (missing CSRF token). For consistency with the login flow in auth.ts (which fails immediately when CSRF is missing), this condition likely warrants throwing a clear error or at least providing better diagnostic information in the error response.
| const verifyResponse = await instance.get(verifyUrl); | ||
| await debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`); | ||
| // Log only non-sensitive data if possible, or truncate heavily | ||
| await debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 50)}...`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Potential crash when response data is undefined
If verifyResponse.data is undefined (which can occur with empty response bodies), JSON.stringify(undefined) returns undefined (not the string "undefined" - the actual value undefined). Calling .substring(0, 50) on undefined throws a TypeError: Cannot read properties of undefined (reading 'substring'). This would crash the authentication verification step unexpectedly.
| "X-CSRFToken": csrfCookie.value, | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| }, | ||
| maxRedirects: 0, // Don't follow redirects, we just need the cookies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Disabled redirects may prevent session cookie capture
Setting maxRedirects: 0 prevents axios from following redirects after login. If the Plane server sets session cookies on the redirect target response (common in web authentication flows) rather than on the initial 302 response, those cookies won't be captured. This aligns with the user's reported error about missing session cookies. Many authentication systems only establish session cookies after the redirect is followed to the final destination.
- Implemented session expiry tracking (1 hour timeout). - Secured cookie retrieval to include path-scoped CSRF tokens. - Added 30s timeout to axios instance. - Explicitly accept 302 redirects for login endpoint while rejecting other 3xx/4xx/5xx in auth flow. - Clarified auth tool descriptions regarding session vs API key scope. - Ensured authentication state is reset on failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (6)
src/common/request-helper.ts (2)
63-79: Fail fast when CSRF token is missing for non-GET session requests.
Right now you log a warning and continue; this turns into a 403 later with worse diagnostics.if (method.toUpperCase() !== "GET") { headers["Content-Type"] = "application/json"; @@ const csrfCookie = cookies.find((c: any) => ["csrftoken", "csrf", "XSRF-TOKEN"].includes(c.key)); if (csrfCookie) { headers["X-CSRFToken"] = csrfCookie.value; await debugLog(`[REQUEST] CSRF token found`); } else { - await debugLog(`[REQUEST] WARNING: No CSRF token found in cookies!`); + await debugLog(`[REQUEST] ERROR: No CSRF token found in cookies`); + throw new Error("Missing CSRF token for session-authenticated write request. Re-run plane_login."); } } }
94-100: EnforcePLANE_API_KEYpresence for/api/v1/requests.
Proceeding without the API key makes failures noisy and delayed (401/403 downstream).} else { // Use API key authentication for /api/v1/ endpoints const headers: Record<string, string> = {}; - if (process.env.PLANE_API_KEY) { - headers["X-API-Key"] = process.env.PLANE_API_KEY; - } + if (!process.env.PLANE_API_KEY) { + throw new Error("API key authentication required. Please set PLANE_API_KEY."); + } + headers["X-API-Key"] = process.env.PLANE_API_KEY;src/common/auth.ts (4)
65-71: Reset auth state at the start of a login attempt (early-return paths currently keep staleisAuthenticated).
If a user was previously authenticated and a later login fails via an earlyreturn { success: false, ... },isAuthenticated/authenticationTimestay as-is.export async function authenticateWithPassword( email: string, password: string, hostUrl: string ): Promise<AuthResult> { try { + // Prevent stale state from prior successful logins on early-return failures. + isAuthenticated = false; + authenticationTime = null; + const instance = getAxiosInstance(); const host = hostUrl.endsWith("/") ? hostUrl : `${hostUrl}/`;
78-81: Never log full response headers (they can containSet-Cookiesession material).
Even in verbose mode, dumping headers risks session hijack via log exfiltration. Log only header names (or explicitly omitset-cookie).if (process.env.PLANE_MCP_DEBUG === 'verbose') { - await debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`); + await debugLog(`[AUTH] CSRF response headers present: ${Object.keys(csrfResponse.headers ?? {}).join(", ")}`); } @@ if (process.env.PLANE_MCP_DEBUG === 'verbose') { - await debugLog(`[AUTH] Login response headers FULL: ${JSON.stringify(loginResponse.headers)}`); + await debugLog(`[AUTH] Login response headers present: ${Object.keys(loginResponse.headers ?? {}).join(", ")}`); }Also applies to: 132-135
114-125: Login redirect handling likely breaks cookie capture (matches reported “No Session cookies received from server”).
WithmaxRedirects: 0, if the Plane auth flow sets the session cookie on the redirect target response, the jar never sees it. Allow redirects and simplify success criteria; the later/api/users/me/check already verifies auth.const loginResponse = await instance.post( `${host}auth/sign-in/`, formData.toString(), { headers: { "X-CSRFToken": csrfCookie.value, "Content-Type": "application/x-www-form-urlencoded", }, - maxRedirects: 0, // Don't follow redirects, we just need the cookies - validateStatus: (status) => (status >= 200 && status < 300) || status === 302, // Accept 2xx and 302 (redirect) as success + // Follow redirects so cookies set on the redirect target are captured. + maxRedirects: 5, + validateStatus: (status) => status >= 200 && status < 300, } );
172-174: Don’t logverifyResponse.data(PII) and guard againstundefinedto avoid crash.
JSON.stringify(undefined)returnsundefined, so.substring(...)can throw; also payload likely contains user/profile data.const verifyResponse = await instance.get(verifyUrl); await debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`); - // Log only non-sensitive data if possible, or truncate heavily - await debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 50)}...`); + if (process.env.PLANE_MCP_DEBUG === "verbose") { + const keys = verifyResponse.data && typeof verifyResponse.data === "object" + ? Object.keys(verifyResponse.data as Record<string, unknown>) + : []; + await debugLog(`[AUTH] Verification response keys: ${keys.join(", ")}`); + }
🧹 Nitpick comments (2)
src/common/request-helper.ts (2)
26-33: Harden pages-endpoint detection to avoid routing misses.
Current regex likely misses endpoints likepages-summarywithout trailing/,favorite-pageswithout trailing/, and.../versionswithout trailing/. Consider consolidating into a single anchored pattern (or explicit allow-list) to prevent accidental/api/v1/routing for Pages calls.
126-133: Avoid logging full error response payloads even in “verbose” mode (or at least redact).
error.response?.datacan contain workspace/project/user data; consider logging onlytype,detail, and/or top-level keys.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/common/auth.ts(1 hunks)src/common/request-helper.ts(1 hunks)src/tools/auth.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tools/auth.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/common/auth.ts (1)
src/common/debug.ts (1)
debugLog(8-20)
src/common/request-helper.ts (2)
src/common/debug.ts (1)
debugLog(8-20)src/common/auth.ts (2)
isSessionAuthenticated(219-235)getAxiosInstance(29-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (1)
src/common/auth.ts (1)
29-42: Axios + jar setup looks good for cross-tool cookie persistence.
Reusing the same wrapped instance is the right approach for MCP tool invocations.
| debugLog(`[AUTH] Session expired, resetting authentication`).catch(() => {}); | ||
| isAuthenticated = false; | ||
| authenticationTime = null; | ||
| return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Session timeout doesn't clear axios instance or cookies
When isSessionAuthenticated() detects that the session has expired (after 1 hour), it resets isAuthenticated and authenticationTime but does not clear axiosInstance. This means subsequent calls to getAxiosInstance() return the old instance with stale cookies still in the jar. When the user re-authenticates via plane_login, the new login adds cookies to a jar that may contain expired or conflicting session data. In contrast, resetAuthentication() properly sets axiosInstance = null. The timeout handler needs the same cleanup to ensure a fresh cookie jar is created on re-authentication.
Summary
This PR adds complete Pages API support to the Plane MCP server with session-based authentication.
What's New
Pages Tools (18 total)
Authentication Tools
plane_login: Email/password authentication with session cookiesplane_auth_status: Check current authentication stateplane_logout: Clear sessionTechnical Implementation
Dual Authentication System
/api/endpoints (Pages)/api/v1/endpoints (Projects, Issues, etc.)Session Management
tough-cookieandaxios-cookiejar-supportPath Routing
/api/vs/api/v1/)Testing
All functionality tested and verified:
Dependencies Added
axios-cookiejar-support: ^6.0.4tough-cookie: ^5.1.2Files Changed
src/common/auth.ts- Session authentication implementationsrc/common/request-helper.ts- Dual authentication routingsrc/tools/auth.ts- Authentication MCP toolssrc/tools/pages.ts- Complete Pages API toolspackage.json- Added dependenciesNote
Adds session (cookie) auth and a full Pages toolset, with request routing between /api (session) and /api/v1 (API key), plus debug logging and new Page schema.
list_page(s),get_page,create_page,update_page,delete_page,archive_page,unarchive_page,lock_page,unlock_page,favorite_page,unfavorite_page,duplicate_page,set_page_access,get_pages_summary,get_page_description,update_page_description,get_page_versions,get_page_version.registerPageToolsinsrc/tools/index.ts.plane_login(email/password with CSRF + cookies),plane_auth_status,plane_logoutinsrc/tools/auth.ts.makePlaneRequestnow auto-selects prefix and auth:/api+ session for Pages,/api/v1+ API key for others; adds sanitized debug logging.src/common/auth.ts: Axios with cookie jar, CSRF retrieval, session verification, expiry handling, reset.src/common/debug.ts: optional file+stderr debug logging.Pageschema insrc/schemas.ts.axios-cookiejar-support,tough-cookie(and related transitive entries).Written by Cursor Bugbot for commit 8a396c6. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.