note-connector 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/paths.js +4 -0
  2. package/dist/setup-dependencies.js +56 -13
  3. package/package.json +3 -2
  4. package/py/pyproject.toml +86 -0
  5. package/py/src/note_mcp/__init__.py +7 -0
  6. package/py/src/note_mcp/__main__.py +65 -0
  7. package/py/src/note_mcp/api/__init__.py +31 -0
  8. package/py/src/note_mcp/api/articles.py +1395 -0
  9. package/py/src/note_mcp/api/client.py +318 -0
  10. package/py/src/note_mcp/api/embeds.py +482 -0
  11. package/py/src/note_mcp/api/images.py +660 -0
  12. package/py/src/note_mcp/api/preview.py +142 -0
  13. package/py/src/note_mcp/api/public_notes.py +150 -0
  14. package/py/src/note_mcp/auth/__init__.py +9 -0
  15. package/py/src/note_mcp/auth/browser.py +574 -0
  16. package/py/src/note_mcp/auth/file_session.py +145 -0
  17. package/py/src/note_mcp/auth/session.py +240 -0
  18. package/py/src/note_mcp/browser/__init__.py +10 -0
  19. package/py/src/note_mcp/browser/config.py +21 -0
  20. package/py/src/note_mcp/browser/manager.py +182 -0
  21. package/py/src/note_mcp/browser/preview.py +68 -0
  22. package/py/src/note_mcp/browser/url_helpers.py +18 -0
  23. package/py/src/note_mcp/chatgpt/__init__.py +1 -0
  24. package/py/src/note_mcp/chatgpt/__main__.py +63 -0
  25. package/py/src/note_mcp/chatgpt/access_log.py +25 -0
  26. package/py/src/note_mcp/chatgpt/auth.py +52 -0
  27. package/py/src/note_mcp/chatgpt/images.py +92 -0
  28. package/py/src/note_mcp/chatgpt/login_once.py +26 -0
  29. package/py/src/note_mcp/chatgpt/middleware.py +31 -0
  30. package/py/src/note_mcp/chatgpt/tools.py +255 -0
  31. package/py/src/note_mcp/chatgpt/widgets.py +121 -0
  32. package/py/src/note_mcp/decorators.py +113 -0
  33. package/py/src/note_mcp/investigator/__init__.py +33 -0
  34. package/py/src/note_mcp/investigator/__main__.py +11 -0
  35. package/py/src/note_mcp/investigator/cli.py +313 -0
  36. package/py/src/note_mcp/investigator/core.py +653 -0
  37. package/py/src/note_mcp/investigator/mcp_tools.py +225 -0
  38. package/py/src/note_mcp/models.py +562 -0
  39. package/py/src/note_mcp/py.typed +0 -0
  40. package/py/src/note_mcp/server.py +944 -0
  41. package/py/src/note_mcp/utils/__init__.py +7 -0
  42. package/py/src/note_mcp/utils/file_parser.py +314 -0
  43. package/py/src/note_mcp/utils/html_to_markdown.py +477 -0
  44. package/py/src/note_mcp/utils/logging.py +119 -0
  45. package/py/src/note_mcp/utils/markdown.py +12 -0
  46. package/py/src/note_mcp/utils/markdown_to_html.py +826 -0
@@ -0,0 +1,562 @@
1
+ """Pydantic data models for note-mcp.
2
+
3
+ This module defines all data models used throughout the note-mcp server,
4
+ including session management, article handling, and error types.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from enum import Enum
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class Session(BaseModel):
16
+ """User authentication session.
17
+
18
+ Stores authentication state including cookies, user information,
19
+ and session expiration.
20
+
21
+ Attributes:
22
+ cookies: note.com authentication cookies (note_gql_auth_token, _note_session_v5)
23
+ user_id: note.com user ID
24
+ username: note.com username (used in URL paths)
25
+ expires_at: Session expiration timestamp (Unix timestamp), None if no expiry
26
+ created_at: Session creation timestamp (Unix timestamp)
27
+ """
28
+
29
+ cookies: dict[str, str]
30
+ user_id: str
31
+ username: str
32
+ expires_at: int | None = None
33
+ created_at: int
34
+
35
+ def is_expired(self) -> bool:
36
+ """Check if the session has expired.
37
+
38
+ Returns:
39
+ True if session has expired, False otherwise.
40
+ Returns False if expires_at is None (no expiry set).
41
+ """
42
+ if self.expires_at is None:
43
+ return False
44
+ return time.time() > self.expires_at
45
+
46
+
47
+ class ArticleStatus(str, Enum):
48
+ """Article publication status."""
49
+
50
+ DRAFT = "draft"
51
+ PUBLISHED = "published"
52
+ PRIVATE = "private"
53
+ DELETED = "deleted"
54
+
55
+
56
+ class ImageType(str, Enum):
57
+ """Image upload type.
58
+
59
+ Determines which note.com API endpoint to use for image upload.
60
+ """
61
+
62
+ EYECATCH = "eyecatch" # Header/eyecatch image (見出し画像)
63
+ BODY = "body" # Inline/body image (記事内埋め込み画像)
64
+
65
+
66
+ class Article(BaseModel):
67
+ """A note.com article.
68
+
69
+ Represents an article with all its metadata as stored on note.com.
70
+
71
+ Attributes:
72
+ id: Article ID (note.com internal ID)
73
+ key: Article key (used in URL path)
74
+ title: Article title
75
+ body: Article body content (HTML format)
76
+ status: Publication status
77
+ tags: List of hashtags (without # prefix)
78
+ eyecatch_image_key: Eyecatch image key (if set)
79
+ prev_access_key: Preview access key for draft articles
80
+ created_at: Creation timestamp (ISO 8601)
81
+ updated_at: Last update timestamp (ISO 8601)
82
+ published_at: Publication timestamp (ISO 8601)
83
+ url: Full article URL
84
+ """
85
+
86
+ id: str
87
+ key: str
88
+ title: str
89
+ body: str
90
+ status: ArticleStatus
91
+ tags: list[str] = []
92
+ eyecatch_image_key: str | None = None
93
+ prev_access_key: str | None = None
94
+ created_at: str | None = None
95
+ updated_at: str | None = None
96
+ published_at: str | None = None
97
+ url: str | None = None
98
+
99
+
100
+ class ArticleInput(BaseModel):
101
+ """Input data for creating or updating an article.
102
+
103
+ Attributes:
104
+ title: Article title
105
+ body: Article body content (Markdown format)
106
+ tags: List of hashtags (# prefix optional, will be normalized)
107
+ eyecatch_image_path: Local path to eyecatch image (optional)
108
+ """
109
+
110
+ title: str
111
+ body: str
112
+ tags: list[str] = []
113
+ eyecatch_image_path: str | None = None
114
+
115
+
116
+ class Image(BaseModel):
117
+ """An uploaded image.
118
+
119
+ Attributes:
120
+ key: note.com image key (None for eyecatch images as API doesn't return it)
121
+ url: Image URL on note.com
122
+ original_path: Original local file path
123
+ size_bytes: File size in bytes (optional)
124
+ uploaded_at: Upload timestamp (Unix timestamp)
125
+ image_type: Type of image (eyecatch or body)
126
+ """
127
+
128
+ key: str | None = None
129
+ url: str
130
+ original_path: str
131
+ size_bytes: int | None = None
132
+ uploaded_at: int
133
+ image_type: ImageType = ImageType.EYECATCH
134
+
135
+
136
+ class Tag(BaseModel):
137
+ """A hashtag for articles.
138
+
139
+ Attributes:
140
+ name: Tag name (without # prefix)
141
+ """
142
+
143
+ name: str
144
+
145
+ @classmethod
146
+ def normalize(cls, tag: str) -> str:
147
+ """Normalize a tag by removing leading # characters.
148
+
149
+ Args:
150
+ tag: Tag string, possibly with # prefix
151
+
152
+ Returns:
153
+ Tag string without # prefix
154
+ """
155
+ return tag.lstrip("#")
156
+
157
+
158
+ class ArticleListResult(BaseModel):
159
+ """Result of listing articles.
160
+
161
+ Attributes:
162
+ articles: List of articles
163
+ total: Total number of articles matching the query
164
+ page: Current page number (1-indexed)
165
+ has_more: Whether there are more articles to fetch
166
+ """
167
+
168
+ articles: list[Article]
169
+ total: int
170
+ page: int
171
+ has_more: bool
172
+
173
+
174
+ class BrowserArticleResult(BaseModel):
175
+ """Result of browser-based article creation/update.
176
+
177
+ Includes the article and optional TOC/alignment/embed/image insertion results for user notification.
178
+
179
+ Attributes:
180
+ article: The created/updated article
181
+ toc_inserted: True if TOC was successfully inserted, False if failed, None if not attempted
182
+ toc_error: Error message if TOC insertion failed
183
+ alignments_applied: Number of text alignments successfully applied, None if not attempted
184
+ alignment_error: Error message if text alignment application failed
185
+ embeds_inserted: Number of embeds successfully inserted, None if not attempted
186
+ embed_error: Error message if embed insertion failed
187
+ images_inserted: Number of images successfully inserted, None if not attempted
188
+ image_error: Error message if image insertion failed
189
+ debug_info: Debug information for troubleshooting (temporary)
190
+ """
191
+
192
+ article: Article
193
+ toc_inserted: bool | None = None
194
+ toc_error: str | None = None
195
+ alignments_applied: int | None = None
196
+ alignment_error: str | None = None
197
+ embeds_inserted: int | None = None
198
+ embed_error: str | None = None
199
+ images_inserted: int | None = None
200
+ image_error: str | None = None
201
+ debug_info: str | None = None
202
+
203
+
204
+ class ErrorCode(str, Enum):
205
+ """Error codes for note-mcp API errors."""
206
+
207
+ NOT_AUTHENTICATED = "not_authenticated"
208
+ SESSION_EXPIRED = "session_expired"
209
+ ARTICLE_NOT_FOUND = "article_not_found"
210
+ RATE_LIMITED = "rate_limited"
211
+ API_ERROR = "api_error"
212
+ UPLOAD_FAILED = "upload_failed"
213
+ INVALID_INPUT = "invalid_input"
214
+ INVALID_BASE64 = "invalid_base64"
215
+ UNSUPPORTED_MIME_TYPE = "unsupported_mime_type"
216
+ INVALID_IMAGE = "invalid_image"
217
+ IMAGE_TOO_LARGE = "image_too_large"
218
+ EYECATCH_SET_FAILED = "eyecatch_set_failed"
219
+
220
+
221
+ class NoteAPIError(Exception):
222
+ """Exception for note.com API errors.
223
+
224
+ Attributes:
225
+ code: Error code
226
+ message: Human-readable error message
227
+ details: Additional error details
228
+ """
229
+
230
+ def __init__(
231
+ self,
232
+ code: ErrorCode,
233
+ message: str,
234
+ details: dict[str, object] | None = None,
235
+ ) -> None:
236
+ """Initialize the error.
237
+
238
+ Args:
239
+ code: Error code from ErrorCode enum
240
+ message: Human-readable error message
241
+ details: Additional context (e.g., status code, response body)
242
+ """
243
+ self.code = code
244
+ self.message = message
245
+ self.details = details or {}
246
+ super().__init__(message)
247
+
248
+
249
+ class LoginError(Exception):
250
+ """ログイン処理でのエラー。
251
+
252
+ reCAPTCHA検出、2FA要求、認証情報エラー時に送出される。
253
+ 手動ログインへのフォールバックは行わず、明確なエラーで通知する。
254
+
255
+ Attributes:
256
+ code: エラーコード(RECAPTCHA_DETECTED, TWO_FACTOR_REQUIRED,
257
+ INVALID_CREDENTIALS, LOGIN_TIMEOUT)
258
+ message: エラーメッセージ
259
+ resolution: 推奨される対処法
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ code: str,
265
+ message: str,
266
+ resolution: str | None = None,
267
+ ) -> None:
268
+ """Initialize the error.
269
+
270
+ Args:
271
+ code: エラーコード
272
+ message: エラーメッセージ
273
+ resolution: 推奨される対処法(オプション)
274
+ """
275
+ self.code = code
276
+ self.message = message
277
+ self.resolution = resolution
278
+ super().__init__(message)
279
+
280
+
281
+ # =============================================================================
282
+ # Issue #141: Delete Draft Models
283
+ # =============================================================================
284
+
285
+ # Delete operation error messages (T020)
286
+ DELETE_ERROR_PUBLISHED_ARTICLE = "公開済み記事は削除できません。下書きのみ削除可能です。"
287
+ DELETE_ERROR_NO_ACCESS = "この記事へのアクセス権がありません。"
288
+ DELETE_ERROR_NOT_FOUND = "記事が見つかりません。キー: {article_key}"
289
+ DELETE_CONFIRM_REQUIRED = "削除を実行するには confirm=True を指定してください。"
290
+
291
+
292
+ class ArticleSummary(BaseModel):
293
+ """Summary information of an article.
294
+
295
+ Used in bulk delete preview/result to show article information
296
+ without the full body content.
297
+
298
+ Attributes:
299
+ article_id: Article ID (note.com internal ID)
300
+ article_key: Article key (used in URL path)
301
+ title: Article title
302
+ """
303
+
304
+ article_id: str
305
+ article_key: str
306
+ title: str
307
+
308
+
309
+ class FailedArticle(BaseModel):
310
+ """Information about a failed deletion.
311
+
312
+ Used in BulkDeleteResult to provide details about articles
313
+ that could not be deleted and why.
314
+
315
+ Attributes:
316
+ article_id: Article ID
317
+ article_key: Article key
318
+ title: Article title
319
+ error: Error message explaining the failure
320
+ """
321
+
322
+ article_id: str
323
+ article_key: str
324
+ title: str
325
+ error: str
326
+
327
+
328
+ class DeleteResult(BaseModel):
329
+ """Result of a single delete operation.
330
+
331
+ Returned when a delete operation completes (success or failure).
332
+
333
+ Attributes:
334
+ success: Whether the deletion was successful
335
+ article_id: ID of the deleted article
336
+ article_key: Key of the deleted article
337
+ article_title: Title of the deleted article
338
+ message: Result message for the user
339
+ """
340
+
341
+ success: bool
342
+ article_id: str
343
+ article_key: str
344
+ article_title: str
345
+ message: str
346
+
347
+
348
+ class DeletePreview(BaseModel):
349
+ """Preview information before deletion.
350
+
351
+ Returned when confirm=False to show what will be deleted
352
+ and prompt for confirmation.
353
+
354
+ Attributes:
355
+ article_id: ID of the article to be deleted
356
+ article_key: Key of the article to be deleted
357
+ article_title: Title of the article to be deleted
358
+ status: Current status of the article
359
+ message: Confirmation prompt message
360
+ """
361
+
362
+ article_id: str
363
+ article_key: str
364
+ article_title: str
365
+ status: ArticleStatus
366
+ message: str
367
+
368
+
369
+ class BulkDeletePreview(BaseModel):
370
+ """Preview information for bulk deletion.
371
+
372
+ Returned when confirm=False to show all drafts that will be deleted.
373
+
374
+ Attributes:
375
+ total_count: Total number of drafts to be deleted
376
+ articles: List of articles to be deleted
377
+ message: Confirmation prompt message
378
+ """
379
+
380
+ total_count: int
381
+ articles: list[ArticleSummary]
382
+ message: str
383
+
384
+
385
+ class BulkDeleteResult(BaseModel):
386
+ """Result of bulk delete operation.
387
+
388
+ Provides detailed information about the bulk deletion,
389
+ including counts and lists of successful/failed deletions.
390
+
391
+ Attributes:
392
+ success: Whether all deletions were successful
393
+ total_count: Total number of articles targeted
394
+ deleted_count: Number of successfully deleted articles
395
+ failed_count: Number of failed deletions
396
+ deleted_articles: List of successfully deleted articles
397
+ failed_articles: List of articles that failed to delete
398
+ message: Summary message
399
+ """
400
+
401
+ success: bool
402
+ total_count: int
403
+ deleted_count: int
404
+ failed_count: int
405
+ deleted_articles: list[ArticleSummary]
406
+ failed_articles: list[FailedArticle]
407
+ message: str
408
+
409
+
410
+ class DeleteDraftInput(BaseModel):
411
+ """Input for note_delete_draft MCP tool.
412
+
413
+ Attributes:
414
+ article_key: Key of the article to delete (format: nXXXXXXXXXXXX)
415
+ confirm: Confirmation flag (must be True to execute deletion)
416
+ """
417
+
418
+ article_key: str
419
+ confirm: bool = False
420
+
421
+
422
+ class PublicArticleSummary(BaseModel):
423
+ """Summary of a public note.com article in search results."""
424
+
425
+ key: str
426
+ title: str
427
+ author_username: str
428
+ author_nickname: str | None = None
429
+ url: str
430
+ published_at: str | None = None
431
+
432
+
433
+ class PublicSearchResult(BaseModel):
434
+ """Public note search results."""
435
+
436
+ items: list[PublicArticleSummary]
437
+ query: str
438
+ is_last_page: bool | None = None
439
+
440
+
441
+ class PublicArticle(BaseModel):
442
+ """A fetched public note.com article."""
443
+
444
+ key: str
445
+ title: str
446
+ body_markdown: str
447
+ author_username: str
448
+ author_nickname: str | None = None
449
+ url: str
450
+ status: str
451
+
452
+
453
+ class DeleteAllDraftsInput(BaseModel):
454
+ """Input for note_delete_all_drafts MCP tool.
455
+
456
+ Attributes:
457
+ confirm: Confirmation flag (must be True to execute deletion)
458
+ """
459
+
460
+ confirm: bool = False
461
+
462
+
463
+ def from_api_response(data: dict[str, object]) -> Article:
464
+ """Create an Article from note.com API response.
465
+
466
+ Article 6 (Data Accuracy Mandate) compliant:
467
+ - Required fields (id, key, status) must be present, no implicit fallbacks
468
+ - Missing required fields raise NoteAPIError
469
+
470
+ Args:
471
+ data: Raw API response dictionary
472
+
473
+ Returns:
474
+ Article instance
475
+
476
+ Raises:
477
+ NoteAPIError: If required fields (id, key, status) are missing or invalid
478
+ """
479
+ # Article 6: Validate required field 'id' - no fallback
480
+ article_id = data.get("id")
481
+ if not article_id:
482
+ raise NoteAPIError(
483
+ code=ErrorCode.API_ERROR,
484
+ message="API response missing required field: id",
485
+ details={"response": data},
486
+ )
487
+
488
+ # Article 6: Validate required field 'key' - no fallback
489
+ article_key = data.get("key")
490
+ if not article_key:
491
+ raise NoteAPIError(
492
+ code=ErrorCode.API_ERROR,
493
+ message="API response missing required field: key",
494
+ details={"response": data},
495
+ )
496
+
497
+ # Article 6: Validate required field 'status' - no fallback or guessing
498
+ status_str = data.get("status")
499
+ if not isinstance(status_str, str) or not status_str:
500
+ raise NoteAPIError(
501
+ code=ErrorCode.API_ERROR,
502
+ message="API response missing or invalid required field: status",
503
+ details={"response": data, "status_value": status_str},
504
+ )
505
+
506
+ # Extract hashtag names from the hashtags array
507
+ # Empty hashtags list is valid - this is not a fallback
508
+ hashtags = data.get("hashtags", [])
509
+ tags: list[str] = []
510
+ if isinstance(hashtags, list):
511
+ for ht in hashtags:
512
+ if isinstance(ht, dict):
513
+ hashtag_obj = ht.get("hashtag", {})
514
+ if isinstance(hashtag_obj, dict):
515
+ # Skip hashtags without name - no fallback to empty string
516
+ name = hashtag_obj.get("name")
517
+ if name:
518
+ tags.append(str(name))
519
+
520
+ # Extract title: use "name" field, fallback to "noteDraft.name" for drafts
521
+ title = data.get("name")
522
+ if not title:
523
+ note_draft = data.get("noteDraft")
524
+ if isinstance(note_draft, dict):
525
+ title = note_draft.get("name")
526
+ title_str = str(title) if title else ""
527
+
528
+ # body can be empty string - this is a valid value, not a missing field
529
+ body = data.get("body")
530
+ body_str = str(body) if body is not None else ""
531
+
532
+ return Article(
533
+ id=str(article_id),
534
+ key=str(article_key),
535
+ title=title_str,
536
+ body=body_str,
537
+ status=ArticleStatus(status_str),
538
+ tags=tags,
539
+ eyecatch_image_key=str(data.get("eyecatch_image_key")) if data.get("eyecatch_image_key") else None,
540
+ prev_access_key=str(data.get("prev_access_key")) if data.get("prev_access_key") else None,
541
+ created_at=str(data.get("created_at")) if data.get("created_at") else None,
542
+ updated_at=str(data.get("updated_at")) if data.get("updated_at") else None,
543
+ published_at=str(data.get("publish_at")) if data.get("publish_at") else None,
544
+ url=str(data.get("noteUrl")) if data.get("noteUrl") else None,
545
+ )
546
+
547
+
548
+ def to_api_request(article_input: ArticleInput, html_body: str) -> dict[str, object]:
549
+ """Convert ArticleInput to note.com API request format.
550
+
551
+ Args:
552
+ article_input: Input data from user
553
+ html_body: HTML-converted body content
554
+
555
+ Returns:
556
+ Dictionary suitable for note.com API request
557
+ """
558
+ return {
559
+ "name": article_input.title,
560
+ "body": html_body,
561
+ "status": "draft",
562
+ }
File without changes