pinescript-mcp 0.7.2__tar.gz → 0.7.4__tar.gz

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. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/PKG-INFO +2 -2
  2. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/README.md +1 -1
  3. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/pyproject.toml +1 -1
  4. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/__init__.py +1 -1
  5. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/server.py +74 -11
  6. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/.gitignore +0 -0
  7. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/LICENSE +0 -0
  8. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/__main__.py +0 -0
  9. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/LLM_MANIFEST.md +0 -0
  10. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/colors_and_display.md +0 -0
  11. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/common_errors.md +0 -0
  12. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/execution_model.md +0 -0
  13. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/methods.md +0 -0
  14. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/objects.md +0 -0
  15. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/concepts/timeframes.md +0 -0
  16. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/pine_v6_functions.json +0 -0
  17. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/annotations.md +0 -0
  18. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/constants.md +0 -0
  19. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/collections.md +0 -0
  20. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/drawing.md +0 -0
  21. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/general.md +0 -0
  22. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/request.md +0 -0
  23. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/strategy.md +0 -0
  24. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/functions/ta.md +0 -0
  25. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/keywords.md +0 -0
  26. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/migration_v5_to_v6.md +0 -0
  27. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/operators.md +0 -0
  28. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/pine_v6_cheatsheet.md +0 -0
  29. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/pine_v6_functions.json +0 -0
  30. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/types.md +0 -0
  31. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/reference/variables.md +0 -0
  32. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/backgrounds.md +0 -0
  33. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/bar_coloring.md +0 -0
  34. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/bar_plotting.md +0 -0
  35. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/colors.md +0 -0
  36. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/fills.md +0 -0
  37. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/levels.md +0 -0
  38. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/lines_and_boxes.md +0 -0
  39. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/overview.md +0 -0
  40. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/plots.md +0 -0
  41. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/tables.md +0 -0
  42. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/visuals/texts_and_shapes.md +0 -0
  43. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/writing_scripts/debugging.md +0 -0
  44. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/writing_scripts/limitations.md +0 -0
  45. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/writing_scripts/profiling_and_optimization.md +0 -0
  46. {pinescript_mcp-0.7.2 → pinescript_mcp-0.7.4}/src/pinescript_mcp/docs/writing_scripts/style_guide.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pinescript-mcp
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: MCP server providing Pine Script v6 documentation for AI assistants
5
5
  Project-URL: Homepage, https://github.com/paulieb89/pinescript-mcp
6
6
  Project-URL: Documentation, https://github.com/paulieb89/pinescript-mcp#readme
@@ -121,7 +121,7 @@ Documentation is bundled in the package — each version contains a frozen snaps
121
121
  "mcpServers": {
122
122
  "pinescript-docs": {
123
123
  "command": "uvx",
124
- "args": ["pinescript-mcp==0.7.0"]
124
+ "args": ["pinescript-mcp==0.7.4"]
125
125
  }
126
126
  }
127
127
  }
@@ -91,7 +91,7 @@ Documentation is bundled in the package — each version contains a frozen snaps
91
91
  "mcpServers": {
92
92
  "pinescript-docs": {
93
93
  "command": "uvx",
94
- "args": ["pinescript-mcp==0.7.0"]
94
+ "args": ["pinescript-mcp==0.7.4"]
95
95
  }
96
96
  }
97
97
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pinescript-mcp"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  description = "MCP server providing Pine Script v6 documentation for AI assistants"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,4 +4,4 @@ Provides tools to list, search, and read Pine Script v6 documentation
4
4
  for use with AI assistants like Claude.
5
5
  """
6
6
 
7
- __version__ = "0.7.1"
7
+ __version__ = "0.7.4"
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  from typing import Literal
12
12
 
13
13
  from fastmcp import FastMCP, Context
14
+ from fastmcp.exceptions import ToolError
14
15
  from fastmcp.server.context import _current_transport
15
16
  from fastmcp.server.middleware.logging import StructuredLoggingMiddleware
16
17
  from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
@@ -325,6 +326,15 @@ TOPIC_MAP = {
325
326
  "migration": "reference/migration_v5_to_v6.md",
326
327
  }
327
328
 
329
+ # Known invalid/renamed functions → specific replacement hints for validate_function()
330
+ KNOWN_REPLACEMENTS: dict[str, str] = {
331
+ "ta.adx": "ta.adx() does NOT exist. Use ta.dmi(diLen, adxSmoothing) → returns [diPlus, diMinus, adx] as a tuple.",
332
+ "ta.sum": "ta.sum() does NOT exist. Use math.sum(source, length) instead.",
333
+ "security": "security() was renamed in v5. Use request.security() instead.",
334
+ "study": "study() was renamed in v5. Use indicator() instead.",
335
+ "input": "input() works but prefer typed variants: input.int(), input.float(), input.string(), input.bool(), etc.",
336
+ }
337
+
328
338
 
329
339
  def _find_section(content: str, header: str, include_children: bool = True) -> tuple[str, int, int]:
330
340
  """Find a section in markdown content by header text.
@@ -470,7 +480,7 @@ async def list_sections(path: str):
470
480
  return "\n".join(headers)
471
481
  except ValueError as e:
472
482
  log["error"] = str(e)
473
- return f"Error: {e}"
483
+ raise ToolError(str(e))
474
484
 
475
485
 
476
486
  @mcp.tool(
@@ -503,7 +513,7 @@ async def get_doc(path: str, limit: int = 0, offset: int = 0):
503
513
 
504
514
  if limit > 0:
505
515
  if offset >= total:
506
- return f"Error: offset {offset} exceeds file size ({total} chars). Use offset < {total}."
516
+ raise ToolError(f"offset {offset} exceeds file size ({total} chars). Use offset < {total}.")
507
517
  end = min(offset + limit, total)
508
518
  content = content[offset:end]
509
519
  has_more = end < total
@@ -515,7 +525,7 @@ async def get_doc(path: str, limit: int = 0, offset: int = 0):
515
525
  return content
516
526
  except ValueError as e:
517
527
  log["error"] = str(e)
518
- return f"Error: {e}"
528
+ raise ToolError(str(e))
519
529
 
520
530
 
521
531
  @mcp.tool(
@@ -543,7 +553,7 @@ async def get_section(path: str, header: str, include_children: bool = True):
543
553
  return f"# {path} (lines {start_line}-{end_line})\n\n{section}"
544
554
  except ValueError as e:
545
555
  log["error"] = str(e)
546
- return f"Error: {e}"
556
+ raise ToolError(str(e))
547
557
 
548
558
 
549
559
  @mcp.tool(
@@ -556,14 +566,19 @@ async def search_docs(query: str, max_results: int = 5):
556
566
  Finds sections containing the query and returns previews with
557
567
  get_section() call hints so you can read the full content.
558
568
 
569
+ Multi-word queries use AND logic: all terms must appear in the
570
+ section (not necessarily on the same line).
571
+
559
572
  Args:
560
- query: Exact string to search for (case-insensitive).
573
+ query: Search terms (case-insensitive). Multi-word queries
574
+ match sections containing ALL terms.
561
575
  max_results: Maximum sections to return (default: 5)
562
576
 
563
577
  Returns matching sections ranked by relevance with get_section() hints.
564
578
  """
565
579
  with _timed_tool("search_docs", query=query, max_results=max_results) as log:
566
- pattern = re.compile(re.escape(query), re.IGNORECASE)
580
+ tokens = query.strip().split()
581
+ patterns = [re.compile(re.escape(t), re.IGNORECASE) for t in tokens]
567
582
  section_hits = []
568
583
 
569
584
  for rel_path in DOCS.keys():
@@ -580,8 +595,9 @@ async def search_docs(query: str, max_results: int = 5):
580
595
  if header_match:
581
596
  # Close previous section — check for hits
582
597
  section_lines = lines[current_start:i]
583
- match_count = sum(1 for l in section_lines if pattern.search(l))
584
- if match_count and current_header != "(preamble)":
598
+ section_text = "\n".join(section_lines)
599
+ if all(p.search(section_text) for p in patterns) and current_header != "(preamble)":
600
+ match_count = sum(sum(1 for p in patterns if p.search(l)) for l in section_lines)
585
601
  section_hits.append({
586
602
  "file": rel_path,
587
603
  "header": current_header,
@@ -596,8 +612,9 @@ async def search_docs(query: str, max_results: int = 5):
596
612
 
597
613
  # Final section
598
614
  section_lines = lines[current_start:]
599
- match_count = sum(1 for l in section_lines if pattern.search(l))
600
- if match_count and current_header != "(preamble)":
615
+ section_text = "\n".join(section_lines)
616
+ if all(p.search(section_text) for p in patterns) and current_header != "(preamble)":
617
+ match_count = sum(sum(1 for p in patterns if p.search(l)) for l in section_lines)
601
618
  section_hits.append({
602
619
  "file": rel_path,
603
620
  "header": current_header,
@@ -687,6 +704,10 @@ async def validate_function(fn_name: str) -> ValidationResult:
687
704
  if fn_name in PINE_V6_TOPLEVEL:
688
705
  return ValidationResult(valid=True, type="toplevel", function=fn_name)
689
706
 
707
+ # Check known invalid/renamed functions before generic fallback
708
+ if fn_name in KNOWN_REPLACEMENTS:
709
+ return ValidationResult(valid=False, type=None, function=fn_name, suggestion=KNOWN_REPLACEMENTS[fn_name])
710
+
690
711
  if "." in fn_name:
691
712
  ns = fn_name.rpartition(".")[0]
692
713
  if ns in PINE_V6_NAMESPACES:
@@ -738,6 +759,16 @@ async def resolve_topic(query: str) -> ResolveResult:
738
759
 
739
760
  log["matches_found"] = len(path_scores)
740
761
 
762
+ if not path_scores:
763
+ # Fallback: scan docs for an exact substring match before returning empty
764
+ fallback_pattern = re.compile(re.escape(query), re.IGNORECASE)
765
+ for rel_path in DOCS:
766
+ doc_lines = _get_doc_lines(rel_path)
767
+ if doc_lines and any(fallback_pattern.search(l) for l in doc_lines):
768
+ path_scores[rel_path] = [query]
769
+ break
770
+ log["fallback_used"] = bool(path_scores)
771
+
741
772
  if not path_scores:
742
773
  return ResolveResult(
743
774
  matches=[],
@@ -930,6 +961,31 @@ async def metrics(request):
930
961
  return Response(generate_latest(METRICS_REGISTRY), media_type=CONTENT_TYPE_LATEST)
931
962
 
932
963
 
964
+ class _AcceptNormalizer:
965
+ """Normalize Accept header on /mcp to prevent 406 Not Acceptable.
966
+
967
+ Workaround for modelcontextprotocol/python-sdk#2349 — the MCP SDK
968
+ requires both application/json AND text/event-stream in the Accept
969
+ header (even in SSE mode), but Anthropic's MCP proxy and other
970
+ clients send them separately per request type. Stamp the combined
971
+ value on /mcp only; SSE paths (/sse, /messages) pass through.
972
+ """
973
+ def __init__(self, app, mcp_path: str = "/mcp"):
974
+ self.app = app
975
+ self._mcp_path = mcp_path.rstrip("/")
976
+
977
+ async def __call__(self, scope, receive, send):
978
+ if scope.get("type") == "http" and scope.get("path", "").rstrip("/") == self._mcp_path:
979
+ headers = [
980
+ (b"accept", b"application/json, text/event-stream")
981
+ if name.lower() == b"accept"
982
+ else (name, value)
983
+ for name, value in scope.get("headers", [])
984
+ ]
985
+ scope = {**scope, "headers": headers}
986
+ await self.app(scope, receive, send)
987
+
988
+
933
989
  def main():
934
990
  """Entry point for the CLI."""
935
991
  import argparse
@@ -965,7 +1021,14 @@ def main():
965
1021
  else:
966
1022
  await streamable_app(scope, receive, send)
967
1023
 
968
- config = uvicorn.Config(app, host=args.host, port=args.port, log_level="info")
1024
+ config = uvicorn.Config(
1025
+ _AcceptNormalizer(app),
1026
+ host=args.host,
1027
+ port=args.port,
1028
+ log_level="info",
1029
+ forwarded_allow_ips="*",
1030
+ proxy_headers=True,
1031
+ )
969
1032
  server = uvicorn.Server(config)
970
1033
  asyncio.run(server.serve())
971
1034
  else:
File without changes