flock-core 0.4.539__py3-none-any.whl → 0.4.541__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/core/logging/live_capture.py +137 -0
- flock/tools/zendesk_tools.py +271 -46
- flock/webapp/app/api/agent_management.py +8 -12
- flock/webapp/app/api/execution.py +4 -8
- flock/webapp/app/api/flock_management.py +4 -8
- flock/webapp/app/api/registry_viewer.py +1 -2
- flock/webapp/app/chat.py +14 -17
- flock/webapp/app/main.py +84 -58
- flock/webapp/app/theme_mapper.py +1 -2
- flock/webapp/run.py +4 -0
- flock/webapp/static/css/layout.css +239 -4
- flock/webapp/templates/base.html +192 -3
- flock/webapp/templates/partials/_live_logs.html +13 -0
- {flock_core-0.4.539.dist-info → flock_core-0.4.541.dist-info}/METADATA +1 -1
- {flock_core-0.4.539.dist-info → flock_core-0.4.541.dist-info}/RECORD +18 -16
- {flock_core-0.4.539.dist-info → flock_core-0.4.541.dist-info}/WHEEL +0 -0
- {flock_core-0.4.539.dist-info → flock_core-0.4.541.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.539.dist-info → flock_core-0.4.541.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from collections import deque
|
|
9
|
+
from typing import Deque, List, Literal, MutableMapping
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"enable_live_log_capture",
|
|
13
|
+
"get_live_log_store",
|
|
14
|
+
"LiveLogStore",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
AnsiSource = Literal["stdout", "stderr"]
|
|
18
|
+
ANSI_ESCAPE_PATTERN = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LiveLogStore:
|
|
22
|
+
"""Thread-safe ring buffer that keeps recent CLI log lines."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, max_lines: int = 800) -> None:
|
|
25
|
+
self._lines: Deque[dict[str, object]] = deque(maxlen=max_lines)
|
|
26
|
+
self._buffers: MutableMapping[AnsiSource, str] = {"stdout": "", "stderr": ""}
|
|
27
|
+
self._lock = threading.Lock()
|
|
28
|
+
|
|
29
|
+
def append_chunk(self, chunk: str, source: AnsiSource) -> None:
|
|
30
|
+
"""Append raw stream data, splitting into sanitized log lines."""
|
|
31
|
+
if not chunk:
|
|
32
|
+
return
|
|
33
|
+
normalized = chunk.replace("\r\n", "\n").replace("\r", "\n")
|
|
34
|
+
with self._lock:
|
|
35
|
+
combined = self._buffers[source] + normalized
|
|
36
|
+
parts = combined.split("\n")
|
|
37
|
+
if combined.endswith("\n"):
|
|
38
|
+
complete, remainder = parts[:-1], ""
|
|
39
|
+
else:
|
|
40
|
+
complete, remainder = parts[:-1], parts[-1]
|
|
41
|
+
self._buffers[source] = remainder
|
|
42
|
+
|
|
43
|
+
timestamp = time.time()
|
|
44
|
+
for raw_line in complete:
|
|
45
|
+
cleaned = ANSI_ESCAPE_PATTERN.sub("", raw_line).rstrip("\r")
|
|
46
|
+
# Preserve deliberate blank lines but normalise whitespace-only strings
|
|
47
|
+
if cleaned and cleaned.strip() == "":
|
|
48
|
+
cleaned = ""
|
|
49
|
+
self._lines.append(
|
|
50
|
+
{
|
|
51
|
+
"timestamp": timestamp,
|
|
52
|
+
"stream": source,
|
|
53
|
+
"text": cleaned,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
timestamp += 1e-6 # keep ordering stable for same chunk
|
|
57
|
+
|
|
58
|
+
def get_entries(self, *, limit: int | None = None) -> List[dict[str, object]]:
|
|
59
|
+
"""Return a copy of the most recent log entries (oldest first)."""
|
|
60
|
+
with self._lock:
|
|
61
|
+
snapshot = list(self._lines)
|
|
62
|
+
if limit is None:
|
|
63
|
+
return snapshot
|
|
64
|
+
if limit <= 0:
|
|
65
|
+
return []
|
|
66
|
+
return snapshot[-limit:]
|
|
67
|
+
|
|
68
|
+
def clear(self) -> None:
|
|
69
|
+
"""Clear buffered log lines and pending chunks."""
|
|
70
|
+
with self._lock:
|
|
71
|
+
self._lines.clear()
|
|
72
|
+
self._buffers = {"stdout": "", "stderr": ""}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _TeeStream(io.TextIOBase):
|
|
76
|
+
"""Duplicate writes to the original stream and the live log store."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, wrapped: io.TextIOBase, source: AnsiSource, store: LiveLogStore) -> None:
|
|
79
|
+
self._wrapped = wrapped
|
|
80
|
+
self._source = source
|
|
81
|
+
self._store = store
|
|
82
|
+
|
|
83
|
+
def write(self, data: str) -> int: # type: ignore[override]
|
|
84
|
+
self._store.append_chunk(data, self._source)
|
|
85
|
+
return self._wrapped.write(data)
|
|
86
|
+
|
|
87
|
+
def flush(self) -> None: # type: ignore[override]
|
|
88
|
+
self._wrapped.flush()
|
|
89
|
+
|
|
90
|
+
def isatty(self) -> bool: # type: ignore[override]
|
|
91
|
+
return self._wrapped.isatty()
|
|
92
|
+
|
|
93
|
+
def fileno(self) -> int: # type: ignore[override]
|
|
94
|
+
return self._wrapped.fileno()
|
|
95
|
+
|
|
96
|
+
def readable(self) -> bool: # type: ignore[override]
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def writable(self) -> bool: # type: ignore[override]
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def seekable(self) -> bool: # type: ignore[override]
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def close(self) -> None: # type: ignore[override]
|
|
106
|
+
self._wrapped.close()
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def closed(self) -> bool: # type: ignore[override]
|
|
110
|
+
return self._wrapped.closed
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def encoding(self) -> str | None: # type: ignore[override]
|
|
114
|
+
return getattr(self._wrapped, "encoding", None)
|
|
115
|
+
|
|
116
|
+
def __getattr__(self, item: str):
|
|
117
|
+
return getattr(self._wrapped, item)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
_live_log_store = LiveLogStore()
|
|
121
|
+
_capture_enabled = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def enable_live_log_capture() -> None:
|
|
125
|
+
"""Wrap stdout/stderr so CLI output is mirrored into the live log store."""
|
|
126
|
+
global _capture_enabled
|
|
127
|
+
if _capture_enabled:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
sys.stdout = _TeeStream(sys.stdout, "stdout", _live_log_store)
|
|
131
|
+
sys.stderr = _TeeStream(sys.stderr, "stderr", _live_log_store)
|
|
132
|
+
_capture_enabled = True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_live_log_store() -> LiveLogStore:
|
|
136
|
+
"""Expose the singleton live log store for dependency injection."""
|
|
137
|
+
return _live_log_store
|
flock/tools/zendesk_tools.py
CHANGED
|
@@ -5,17 +5,27 @@ import os
|
|
|
5
5
|
import httpx
|
|
6
6
|
from mcp.server.fastmcp import FastMCP
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
from flock.core.logging.logging import get_logger
|
|
11
9
|
|
|
10
|
+
mcp = FastMCP("ZendeskTools")
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
12
|
|
|
13
13
|
def _get_headers() -> dict:
|
|
14
|
+
logger.debug("Preparing headers for Zendesk API request")
|
|
15
|
+
|
|
14
16
|
token = os.getenv("ZENDESK_BEARER_TOKEN")
|
|
15
17
|
if not token:
|
|
18
|
+
logger.error("ZENDESK_BEARER_TOKEN environment variable is not set")
|
|
16
19
|
raise ValueError(
|
|
17
20
|
"ZENDESK_BEARER_TOKEN environment variable is not set"
|
|
18
21
|
)
|
|
22
|
+
|
|
23
|
+
logger.debug("Successfully retrieved bearer token from environment")
|
|
24
|
+
# Log a masked version of the token for debugging
|
|
25
|
+
masked_token = f"{token[:10]}...{token[-4:] if len(token) > 14 else 'short'}"
|
|
26
|
+
logger.debug(f"Using bearer token: {masked_token}")
|
|
27
|
+
logger.debug("Headers prepared successfully")
|
|
28
|
+
|
|
19
29
|
return {
|
|
20
30
|
"Authorization": f"Bearer {token}",
|
|
21
31
|
"Accept": "application/json",
|
|
@@ -25,69 +35,169 @@ def _get_headers() -> dict:
|
|
|
25
35
|
@mcp.tool()
|
|
26
36
|
def zendesk_get_tickets(number_of_tickets: int = 10) -> list[dict]:
|
|
27
37
|
"""Get all tickets."""
|
|
38
|
+
logger.info(f"Starting zendesk_get_tickets with number_of_tickets: {number_of_tickets}")
|
|
39
|
+
|
|
28
40
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
41
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
42
|
+
|
|
29
43
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
30
44
|
url = f"{BASE_URL}/api/v2/tickets.json"
|
|
45
|
+
logger.debug(f"Initial URL: {url}")
|
|
46
|
+
|
|
31
47
|
all_tickets = []
|
|
48
|
+
page_count = 0
|
|
49
|
+
|
|
32
50
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
51
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
52
|
+
|
|
33
53
|
while url and len(all_tickets) < number_of_tickets:
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
page_count += 1
|
|
55
|
+
logger.debug(f"Fetching page {page_count} from URL: {url}")
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
try:
|
|
58
|
+
response = client.get(url)
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
data = response.json()
|
|
63
|
+
tickets = data.get("tickets", [])
|
|
64
|
+
logger.debug(f"Retrieved {len(tickets)} tickets from page {page_count}")
|
|
65
|
+
|
|
66
|
+
all_tickets.extend(tickets)
|
|
67
|
+
logger.debug(f"Total tickets collected so far: {len(all_tickets)}")
|
|
68
|
+
|
|
69
|
+
url = data.get("next_page")
|
|
70
|
+
if url:
|
|
71
|
+
logger.debug(f"Next page URL: {url}")
|
|
72
|
+
else:
|
|
73
|
+
logger.debug("No more pages available")
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Error fetching tickets on page {page_count}: {e}")
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
logger.info(f"Successfully retrieved {len(all_tickets)} tickets across {page_count} pages")
|
|
42
80
|
return all_tickets
|
|
43
81
|
|
|
44
82
|
@mcp.tool()
|
|
45
83
|
def zendesk_get_ticket_by_id(ticket_id: str) -> dict:
|
|
46
84
|
"""Get a ticket by ID."""
|
|
85
|
+
logger.info(f"Starting zendesk_get_ticket_by_id for ticket_id: {ticket_id}")
|
|
86
|
+
|
|
47
87
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
88
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
89
|
+
|
|
48
90
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
49
91
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}"
|
|
92
|
+
logger.debug(f"Request URL: {url}")
|
|
93
|
+
|
|
50
94
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
logger.debug(f"Making GET request for ticket {ticket_id}")
|
|
99
|
+
response = client.get(url)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
102
|
+
|
|
103
|
+
ticket_data = response.json()["ticket"]
|
|
104
|
+
logger.info(f"Successfully retrieved ticket {ticket_id} with subject: {ticket_data.get('subject', 'N/A')}")
|
|
105
|
+
return ticket_data
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error fetching ticket {ticket_id}: {e}")
|
|
109
|
+
raise
|
|
54
110
|
|
|
55
111
|
@mcp.tool()
|
|
56
112
|
def zendesk_get_comments_by_ticket_id(ticket_id: str) -> list[dict]:
|
|
57
113
|
"""Get all comments for a ticket."""
|
|
114
|
+
logger.info(f"Starting zendesk_get_comments_by_ticket_id for ticket_id: {ticket_id}")
|
|
115
|
+
|
|
58
116
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
117
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
118
|
+
|
|
59
119
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
60
120
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}/comments"
|
|
121
|
+
logger.debug(f"Request URL: {url}")
|
|
122
|
+
|
|
61
123
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
124
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
logger.debug(f"Making GET request for comments of ticket {ticket_id}")
|
|
128
|
+
response = client.get(url)
|
|
129
|
+
response.raise_for_status()
|
|
130
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
131
|
+
|
|
132
|
+
comments = response.json()["comments"]
|
|
133
|
+
logger.info(f"Successfully retrieved {len(comments)} comments for ticket {ticket_id}")
|
|
134
|
+
return comments
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Error fetching comments for ticket {ticket_id}: {e}")
|
|
138
|
+
raise
|
|
65
139
|
|
|
66
140
|
@mcp.tool()
|
|
67
141
|
def zendesk_get_article_by_id(article_id: str) -> dict:
|
|
68
142
|
"""Get an article by ID."""
|
|
143
|
+
logger.info(f"Starting zendesk_get_article_by_id for article_id: {article_id}")
|
|
144
|
+
|
|
69
145
|
ZENDESK_LOCALE = os.getenv("ZENDESK_ARTICLE_LOCALE")
|
|
70
146
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_ARTICLE")
|
|
147
|
+
logger.debug(f"Using locale: {ZENDESK_LOCALE}, subdomain: {ZENDESK_SUBDOMAIN}")
|
|
148
|
+
|
|
71
149
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
72
150
|
url = (
|
|
73
151
|
f"{BASE_URL}/api/v2/help_center/{ZENDESK_LOCALE}/articles/{article_id}"
|
|
74
152
|
)
|
|
153
|
+
logger.debug(f"Request URL: {url}")
|
|
154
|
+
|
|
75
155
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
156
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
logger.debug(f"Making GET request for article {article_id}")
|
|
160
|
+
response = client.get(url)
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
163
|
+
|
|
164
|
+
article = response.json()["article"]
|
|
165
|
+
logger.info(f"Successfully retrieved article {article_id} with title: {article.get('title', 'N/A')}")
|
|
166
|
+
return article
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Error fetching article {article_id}: {e}")
|
|
170
|
+
raise
|
|
79
171
|
|
|
80
172
|
@mcp.tool()
|
|
81
173
|
def zendesk_get_articles() -> list[dict]:
|
|
82
174
|
"""Get all articles."""
|
|
175
|
+
logger.info("Starting zendesk_get_articles")
|
|
176
|
+
|
|
83
177
|
ZENDESK_LOCALE = os.getenv("ZENDESK_ARTICLE_LOCALE")
|
|
84
178
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_ARTICLE")
|
|
179
|
+
logger.debug(f"Using locale: {ZENDESK_LOCALE}, subdomain: {ZENDESK_SUBDOMAIN}")
|
|
180
|
+
|
|
85
181
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
86
182
|
url = f"{BASE_URL}/api/v2/help_center/{ZENDESK_LOCALE}/articles.json"
|
|
183
|
+
logger.debug(f"Request URL: {url}")
|
|
184
|
+
|
|
87
185
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
186
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
logger.debug("Making GET request for articles")
|
|
190
|
+
response = client.get(url)
|
|
191
|
+
response.raise_for_status()
|
|
192
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
193
|
+
|
|
194
|
+
articles = response.json()["articles"]
|
|
195
|
+
logger.info(f"Successfully retrieved {len(articles)} articles")
|
|
196
|
+
return articles
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Error fetching articles: {e}")
|
|
200
|
+
raise
|
|
91
201
|
|
|
92
202
|
@mcp.tool()
|
|
93
203
|
def zendesk_get_articles_count() -> int:
|
|
@@ -129,10 +239,15 @@ def zendesk_get_articles_count() -> int:
|
|
|
129
239
|
@mcp.tool()
|
|
130
240
|
def zendesk_search_articles(query: str) -> list[dict]:
|
|
131
241
|
"""Search Zendesk Help Center articles using a query string."""
|
|
242
|
+
logger.info(f"Starting zendesk_search_articles with query: '{query}'")
|
|
243
|
+
|
|
132
244
|
ZENDESK_LOCALE = os.getenv("ZENDESK_ARTICLE_LOCALE") # e.g., "en-us"
|
|
133
245
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_ARTICLE")
|
|
246
|
+
logger.debug(f"Using locale: {ZENDESK_LOCALE}, subdomain: {ZENDESK_SUBDOMAIN}")
|
|
247
|
+
|
|
134
248
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
135
249
|
url = f"{BASE_URL}/api/v2/help_center/articles/search.json"
|
|
250
|
+
logger.debug(f"Search URL: {url}")
|
|
136
251
|
|
|
137
252
|
params = {
|
|
138
253
|
"query": query,
|
|
@@ -140,11 +255,24 @@ def zendesk_search_articles(query: str) -> list[dict]:
|
|
|
140
255
|
"sort_by": "updated_at",
|
|
141
256
|
"sort_order": "desc",
|
|
142
257
|
}
|
|
258
|
+
logger.debug(f"Search parameters: {params}")
|
|
143
259
|
|
|
144
260
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
261
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
logger.debug(f"Making GET request to search articles with query: '{query}'")
|
|
265
|
+
response = client.get(url, params=params)
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
268
|
+
|
|
269
|
+
results = response.json().get("results", [])
|
|
270
|
+
logger.info(f"Search completed successfully, found {len(results)} articles matching query: '{query}'")
|
|
271
|
+
return results
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Error searching articles with query '{query}': {e}")
|
|
275
|
+
raise
|
|
148
276
|
|
|
149
277
|
@mcp.tool()
|
|
150
278
|
def zendesk_add_comment_to_ticket(ticket_id: str, comment_body: str, public: bool = False) -> dict:
|
|
@@ -153,9 +281,15 @@ def zendesk_add_comment_to_ticket(ticket_id: str, comment_body: str, public: boo
|
|
|
153
281
|
Updates the ticket with a new comment via Zendesk Ticketing API:
|
|
154
282
|
PUT /api/v2/tickets/{ticket_id}.json
|
|
155
283
|
"""
|
|
284
|
+
logger.info(f"Starting zendesk_add_comment_to_ticket for ticket_id: {ticket_id}, public: {public}")
|
|
285
|
+
logger.debug(f"Comment body length: {len(comment_body)} characters")
|
|
286
|
+
|
|
156
287
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
288
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
289
|
+
|
|
157
290
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
158
291
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}.json"
|
|
292
|
+
logger.debug(f"Request URL: {url}")
|
|
159
293
|
|
|
160
294
|
payload = {
|
|
161
295
|
"ticket": {
|
|
@@ -165,12 +299,25 @@ def zendesk_add_comment_to_ticket(ticket_id: str, comment_body: str, public: boo
|
|
|
165
299
|
}
|
|
166
300
|
}
|
|
167
301
|
}
|
|
302
|
+
logger.debug(f"Payload prepared for ticket {ticket_id}")
|
|
168
303
|
|
|
169
304
|
import httpx
|
|
170
305
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
306
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
logger.debug(f"Making PUT request to add comment to ticket {ticket_id}")
|
|
310
|
+
response = client.put(url, json=payload)
|
|
311
|
+
response.raise_for_status()
|
|
312
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
313
|
+
|
|
314
|
+
ticket_data = response.json()["ticket"]
|
|
315
|
+
logger.info(f"Successfully added comment to ticket {ticket_id}")
|
|
316
|
+
return ticket_data
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Error adding comment to ticket {ticket_id}: {e}")
|
|
320
|
+
raise
|
|
174
321
|
|
|
175
322
|
@mcp.tool()
|
|
176
323
|
def zendesk_set_ticket_custom_field(
|
|
@@ -181,11 +328,19 @@ def zendesk_set_ticket_custom_field(
|
|
|
181
328
|
Uses Zendesk's Update Ticket API to set a custom field value:
|
|
182
329
|
PUT /api/v2/tickets/{ticket_id}.json
|
|
183
330
|
"""#
|
|
331
|
+
logger.info(f"Starting zendesk_set_ticket_custom_field for ticket_id: {ticket_id}, field_id: {custom_field_id}")
|
|
332
|
+
logger.debug(f"Custom field value: {custom_field_value}, is_multi_option: {is_multi_option}")
|
|
333
|
+
|
|
184
334
|
if is_multi_option:
|
|
185
335
|
custom_field_value = [custom_field_value]
|
|
336
|
+
logger.debug("Converted custom field value to list for multi-option field")
|
|
337
|
+
|
|
186
338
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
339
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
340
|
+
|
|
187
341
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
188
342
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}.json"
|
|
343
|
+
logger.debug(f"Request URL: {url}")
|
|
189
344
|
|
|
190
345
|
payload = {
|
|
191
346
|
"ticket": {
|
|
@@ -197,48 +352,99 @@ def zendesk_set_ticket_custom_field(
|
|
|
197
352
|
]
|
|
198
353
|
}
|
|
199
354
|
}
|
|
355
|
+
logger.debug(f"Payload prepared for ticket {ticket_id} with custom field {custom_field_id}")
|
|
200
356
|
|
|
201
357
|
import httpx
|
|
202
358
|
|
|
203
359
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
360
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
logger.debug(f"Making PUT request to set custom field {custom_field_id} on ticket {ticket_id}")
|
|
364
|
+
response = client.put(url, json=payload)
|
|
365
|
+
response.raise_for_status()
|
|
366
|
+
logger.debug(f"Successfully received response with status: {response.status_code}")
|
|
367
|
+
|
|
368
|
+
ticket_data = response.json()["ticket"]
|
|
369
|
+
logger.info(f"Successfully set custom field {custom_field_id} on ticket {ticket_id}")
|
|
370
|
+
return ticket_data
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"Error setting custom field {custom_field_id} on ticket {ticket_id}: {e}")
|
|
374
|
+
raise
|
|
207
375
|
|
|
208
376
|
|
|
209
377
|
|
|
210
378
|
@mcp.tool()
|
|
211
379
|
def zendesk_set_ticket_tags(ticket_id: str, tags: list[str]) -> list[str]:
|
|
212
380
|
"""Set the complete tag list for a ticket (overwrites existing tags)."""
|
|
381
|
+
logger.info(f"Starting zendesk_set_ticket_tags for ticket_id: {ticket_id}")
|
|
382
|
+
logger.debug(f"Setting tags: {tags} (total: {len(tags)} tags)")
|
|
383
|
+
|
|
213
384
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
385
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
386
|
+
|
|
214
387
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
215
388
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}/tags.json"
|
|
389
|
+
logger.debug(f"Request URL: {url}")
|
|
216
390
|
|
|
217
391
|
payload = {"tags": tags}
|
|
392
|
+
logger.debug(f"Payload prepared for ticket {ticket_id}")
|
|
218
393
|
|
|
219
394
|
import httpx
|
|
220
395
|
|
|
221
396
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
397
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
logger.debug(f"Making PUT request to set tags on ticket {ticket_id}")
|
|
401
|
+
resp = client.put(url, json=payload)
|
|
402
|
+
resp.raise_for_status()
|
|
403
|
+
logger.debug(f"Successfully received response with status: {resp.status_code}")
|
|
404
|
+
|
|
405
|
+
result_tags = resp.json().get("tags", [])
|
|
406
|
+
logger.info(f"Successfully set {len(result_tags)} tags on ticket {ticket_id}")
|
|
407
|
+
return result_tags
|
|
408
|
+
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(f"Error setting tags on ticket {ticket_id}: {e}")
|
|
411
|
+
raise
|
|
225
412
|
|
|
226
413
|
|
|
227
414
|
@mcp.tool()
|
|
228
415
|
def zendesk_add_ticket_tags(ticket_id: str, tags: list[str]) -> list[str]:
|
|
229
416
|
"""Add tags to a ticket (preserves existing tags)."""
|
|
417
|
+
logger.info(f"Starting zendesk_add_ticket_tags for ticket_id: {ticket_id}")
|
|
418
|
+
logger.debug(f"Adding tags: {tags} (total: {len(tags)} tags)")
|
|
419
|
+
|
|
230
420
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
421
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
422
|
+
|
|
231
423
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
232
424
|
url = f"{BASE_URL}/api/v2/tickets/{ticket_id}/tags.json"
|
|
425
|
+
logger.debug(f"Request URL: {url}")
|
|
233
426
|
|
|
234
427
|
payload = {"tags": tags}
|
|
428
|
+
logger.debug(f"Payload prepared for ticket {ticket_id}")
|
|
235
429
|
|
|
236
430
|
import httpx
|
|
237
431
|
|
|
238
432
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
433
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
logger.debug(f"Making POST request to add tags to ticket {ticket_id}")
|
|
437
|
+
resp = client.post(url, json=payload)
|
|
438
|
+
resp.raise_for_status()
|
|
439
|
+
logger.debug(f"Successfully received response with status: {resp.status_code}")
|
|
440
|
+
|
|
441
|
+
result_tags = resp.json().get("tags", [])
|
|
442
|
+
logger.info(f"Successfully added tags to ticket {ticket_id}, total tags now: {len(result_tags)}")
|
|
443
|
+
return result_tags
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error(f"Error adding tags to ticket {ticket_id}: {e}")
|
|
447
|
+
raise
|
|
242
448
|
|
|
243
449
|
|
|
244
450
|
@mcp.tool()
|
|
@@ -250,23 +456,42 @@ def zendesk_get_ticket_field_type(field_id: int) -> dict:
|
|
|
250
456
|
Returns a dict containing at least:
|
|
251
457
|
{ "type": str, "custom_field_options": list }
|
|
252
458
|
"""
|
|
459
|
+
logger.info(f"Starting zendesk_get_ticket_field_type for field_id: {field_id}")
|
|
460
|
+
|
|
253
461
|
ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN_TICKET")
|
|
462
|
+
logger.debug(f"Using Zendesk subdomain: {ZENDESK_SUBDOMAIN}")
|
|
463
|
+
|
|
254
464
|
BASE_URL = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com"
|
|
255
465
|
url = f"{BASE_URL}/api/v2/ticket_fields/{field_id}.json"
|
|
466
|
+
logger.debug(f"Request URL: {url}")
|
|
256
467
|
|
|
257
468
|
import httpx
|
|
258
469
|
|
|
259
470
|
with httpx.Client(headers=_get_headers(), timeout=30.0) as client:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
471
|
+
logger.debug("Created HTTP client for Zendesk API")
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
logger.debug(f"Making GET request for ticket field {field_id}")
|
|
475
|
+
resp = client.get(url)
|
|
476
|
+
resp.raise_for_status()
|
|
477
|
+
logger.debug(f"Successfully received response with status: {resp.status_code}")
|
|
478
|
+
|
|
479
|
+
field = resp.json().get("ticket_field", {})
|
|
480
|
+
result = {
|
|
481
|
+
"id": field.get("id"),
|
|
482
|
+
"type": field.get("type"),
|
|
483
|
+
"title": field.get("title"),
|
|
484
|
+
"required": field.get("required"),
|
|
485
|
+
"custom_field_options": field.get("custom_field_options", []),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
logger.info(f"Successfully retrieved field info for {field_id}: type={result['type']}, title='{result['title']}'")
|
|
489
|
+
logger.debug(f"Field has {len(result['custom_field_options'])} custom options")
|
|
490
|
+
return result
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
logger.error(f"Error fetching ticket field {field_id}: {e}")
|
|
494
|
+
raise
|
|
270
495
|
|
|
271
496
|
|
|
272
497
|
|