feat: add fastmcp based MCP server #54
Conversation
- Supports both self-hosted (using stdio) and cloud using stdio and http transports. - Stdio supports PAT based auth and http transport uses OAuth
WalkthroughReplaces a Node.js/TypeScript MCP server implementation with a Python/FastMCP-based implementation. Removes all TypeScript source files and Node.js tooling; adds Python modules for OAuth and header authentication providers, multiple server modes (stdio, HTTP, SSE), tool registrations for Plane integration, and corresponding Docker and configuration files. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as CLI/Docker
participant Main as __main__.py<br/>(ServerMode)
participant OAuth as OAuth MCP<br/>App
participant Header as Header MCP<br/>App
participant SSE as SSE MCP<br/>App
participant Starlette as Starlette<br/>App + CORS
participant Uvicorn
participant PlaneAPI as Plane API
User->>CLI: Start server (HTTP mode)
CLI->>Main: Invoke main()
rect rgb(100, 150, 200)
Note over Main: HTTP Mode: Multi-MCP Setup
Main->>OAuth: get_oauth_mcp()<br/>(init with PlaneOAuthProvider)
OAuth->>PlaneAPI: Token verification endpoint configured
Main->>Header: get_header_mcp()<br/>(init with PlaneHeaderAuthProvider)
Main->>SSE: get_sse_mcp()<br/>(init for SSE transport)
end
rect rgb(150, 100, 200)
Note over Main: Lifespan Management
Main->>OAuth: Enter combined_lifespan context
Main->>Header: Setup lifespans
Main->>SSE: Setup lifespans
end
rect rgb(100, 200, 150)
Note over Main: Route Assembly & CORS
Main->>Starlette: Mount OAuth app at /.well-known/openid-configuration
Main->>Starlette: Mount Header app at /v1/http/headers
Main->>Starlette: Mount SSE app at /sse
Main->>Starlette: Attach permissive CORS policy
end
Main->>Uvicorn: Run on 0.0.0.0:8211<br/>(FASTMCP_PORT)
Main->>Uvicorn: Set startup logging
User->>Uvicorn: HTTP request (e.g., OAuth token exchange)
Uvicorn->>Starlette: Route to mounted app
Starlette->>OAuth: Process auth/request
OAuth->>PlaneAPI: Validate token
PlaneAPI-->>OAuth: User + app installation context
OAuth-->>Starlette: Response
Starlette-->>Uvicorn: Response with CORS headers
Uvicorn-->>User: JSON response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Areas requiring extra attention:
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.
Actionable comments posted: 18
🧹 Nitpick comments (9)
.github/workflows/publish-pypi.yml (1)
41-49: Version extraction could be more robust.The current
grep | cutapproach may break if thepyproject.tomlformat changes or if there are multipleversion =strings. Consider using a dedicated TOML parser for reliability.🔎 Alternative using Python:
- name: Get version from pyproject.toml id: package-version run: | - VERSION=$(grep "version = " pyproject.toml | cut -d'"' -f2 | head -1) + VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") if [ -z "$VERSION" ]; then echo "Error: Could not extract version from pyproject.toml" exit 1 fi echo "current-version=$VERSION" >> $GITHUB_OUTPUTplane_mcp/auth/plane_oauth_provider.py (2)
127-129: Consider reducing token exposure in logs.Logging the first 20 characters of tokens, even in info-level logs, could expose sensitive information in production environments. Consider using debug level or redacting entirely.
🔎 Apply this diff:
- logger.info( - f"verify_token called with token (first 20 chars): {token[:20] if token else 'None'}..." - ) + logger.debug("verify_token called with token")
197-202: Consider more specific exception handling.The broad
except Exceptioncatch could mask unexpected errors. Consider catching specific exceptions or at least logging with warning/error level for unexpected cases.🔎 Apply this diff:
except httpx.RequestError as e: - logger.info(f"Failed to verify Plane token (request error): {e}") + logger.warning(f"Failed to verify Plane token (request error): {e}") return None except Exception as e: - logger.info(f"Failed to verify Plane token: {e}", exc_info=True) + logger.warning(f"Failed to verify Plane token: {e}", exc_info=True) return Noneplane_mcp/tools/users.py (1)
20-21: Prefix unusedworkspace_slugwith underscore.The static analysis correctly flags that
workspace_slugis unpacked but never used in this function.🔎 Apply this diff:
- client, workspace_slug = get_plane_client_context() + client, _ = get_plane_client_context()plane_mcp/tools/cycles.py (1)
28-38: Docstring incorrectly listsworkspace_slugas a parameter.The docstring mentions
workspace_slug: The workspace slug identifieras an argument, but it's not an actual function parameter—it's obtained internally fromget_plane_client_context(). This pattern repeats across all tools in this file.🔎 Example fix for list_cycles:
""" List all cycles in a project. Args: - workspace_slug: The workspace slug identifier project_id: UUID of the project params: Optional query parameters as a dictionary Returns: List of Cycle objects """The same fix applies to all other tool functions in this file (
create_cycle,retrieve_cycle,update_cycle,delete_cycle,list_archived_cycles,add_work_items_to_cycle,remove_work_item_from_cycle,list_cycle_work_items,transfer_cycle_work_items,archive_cycle,unarchive_cycle).plane_mcp/__main__.py (1)
72-72: Unused lambda parameter.The
appparameter in the lambda is unused (flagged by static analysis). Use_to indicate intentionally unused.🔎 Suggested fix:
- lifespan=lambda app: combined_lifespan(oauth_app, header_app, sse_app), + lifespan=lambda _: combined_lifespan(oauth_app, header_app, sse_app),plane_mcp/tools/work_items.py (1)
89-89: Parametertypeshadows Python built-in.Using
typeas a parameter name shadows Python's built-intype()function. Consider renaming towork_item_typeortype_namefor clarity. Same applies to line 253 inupdate_work_item.plane_mcp/tools/intake.py (2)
48-69: Inconsistent API design withwork_items.py.This file uses
data: dict[str, Any]for create/update operations, whilework_items.pyuses explicit typed parameters (e.g.,name: str,assignees: list[str] | None). The explicit parameter approach inwork_items.pyis more discoverable and type-safe for MCP tool consumers.Consider aligning the intake tools to use explicit parameters for consistency, or documenting the expected dict keys in the docstring.
63-69: Consider catching validation errors for clearer error messages.If
datacontains invalid keys or values,CreateIntakeWorkItem(**data)will raise a PydanticValidationError. The raw error may be confusing to MCP clients. Consider wrapping with a try/except to provide a clearer error message about which fields are invalid.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonuv.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
.dockerignore(1 hunks).github/workflows/build_check.yml(0 hunks).github/workflows/ci.yml(0 hunks).github/workflows/publish-pypi.yml(1 hunks).github/workflows/publish.yml(0 hunks).gitignore(1 hunks).prettierrc(0 hunks)Dockerfile(1 hunks)LICENSE(2 hunks)README.md(1 hunks)eslint.config.mjs(0 hunks)package.json(0 hunks)plane_mcp/__init__.py(1 hunks)plane_mcp/__main__.py(1 hunks)plane_mcp/auth/__init__.py(1 hunks)plane_mcp/auth/plane_header_auth_provider.py(1 hunks)plane_mcp/auth/plane_oauth_provider.py(1 hunks)plane_mcp/client.py(1 hunks)plane_mcp/server.py(1 hunks)plane_mcp/tools/__init__.py(1 hunks)plane_mcp/tools/cycles.py(1 hunks)plane_mcp/tools/initiatives.py(1 hunks)plane_mcp/tools/intake.py(1 hunks)plane_mcp/tools/modules.py(1 hunks)plane_mcp/tools/projects.py(1 hunks)plane_mcp/tools/users.py(1 hunks)plane_mcp/tools/work_item_properties.py(1 hunks)plane_mcp/tools/work_items.py(1 hunks)pyproject.toml(1 hunks)src/common/request-helper.ts(0 hunks)src/common/version.ts(0 hunks)src/index.ts(0 hunks)src/schemas.ts(0 hunks)src/server.ts(0 hunks)src/tools/cycle-issues.ts(0 hunks)src/tools/cycles.ts(0 hunks)src/tools/index.ts(0 hunks)src/tools/issues.ts(0 hunks)src/tools/metadata.ts(0 hunks)src/tools/module-issues.ts(0 hunks)src/tools/modules.ts(0 hunks)src/tools/projects.ts(0 hunks)src/tools/user.ts(0 hunks)src/tools/work-log.ts(0 hunks)tsconfig.json(0 hunks)
💤 Files with no reviewable changes (22)
- src/common/version.ts
- .github/workflows/publish.yml
- src/tools/user.ts
- src/tools/cycles.ts
- src/server.ts
- src/tools/metadata.ts
- src/tools/module-issues.ts
- src/tools/cycle-issues.ts
- src/tools/modules.ts
- .github/workflows/build_check.yml
- src/tools/work-log.ts
- src/tools/index.ts
- tsconfig.json
- .github/workflows/ci.yml
- src/tools/projects.ts
- src/tools/issues.ts
- src/common/request-helper.ts
- package.json
- .prettierrc
- src/schemas.ts
- src/index.ts
- eslint.config.mjs
🧰 Additional context used
🧬 Code graph analysis (12)
plane_mcp/auth/__init__.py (2)
plane_mcp/auth/plane_header_auth_provider.py (1)
PlaneHeaderAuthProvider(10-39)plane_mcp/auth/plane_oauth_provider.py (1)
PlaneOAuthProvider(205-360)
plane_mcp/tools/users.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
plane_mcp/tools/work_items.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
plane_mcp/tools/intake.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
plane_mcp/auth/plane_header_auth_provider.py (1)
plane_mcp/auth/plane_oauth_provider.py (1)
verify_token(125-202)
plane_mcp/server.py (3)
plane_mcp/auth/plane_header_auth_provider.py (1)
PlaneHeaderAuthProvider(10-39)plane_mcp/auth/plane_oauth_provider.py (1)
PlaneOAuthProvider(205-360)plane_mcp/tools/__init__.py (1)
register_tools(15-24)
plane_mcp/tools/cycles.py (2)
plane_mcp/client.py (1)
get_plane_client_context(21-73)plane_mcp/tools/work_items.py (1)
list_work_items(22-67)
plane_mcp/tools/projects.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
plane_mcp/tools/initiatives.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
plane_mcp/tools/modules.py (2)
plane_mcp/client.py (1)
get_plane_client_context(21-73)plane_mcp/tools/work_items.py (1)
list_work_items(22-67)
plane_mcp/__main__.py (1)
plane_mcp/server.py (3)
get_header_mcp(40-48)get_oauth_mcp(14-37)get_stdio_mcp(51-56)
plane_mcp/tools/work_item_properties.py (1)
plane_mcp/client.py (1)
get_plane_client_context(21-73)
🪛 actionlint (1.7.9)
.github/workflows/publish-pypi.yml
18-18: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 LanguageTool
README.md
[grammar] ~16-~16: Ensure spelling is correct
Context: ...t doesn't require installation. ### 1. Stdio Transport (for local use) **MCP Client...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[style] ~250-~250: Consider using a less common alternative to make your writing sound more unique and professional.
Context: ...ontributing Contributions are welcome! Please feel free to submit a Pull Request. ## Deprecation ...
(FEEL_FREE_TO_STYLE_ME)
[grammar] ~256-~256: Use a hyphen to join words.
Context: ...sitory represents the new Python+FastMCP based implementation of the Plane MCP se...
(QB_NEW_EN_HYPHEN)
🪛 Ruff (0.14.8)
plane_mcp/tools/users.py
20-20: Unpacked variable workspace_slug is never used
Prefix it with an underscore or any other dummy variable pattern
(RUF059)
plane_mcp/__main__.py
44-44: Avoid specifying long messages outside the exception class
(TRY003)
46-46: Avoid specifying long messages outside the exception class
(TRY003)
72-72: Unused lambda argument: app
(ARG005)
84-84: Possible binding to all interfaces
(S104)
plane_mcp/auth/plane_oauth_provider.py
172-172: Abstract raise to an inner function
(TRY301)
172-172: Avoid specifying long messages outside the exception class
(TRY003)
200-200: Do not catch blind exception: Exception
(BLE001)
309-311: Avoid specifying long messages outside the exception class
(TRY003)
313-316: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (17)
LICENSE (1)
3-3: LGTM!The updated copyright notice appropriately reflects the contributor-based ownership model for this open-source project.
pyproject.toml (1)
1-50: LGTM!The project configuration is well-structured with pinned dependencies for reproducibility, appropriate Python version requirement, and consistent tooling configuration for Black and Ruff.
.gitignore (1)
1-57: LGTM!Comprehensive
.gitignoreconfiguration covering Python build artifacts, virtual environments, IDE files, testing caches, and environment files.plane_mcp/tools/initiatives.py (1)
17-151: Implementation pattern is sound.The tools follow a consistent pattern: obtain client context, construct request payloads using Plane SDK models, and delegate to the appropriate client methods. The tool registration via
@mcp.tool()decorator is idiomatic for FastMCP.plane_mcp/tools/modules.py (1)
20-303: Comprehensive module tooling implementation.The module tools provide complete CRUD operations plus archive/unarchive and work item association management. The implementation is consistent with the pattern established in other tool modules.
README.md (1)
1-290: Comprehensive and well-structured documentation.The README provides clear guidance for all transport methods, authentication options, and tool categories. The deprecation notice with migration guidance is helpful for users transitioning from the Node.js version.
plane_mcp/auth/plane_oauth_provider.py (1)
205-360: Well-designed OAuth provider implementation.The
PlaneOAuthProviderclass provides a clean abstraction over Plane's OAuth flow with sensible defaults, environment variable fallbacks, and comprehensive documentation. The integration with FastMCP'sOAuthProxyis appropriate.plane_mcp/__init__.py (1)
1-1: LGTM!Simple and clean package initialization with an appropriate module docstring.
Dockerfile (1)
1-33: LGTM!Well-structured Dockerfile with good practices:
- Uses slim base image to minimize attack surface
- Cleans up apt lists to reduce image size
- Leverages
uvfor faster dependency installation- Separates
ENTRYPOINTandCMDallowing transport mode override.dockerignore (1)
1-42: LGTM!Comprehensive
.dockerignorewith appropriate patterns for a Python project. Good exclusion of development artifacts, IDE files, and test caches to minimize build context size.plane_mcp/auth/__init__.py (1)
1-7: LGTM!Clean package initialization with explicit
__all__exports following Python conventions.plane_mcp/tools/__init__.py (1)
1-24: LGTM!Clean centralized entry point for tool registration. The pattern of aggregating individual
register_*_toolsfunctions into a singleregister_toolsfunction provides good modularity and maintainability.plane_mcp/tools/cycles.py (1)
20-328: LGTM on the implementation!Comprehensive and well-structured cycle management tooling. The pattern of obtaining client context and delegating to typed SDK methods is consistent and maintainable.
plane_mcp/client.py (1)
14-73: Overall implementation is solid.The authentication flow correctly handles multiple auth methods (OAuth, API key via env, API key via headers) and constructs the appropriate PlaneClient. The NamedTuple pattern for the return value is clean and Pythonic.
plane_mcp/__main__.py (1)
75-81: Permissive CORS configuration.
allow_origins=["*"]withallow_credentials=Trueis very permissive. This may be intentional for MCP client compatibility, but be aware that it allows any origin to make credentialed requests. For production deployments, consider restricting origins if specific MCP clients are known.plane_mcp/server.py (1)
50-56: LGTM!The stdio MCP correctly omits auth configuration since authentication is handled via environment variables (
PLANE_API_KEY,PLANE_WORKSPACE_SLUG) which are validated in__main__.pybefore this function is called.plane_mcp/tools/work_items.py (1)
18-68: LGTM!The
list_work_itemstool implementation is clean: properly constructs query params, calls the client API, and returns results. The pattern is consistent across all work item tools.
| - name: Set up Python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: "3.10" |
There was a problem hiding this comment.
Consider upgrading to actions/setup-python@v6.
The current stable version is v6. Upgrade from @v4 to @v6 for improved compatibility and the latest features.
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
python-version: "3.10"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Set up Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: "3.10" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.10" |
🧰 Tools
🪛 actionlint (1.7.9)
18-18: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🤖 Prompt for AI Agents
In .github/workflows/publish-pypi.yml around lines 17 to 20, update the GitHub
Action reference from actions/setup-python@v4 to actions/setup-python@v6 to use
the latest stable release; edit the uses line to point to
actions/setup-python@v6 (keeping the existing python-version input as-is) and
commit the workflow change.
| class ServerMode(Enum): | ||
| STDIO = "stdio" | ||
| SSE = "sse" | ||
| HTTP = "http" | ||
|
|
There was a problem hiding this comment.
SSE enum value is defined but not handled.
The ServerMode.SSE enum is defined but there's no corresponding if server_mode == ServerMode.SSE: branch. If a user runs with python -m plane_mcp sse, the function exits without doing anything (no explicit handling, no error). Either add SSE mode handling or remove the enum value if SSE is always served under HTTP mode.
🤖 Prompt for AI Agents
In plane_mcp/__main__.py around lines 19 to 23, ServerMode.SSE is declared but
never handled; if invoked with "sse" nothing happens. Fix by either (A) adding
an explicit branch for ServerMode.SSE that performs the intended behavior (e.g.,
start an SSE server or delegate to the HTTP start routine with an SSE flag)
mirroring how STDIO/HTTP are handled, ensuring any required imports/parameters
are passed and a clear log/error is emitted on failure; or (B) if SSE is not
supported, remove the SSE enum value and ensure argument parsing/usage docs are
updated to prevent users from selecting it, and add a guard that raises a clear
error if an unknown/unsupported mode is provided.
| server_mode = ServerMode.STDIO | ||
| if len(sys.argv) > 1: | ||
| server_mode = ServerMode(sys.argv[1]) |
There was a problem hiding this comment.
Missing error handling for invalid server mode argument.
If an invalid mode is passed via sys.argv[1], ServerMode(sys.argv[1]) raises a ValueError with an unclear message. Consider catching this and providing a helpful error message listing valid modes.
🔎 Suggested fix:
server_mode = ServerMode.STDIO
if len(sys.argv) > 1:
- server_mode = ServerMode(sys.argv[1])
+ try:
+ server_mode = ServerMode(sys.argv[1])
+ except ValueError:
+ valid_modes = ", ".join(m.value for m in ServerMode)
+ raise SystemExit(f"Invalid server mode: {sys.argv[1]}. Valid modes: {valid_modes}")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| server_mode = ServerMode.STDIO | |
| if len(sys.argv) > 1: | |
| server_mode = ServerMode(sys.argv[1]) | |
| server_mode = ServerMode.STDIO | |
| if len(sys.argv) > 1: | |
| try: | |
| server_mode = ServerMode(sys.argv[1]) | |
| except ValueError: | |
| valid_modes = ", ".join(m.value for m in ServerMode) | |
| raise SystemExit(f"Invalid server mode: {sys.argv[1]}. Valid modes: {valid_modes}") |
🤖 Prompt for AI Agents
In plane_mcp/__main__.py around lines 37 to 39, the code directly constructs
ServerMode from sys.argv[1] which will raise a ValueError for invalid input;
wrap the conversion in a try/except catching ValueError, print a clear error
message that includes the invalid value and the list of valid modes (derive
names from ServerMode, e.g., ", ".join(m.name for m in ServerMode)), and exit
with a non-zero status; keep the default to ServerMode.STDIO when no arg is
provided.
| allow_headers=["*"], | ||
| ) | ||
|
|
||
| logger.info("Starting HTTP server at URLs: /mcp and /header/mcp") |
There was a problem hiding this comment.
Inaccurate log message.
The log states URLs are /mcp and /header/mcp, but the actual mounts are /http (OAuth), /http/api-key (header auth), and / (SSE). Update to reflect actual routes.
🔎 Suggested fix:
- logger.info("Starting HTTP server at URLs: /mcp and /header/mcp")
+ logger.info("Starting HTTP server with routes: /http (OAuth), /http/api-key (header auth), / (SSE)")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.info("Starting HTTP server at URLs: /mcp and /header/mcp") | |
| logger.info("Starting HTTP server with routes: /http (OAuth), /http/api-key (header auth), / (SSE)") |
🤖 Prompt for AI Agents
In plane_mcp/__main__.py around line 83, the logger.info call currently reports
incorrect routes; change the log message to list the actual mounted routes —
"/http" (OAuth), "/http/api-key" (header auth), and "/" (SSE) — so the log
accurately reflects the server endpoints and their auth types.
| async def verify_token(self, token: str) -> AccessToken | None: | ||
| try: | ||
| from fastmcp.server.dependencies import get_http_headers | ||
|
|
||
| headers = get_http_headers() | ||
|
|
||
| if token: | ||
| workspace_slug = headers.get("x-workspace-slug") | ||
| if workspace_slug: | ||
| logger.info("Using API key from HTTP headers") | ||
| expires_at = int(time.time() + 3600) | ||
| return AccessToken( | ||
| token=token, | ||
| client_id="api_key_header_user", | ||
| scopes=["read", "write"], | ||
| expires_at=expires_at, | ||
| claims={ | ||
| "auth_method": "api_key_header", | ||
| "workspace_slug": workspace_slug, | ||
| }, | ||
| ) | ||
| else: | ||
| logger.warning("x-api-key header found but x-workspace-slug is missing") | ||
| except RuntimeError: | ||
| # No active HTTP request available (e.g., stdio transport) | ||
| logger.debug("No active HTTP request available for header check") |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
find . -type f -name "*.py" | grep -E "(auth|plane)" | head -20Repository: makeplane/plane-mcp-server
Repository: makeplane/plane-mcp-server
Exit code: 0
stdout:
./plane_mcp/__init__.py
./plane_mcp/__main__.py
./plane_mcp/auth/__init__.py
./plane_mcp/auth/plane_header_auth_provider.py
./plane_mcp/auth/plane_oauth_provider.py
./plane_mcp/client.py
./plane_mcp/server.py
./plane_mcp/tools/__init__.py
./plane_mcp/tools/cycles.py
./plane_mcp/tools/initiatives.py
./plane_mcp/tools/intake.py
./plane_mcp/tools/modules.py
./plane_mcp/tools/projects.py
./plane_mcp/tools/users.py
./plane_mcp/tools/work_item_properties.py
./plane_mcp/tools/work_items.py
Script executed:
cat -n plane_mcp/auth/plane_header_auth_provider.pyRepository: makeplane/plane-mcp-server
Repository: makeplane/plane-mcp-server
Exit code: 0
stdout:
1 import time
2
3 from fastmcp.server.auth import TokenVerifier
4 from fastmcp.server.auth.auth import AccessToken
5 from fastmcp.utilities.logging import get_logger
6
7 logger = get_logger(__name__)
8
9
10 class PlaneHeaderAuthProvider(TokenVerifier):
11 def __init__(self, required_scopes: list[str] | None = None):
12 super().__init__(required_scopes=required_scopes)
13
14 async def verify_token(self, token: str) -> AccessToken | None:
15 try:
16 from fastmcp.server.dependencies import get_http_headers
17
18 headers = get_http_headers()
19
20 if token:
21 workspace_slug = headers.get("x-workspace-slug")
22 if workspace_slug:
23 logger.info("Using API key from HTTP headers")
24 expires_at = int(time.time() + 3600)
25 return AccessToken(
26 token=token,
27 client_id="api_key_header_user",
28 scopes=["read", "write"],
29 expires_at=expires_at,
30 claims={
31 "auth_method": "api_key_header",
32 "workspace_slug": workspace_slug,
33 },
34 )
35 else:
36 logger.warning("x-api-key header found but x-workspace-slug is missing")
37 except RuntimeError:
38 # No active HTTP request available (e.g., stdio transport)
39 logger.debug("No active HTTP request available for header check")
Script executed:
cat -n plane_mcp/auth/plane_oauth_provider.pyRepository: makeplane/plane-mcp-server
Repository: makeplane/plane-mcp-server
Exit code: 0
stdout:
1 """Plane OAuth provider for FastMCP.
2
3 This module provides a complete Plane OAuth integration that's ready to use
4 with just a client ID and client secret. It handles all the complexity of
5 Plane's OAuth flow, token validation, and user management.
6
7 Example:
8 ```python
9 from fastmcp import FastMCP
10 from plane_mcp.plane_oauth_provider import PlaneOAuthProvider
11
12 # Simple Plane OAuth protection
13 auth = PlaneOAuthProvider(
14 client_id="your-plane-client-id",
15 client_secret="your-plane-client-secret",
16 base_url="https://api.plane.so"
17 )
18
19 mcp = FastMCP("My Protected Server", auth=auth)
20 ```
21 """
22
23 from __future__ import annotations
24
25 import os
26 import time
27
28 import httpx
29 from fastmcp.server.auth import TokenVerifier
30 from fastmcp.server.auth.auth import AccessToken
31 from fastmcp.server.auth.oauth_proxy import OAuthProxy
32 from fastmcp.settings import ENV_FILE
33 from fastmcp.utilities.auth import parse_scopes
34 from fastmcp.utilities.logging import get_logger
35 from fastmcp.utilities.types import NotSet, NotSetT
36 from key_value.aio.protocols import AsyncKeyValue
37 from plane.models.users import UserLite
38 from pydantic import AnyHttpUrl, BaseModel, SecretStr, field_validator
39 from pydantic_settings import BaseSettings, SettingsConfigDict
40
41 logger = get_logger(__name__)
42
43
44 DEFAULT_PLANE_BASE_URL = "https://api.plane.so"
45
46
47 class WorkspaceDetail(BaseModel):
48 """Workspace detail information."""
49
50 name: str
51 slug: str
52 id: str
53 logo_url: str | None = None
54
55
56 class PlaneOAuthAppInstallation(BaseModel):
57 """Plane OAuth app installation information."""
58
59 id: str
60 workspace_detail: WorkspaceDetail
61 created_at: str
62 updated_at: str
63 deleted_at: str | None = None
64 status: str
65 created_by: str | None = None
66 updated_by: str | None = None
67 workspace: str
68 application: str
69 installed_by: str
70 app_bot: str
71 webhook: str | None = None
72
73
74 class PlaneOAuthProviderSettings(BaseSettings):
75 """Settings for Plane OAuth provider."""
76
77 model_config = SettingsConfigDict(
78 env_prefix="PLANE_OAUTH_PROVIDER_",
79 env_file=ENV_FILE,
80 extra="ignore",
81 )
82
83 client_id: str | None = None
84 client_secret: SecretStr | None = None
85 base_url: AnyHttpUrl | str | None = None
86 issuer_url: AnyHttpUrl | str | None = None
87 redirect_path: str | None = None
88 required_scopes: list[str] | None = None
89 timeout_seconds: int | None = None
90 allowed_client_redirect_uris: list[str] | None = None
91 jwt_signing_key: str | None = None
92 plane_base_url: str | None = None
93
94 @field_validator("required_scopes", mode="before")
95 @classmethod
96 def _parse_scopes(cls, v):
97 return parse_scopes(v)
98
99
100 class PlaneOAuthTokenVerifier(TokenVerifier):
101 """Token verifier for Plane OAuth tokens.
102
103 Plane OAuth tokens are verified by calling Plane's API to check if they're
104 valid and get user info.
105 """
106
107 def __init__(
108 self,
109 *,
110 required_scopes: list[str] | None = None,
111 timeout_seconds: int = 10,
112 plane_base_url: str | None = None,
113 ):
114 """Initialize the Plane token verifier.
115
116 Args:
117 required_scopes: Required OAuth scopes (currently not enforced by Plane API)
118 timeout_seconds: HTTP request timeout
119 plane_base_url: Base URL for Plane API (defaults to https://api.plane.so)
120 """
121 super().__init__(required_scopes=required_scopes)
122 self.timeout_seconds = timeout_seconds
123 self.plane_base_url = plane_base_url or os.getenv("PLANE_BASE_URL", DEFAULT_PLANE_BASE_URL)
124
125 async def verify_token(self, token: str) -> AccessToken | None:
126 """Verify Plane OAuth token by calling Plane API."""
127 logger.info(
128 f"verify_token called with token (first 20 chars): {token[:20] if token else 'None'}..."
129 )
130 try:
131 # Build the user endpoint URL
132 base_url = self.plane_base_url.rstrip("/")
133 user_url = f"{base_url}/api/v1/users/me/"
134 logger.info(f"Verifying token against: {user_url}")
135
136 async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
137 # Get current user info to verify token
138 response = await client.get(
139 user_url,
140 headers={
141 "Authorization": f"Bearer {token}",
142 "Content-Type": "application/json",
143 },
144 )
145
146 logger.info(f"Plane API response status: {response.status_code}")
147 if response.status_code != 200:
148 logger.info(
149 f"Plane token verification failed: {response.status_code} - {response.text[:200]}"
150 )
151 return None
152
153 # Parse user data
154 user_data = response.json()
155 user = UserLite.model_validate(user_data)
156
157 expires_at = int(time.time() + 3600)
158
159 logger.info(f"User: ({user.id}) - {user.display_name}")
160
161 installations_response = await client.get(
162 f"{base_url}/auth/o/app-installation/",
163 headers={
164 "Authorization": f"Bearer {token}",
165 "Content-Type": "application/json",
166 },
167 )
168
169 installations: list[PlaneOAuthAppInstallation] = installations_response.json()
170
171 if not installations:
172 raise ValueError("No app installations found")
173
174 installation = installations[0]
175
176 # Create AccessToken with Plane user info
177 return AccessToken(
178 token=token,
179 client_id=user.id or "unknown",
180 scopes=["read", "write"], # Plane doesn't expose scopes in user endpoint
181 expires_at=expires_at, # Plane tokens don't typically expire
182 claims={
183 "auth_method": "oauth",
184 "sub": user.id or "unknown",
185 "email": user.email,
186 "first_name": user.first_name,
187 "last_name": user.last_name,
188 "display_name": user.display_name,
189 "avatar": user.avatar,
190 "avatar_url": user.avatar_url,
191 "plane_user_data": user_data,
192 "workspace_slug": installation.get("workspace_detail", {}).get("slug"),
193 "workspace": installation.get("workspace_detail", {}),
194 },
195 )
196
197 except httpx.RequestError as e:
198 logger.info(f"Failed to verify Plane token (request error): {e}")
199 return None
200 except Exception as e:
201 logger.info(f"Failed to verify Plane token: {e}", exc_info=True)
202 return None
203
204
205 class PlaneOAuthProvider(OAuthProxy):
206 """Complete Plane OAuth provider for FastMCP.
207
208 This provider makes it trivial to add Plane OAuth protection to any
209 FastMCP server. Just provide your Plane OAuth app credentials and
210 a base URL, and you're ready to go.
211
212 Features:
213 - Transparent OAuth proxy to Plane
214 - Automatic token validation via Plane API
215 - User information extraction
216 - Minimal configuration required
217
218 Example:
219 ```python
220 from fastmcp import FastMCP
221 from plane_mcp.plane_oauth_provider import PlaneOAuthProvider
222
223 auth = PlaneOAuthProvider(
224 client_id="your-client-id",
225 client_secret="your-client-secret",
226 base_url="https://my-server.com",
227 plane_base_url="https://api.plane.so"
228 )
229
230 mcp = FastMCP("My App", auth=auth)
231 ```
232 """
233
234 def __init__(
235 self,
236 *,
237 client_id: str | NotSetT = NotSet,
238 client_secret: str | NotSetT = NotSet,
239 base_url: AnyHttpUrl | str | NotSetT = NotSet,
240 issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
241 redirect_path: str | NotSetT = NotSet,
242 required_scopes: list[str] | NotSetT = NotSet,
243 timeout_seconds: int | NotSetT = NotSet,
244 allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
245 client_storage: AsyncKeyValue | None = None,
246 jwt_signing_key: str | bytes | NotSetT = NotSet,
247 require_authorization_consent: bool = True,
248 plane_base_url: str | NotSetT = NotSet,
249 ):
250 """Initialize Plane OAuth provider.
251
252 Args:
253 client_id: Plane OAuth app client ID
254 client_secret: Plane OAuth app client secret
255 base_url: Public URL where OAuth endpoints will be accessible
256 (includes any mount path)
257 issuer_url: Issuer URL for OAuth metadata (defaults to base_url).
258 Use root-level URL to avoid 404s during discovery when mounting
259 under a path.
260 redirect_path: Redirect path configured in Plane OAuth app
261 (defaults to "/auth/callback")
262 required_scopes: Required Plane scopes
263 (currently not enforced by Plane API)
264 timeout_seconds: HTTP request timeout for Plane API calls
265 allowed_client_redirect_uris: List of allowed redirect URI patterns
266 for MCP clients. If None (default), all URIs are allowed.
267 If empty list, no URIs are allowed.
268 client_storage: Storage backend for OAuth state
269 (client registrations, encrypted tokens). If None, a DiskStore
270 will be created in the data directory (derived from
271 `platformdirs`). The disk store will be encrypted using a key
272 derived from the JWT Signing Key.
273 jwt_signing_key: Secret for signing FastMCP JWT tokens
274 (any string or bytes). If bytes are provided, they will be used
275 as is. If a string is provided, it will be derived into a
276 32-byte key. If not provided, the upstream client secret will be
277 used to derive a 32-byte key using PBKDF2.
278 require_authorization_consent: Whether to require user consent
279 before authorizing clients (default True). When True, users see
280 a consent screen before being redirected to Plane. When False,
281 authorization proceeds directly without user confirmation.
282 SECURITY WARNING: Only disable for local development or
283 testing environments.
284 plane_base_url: Base URL for Plane API
285 (defaults to https://api.plane.so or PLANE_BASE_URL env var)
286 """
287
288 settings = PlaneOAuthProviderSettings.model_validate(
289 {
290 k: v
291 for k, v in {
292 "client_id": client_id,
293 "client_secret": client_secret,
294 "base_url": base_url,
295 "issuer_url": issuer_url,
296 "redirect_path": redirect_path,
297 "required_scopes": required_scopes,
298 "timeout_seconds": timeout_seconds,
299 "allowed_client_redirect_uris": allowed_client_redirect_uris,
300 "jwt_signing_key": jwt_signing_key,
301 "plane_base_url": plane_base_url,
302 }.items()
303 if v is not NotSet
304 }
305 )
306
307 # Validate required settings
308 if not settings.client_id:
309 raise ValueError(
310 "client_id is required - set via parameter or PLANE_OAUTH_PROVIDER_CLIENT_ID"
311 )
312 if not settings.client_secret:
313 raise ValueError(
314 "client_secret is required - set via parameter or "
315 "PLANE_OAUTH_PROVIDER_CLIENT_SECRET"
316 )
317
318 # Apply defaults
319 timeout_seconds_final = settings.timeout_seconds or 10
320 required_scopes_final = settings.required_scopes or []
321 allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
322 plane_base_url_final = settings.plane_base_url or os.getenv(
323 "PLANE_BASE_URL", DEFAULT_PLANE_BASE_URL
324 )
325
326 # Create Plane token verifier
327 token_verifier = PlaneOAuthTokenVerifier(
328 required_scopes=required_scopes_final,
329 timeout_seconds=timeout_seconds_final,
330 plane_base_url=plane_base_url_final,
331 )
332
333 # Extract secret string from SecretStr
334 client_secret_str = (
335 settings.client_secret.get_secret_value() if settings.client_secret else ""
336 )
337
338 # Initialize OAuth proxy with Plane endpoints
339 super().__init__(
340 upstream_authorization_endpoint=(f"{plane_base_url_final}/auth/o/authorize-app/"),
341 upstream_token_endpoint=f"{plane_base_url_final}/auth/o/token/",
342 upstream_client_id=settings.client_id,
343 upstream_client_secret=client_secret_str,
344 token_verifier=token_verifier,
345 base_url=settings.base_url,
346 redirect_path=settings.redirect_path,
347 issuer_url=settings.issuer_url
348 or settings.base_url, # Default to base_url if not specified
349 allowed_client_redirect_uris=allowed_client_redirect_uris_final,
350 client_storage=client_storage,
351 jwt_signing_key=settings.jwt_signing_key,
352 require_authorization_consent=require_authorization_consent,
353 valid_scopes=["read", "write"],
354 )
355
356 logger.info(
357 "Initialized Plane OAuth provider for client %s with scopes: %s",
358 settings.client_id,
359 required_scopes_final,
360 )
Missing explicit return None and no token validation against Plane API.
Two observations:
-
The function lacks an explicit
return Noneat the end (after line 39). While Python implicitly returnsNone, adding it explicitly improves clarity, matching the pattern inPlaneOAuthTokenVerifierwhich explicitly returnsNonein all error paths. -
More critically,
PlaneHeaderAuthProvider.verify_tokendoesn't validate the provided token against the Plane API. It only checks for the presence of thex-workspace-slugheader and constructs anAccessTokenif found, accepting any arbitrary string as a token. In contrast,PlaneOAuthTokenVerifier.verify_tokencalls the Plane API endpoint (/api/v1/users/me/) to verify tokens. This means any string passed as a token will be accepted if the workspace header is present—a security risk unless upstream validation is guaranteed.
Add explicit return None at the end, and clarify whether the header-based API key should be validated against the Plane API for security parity with OAuth validation, or document that this pattern relies on upstream validation only.
| """ | ||
| List all modules in a project. | ||
|
|
||
| Args: | ||
| workspace_slug: The workspace slug identifier | ||
| project_id: UUID of the project | ||
| params: Optional query parameters as a dictionary | ||
|
|
||
| Returns: | ||
| List of Module objects | ||
| """ | ||
| client, workspace_slug = get_plane_client_context() | ||
| response: PaginatedModuleResponse = client.modules.list( | ||
| workspace_slug=workspace_slug, project_id=project_id, params=params | ||
| ) | ||
| return response.results |
There was a problem hiding this comment.
Same docstring inconsistency as in initiatives.py.
All function docstrings in this file document workspace_slug as an argument, but it's obtained from get_plane_client_context() rather than passed as a parameter. Consider removing these misleading argument descriptions.
🤖 Prompt for AI Agents
In plane_mcp/tools/modules.py around lines 28 to 43, the function docstring
incorrectly documents workspace_slug as a parameter even though it is retrieved
internally via get_plane_client_context(); update the docstring to remove
workspace_slug from the Args section (or replace that entry with a brief note
that workspace_slug is derived from get_plane_client_context()) and ensure the
remaining Args list correctly reflects only project_id and params, keeping the
description and return value intact.
| def list_projects( | ||
| cursor: str | None = None, | ||
| per_page: int | None = None, | ||
| expand: str | None = None, | ||
| fields: str | None = None, | ||
| order_by: str | None = None, | ||
| ) -> list[Project]: | ||
| """ | ||
| List all projects in a workspace. | ||
|
|
||
| Args: | ||
| workspace_slug: The workspace slug identifier | ||
| cursor: Pagination cursor for getting next set of results | ||
| per_page: Number of results per page (1-100) | ||
| expand: Comma-separated list of related fields to expand in response | ||
| fields: Comma-separated list of fields to include in response | ||
| order_by: Field to order results by. Prefix with '-' for descending order | ||
|
|
||
| Returns: | ||
| List of Project objects | ||
| """ | ||
| client, workspace_slug = get_plane_client_context() | ||
|
|
||
| params = PaginatedQueryParams( | ||
| cursor=cursor, | ||
| per_page=per_page, | ||
| expand=expand, | ||
| fields=fields, | ||
| order_by=order_by, | ||
| ) | ||
|
|
||
| response: PaginatedProjectResponse = client.projects.list( | ||
| workspace_slug=workspace_slug, | ||
| params=params, | ||
| ) | ||
|
|
||
| return response.results |
There was a problem hiding this comment.
Remove workspace_slug from docstrings.
All nine tool functions document workspace_slug as a parameter in their docstrings, but it is not part of any function signature. Instead, workspace_slug is obtained internally via get_plane_client_context(). This documentation inconsistency could confuse MCP tool consumers.
Apply similar fixes as in work_item_properties.py: remove the workspace_slug line from the Args section of each function's docstring:
list_projects(line 35)create_project(line 88)retrieve_project(line 144)update_project(line 183)delete_project(line 248)get_project_worklog_summary(line 260)get_project_members(line 279)get_project_features(line 297)update_project_features(line 321)
Also applies to: 63-136, 139-151, 154-240, 243-252, 255-269, 272-289, 292-304, 307-348
🤖 Prompt for AI Agents
In plane_mcp/tools/projects.py edit the docstrings at the specified ranges
(lines ~24-60, 63-136, 139-151, 154-240, 243-252, 255-269, 272-289, 292-304,
307-348) to remove the incorrect "workspace_slug" entry from each function's
Args section (specifically remove the workspace_slug parameter line in
list_projects at ~line 35 and the same line in create_project, retrieve_project,
update_project, delete_project, get_project_worklog_summary,
get_project_members, get_project_features, update_project_features), leaving the
rest of the Arg descriptions intact; ensure docstrings reflect that
workspace_slug is obtained internally via get_plane_client_context() and run a
quick lint/check to keep formatting consistent.
| def list_work_item_properties( | ||
| project_id: str, | ||
| type_id: str, | ||
| params: dict[str, Any] | None = None, | ||
| ) -> list[WorkItemProperty]: | ||
| """ | ||
| List work item properties for a work item type. | ||
|
|
||
| Args: | ||
| workspace_slug: The workspace slug identifier | ||
| project_id: UUID of the project | ||
| type_id: UUID of the work item type | ||
| params: Optional query parameters as a dictionary | ||
|
|
||
| Returns: | ||
| List of WorkItemProperty objects | ||
| """ | ||
| client, workspace_slug = get_plane_client_context() | ||
| return client.work_item_properties.list( | ||
| workspace_slug=workspace_slug, | ||
| project_id=project_id, | ||
| type_id=type_id, | ||
| params=params, | ||
| ) |
There was a problem hiding this comment.
Remove workspace_slug from docstrings.
All five tool functions document workspace_slug as a parameter in their docstrings, but it is not part of the function signature. Instead, workspace_slug is obtained internally via get_plane_client_context(). This documentation inconsistency could confuse MCP tool consumers (including LLM clients).
🔎 Apply this diff to remove workspace_slug from docstrings:
For list_work_item_properties:
"""
List work item properties for a work item type.
Args:
- workspace_slug: The workspace slug identifier
project_id: UUID of the project
type_id: UUID of the work item type
params: Optional query parameters as a dictionaryFor create_work_item_property:
"""
Create a new work item property.
Args:
- workspace_slug: The workspace slug identifier
project_id: UUID of the project
type_id: UUID of the work item type
display_name: Display name for the propertyFor retrieve_work_item_property:
"""
Retrieve a work item property by ID.
Args:
- workspace_slug: The workspace slug identifier
project_id: UUID of the project
type_id: UUID of the work item type
work_item_property_id: UUID of the propertyFor update_work_item_property:
"""
Update a work item property by ID.
Args:
- workspace_slug: The workspace slug identifier
project_id: UUID of the project
type_id: UUID of the work item type
work_item_property_id: UUID of the propertyFor delete_work_item_property:
"""
Delete a work item property by ID.
Args:
- workspace_slug: The workspace slug identifier
project_id: UUID of the project
type_id: UUID of the work item type
work_item_property_id: UUID of the propertyAlso applies to: 53-129, 132-155, 158-237, 240-260
🤖 Prompt for AI Agents
In plane_mcp/tools/work_item_properties.py around lines 27 to 50 (and similarly
for the other affected ranges 53-129, 132-155, 158-237, 240-260), the docstrings
incorrectly list workspace_slug as a parameter although the function obtains
workspace_slug internally via get_plane_client_context(); remove any mention of
workspace_slug from each function's Args section and update the parameter list
to only include the actual function parameters (project_id, type_id, params,
etc.), leaving the rest of the docstring intact (description, Returns) so docs
match the signature and avoid confusion for tool consumers.
| # Convert settings dict to appropriate settings object if needed | ||
| processed_settings: PropertySettings = None | ||
| if settings and property_type: | ||
| prop_type = ( | ||
| property_type.value if isinstance(property_type, PropertyType) else property_type | ||
| ) | ||
| if prop_type == "TEXT" and isinstance(settings, dict): | ||
| processed_settings = TextAttributeSettings(**settings) | ||
| elif prop_type == "DATETIME" and isinstance(settings, dict): | ||
| processed_settings = DateAttributeSettings(**settings) | ||
| else: | ||
| processed_settings = settings |
There was a problem hiding this comment.
Critical: Settings are silently dropped when property_type is not provided.
The condition on line 205 requires both settings and property_type to process the settings. If a user provides settings but not property_type (e.g., updating settings for an existing TEXT property without re-specifying the type), processed_settings remains None, and the user's settings are silently lost.
🔎 Apply this diff to preserve settings when property_type is not provided:
# Convert settings dict to appropriate settings object if needed
-processed_settings: PropertySettings = None
+processed_settings: PropertySettings = settings
if settings and property_type:
prop_type = (
property_type.value if isinstance(property_type, PropertyType) else property_type
)
if prop_type == "TEXT" and isinstance(settings, dict):
processed_settings = TextAttributeSettings(**settings)
elif prop_type == "DATETIME" and isinstance(settings, dict):
processed_settings = DateAttributeSettings(**settings)
- else:
- processed_settings = settings📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Convert settings dict to appropriate settings object if needed | |
| processed_settings: PropertySettings = None | |
| if settings and property_type: | |
| prop_type = ( | |
| property_type.value if isinstance(property_type, PropertyType) else property_type | |
| ) | |
| if prop_type == "TEXT" and isinstance(settings, dict): | |
| processed_settings = TextAttributeSettings(**settings) | |
| elif prop_type == "DATETIME" and isinstance(settings, dict): | |
| processed_settings = DateAttributeSettings(**settings) | |
| else: | |
| processed_settings = settings | |
| # Convert settings dict to appropriate settings object if needed | |
| processed_settings: PropertySettings = settings | |
| if settings and property_type: | |
| prop_type = ( | |
| property_type.value if isinstance(property_type, PropertyType) else property_type | |
| ) | |
| if prop_type == "TEXT" and isinstance(settings, dict): | |
| processed_settings = TextAttributeSettings(**settings) | |
| elif prop_type == "DATETIME" and isinstance(settings, dict): | |
| processed_settings = DateAttributeSettings(**settings) |
🤖 Prompt for AI Agents
In plane_mcp/tools/work_item_properties.py around lines 203 to 214,
processed_settings is set to None unless both settings and property_type are
provided, which drops user-supplied settings when property_type is omitted;
change the logic so that if settings is provided and property_type is missing,
processed_settings is set to settings (preserving whatever object or dict was
passed), and if property_type is provided keep the existing conversion branches
(coerce to TextAttributeSettings or DateAttributeSettings when prop_type matches
and settings is a dict), otherwise fall back to assigning processed_settings =
settings.
| Args: | ||
| workspace_slug: The workspace slug identifier | ||
| project_id: UUID of the project |
There was a problem hiding this comment.
Docstring lists workspace_slug as an argument but it's not a parameter.
The docstring says workspace_slug: The workspace slug identifier but workspace_slug is obtained from get_plane_client_context(), not passed as an argument. This appears in multiple functions throughout this file. Remove these misleading docstring entries.
🤖 Prompt for AI Agents
In plane_mcp/tools/work_items.py around lines 35 to 37, the docstring
incorrectly lists workspace_slug as a parameter even though it is not passed to
the function (it’s derived from get_plane_client_context()); remove the
misleading workspace_slug entry from the docstring (and similarly from other
functions in this file that list workspace_slug but do not accept it) so
parameter docs only list actual function parameters and update any parameter
ordering/formatting to remain consistent.
Description
Add new python MCP that
Delete existing Plane MCP Server code
Type of Change
Test Scenarios
Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.