decodingtrust-agent-sdk 0.1.0__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.
- agent/__init__.py +30 -0
- agent/claudesdk/__init__.py +8 -0
- agent/claudesdk/example.py +221 -0
- agent/claudesdk/src/__init__.py +8 -0
- agent/claudesdk/src/agent.py +400 -0
- agent/claudesdk/src/mcp_proxy.py +409 -0
- agent/claudesdk/src/utils.py +420 -0
- agent/googleadk/__init__.py +15 -0
- agent/googleadk/example.py +237 -0
- agent/googleadk/src/__init__.py +12 -0
- agent/googleadk/src/agent.py +401 -0
- agent/googleadk/src/mcp_wrapper.py +163 -0
- agent/googleadk/src/utils.py +602 -0
- agent/langchain/__init__.py +8 -0
- agent/langchain/example.py +213 -0
- agent/langchain/src/__init__.py +8 -0
- agent/langchain/src/agent.py +645 -0
- agent/langchain/src/utils.py +433 -0
- agent/openaisdk/__init__.py +17 -0
- agent/openaisdk/example.py +228 -0
- agent/openaisdk/src/__init__.py +12 -0
- agent/openaisdk/src/agent.py +491 -0
- agent/openaisdk/src/agent_wrapper.py +143 -0
- agent/openaisdk/src/mcp_wrapper.py +395 -0
- agent/openaisdk/src/utils.py +493 -0
- agent/openclaw/__init__.py +10 -0
- agent/openclaw/example.py +251 -0
- agent/openclaw/src/__init__.py +14 -0
- agent/openclaw/src/agent.py +930 -0
- agent/openclaw/src/helpers/__init__.py +1 -0
- agent/openclaw/src/helpers/auth_helpers.py +55 -0
- agent/openclaw/src/mcp_proxy.py +564 -0
- agent/openclaw/src/plugin_generator.py +231 -0
- agent/openclaw/src/utils.py +341 -0
- agent/pocketflow/__init__.py +18 -0
- agent/pocketflow/example.py +221 -0
- agent/pocketflow/prompts/react_agent.py +46 -0
- agent/pocketflow/src/__init__.py +6 -0
- agent/pocketflow/src/agent.py +507 -0
- agent/pocketflow/src/agent_wrapper.py +159 -0
- agent/pocketflow/src/async_helper.py +92 -0
- agent/pocketflow/src/mcp_react_agent.py +279 -0
- agent/pocketflow/src/native_agent.py +74 -0
- agent/pocketflow/src/nodes.py +467 -0
- benchmark/__init__.py +0 -0
- benchmark/browser/benign.jsonl +34 -0
- benchmark/browser/direct.jsonl +85 -0
- benchmark/browser/indirect.jsonl +82 -0
- benchmark/code/benign.jsonl +0 -0
- benchmark/code/direct.jsonl +121 -0
- benchmark/code/indirect.jsonl +165 -0
- benchmark/crm/benign.jsonl +165 -0
- benchmark/crm/direct.jsonl +90 -0
- benchmark/crm/indirect.jsonl +150 -0
- benchmark/customer-service/benign.jsonl +160 -0
- benchmark/customer-service/direct.jsonl +100 -0
- benchmark/customer-service/indirect.jsonl +101 -0
- benchmark/finance/benign.jsonl +0 -0
- benchmark/finance/direct.jsonl +200 -0
- benchmark/finance/indirect.jsonl +200 -0
- benchmark/legal/benign.jsonl +0 -0
- benchmark/legal/direct.jsonl +200 -0
- benchmark/legal/indirect.jsonl +200 -0
- benchmark/macos/benign.jsonl +30 -0
- benchmark/macos/direct.jsonl +50 -0
- benchmark/macos/indirect.jsonl +50 -0
- benchmark/medical/benign.jsonl +642 -0
- benchmark/medical/direct.jsonl +229 -0
- benchmark/medical/indirect.jsonl +222 -0
- benchmark/os-filesystem/benign.jsonl +200 -0
- benchmark/os-filesystem/direct.jsonl +200 -0
- benchmark/os-filesystem/indirect.jsonl +200 -0
- benchmark/research/benign.jsonl +0 -0
- benchmark/research/direct.jsonl +119 -0
- benchmark/research/indirect.jsonl +125 -0
- benchmark/telecom/benign.jsonl +120 -0
- benchmark/telecom/direct.jsonl +161 -0
- benchmark/telecom/indirect.jsonl +166 -0
- benchmark/travel/benign.jsonl +130 -0
- benchmark/travel/direct.jsonl +105 -0
- benchmark/travel/indirect.jsonl +120 -0
- benchmark/windows/benign.jsonl +100 -0
- benchmark/windows/direct.jsonl +140 -0
- benchmark/windows/indirect.jsonl +107 -0
- benchmark/workflow/benign.jsonl +335 -0
- benchmark/workflow/direct.jsonl +78 -0
- benchmark/workflow/indirect.jsonl +107 -0
- cli/__init__.py +5 -0
- cli/main.py +182 -0
- cli/scaffold.py +334 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
- dt_arena/config/env.yaml +515 -0
- dt_arena/config/injection_mcp.yaml +430 -0
- dt_arena/config/mcp.yaml +642 -0
- dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
- dt_arena/envs/arxiv/docker-compose.yml +36 -0
- dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
- dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
- dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
- dt_arena/envs/atlassian/docker-compose.yml +72 -0
- dt_arena/envs/bigquery/docker-compose.yml +20 -0
- dt_arena/envs/booking/docker-compose.yml +59 -0
- dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
- dt_arena/envs/calendar/docker-compose.yml +42 -0
- dt_arena/envs/custom-website/docker-compose.yml +6 -0
- dt_arena/envs/customer_service/docker-compose.yml +59 -0
- dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
- dt_arena/envs/databricks/docker-compose.yml +51 -0
- dt_arena/envs/ecommerce/docker-compose.yml +6 -0
- dt_arena/envs/ers/docker-compose.yml +36 -0
- dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
- dt_arena/envs/finance/docker-compose.yml +23 -0
- dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
- dt_arena/envs/github/docker/docker-compose.yml +50 -0
- dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
- dt_arena/envs/gmail/docker-compose.yml +65 -0
- dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
- dt_arena/envs/google-form/docker-compose.yml +41 -0
- dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
- dt_arena/envs/googledocs/docker-compose.yml +78 -0
- dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
- dt_arena/envs/hospital/docker-compose.yml +27 -0
- dt_arena/envs/legal/docker-compose.yml +22 -0
- dt_arena/envs/linkedin/docker-compose.yml +63 -0
- dt_arena/envs/macos/docker-compose.yml +79 -0
- dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
- dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
- dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
- dt_arena/envs/paypal/docker-compose.yml +63 -0
- dt_arena/envs/research/docker-compose-hub.yml +13 -0
- dt_arena/envs/research/docker-compose.yml +24 -0
- dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
- dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
- dt_arena/envs/slack/docker-compose-hub.yml +28 -0
- dt_arena/envs/slack/docker-compose.yml +41 -0
- dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
- dt_arena/envs/snowflake/docker-compose.yml +44 -0
- dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
- dt_arena/envs/telecom/docker-compose.yml +17 -0
- dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
- dt_arena/envs/telegram/docker-compose.yml +62 -0
- dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
- dt_arena/envs/terminal/docker-compose.yml +26 -0
- dt_arena/envs/travel/docker-compose-hub.yml +19 -0
- dt_arena/envs/travel/docker-compose.yml +19 -0
- dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
- dt_arena/envs/whatsapp/docker-compose.yml +78 -0
- dt_arena/envs/windows/docker-compose.yml +71 -0
- dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
- dt_arena/envs/zoom/docker-compose.yml +40 -0
- dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
- dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
- dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
- dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
- dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
- dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
- dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
- dt_arena/injection_mcp_server/github/env_injection.py +206 -0
- dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
- dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
- dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
- dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
- dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
- dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
- dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
- dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
- dt_arena/injection_mcp_server/research/env_injection.py +616 -0
- dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
- dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
- dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
- dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
- dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
- dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
- dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
- dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
- dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
- dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
- dt_arena/mcp_server/atlassian/main.py +1554 -0
- dt_arena/mcp_server/atlassian/test_server.py +66 -0
- dt_arena/mcp_server/bigquery/main.py +333 -0
- dt_arena/mcp_server/booking/main.py +310 -0
- dt_arena/mcp_server/browser/main.py +1741 -0
- dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
- dt_arena/mcp_server/calendar/main.py +792 -0
- dt_arena/mcp_server/calendar/test_mcp.py +135 -0
- dt_arena/mcp_server/customer_service/main.py +1063 -0
- dt_arena/mcp_server/databricks/main.py +566 -0
- dt_arena/mcp_server/databricks/probe.py +102 -0
- dt_arena/mcp_server/ers/main.py +845 -0
- dt_arena/mcp_server/finance/__init__.py +87 -0
- dt_arena/mcp_server/finance/core/__init__.py +12 -0
- dt_arena/mcp_server/finance/core/data_loader.py +558 -0
- dt_arena/mcp_server/finance/core/portfolio.py +565 -0
- dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
- dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
- dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
- dt_arena/mcp_server/finance/injection/__init__.py +66 -0
- dt_arena/mcp_server/finance/injection/config.py +176 -0
- dt_arena/mcp_server/finance/injection/content.py +755 -0
- dt_arena/mcp_server/finance/injection/html.py +409 -0
- dt_arena/mcp_server/finance/injection/locations.py +167 -0
- dt_arena/mcp_server/finance/injection/methods.py +193 -0
- dt_arena/mcp_server/finance/injection/presets.py +1023 -0
- dt_arena/mcp_server/finance/main.py +361 -0
- dt_arena/mcp_server/finance/run_mcp.py +21 -0
- dt_arena/mcp_server/finance/run_web.py +26 -0
- dt_arena/mcp_server/finance/server/__init__.py +41 -0
- dt_arena/mcp_server/finance/server/extractor.py +1453 -0
- dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
- dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
- dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
- dt_arena/mcp_server/finance/server/mcp.py +451 -0
- dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
- dt_arena/mcp_server/finance/server/tools/account.py +88 -0
- dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
- dt_arena/mcp_server/finance/server/tools/social.py +73 -0
- dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
- dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
- dt_arena/mcp_server/finance/server/web.py +2139 -0
- dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
- dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
- dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
- dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
- dt_arena/mcp_server/github/main.py +441 -0
- dt_arena/mcp_server/gmail/main.py +1004 -0
- dt_arena/mcp_server/google_form/main.py +141 -0
- dt_arena/mcp_server/googledocs/main.py +458 -0
- dt_arena/mcp_server/hospital/mcp_server.py +458 -0
- dt_arena/mcp_server/legal/__init__.py +9 -0
- dt_arena/mcp_server/legal/core/__init__.py +14 -0
- dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
- dt_arena/mcp_server/legal/core/data_loader.py +266 -0
- dt_arena/mcp_server/legal/core/document_store.py +197 -0
- dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
- dt_arena/mcp_server/legal/main.py +89 -0
- dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
- dt_arena/mcp_server/legal/server/__init__.py +14 -0
- dt_arena/mcp_server/legal/server/mcp.py +2330 -0
- dt_arena/mcp_server/macos/client_test.py +270 -0
- dt_arena/mcp_server/macos/mcp_server.py +285 -0
- dt_arena/mcp_server/os-filesystem/main.py +1380 -0
- dt_arena/mcp_server/paypal/main.py +501 -0
- dt_arena/mcp_server/research/main.py +777 -0
- dt_arena/mcp_server/salesforce/main.py +2006 -0
- dt_arena/mcp_server/slack/main.py +318 -0
- dt_arena/mcp_server/snowflake/main.py +612 -0
- dt_arena/mcp_server/snowflake/probe.py +183 -0
- dt_arena/mcp_server/telecom/mcp_client.py +423 -0
- dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
- dt_arena/mcp_server/telegram/main.py +338 -0
- dt_arena/mcp_server/terminal/main.py +163 -0
- dt_arena/mcp_server/travel/client_test.py +16 -0
- dt_arena/mcp_server/travel/mcp_server.py +404 -0
- dt_arena/mcp_server/whatsapp/main.py +318 -0
- dt_arena/mcp_server/windows/client_test.py +270 -0
- dt_arena/mcp_server/windows/mcp_server.py +218 -0
- dt_arena/mcp_server/zoom/main.py +466 -0
- dt_arena/src/__init__.py +0 -0
- dt_arena/src/hooks/__init__.py +0 -0
- dt_arena/src/hooks/audit_log.py +30 -0
- dt_arena/src/hooks/hooks.json +3 -0
- dt_arena/src/run_benign.py +142 -0
- dt_arena/src/types/__init__.py +0 -0
- dt_arena/src/types/agent.py +441 -0
- dt_arena/src/types/attacks.py +2 -0
- dt_arena/src/types/environment.py +2 -0
- dt_arena/src/types/hooks.py +174 -0
- dt_arena/src/types/judge.py +52 -0
- dt_arena/src/types/red_teaming_trajectory.py +385 -0
- dt_arena/src/types/task.py +260 -0
- dt_arena/src/types/trajectory.py +315 -0
- dt_arena/utils/__init__.py +1 -0
- dt_arena/utils/atlassian/__init__.py +27 -0
- dt_arena/utils/atlassian/helpers.py +520 -0
- dt_arena/utils/bigquery/__init__.py +1 -0
- dt_arena/utils/bigquery/helpers.py +246 -0
- dt_arena/utils/calendar/__init__.py +1 -0
- dt_arena/utils/calendar/helpers.py +87 -0
- dt_arena/utils/customer_service/__init__.py +17 -0
- dt_arena/utils/customer_service/cs_env_client.py +940 -0
- dt_arena/utils/customer_service/helpers.py +339 -0
- dt_arena/utils/customer_service/judges/__init__.py +20 -0
- dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
- dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
- dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
- dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
- dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
- dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
- dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
- dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
- dt_arena/utils/customer_service/judges/text_utils.py +21 -0
- dt_arena/utils/databricks/__init__.py +2 -0
- dt_arena/utils/databricks/helpers.py +210 -0
- dt_arena/utils/finance/__init__.py +0 -0
- dt_arena/utils/finance/helpers.py +263 -0
- dt_arena/utils/github/__init__.py +1 -0
- dt_arena/utils/github/helpers.py +249 -0
- dt_arena/utils/gmail/__init__.py +1 -0
- dt_arena/utils/gmail/helpers.py +344 -0
- dt_arena/utils/google_form/__init__.py +2 -0
- dt_arena/utils/google_form/helpers.py +133 -0
- dt_arena/utils/legal/__init__.py +0 -0
- dt_arena/utils/legal/helpers.py +228 -0
- dt_arena/utils/macos/__init__.py +0 -0
- dt_arena/utils/macos/env_setup.py +215 -0
- dt_arena/utils/macos/helpers.py +61 -0
- dt_arena/utils/os_filesystem/__init__.py +1 -0
- dt_arena/utils/os_filesystem/helpers.py +366 -0
- dt_arena/utils/paypal/__init__.py +1 -0
- dt_arena/utils/paypal/helpers.py +178 -0
- dt_arena/utils/port_allocator.py +266 -0
- dt_arena/utils/research/__init__.py +0 -0
- dt_arena/utils/research/helpers.py +251 -0
- dt_arena/utils/salesforce/__init__.py +1 -0
- dt_arena/utils/salesforce/helpers.py +719 -0
- dt_arena/utils/slack/__init__.py +1 -0
- dt_arena/utils/slack/helpers.py +176 -0
- dt_arena/utils/snowflake/__init__.py +1 -0
- dt_arena/utils/snowflake/helpers.py +166 -0
- dt_arena/utils/telecom/__init__.py +1 -0
- dt_arena/utils/telecom/helpers.py +760 -0
- dt_arena/utils/telegram/__init__.py +0 -0
- dt_arena/utils/telegram/helpers.py +174 -0
- dt_arena/utils/terminal/__init__.py +0 -0
- dt_arena/utils/terminal/helpers.py +20 -0
- dt_arena/utils/travel/__init__.py +0 -0
- dt_arena/utils/travel/env_client.py +537 -0
- dt_arena/utils/travel/llm_judge.py +137 -0
- dt_arena/utils/travel/prompts.py +64 -0
- dt_arena/utils/utils/__init__.py +122 -0
- dt_arena/utils/whatsapp/__init__.py +0 -0
- dt_arena/utils/whatsapp/helpers.py +226 -0
- dt_arena/utils/windows/__init__.py +0 -0
- dt_arena/utils/windows/env_reset.py +224 -0
- dt_arena/utils/windows/env_setup.py +280 -0
- dt_arena/utils/windows/exfil_helpers.py +170 -0
- dt_arena/utils/windows/helpers.py +74 -0
- dt_arena/utils/zoom/__init__.py +1 -0
- dt_arena/utils/zoom/helpers.py +70 -0
- eval/__init__.py +1 -0
- eval/evaluation.py +426 -0
- eval/task_runner.py +449 -0
- utils/__init__.py +148 -0
- utils/agent_helpers.py +308 -0
- utils/agent_wrapper.py +189 -0
- utils/compose_utils.py +135 -0
- utils/config.py +77 -0
- utils/env_helpers.py +104 -0
- utils/eval_stats.py +88 -0
- utils/injection_helpers.py +429 -0
- utils/injection_mcp_helpers.py +152 -0
- utils/judge_helpers.py +181 -0
- utils/judge_utils.py +472 -0
- utils/llm.py +196 -0
- utils/logging.py +45 -0
- utils/mcp_helpers.py +232 -0
- utils/mcp_manager.py +235 -0
- utils/memory_guard.py +18 -0
- utils/red_teaming_sandbox.py +476 -0
- utils/reset_helpers.py +318 -0
- utils/resource_manager.py +370 -0
- utils/skill_helpers.py +447 -0
- utils/task_executor.py +904 -0
- utils/task_helpers.py +270 -0
- utils/template_helpers.py +179 -0
|
@@ -0,0 +1,1554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atlassian MCP Server (Jira + Confluence Sandbox)
|
|
3
|
+
|
|
4
|
+
This MCP server provides tools for interacting with a local Jira-like
|
|
5
|
+
issue tracking and Confluence-like page management system.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional, Dict, Any, List
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import yaml
|
|
19
|
+
except Exception:
|
|
20
|
+
yaml = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Configuration
|
|
24
|
+
ATLASSIAN_API = os.getenv("ATLASSIAN_API_URL", "http://localhost:8040")
|
|
25
|
+
DEFAULT_USER_ACCESS_TOKEN = os.getenv("USER_ACCESS_TOKEN", "")
|
|
26
|
+
# Default cloudId for sandbox environment
|
|
27
|
+
DEFAULT_CLOUD_ID = os.getenv("ATLASSIAN_CLOUD_ID", "sandbox-cloud-001")
|
|
28
|
+
|
|
29
|
+
mcp = FastMCP("Atlassian MCP Server (Jira/Confluence Sandbox)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _port_from_registry(default_port: int) -> int:
|
|
33
|
+
"""Resolve port from registry.yaml as a static fallback."""
|
|
34
|
+
try:
|
|
35
|
+
if yaml is None:
|
|
36
|
+
return default_port
|
|
37
|
+
registry_path = Path(__file__).resolve().parent.parent / "registry.yaml"
|
|
38
|
+
if not registry_path.exists():
|
|
39
|
+
return default_port
|
|
40
|
+
data = yaml.safe_load(registry_path.read_text()) or {}
|
|
41
|
+
service_name = Path(__file__).resolve().parent.name
|
|
42
|
+
for srv in (data.get("servers") or []):
|
|
43
|
+
if isinstance(srv, dict) and srv.get("name") == service_name:
|
|
44
|
+
env = srv.get("env") or {}
|
|
45
|
+
port_str = str(env.get("PORT") or "").strip().strip('"')
|
|
46
|
+
return int(port_str) if port_str else default_port
|
|
47
|
+
except Exception:
|
|
48
|
+
return default_port
|
|
49
|
+
return default_port
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_token(token: Optional[str] = None) -> str:
|
|
53
|
+
"""Resolve the access token to use."""
|
|
54
|
+
return (token or DEFAULT_USER_ACCESS_TOKEN or "").strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _headers(token: Optional[str] = None) -> Dict[str, str]:
|
|
58
|
+
"""Build request headers with authentication."""
|
|
59
|
+
resolved = _resolve_token(token)
|
|
60
|
+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
|
61
|
+
if resolved:
|
|
62
|
+
headers["Authorization"] = f"Bearer {resolved}"
|
|
63
|
+
return headers
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_list_param(val: Any) -> Optional[List[str]]:
|
|
67
|
+
"""Parse a parameter that should be a list but might be a JSON string."""
|
|
68
|
+
if val is None:
|
|
69
|
+
return None
|
|
70
|
+
if isinstance(val, list):
|
|
71
|
+
return val
|
|
72
|
+
if isinstance(val, str):
|
|
73
|
+
val = val.strip()
|
|
74
|
+
if val.startswith('['):
|
|
75
|
+
try:
|
|
76
|
+
parsed = json.loads(val)
|
|
77
|
+
if isinstance(parsed, list):
|
|
78
|
+
return parsed
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
if val:
|
|
82
|
+
return [val]
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_priority(value: Any) -> str:
|
|
87
|
+
"""Map Jira-like priority inputs to backend-supported enum values."""
|
|
88
|
+
priority = str(value or "medium").strip().lower()
|
|
89
|
+
mapping = {
|
|
90
|
+
"critical": "highest",
|
|
91
|
+
"highest": "highest",
|
|
92
|
+
"high": "high",
|
|
93
|
+
"medium": "medium",
|
|
94
|
+
"low": "low",
|
|
95
|
+
"lowest": "lowest",
|
|
96
|
+
"p1": "highest",
|
|
97
|
+
"p2": "high",
|
|
98
|
+
"p3": "medium",
|
|
99
|
+
"p4": "low",
|
|
100
|
+
"p5": "lowest",
|
|
101
|
+
}
|
|
102
|
+
return mapping.get(priority, "medium")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize_issue_reference(value: Any) -> str:
|
|
106
|
+
"""Normalize issue references for fuzzy matching."""
|
|
107
|
+
return str(value or "").strip().lower()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _issue_matches_reference(item: Dict[str, Any], reference: str) -> bool:
|
|
111
|
+
"""Check whether a search result matches an issue key/reference loosely."""
|
|
112
|
+
ref = _normalize_issue_reference(reference)
|
|
113
|
+
if not ref:
|
|
114
|
+
return False
|
|
115
|
+
key = _normalize_issue_reference(item.get("key"))
|
|
116
|
+
if key == ref:
|
|
117
|
+
return True
|
|
118
|
+
haystacks = [
|
|
119
|
+
item.get("title"),
|
|
120
|
+
item.get("summary"),
|
|
121
|
+
item.get("description"),
|
|
122
|
+
]
|
|
123
|
+
return any(ref in _normalize_issue_reference(value) for value in haystacks)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def _resolve_user_id(client: httpx.AsyncClient, user_ref: Optional[str]) -> Optional[str]:
|
|
127
|
+
"""Resolve user IDs from UUIDs, emails, or display-name-like references."""
|
|
128
|
+
raw = str(user_ref or "").strip()
|
|
129
|
+
if not raw:
|
|
130
|
+
return None
|
|
131
|
+
if _looks_like_uuid(raw):
|
|
132
|
+
return raw
|
|
133
|
+
|
|
134
|
+
for endpoint, params in (
|
|
135
|
+
(f"{ATLASSIAN_API}/api/users/search", {"query": raw}),
|
|
136
|
+
(f"{ATLASSIAN_API}/api/users", {"search": raw}),
|
|
137
|
+
):
|
|
138
|
+
r = await client.get(endpoint, params=params, headers=_headers())
|
|
139
|
+
if r.status_code != 200:
|
|
140
|
+
continue
|
|
141
|
+
payload = r.json()
|
|
142
|
+
items = payload if isinstance(payload, list) else payload.get("items", [])
|
|
143
|
+
if not isinstance(items, list):
|
|
144
|
+
continue
|
|
145
|
+
raw_lower = raw.lower()
|
|
146
|
+
for item in items:
|
|
147
|
+
if not isinstance(item, dict):
|
|
148
|
+
continue
|
|
149
|
+
item_id = item.get("id")
|
|
150
|
+
if not item_id:
|
|
151
|
+
continue
|
|
152
|
+
emails = [
|
|
153
|
+
item.get("email"),
|
|
154
|
+
item.get("emailAddress"),
|
|
155
|
+
]
|
|
156
|
+
names = [
|
|
157
|
+
item.get("displayName"),
|
|
158
|
+
item.get("name"),
|
|
159
|
+
]
|
|
160
|
+
if any(str(v or "").strip().lower() == raw_lower for v in emails + names):
|
|
161
|
+
return str(item_id)
|
|
162
|
+
if items and isinstance(items[0], dict) and items[0].get("id"):
|
|
163
|
+
return str(items[0]["id"])
|
|
164
|
+
return raw
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _looks_like_uuid(value: Optional[str]) -> bool:
|
|
168
|
+
"""Return True when a value looks like a UUID string."""
|
|
169
|
+
if not value:
|
|
170
|
+
return False
|
|
171
|
+
text = str(value).strip()
|
|
172
|
+
return len(text) == 36 and text.count("-") == 4
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def _resolve_issue_id(client: httpx.AsyncClient, issue_id_or_key: str) -> str:
|
|
176
|
+
"""Resolve Jira issue keys like ACME-1 to backend UUIDs.
|
|
177
|
+
|
|
178
|
+
The sandbox write/detail endpoints expect UUIDs, but agents frequently pass
|
|
179
|
+
issue keys. Scan paginated listings conservatively before falling back.
|
|
180
|
+
"""
|
|
181
|
+
key = str(issue_id_or_key or "").strip()
|
|
182
|
+
if not key or _looks_like_uuid(key):
|
|
183
|
+
return key
|
|
184
|
+
|
|
185
|
+
direct_search = await client.get(
|
|
186
|
+
f"{ATLASSIAN_API}/api/issues/search",
|
|
187
|
+
params={"q": key, "limit": 25, "offset": 0},
|
|
188
|
+
headers=_headers(),
|
|
189
|
+
)
|
|
190
|
+
if direct_search.status_code == 200:
|
|
191
|
+
items = direct_search.json().get("items", [])
|
|
192
|
+
for item in items:
|
|
193
|
+
if isinstance(item, dict) and _issue_matches_reference(item, key) and item.get("id"):
|
|
194
|
+
return str(item["id"])
|
|
195
|
+
|
|
196
|
+
page_size = 200
|
|
197
|
+
for offset in range(0, 1000, page_size):
|
|
198
|
+
search_r = await client.get(
|
|
199
|
+
f"{ATLASSIAN_API}/api/issues/search",
|
|
200
|
+
params={"limit": page_size, "offset": offset},
|
|
201
|
+
headers=_headers(),
|
|
202
|
+
)
|
|
203
|
+
if search_r.status_code != 200:
|
|
204
|
+
break
|
|
205
|
+
items = search_r.json().get("items", [])
|
|
206
|
+
for item in items:
|
|
207
|
+
if isinstance(item, dict) and _issue_matches_reference(item, key) and item.get("id"):
|
|
208
|
+
return str(item["id"])
|
|
209
|
+
if len(items) < page_size:
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
project_key = key.split("-", 1)[0].strip().upper()
|
|
213
|
+
if project_key:
|
|
214
|
+
project_r = await client.get(
|
|
215
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
216
|
+
params={"search": project_key, "limit": 50, "offset": 0},
|
|
217
|
+
headers=_headers(),
|
|
218
|
+
)
|
|
219
|
+
if project_r.status_code == 200:
|
|
220
|
+
for project in project_r.json().get("items", []):
|
|
221
|
+
if (project.get("key") or "").upper() != project_key:
|
|
222
|
+
continue
|
|
223
|
+
project_id = project.get("id")
|
|
224
|
+
if not project_id:
|
|
225
|
+
continue
|
|
226
|
+
for offset in range(0, 1000, page_size):
|
|
227
|
+
list_r = await client.get(
|
|
228
|
+
f"{ATLASSIAN_API}/api/issues/projects/{project_id}",
|
|
229
|
+
params={"limit": page_size, "offset": offset},
|
|
230
|
+
headers=_headers(),
|
|
231
|
+
)
|
|
232
|
+
if list_r.status_code != 200:
|
|
233
|
+
break
|
|
234
|
+
items = list_r.json().get("items", [])
|
|
235
|
+
for item in items:
|
|
236
|
+
if item.get("key") == key and item.get("id"):
|
|
237
|
+
return str(item["id"])
|
|
238
|
+
if len(items) < page_size:
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
return key
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ============================================================================
|
|
245
|
+
# General Atlassian Tools
|
|
246
|
+
# ============================================================================
|
|
247
|
+
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
async def atlassianUserInfo() -> Any:
|
|
250
|
+
"""Get current user info from Atlassian.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Current user profile information including id, display name, email
|
|
254
|
+
"""
|
|
255
|
+
async with httpx.AsyncClient() as client:
|
|
256
|
+
r = await client.get(
|
|
257
|
+
f"{ATLASSIAN_API}/api/auth/me",
|
|
258
|
+
headers=_headers()
|
|
259
|
+
)
|
|
260
|
+
r.raise_for_status()
|
|
261
|
+
return r.json()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@mcp.tool()
|
|
265
|
+
async def getAccessibleAtlassianResources() -> Any:
|
|
266
|
+
"""Get cloudId to construct API calls to Atlassian REST APIs.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of accessible resources including cloud IDs for Jira and Confluence
|
|
270
|
+
"""
|
|
271
|
+
async with httpx.AsyncClient() as client:
|
|
272
|
+
r = await client.get(
|
|
273
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
274
|
+
headers=_headers()
|
|
275
|
+
)
|
|
276
|
+
r.raise_for_status()
|
|
277
|
+
data = r.json()
|
|
278
|
+
return [{
|
|
279
|
+
"id": DEFAULT_CLOUD_ID,
|
|
280
|
+
"name": "Sandbox Atlassian",
|
|
281
|
+
"url": ATLASSIAN_API,
|
|
282
|
+
"scopes": ["read:jira-work", "write:jira-work", "read:confluence-content.all", "write:confluence-content"],
|
|
283
|
+
"avatarUrl": None
|
|
284
|
+
}]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@mcp.tool()
|
|
288
|
+
async def search(query: str) -> Any:
|
|
289
|
+
"""Search across Jira and Confluence using Rovo Search (use by default for searches).
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
query: Search query text (required)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Unified search results from Jira and Confluence
|
|
296
|
+
"""
|
|
297
|
+
results = {"query": query, "results": []}
|
|
298
|
+
|
|
299
|
+
async with httpx.AsyncClient() as client:
|
|
300
|
+
# Search Jira issues
|
|
301
|
+
r = await client.get(
|
|
302
|
+
f"{ATLASSIAN_API}/api/issues/search",
|
|
303
|
+
params={"q": query, "limit": 25},
|
|
304
|
+
headers=_headers()
|
|
305
|
+
)
|
|
306
|
+
if r.status_code == 200:
|
|
307
|
+
data = r.json()
|
|
308
|
+
for item in data.get("items", []):
|
|
309
|
+
results["results"].append({
|
|
310
|
+
"type": "jira_issue",
|
|
311
|
+
"id": item.get("id"),
|
|
312
|
+
"title": item.get("title"),
|
|
313
|
+
"key": item.get("key"),
|
|
314
|
+
"url": f"{ATLASSIAN_API}/issues/{item.get('id')}"
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
# Search Confluence pages (projects as spaces in sandbox)
|
|
318
|
+
r = await client.get(
|
|
319
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
320
|
+
params={"search": query, "limit": 25},
|
|
321
|
+
headers=_headers()
|
|
322
|
+
)
|
|
323
|
+
if r.status_code == 200:
|
|
324
|
+
data = r.json()
|
|
325
|
+
for item in data.get("items", []):
|
|
326
|
+
results["results"].append({
|
|
327
|
+
"type": "confluence_space",
|
|
328
|
+
"id": item.get("id"),
|
|
329
|
+
"title": item.get("name"),
|
|
330
|
+
"key": item.get("key"),
|
|
331
|
+
"url": f"{ATLASSIAN_API}/projects/{item.get('id')}"
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return results
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool()
|
|
338
|
+
async def fetch(id: str) -> Any:
|
|
339
|
+
"""Get details by ARI (Atlassian Resource Identifier).
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
id: Atlassian Resource Identifier (required)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Resource details (Jira issue or Confluence page)
|
|
346
|
+
"""
|
|
347
|
+
# Parse ARI to extract resource type and ID
|
|
348
|
+
parts = id.split("/")
|
|
349
|
+
resource_id = parts[-1] if parts else id
|
|
350
|
+
|
|
351
|
+
# Determine if it's a Jira issue or Confluence page
|
|
352
|
+
if "issue" in id.lower() or "jira" in id.lower():
|
|
353
|
+
return await getJiraIssue(cloudId=DEFAULT_CLOUD_ID, issueIdOrKey=resource_id)
|
|
354
|
+
elif "page" in id.lower() or "confluence" in id.lower():
|
|
355
|
+
return await getConfluencePage(cloudId=DEFAULT_CLOUD_ID, pageId=resource_id)
|
|
356
|
+
else:
|
|
357
|
+
# Try as issue first
|
|
358
|
+
try:
|
|
359
|
+
return await getJiraIssue(cloudId=DEFAULT_CLOUD_ID, issueIdOrKey=resource_id)
|
|
360
|
+
except Exception:
|
|
361
|
+
return await getConfluencePage(cloudId=DEFAULT_CLOUD_ID, pageId=resource_id)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ============================================================================
|
|
365
|
+
# Jira - Projects
|
|
366
|
+
# ============================================================================
|
|
367
|
+
|
|
368
|
+
@mcp.tool()
|
|
369
|
+
async def getVisibleJiraProjects(
|
|
370
|
+
cloudId: str,
|
|
371
|
+
searchString: Optional[str] = None,
|
|
372
|
+
action: Optional[str] = None,
|
|
373
|
+
expandIssueTypes: Optional[bool] = None,
|
|
374
|
+
startAt: Optional[int] = None,
|
|
375
|
+
maxResults: Optional[int] = None
|
|
376
|
+
) -> Any:
|
|
377
|
+
"""Get Jira projects visible to the user.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
cloudId: Cloud instance ID (required)
|
|
381
|
+
searchString: Optional search query to filter projects by name
|
|
382
|
+
action: Optional action to filter by (e.g., "view", "edit")
|
|
383
|
+
expandIssueTypes: Whether to expand issue types
|
|
384
|
+
startAt: Index of the first result to return (default: 0)
|
|
385
|
+
maxResults: Maximum number of results to return (default: 25)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
List of accessible projects with their details
|
|
389
|
+
"""
|
|
390
|
+
params = {
|
|
391
|
+
"limit": maxResults or 25,
|
|
392
|
+
"offset": startAt or 0
|
|
393
|
+
}
|
|
394
|
+
if searchString:
|
|
395
|
+
params["search"] = searchString
|
|
396
|
+
|
|
397
|
+
async with httpx.AsyncClient() as client:
|
|
398
|
+
r = await client.get(
|
|
399
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
400
|
+
params=params,
|
|
401
|
+
headers=_headers()
|
|
402
|
+
)
|
|
403
|
+
r.raise_for_status()
|
|
404
|
+
return r.json()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@mcp.tool()
|
|
408
|
+
async def getJiraProjectIssueTypesMetadata(
|
|
409
|
+
cloudId: str,
|
|
410
|
+
projectIdOrKey: str,
|
|
411
|
+
startAt: Optional[int] = None,
|
|
412
|
+
maxResults: Optional[int] = None
|
|
413
|
+
) -> Any:
|
|
414
|
+
"""Get all issue type metadata for a project.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
cloudId: Cloud instance ID (required)
|
|
418
|
+
projectIdOrKey: The UUID or key of the project (required)
|
|
419
|
+
startAt: Index of the first result to return
|
|
420
|
+
maxResults: Maximum number of results to return
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Issue type metadata including available types (story, task, bug, epic)
|
|
424
|
+
"""
|
|
425
|
+
return {
|
|
426
|
+
"projectId": projectIdOrKey,
|
|
427
|
+
"issueTypes": [
|
|
428
|
+
{"id": "story", "name": "Story", "description": "User story", "subtask": False},
|
|
429
|
+
{"id": "task", "name": "Task", "description": "A task to be done", "subtask": False},
|
|
430
|
+
{"id": "bug", "name": "Bug", "description": "A defect or issue", "subtask": False},
|
|
431
|
+
{"id": "epic", "name": "Epic", "description": "A large body of work", "subtask": False}
|
|
432
|
+
],
|
|
433
|
+
"startAt": startAt or 0,
|
|
434
|
+
"maxResults": maxResults or 50,
|
|
435
|
+
"total": 4
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@mcp.tool()
|
|
440
|
+
async def getJiraIssueTypeMetaWithFields(
|
|
441
|
+
cloudId: str,
|
|
442
|
+
projectIdOrKey: str,
|
|
443
|
+
issueTypeId: str,
|
|
444
|
+
startAt: Optional[int] = None,
|
|
445
|
+
maxResults: Optional[int] = None
|
|
446
|
+
) -> Any:
|
|
447
|
+
"""Get metadata for a specific issue type in a project.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
cloudId: Cloud instance ID (required)
|
|
451
|
+
projectIdOrKey: The UUID or key of the project (required)
|
|
452
|
+
issueTypeId: The issue type ID (required)
|
|
453
|
+
startAt: Index of the first result to return
|
|
454
|
+
maxResults: Maximum number of results to return
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Field metadata for creating issues of the specified type
|
|
458
|
+
"""
|
|
459
|
+
return {
|
|
460
|
+
"projectId": projectIdOrKey,
|
|
461
|
+
"issueTypeId": issueTypeId,
|
|
462
|
+
"fields": {
|
|
463
|
+
"summary": {"required": True, "schema": {"type": "string"}, "name": "Summary"},
|
|
464
|
+
"description": {"required": False, "schema": {"type": "string"}, "name": "Description"},
|
|
465
|
+
"priority": {"required": True, "schema": {"type": "priority"}, "name": "Priority",
|
|
466
|
+
"allowedValues": [
|
|
467
|
+
{"id": "highest", "name": "Highest"},
|
|
468
|
+
{"id": "high", "name": "High"},
|
|
469
|
+
{"id": "medium", "name": "Medium"},
|
|
470
|
+
{"id": "low", "name": "Low"},
|
|
471
|
+
{"id": "lowest", "name": "Lowest"}
|
|
472
|
+
]},
|
|
473
|
+
"assignee": {"required": False, "schema": {"type": "user"}, "name": "Assignee"},
|
|
474
|
+
"labels": {"required": False, "schema": {"type": "array", "items": "string"}, "name": "Labels"},
|
|
475
|
+
"duedate": {"required": False, "schema": {"type": "date"}, "name": "Due Date"}
|
|
476
|
+
},
|
|
477
|
+
"startAt": startAt or 0,
|
|
478
|
+
"maxResults": maxResults or 50
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@mcp.tool()
|
|
483
|
+
async def lookupJiraAccountId(cloudId: str, searchString: str) -> Any:
|
|
484
|
+
"""Look up user account IDs by display name or email.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
cloudId: Cloud instance ID (required)
|
|
488
|
+
searchString: Search query - display name or email (required)
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
List of matching user accounts
|
|
492
|
+
"""
|
|
493
|
+
async with httpx.AsyncClient() as client:
|
|
494
|
+
r = await client.get(
|
|
495
|
+
f"{ATLASSIAN_API}/api/users/search",
|
|
496
|
+
params={"query": searchString},
|
|
497
|
+
headers=_headers()
|
|
498
|
+
)
|
|
499
|
+
if r.status_code == 404:
|
|
500
|
+
# Try alternate endpoint
|
|
501
|
+
r = await client.get(
|
|
502
|
+
f"{ATLASSIAN_API}/api/users",
|
|
503
|
+
params={"search": searchString},
|
|
504
|
+
headers=_headers()
|
|
505
|
+
)
|
|
506
|
+
if r.status_code == 200:
|
|
507
|
+
return r.json()
|
|
508
|
+
return []
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ============================================================================
|
|
512
|
+
# Jira - Issues
|
|
513
|
+
# ============================================================================
|
|
514
|
+
|
|
515
|
+
@mcp.tool()
|
|
516
|
+
async def getJiraIssue(
|
|
517
|
+
cloudId: str,
|
|
518
|
+
issueIdOrKey: str,
|
|
519
|
+
expand: Optional[str] = None,
|
|
520
|
+
fields: Optional[List[str]] = None,
|
|
521
|
+
fieldsByKeys: Optional[bool] = None,
|
|
522
|
+
properties: Optional[List[str]] = None,
|
|
523
|
+
updateHistory: Optional[bool] = None,
|
|
524
|
+
failFast: Optional[bool] = None
|
|
525
|
+
) -> Any:
|
|
526
|
+
"""Get details of a Jira issue by ID or key.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
cloudId: Cloud instance ID (required)
|
|
530
|
+
issueIdOrKey: The UUID or key (e.g., PROJ-123) of the issue (required)
|
|
531
|
+
expand: Comma-separated list of fields to expand
|
|
532
|
+
fields: List of fields to return
|
|
533
|
+
fieldsByKeys: Whether fields are specified by keys
|
|
534
|
+
properties: List of properties to return
|
|
535
|
+
updateHistory: Whether to update view history
|
|
536
|
+
failFast: Whether to fail fast on errors
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Full issue details including status, assignee, comments, etc.
|
|
540
|
+
"""
|
|
541
|
+
async with httpx.AsyncClient() as client:
|
|
542
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
543
|
+
if not issue_id:
|
|
544
|
+
return {
|
|
545
|
+
"id": str(issueIdOrKey),
|
|
546
|
+
"key": str(issueIdOrKey),
|
|
547
|
+
"notFound": True,
|
|
548
|
+
"message": f"Issue '{issueIdOrKey}' not found",
|
|
549
|
+
}
|
|
550
|
+
r = await client.get(
|
|
551
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}",
|
|
552
|
+
headers=_headers()
|
|
553
|
+
)
|
|
554
|
+
if r.status_code == 404:
|
|
555
|
+
return {
|
|
556
|
+
"id": str(issueIdOrKey),
|
|
557
|
+
"key": str(issueIdOrKey),
|
|
558
|
+
"notFound": True,
|
|
559
|
+
"message": f"Issue '{issueIdOrKey}' not found",
|
|
560
|
+
}
|
|
561
|
+
r.raise_for_status()
|
|
562
|
+
return r.json()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@mcp.tool()
|
|
566
|
+
async def createJiraIssue(
|
|
567
|
+
cloudId: str,
|
|
568
|
+
projectKey: str,
|
|
569
|
+
issueTypeName: str,
|
|
570
|
+
summary: str,
|
|
571
|
+
description: Optional[str] = None,
|
|
572
|
+
assignee_account_id: Optional[str] = None,
|
|
573
|
+
parent: Optional[str] = None,
|
|
574
|
+
additional_fields: Optional[Dict[str, Any]] = None
|
|
575
|
+
) -> Any:
|
|
576
|
+
"""Creates a new Jira issue in a project.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
cloudId: Cloud instance ID (required)
|
|
580
|
+
projectKey: Project key, e.g., "PROJ" (required)
|
|
581
|
+
issueTypeName: Issue type name: story, task, bug, epic (required)
|
|
582
|
+
summary: Issue title/summary (required)
|
|
583
|
+
description: Detailed description
|
|
584
|
+
assignee_account_id: Account ID of the assignee
|
|
585
|
+
parent: Parent issue key (for subtasks)
|
|
586
|
+
additional_fields: Additional fields as object (priority, labels, dueDate, etc.)
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Created issue details
|
|
590
|
+
"""
|
|
591
|
+
async with httpx.AsyncClient() as client:
|
|
592
|
+
r = await client.get(
|
|
593
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
594
|
+
params={"search": projectKey},
|
|
595
|
+
headers=_headers()
|
|
596
|
+
)
|
|
597
|
+
r.raise_for_status()
|
|
598
|
+
projects = r.json().get("items", [])
|
|
599
|
+
project = next((p for p in projects if p.get("key") == projectKey), None)
|
|
600
|
+
if not project:
|
|
601
|
+
raise ValueError(f"Project {projectKey} not found")
|
|
602
|
+
project_id = project["id"]
|
|
603
|
+
|
|
604
|
+
# Map issue type to valid types (story, task, bug, epic)
|
|
605
|
+
issue_type = issueTypeName.lower()
|
|
606
|
+
valid_types = {"story", "task", "bug", "epic"}
|
|
607
|
+
type_mapping = {"incident": "task", "feature": "story", "improvement": "story", "subtask": "task"}
|
|
608
|
+
if issue_type not in valid_types:
|
|
609
|
+
issue_type = type_mapping.get(issue_type, "task")
|
|
610
|
+
|
|
611
|
+
body = {
|
|
612
|
+
"projectId": project_id,
|
|
613
|
+
"title": summary,
|
|
614
|
+
"type": issue_type,
|
|
615
|
+
"priority": "medium"
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if description:
|
|
619
|
+
body["description"] = description
|
|
620
|
+
if assignee_account_id:
|
|
621
|
+
resolved_assignee_id = await _resolve_user_id(client, assignee_account_id)
|
|
622
|
+
if resolved_assignee_id:
|
|
623
|
+
body["assigneeId"] = resolved_assignee_id
|
|
624
|
+
if parent:
|
|
625
|
+
body["parentIssueId"] = await _resolve_issue_id(client, parent)
|
|
626
|
+
|
|
627
|
+
if additional_fields:
|
|
628
|
+
if "priority" in additional_fields:
|
|
629
|
+
p = additional_fields["priority"]
|
|
630
|
+
if isinstance(p, dict):
|
|
631
|
+
p = p.get("name", p.get("id", "medium"))
|
|
632
|
+
body["priority"] = _normalize_priority(p)
|
|
633
|
+
if "labels" in additional_fields:
|
|
634
|
+
body["labels"] = _parse_list_param(additional_fields["labels"])
|
|
635
|
+
if "dueDate" in additional_fields:
|
|
636
|
+
body["dueDate"] = additional_fields["dueDate"]
|
|
637
|
+
if "storyPoints" in additional_fields:
|
|
638
|
+
body["storyPoints"] = additional_fields["storyPoints"]
|
|
639
|
+
|
|
640
|
+
r = await client.post(
|
|
641
|
+
f"{ATLASSIAN_API}/api/issues",
|
|
642
|
+
json=body,
|
|
643
|
+
headers=_headers()
|
|
644
|
+
)
|
|
645
|
+
r.raise_for_status()
|
|
646
|
+
return r.json()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@mcp.tool()
|
|
650
|
+
async def editJiraIssue(
|
|
651
|
+
cloudId: str,
|
|
652
|
+
issueIdOrKey: str,
|
|
653
|
+
fields: Dict[str, Any]
|
|
654
|
+
) -> Any:
|
|
655
|
+
"""Update details of an existing Jira issue.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
cloudId: Cloud instance ID (required)
|
|
659
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
660
|
+
fields: Object containing fields to update (required)
|
|
661
|
+
Supported: summary, description, priority, assignee, labels, dueDate
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
Updated issue details
|
|
665
|
+
"""
|
|
666
|
+
body = {}
|
|
667
|
+
|
|
668
|
+
# Map field names to backend format
|
|
669
|
+
field_mapping = {
|
|
670
|
+
"summary": "title",
|
|
671
|
+
"description": "description",
|
|
672
|
+
"priority": "priority",
|
|
673
|
+
"assignee": "assigneeId",
|
|
674
|
+
"labels": "labels",
|
|
675
|
+
"dueDate": "dueDate",
|
|
676
|
+
"duedate": "dueDate"
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for key, value in fields.items():
|
|
680
|
+
if key in field_mapping:
|
|
681
|
+
backend_key = field_mapping[key]
|
|
682
|
+
if key == "assignee" and isinstance(value, dict):
|
|
683
|
+
body[backend_key] = value.get("accountId") or value.get("id")
|
|
684
|
+
elif key == "labels":
|
|
685
|
+
body[backend_key] = _parse_list_param(value)
|
|
686
|
+
elif key == "priority" and isinstance(value, dict):
|
|
687
|
+
body[backend_key] = _normalize_priority(value.get("name") or value.get("id") or "medium")
|
|
688
|
+
elif key == "priority":
|
|
689
|
+
body[backend_key] = _normalize_priority(value)
|
|
690
|
+
else:
|
|
691
|
+
body[backend_key] = value
|
|
692
|
+
|
|
693
|
+
async with httpx.AsyncClient() as client:
|
|
694
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
695
|
+
r = await client.patch(
|
|
696
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}",
|
|
697
|
+
json=body,
|
|
698
|
+
headers=_headers()
|
|
699
|
+
)
|
|
700
|
+
r.raise_for_status()
|
|
701
|
+
return r.json()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@mcp.tool()
|
|
705
|
+
async def addCommentToJiraIssue(
|
|
706
|
+
cloudId: str,
|
|
707
|
+
issueIdOrKey: str,
|
|
708
|
+
commentBody: str,
|
|
709
|
+
commentVisibility: Optional[Dict[str, Any]] = None
|
|
710
|
+
) -> Any:
|
|
711
|
+
"""Adds a comment to a Jira issue.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
cloudId: Cloud instance ID (required)
|
|
715
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
716
|
+
commentBody: The comment text content (required)
|
|
717
|
+
commentVisibility: Visibility settings (optional)
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
Created comment details
|
|
721
|
+
"""
|
|
722
|
+
async with httpx.AsyncClient() as client:
|
|
723
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
724
|
+
r = await client.post(
|
|
725
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}/comments",
|
|
726
|
+
json={"body": commentBody},
|
|
727
|
+
headers=_headers()
|
|
728
|
+
)
|
|
729
|
+
r.raise_for_status()
|
|
730
|
+
return r.json()
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@mcp.tool()
|
|
734
|
+
async def addWorklogToJiraIssue(
|
|
735
|
+
cloudId: str,
|
|
736
|
+
issueIdOrKey: str,
|
|
737
|
+
timeSpent: str,
|
|
738
|
+
visibility: Optional[Dict[str, Any]] = None
|
|
739
|
+
) -> Any:
|
|
740
|
+
"""Adds a worklog entry to a Jira issue.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
cloudId: Cloud instance ID (required)
|
|
744
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
745
|
+
timeSpent: Time spent string, e.g., "2h 30m" (required)
|
|
746
|
+
visibility: Visibility settings (optional)
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
Created worklog details
|
|
750
|
+
"""
|
|
751
|
+
# Parse timeSpent to seconds
|
|
752
|
+
seconds = 0
|
|
753
|
+
time_str = timeSpent.lower()
|
|
754
|
+
if 'h' in time_str:
|
|
755
|
+
parts = time_str.split('h')
|
|
756
|
+
seconds += int(parts[0].strip()) * 3600
|
|
757
|
+
time_str = parts[1] if len(parts) > 1 else ""
|
|
758
|
+
if 'm' in time_str:
|
|
759
|
+
parts = time_str.split('m')
|
|
760
|
+
seconds += int(parts[0].strip()) * 60
|
|
761
|
+
|
|
762
|
+
async with httpx.AsyncClient() as client:
|
|
763
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
764
|
+
r = await client.post(
|
|
765
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}/worklog",
|
|
766
|
+
json={"timeSpentSeconds": seconds},
|
|
767
|
+
headers=_headers()
|
|
768
|
+
)
|
|
769
|
+
if r.status_code == 404:
|
|
770
|
+
return {"message": "Worklog added (simulated)", "timeSpent": timeSpent, "timeSpentSeconds": seconds}
|
|
771
|
+
r.raise_for_status()
|
|
772
|
+
return r.json()
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
@mcp.tool()
|
|
776
|
+
async def getTransitionsForJiraIssue(
|
|
777
|
+
cloudId: str,
|
|
778
|
+
issueIdOrKey: str,
|
|
779
|
+
expand: Optional[str] = None,
|
|
780
|
+
transitionId: Optional[str] = None,
|
|
781
|
+
skipRemoteOnlyCondition: Optional[bool] = None,
|
|
782
|
+
includeUnavailableTransitions: Optional[bool] = None,
|
|
783
|
+
sortByOpsBarAndStatus: Optional[bool] = None
|
|
784
|
+
) -> Any:
|
|
785
|
+
"""Get available transitions for a Jira issue.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
cloudId: Cloud instance ID (required)
|
|
789
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
790
|
+
expand: Comma-separated list to expand
|
|
791
|
+
transitionId: Filter by specific transition ID
|
|
792
|
+
skipRemoteOnlyCondition: Skip remote-only conditions
|
|
793
|
+
includeUnavailableTransitions: Include unavailable transitions
|
|
794
|
+
sortByOpsBarAndStatus: Sort by ops bar and status
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
List of available status transitions
|
|
798
|
+
"""
|
|
799
|
+
async with httpx.AsyncClient() as client:
|
|
800
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
801
|
+
r = await client.get(
|
|
802
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}/transitions",
|
|
803
|
+
headers=_headers()
|
|
804
|
+
)
|
|
805
|
+
r.raise_for_status()
|
|
806
|
+
data = r.json()
|
|
807
|
+
|
|
808
|
+
# Format as Jira API response
|
|
809
|
+
transitions = data if isinstance(data, list) else data.get("items", [])
|
|
810
|
+
return {
|
|
811
|
+
"expand": expand or "",
|
|
812
|
+
"transitions": transitions
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@mcp.tool()
|
|
817
|
+
async def transitionJiraIssue(
|
|
818
|
+
cloudId: str,
|
|
819
|
+
issueIdOrKey: str,
|
|
820
|
+
transition: Dict[str, Any],
|
|
821
|
+
fields: Optional[Dict[str, Any]] = None,
|
|
822
|
+
update: Optional[Dict[str, Any]] = None,
|
|
823
|
+
historyMetadata: Optional[Dict[str, Any]] = None
|
|
824
|
+
) -> Any:
|
|
825
|
+
"""Transition a Jira issue to a new status.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
cloudId: Cloud instance ID (required)
|
|
829
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
830
|
+
transition: Transition object with id field (required)
|
|
831
|
+
fields: Fields to update during transition
|
|
832
|
+
update: Update operations during transition
|
|
833
|
+
historyMetadata: Metadata for history
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Updated issue details after transition
|
|
837
|
+
"""
|
|
838
|
+
transition_id = transition.get("id") if isinstance(transition, dict) else transition
|
|
839
|
+
|
|
840
|
+
body = {"transitionId": transition_id}
|
|
841
|
+
if fields and "status" in fields:
|
|
842
|
+
body["toStatusId"] = fields["status"].get("id") if isinstance(fields["status"], dict) else fields["status"]
|
|
843
|
+
|
|
844
|
+
async with httpx.AsyncClient() as client:
|
|
845
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
846
|
+
r = await client.post(
|
|
847
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}/transitions",
|
|
848
|
+
json=body,
|
|
849
|
+
headers=_headers()
|
|
850
|
+
)
|
|
851
|
+
r.raise_for_status()
|
|
852
|
+
return r.json()
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@mcp.tool()
|
|
856
|
+
async def getJiraIssueRemoteIssueLinks(
|
|
857
|
+
cloudId: str,
|
|
858
|
+
issueIdOrKey: str,
|
|
859
|
+
globalId: Optional[str] = None
|
|
860
|
+
) -> Any:
|
|
861
|
+
"""Get remote issue links (e.g., Confluence links) for a Jira issue.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
cloudId: Cloud instance ID (required)
|
|
865
|
+
issueIdOrKey: The UUID or key of the issue (required)
|
|
866
|
+
globalId: Filter by global ID
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
List of remote links associated with the issue
|
|
870
|
+
"""
|
|
871
|
+
async with httpx.AsyncClient() as client:
|
|
872
|
+
issue_id = await _resolve_issue_id(client, issueIdOrKey)
|
|
873
|
+
r = await client.get(
|
|
874
|
+
f"{ATLASSIAN_API}/api/issues/{issue_id}/remotelinks",
|
|
875
|
+
headers=_headers()
|
|
876
|
+
)
|
|
877
|
+
if r.status_code == 404:
|
|
878
|
+
return []
|
|
879
|
+
r.raise_for_status()
|
|
880
|
+
return r.json()
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
@mcp.tool()
|
|
884
|
+
async def searchJiraIssuesUsingJql(
|
|
885
|
+
cloudId: str,
|
|
886
|
+
jql: str,
|
|
887
|
+
fields: Optional[List[str]] = None,
|
|
888
|
+
maxResults: Optional[int] = None,
|
|
889
|
+
nextPageToken: Optional[str] = None
|
|
890
|
+
) -> Any:
|
|
891
|
+
"""Search Jira issues using JQL (only use when JQL is specifically mentioned).
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
cloudId: Cloud instance ID (required)
|
|
895
|
+
jql: JQL query string (required)
|
|
896
|
+
fields: List of fields to return
|
|
897
|
+
maxResults: Maximum number of results
|
|
898
|
+
nextPageToken: Token for pagination
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Search results with matching issues
|
|
902
|
+
"""
|
|
903
|
+
import re
|
|
904
|
+
|
|
905
|
+
limit = maxResults or 25
|
|
906
|
+
|
|
907
|
+
async with httpx.AsyncClient() as client:
|
|
908
|
+
# Parse JQL to extract project key and search text
|
|
909
|
+
project_key = None
|
|
910
|
+
issue_key = None
|
|
911
|
+
search_text = None
|
|
912
|
+
|
|
913
|
+
# Match: project = KAN or project = "KAN"
|
|
914
|
+
project_match = re.search(r'project\s*=\s*["\']?(\w+)["\']?', jql, re.IGNORECASE)
|
|
915
|
+
if project_match:
|
|
916
|
+
project_key = project_match.group(1).upper()
|
|
917
|
+
|
|
918
|
+
# Match: key = KAN-1 or issue key pattern like KAN-123
|
|
919
|
+
key_match = re.search(r'(?:key\s*=\s*)?["\']?([A-Z]+-\d+)["\']?', jql, re.IGNORECASE)
|
|
920
|
+
if key_match:
|
|
921
|
+
issue_key = key_match.group(1).upper()
|
|
922
|
+
|
|
923
|
+
# Extract text search terms (after removing project/key clauses)
|
|
924
|
+
remaining = jql
|
|
925
|
+
remaining = re.sub(r'project\s*=\s*["\']?\w+["\']?', '', remaining, flags=re.IGNORECASE)
|
|
926
|
+
remaining = re.sub(r'key\s*=\s*["\']?[\w-]+["\']?', '', remaining, flags=re.IGNORECASE)
|
|
927
|
+
remaining = re.sub(r'\s*(AND|OR)\s*', ' ', remaining, flags=re.IGNORECASE)
|
|
928
|
+
# Extract quoted text or remaining words
|
|
929
|
+
quoted = re.findall(r'["\']([^"\']+)["\']', remaining)
|
|
930
|
+
if quoted:
|
|
931
|
+
search_text = ' '.join(quoted)
|
|
932
|
+
else:
|
|
933
|
+
remaining = remaining.strip()
|
|
934
|
+
if remaining and not remaining.upper().startswith(('ORDER', 'LIMIT')):
|
|
935
|
+
search_text = remaining
|
|
936
|
+
|
|
937
|
+
# If we have an issue key, try to find it directly
|
|
938
|
+
if issue_key:
|
|
939
|
+
all_issues_r = await client.get(
|
|
940
|
+
f"{ATLASSIAN_API}/api/issues/search",
|
|
941
|
+
params={"limit": 100},
|
|
942
|
+
headers=_headers()
|
|
943
|
+
)
|
|
944
|
+
if all_issues_r.status_code == 200:
|
|
945
|
+
all_data = all_issues_r.json()
|
|
946
|
+
matching = [i for i in all_data.get("items", []) if i.get("key") == issue_key]
|
|
947
|
+
if matching:
|
|
948
|
+
return {"items": matching, "total": len(matching), "limit": limit, "offset": 0}
|
|
949
|
+
|
|
950
|
+
# If we have a project key, get project ID and filter
|
|
951
|
+
project_id = None
|
|
952
|
+
if project_key:
|
|
953
|
+
projects_r = await client.get(f"{ATLASSIAN_API}/api/projects", headers=_headers())
|
|
954
|
+
if projects_r.status_code == 200:
|
|
955
|
+
projects_data = projects_r.json()
|
|
956
|
+
# Handle both list and {"items": [...]} formats
|
|
957
|
+
projects_list = projects_data.get("items", projects_data) if isinstance(projects_data, dict) else projects_data
|
|
958
|
+
for p in projects_list:
|
|
959
|
+
if p.get("key", "").upper() == project_key:
|
|
960
|
+
project_id = p.get("id")
|
|
961
|
+
break
|
|
962
|
+
|
|
963
|
+
# Build search params
|
|
964
|
+
params = {"limit": limit}
|
|
965
|
+
if project_id:
|
|
966
|
+
params["projectId"] = project_id
|
|
967
|
+
if search_text:
|
|
968
|
+
params["q"] = search_text
|
|
969
|
+
|
|
970
|
+
# If we have project but no search text, list all project issues
|
|
971
|
+
if project_id and not search_text:
|
|
972
|
+
r = await client.get(f"{ATLASSIAN_API}/api/issues", params=params, headers=_headers())
|
|
973
|
+
else:
|
|
974
|
+
# Default: use search endpoint
|
|
975
|
+
if not params.get("q"):
|
|
976
|
+
params["q"] = jql # Fallback to original JQL as search text
|
|
977
|
+
r = await client.get(f"{ATLASSIAN_API}/api/issues/search", params=params, headers=_headers())
|
|
978
|
+
|
|
979
|
+
r.raise_for_status()
|
|
980
|
+
result = r.json()
|
|
981
|
+
|
|
982
|
+
# If no results and we have a project key, try listing all project issues
|
|
983
|
+
if result.get("total", 0) == 0 and project_id:
|
|
984
|
+
r2 = await client.get(
|
|
985
|
+
f"{ATLASSIAN_API}/api/issues",
|
|
986
|
+
params={"projectId": project_id, "limit": limit},
|
|
987
|
+
headers=_headers()
|
|
988
|
+
)
|
|
989
|
+
if r2.status_code == 200:
|
|
990
|
+
result = r2.json()
|
|
991
|
+
|
|
992
|
+
return result
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
# ============================================================================
|
|
996
|
+
# Confluence - Spaces & Pages
|
|
997
|
+
# ============================================================================
|
|
998
|
+
|
|
999
|
+
@mcp.tool()
|
|
1000
|
+
async def getConfluenceSpaces(
|
|
1001
|
+
cloudId: str,
|
|
1002
|
+
ids: Optional[str] = None,
|
|
1003
|
+
keys: Optional[str] = None,
|
|
1004
|
+
type: Optional[str] = None,
|
|
1005
|
+
status: Optional[str] = None,
|
|
1006
|
+
labels: Optional[str] = None,
|
|
1007
|
+
favoritedBy: Optional[str] = None,
|
|
1008
|
+
notFavoritedBy: Optional[str] = None,
|
|
1009
|
+
sort: Optional[str] = None,
|
|
1010
|
+
descriptionFormat: Optional[str] = None,
|
|
1011
|
+
includeIcon: Optional[bool] = None,
|
|
1012
|
+
cursor: Optional[str] = None,
|
|
1013
|
+
limit: Optional[int] = None
|
|
1014
|
+
) -> Any:
|
|
1015
|
+
"""Get Confluence spaces.
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
cloudId: Cloud instance ID (required)
|
|
1019
|
+
ids: Comma-separated list of space IDs
|
|
1020
|
+
keys: Comma-separated list of space keys
|
|
1021
|
+
type: Space type filter
|
|
1022
|
+
status: Status filter
|
|
1023
|
+
labels: Labels filter
|
|
1024
|
+
favoritedBy: Favorited by user
|
|
1025
|
+
notFavoritedBy: Not favorited by user
|
|
1026
|
+
sort: Sort order
|
|
1027
|
+
descriptionFormat: Format for description
|
|
1028
|
+
includeIcon: Whether to include icon
|
|
1029
|
+
cursor: Pagination cursor
|
|
1030
|
+
limit: Maximum results
|
|
1031
|
+
|
|
1032
|
+
Returns:
|
|
1033
|
+
List of Confluence spaces
|
|
1034
|
+
"""
|
|
1035
|
+
params = {"limit": limit or 25, "offset": 0}
|
|
1036
|
+
if keys:
|
|
1037
|
+
params["search"] = keys
|
|
1038
|
+
|
|
1039
|
+
async with httpx.AsyncClient() as client:
|
|
1040
|
+
r = await client.get(
|
|
1041
|
+
f"{ATLASSIAN_API}/api/projects",
|
|
1042
|
+
params=params,
|
|
1043
|
+
headers=_headers()
|
|
1044
|
+
)
|
|
1045
|
+
r.raise_for_status()
|
|
1046
|
+
data = r.json()
|
|
1047
|
+
|
|
1048
|
+
spaces = []
|
|
1049
|
+
for item in data.get("items", []):
|
|
1050
|
+
spaces.append({
|
|
1051
|
+
"id": item.get("id"),
|
|
1052
|
+
"key": item.get("key"),
|
|
1053
|
+
"name": item.get("name"),
|
|
1054
|
+
"description": {"plain": {"value": item.get("description", "")}},
|
|
1055
|
+
"type": "global",
|
|
1056
|
+
"status": "current",
|
|
1057
|
+
"icon": None
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
return {"results": spaces, "_links": {}}
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
@mcp.tool()
|
|
1064
|
+
async def getConfluencePage(
|
|
1065
|
+
cloudId: str,
|
|
1066
|
+
pageId: str,
|
|
1067
|
+
contentFormat: Optional[str] = None
|
|
1068
|
+
) -> Any:
|
|
1069
|
+
"""Get a specific Confluence page by ID.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
cloudId: Cloud instance ID (required)
|
|
1073
|
+
pageId: The ID of the page (required)
|
|
1074
|
+
contentFormat: Content format (storage, view, atlas_doc_format)
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
Page content and metadata
|
|
1078
|
+
"""
|
|
1079
|
+
async with httpx.AsyncClient() as client:
|
|
1080
|
+
r = await client.get(
|
|
1081
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}",
|
|
1082
|
+
headers=_headers()
|
|
1083
|
+
)
|
|
1084
|
+
r.raise_for_status()
|
|
1085
|
+
data = r.json()
|
|
1086
|
+
|
|
1087
|
+
issue = data.get("issue", data)
|
|
1088
|
+
return {
|
|
1089
|
+
"id": issue.get("id"),
|
|
1090
|
+
"type": "page",
|
|
1091
|
+
"status": "current",
|
|
1092
|
+
"title": issue.get("title"),
|
|
1093
|
+
"spaceId": issue.get("project", {}).get("id"),
|
|
1094
|
+
"body": {
|
|
1095
|
+
"storage": {"value": issue.get("description", ""), "representation": "storage"},
|
|
1096
|
+
"view": {"value": issue.get("description", ""), "representation": "view"}
|
|
1097
|
+
},
|
|
1098
|
+
"version": {"number": 1, "createdAt": issue.get("createdAt")},
|
|
1099
|
+
"createdAt": issue.get("createdAt"),
|
|
1100
|
+
"authorId": issue.get("reporter", {}).get("id"),
|
|
1101
|
+
"_links": {"webui": f"/issues/{issue.get('id')}"}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
@mcp.tool()
|
|
1106
|
+
async def createConfluencePage(
|
|
1107
|
+
cloudId: str,
|
|
1108
|
+
spaceId: str,
|
|
1109
|
+
body: str,
|
|
1110
|
+
title: Optional[str] = None,
|
|
1111
|
+
parentId: Optional[str] = None,
|
|
1112
|
+
contentFormat: Optional[str] = None,
|
|
1113
|
+
subtype: Optional[str] = None,
|
|
1114
|
+
isPrivate: Optional[bool] = None
|
|
1115
|
+
) -> Any:
|
|
1116
|
+
"""Create a new page in Confluence.
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
cloudId: Cloud instance ID (required)
|
|
1120
|
+
spaceId: The ID of the space (required)
|
|
1121
|
+
body: Page content (required)
|
|
1122
|
+
title: Page title
|
|
1123
|
+
parentId: Parent page ID
|
|
1124
|
+
contentFormat: Content format
|
|
1125
|
+
subtype: Page subtype (page or live_doc)
|
|
1126
|
+
isPrivate: Whether page is private
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
Created page details
|
|
1130
|
+
"""
|
|
1131
|
+
issue_body = {
|
|
1132
|
+
"projectId": spaceId,
|
|
1133
|
+
"title": title or "Untitled Page",
|
|
1134
|
+
"description": body,
|
|
1135
|
+
"type": "task"
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if parentId:
|
|
1139
|
+
issue_body["parentIssueId"] = parentId
|
|
1140
|
+
|
|
1141
|
+
async with httpx.AsyncClient() as client:
|
|
1142
|
+
r = await client.post(
|
|
1143
|
+
f"{ATLASSIAN_API}/api/issues",
|
|
1144
|
+
json=issue_body,
|
|
1145
|
+
headers=_headers()
|
|
1146
|
+
)
|
|
1147
|
+
r.raise_for_status()
|
|
1148
|
+
data = r.json()
|
|
1149
|
+
|
|
1150
|
+
return {
|
|
1151
|
+
"id": data.get("id"),
|
|
1152
|
+
"type": subtype or "page",
|
|
1153
|
+
"status": "current",
|
|
1154
|
+
"title": data.get("title"),
|
|
1155
|
+
"spaceId": spaceId,
|
|
1156
|
+
"body": {"storage": {"value": body, "representation": "storage"}},
|
|
1157
|
+
"version": {"number": 1},
|
|
1158
|
+
"createdAt": data.get("createdAt")
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
@mcp.tool()
|
|
1163
|
+
async def updateConfluencePage(
|
|
1164
|
+
cloudId: str,
|
|
1165
|
+
pageId: str,
|
|
1166
|
+
body: str,
|
|
1167
|
+
title: Optional[str] = None,
|
|
1168
|
+
spaceId: Optional[str] = None,
|
|
1169
|
+
parentId: Optional[str] = None,
|
|
1170
|
+
status: Optional[str] = None,
|
|
1171
|
+
contentFormat: Optional[str] = None,
|
|
1172
|
+
versionMessage: Optional[str] = None
|
|
1173
|
+
) -> Any:
|
|
1174
|
+
"""Update an existing Confluence page.
|
|
1175
|
+
|
|
1176
|
+
Args:
|
|
1177
|
+
cloudId: Cloud instance ID (required)
|
|
1178
|
+
pageId: The ID of the page (required)
|
|
1179
|
+
body: Updated page content (required)
|
|
1180
|
+
title: New title
|
|
1181
|
+
spaceId: New space ID
|
|
1182
|
+
parentId: New parent page ID
|
|
1183
|
+
status: Page status
|
|
1184
|
+
contentFormat: Content format
|
|
1185
|
+
versionMessage: Version message
|
|
1186
|
+
|
|
1187
|
+
Returns:
|
|
1188
|
+
Updated page details
|
|
1189
|
+
"""
|
|
1190
|
+
update_body = {"description": body}
|
|
1191
|
+
if title:
|
|
1192
|
+
update_body["title"] = title
|
|
1193
|
+
|
|
1194
|
+
async with httpx.AsyncClient() as client:
|
|
1195
|
+
r = await client.patch(
|
|
1196
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}",
|
|
1197
|
+
json=update_body,
|
|
1198
|
+
headers=_headers()
|
|
1199
|
+
)
|
|
1200
|
+
r.raise_for_status()
|
|
1201
|
+
data = r.json()
|
|
1202
|
+
|
|
1203
|
+
return {
|
|
1204
|
+
"id": data.get("id"),
|
|
1205
|
+
"type": "page",
|
|
1206
|
+
"status": "current",
|
|
1207
|
+
"title": data.get("title"),
|
|
1208
|
+
"body": {"storage": {"value": body, "representation": "storage"}},
|
|
1209
|
+
"version": {"number": 2, "message": versionMessage},
|
|
1210
|
+
"updatedAt": data.get("updatedAt")
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
@mcp.tool()
|
|
1215
|
+
async def getPagesInConfluenceSpace(
|
|
1216
|
+
cloudId: str,
|
|
1217
|
+
spaceId: str,
|
|
1218
|
+
title: Optional[str] = None,
|
|
1219
|
+
status: Optional[str] = None,
|
|
1220
|
+
subtype: Optional[str] = None,
|
|
1221
|
+
depth: Optional[str] = None,
|
|
1222
|
+
sort: Optional[str] = None,
|
|
1223
|
+
cursor: Optional[str] = None,
|
|
1224
|
+
limit: Optional[int] = None
|
|
1225
|
+
) -> Any:
|
|
1226
|
+
"""Get all pages within a Confluence space.
|
|
1227
|
+
|
|
1228
|
+
Args:
|
|
1229
|
+
cloudId: Cloud instance ID (required)
|
|
1230
|
+
spaceId: The ID of the space (required)
|
|
1231
|
+
title: Filter by title
|
|
1232
|
+
status: Filter by status
|
|
1233
|
+
subtype: Filter by subtype
|
|
1234
|
+
depth: Depth of results
|
|
1235
|
+
sort: Sort order
|
|
1236
|
+
cursor: Pagination cursor
|
|
1237
|
+
limit: Maximum results
|
|
1238
|
+
|
|
1239
|
+
Returns:
|
|
1240
|
+
List of pages in the space
|
|
1241
|
+
"""
|
|
1242
|
+
params = {"limit": limit or 25}
|
|
1243
|
+
if title:
|
|
1244
|
+
params["search"] = title
|
|
1245
|
+
|
|
1246
|
+
async with httpx.AsyncClient() as client:
|
|
1247
|
+
r = await client.get(
|
|
1248
|
+
f"{ATLASSIAN_API}/api/issues/projects/{spaceId}",
|
|
1249
|
+
params=params,
|
|
1250
|
+
headers=_headers()
|
|
1251
|
+
)
|
|
1252
|
+
r.raise_for_status()
|
|
1253
|
+
data = r.json()
|
|
1254
|
+
|
|
1255
|
+
pages = []
|
|
1256
|
+
for item in data.get("items", []):
|
|
1257
|
+
pages.append({
|
|
1258
|
+
"id": item.get("id"),
|
|
1259
|
+
"type": "page",
|
|
1260
|
+
"status": "current",
|
|
1261
|
+
"title": item.get("title"),
|
|
1262
|
+
"spaceId": spaceId,
|
|
1263
|
+
"createdAt": item.get("createdAt")
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
return {"results": pages, "_links": {}}
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
@mcp.tool()
|
|
1270
|
+
async def getConfluencePageDescendants(
|
|
1271
|
+
cloudId: str,
|
|
1272
|
+
pageId: str,
|
|
1273
|
+
depth: Optional[int] = None,
|
|
1274
|
+
cursor: Optional[str] = None,
|
|
1275
|
+
limit: Optional[int] = None
|
|
1276
|
+
) -> Any:
|
|
1277
|
+
"""Get all child pages of a specific page.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
cloudId: Cloud instance ID (required)
|
|
1281
|
+
pageId: The ID of the parent page (required)
|
|
1282
|
+
depth: Depth of descendants to retrieve
|
|
1283
|
+
cursor: Pagination cursor
|
|
1284
|
+
limit: Maximum results
|
|
1285
|
+
|
|
1286
|
+
Returns:
|
|
1287
|
+
List of child pages
|
|
1288
|
+
"""
|
|
1289
|
+
async with httpx.AsyncClient() as client:
|
|
1290
|
+
r = await client.get(
|
|
1291
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}",
|
|
1292
|
+
headers=_headers()
|
|
1293
|
+
)
|
|
1294
|
+
r.raise_for_status()
|
|
1295
|
+
issue = r.json()
|
|
1296
|
+
|
|
1297
|
+
issue_data = issue.get("issue", issue)
|
|
1298
|
+
subtasks = issue_data.get("subtasks", [])
|
|
1299
|
+
|
|
1300
|
+
return {
|
|
1301
|
+
"results": [
|
|
1302
|
+
{
|
|
1303
|
+
"id": st.get("id"),
|
|
1304
|
+
"type": "page",
|
|
1305
|
+
"status": "current",
|
|
1306
|
+
"title": st.get("title"),
|
|
1307
|
+
"parentId": pageId
|
|
1308
|
+
}
|
|
1309
|
+
for st in subtasks
|
|
1310
|
+
],
|
|
1311
|
+
"_links": {}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
@mcp.tool()
|
|
1316
|
+
async def createConfluenceFooterComment(
|
|
1317
|
+
cloudId: str,
|
|
1318
|
+
body: str,
|
|
1319
|
+
pageId: Optional[str] = None,
|
|
1320
|
+
customContentId: Optional[str] = None,
|
|
1321
|
+
attachmentId: Optional[str] = None,
|
|
1322
|
+
parentCommentId: Optional[str] = None
|
|
1323
|
+
) -> Any:
|
|
1324
|
+
"""Create a footer comment on a Confluence page.
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
cloudId: Cloud instance ID (required)
|
|
1328
|
+
body: Comment content (required)
|
|
1329
|
+
pageId: Page ID to comment on
|
|
1330
|
+
customContentId: Custom content ID
|
|
1331
|
+
attachmentId: Attachment ID
|
|
1332
|
+
parentCommentId: Parent comment ID for replies
|
|
1333
|
+
|
|
1334
|
+
Returns:
|
|
1335
|
+
Created comment details
|
|
1336
|
+
"""
|
|
1337
|
+
target_id = pageId or customContentId or attachmentId
|
|
1338
|
+
if not target_id:
|
|
1339
|
+
raise ValueError("One of pageId, customContentId, or attachmentId is required")
|
|
1340
|
+
|
|
1341
|
+
async with httpx.AsyncClient() as client:
|
|
1342
|
+
r = await client.post(
|
|
1343
|
+
f"{ATLASSIAN_API}/api/issues/{target_id}/comments",
|
|
1344
|
+
json={"body": body},
|
|
1345
|
+
headers=_headers()
|
|
1346
|
+
)
|
|
1347
|
+
r.raise_for_status()
|
|
1348
|
+
data = r.json()
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
"id": data.get("id"),
|
|
1352
|
+
"type": "footer",
|
|
1353
|
+
"body": {"storage": {"value": body, "representation": "storage"}},
|
|
1354
|
+
"createdAt": data.get("createdAt")
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@mcp.tool()
|
|
1359
|
+
async def createConfluenceInlineComment(
|
|
1360
|
+
cloudId: str,
|
|
1361
|
+
body: str,
|
|
1362
|
+
pageId: Optional[str] = None,
|
|
1363
|
+
parentCommentId: Optional[str] = None,
|
|
1364
|
+
inlineCommentProperties: Optional[Dict[str, Any]] = None
|
|
1365
|
+
) -> Any:
|
|
1366
|
+
"""Create an inline comment on a Confluence page.
|
|
1367
|
+
|
|
1368
|
+
Args:
|
|
1369
|
+
cloudId: Cloud instance ID (required)
|
|
1370
|
+
body: Comment content (required)
|
|
1371
|
+
pageId: Page ID to comment on
|
|
1372
|
+
parentCommentId: Parent comment ID for replies
|
|
1373
|
+
inlineCommentProperties: Properties for inline positioning
|
|
1374
|
+
|
|
1375
|
+
Returns:
|
|
1376
|
+
Created inline comment details
|
|
1377
|
+
"""
|
|
1378
|
+
if not pageId:
|
|
1379
|
+
raise ValueError("pageId is required")
|
|
1380
|
+
|
|
1381
|
+
comment_body = {"body": body}
|
|
1382
|
+
if inlineCommentProperties:
|
|
1383
|
+
comment_body["textSelection"] = inlineCommentProperties.get("textSelection", "")
|
|
1384
|
+
|
|
1385
|
+
async with httpx.AsyncClient() as client:
|
|
1386
|
+
r = await client.post(
|
|
1387
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}/comments",
|
|
1388
|
+
json=comment_body,
|
|
1389
|
+
headers=_headers()
|
|
1390
|
+
)
|
|
1391
|
+
r.raise_for_status()
|
|
1392
|
+
data = r.json()
|
|
1393
|
+
|
|
1394
|
+
return {
|
|
1395
|
+
"id": data.get("id"),
|
|
1396
|
+
"type": "inline",
|
|
1397
|
+
"body": {"storage": {"value": body, "representation": "storage"}},
|
|
1398
|
+
"inlineProperties": inlineCommentProperties,
|
|
1399
|
+
"createdAt": data.get("createdAt")
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
@mcp.tool()
|
|
1404
|
+
async def getConfluencePageFooterComments(
|
|
1405
|
+
cloudId: str,
|
|
1406
|
+
pageId: str,
|
|
1407
|
+
status: Optional[str] = None,
|
|
1408
|
+
sort: Optional[str] = None,
|
|
1409
|
+
cursor: Optional[str] = None,
|
|
1410
|
+
limit: Optional[int] = None
|
|
1411
|
+
) -> Any:
|
|
1412
|
+
"""Get footer comments for a Confluence page.
|
|
1413
|
+
|
|
1414
|
+
Args:
|
|
1415
|
+
cloudId: Cloud instance ID (required)
|
|
1416
|
+
pageId: The ID of the page (required)
|
|
1417
|
+
status: Filter by status
|
|
1418
|
+
sort: Sort order
|
|
1419
|
+
cursor: Pagination cursor
|
|
1420
|
+
limit: Maximum results
|
|
1421
|
+
|
|
1422
|
+
Returns:
|
|
1423
|
+
List of footer comments
|
|
1424
|
+
"""
|
|
1425
|
+
async with httpx.AsyncClient() as client:
|
|
1426
|
+
r = await client.get(
|
|
1427
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}/comments",
|
|
1428
|
+
params={"limit": limit or 25},
|
|
1429
|
+
headers=_headers()
|
|
1430
|
+
)
|
|
1431
|
+
r.raise_for_status()
|
|
1432
|
+
data = r.json()
|
|
1433
|
+
|
|
1434
|
+
comments_list = data if isinstance(data, list) else data.get("items", [])
|
|
1435
|
+
comments = []
|
|
1436
|
+
for item in comments_list:
|
|
1437
|
+
comments.append({
|
|
1438
|
+
"id": item.get("id"),
|
|
1439
|
+
"type": "footer",
|
|
1440
|
+
"body": {"storage": {"value": item.get("body", ""), "representation": "storage"}},
|
|
1441
|
+
"authorId": item.get("author", {}).get("id"),
|
|
1442
|
+
"createdAt": item.get("createdAt")
|
|
1443
|
+
})
|
|
1444
|
+
|
|
1445
|
+
return {"results": comments, "_links": {}}
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
@mcp.tool()
|
|
1449
|
+
async def getConfluencePageInlineComments(
|
|
1450
|
+
cloudId: str,
|
|
1451
|
+
pageId: str,
|
|
1452
|
+
status: Optional[str] = None,
|
|
1453
|
+
resolutionStatus: Optional[str] = None,
|
|
1454
|
+
sort: Optional[str] = None,
|
|
1455
|
+
cursor: Optional[str] = None,
|
|
1456
|
+
limit: Optional[int] = None
|
|
1457
|
+
) -> Any:
|
|
1458
|
+
"""Get inline comments for a Confluence page.
|
|
1459
|
+
|
|
1460
|
+
Args:
|
|
1461
|
+
cloudId: Cloud instance ID (required)
|
|
1462
|
+
pageId: The ID of the page (required)
|
|
1463
|
+
status: Filter by status
|
|
1464
|
+
resolutionStatus: Filter by resolution status
|
|
1465
|
+
sort: Sort order
|
|
1466
|
+
cursor: Pagination cursor
|
|
1467
|
+
limit: Maximum results
|
|
1468
|
+
|
|
1469
|
+
Returns:
|
|
1470
|
+
List of inline comments
|
|
1471
|
+
"""
|
|
1472
|
+
async with httpx.AsyncClient() as client:
|
|
1473
|
+
r = await client.get(
|
|
1474
|
+
f"{ATLASSIAN_API}/api/issues/{pageId}/comments",
|
|
1475
|
+
params={"limit": limit or 25},
|
|
1476
|
+
headers=_headers()
|
|
1477
|
+
)
|
|
1478
|
+
r.raise_for_status()
|
|
1479
|
+
data = r.json()
|
|
1480
|
+
|
|
1481
|
+
comments_list = data if isinstance(data, list) else data.get("items", [])
|
|
1482
|
+
comments = []
|
|
1483
|
+
for item in comments_list:
|
|
1484
|
+
comments.append({
|
|
1485
|
+
"id": item.get("id"),
|
|
1486
|
+
"type": "inline",
|
|
1487
|
+
"body": {"storage": {"value": item.get("body", ""), "representation": "storage"}},
|
|
1488
|
+
"authorId": item.get("author", {}).get("id"),
|
|
1489
|
+
"createdAt": item.get("createdAt"),
|
|
1490
|
+
"inlineProperties": {"textSelection": item.get("textSelection", "")}
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
return {"results": comments, "_links": {}}
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
@mcp.tool()
|
|
1497
|
+
async def searchConfluenceUsingCql(
|
|
1498
|
+
cloudId: str,
|
|
1499
|
+
cql: str,
|
|
1500
|
+
cqlcontext: Optional[str] = None,
|
|
1501
|
+
cursor: Optional[str] = None,
|
|
1502
|
+
limit: Optional[int] = None,
|
|
1503
|
+
expand: Optional[str] = None,
|
|
1504
|
+
next: Optional[bool] = None,
|
|
1505
|
+
prev: Optional[bool] = None
|
|
1506
|
+
) -> Any:
|
|
1507
|
+
"""Search Confluence using CQL (only use when CQL is specifically mentioned).
|
|
1508
|
+
|
|
1509
|
+
Args:
|
|
1510
|
+
cloudId: Cloud instance ID (required)
|
|
1511
|
+
cql: CQL query string (required)
|
|
1512
|
+
cqlcontext: CQL context
|
|
1513
|
+
cursor: Pagination cursor
|
|
1514
|
+
limit: Maximum results
|
|
1515
|
+
expand: Fields to expand
|
|
1516
|
+
next: Get next page
|
|
1517
|
+
prev: Get previous page
|
|
1518
|
+
|
|
1519
|
+
Returns:
|
|
1520
|
+
Search results matching the CQL query
|
|
1521
|
+
"""
|
|
1522
|
+
params = {"q": cql, "limit": limit or 25}
|
|
1523
|
+
|
|
1524
|
+
async with httpx.AsyncClient() as client:
|
|
1525
|
+
r = await client.get(
|
|
1526
|
+
f"{ATLASSIAN_API}/api/issues/search",
|
|
1527
|
+
params=params,
|
|
1528
|
+
headers=_headers()
|
|
1529
|
+
)
|
|
1530
|
+
r.raise_for_status()
|
|
1531
|
+
data = r.json()
|
|
1532
|
+
|
|
1533
|
+
results = []
|
|
1534
|
+
for item in data.get("items", []):
|
|
1535
|
+
results.append({
|
|
1536
|
+
"content": {
|
|
1537
|
+
"id": item.get("id"),
|
|
1538
|
+
"type": "page",
|
|
1539
|
+
"title": item.get("title"),
|
|
1540
|
+
"status": "current"
|
|
1541
|
+
},
|
|
1542
|
+
"excerpt": item.get("description", "")[:200] if item.get("description") else ""
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
return {"results": results, "cqlQuery": cql, "_links": {}}
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
# ============================================================================
|
|
1549
|
+
# Server Entry Point
|
|
1550
|
+
# ============================================================================
|
|
1551
|
+
|
|
1552
|
+
if __name__ == "__main__":
|
|
1553
|
+
port = int(os.getenv("PORT") or _port_from_registry(8862))
|
|
1554
|
+
mcp.run(transport="http", host="0.0.0.0", port=port)
|