Skip to content

Commit 8d58af6

Browse files
Develop and test running sections, including testing running section-wise tasks
1 parent 617f3e7 commit 8d58af6

File tree

5 files changed

+294
-1
lines changed

5 files changed

+294
-1
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ matlab_scripts/
4949

5050
# Development files
5151
examples/matlab_scripts/section_test.m
52+
53+
# Jupyter
54+
.ipynb_checkpoints/
55+
56+
# DS_Store
57+
.DS_Store

examples/test_sections.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Test script for MATLAB section execution."""
2+
3+
import asyncio
4+
from pathlib import Path
5+
6+
from matlab_mcp.server import MatlabServer
7+
from matlab_mcp.utils.section_parser import get_section_info
8+
9+
10+
async def test_section_execution():
11+
"""Test section-based MATLAB script execution."""
12+
print("Testing MATLAB Section Execution...")
13+
14+
server = MatlabServer()
15+
16+
# Test section parsing
17+
print("\nTesting section parsing...")
18+
script_path = Path("examples/matlab_scripts/section_test.m")
19+
if not script_path.exists():
20+
print(f"Error: Script not found at {script_path}")
21+
return
22+
23+
sections = get_section_info(script_path)
24+
print(f"Found {len(sections)} sections:")
25+
for section in sections:
26+
print(f"- {section['title']} (lines {section['start_line']}-{section['end_line']})")
27+
print(f" Preview: {section['preview']}")
28+
29+
# Test executing each section
30+
print("\nExecuting sections one by one...")
31+
for section in sections:
32+
print(f"\nExecuting section: {section['title']}")
33+
result = await server.engine.execute_section(
34+
str(script_path),
35+
(section['start_line'], section['end_line'])
36+
)
37+
38+
print("Output:")
39+
print(result.output)
40+
41+
print("Workspace variables:")
42+
for var, value in result.workspace.items():
43+
print(f"- {var}: {value}")
44+
45+
if result.figures:
46+
print(f"Generated {len(result.figures)} figures")
47+
48+
# Save figures
49+
output_dir = Path("test_output")
50+
output_dir.mkdir(exist_ok=True)
51+
52+
for i, fig_data in enumerate(result.figures):
53+
output_path = output_dir / f"section_{section['start_line']}_figure_{i}.png"
54+
output_path.write_bytes(fig_data)
55+
print(f"Saved figure to: {output_path}")
56+
57+
# Clean up
58+
server.engine.cleanup()
59+
print("\nTest completed successfully!")
60+
61+
62+
if __name__ == "__main__":
63+
asyncio.run(test_section_execution())

src/matlab_mcp/engine.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from mcp.server.fastmcp import Context
1010

1111
from .models import ExecutionResult
12+
from .utils.section_parser import extract_section
1213

1314

1415
class MatlabEngine:
@@ -196,6 +197,49 @@ async def get_workspace(self) -> Dict[str, Any]:
196197

197198
return workspace
198199

200+
async def execute_section(
201+
self,
202+
file_path: str,
203+
section_range: tuple[int, int],
204+
maintain_workspace: bool = True,
205+
capture_plots: bool = True,
206+
ctx: Optional[Context] = None
207+
) -> ExecutionResult:
208+
"""Execute a specific section of a MATLAB script.
209+
210+
Args:
211+
file_path: Path to the MATLAB script
212+
section_range: Tuple of (start_line, end_line) for the section
213+
maintain_workspace: Whether to maintain workspace between sections
214+
capture_plots: Whether to capture generated plots
215+
ctx: MCP context for progress reporting
216+
217+
Returns:
218+
ExecutionResult containing output, workspace state, and figures
219+
"""
220+
script_path = Path(file_path)
221+
if not script_path.exists():
222+
raise FileNotFoundError(f"Script not found: {file_path}")
223+
224+
# Extract the section code
225+
section_code = extract_section(
226+
script_path,
227+
section_range[0],
228+
section_range[1],
229+
maintain_workspace
230+
)
231+
232+
if ctx:
233+
ctx.info(f"Executing section (lines {section_range[0]}-{section_range[1]})")
234+
235+
# Execute the section
236+
return await self.execute(
237+
section_code,
238+
is_file=False,
239+
capture_plots=capture_plots,
240+
ctx=ctx
241+
)
242+
199243
def cleanup(self) -> None:
200244
"""Clean up MATLAB engine and resources."""
201245
if self.eng is not None:

src/matlab_mcp/server.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""MATLAB MCP Server implementation."""
22

33
from pathlib import Path
4-
from typing import Dict, Any, Optional
4+
from typing import Dict, Any, Optional, List
55

66
from mcp.server.fastmcp import FastMCP, Image, Context
77

88
from .engine import MatlabEngine
9+
from .utils.section_parser import get_section_info
910

1011

1112
class MatlabServer:
@@ -29,6 +30,71 @@ def __init__(self):
2930

3031
def _setup_tools(self):
3132
"""Set up MCP tools for MATLAB operations."""
33+
34+
@self.mcp.tool()
35+
async def get_script_sections(
36+
script_name: str,
37+
ctx: Context = None
38+
) -> List[dict]:
39+
"""Get information about sections in a MATLAB script.
40+
41+
Args:
42+
script_name: Name of the script (without .m extension)
43+
ctx: MCP context for progress reporting
44+
45+
Returns:
46+
List of dictionaries containing section information
47+
"""
48+
script_path = self.scripts_dir / f"{script_name}.m"
49+
if not script_path.exists():
50+
raise FileNotFoundError(f"Script {script_name}.m not found")
51+
52+
if ctx:
53+
ctx.info(f"Getting sections for script: {script_name}")
54+
55+
return get_section_info(script_path)
56+
57+
@self.mcp.tool()
58+
async def execute_script_section(
59+
script_name: str,
60+
section_range: tuple[int, int],
61+
maintain_workspace: bool = True,
62+
ctx: Context = None
63+
) -> Dict[str, Any]:
64+
"""Execute a specific section of a MATLAB script.
65+
66+
Args:
67+
script_name: Name of the script (without .m extension)
68+
section_range: Tuple of (start_line, end_line) for the section
69+
maintain_workspace: Whether to maintain workspace between sections
70+
ctx: MCP context for progress reporting
71+
72+
Returns:
73+
Dictionary containing execution results
74+
"""
75+
script_path = self.scripts_dir / f"{script_name}.m"
76+
if not script_path.exists():
77+
raise FileNotFoundError(f"Script {script_name}.m not found")
78+
79+
result = await self.engine.execute_section(
80+
str(script_path),
81+
section_range,
82+
maintain_workspace=maintain_workspace,
83+
ctx=ctx
84+
)
85+
86+
# Convert raw bytes to MCP Image objects
87+
figures = [
88+
Image(data=fig_data, format='png')
89+
for fig_data in result.figures
90+
]
91+
92+
return {
93+
"output": result.output,
94+
"error": result.error,
95+
"workspace": result.workspace,
96+
"figures": figures
97+
}
3298

3399
@self.mcp.tool()
34100
async def execute_script(
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Utility functions for parsing and executing MATLAB script sections."""
2+
3+
from pathlib import Path
4+
from typing import List, Tuple
5+
6+
7+
def parse_sections(file_path: Path) -> List[Tuple[int, int, str]]:
8+
"""Parse a MATLAB script into sections.
9+
10+
Args:
11+
file_path: Path to the MATLAB script
12+
13+
Returns:
14+
List of tuples containing (start_line, end_line, section_title)
15+
for each section in the script. If no sections are found, returns
16+
a single section spanning the entire file.
17+
"""
18+
sections = []
19+
current_start = 0
20+
current_title = "Main"
21+
22+
with open(file_path) as f:
23+
lines = f.readlines()
24+
25+
for i, line in enumerate(lines):
26+
if line.startswith('%%'):
27+
# If we found a section marker, end the previous section
28+
if current_start < i:
29+
sections.append((current_start, i - 1, current_title))
30+
31+
# Start a new section
32+
current_start = i
33+
# Extract section title (everything after %% on the same line)
34+
current_title = line[2:].strip()
35+
36+
# Add the final section
37+
if current_start < len(lines):
38+
sections.append((current_start, len(lines) - 1, current_title))
39+
40+
# If no sections were found, treat the entire file as one section
41+
if not sections:
42+
sections = [(0, len(lines) - 1, "Main")]
43+
44+
return sections
45+
46+
47+
def extract_section(
48+
file_path: Path,
49+
start_line: int,
50+
end_line: int,
51+
maintain_workspace: bool = True
52+
) -> str:
53+
"""Extract a section of MATLAB code from a file.
54+
55+
Args:
56+
file_path: Path to the MATLAB script
57+
start_line: Starting line number (0-based)
58+
end_line: Ending line number (0-based)
59+
maintain_workspace: Whether to maintain workspace variables
60+
61+
Returns:
62+
MATLAB code for the specified section
63+
"""
64+
with open(file_path) as f:
65+
lines = f.readlines()
66+
67+
# Extract the section lines
68+
section_lines = lines[start_line:end_line + 1]
69+
70+
# If not maintaining workspace, add clear command at the start
71+
if not maintain_workspace:
72+
section_lines.insert(0, 'clear;\n')
73+
74+
return ''.join(section_lines)
75+
76+
77+
def get_section_info(file_path: Path) -> List[dict]:
78+
"""Get information about sections in a MATLAB script.
79+
80+
Args:
81+
file_path: Path to the MATLAB script
82+
83+
Returns:
84+
List of dictionaries containing section information:
85+
{
86+
'title': section title,
87+
'start_line': starting line number,
88+
'end_line': ending line number,
89+
'preview': first non-comment line of the section
90+
}
91+
"""
92+
sections = parse_sections(file_path)
93+
section_info = []
94+
95+
with open(file_path) as f:
96+
lines = f.readlines()
97+
98+
for start, end, title in sections:
99+
# Find first non-comment line for preview
100+
preview = ""
101+
for line in lines[start:end + 1]:
102+
stripped = line.strip()
103+
if stripped and not stripped.startswith('%'):
104+
preview = stripped
105+
break
106+
107+
section_info.append({
108+
'title': title,
109+
'start_line': start,
110+
'end_line': end,
111+
'preview': preview
112+
})
113+
114+
return section_info

0 commit comments

Comments
 (0)