cacodes-mcp-server 0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. cacodes_mcp_server-0.2.9.dist-info/METADATA +424 -0
  2. cacodes_mcp_server-0.2.9.dist-info/RECORD +40 -0
  3. cacodes_mcp_server-0.2.9.dist-info/WHEEL +5 -0
  4. cacodes_mcp_server-0.2.9.dist-info/entry_points.txt +3 -0
  5. cacodes_mcp_server-0.2.9.dist-info/top_level.txt +1 -0
  6. plane_mcp/__init__.py +1 -0
  7. plane_mcp/__main__.py +178 -0
  8. plane_mcp/auth/__init__.py +7 -0
  9. plane_mcp/auth/plane_header_auth_provider.py +73 -0
  10. plane_mcp/auth/plane_oauth_provider.py +366 -0
  11. plane_mcp/aws_secrets.py +102 -0
  12. plane_mcp/client.py +74 -0
  13. plane_mcp/extensions/__init__.py +5 -0
  14. plane_mcp/extensions/templates.py +63 -0
  15. plane_mcp/instructions.py +23 -0
  16. plane_mcp/server.py +75 -0
  17. plane_mcp/storage.py +166 -0
  18. plane_mcp/tools/__init__.py +52 -0
  19. plane_mcp/tools/cycles.py +367 -0
  20. plane_mcp/tools/initiatives.py +210 -0
  21. plane_mcp/tools/intake.py +173 -0
  22. plane_mcp/tools/labels.py +154 -0
  23. plane_mcp/tools/milestones.py +202 -0
  24. plane_mcp/tools/modules.py +313 -0
  25. plane_mcp/tools/pages.py +201 -0
  26. plane_mcp/tools/pql.py +34 -0
  27. plane_mcp/tools/pql_reference.py +345 -0
  28. plane_mcp/tools/projects.py +609 -0
  29. plane_mcp/tools/states.py +177 -0
  30. plane_mcp/tools/users.py +21 -0
  31. plane_mcp/tools/work_item_activities.py +66 -0
  32. plane_mcp/tools/work_item_attachments.py +378 -0
  33. plane_mcp/tools/work_item_comments.py +188 -0
  34. plane_mcp/tools/work_item_links.py +149 -0
  35. plane_mcp/tools/work_item_properties.py +720 -0
  36. plane_mcp/tools/work_item_relations.py +117 -0
  37. plane_mcp/tools/work_item_types.py +289 -0
  38. plane_mcp/tools/work_items.py +671 -0
  39. plane_mcp/tools/work_logs.py +130 -0
  40. plane_mcp/tools/workspaces.py +86 -0
@@ -0,0 +1,424 @@
1
+ Metadata-Version: 2.4
2
+ Name: cacodes-mcp-server
3
+ Version: 0.2.9
4
+ Summary: Model Context Protocol server for Cacodes (Plane fork) integration
5
+ Author-email: CaCodes <contato@cacodes.com.br>
6
+ License: MIT
7
+ Project-URL: Homepage, https://tarefas.cacodes.com.br/docs/developers/dev-tools/mcp
8
+ Project-URL: Repository, https://github.com/Caiocesar173/plane
9
+ Project-URL: Documentation, https://tarefas.cacodes.com.br/docs/developers/dev-tools/mcp
10
+ Keywords: mcp,cacodes,plane,fastmcp,ai,automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: fastmcp==3.2.0
22
+ Requires-Dist: plane-sdk==0.2.16
23
+ Requires-Dist: py-key-value-aio[redis]<0.5.0,>=0.4.4
24
+ Requires-Dist: mcp==1.26.0
25
+ Requires-Dist: PyJWT>=2.12.0
26
+ Requires-Dist: authlib>=1.6.9
27
+ Requires-Dist: boto3>=1.34.0
28
+ Requires-Dist: fakeredis[lua]<2.35.0,>=2.32.1
29
+ Requires-Dist: httpx>=0.27.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+
34
+ # Plane MCP Server
35
+
36
+ A Model Context Protocol (MCP) server for Plane integration. This server provides tools and resources for interacting with Plane through AI agents.
37
+
38
+ ## Features
39
+
40
+ * 🔧 **Plane Integration**: Interact with Plane APIs and services
41
+ * 🔌 **Multiple Transports**: Supports stdio, SSE, and streamable HTTP transports
42
+ * 🌐 **Remote & Local**: Works both locally and as a remote service
43
+ * 🛠️ **Extensible**: Easy to add new tools and resources
44
+
45
+ ## Usage
46
+
47
+ The server supports three transport methods. **We recommend using `uvx`** as it doesn't require installation.
48
+
49
+ **Requirements**:
50
+ - **Python 3.10+** (for stdio transport, via `uvx`)
51
+ - **Node.js 22+** (for remote transports, via `npx`)
52
+
53
+ ### 1. Stdio Transport (for local use)
54
+
55
+ **MCP Client Configuration** (using uvx - recommended):
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "plane": {
61
+ "command": "uvx",
62
+ "args": ["plane-mcp-server", "stdio"],
63
+ "env": {
64
+ "PLANE_API_KEY": "<your-api-key>",
65
+ "PLANE_WORKSPACE_SLUG": "<your-workspace-slug>",
66
+ "PLANE_BASE_URL": "https://api.plane.so"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### 2. Remote HTTP Transport with OAuth
74
+
75
+ Connect to the hosted Plane MCP server using OAuth authentication.
76
+
77
+ **URL**: `https://mcp.plane.so/http/mcp`
78
+
79
+ **MCP Client Configuration** (for tools like Claude Desktop without native remote MCP support):
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "plane": {
85
+ "command": "npx",
86
+ "args": ["mcp-remote@latest", "https://mcp.plane.so/http/mcp"]
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ **Note**: OAuth authentication will be handled automatically when connecting to the remote server.
93
+
94
+ ### 3. Remote HTTP Transport using PAT Token
95
+
96
+ Connect to the hosted Plane MCP server using a Personal Access Token (PAT).
97
+
98
+ **URL**: `https://mcp.plane.so/http/api-key/mcp`
99
+
100
+ **Headers**:
101
+ - `Authorization: Bearer <PAT_TOKEN>`
102
+ - `X-Workspace-slug: <SLUG>`
103
+
104
+ **MCP Client Configuration** (for tools like Claude Desktop without native remote MCP support):
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "plane": {
110
+ "command": "npx",
111
+ "args": ["mcp-remote@latest", "https://mcp.plane.so/http/api-key/mcp"],
112
+ "headers": {
113
+ "Authorization": "Bearer <PAT_TOKEN>",
114
+ "X-Workspace-slug": "<SLUG>"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### 4. SSE Transport (Legacy)
122
+
123
+ ⚠️ **Legacy Transport**: SSE (Server-Sent Events) transport is maintained for backward compatibility. New implementations should use the HTTP transport (sections 2 or 3) instead.
124
+
125
+ Connect to the hosted Plane MCP server using OAuth authentication via Server-Sent Events.
126
+
127
+ **URL**: `https://mcp.plane.so/sse`
128
+
129
+ **MCP Client Configuration** (for tools that support SSE transport):
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "plane": {
135
+ "command": "npx",
136
+ "args": ["mcp-remote@latest", "https://mcp.plane.so/sse"]
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ **Note**: OAuth authentication will be handled automatically when connecting to the remote server. This transport is deprecated in favor of the HTTP transport.
143
+
144
+
145
+ ## Configuration
146
+
147
+ ### Authentication
148
+
149
+ The server requires authentication via environment variables:
150
+
151
+ - `PLANE_BASE_URL`: Base URL for Plane API (default: `https://api.plane.so`) - Optional
152
+ - `PLANE_API_KEY`: API key for authentication (required for stdio transport)
153
+ - `PLANE_WORKSPACE_SLUG`: Workspace slug identifier (required for stdio transport)
154
+ - `PLANE_ACCESS_TOKEN`: Access token for authentication (alternative to API key)
155
+
156
+ **Example** (for stdio transport):
157
+ ```bash
158
+ export PLANE_BASE_URL="https://api.plane.so"
159
+ export PLANE_API_KEY="your-api-key"
160
+ export PLANE_WORKSPACE_SLUG="your-workspace-slug"
161
+ ```
162
+
163
+ **Note**: For remote HTTP transports (OAuth or PAT), authentication is handled via the connection method (OAuth flow or PAT headers) and does not require these environment variables.
164
+
165
+ ## Available Tools
166
+
167
+ The server provides comprehensive tools for interacting with Plane. All tools use Pydantic models from the Plane SDK for type safety and validation.
168
+
169
+ ### Projects
170
+
171
+ | Tool Name | Description |
172
+ |-----------|-------------|
173
+ | `list_projects` | List all projects in a workspace with optional pagination and filtering |
174
+ | `create_project` | Create a new project with name, identifier, and optional configuration |
175
+ | `retrieve_project` | Retrieve a project by ID |
176
+ | `update_project` | Update a project with partial data |
177
+ | `delete_project` | Delete a project by ID |
178
+ | `get_project_worklog_summary` | Get work log summary for a project |
179
+ | `get_project_members` | Get all members of a project |
180
+ | `update_project_features` | Update features configuration of a project |
181
+
182
+ ### Work Items
183
+
184
+ | Tool Name | Description |
185
+ |-----------|-------------|
186
+ | `list_work_items` | List all work items in a project with optional filtering and pagination |
187
+ | `create_work_item` | Create a new work item with name, assignees, labels, and other attributes |
188
+ | `retrieve_work_item` | Retrieve a work item by ID with optional field expansion |
189
+ | `retrieve_work_item_by_identifier` | Retrieve a work item by project identifier and issue sequence number |
190
+ | `update_work_item` | Update a work item with partial data |
191
+ | `delete_work_item` | Delete a work item by ID |
192
+ | `search_work_items` | Search work items across a workspace with query string |
193
+
194
+ ### Cycles
195
+
196
+ | Tool Name | Description |
197
+ |-----------|-------------|
198
+ | `list_cycles` | List cycles in a project (set `archived=true` for archived) |
199
+ | `create_cycle` | Create a new cycle with name, dates, and owner |
200
+ | `retrieve_cycle` | Retrieve a cycle by ID |
201
+ | `update_cycle` | Update a cycle with partial data |
202
+ | `delete_cycle` | Delete a cycle by ID |
203
+ | `manage_cycle_work_items` | Add and/or remove work items on a cycle |
204
+ | `list_cycle_work_items` | List work items in a cycle |
205
+ | `transfer_cycle_work_items` | Transfer work items from one cycle to another |
206
+ | `manage_cycle_archive` | Archive or unarchive a cycle |
207
+
208
+ ### Modules
209
+
210
+ | Tool Name | Description |
211
+ |-----------|-------------|
212
+ | `list_modules` | List modules in a project (set `archived=true` for archived) |
213
+ | `create_module` | Create a new module with name, dates, status, and members |
214
+ | `retrieve_module` | Retrieve a module by ID |
215
+ | `update_module` | Update a module with partial data |
216
+ | `delete_module` | Delete a module by ID |
217
+ | `manage_module_work_items` | Add and/or remove work items on a module |
218
+ | `list_module_work_items` | List work items in a module |
219
+ | `manage_module_archive` | Archive or unarchive a module |
220
+
221
+ ### Initiatives
222
+
223
+ | Tool Name | Description |
224
+ |-----------|-------------|
225
+ | `list_initiatives` | List all initiatives in a workspace |
226
+ | `create_initiative` | Create a new initiative with name, dates, state, and lead |
227
+ | `retrieve_initiative` | Retrieve an initiative by ID |
228
+ | `update_initiative` | Update an initiative with partial data |
229
+ | `delete_initiative` | Delete an initiative by ID |
230
+
231
+ ### Intake Work Items
232
+
233
+ | Tool Name | Description |
234
+ |-----------|-------------|
235
+ | `list_intake_work_items` | List all intake work items in a project with optional pagination |
236
+ | `create_intake_work_item` | Create a new intake work item in a project |
237
+ | `retrieve_intake_work_item` | Retrieve an intake work item by work item ID with optional field expansion |
238
+ | `update_intake_work_item` | Update an intake work item with partial data |
239
+ | `delete_intake_work_item` | Delete an intake work item by work item ID |
240
+
241
+ ### Work Item Properties
242
+
243
+ | Tool Name | Description |
244
+ |-----------|-------------|
245
+ | `list_work_item_properties` | List work item properties for a work item type |
246
+ | `create_work_item_property` | Create a new work item property with type, settings, and validation rules |
247
+ | `retrieve_work_item_property` | Retrieve a work item property by ID |
248
+ | `update_work_item_property` | Update a work item property with partial data |
249
+ | `delete_work_item_property` | Delete a work item property by ID |
250
+
251
+ ### Milestones
252
+
253
+ | Tool Name | Description |
254
+ |-----------|-------------|
255
+ | `list_milestones` | List all milestones in a project |
256
+ | `create_milestone` | Create a new milestone |
257
+ | `retrieve_milestone` | Retrieve a milestone by ID |
258
+ | `update_milestone` | Update a milestone by ID |
259
+ | `delete_milestone` | Delete a milestone by ID |
260
+ | `manage_milestone_work_items` | Add and/or remove work items on a milestone |
261
+ | `list_milestone_work_items` | List work items in a milestone |
262
+
263
+ ### Labels
264
+
265
+ | Tool Name | Description |
266
+ |-----------|-------------|
267
+ | `list_labels` | List all labels in a project |
268
+ | `create_label` | Create a new label |
269
+ | `retrieve_label` | Retrieve a label by ID |
270
+ | `update_label` | Update a label by ID |
271
+ | `delete_label` | Delete a label by ID |
272
+
273
+ ### States
274
+
275
+ | Tool Name | Description |
276
+ |-----------|-------------|
277
+ | `list_states` | List all states in a project |
278
+ | `create_state` | Create a new state |
279
+ | `retrieve_state` | Retrieve a state by ID |
280
+ | `update_state` | Update a state by ID |
281
+ | `delete_state` | Delete a state by ID |
282
+
283
+ ### Work Item Comments
284
+
285
+ | Tool Name | Description |
286
+ |-----------|-------------|
287
+ | `list_work_item_comments` | List comments for a work item |
288
+ | `retrieve_work_item_comment` | Retrieve a specific comment for a work item |
289
+ | `create_work_item_comment` | Create a comment for a work item |
290
+ | `update_work_item_comment` | Update a comment for a work item |
291
+ | `delete_work_item_comment` | Delete a comment for a work item |
292
+
293
+ ### Work Item Links
294
+
295
+ | Tool Name | Description |
296
+ |-----------|-------------|
297
+ | `list_work_item_links` | List links for a work item |
298
+ | `retrieve_work_item_link` | Retrieve a specific link for a work item |
299
+ | `create_work_item_link` | Create a link for a work item |
300
+ | `update_work_item_link` | Update a link for a work item |
301
+ | `delete_work_item_link` | Delete a link for a work item |
302
+
303
+ ### Work Item Types
304
+
305
+ | Tool Name | Description |
306
+ |-----------|-------------|
307
+ | `list_work_item_types` | List all work item types in a project |
308
+ | `create_work_item_type` | Create a new work item type |
309
+ | `retrieve_work_item_type` | Retrieve a work item type by ID |
310
+ | `update_work_item_type` | Update a work item type by ID |
311
+ | `delete_work_item_type` | Delete a work item type by ID |
312
+ | `import_work_item_types_to_project` | Bulk-link workspace-level work item types to a project |
313
+ | `resolve_work_item_type` | Find or create a named type for a project, auto-handling workspace vs project scope and import |
314
+
315
+ ### Work Item Relations
316
+
317
+ | Tool Name | Description |
318
+ |-----------|-------------|
319
+ | `list_work_item_relations` | List relations for a work item |
320
+ | `create_work_item_relation` | Create relations for a work item |
321
+ | `remove_work_item_relation` | Remove a relation from a work item |
322
+
323
+ ### Work Item Activities
324
+
325
+ | Tool Name | Description |
326
+ |-----------|-------------|
327
+ | `list_work_item_activities` | List activities for a work item |
328
+ | `retrieve_work_item_activity` | Retrieve a specific activity for a work item |
329
+
330
+ ### Work Logs
331
+
332
+ | Tool Name | Description |
333
+ |-----------|-------------|
334
+ | `list_work_logs` | List work logs for a work item |
335
+ | `create_work_log` | Create a work log for a work item |
336
+ | `update_work_log` | Update a work log for a work item |
337
+ | `delete_work_log` | Delete a work log for a work item |
338
+
339
+ ### Pages
340
+
341
+ | Tool Name | Description |
342
+ |-----------|-------------|
343
+ | `list_pages` | List pages (workspace, or a project's if `project_id` given) |
344
+ | `retrieve_page` | Retrieve a page by ID (workspace, or project's if `project_id` given) |
345
+ | `create_page` | Create a workspace or project page |
346
+
347
+ ### Workspaces
348
+
349
+ | Tool Name | Description |
350
+ |-----------|-------------|
351
+ | `get_workspace_members` | Get all members of the current workspace |
352
+ | `get_features` | Get feature flags (workspace, or a project's if `project_id` given) |
353
+ | `update_workspace_features` | Update features of the current workspace |
354
+
355
+ ### Users
356
+
357
+ | Tool Name | Description |
358
+ |-----------|-------------|
359
+ | `get_me` | Get current authenticated user information |
360
+
361
+ **Total Tools**: 100+ tools across 20 categories
362
+
363
+ ## Development
364
+
365
+ ### Running Tests
366
+
367
+ ```bash
368
+ pytest
369
+ ```
370
+
371
+ ### Code Formatting
372
+
373
+ ```bash
374
+ black plane_mcp/
375
+ ruff check plane_mcp/
376
+ ```
377
+
378
+ ## License
379
+
380
+ MIT License - see LICENSE for details.
381
+
382
+ ## Contributing
383
+
384
+ Contributions are welcome! Please feel free to submit a Pull Request.
385
+
386
+ ## Deprecation Notice
387
+
388
+ ⚠️ **The Node.js-based `plane-mcp-server` is deprecated and no longer maintained.**
389
+
390
+ This repository represents the new Python+FastMCP based implementation of the Plane MCP server. If you were using the previous Node.js version, please migrate to this Python-based version for continued support and updates.
391
+
392
+ The new implementation offers:
393
+ - Better type safety with Pydantic models
394
+ - Improved performance with FastMCP
395
+ - Enhanced tool coverage
396
+ - Active maintenance and development
397
+
398
+ For migration assistance, please refer to the configuration examples in this README or open an issue for support.
399
+
400
+ **Old Node.js Configuration (Deprecated):**
401
+
402
+ If you were using the previous Node.js-based `@makeplane/plane-mcp-server`, your configuration looked like this:
403
+
404
+ ```json
405
+ {
406
+ "mcpServers": {
407
+ "plane": {
408
+ "command": "npx",
409
+ "args": [
410
+ "-y",
411
+ "@makeplane/plane-mcp-server"
412
+ ],
413
+ "env": {
414
+ "PLANE_API_KEY": "<YOUR_API_KEY>",
415
+ "PLANE_API_HOST_URL": "<HOST_URL_FOR_SELF_HOSTED>",
416
+ "PLANE_WORKSPACE_SLUG": "<YOUR_WORKSPACE_SLUG>"
417
+ }
418
+ }
419
+ }
420
+ }
421
+ ```
422
+
423
+ **Please migrate to the new Python-based configuration shown in the Usage section above.**
424
+
@@ -0,0 +1,40 @@
1
+ plane_mcp/__init__.py,sha256=FiUOxQD6H_F6Aty5HrXCvd_aZJZcVjLkqDAxlSMfnrA,80
2
+ plane_mcp/__main__.py,sha256=NzDCOi4n4M5hRKNejHLlx-WOXUlfV2-Un5l39Yqm-54,6061
3
+ plane_mcp/aws_secrets.py,sha256=s0_doZeOGxAEdZTDYU8qNl8VztSVtzsHIpZBUvdbkNM,3098
4
+ plane_mcp/client.py,sha256=kJMsZzAFeAzKtfg4FT70w6VNUAWiC7oEqLwVmSheCIc,2513
5
+ plane_mcp/instructions.py,sha256=r5wm6IA-LzTWcNmUbUCD_ChvNuJxQBfvh1fDvSANeiw,1261
6
+ plane_mcp/server.py,sha256=4J-9c8Z-G5XZ-fTxqD94BGmXtgCX-p7_TIWBt0gu_Vs,2809
7
+ plane_mcp/storage.py,sha256=StpA72xZ1schJfzgR7LD7MDBDzB8HgUmIkymbyPfJUU,5993
8
+ plane_mcp/auth/__init__.py,sha256=K7w0oX7nHjDwA5JtKr_ulqE15yF9xLibb1S8n0gdm-g,217
9
+ plane_mcp/auth/plane_header_auth_provider.py,sha256=hnH3AreCZeOwM2QEJc_-SUH-UwBkjSwBn6z-7D-l1Fk,2829
10
+ plane_mcp/auth/plane_oauth_provider.py,sha256=4G8JgFdztQSw60j3uOD3JyrKnJm4ZW1dIXFvnVqqirA,14714
11
+ plane_mcp/extensions/__init__.py,sha256=FV5lR_qzXYwv-yyuPDQPNG5xTDUfVsrzkvjC1NJg5bI,146
12
+ plane_mcp/extensions/templates.py,sha256=z2nCE-mwNF-M0ASB_Ml3BnVnAJdFNFy2PX_TwU9KQBA,2023
13
+ plane_mcp/tools/__init__.py,sha256=3rQ52JDmAWkR1vmCWmksL6_YRxnSGh5xn1t2gX8vznM,2376
14
+ plane_mcp/tools/cycles.py,sha256=FJt3cLjPB6MIHLo-3LleZuxPhvc3w75Fe009wcTh2vc,12857
15
+ plane_mcp/tools/initiatives.py,sha256=vhe2GIsP7QQhWR5kMCObzbs2wCMonli2nZGMnaxi-mY,7850
16
+ plane_mcp/tools/intake.py,sha256=t8Wsdc9fNF190pWxwdQthOf4SGkNrrlMkV7VvKSqThQ,5988
17
+ plane_mcp/tools/labels.py,sha256=k8i-61JqUHjGL1YaShAJuO1caC5_kfJqzNV40PVSejw,4528
18
+ plane_mcp/tools/milestones.py,sha256=npIB41mhzbsQpaYhchjgFzzauGm4b7aO5xUDcGt3FSE,6299
19
+ plane_mcp/tools/modules.py,sha256=f2QkwEFOpgzmNP45brnsHwdcxg3f6BFmxi91d4Fo9ck,11368
20
+ plane_mcp/tools/pages.py,sha256=JMwnwjXrYGT1qj2u9WFPLtmLdaAj_ZDF6STy3NzL8YE,6202
21
+ plane_mcp/tools/pql.py,sha256=WWB3QgskCPfDkTDsw481OaAYUpaa3ZY1H7IvFs_av_Q,1216
22
+ plane_mcp/tools/pql_reference.py,sha256=HID-tI4mc_msfTu7mm5hnR2fqBNawqSwguS6DEk_4nM,16878
23
+ plane_mcp/tools/projects.py,sha256=8Rb0nm1n0Mbl5fAFSAMgQVRTwPme4tS35-ix7UxctxI,21196
24
+ plane_mcp/tools/states.py,sha256=D6-U124AtlheHrxnYFAv9amFMi--YNGVQulExaCRac8,5499
25
+ plane_mcp/tools/users.py,sha256=0FCnV9IsFFsVGFPpTuJAsIpQC6Zlgj0BD2xaXhwxwzc,571
26
+ plane_mcp/tools/work_item_activities.py,sha256=8aBQdcyeXMVQ7aBmexpWrhb3vqysHNF9z2d47GQT0v8,1947
27
+ plane_mcp/tools/work_item_attachments.py,sha256=3XE1sNxaVDd_ZAPKACTBM6r0Du3Jln7hW770r4lf2nc,14965
28
+ plane_mcp/tools/work_item_comments.py,sha256=wmOcut3-eS2G5jWTgnpku775n6y5GYB8g4u9NqfL4NU,5925
29
+ plane_mcp/tools/work_item_links.py,sha256=v2nJXRNdrfpVynQtOEv7bte87vqfk057lsrQNObs34U,4027
30
+ plane_mcp/tools/work_item_properties.py,sha256=KY8AOzWpc35dCTLsS8a0lwMdputbV7PTcsVHA6ptStU,27834
31
+ plane_mcp/tools/work_item_relations.py,sha256=06v4eM0UXZv0UrCn6GaRueTMKIoQ7oWymPcvkxqui1E,4296
32
+ plane_mcp/tools/work_item_types.py,sha256=PVEP9GxBC44UF8KCSdbbTzVK1BbkJEfzSJz9o1wdz2s,10787
33
+ plane_mcp/tools/work_items.py,sha256=01vcv2-5eycsAuyrJbP-AEkhN-uJYv6Wc3ItVtysilc,26522
34
+ plane_mcp/tools/work_logs.py,sha256=8rvoLS-mH6sv3uJV-22uO_mI_g2rY2UNBZ9MpFHsV4Q,3778
35
+ plane_mcp/tools/workspaces.py,sha256=GNeN5gv24iux8a9BtfzcrrIWeMjFmzNbPIkmrtrcn8E,3069
36
+ cacodes_mcp_server-0.2.9.dist-info/METADATA,sha256=d_DAuuZO2zguZyHfn03M9xUNDiLveOBD9_hvroOLw1w,14711
37
+ cacodes_mcp_server-0.2.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
38
+ cacodes_mcp_server-0.2.9.dist-info/entry_points.txt,sha256=HRwdvQ3Hao-HN4LkKvbUPpTzCuujC_xNnBZDa7iDzzc,106
39
+ cacodes_mcp_server-0.2.9.dist-info/top_level.txt,sha256=HlNwe99scg9OD2UWbDrcLlew2X3LEvUjvvoBiOW1w1M,10
40
+ cacodes_mcp_server-0.2.9.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cacodes-mcp-server = plane_mcp.__main__:main
3
+ plane-mcp-server = plane_mcp.__main__:main
@@ -0,0 +1 @@
1
+ plane_mcp
plane_mcp/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Plane MCP Server - A Model Context Protocol server for Plane integration."""
plane_mcp/__main__.py ADDED
@@ -0,0 +1,178 @@
1
+ """Main entry point for the Plane MCP Server."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ from contextlib import asynccontextmanager
8
+ from datetime import datetime, timezone
9
+ from enum import Enum
10
+
11
+ import uvicorn
12
+ from fastmcp.server.dependencies import get_access_token
13
+ from starlette.applications import Starlette
14
+ from starlette.middleware.cors import CORSMiddleware
15
+ from starlette.routing import Mount
16
+
17
+ from plane_mcp.server import get_header_mcp, get_oauth_mcp, get_stdio_mcp
18
+
19
+
20
+ class UserContextFilter(logging.Filter):
21
+ """Attach the authenticated user's id to every log record.
22
+
23
+ Pulls the current request's access token via FastMCP's dependency, which
24
+ returns None (never raises) outside a request context — so startup logs and
25
+ stdio mode simply carry no user info. Only the opaque user id is recorded;
26
+ PII such as the display name / email is intentionally never logged.
27
+ """
28
+
29
+ def filter(self, record: logging.LogRecord) -> bool:
30
+ user_id = None
31
+ try:
32
+ token = get_access_token()
33
+ if token:
34
+ user_id = token.claims.get("sub")
35
+ except Exception as exc:
36
+ # Never let logging enrichment break a request, but leave a signal.
37
+ record.user_context_enrichment_error = type(exc).__name__
38
+ record.user_id = user_id
39
+ return True
40
+
41
+
42
+ class JSONFormatter(logging.Formatter):
43
+ """JSON log formatter for structured logging (Datadog, ELK, etc.)."""
44
+
45
+ def format(self, record: logging.LogRecord) -> str:
46
+ log_entry = {
47
+ "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
48
+ "level": record.levelname,
49
+ "logger": record.name,
50
+ "message": record.getMessage(),
51
+ }
52
+ user_id = getattr(record, "user_id", None)
53
+ if user_id:
54
+ log_entry["user_id"] = user_id
55
+ err = getattr(record, "user_context_enrichment_error", None)
56
+ if err:
57
+ log_entry["user_context_enrichment_error"] = err
58
+ if record.exc_info and record.exc_info[1]:
59
+ log_entry["error"] = {
60
+ "type": type(record.exc_info[1]).__name__,
61
+ "message": str(record.exc_info[1]),
62
+ }
63
+ return json.dumps(log_entry)
64
+
65
+
66
+ def configure_json_logging():
67
+ """Replace FastMCP's Rich handlers with a JSON formatter on the fastmcp logger."""
68
+ fastmcp_logger = logging.getLogger("fastmcp")
69
+
70
+ # Remove all existing handlers (Rich)
71
+ for handler in fastmcp_logger.handlers[:]:
72
+ fastmcp_logger.removeHandler(handler)
73
+
74
+ handler = logging.StreamHandler(sys.stderr)
75
+ handler.setFormatter(JSONFormatter())
76
+ handler.addFilter(UserContextFilter())
77
+ fastmcp_logger.addHandler(handler)
78
+ fastmcp_logger.setLevel(logging.INFO)
79
+ fastmcp_logger.propagate = False
80
+
81
+
82
+ configure_json_logging()
83
+
84
+ logger = logging.getLogger("fastmcp.plane_mcp")
85
+
86
+
87
+ class ServerMode(Enum):
88
+ STDIO = "stdio"
89
+ SSE = "sse"
90
+ HTTP = "http"
91
+
92
+
93
+ @asynccontextmanager
94
+ async def combined_lifespan(oauth_app, header_app, sse_app):
95
+ """Combine lifespans from both OAuth and Header MCP apps."""
96
+ # Start both lifespans
97
+ async with oauth_app.lifespan(oauth_app):
98
+ async with header_app.lifespan(header_app):
99
+ async with sse_app.lifespan(sse_app):
100
+ yield
101
+
102
+
103
+ def main() -> None:
104
+ """Run the MCP server."""
105
+ server_mode = ServerMode.STDIO
106
+ if len(sys.argv) > 1:
107
+ server_mode = ServerMode(sys.argv[1])
108
+
109
+ if server_mode == ServerMode.STDIO:
110
+ # Validate API_KEY and PLANE_WORKSPACE_SLUG are set
111
+ if not os.getenv("PLANE_API_KEY"):
112
+ raise ValueError("PLANE_API_KEY is not set")
113
+ if not os.getenv("PLANE_WORKSPACE_SLUG"):
114
+ os.environ["PLANE_WORKSPACE_SLUG"] = "cacodes"
115
+
116
+ get_stdio_mcp().run()
117
+ return
118
+
119
+ if server_mode == ServerMode.HTTP:
120
+ prefix = os.getenv("MCP_PATH_PREFIX") or ""
121
+
122
+ oauth_mcp = get_oauth_mcp(prefix + "/http")
123
+ oauth_app = oauth_mcp.http_app(stateless_http=True)
124
+ header_app = get_header_mcp().http_app(stateless_http=True)
125
+
126
+ sse_mcp = get_oauth_mcp(prefix)
127
+ sse_app = sse_mcp.http_app(transport="sse")
128
+
129
+ # mcp_path is appended to the auth provider's base_url to form the
130
+ # advertised resource URL. base_url already carries the prefix, so these
131
+ # stay at /mcp and /sse to avoid double-prefixing.
132
+ oauth_well_known = oauth_mcp.auth.get_well_known_routes(mcp_path="/mcp")
133
+ sse_well_known = sse_mcp.auth.get_well_known_routes(mcp_path="/sse")
134
+
135
+ app = Starlette(
136
+ routes=[
137
+ # Well-known routes for OAuth and Header HTTP
138
+ *oauth_well_known,
139
+ *sse_well_known,
140
+ # Mount both MCP servers
141
+ Mount(prefix + "/http/api-key", app=header_app),
142
+ Mount(prefix + "/http", app=oauth_app),
143
+ Mount(prefix or "/", app=sse_app),
144
+ ],
145
+ lifespan=lambda app: combined_lifespan(oauth_app, header_app, sse_app),
146
+ )
147
+
148
+ app.add_middleware(
149
+ CORSMiddleware,
150
+ allow_origins=["*"],
151
+ allow_credentials=False,
152
+ allow_methods=["*"],
153
+ allow_headers=["*"],
154
+ )
155
+
156
+ # Configure uvicorn loggers to use JSON formatting too
157
+ for uv_logger_name in ("uvicorn", "uvicorn.error"):
158
+ uv_logger = logging.getLogger(uv_logger_name)
159
+ for h in uv_logger.handlers[:]:
160
+ uv_logger.removeHandler(h)
161
+ uv_handler = logging.StreamHandler(sys.stderr)
162
+ uv_handler.setFormatter(JSONFormatter())
163
+ uv_handler.addFilter(UserContextFilter())
164
+ uv_logger.addHandler(uv_handler)
165
+
166
+ logger.info("Starting HTTP server at URLs: /mcp and /header/mcp")
167
+ uvicorn.run(
168
+ app,
169
+ host="0.0.0.0",
170
+ port=8211,
171
+ log_level="info",
172
+ access_log=False,
173
+ )
174
+ return
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()
@@ -0,0 +1,7 @@
1
+ from plane_mcp.auth.plane_header_auth_provider import PlaneHeaderAuthProvider
2
+ from plane_mcp.auth.plane_oauth_provider import PlaneOAuthProvider
3
+
4
+ __all__ = [
5
+ "PlaneHeaderAuthProvider",
6
+ "PlaneOAuthProvider",
7
+ ]