mathpix 0.1.0

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +21 -0
  4. data/README.md +171 -0
  5. data/SECURITY.md +137 -0
  6. data/lib/mathpix/balanced_ternary.rb +86 -0
  7. data/lib/mathpix/batch.rb +155 -0
  8. data/lib/mathpix/capture_builder.rb +142 -0
  9. data/lib/mathpix/chemistry.rb +69 -0
  10. data/lib/mathpix/client.rb +439 -0
  11. data/lib/mathpix/configuration.rb +187 -0
  12. data/lib/mathpix/configuration.rb.backup +125 -0
  13. data/lib/mathpix/conversion.rb +257 -0
  14. data/lib/mathpix/document.rb +320 -0
  15. data/lib/mathpix/errors.rb +78 -0
  16. data/lib/mathpix/mcp/auth/oauth_provider.rb +346 -0
  17. data/lib/mathpix/mcp/auth/token_manager.rb +31 -0
  18. data/lib/mathpix/mcp/auth.rb +18 -0
  19. data/lib/mathpix/mcp/base_tool.rb +117 -0
  20. data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +162 -0
  21. data/lib/mathpix/mcp/elicitations/base_elicitation.rb +141 -0
  22. data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +162 -0
  23. data/lib/mathpix/mcp/elicitations.rb +78 -0
  24. data/lib/mathpix/mcp/middleware/cors_middleware.rb +94 -0
  25. data/lib/mathpix/mcp/middleware/oauth_middleware.rb +72 -0
  26. data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +140 -0
  27. data/lib/mathpix/mcp/middleware.rb +13 -0
  28. data/lib/mathpix/mcp/resources/formats_list_resource.rb +113 -0
  29. data/lib/mathpix/mcp/resources/hierarchical_router.rb +237 -0
  30. data/lib/mathpix/mcp/resources/latest_snip_resource.rb +60 -0
  31. data/lib/mathpix/mcp/resources/recent_snips_resource.rb +75 -0
  32. data/lib/mathpix/mcp/resources/snip_stats_resource.rb +78 -0
  33. data/lib/mathpix/mcp/resources.rb +15 -0
  34. data/lib/mathpix/mcp/server.rb +174 -0
  35. data/lib/mathpix/mcp/tools/batch_convert_tool.rb +106 -0
  36. data/lib/mathpix/mcp/tools/check_document_status_tool.rb +66 -0
  37. data/lib/mathpix/mcp/tools/convert_document_tool.rb +90 -0
  38. data/lib/mathpix/mcp/tools/convert_image_tool.rb +91 -0
  39. data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +82 -0
  40. data/lib/mathpix/mcp/tools/get_account_info_tool.rb +57 -0
  41. data/lib/mathpix/mcp/tools/get_usage_tool.rb +62 -0
  42. data/lib/mathpix/mcp/tools/list_formats_tool.rb +81 -0
  43. data/lib/mathpix/mcp/tools/search_results_tool.rb +111 -0
  44. data/lib/mathpix/mcp/transports/http_streaming_transport.rb +622 -0
  45. data/lib/mathpix/mcp/transports/sse_stream_handler.rb +236 -0
  46. data/lib/mathpix/mcp/transports.rb +12 -0
  47. data/lib/mathpix/mcp.rb +52 -0
  48. data/lib/mathpix/result.rb +364 -0
  49. data/lib/mathpix/version.rb +22 -0
  50. data/lib/mathpix.rb +229 -0
  51. metadata +283 -0
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'mcp'
5
+ require 'mcp/transports/stdio' # Transport classes not auto-loaded
6
+ rescue LoadError
7
+ raise LoadError, <<~ERROR
8
+ The 'mcp' gem is required for MCP server functionality.
9
+
10
+ Add to your Gemfile:
11
+ gem 'mcp'
12
+
13
+ Or install directly:
14
+ gem install mcp
15
+
16
+ Official Ruby MCP SDK: https://github.com/modelcontextprotocol/ruby-sdk
17
+ ERROR
18
+ end
19
+
20
+ module Mathpix
21
+ module MCP
22
+ # MCP Server for Mathpix API
23
+ #
24
+ # Uses official Ruby MCP SDK (modelcontextprotocol/ruby-sdk)
25
+ # Provides 9 tools and 4 resources as thin delegates to core Mathpix::Client
26
+ #
27
+ # The geodesic path: official SDK, thin wrapper, thick core
28
+ #
29
+ # @example Start STDIO server
30
+ # require 'mathpix/mcp'
31
+ #
32
+ # Mathpix.configure do |config|
33
+ # config.app_id = ENV['MATHPIX_APP_ID']
34
+ # config.app_key = ENV['MATHPIX_APP_KEY']
35
+ # end
36
+ #
37
+ # Mathpix::MCP::Server.run
38
+ #
39
+ # @example With custom configuration
40
+ # server = Mathpix::MCP::Server.new(
41
+ # name: "mathpix-custom",
42
+ # version: "1.0.0",
43
+ # mathpix_client: custom_client
44
+ # )
45
+ # transport = server.create_stdio_transport
46
+ # transport.open
47
+ class Server
48
+ attr_reader :name, :version, :mathpix_client, :mcp_server
49
+
50
+ # Initialize MCP server
51
+ #
52
+ # @param name [String] server name
53
+ # @param version [String] server version
54
+ # @param mathpix_client [Mathpix::Client] optional client instance
55
+ def initialize(name: "mathpix", version: Mathpix::VERSION, mathpix_client: nil)
56
+ @name = name
57
+ @version = version
58
+ @mathpix_client = mathpix_client || Mathpix.client
59
+ @mcp_server = create_mcp_server
60
+ end
61
+
62
+ # Create STDIO transport (standard MCP transport)
63
+ #
64
+ # @return [::MCP::Transports::StdioTransport]
65
+ def create_stdio_transport
66
+ ::MCP::Transports::StdioTransport.new(@mcp_server)
67
+ end
68
+
69
+ # Run MCP server with STDIO transport (blocking)
70
+ #
71
+ # Standard way to run MCP server via stdio
72
+ def run
73
+ transport = create_stdio_transport
74
+ transport.open
75
+ end
76
+
77
+ # Server capabilities
78
+ #
79
+ # @return [Hash] MCP server capabilities
80
+ def capabilities
81
+ {
82
+ tools: tool_classes.map(&:name),
83
+ resources: resource_specs.map { |r| r[:uri] }
84
+ }
85
+ end
86
+
87
+ # Class method: run server directly
88
+ #
89
+ # @example
90
+ # Mathpix::MCP::Server.run
91
+ def self.run(**options)
92
+ new(**options).run
93
+ end
94
+
95
+ private
96
+
97
+ # Create official MCP::Server with tools and resources
98
+ #
99
+ # Uses official Ruby MCP SDK structure
100
+ def create_mcp_server
101
+ ::MCP::Server.new(
102
+ name: @name,
103
+ version: @version,
104
+ tools: tool_classes,
105
+ resources: build_resources,
106
+ server_context: { mathpix_client: @mathpix_client }
107
+ )
108
+ end
109
+
110
+ # List of all tool classes (using official MCP::Tool)
111
+ #
112
+ # @return [Array<Class>] tool classes
113
+ def tool_classes
114
+ [
115
+ Tools::ConvertImageTool,
116
+ Tools::ConvertDocumentTool,
117
+ Tools::ConvertStrokesTool,
118
+ Tools::BatchConvertTool,
119
+ Tools::CheckDocumentStatusTool,
120
+ Tools::SearchResultsTool,
121
+ Tools::GetUsageTool,
122
+ Tools::GetAccountInfoTool,
123
+ Tools::ListFormatsTool
124
+ ]
125
+ end
126
+
127
+ # Build MCP::Resource objects from specifications
128
+ #
129
+ # @return [Array<::MCP::Resource>] resource objects
130
+ def build_resources
131
+ resource_specs.map do |spec|
132
+ ::MCP::Resource.new(
133
+ uri: spec[:uri],
134
+ name: spec[:name],
135
+ description: spec[:description],
136
+ mime_type: spec[:mime_type]
137
+ )
138
+ end
139
+ end
140
+
141
+ # Resource specifications
142
+ #
143
+ # @return [Array<Hash>] resource specs
144
+ def resource_specs
145
+ [
146
+ {
147
+ uri: "mathpix://snip/recent/latest",
148
+ name: "latest-snip",
149
+ description: "Most recent capture result",
150
+ mime_type: "application/json"
151
+ },
152
+ {
153
+ uri: "mathpix://snip/stats",
154
+ name: "snip-stats",
155
+ description: "Overall capture statistics",
156
+ mime_type: "application/json"
157
+ },
158
+ {
159
+ uri: "mathpix://snip/recent?limit=10",
160
+ name: "recent-snips",
161
+ description: "Recent capture results (default limit 10)",
162
+ mime_type: "application/json"
163
+ },
164
+ {
165
+ uri: "mathpix://formats/list",
166
+ name: "formats-list",
167
+ description: "Available output formats",
168
+ mime_type: "application/json"
169
+ }
170
+ ]
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Batch Convert Tool
9
+ #
10
+ # Converts multiple images in batch for efficiency
11
+ # Thin delegate to Mathpix::Client#snap with batch processing
12
+ #
13
+ # The geodesic path: official SDK + batch iteration
14
+ class BatchConvertTool < BaseTool
15
+ description "Convert multiple images in batch using Mathpix OCR"
16
+
17
+ input_schema(
18
+ properties: {
19
+ image_paths: {
20
+ type: "array",
21
+ items: { type: "string" },
22
+ description: "Array of image paths or URLs to process"
23
+ },
24
+ formats: {
25
+ type: "array",
26
+ items: { type: "string" },
27
+ description: "Output formats for all images: latex, text, mathml, asciimath (default: latex_styled, text)"
28
+ },
29
+ parallel: {
30
+ type: "boolean",
31
+ description: "Process images in parallel (default: false)"
32
+ },
33
+ max_parallel: {
34
+ type: "number",
35
+ description: "Maximum number of parallel requests (default: 3)"
36
+ }
37
+ },
38
+ required: ["image_paths"]
39
+ )
40
+
41
+ def self.call(image_paths:, formats: nil, parallel: false, max_parallel: 3, server_context:)
42
+ safe_execute do
43
+ client = mathpix_client(server_context)
44
+
45
+ # Extract formats or use defaults
46
+ output_formats = extract_formats(formats, client)
47
+
48
+ # Normalize paths
49
+ normalized_paths = image_paths.map do |path|
50
+ url?(path) ? path : normalize_path(path)
51
+ end
52
+
53
+ # Process images
54
+ results = if parallel
55
+ # TODO: Implement parallel processing (requires threading)
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
+ end
61
+
62
+ # Format response
63
+ response_data = {
64
+ success: true,
65
+ batch_size: image_paths.length,
66
+ formats: output_formats,
67
+ results: results,
68
+ summary: {
69
+ total: results.length,
70
+ successful: results.count { |r| r[:success] },
71
+ failed: results.count { |r| !r[:success] }
72
+ }
73
+ }
74
+
75
+ json_response(response_data)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def self.process_batch_sequential(client, paths, formats)
82
+ 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
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Check Document Status Tool
9
+ #
10
+ # Polls document conversion status for async operations
11
+ # Thin delegate to Mathpix::Client#get_document_status
12
+ #
13
+ # The geodesic path: official SDK + status polling
14
+ class CheckDocumentStatusTool < BaseTool
15
+ description "Check the status of a document conversion (PDF, DOCX, PPTX)"
16
+
17
+ input_schema(
18
+ properties: {
19
+ conversion_id: {
20
+ type: "string",
21
+ description: "Document conversion ID returned from convert_document"
22
+ }
23
+ },
24
+ required: ["conversion_id"]
25
+ )
26
+
27
+ def self.call(conversion_id:, server_context:)
28
+ safe_execute do
29
+ client = mathpix_client(server_context)
30
+
31
+ # Delegate to core gem
32
+ status_data = client.get_document_status(conversion_id)
33
+
34
+ # Format response
35
+ response_data = {
36
+ success: true,
37
+ conversion_id: conversion_id,
38
+ status: status_data['status'],
39
+ progress: status_data['percent_done'],
40
+ metadata: {}
41
+ }
42
+
43
+ # Add completion data if available
44
+ if status_data['status'] == 'completed'
45
+ response_data[:metadata][:pages] = status_data['num_pages']
46
+ response_data[:metadata][:processing_time] = status_data['processing_time']
47
+ response_data[:results] = {
48
+ markdown_url: status_data['markdown_url'],
49
+ latex_url: status_data['latex_url'],
50
+ html_url: status_data['html_url']
51
+ }
52
+ end
53
+
54
+ # Add error info if failed
55
+ if status_data['status'] == 'error' || status_data['status'] == 'failed'
56
+ response_data[:error] = status_data['error']
57
+ response_data[:error_info] = status_data['error_info']
58
+ end
59
+
60
+ json_response(response_data)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Convert Document Tool
9
+ #
10
+ # Converts documents (PDF, DOCX, PPTX) to Markdown, LaTeX, or other formats
11
+ # Thin delegate to Mathpix::Document
12
+ #
13
+ # The geodesic path: official SDK + Document class delegation
14
+ class ConvertDocumentTool < BaseTool
15
+ description "Convert document (PDF, DOCX, PPTX) to Markdown, LaTeX, HTML, or other formats using Mathpix OCR"
16
+
17
+ input_schema(
18
+ properties: {
19
+ document_path: {
20
+ type: "string",
21
+ description: "Path to document file or URL (PDF, DOCX, or PPTX)"
22
+ },
23
+ formats: {
24
+ type: "array",
25
+ items: { type: "string" },
26
+ description: "Output formats: markdown, latex, html, docx (default: markdown)"
27
+ },
28
+ include_tables: {
29
+ type: "boolean",
30
+ description: "Include table extraction as HTML"
31
+ },
32
+ max_wait: {
33
+ type: "number",
34
+ description: "Maximum wait time in seconds for conversion (default: 600)"
35
+ },
36
+ poll_interval: {
37
+ type: "number",
38
+ description: "Polling interval in seconds (default: 3.0)"
39
+ }
40
+ },
41
+ required: ["document_path"]
42
+ )
43
+
44
+ def self.call(document_path:, formats: nil, include_tables: false, max_wait: 600, poll_interval: 3.0, server_context:)
45
+ safe_execute do
46
+ client = mathpix_client(server_context)
47
+
48
+ # Normalize path
49
+ document_path = normalize_path(document_path) unless url?(document_path)
50
+
51
+ # Extract formats or use defaults
52
+ output_formats = extract_formats(formats, client)
53
+
54
+ # Use Document class (new unified interface)
55
+ doc = Mathpix::Document.new(client, document_path)
56
+ doc.with_formats(*output_formats)
57
+ doc.with_tables if include_tables
58
+
59
+ # Start conversion
60
+ conversion = doc.convert
61
+
62
+ # Wait for completion
63
+ conversion.wait_until_complete(max_wait: max_wait, poll_interval: poll_interval)
64
+ result = conversion.result
65
+
66
+ # Format response
67
+ response_data = {
68
+ success: true,
69
+ document_path: document_path,
70
+ formats: output_formats,
71
+ conversion_id: conversion.conversion_id,
72
+ results: {
73
+ markdown: result.markdown,
74
+ latex: result.latex,
75
+ html: result.html
76
+ },
77
+ metadata: {
78
+ document_type: conversion.document_type,
79
+ pages: result.pages,
80
+ processing_time: result.processing_time
81
+ }
82
+ }
83
+
84
+ json_response(response_data)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Convert Image Tool
9
+ #
10
+ # Converts images (PNG, JPG, etc.) to LaTeX, text, or other formats
11
+ # Thin delegate to Mathpix::Client#snap
12
+ #
13
+ # The geodesic path: official SDK + core gem delegation
14
+ class ConvertImageTool < BaseTool
15
+ description "Convert image (PNG, JPG, etc.) to LaTeX, text, or other formats using Mathpix OCR"
16
+
17
+ input_schema(
18
+ properties: {
19
+ image_path: {
20
+ type: "string",
21
+ description: "Path to image file or URL (http:// or https://)"
22
+ },
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)"
27
+ },
28
+ include_line_data: {
29
+ type: "boolean",
30
+ description: "Include line-level bounding boxes in response"
31
+ },
32
+ include_word_data: {
33
+ type: "boolean",
34
+ description: "Include word-level bounding boxes in response"
35
+ },
36
+ confidence_threshold: {
37
+ type: "number",
38
+ description: "Minimum confidence threshold (0.0-1.0)"
39
+ }
40
+ },
41
+ required: ["image_path"]
42
+ )
43
+
44
+ def self.call(image_path:, formats: nil, include_line_data: false, include_word_data: false, confidence_threshold: nil, server_context:)
45
+ safe_execute do
46
+ client = mathpix_client(server_context)
47
+
48
+ # Normalize path (expand ~, resolve relative paths)
49
+ image_path = normalize_path(image_path) unless url?(image_path)
50
+
51
+ # Extract formats or use defaults
52
+ output_formats = extract_formats(formats, client)
53
+
54
+ # Build options
55
+ options = {}
56
+ options[:formats] = output_formats
57
+ options[:include_line_data] = true if include_line_data
58
+ options[:include_word_data] = true if include_word_data
59
+ options[:confidence_threshold] = confidence_threshold if confidence_threshold
60
+
61
+ # Delegate to core gem
62
+ result = client.snap(image_path, **options)
63
+
64
+ # Format response
65
+ response_data = {
66
+ success: true,
67
+ image_path: image_path,
68
+ formats: output_formats,
69
+ results: {
70
+ latex: result.latex,
71
+ text: result.text,
72
+ confidence: result.confidence,
73
+ is_printed: result.printed?,
74
+ is_handwritten: result.handwritten?,
75
+ position: result.position
76
+ }
77
+ }
78
+
79
+ # Add optional data
80
+ response_data[:line_data] = result.line_data if include_line_data
81
+ response_data[:word_data] = result.word_data if include_word_data
82
+ response_data[:results][:mathml] = result.mathml if result.mathml
83
+ response_data[:results][:asciimath] = result.asciimath if result.asciimath
84
+
85
+ json_response(response_data)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Convert Strokes Tool
9
+ #
10
+ # Converts handwritten strokes to LaTeX, text, or other formats
11
+ # Thin delegate to Mathpix::Client#snap with strokes format
12
+ #
13
+ # The geodesic path: official SDK + stroke recognition
14
+ class ConvertStrokesTool < BaseTool
15
+ description "Convert handwritten strokes to LaTeX, text, or other formats using Mathpix OCR"
16
+
17
+ input_schema(
18
+ properties: {
19
+ strokes: {
20
+ type: "array",
21
+ description: "Array of stroke arrays, where each stroke is an array of [x, y] coordinates"
22
+ },
23
+ formats: {
24
+ type: "array",
25
+ items: { type: "string" },
26
+ description: "Output formats: latex, text, mathml, asciimath (default: latex_styled, text)"
27
+ },
28
+ width: {
29
+ type: "number",
30
+ description: "Canvas width for stroke normalization"
31
+ },
32
+ height: {
33
+ type: "number",
34
+ description: "Canvas height for stroke normalization"
35
+ }
36
+ },
37
+ required: ["strokes"]
38
+ )
39
+
40
+ def self.call(strokes:, formats: nil, width: nil, height: nil, server_context:)
41
+ safe_execute do
42
+ client = mathpix_client(server_context)
43
+
44
+ # Extract formats or use defaults
45
+ output_formats = extract_formats(formats, client)
46
+
47
+ # Build strokes data structure
48
+ strokes_data = {
49
+ strokes: strokes
50
+ }
51
+ strokes_data[:width] = width if width
52
+ strokes_data[:height] = height if height
53
+
54
+ # Delegate to core gem snap method with strokes
55
+ result = client.snap(strokes_data, formats: output_formats)
56
+
57
+ # Format response
58
+ response_data = {
59
+ success: true,
60
+ input_type: "strokes",
61
+ stroke_count: strokes.length,
62
+ formats: output_formats,
63
+ results: {
64
+ latex: result.latex,
65
+ text: result.text,
66
+ confidence: result.confidence,
67
+ is_handwritten: result.handwritten?,
68
+ position: result.position
69
+ }
70
+ }
71
+
72
+ # Add optional formats
73
+ response_data[:results][:mathml] = result.mathml if result.mathml
74
+ response_data[:results][:asciimath] = result.asciimath if result.asciimath
75
+
76
+ json_response(response_data)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+
5
+ module Mathpix
6
+ module MCP
7
+ module Tools
8
+ # Get Account Info Tool
9
+ #
10
+ # Retrieves account information and plan details
11
+ # Thin delegate to Mathpix::Client (account endpoint)
12
+ #
13
+ # The geodesic path: official SDK + account management
14
+ class GetAccountInfoTool < BaseTool
15
+ description "Get Mathpix account information, plan details, and limits"
16
+
17
+ input_schema(
18
+ properties: {},
19
+ required: []
20
+ )
21
+
22
+ def self.call(server_context:)
23
+ safe_execute do
24
+ client = mathpix_client(server_context)
25
+
26
+ begin
27
+ # Try to call the account endpoint
28
+ account_data = client.get('/account')
29
+
30
+ response_data = {
31
+ success: true,
32
+ account: {
33
+ email: account_data['email'],
34
+ plan: account_data['plan'],
35
+ created_at: account_data['created_at'],
36
+ limits: {
37
+ monthly_requests: account_data['monthly_requests'],
38
+ max_file_size: account_data['max_file_size'],
39
+ features: account_data['features']
40
+ }
41
+ }
42
+ }
43
+
44
+ json_response(response_data)
45
+ rescue Mathpix::APIError => e
46
+ # Handle API-specific errors properly
47
+ error_response(e)
48
+ rescue StandardError => e
49
+ # Handle unexpected errors
50
+ error_response(e)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end