note-connector 0.2.7 → 0.2.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "note-connector",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/py/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "note-connector"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "note-connector: MCP server and ChatGPT connector for note.com"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -117,6 +117,43 @@ _MAGIC_BYTES: dict[str, tuple[bytes, int]] = {
117
117
  }
118
118
 
119
119
 
120
+ def _extract_image_url_from_response(response: dict[str, Any]) -> str | None:
121
+ """Extract image URL from API response, trying multiple possible field names.
122
+
123
+ note.com API may return the URL under different field names.
124
+ This function tries known field names in order of preference.
125
+
126
+ Args:
127
+ response: Full API response dictionary
128
+
129
+ Returns:
130
+ Image URL string if found, None otherwise
131
+ """
132
+ data = response.get("data", {})
133
+
134
+ # Try known field names for image URL
135
+ candidate_keys = [
136
+ "url",
137
+ "image_url",
138
+ "src",
139
+ "download_url",
140
+ "note_image_url",
141
+ ]
142
+
143
+ for key in candidate_keys:
144
+ value = data.get(key)
145
+ if value and isinstance(value, str) and value.strip():
146
+ return str(value)
147
+
148
+ # Check if the top-level response itself is a string URL
149
+ for key in candidate_keys:
150
+ value = response.get(key)
151
+ if value and isinstance(value, str) and value.strip():
152
+ return str(value)
153
+
154
+ return None
155
+
156
+
120
157
  def validate_image_file(file_path: str) -> None:
121
158
  """Validate image file before upload.
122
159
 
@@ -207,25 +244,36 @@ async def _upload_image_internal(
207
244
  async with NoteAPIClient(session) as client:
208
245
  response = await client.post(endpoint, files=files, data=data)
209
246
 
210
- # Parse response - Article 6: validate required fields, no fallback
211
- image_data = response.get("data", {})
247
+ # Debug: log full API response for investigation
248
+ logger.debug(
249
+ "Image upload response for note_id=%s, endpoint=%s: %s",
250
+ numeric_note_id,
251
+ endpoint,
252
+ {k: v for k, v in response.items() if k != "data"},
253
+ )
254
+ if "data" in response:
255
+ logger.debug("Image upload response data keys: %s", list(response["data"].keys()))
256
+ logger.debug("Image upload response data: %s", response["data"])
212
257
 
213
- # Note: The eyecatch upload endpoint (/v1/image_upload/note_eyecatch) returns
214
- # only 'url' in the response, not 'key'. This is expected behavior based on
215
- # API testing - body images (via presigned_post) return 'key', eyecatch does not.
258
+ # Extract image URL from response (tries multiple field names)
259
+ image_url = _extract_image_url_from_response(response)
260
+
261
+ image_data = response.get("data", {})
216
262
  image_key = image_data.get("key")
217
263
 
218
- image_url = image_data.get("url")
264
+ # URL is optional - eyecatch API may not always return it
265
+ # The eyecatch is set server-side even without a URL in the response
219
266
  if not image_url:
220
- raise NoteAPIError(
221
- code=ErrorCode.API_ERROR,
222
- message="Image upload failed: API response missing required field 'url'",
223
- details={"response": response},
267
+ logger.warning(
268
+ "Image upload response missing image URL for note_id=%s. Response keys: top=%s, data=%s",
269
+ numeric_note_id,
270
+ list(response.keys()),
271
+ list(image_data.keys()) if image_data else "none",
224
272
  )
225
273
 
226
274
  return Image(
227
275
  key=str(image_key) if image_key else None,
228
- url=str(image_url),
276
+ url=str(image_url) if image_url else "",
229
277
  original_path=file_path,
230
278
  size_bytes=file_size,
231
279
  uploaded_at=int(time.time()),
@@ -522,14 +570,30 @@ def _decode_base64_image(image_base64: str) -> bytes:
522
570
  """
523
571
  clean = _strip_data_url_prefix(image_base64)
524
572
 
525
- if not clean.strip():
573
+ # Strip whitespace and newlines (common in ChatGPT output)
574
+ clean = re.sub(r"\s+", "", clean)
575
+
576
+ if not clean:
526
577
  raise NoteAPIError(
527
578
  code=ErrorCode.INVALID_BASE64,
528
579
  message="image_base64 が空です。",
529
580
  )
530
581
 
582
+ # Fix missing padding: base64 length must be a multiple of 4
583
+ # ChatGPT sometimes drops trailing '=' padding chars
584
+ remainder = len(clean) % 4
585
+ if remainder:
586
+ clean += "=" * (4 - remainder)
587
+
531
588
  try:
532
- return base64.b64decode(clean, validate=True)
589
+ # validate=False: tolerate non-strict base64 (extra chars, minor format issues)
590
+ result = base64.b64decode(clean, validate=False)
591
+ if not result:
592
+ raise NoteAPIError(
593
+ code=ErrorCode.INVALID_BASE64,
594
+ message="image_base64 をデコードしましたが、データが空でした。",
595
+ )
596
+ return result
533
597
  except (binascii.Error, ValueError) as e:
534
598
  raise NoteAPIError(
535
599
  code=ErrorCode.INVALID_BASE64,
@@ -285,7 +285,9 @@ async def note_upload_eyecatch(
285
285
  アップロード結果(画像URLを含む)
286
286
  """
287
287
  image = await upload_eyecatch_image(session, file_path, note_id=note_id)
288
- return f"アイキャッチ画像をアップロードしました。URL: {image.url}"
288
+ if image.url:
289
+ return f"アイキャッチ画像をアップロードしました。URL: {image.url}"
290
+ return "アイキャッチ画像をアップロードしました。"
289
291
 
290
292
 
291
293
  @mcp.tool()
@@ -319,7 +321,9 @@ async def note_set_eyecatch_base64(
319
321
  mime_type=mime_type,
320
322
  image_base64=image_base64,
321
323
  )
322
- return f"アイキャッチ画像を設定しました。URL: {image.url}"
324
+ if image.url:
325
+ return f"アイキャッチ画像を設定しました。URL: {image.url}"
326
+ return "アイキャッチ画像を設定しました。"
323
327
 
324
328
 
325
329
  @mcp.tool()