mathpix 0.1.0 → 0.1.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +115 -2
  4. data/SECURITY.md +1 -1
  5. data/bin/mathpix-mcp +55 -0
  6. data/lib/mathpix/batch.rb +7 -8
  7. data/lib/mathpix/batched_document_conversion.rb +238 -0
  8. data/lib/mathpix/client.rb +33 -27
  9. data/lib/mathpix/configuration.rb +5 -9
  10. data/lib/mathpix/conversion.rb +2 -6
  11. data/lib/mathpix/document.rb +47 -12
  12. data/lib/mathpix/document_batcher.rb +191 -0
  13. data/lib/mathpix/mcp/auth/oauth_provider.rb +8 -9
  14. data/lib/mathpix/mcp/base_tool.rb +8 -5
  15. data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +8 -11
  16. data/lib/mathpix/mcp/elicitations/base_elicitation.rb +2 -0
  17. data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +2 -1
  18. data/lib/mathpix/mcp/elicitations.rb +1 -1
  19. data/lib/mathpix/mcp/middleware/cors_middleware.rb +2 -6
  20. data/lib/mathpix/mcp/middleware/oauth_middleware.rb +2 -6
  21. data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +19 -18
  22. data/lib/mathpix/mcp/resources/formats_list_resource.rb +54 -54
  23. data/lib/mathpix/mcp/resources/hierarchical_router.rb +9 -18
  24. data/lib/mathpix/mcp/resources/latest_snip_resource.rb +22 -22
  25. data/lib/mathpix/mcp/resources/recent_snips_resource.rb +11 -10
  26. data/lib/mathpix/mcp/resources/snip_stats_resource.rb +14 -12
  27. data/lib/mathpix/mcp/server.rb +18 -18
  28. data/lib/mathpix/mcp/tools/batch_convert_tool.rb +31 -37
  29. data/lib/mathpix/mcp/tools/check_document_status_tool.rb +5 -5
  30. data/lib/mathpix/mcp/tools/convert_document_tool.rb +15 -14
  31. data/lib/mathpix/mcp/tools/convert_image_tool.rb +15 -14
  32. data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +13 -13
  33. data/lib/mathpix/mcp/tools/get_account_info_tool.rb +1 -1
  34. data/lib/mathpix/mcp/tools/get_usage_tool.rb +5 -7
  35. data/lib/mathpix/mcp/tools/list_formats_tool.rb +30 -30
  36. data/lib/mathpix/mcp/tools/search_results_tool.rb +13 -14
  37. data/lib/mathpix/mcp/transports/http_streaming_transport.rb +129 -118
  38. data/lib/mathpix/mcp/transports/sse_stream_handler.rb +37 -35
  39. data/lib/mathpix/result.rb +3 -2
  40. data/lib/mathpix/version.rb +1 -1
  41. data/lib/mathpix.rb +3 -1
  42. metadata +75 -12
@@ -12,33 +12,33 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + batch iteration
14
14
  class BatchConvertTool < BaseTool
15
- description "Convert multiple images in batch using Mathpix OCR"
15
+ description 'Convert multiple images in batch using Mathpix OCR'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  image_paths: {
20
- type: "array",
21
- items: { type: "string" },
22
- description: "Array of image paths or URLs to process"
20
+ type: 'array',
21
+ items: { type: 'string' },
22
+ description: 'Array of image paths or URLs to process'
23
23
  },
24
24
  formats: {
25
- type: "array",
26
- items: { type: "string" },
27
- description: "Output formats for all images: latex, text, mathml, asciimath (default: latex_styled, text)"
25
+ type: 'array',
26
+ items: { type: 'string' },
27
+ description: 'Output formats for all images: latex, text, mathml, asciimath (default: latex_styled, text)'
28
28
  },
29
29
  parallel: {
30
- type: "boolean",
31
- description: "Process images in parallel (default: false)"
30
+ type: 'boolean',
31
+ description: 'Process images in parallel (default: false)'
32
32
  },
33
33
  max_parallel: {
34
- type: "number",
35
- description: "Maximum number of parallel requests (default: 3)"
34
+ type: 'number',
35
+ description: 'Maximum number of parallel requests (default: 3)'
36
36
  }
37
37
  },
38
- required: ["image_paths"]
38
+ required: ['image_paths']
39
39
  )
40
40
 
41
- def self.call(image_paths:, formats: nil, parallel: false, max_parallel: 3, server_context:)
41
+ def self.call(image_paths:, server_context:, formats: nil, parallel: false, max_parallel: 3)
42
42
  safe_execute do
43
43
  client = mathpix_client(server_context)
44
44
 
@@ -51,13 +51,11 @@ module Mathpix
51
51
  end
52
52
 
53
53
  # Process images
54
- results = if parallel
54
+ if parallel
55
55
  # TODO: Implement parallel processing (requires threading)
56
56
  # For now, process sequentially
57
- process_batch_sequential(client, normalized_paths, output_formats)
58
- else
59
- process_batch_sequential(client, normalized_paths, output_formats)
60
57
  end
58
+ results = process_batch_sequential(client, normalized_paths, output_formats)
61
59
 
62
60
  # Format response
63
61
  response_data = {
@@ -76,28 +74,24 @@ module Mathpix
76
74
  end
77
75
  end
78
76
 
79
- private
80
-
81
77
  def self.process_batch_sequential(client, paths, formats)
82
78
  paths.map.with_index do |path, index|
83
- begin
84
- result = client.snap(path, formats: formats)
85
- {
86
- index: index,
87
- image_path: path,
88
- success: true,
89
- latex: result.latex,
90
- text: result.text,
91
- confidence: result.confidence
92
- }
93
- rescue Mathpix::Error => e
94
- {
95
- index: index,
96
- image_path: path,
97
- success: false,
98
- error: e.message
99
- }
100
- end
79
+ result = client.snap(path, formats: formats)
80
+ {
81
+ index: index,
82
+ image_path: path,
83
+ success: true,
84
+ latex: result.latex,
85
+ text: result.text,
86
+ confidence: result.confidence
87
+ }
88
+ rescue Mathpix::Error => e
89
+ {
90
+ index: index,
91
+ image_path: path,
92
+ success: false,
93
+ error: e.message
94
+ }
101
95
  end
102
96
  end
103
97
  end
@@ -12,16 +12,16 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + status polling
14
14
  class CheckDocumentStatusTool < BaseTool
15
- description "Check the status of a document conversion (PDF, DOCX, PPTX)"
15
+ description 'Check the status of a document conversion (PDF, DOCX, PPTX)'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  conversion_id: {
20
- type: "string",
21
- description: "Document conversion ID returned from convert_document"
20
+ type: 'string',
21
+ description: 'Document conversion ID returned from convert_document'
22
22
  }
23
23
  },
24
- required: ["conversion_id"]
24
+ required: ['conversion_id']
25
25
  )
26
26
 
27
27
  def self.call(conversion_id:, server_context:)
@@ -52,7 +52,7 @@ module Mathpix
52
52
  end
53
53
 
54
54
  # Add error info if failed
55
- if status_data['status'] == 'error' || status_data['status'] == 'failed'
55
+ if %w[error failed].include?(status_data['status'])
56
56
  response_data[:error] = status_data['error']
57
57
  response_data[:error_info] = status_data['error_info']
58
58
  end
@@ -12,36 +12,37 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + Document class delegation
14
14
  class ConvertDocumentTool < BaseTool
15
- description "Convert document (PDF, DOCX, PPTX) to Markdown, LaTeX, HTML, or other formats using Mathpix OCR"
15
+ description 'Convert document (PDF, DOCX, PPTX) to Markdown, LaTeX, HTML, or other formats using Mathpix OCR'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  document_path: {
20
- type: "string",
21
- description: "Path to document file or URL (PDF, DOCX, or PPTX)"
20
+ type: 'string',
21
+ description: 'Path to document file or URL (PDF, DOCX, or PPTX)'
22
22
  },
23
23
  formats: {
24
- type: "array",
25
- items: { type: "string" },
26
- description: "Output formats: markdown, latex, html, docx (default: markdown)"
24
+ type: 'array',
25
+ items: { type: 'string' },
26
+ description: 'Output formats: markdown, latex, html, docx (default: markdown)'
27
27
  },
28
28
  include_tables: {
29
- type: "boolean",
30
- description: "Include table extraction as HTML"
29
+ type: 'boolean',
30
+ description: 'Include table extraction as HTML'
31
31
  },
32
32
  max_wait: {
33
- type: "number",
34
- description: "Maximum wait time in seconds for conversion (default: 600)"
33
+ type: 'number',
34
+ description: 'Maximum wait time in seconds for conversion (default: 600)'
35
35
  },
36
36
  poll_interval: {
37
- type: "number",
38
- description: "Polling interval in seconds (default: 3.0)"
37
+ type: 'number',
38
+ description: 'Polling interval in seconds (default: 3.0)'
39
39
  }
40
40
  },
41
- required: ["document_path"]
41
+ required: ['document_path']
42
42
  )
43
43
 
44
- def self.call(document_path:, formats: nil, include_tables: false, max_wait: 600, poll_interval: 3.0, server_context:)
44
+ def self.call(document_path:, server_context:, formats: nil, include_tables: false, max_wait: 600,
45
+ poll_interval: 3.0)
45
46
  safe_execute do
46
47
  client = mathpix_client(server_context)
47
48
 
@@ -12,36 +12,37 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + core gem delegation
14
14
  class ConvertImageTool < BaseTool
15
- description "Convert image (PNG, JPG, etc.) to LaTeX, text, or other formats using Mathpix OCR"
15
+ description 'Convert image (PNG, JPG, etc.) to LaTeX, text, or other formats using Mathpix OCR'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  image_path: {
20
- type: "string",
21
- description: "Path to image file or URL (http:// or https://)"
20
+ type: 'string',
21
+ description: 'Path to image file or URL (http:// or https://)'
22
22
  },
23
23
  formats: {
24
- type: "array",
25
- items: { type: "string" },
26
- description: "Output formats: latex, text, mathml, asciimath, latex_styled, text_display, data, html (default: latex_styled, text)"
24
+ type: 'array',
25
+ items: { type: 'string' },
26
+ description: 'Output formats: latex, text, mathml, asciimath, latex_styled, text_display, data, html (default: latex_styled, text)'
27
27
  },
28
28
  include_line_data: {
29
- type: "boolean",
30
- description: "Include line-level bounding boxes in response"
29
+ type: 'boolean',
30
+ description: 'Include line-level bounding boxes in response'
31
31
  },
32
32
  include_word_data: {
33
- type: "boolean",
34
- description: "Include word-level bounding boxes in response"
33
+ type: 'boolean',
34
+ description: 'Include word-level bounding boxes in response'
35
35
  },
36
36
  confidence_threshold: {
37
- type: "number",
38
- description: "Minimum confidence threshold (0.0-1.0)"
37
+ type: 'number',
38
+ description: 'Minimum confidence threshold (0.0-1.0)'
39
39
  }
40
40
  },
41
- required: ["image_path"]
41
+ required: ['image_path']
42
42
  )
43
43
 
44
- def self.call(image_path:, formats: nil, include_line_data: false, include_word_data: false, confidence_threshold: nil, server_context:)
44
+ def self.call(image_path:, server_context:, formats: nil, include_line_data: false, include_word_data: false,
45
+ confidence_threshold: nil)
45
46
  safe_execute do
46
47
  client = mathpix_client(server_context)
47
48
 
@@ -12,32 +12,32 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + stroke recognition
14
14
  class ConvertStrokesTool < BaseTool
15
- description "Convert handwritten strokes to LaTeX, text, or other formats using Mathpix OCR"
15
+ description 'Convert handwritten strokes to LaTeX, text, or other formats using Mathpix OCR'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  strokes: {
20
- type: "array",
21
- description: "Array of stroke arrays, where each stroke is an array of [x, y] coordinates"
20
+ type: 'array',
21
+ description: 'Array of stroke arrays, where each stroke is an array of [x, y] coordinates'
22
22
  },
23
23
  formats: {
24
- type: "array",
25
- items: { type: "string" },
26
- description: "Output formats: latex, text, mathml, asciimath (default: latex_styled, text)"
24
+ type: 'array',
25
+ items: { type: 'string' },
26
+ description: 'Output formats: latex, text, mathml, asciimath (default: latex_styled, text)'
27
27
  },
28
28
  width: {
29
- type: "number",
30
- description: "Canvas width for stroke normalization"
29
+ type: 'number',
30
+ description: 'Canvas width for stroke normalization'
31
31
  },
32
32
  height: {
33
- type: "number",
34
- description: "Canvas height for stroke normalization"
33
+ type: 'number',
34
+ description: 'Canvas height for stroke normalization'
35
35
  }
36
36
  },
37
- required: ["strokes"]
37
+ required: ['strokes']
38
38
  )
39
39
 
40
- def self.call(strokes:, formats: nil, width: nil, height: nil, server_context:)
40
+ def self.call(strokes:, server_context:, formats: nil, width: nil, height: nil)
41
41
  safe_execute do
42
42
  client = mathpix_client(server_context)
43
43
 
@@ -57,7 +57,7 @@ module Mathpix
57
57
  # Format response
58
58
  response_data = {
59
59
  success: true,
60
- input_type: "strokes",
60
+ input_type: 'strokes',
61
61
  stroke_count: strokes.length,
62
62
  formats: output_formats,
63
63
  results: {
@@ -12,7 +12,7 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + account management
14
14
  class GetAccountInfoTool < BaseTool
15
- description "Get Mathpix account information, plan details, and limits"
15
+ description 'Get Mathpix account information, plan details, and limits'
16
16
 
17
17
  input_schema(
18
18
  properties: {},
@@ -12,19 +12,19 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + usage tracking
14
14
  class GetUsageTool < BaseTool
15
- description "Get Mathpix API usage statistics and remaining credits"
15
+ description 'Get Mathpix API usage statistics and remaining credits'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  detailed: {
20
- type: "boolean",
21
- description: "Include detailed breakdown by operation type (default: false)"
20
+ type: 'boolean',
21
+ description: 'Include detailed breakdown by operation type (default: false)'
22
22
  }
23
23
  },
24
24
  required: []
25
25
  )
26
26
 
27
- def self.call(detailed: false, server_context:)
27
+ def self.call(server_context:, detailed: false)
28
28
  safe_execute do
29
29
  client = mathpix_client(server_context)
30
30
 
@@ -42,9 +42,7 @@ module Mathpix
42
42
  }
43
43
  }
44
44
 
45
- if detailed && usage_data['breakdown']
46
- response_data[:breakdown] = usage_data['breakdown']
47
- end
45
+ response_data[:breakdown] = usage_data['breakdown'] if detailed && usage_data['breakdown']
48
46
 
49
47
  json_response(response_data)
50
48
  rescue Mathpix::APIError => e
@@ -12,53 +12,53 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + format catalog
14
14
  class ListFormatsTool < BaseTool
15
- description "List all available output formats for Mathpix OCR operations"
15
+ description 'List all available output formats for Mathpix OCR operations'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  category: {
20
- type: "string",
21
- description: "Filter by category: image, document, or all (default: all)",
22
- enum: ["image", "document", "all"]
20
+ type: 'string',
21
+ description: 'Filter by category: image, document, or all (default: all)',
22
+ enum: %w[image document all]
23
23
  }
24
24
  },
25
25
  required: []
26
26
  )
27
27
 
28
- def self.call(category: "all", server_context:)
28
+ def self.call(server_context:, category: 'all')
29
29
  safe_execute do
30
30
  # Static format definitions
31
31
  image_formats = [
32
- { name: "latex_styled", description: "LaTeX with styling", type: "image" },
33
- { name: "text", description: "Plain text", type: "image" },
34
- { name: "latex_list", description: "Array of LaTeX expressions", type: "image" },
35
- { name: "mathml", description: "MathML markup", type: "image" },
36
- { name: "asciimath", description: "AsciiMath notation", type: "image" },
37
- { name: "text_display", description: "Display-style text", type: "image" },
38
- { name: "latex_simplified", description: "Simplified LaTeX", type: "image" },
39
- { name: "data", description: "Full response data with metadata", type: "image" },
40
- { name: "html", description: "HTML markup", type: "image" }
32
+ { name: 'latex_styled', description: 'LaTeX with styling', type: 'image' },
33
+ { name: 'text', description: 'Plain text', type: 'image' },
34
+ { name: 'latex_list', description: 'Array of LaTeX expressions', type: 'image' },
35
+ { name: 'mathml', description: 'MathML markup', type: 'image' },
36
+ { name: 'asciimath', description: 'AsciiMath notation', type: 'image' },
37
+ { name: 'text_display', description: 'Display-style text', type: 'image' },
38
+ { name: 'latex_simplified', description: 'Simplified LaTeX', type: 'image' },
39
+ { name: 'data', description: 'Full response data with metadata', type: 'image' },
40
+ { name: 'html', description: 'HTML markup', type: 'image' }
41
41
  ]
42
42
 
43
43
  document_formats = [
44
- { name: "markdown", description: "Markdown format", type: "document" },
45
- { name: "latex", description: "LaTeX document", type: "document" },
46
- { name: "html", description: "HTML document", type: "document" },
47
- { name: "docx", description: "Microsoft Word document", type: "document" },
48
- { name: "tex.zip", description: "LaTeX with figures (zipped)", type: "document" }
44
+ { name: 'markdown', description: 'Markdown format', type: 'document' },
45
+ { name: 'latex', description: 'LaTeX document', type: 'document' },
46
+ { name: 'html', description: 'HTML document', type: 'document' },
47
+ { name: 'docx', description: 'Microsoft Word document', type: 'document' },
48
+ { name: 'tex.zip', description: 'LaTeX with figures (zipped)', type: 'document' }
49
49
  ]
50
50
 
51
51
  # Filter by category
52
52
  formats = case category
53
- when "image"
54
- image_formats
55
- when "document"
56
- document_formats
57
- when "all"
58
- image_formats + document_formats
59
- else
60
- image_formats + document_formats
61
- end
53
+ when 'image'
54
+ image_formats
55
+ when 'document'
56
+ document_formats
57
+ when 'all'
58
+ image_formats + document_formats
59
+ else
60
+ image_formats + document_formats
61
+ end
62
62
 
63
63
  # Format response
64
64
  response_data = {
@@ -67,8 +67,8 @@ module Mathpix
67
67
  count: formats.length,
68
68
  formats: formats,
69
69
  usage: {
70
- image_capture: "Use with snap() or ConvertImageTool",
71
- document_conversion: "Use with document() or ConvertDocumentTool"
70
+ image_capture: 'Use with snap() or ConvertImageTool',
71
+ document_conversion: 'Use with document() or ConvertDocumentTool'
72
72
  }
73
73
  }
74
74
 
@@ -12,31 +12,31 @@ module Mathpix
12
12
  #
13
13
  # The geodesic path: official SDK + search filtering
14
14
  class SearchResultsTool < BaseTool
15
- description "Search recent Mathpix capture results with optional text filtering"
15
+ description 'Search recent Mathpix capture results with optional text filtering'
16
16
 
17
17
  input_schema(
18
18
  properties: {
19
19
  query: {
20
- type: "string",
21
- description: "Search query to filter results by LaTeX or text content"
20
+ type: 'string',
21
+ description: 'Search query to filter results by LaTeX or text content'
22
22
  },
23
23
  limit: {
24
- type: "number",
25
- description: "Maximum number of results to return (default: 10, max: 100)"
24
+ type: 'number',
25
+ description: 'Maximum number of results to return (default: 10, max: 100)'
26
26
  },
27
27
  offset: {
28
- type: "number",
29
- description: "Offset for pagination (default: 0)"
28
+ type: 'number',
29
+ description: 'Offset for pagination (default: 0)'
30
30
  },
31
31
  include_content: {
32
- type: "boolean",
33
- description: "Include full LaTeX/text content in results (default: false)"
32
+ type: 'boolean',
33
+ description: 'Include full LaTeX/text content in results (default: false)'
34
34
  }
35
35
  },
36
36
  required: []
37
37
  )
38
38
 
39
- def self.call(query: nil, limit: 10, offset: 0, include_content: false, server_context:)
39
+ def self.call(server_context:, query: nil, limit: 10, offset: 0, include_content: false)
40
40
  safe_execute do
41
41
  client = mathpix_client(server_context)
42
42
 
@@ -53,8 +53,8 @@ module Mathpix
53
53
  if query && !query.empty?
54
54
  query_lower = query.downcase
55
55
  results = results.select do |result|
56
- (result.latex&.downcase&.include?(query_lower)) ||
57
- (result.text&.downcase&.include?(query_lower))
56
+ result.latex&.downcase&.include?(query_lower) ||
57
+ result.text&.downcase&.include?(query_lower)
58
58
  end
59
59
  end
60
60
 
@@ -98,11 +98,10 @@ module Mathpix
98
98
  end
99
99
  end
100
100
 
101
- private
102
-
103
101
  def self.truncate(text, max_length)
104
102
  return nil unless text
105
103
  return text if text.length <= max_length
104
+
106
105
  "#{text[0...max_length]}..."
107
106
  end
108
107
  end