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.

@@ -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
@@ -5,17 +5,27 @@ import os
5
5
  import httpx
6
6
  from mcp.server.fastmcp import FastMCP
7
7
 
8
- mcp = FastMCP("ZendeskTools")
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
- response = client.get(url)
35
- response.raise_for_status()
54
+ page_count += 1
55
+ logger.debug(f"Fetching page {page_count} from URL: {url}")
36
56
 
37
- data = response.json()
38
- tickets = data.get("tickets", [])
39
- all_tickets.extend(tickets)
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
- url = data.get("next_page")
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
- response = client.get(url)
52
- response.raise_for_status()
53
- return response.json()["ticket"]
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
- response = client.get(url)
63
- response.raise_for_status()
64
- return response.json()["comments"]
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
- response = client.get(url)
77
- response.raise_for_status()
78
- return response.json()["article"]
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
- response = client.get(url)
89
- response.raise_for_status()
90
- return response.json()["articles"]
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
- response = client.get(url, params=params)
146
- response.raise_for_status()
147
- return response.json().get("results", [])
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
- response = client.put(url, json=payload)
172
- response.raise_for_status()
173
- return response.json()["ticket"]
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
- response = client.put(url, json=payload)
205
- response.raise_for_status()
206
- return response.json()["ticket"]
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
- resp = client.put(url, json=payload)
223
- resp.raise_for_status()
224
- return resp.json().get("tags", [])
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
- resp = client.post(url, json=payload)
240
- resp.raise_for_status()
241
- return resp.json().get("tags", [])
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
- resp = client.get(url)
261
- resp.raise_for_status()
262
- field = resp.json().get("ticket_field", {})
263
- return {
264
- "id": field.get("id"),
265
- "type": field.get("type"),
266
- "title": field.get("title"),
267
- "required": field.get("required"),
268
- "custom_field_options": field.get("custom_field_options", []),
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