-
Notifications
You must be signed in to change notification settings - Fork 38
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 and password login, dynamic request routing (session vs API-key), a new Page Zod schema/type, MCP tools for authentication and page management, and two runtime dependencies for cookie support. Changes
Sequence Diagram(s)sequenceDiagram
actor 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(...)
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 (Set-Cookie)
Auth-->>MCP: AuthResult (success/error)
User->>MCP: call page tool (e.g., list_pages)
MCP->>Req: makePlaneRequest(...)
Req->>Auth: isSessionAuthenticated()
Auth-->>Req: true
Req->>Axios: use cookie-jar Axios
Req->>Plane: GET /api/.../pages (cookies sent)
Plane-->>Req: page data
Req-->>MCP: formatted response
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❌ Failed checks (1 warning)
✅ Passed checks (2 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.
| 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.
| } | ||
|
|
||
| return { success: false, error: 'unknown', message: String(error) }; | ||
| } |
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: Stale Authentication State After Failed Login
When authenticateWithPassword fails, the isAuthenticated flag isn't reset to false. If a user successfully authenticates once then attempts to re-authenticate with invalid credentials, the failure leaves isAuthenticated as true while the actual session cookies may be invalid or partially updated, causing subsequent authenticated requests to use stale credentials with incorrect authentication state.
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
| 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 utility module to eliminate duplication.
The debugLog function is duplicated between auth.ts and request-helper.ts with identical implementations. This violates the DRY principle and makes maintenance harder (fixes must be applied in multiple places).
Create a shared src/common/debug.ts module:
// 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) {
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}`);
}
if (DEBUG_ENABLED) {
console.error(message);
}
}Then import and use it in both files:
import { debugLog } from "./debug.js";This also addresses the synchronous I/O concerns by making it async and adds an environment flag control.
🤖 Prompt for AI Agents
In src/common/request-helper.ts around lines 7 to 17, the local debugLog is
duplicated across files; extract it into a shared module src/common/debug.ts
that exports an async debugLog(message: string) which checks an env flag
(process.env.PLANE_MCP_DEBUG === 'true'), computes the timestamped log message,
uses fs.promises.appendFile to write to path.join(os.tmpdir(),
"plane-mcp-debug.log") inside a try/catch (logging write failures via
console.error), and prints the message to stderr when DEBUG is enabled; then
remove the local debugLog and replace it with an import { debugLog } from
"./debug.js" and update call sites to await debugLog(...) or handle the returned
Promise so there are no unhandled promises.
| debugLog(`[REQUEST] Error: ${error.message}, status: ${error.response?.status}`); | ||
| debugLog(`[REQUEST] Error response: ${JSON.stringify(error.response?.data)}`); | ||
| throw new Error(`Request failed: ${error.message} (${error.response?.status}). Response: ${JSON.stringify(error.response?.data)}`); |
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: Avoid logging and exposing full error response data.
Lines 126-127 log and throw the complete error.response?.data, which may contain sensitive project information, user data, or other PII. This violates data privacy principles and could expose sensitive information in logs and error messages shown to users.
Apply this diff to log only error metadata:
debugLog(`[REQUEST] Error: ${error.message}, status: ${error.response?.status}`);
- debugLog(`[REQUEST] Error response: ${JSON.stringify(error.response?.data)}`);
- throw new Error(`Request failed: ${error.message} (${error.response?.status}). Response: ${JSON.stringify(error.response?.data)}`);
+ // Log only non-sensitive error metadata
+ const errorType = error.response?.data?.type || 'unknown';
+ debugLog(`[REQUEST] Error response status: ${error.response?.status}, type: ${errorType}`);
+ throw new Error(`Request failed: ${error.message} (${error.response?.status})`);If detailed error information is needed for debugging, gate it behind a verbose debug flag:
if (process.env.PLANE_MCP_DEBUG === 'verbose') {
debugLog(`[REQUEST] Full error response: ${JSON.stringify(error.response?.data)}`);
}🤖 Prompt for AI Agents
In src/common/request-helper.ts around lines 125 to 127, the code currently logs
and throws the complete error.response?.data which may contain sensitive PII;
change it to log only non-sensitive metadata (e.g., error.message and
error.response?.status and headers) and remove
JSON.stringify(error.response?.data) from both the debug log and the thrown
Error message; if full response data is required for debugging, wrap a
debug-only conditional that logs the full payload only when
process.env.PLANE_MCP_DEBUG === 'verbose'; ensure the Error thrown contains only
a safe summary (message and status) not the full response body.
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: |
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-based auth and a full suite of Pages tools, updates request routing for /api vs /api/v1, and introduces cookie-jar dependencies.
src/tools/pages.ts):list_pages,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.src/common/auth.ts): CSRF flow, cookie-jar Axios, session verification, reset.src/tools/auth.ts):plane_login,plane_auth_status,plane_logout.src/common/request-helper.ts):/api/with session cookies; others to/api/v1/with API key.Pagetype (src/schemas.ts).src/tools/index.ts.axios-cookiejar-support,tough-cookie(package.json).Written by Cursor Bugbot for commit 4608a5a. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Improvements
Chores