note-connector 0.2.5 → 0.2.6

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 +456 -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 +557 -0
  39. package/py/src/note_mcp/py.typed +0 -0
  40. package/py/src/note_mcp/server.py +905 -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,113 @@
1
+ """Decorators for MCP tool handlers.
2
+
3
+ Provides common functionality for MCP tool handlers:
4
+ - Session validation
5
+ - API error handling
6
+
7
+ Decorator Order:
8
+ When combining decorators, apply in this order (outermost first):
9
+
10
+ @handle_api_error # Catches NoteAPIError from the inner function
11
+ @require_session # Validates session before calling handler
12
+ async def handler(session: Session, ...) -> str:
13
+ ...
14
+
15
+ This ensures session validation happens first, then API errors
16
+ are caught and formatted.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import logging
23
+ from collections.abc import Awaitable, Callable
24
+ from functools import wraps
25
+ from typing import TYPE_CHECKING, Concatenate
26
+
27
+ from note_mcp.auth.session import SessionManager
28
+ from note_mcp.models import NoteAPIError
29
+
30
+ if TYPE_CHECKING:
31
+ from note_mcp.models import Session
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Session manager instance for decorators
36
+ # (SessionManager uses persistent storage, so multiple instances are safe)
37
+ _session_manager = SessionManager()
38
+
39
+
40
+ def require_session[**P](
41
+ func: Callable[Concatenate[Session, P], Awaitable[str]],
42
+ ) -> Callable[P, Awaitable[str]]:
43
+ """Decorator to validate session before executing handler.
44
+
45
+ Loads the session from SessionManager. If the session is valid,
46
+ it is passed as the first argument to the decorated function.
47
+ If the session is invalid or expired, returns an error message.
48
+
49
+ Usage:
50
+ @require_session
51
+ async def my_handler(session: Session, arg1: str) -> str:
52
+ # session is automatically injected
53
+ return await do_something(session, arg1)
54
+
55
+ Args:
56
+ func: The async function to wrap. Must accept Session as first argument.
57
+
58
+ Returns:
59
+ Wrapped function that validates session before calling the original.
60
+ """
61
+
62
+ @wraps(func)
63
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
64
+ session = _session_manager.load()
65
+ if session is None or session.is_expired():
66
+ return "セッションが無効です。note_loginでログインしてください。"
67
+ return await func(session, *args, **kwargs)
68
+
69
+ # Remove 'session' parameter from the signature so it's not exposed in MCP schema
70
+ # See: https://github.com/drillan/note-mcp/issues/238
71
+ original_sig = inspect.signature(func)
72
+ new_params = [param for name, param in original_sig.parameters.items() if name != "session"]
73
+ wrapper.__signature__ = original_sig.replace(parameters=new_params) # type: ignore[attr-defined]
74
+
75
+ return wrapper
76
+
77
+
78
+ def handle_api_error[**P](
79
+ func: Callable[P, Awaitable[str]],
80
+ ) -> Callable[P, Awaitable[str]]:
81
+ """Decorator to catch and format NoteAPIError exceptions.
82
+
83
+ Wraps the function in a try-except block. If NoteAPIError is raised,
84
+ returns a formatted error message. Other exceptions are propagated.
85
+
86
+ Usage:
87
+ @handle_api_error
88
+ async def my_handler(session: Session, arg1: str) -> str:
89
+ result = await api_call(session, arg1)
90
+ return f"Success: {result}"
91
+
92
+ Args:
93
+ func: The async function to wrap.
94
+
95
+ Returns:
96
+ Wrapped function that catches NoteAPIError exceptions.
97
+ """
98
+
99
+ @wraps(func)
100
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
101
+ try:
102
+ return await func(*args, **kwargs)
103
+ except NoteAPIError as e:
104
+ logger.error(
105
+ "NoteAPIError in %s: code=%s, message=%s",
106
+ func.__name__,
107
+ e.code.value,
108
+ e.message,
109
+ exc_info=True,
110
+ )
111
+ return f"エラー [{e.code.value}]: {e.message}"
112
+
113
+ return wrapper
@@ -0,0 +1,33 @@
1
+ """HTTP traffic investigation module for note.com API analysis.
2
+
3
+ This module provides tools for capturing and analyzing HTTP traffic
4
+ to understand note.com API behavior.
5
+
6
+ Usage:
7
+ # Interactive capture
8
+ uv run python -m note_mcp.investigator capture
9
+
10
+ # Analyze captured traffic
11
+ uv run python -m note_mcp.investigator analyze traffic.flow
12
+
13
+ # Export to JSON
14
+ uv run python -m note_mcp.investigator export traffic.flow
15
+ """
16
+
17
+ from note_mcp.investigator.core import (
18
+ CapturedRequest,
19
+ CaptureSession,
20
+ CaptureSessionManager,
21
+ ProxyManager,
22
+ run_capture_session,
23
+ )
24
+ from note_mcp.investigator.mcp_tools import register_investigator_tools
25
+
26
+ __all__ = [
27
+ "CapturedRequest",
28
+ "CaptureSession",
29
+ "CaptureSessionManager",
30
+ "ProxyManager",
31
+ "register_investigator_tools",
32
+ "run_capture_session",
33
+ ]
@@ -0,0 +1,11 @@
1
+ """Entry point for running investigator as a module.
2
+
3
+ Usage:
4
+ uv run python -m note_mcp.investigator capture
5
+ uv run python -m note_mcp.investigator analyze traffic.flow
6
+ """
7
+
8
+ from note_mcp.investigator.cli import main
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,313 @@
1
+ """CLI interface for HTTP traffic investigation.
2
+
3
+ Provides commands for capturing and analyzing HTTP traffic
4
+ to investigate note.com API behavior.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import re
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Annotated
14
+
15
+ import typer
16
+
17
+ app = typer.Typer(
18
+ name="investigator",
19
+ help="note.com API investigation tool - capture and analyze HTTP traffic",
20
+ )
21
+
22
+
23
+ @app.command()
24
+ def capture(
25
+ output: Annotated[
26
+ Path,
27
+ typer.Option(
28
+ "--output",
29
+ "-o",
30
+ help="Output file for captured traffic (.flow format)",
31
+ ),
32
+ ] = Path("traffic.flow"),
33
+ url: Annotated[
34
+ str,
35
+ typer.Option(
36
+ "--url",
37
+ "-u",
38
+ help="Initial URL to navigate to",
39
+ ),
40
+ ] = "https://note.com",
41
+ port: Annotated[
42
+ int,
43
+ typer.Option(
44
+ "--port",
45
+ "-p",
46
+ help="Proxy server port",
47
+ ),
48
+ ] = 8080,
49
+ domain: Annotated[
50
+ str | None,
51
+ typer.Option(
52
+ "--domain",
53
+ "-d",
54
+ help="Filter traffic by domain (e.g., 'api.note.com')",
55
+ ),
56
+ ] = None,
57
+ no_session: Annotated[
58
+ bool,
59
+ typer.Option(
60
+ "--no-session",
61
+ help="Don't restore saved session (require fresh login)",
62
+ ),
63
+ ] = False,
64
+ ) -> None:
65
+ """Start interactive traffic capture session.
66
+
67
+ Opens a browser with proxy configured to capture all HTTP traffic.
68
+ By default, restores saved session cookies from note_login.
69
+ Perform your investigation in the browser, then close it to save the capture.
70
+
71
+ Example:
72
+ uv run python -m note_mcp.investigator capture --domain api.note.com
73
+ """
74
+ from note_mcp.investigator.core import run_capture_session
75
+
76
+ typer.echo("🔍 Starting traffic capture...")
77
+ typer.echo(f" Output: {output}")
78
+ typer.echo(f" Proxy port: {port}")
79
+ if domain:
80
+ typer.echo(f" Domain filter: {domain}")
81
+ typer.echo(f" Session restore: {'disabled' if no_session else 'enabled'}")
82
+ typer.echo()
83
+ typer.echo("📋 Instructions:")
84
+ typer.echo(" 1. Browser will open with proxy configured")
85
+ if not no_session:
86
+ typer.echo(" 2. If logged in, session will be restored automatically")
87
+ typer.echo(" 3. If auto-navigation fails, manually enter the URL")
88
+ typer.echo(" 4. Perform the actions you want to investigate")
89
+ typer.echo(" 5. Close the browser when done")
90
+ typer.echo()
91
+
92
+ try:
93
+ asyncio.run(
94
+ run_capture_session(
95
+ output=output,
96
+ initial_url=url,
97
+ proxy_port=port,
98
+ domain_filter=domain,
99
+ restore_session=not no_session,
100
+ )
101
+ )
102
+ typer.echo(f"✅ Capture saved to: {output}")
103
+ typer.echo()
104
+ typer.echo("Next steps:")
105
+ typer.echo(f" uv run python -m note_mcp.investigator analyze {output}")
106
+
107
+ except RuntimeError as e:
108
+ typer.echo(f"❌ Error: {e}", err=True)
109
+ raise typer.Exit(1) from e
110
+ except KeyboardInterrupt:
111
+ typer.echo("\n⚠️ Capture interrupted")
112
+ raise typer.Exit(0) from None
113
+
114
+
115
+ @app.command()
116
+ def analyze(
117
+ file: Annotated[
118
+ Path,
119
+ typer.Argument(
120
+ help="Traffic capture file to analyze (.flow format)",
121
+ ),
122
+ ],
123
+ pattern: Annotated[
124
+ str | None,
125
+ typer.Option(
126
+ "--pattern",
127
+ "-p",
128
+ help="Regex pattern to search in requests/responses",
129
+ ),
130
+ ] = None,
131
+ domain: Annotated[
132
+ str | None,
133
+ typer.Option(
134
+ "--domain",
135
+ "-d",
136
+ help="Filter by domain",
137
+ ),
138
+ ] = None,
139
+ method: Annotated[
140
+ str | None,
141
+ typer.Option(
142
+ "--method",
143
+ "-m",
144
+ help="Filter by HTTP method (GET, POST, PUT, etc.)",
145
+ ),
146
+ ] = None,
147
+ show_body: Annotated[
148
+ bool,
149
+ typer.Option(
150
+ "--body",
151
+ "-b",
152
+ help="Show request/response bodies",
153
+ ),
154
+ ] = False,
155
+ ) -> None:
156
+ """Analyze captured traffic file.
157
+
158
+ Reads a .flow file captured by mitmproxy and displays matching requests.
159
+
160
+ Example:
161
+ uv run python -m note_mcp.investigator analyze traffic.flow --pattern citation
162
+ """
163
+ if not file.exists():
164
+ typer.echo(f"❌ File not found: {file}", err=True)
165
+ raise typer.Exit(1)
166
+
167
+ typer.echo(f"📊 Analyzing: {file}")
168
+ if pattern:
169
+ typer.echo(f" Pattern: {pattern}")
170
+ if domain:
171
+ typer.echo(f" Domain: {domain}")
172
+ if method:
173
+ typer.echo(f" Method: {method}")
174
+ typer.echo()
175
+
176
+ # Build mitmdump filter
177
+ filters: list[str] = []
178
+ if domain:
179
+ filters.append(f"~d {domain}")
180
+ if method:
181
+ filters.append(f"~m {method.upper()}")
182
+
183
+ # Use mitmdump to read and display the flow
184
+ cmd = ["mitmdump", "-n", "-r", str(file)]
185
+ if filters:
186
+ cmd.extend(["--set", f"flow_filter={'&'.join(filters)}"])
187
+
188
+ try:
189
+ result = subprocess.run(
190
+ cmd,
191
+ capture_output=True,
192
+ text=True,
193
+ check=False,
194
+ )
195
+
196
+ output_lines = result.stdout.strip().split("\n") if result.stdout else []
197
+
198
+ if pattern:
199
+ # Filter lines by pattern
200
+ regex = re.compile(pattern, re.IGNORECASE)
201
+ matched_lines: list[str] = []
202
+ for line in output_lines:
203
+ if regex.search(line):
204
+ matched_lines.append(line)
205
+ output_lines = matched_lines
206
+
207
+ if not output_lines or (len(output_lines) == 1 and not output_lines[0]):
208
+ typer.echo("No matching requests found.")
209
+ return
210
+
211
+ typer.echo(f"Found {len(output_lines)} matching request(s):\n")
212
+ for line in output_lines:
213
+ typer.echo(line)
214
+
215
+ if result.stderr:
216
+ typer.echo(f"\n⚠️ Warnings: {result.stderr}", err=True)
217
+
218
+ except FileNotFoundError:
219
+ typer.echo("❌ mitmdump not found. Install mitmproxy first.", err=True)
220
+ raise typer.Exit(1) from None
221
+
222
+
223
+ @app.command()
224
+ def export(
225
+ file: Annotated[
226
+ Path,
227
+ typer.Argument(
228
+ help="Traffic capture file to export (.flow format)",
229
+ ),
230
+ ],
231
+ output: Annotated[
232
+ Path,
233
+ typer.Option(
234
+ "--output",
235
+ "-o",
236
+ help="Output file (JSON format)",
237
+ ),
238
+ ] = Path("traffic.json"),
239
+ domain: Annotated[
240
+ str | None,
241
+ typer.Option(
242
+ "--domain",
243
+ "-d",
244
+ help="Filter by domain",
245
+ ),
246
+ ] = None,
247
+ ) -> None:
248
+ """Export captured traffic to JSON format.
249
+
250
+ Converts mitmproxy .flow format to readable JSON for further analysis.
251
+
252
+ Example:
253
+ uv run python -m note_mcp.investigator export traffic.flow -o api_calls.json
254
+ """
255
+ if not file.exists():
256
+ typer.echo(f"❌ File not found: {file}", err=True)
257
+ raise typer.Exit(1)
258
+
259
+ typer.echo(f"📤 Exporting: {file} -> {output}")
260
+
261
+ # Use mitmproxy Python API to read flows
262
+ try:
263
+ from mitmproxy import io as mio
264
+ from mitmproxy.http import HTTPFlow
265
+ except ImportError:
266
+ typer.echo("❌ mitmproxy not installed. Run: uv sync --group dev", err=True)
267
+ raise typer.Exit(1) from None
268
+
269
+ import json
270
+
271
+ flows_data: list[dict[str, object]] = []
272
+
273
+ with open(file, "rb") as f:
274
+ reader = mio.FlowReader(f)
275
+ for flow in reader.stream():
276
+ if not isinstance(flow, HTTPFlow):
277
+ continue
278
+
279
+ # Apply domain filter
280
+ if domain and domain not in flow.request.host:
281
+ continue
282
+
283
+ flow_data: dict[str, object] = {
284
+ "request": {
285
+ "method": flow.request.method,
286
+ "url": flow.request.url,
287
+ "headers": dict(flow.request.headers),
288
+ "body": flow.request.get_text(strict=False),
289
+ },
290
+ }
291
+
292
+ if flow.response:
293
+ flow_data["response"] = {
294
+ "status_code": flow.response.status_code,
295
+ "headers": dict(flow.response.headers),
296
+ "body": flow.response.get_text(strict=False),
297
+ }
298
+
299
+ flows_data.append(flow_data)
300
+
301
+ with open(output, "w", encoding="utf-8") as f:
302
+ json.dump(flows_data, f, indent=2, ensure_ascii=False)
303
+
304
+ typer.echo(f"✅ Exported {len(flows_data)} request(s) to: {output}")
305
+
306
+
307
+ def main() -> None:
308
+ """Entry point for the CLI."""
309
+ app()
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()