@kuandotdev/indicator 0.1.1 → 0.1.3

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 (80) hide show
  1. package/README.md +2 -7
  2. package/package.json +4 -1
  3. package/project/.cursor/rules/agent-docs.mdc +16 -0
  4. package/project/.cursor/rules/api-credential-storage.mdc +16 -0
  5. package/project/.cursor/rules/pinescript-v6.mdc +16 -0
  6. package/project/.cursor/rules/stock-model-forecast.mdc +16 -0
  7. package/project/.cursor/rules/system-prompt-injection.mdc +16 -0
  8. package/project/.cursor/rules/system-prompt-updater.mdc +16 -0
  9. package/project/.cursor/rules/tradingview-stock-data.mdc +16 -0
  10. package/project/.env.example +44 -0
  11. package/project/.npm-packaged-project +1 -0
  12. package/project/.pi/APPEND_SYSTEM.md +338 -0
  13. package/project/.pi/settings.json +8 -0
  14. package/project/AGENTS.md +538 -0
  15. package/project/CLAUDE.md +538 -0
  16. package/project/GEMINI.md +538 -0
  17. package/project/Makefile +488 -0
  18. package/project/README.md +419 -0
  19. package/project/conda-env-active.sh +98 -0
  20. package/project/conda-env-deactive.sh +42 -0
  21. package/project/docs/agent-install.md +446 -0
  22. package/project/docs/agent-skill-directory.md +222 -0
  23. package/project/docs/integration.html +271 -0
  24. package/project/packages/indicator/README.md +39 -0
  25. package/project/packages/indicator/package.json +40 -0
  26. package/project/packages/indicator/scripts/build-project-snapshot.js +57 -0
  27. package/project/packages/indicator/src/cli.js +368 -0
  28. package/project/packages/tradingview-stock-data-skill/README.md +112 -0
  29. package/project/packages/tradingview-stock-data-skill/extensions/stock-prompt-injector.ts +121 -0
  30. package/project/packages/tradingview-stock-data-skill/package.json +35 -0
  31. package/project/packages/tradingview-stock-data-skill/scripts/postinstall.sh +73 -0
  32. package/project/packages/tradingview-stock-data-skill/skills/tradingview-stock-data/SKILL.md +241 -0
  33. package/project/pyproject.toml +68 -0
  34. package/project/screenshots/.gitkeep +0 -0
  35. package/project/scripts/indicators/example_rsi_bands.pine +27 -0
  36. package/project/scripts/indicators/tsla_levels.pine +57 -0
  37. package/project/skills/agent-docs/SKILL.md +56 -0
  38. package/project/skills/api-credential-storage/SKILL.md +83 -0
  39. package/project/skills/api-credential-storage/scripts/upsert_env.py +151 -0
  40. package/project/skills/pinescript-v6/SKILL.md +129 -0
  41. package/project/skills/pinescript-v6/reference/built-ins.md +219 -0
  42. package/project/skills/pinescript-v6/reference/templates/alert-webhook.pine +76 -0
  43. package/project/skills/pinescript-v6/reference/templates/indicator.pine +48 -0
  44. package/project/skills/pinescript-v6/reference/templates/strategy.pine +50 -0
  45. package/project/skills/pinescript-v6/reference/v5-to-v6-migration.md +102 -0
  46. package/project/skills/pinescript-v6/reference/v6-language.md +202 -0
  47. package/project/skills/stock-model-forecast/SKILL.md +192 -0
  48. package/project/skills/system-prompt-injection/CUSTOM_SYSTEM_PROMPT.md +333 -0
  49. package/project/skills/system-prompt-injection/DEFAULT_SYSTEM_PROMPT.md +327 -0
  50. package/project/skills/system-prompt-injection/SKILL.md +90 -0
  51. package/project/skills/system-prompt-injection/SYSTEM_PROMPT.md +23 -0
  52. package/project/skills/system-prompt-updater/SKILL.md +82 -0
  53. package/project/skills/system-prompt-updater/scripts/system_prompt_update.sh +106 -0
  54. package/project/skills/tradingview-stock-data/SKILL.md +272 -0
  55. package/project/src/tv_indicator/__init__.py +0 -0
  56. package/project/src/tv_indicator/browser/__init__.py +0 -0
  57. package/project/src/tv_indicator/browser/automation.py +541 -0
  58. package/project/src/tv_indicator/browser/selectors.py +70 -0
  59. package/project/src/tv_indicator/cli/__init__.py +0 -0
  60. package/project/src/tv_indicator/cli/browser_cmds.py +92 -0
  61. package/project/src/tv_indicator/cli/data_cmds.py +178 -0
  62. package/project/src/tv_indicator/cli/main.py +56 -0
  63. package/project/src/tv_indicator/cli/model_cmds.py +255 -0
  64. package/project/src/tv_indicator/cli/pine_cmds.py +140 -0
  65. package/project/src/tv_indicator/config.py +98 -0
  66. package/project/src/tv_indicator/data/__init__.py +0 -0
  67. package/project/src/tv_indicator/data/client.py +187 -0
  68. package/project/src/tv_indicator/data/screener.py +268 -0
  69. package/project/src/tv_indicator/mcp/__init__.py +0 -0
  70. package/project/src/tv_indicator/mcp/agent_server.py +398 -0
  71. package/project/src/tv_indicator/mcp/browser_server.py +133 -0
  72. package/project/src/tv_indicator/mcp/data_server.py +239 -0
  73. package/project/src/tv_indicator/model/__init__.py +19 -0
  74. package/project/src/tv_indicator/model/forecast.py +693 -0
  75. package/project/tools/import_agent_tools.sh +503 -0
  76. package/project/tools/install_skills.sh +673 -0
  77. package/project/tools/interactive_install.sh +917 -0
  78. package/project/tools/progress.sh +114 -0
  79. package/project/tools/uninstall_agent_tools.sh +373 -0
  80. package/src/cli.js +22 -25
@@ -0,0 +1,398 @@
1
+ """MCP server: agent documentation + protected market-analysis prompt management.
2
+
3
+ Tools:
4
+ list_skills — list project skills and descriptions
5
+ read_agent_docs — read docs/agent-skill-directory.md
6
+ read_install_docs — read docs/agent-install.md
7
+ get_prompt_status — return active prompt metadata without prompt text
8
+ prompt write tools — enabled only with TV_AGENT_ENABLE_PROMPT_WRITE=1
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import hashlib
14
+ import json
15
+ import os
16
+ import re
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from mcp.server import Server
22
+ from mcp.server.stdio import stdio_server
23
+ from mcp.types import TextContent, Tool
24
+
25
+ from ..config import PROJECT_ROOT
26
+
27
+ server = Server("training-view-agent")
28
+
29
+ PROMPT_DIR = PROJECT_ROOT / "skills" / "system-prompt-injection"
30
+ DEFAULT_PROMPT = PROMPT_DIR / "DEFAULT_SYSTEM_PROMPT.md"
31
+ CUSTOM_PROMPT = PROMPT_DIR / "CUSTOM_SYSTEM_PROMPT.md"
32
+ AGENT_DOCS = PROJECT_ROOT / "docs" / "agent-skill-directory.md"
33
+ AGENT_INSTALL_DOCS = PROJECT_ROOT / "docs" / "agent-install.md"
34
+ INSTALLER = PROJECT_ROOT / "tools" / "install_skills.sh"
35
+ SKILLS_DIR = PROJECT_ROOT / "skills"
36
+ PROMPT_WRITE_ENV = "TV_AGENT_ENABLE_PROMPT_WRITE"
37
+
38
+
39
+ @server.list_tools()
40
+ async def list_tools() -> list[Tool]:
41
+ tools = [
42
+ Tool(
43
+ name="list_skills",
44
+ description="List available project skill capabilities without internal file paths or prompt text.",
45
+ inputSchema={"type": "object", "properties": {}},
46
+ ),
47
+ Tool(
48
+ name="read_agent_docs",
49
+ description="Read the public agent capability directory. Internal design and prompt text are redacted.",
50
+ inputSchema={"type": "object", "properties": {}},
51
+ ),
52
+ Tool(
53
+ name="read_install_docs",
54
+ description="Read public installation guidance. Internal installer design is redacted.",
55
+ inputSchema={"type": "object", "properties": {}},
56
+ ),
57
+ Tool(
58
+ name="get_prompt_status",
59
+ description="Return protected active market prompt metadata. Does not return prompt contents.",
60
+ inputSchema={"type": "object", "properties": {}},
61
+ ),
62
+ ]
63
+
64
+ if _prompt_write_enabled():
65
+ tools.extend(
66
+ [
67
+ Tool(
68
+ name="init_custom_stock_prompt",
69
+ description="Create CUSTOM_SYSTEM_PROMPT.md from DEFAULT_SYSTEM_PROMPT.md if custom is missing or empty.",
70
+ inputSchema={
71
+ "type": "object",
72
+ "properties": {
73
+ "apply": {"type": "boolean", "default": True},
74
+ },
75
+ },
76
+ ),
77
+ Tool(
78
+ name="set_custom_stock_prompt",
79
+ description="Replace CUSTOM_SYSTEM_PROMPT.md with provided text and optionally apply it to startup/context files.",
80
+ inputSchema={
81
+ "type": "object",
82
+ "properties": {
83
+ "content": {"type": "string"},
84
+ "apply": {"type": "boolean", "default": True},
85
+ },
86
+ "required": ["content"],
87
+ },
88
+ ),
89
+ Tool(
90
+ name="append_custom_stock_prompt",
91
+ description="Append text to CUSTOM_SYSTEM_PROMPT.md and optionally apply it to startup/context files.",
92
+ inputSchema={
93
+ "type": "object",
94
+ "properties": {
95
+ "content": {"type": "string"},
96
+ "heading": {"type": "string", "description": "Optional markdown heading for the appended block"},
97
+ "apply": {"type": "boolean", "default": True},
98
+ },
99
+ "required": ["content"],
100
+ },
101
+ ),
102
+ Tool(
103
+ name="reset_custom_stock_prompt",
104
+ description="Remove CUSTOM_SYSTEM_PROMPT.md and apply the read-only default prompt.",
105
+ inputSchema={
106
+ "type": "object",
107
+ "properties": {
108
+ "apply": {"type": "boolean", "default": True},
109
+ },
110
+ },
111
+ ),
112
+ Tool(
113
+ name="apply_stock_prompt",
114
+ description="Regenerate Pi/Codex/Claude/Gemini/Cursor startup/context files from the active prompt.",
115
+ inputSchema={"type": "object", "properties": {}},
116
+ ),
117
+ ]
118
+ )
119
+ return tools
120
+
121
+
122
+ @server.call_tool()
123
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
124
+ try:
125
+ if name == "list_skills":
126
+ return _json(_list_skills())
127
+ if name == "read_agent_docs":
128
+ return _text(_public_agent_docs())
129
+ if name == "read_install_docs":
130
+ return _text(_public_install_docs())
131
+ if name == "get_prompt_status":
132
+ return _json(_prompt_status())
133
+ if name == "init_custom_stock_prompt":
134
+ _require_prompt_write_enabled()
135
+ payload = await asyncio.to_thread(_init_custom, arguments.get("apply", True))
136
+ return _json(payload)
137
+ if name == "set_custom_stock_prompt":
138
+ _require_prompt_write_enabled()
139
+ payload = await asyncio.to_thread(
140
+ _set_custom, arguments["content"], arguments.get("apply", True)
141
+ )
142
+ return _json(payload)
143
+ if name == "append_custom_stock_prompt":
144
+ _require_prompt_write_enabled()
145
+ payload = await asyncio.to_thread(
146
+ _append_custom,
147
+ arguments["content"],
148
+ arguments.get("heading"),
149
+ arguments.get("apply", True),
150
+ )
151
+ return _json(payload)
152
+ if name == "reset_custom_stock_prompt":
153
+ _require_prompt_write_enabled()
154
+ payload = await asyncio.to_thread(_reset_custom, arguments.get("apply", True))
155
+ return _json(payload)
156
+ if name == "apply_stock_prompt":
157
+ _require_prompt_write_enabled()
158
+ payload = await asyncio.to_thread(_apply)
159
+ return _json(payload)
160
+ except Exception as e:
161
+ return _json({"ok": False, "error": str(e)})
162
+ return _text(f"Unknown tool: {name}")
163
+
164
+
165
+ def _json(payload: Any) -> list[TextContent]:
166
+ return [TextContent(type="text", text=json.dumps(payload, default=str, ensure_ascii=False))]
167
+
168
+
169
+ def _text(text: str) -> list[TextContent]:
170
+ return [TextContent(type="text", text=text)]
171
+
172
+
173
+ def _read_required(path: Path) -> str:
174
+ if not path.exists():
175
+ raise FileNotFoundError(path)
176
+ return path.read_text(encoding="utf-8")
177
+
178
+
179
+ def _active_prompt_path() -> Path:
180
+ if CUSTOM_PROMPT.exists() and CUSTOM_PROMPT.stat().st_size > 0:
181
+ return CUSTOM_PROMPT
182
+ return DEFAULT_PROMPT
183
+
184
+
185
+ def _prompt_write_enabled() -> bool:
186
+ value = os.environ.get(PROMPT_WRITE_ENV, "")
187
+ return value.lower() in {"1", "true", "yes", "on"}
188
+
189
+
190
+ def _require_prompt_write_enabled() -> None:
191
+ if not _prompt_write_enabled():
192
+ raise PermissionError(
193
+ f"Prompt write tools are disabled. Set {PROMPT_WRITE_ENV}=1 in the MCP server environment for local development."
194
+ )
195
+
196
+
197
+ def _prompt_status() -> dict[str, Any]:
198
+ source = _active_prompt_path()
199
+ content = _read_required(source)
200
+ digest = hashlib.sha256(content.encode("utf-8")).hexdigest()
201
+ return {
202
+ "ok": True,
203
+ "active": "custom" if source == CUSTOM_PROMPT else "default",
204
+ "configured": True,
205
+ "bytes": len(content.encode("utf-8")),
206
+ "sha256": digest,
207
+ "content_protected": True,
208
+ "prompt_write_enabled": _prompt_write_enabled(),
209
+ "message": "Prompt contents are protected and are not returned by MCP. Use high-level capability docs or enable prompt write tools explicitly for local development.",
210
+ }
211
+
212
+
213
+ def _parse_frontmatter(text: str) -> dict[str, str]:
214
+ if not text.startswith("---\n"):
215
+ return {}
216
+ end = text.find("\n---\n", 4)
217
+ if end < 0:
218
+ return {}
219
+ block = text[4:end]
220
+ data: dict[str, str] = {}
221
+ current_key: str | None = None
222
+ for line in block.splitlines():
223
+ if re.match(r"^[A-Za-z0-9_-]+:\s*", line):
224
+ key, value = line.split(":", 1)
225
+ current_key = key.strip()
226
+ data[current_key] = value.strip().strip('"')
227
+ elif current_key and line.startswith(" "):
228
+ data[current_key] += " " + line.strip().strip('"')
229
+ return data
230
+
231
+
232
+ def _list_skills() -> dict[str, Any]:
233
+ skills = []
234
+ for skill_file in sorted(SKILLS_DIR.glob("*/SKILL.md")):
235
+ text = skill_file.read_text(encoding="utf-8")
236
+ fm = _parse_frontmatter(text)
237
+ skills.append(
238
+ {
239
+ "name": fm.get("name", skill_file.parent.name),
240
+ "description": fm.get("description", ""),
241
+ }
242
+ )
243
+ return {
244
+ "skills": skills,
245
+ "docs": "Use read_agent_docs for public capability guidance. Internal file paths are protected.",
246
+ "prompt": _prompt_status(),
247
+ }
248
+
249
+
250
+ def _public_agent_docs() -> str:
251
+ return """# TradingView Toolkit Agent Capabilities
252
+
253
+ This public MCP document lists what the agent can do, not how the toolkit is implemented.
254
+ Protected content is not exposed here: system/developer prompts, generated startup context,
255
+ skill source text, MCP source, package extension implementation details, installer internals,
256
+ model source/architecture, browser session data, cookies, and API keys.
257
+
258
+ ## Capabilities
259
+
260
+ - Resolve TradingView symbols for stocks, ETFs, listed funds, indexes, crypto, forex, futures, and other supported symbols.
261
+ - Fetch current market state and OHLCV history.
262
+ - Analyze PineScript-compatible technical indicators. MACD is the default unless the user asks for another indicator.
263
+ - Run the server-side stock/ETF/index forecast workflow when applicable and available.
264
+ - Skip the stock model for crypto, forex, futures, commodities, and perpetual pairs unless explicitly asked to test it.
265
+ - Fetch recent online news when available and combine it with technicals, fundamentals, asset metrics, and model output when applicable.
266
+ - Produce the configured market-analysis report format with an agent-evaluated Rating, Buy score, and Sell score.
267
+ - Store user-provided optional API/session values safely when the credential-storage workflow is explicitly requested.
268
+
269
+ ## Public MCP Tools
270
+
271
+ - training-view-data: market data, symbol search, screener, model forecast, and raw TradingView technical rating when explicitly requested.
272
+ - training-view-browser: TradingView browser/session checks and PineScript push workflows.
273
+ - training-view-agent: public capability docs, install guidance, and protected prompt status.
274
+
275
+ ## Prompt Protection
276
+
277
+ Prompt status is available through get_prompt_status, but prompt text is intentionally not returned.
278
+ Prompt write tools are disabled by default and require an explicit local-development environment flag.
279
+ """
280
+
281
+
282
+ def _public_install_docs() -> str:
283
+ return """# TradingView Toolkit Installation
284
+
285
+ Use the public install commands from the project root:
286
+
287
+ ```bash
288
+ make install
289
+ make agent-import PLATFORM=auto
290
+ ```
291
+
292
+ Reinstall only when explicitly needed:
293
+
294
+ ```bash
295
+ FORCE_INSTALL=1 make install
296
+ make agent-import PLATFORM=auto
297
+ ```
298
+
299
+ Optional API/session prompts are skipped by default. Prompt text, installer internals,
300
+ generated startup context, and package-extension implementation details are protected
301
+ and are not exposed through MCP.
302
+ """
303
+
304
+
305
+ def _run_installer() -> dict[str, Any]:
306
+ if not INSTALLER.exists():
307
+ raise FileNotFoundError(INSTALLER)
308
+ proc = subprocess.run(
309
+ ["bash", str(INSTALLER)],
310
+ cwd=PROJECT_ROOT,
311
+ text=True,
312
+ capture_output=True,
313
+ check=False,
314
+ )
315
+ return {
316
+ "returncode": proc.returncode,
317
+ "stdout": proc.stdout,
318
+ "stderr": proc.stderr,
319
+ }
320
+
321
+
322
+ def _apply() -> dict[str, Any]:
323
+ result = _run_installer()
324
+ return {
325
+ "ok": result["returncode"] == 0,
326
+ "prompt": _prompt_status(),
327
+ "installer": result,
328
+ }
329
+
330
+
331
+ def _init_custom(apply: bool = True) -> dict[str, Any]:
332
+ _read_required(DEFAULT_PROMPT)
333
+ created = False
334
+ if not CUSTOM_PROMPT.exists() or CUSTOM_PROMPT.stat().st_size == 0:
335
+ CUSTOM_PROMPT.write_text(DEFAULT_PROMPT.read_text(encoding="utf-8"), encoding="utf-8")
336
+ created = True
337
+ payload: dict[str, Any] = {
338
+ "ok": True,
339
+ "created": created,
340
+ "custom_prompt": str(CUSTOM_PROMPT.relative_to(PROJECT_ROOT)),
341
+ }
342
+ if apply:
343
+ payload["apply"] = _apply()
344
+ return payload
345
+
346
+
347
+ def _set_custom(content: str, apply: bool = True) -> dict[str, Any]:
348
+ CUSTOM_PROMPT.write_text(content.rstrip() + "\n", encoding="utf-8")
349
+ payload: dict[str, Any] = {
350
+ "ok": True,
351
+ "custom_prompt": str(CUSTOM_PROMPT.relative_to(PROJECT_ROOT)),
352
+ "bytes": CUSTOM_PROMPT.stat().st_size,
353
+ }
354
+ if apply:
355
+ payload["apply"] = _apply()
356
+ return payload
357
+
358
+
359
+ def _append_custom(content: str, heading: str | None = None, apply: bool = True) -> dict[str, Any]:
360
+ _init_custom(apply=False)
361
+ block = "\n\n"
362
+ if heading:
363
+ block += f"## {heading.strip()}\n\n"
364
+ block += content.rstrip() + "\n"
365
+ with CUSTOM_PROMPT.open("a", encoding="utf-8") as f:
366
+ f.write(block)
367
+ payload: dict[str, Any] = {
368
+ "ok": True,
369
+ "custom_prompt": str(CUSTOM_PROMPT.relative_to(PROJECT_ROOT)),
370
+ "bytes": CUSTOM_PROMPT.stat().st_size,
371
+ }
372
+ if apply:
373
+ payload["apply"] = _apply()
374
+ return payload
375
+
376
+
377
+ def _reset_custom(apply: bool = True) -> dict[str, Any]:
378
+ existed = CUSTOM_PROMPT.exists()
379
+ CUSTOM_PROMPT.unlink(missing_ok=True)
380
+ payload: dict[str, Any] = {"ok": True, "removed_custom": existed}
381
+ if apply:
382
+ payload["apply"] = _apply()
383
+ return payload
384
+
385
+
386
+ async def _run() -> None:
387
+ async with stdio_server() as (read_stream, write_stream):
388
+ await server.run(
389
+ read_stream, write_stream, server.create_initialization_options()
390
+ )
391
+
392
+
393
+ def main() -> None:
394
+ asyncio.run(_run())
395
+
396
+
397
+ if __name__ == "__main__":
398
+ main()
@@ -0,0 +1,133 @@
1
+ """MCP server: TradingView browser automation.
2
+
3
+ Tools:
4
+ session_check — verify the saved profile / cookie is still logged in
5
+ push_script — paste a .pine file into TV's Pine Editor, save, optionally apply + screenshot
6
+ screenshot_chart — open a chart, optionally change symbol, save a screenshot
7
+
8
+ Register in claude_desktop_config.json:
9
+ {
10
+ "mcpServers": {
11
+ "training-view-browser": {
12
+ "command": "training-view-mcp-browser",
13
+ "env": {
14
+ "TV_SESSIONID": "...",
15
+ "TV_BROWSER_HEADLESS": "true"
16
+ }
17
+ }
18
+ }
19
+ }
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import json
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from mcp.server import Server
29
+ from mcp.server.stdio import stdio_server
30
+ from mcp.types import TextContent, Tool
31
+
32
+ server = Server("training-view-browser")
33
+
34
+
35
+ @server.list_tools()
36
+ async def list_tools() -> list[Tool]:
37
+ return [
38
+ Tool(
39
+ name="session_check",
40
+ description="Verify that the saved Playwright profile / session cookie can log into TradingView.",
41
+ inputSchema={"type": "object", "properties": {}},
42
+ ),
43
+ Tool(
44
+ name="push_script",
45
+ description=(
46
+ "Open TradingView, paste a .pine script into the Pine Editor, save it, "
47
+ "optionally apply it to a symbol's chart, and optionally screenshot. "
48
+ "Requires the user to be already logged in (session cookie or `training-view browser login` once)."
49
+ ),
50
+ inputSchema={
51
+ "type": "object",
52
+ "properties": {
53
+ "script_path": {
54
+ "type": "string",
55
+ "description": "Absolute or relative path to the .pine file",
56
+ },
57
+ "apply_to": {
58
+ "type": "string",
59
+ "description": "Optional symbol (e.g. 'NASDAQ:AAPL') to switch the chart to",
60
+ },
61
+ "screenshot": {"type": "boolean", "default": False},
62
+ "script_name": {
63
+ "type": "string",
64
+ "description": "Name to save the script under in TradingView (default: filename)",
65
+ },
66
+ },
67
+ "required": ["script_path"],
68
+ },
69
+ ),
70
+ Tool(
71
+ name="screenshot_chart",
72
+ description="Open a chart on TradingView and save a screenshot.",
73
+ inputSchema={
74
+ "type": "object",
75
+ "properties": {
76
+ "symbol": {"type": "string"},
77
+ "layout_url": {
78
+ "type": "string",
79
+ "description": "Full URL of a saved TV layout (overrides default)",
80
+ },
81
+ "out_path": {"type": "string", "description": "Where to save the PNG"},
82
+ },
83
+ },
84
+ ),
85
+ ]
86
+
87
+
88
+ @server.call_tool()
89
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
90
+ if name == "session_check":
91
+ from ..browser.automation import session_check_async
92
+
93
+ r = await session_check_async()
94
+ return [TextContent(type="text", text=json.dumps(r, default=str))]
95
+
96
+ if name == "push_script":
97
+ from ..browser.automation import push_script_async
98
+
99
+ r = await push_script_async(
100
+ Path(arguments["script_path"]),
101
+ apply_to=arguments.get("apply_to"),
102
+ screenshot=arguments.get("screenshot", False),
103
+ script_name=arguments.get("script_name"),
104
+ )
105
+ return [TextContent(type="text", text=json.dumps(r, default=str))]
106
+
107
+ if name == "screenshot_chart":
108
+ from ..browser.automation import screenshot_chart_async
109
+
110
+ out = arguments.get("out_path")
111
+ path = await screenshot_chart_async(
112
+ symbol=arguments.get("symbol"),
113
+ layout_url=arguments.get("layout_url"),
114
+ out_path=Path(out) if out else None,
115
+ )
116
+ return [TextContent(type="text", text=json.dumps({"screenshot": str(path)}))]
117
+
118
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
119
+
120
+
121
+ async def _run() -> None:
122
+ async with stdio_server() as (read_stream, write_stream):
123
+ await server.run(
124
+ read_stream, write_stream, server.create_initialization_options()
125
+ )
126
+
127
+
128
+ def main() -> None:
129
+ asyncio.run(_run())
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()