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,318 @@
1
+ """note.com API client using httpx.
2
+
3
+ Provides authenticated access to note.com API endpoints
4
+ with rate limiting and error handling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from types import TracebackType
11
+ from typing import TYPE_CHECKING, Any, Self
12
+
13
+ import httpx
14
+
15
+ from note_mcp.models import ErrorCode, NoteAPIError, Session
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+
21
+ # API base URL
22
+ NOTE_API_BASE = "https://note.com/api"
23
+
24
+ # Editor origin and referer for mutating API requests
25
+ NOTE_EDITOR_ORIGIN = "https://editor.note.com"
26
+ NOTE_EDITOR_REFERER = "https://editor.note.com/"
27
+
28
+ # Common User-Agent string for API requests
29
+ USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
30
+
31
+ # Rate limiting configuration
32
+ RATE_LIMIT_REQUESTS = 10 # Maximum requests per minute
33
+ RATE_LIMIT_WINDOW = 60 # Window size in seconds
34
+
35
+ # Default timeout for requests (seconds)
36
+ DEFAULT_TIMEOUT = 30
37
+
38
+
39
+ class NoteAPIClient:
40
+ """HTTP client for note.com API.
41
+
42
+ Provides authenticated requests with rate limiting and error handling.
43
+ Use as async context manager for proper resource management.
44
+
45
+ Attributes:
46
+ session: User session with authentication cookies
47
+ """
48
+
49
+ def __init__(self, session: Session | None = None) -> None:
50
+ """Initialize API client.
51
+
52
+ Args:
53
+ session: User session for authentication (optional for public endpoints)
54
+ """
55
+ self.session = session
56
+ self._client: httpx.AsyncClient | None = None
57
+ self._request_times: list[float] = []
58
+
59
+ async def __aenter__(self) -> Self:
60
+ """Enter async context manager."""
61
+ self._client = httpx.AsyncClient(
62
+ base_url=NOTE_API_BASE,
63
+ timeout=httpx.Timeout(DEFAULT_TIMEOUT),
64
+ )
65
+ return self
66
+
67
+ async def __aexit__(
68
+ self,
69
+ exc_type: type[BaseException] | None,
70
+ exc_val: BaseException | None,
71
+ exc_tb: TracebackType | None,
72
+ ) -> None:
73
+ """Exit async context manager."""
74
+ if self._client is not None:
75
+ await self._client.aclose()
76
+ self._client = None
77
+
78
+ def _build_headers(self, include_xsrf: bool = False) -> dict[str, str]:
79
+ """Build request headers with authentication.
80
+
81
+ Args:
82
+ include_xsrf: Whether to include X-XSRF-TOKEN header (for POST/PUT/DELETE)
83
+
84
+ Returns:
85
+ Headers dictionary with Accept and Cookie if session exists
86
+ """
87
+ headers: dict[str, str] = {
88
+ "Accept": "*/*",
89
+ "User-Agent": USER_AGENT,
90
+ }
91
+
92
+ if self.session is not None:
93
+ cookie_parts = [f"{k}={v}" for k, v in self.session.cookies.items()]
94
+ headers["Cookie"] = "; ".join(cookie_parts)
95
+
96
+ # Add XSRF token and editor headers for mutating requests
97
+ if include_xsrf:
98
+ xsrf_token = self.session.cookies.get("XSRF-TOKEN")
99
+ if xsrf_token:
100
+ headers["X-XSRF-TOKEN"] = xsrf_token
101
+ # Required headers for editor API
102
+ headers["Origin"] = NOTE_EDITOR_ORIGIN
103
+ headers["Referer"] = NOTE_EDITOR_REFERER
104
+ headers["X-Requested-With"] = "XMLHttpRequest"
105
+ # Sec-Fetch headers (browser security headers)
106
+ headers["Sec-Fetch-Site"] = "same-site"
107
+ headers["Sec-Fetch-Mode"] = "cors"
108
+ headers["Sec-Fetch-Dest"] = "empty"
109
+
110
+ return headers
111
+
112
+ async def _check_rate_limit(self) -> None:
113
+ """Check and enforce rate limiting.
114
+
115
+ Cleans up old timestamps and checks if we've exceeded the limit.
116
+ """
117
+ now = time.time()
118
+ window_start = now - RATE_LIMIT_WINDOW
119
+
120
+ # Clean up old timestamps
121
+ self._request_times = [t for t in self._request_times if t > window_start]
122
+
123
+ # Note: We don't actively wait here, we just track
124
+ # The actual rate limit error will come from the server
125
+ # This is mainly for client-side tracking
126
+
127
+ def _track_request(self) -> None:
128
+ """Track a request for rate limiting."""
129
+ self._request_times.append(time.time())
130
+
131
+ async def _request(
132
+ self,
133
+ method: str,
134
+ path: str,
135
+ *,
136
+ params: dict[str, Any] | None = None,
137
+ json: dict[str, Any] | None = None,
138
+ data: dict[str, Any] | None = None,
139
+ files: dict[str, Any] | None = None,
140
+ include_xsrf: bool = False,
141
+ ) -> dict[str, Any]:
142
+ """Make an HTTP request to the API.
143
+
144
+ Centralizes: init check, rate limit, tracking, headers, error handling.
145
+
146
+ Args:
147
+ method: HTTP method (GET, POST, PUT, DELETE)
148
+ path: API endpoint path
149
+ params: Query parameters
150
+ json: JSON body
151
+ data: Form data
152
+ files: Files to upload
153
+ include_xsrf: Whether to include X-XSRF-TOKEN header
154
+
155
+ Returns:
156
+ JSON response as dictionary
157
+
158
+ Raises:
159
+ RuntimeError: If client not initialized
160
+ NoteAPIError: If request fails
161
+ """
162
+ if self._client is None:
163
+ raise RuntimeError("Client not initialized. Use 'async with' context manager.")
164
+
165
+ await self._check_rate_limit()
166
+ self._track_request()
167
+
168
+ headers = self._build_headers(include_xsrf=include_xsrf)
169
+
170
+ # Set Content-Type for JSON requests (not multipart)
171
+ if json is not None and files is None:
172
+ headers["Content-Type"] = "application/json"
173
+
174
+ request_method = getattr(self._client, method.lower())
175
+
176
+ # Build kwargs based on method - GET doesn't support json/data/files
177
+ kwargs: dict[str, Any] = {"headers": headers}
178
+ if params is not None:
179
+ kwargs["params"] = params
180
+ if method.upper() != "GET":
181
+ if json is not None:
182
+ kwargs["json"] = json
183
+ if data is not None:
184
+ kwargs["data"] = data
185
+ if files is not None:
186
+ kwargs["files"] = files
187
+
188
+ response = await request_method(path, **kwargs)
189
+
190
+ if not response.is_success:
191
+ self._handle_error_response(response)
192
+
193
+ result: dict[str, Any] = response.json()
194
+ return result
195
+
196
+ def _handle_error_response(self, response: httpx.Response) -> None:
197
+ """Handle error responses from the API.
198
+
199
+ Args:
200
+ response: HTTP response object
201
+
202
+ Raises:
203
+ NoteAPIError: With appropriate error code
204
+ """
205
+ status = response.status_code
206
+
207
+ if status == 401:
208
+ raise NoteAPIError(
209
+ code=ErrorCode.NOT_AUTHENTICATED,
210
+ message="Authentication required. Please log in first.",
211
+ details={"status_code": status},
212
+ )
213
+ elif status == 403:
214
+ raise NoteAPIError(
215
+ code=ErrorCode.API_ERROR,
216
+ message="Access denied.",
217
+ details={"status_code": status, "response": response.text},
218
+ )
219
+ elif status == 404:
220
+ raise NoteAPIError(
221
+ code=ErrorCode.ARTICLE_NOT_FOUND,
222
+ message="Resource not found.",
223
+ details={"status_code": status, "response": response.text},
224
+ )
225
+ elif status == 429:
226
+ raise NoteAPIError(
227
+ code=ErrorCode.RATE_LIMITED,
228
+ message="Rate limit exceeded. Please wait before making more requests.",
229
+ details={"status_code": status},
230
+ )
231
+ elif status >= 500:
232
+ raise NoteAPIError(
233
+ code=ErrorCode.API_ERROR,
234
+ message="Server error. Please try again later.",
235
+ details={"status_code": status, "response": response.text},
236
+ )
237
+ else:
238
+ # Include response text in error message for debugging
239
+ raise NoteAPIError(
240
+ code=ErrorCode.API_ERROR,
241
+ message=f"API request failed with status {status}. Response: {response.text[:500]}",
242
+ details={"status_code": status, "response": response.text},
243
+ )
244
+
245
+ async def get(
246
+ self,
247
+ path: str,
248
+ params: dict[str, Any] | None = None,
249
+ ) -> dict[str, Any]:
250
+ """Make a GET request to the API.
251
+
252
+ Args:
253
+ path: API endpoint path (e.g., "/v1/articles")
254
+ params: Query parameters
255
+
256
+ Returns:
257
+ JSON response as dictionary
258
+
259
+ Raises:
260
+ NoteAPIError: If request fails
261
+ """
262
+ return await self._request("GET", path, params=params)
263
+
264
+ async def post(
265
+ self,
266
+ path: str,
267
+ json: dict[str, Any] | None = None,
268
+ data: dict[str, Any] | None = None,
269
+ files: dict[str, Any] | None = None,
270
+ ) -> dict[str, Any]:
271
+ """Make a POST request to the API.
272
+
273
+ Args:
274
+ path: API endpoint path
275
+ json: JSON body
276
+ data: Form data
277
+ files: Files to upload
278
+
279
+ Returns:
280
+ JSON response as dictionary
281
+
282
+ Raises:
283
+ NoteAPIError: If request fails
284
+ """
285
+ return await self._request("POST", path, json=json, data=data, files=files, include_xsrf=True)
286
+
287
+ async def put(
288
+ self,
289
+ path: str,
290
+ json: dict[str, Any] | None = None,
291
+ ) -> dict[str, Any]:
292
+ """Make a PUT request to the API.
293
+
294
+ Args:
295
+ path: API endpoint path
296
+ json: JSON body
297
+
298
+ Returns:
299
+ JSON response as dictionary
300
+
301
+ Raises:
302
+ NoteAPIError: If request fails
303
+ """
304
+ return await self._request("PUT", path, json=json, include_xsrf=True)
305
+
306
+ async def delete(self, path: str) -> dict[str, Any]:
307
+ """Make a DELETE request to the API.
308
+
309
+ Args:
310
+ path: API endpoint path
311
+
312
+ Returns:
313
+ JSON response as dictionary
314
+
315
+ Raises:
316
+ NoteAPIError: If request fails
317
+ """
318
+ return await self._request("DELETE", path, include_xsrf=True)