mcp-server-make 0.1.0__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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-make
3
+ Version: 0.1.0
4
+ Summary: MCP Server for GNU Make
5
+ Project-URL: Homepage, https://github.com/modelcontextprotocol/mcp-server-make
6
+ Project-URL: Documentation, https://github.com/modelcontextprotocol/mcp-server-make#readme
7
+ Project-URL: Repository, https://github.com/modelcontextprotocol/mcp-server-make.git
8
+ Project-URL: Changelog, https://github.com/modelcontextprotocol/mcp-server-make/blob/main/CHANGELOG.md
9
+ Author-email: "Joshua M. Dotson" <contact@jmdots.com>
10
+ License-Expression: MIT
11
+ Keywords: build,claude,llm,make,mcp
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: mcp>=1.1.2
23
+ Requires-Dist: pydantic>=2.10.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23.3; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.4.4; extra == 'dev'
29
+ Requires-Dist: ruff>=0.1.13; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # mcp-server-make
33
+
34
+ MCP Server for GNU Make - providing controlled and secure access to Make systems from LLMs.
35
+
36
+ ## Features
37
+
38
+ ### Resources
39
+ - `make://current/makefile` - Access current Makefile content securely
40
+ - `make://targets` - List available Make targets with documentation
41
+
42
+ ### Tools
43
+ - `list-targets`: List available Make targets
44
+ - Returns target names and documentation
45
+ - Optional pattern filtering for searching targets
46
+ - `run-target`: Execute Make targets safely
47
+ - Required: target name
48
+ - Optional: timeout (1-3600 seconds, default 300)
49
+
50
+ ## Quick Start
51
+
52
+ ### Prerequisites
53
+ - Python 3.12+
54
+ - GNU Make
55
+ - pip or uv package manager
56
+
57
+ ### Installation
58
+
59
+ Using uv (recommended):
60
+ ```bash
61
+ uvx mcp-server-make
62
+ ```
63
+
64
+ Using pip:
65
+ ```bash
66
+ pip install mcp-server-make
67
+ ```
68
+
69
+ ### Claude Desktop Integration
70
+
71
+ Add to your Claude Desktop configuration:
72
+
73
+ MacOS:
74
+ ```bash
75
+ nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
76
+ ```
77
+
78
+ Windows:
79
+ ```bash
80
+ notepad %APPDATA%\Claude\claude_desktop_config.json
81
+ ```
82
+
83
+ Add configuration:
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "mcp-server-make": {
88
+ "command": "uv",
89
+ "args": [
90
+ "--directory",
91
+ "/path/to/server",
92
+ "run",
93
+ "mcp-server-make",
94
+ "--makefile-dir", "/path/to/project"
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ - `--directory`: Path to the installed mcp-server-make package
102
+ - `--makefile-dir`: Directory containing the Makefile to manage
103
+
104
+ Restart Claude Desktop to activate the Make server.
105
+
106
+ ## Usage Examples
107
+
108
+ ### List Available Targets
109
+ ```
110
+ I see you have a Makefile. Can you list the available targets?
111
+ ```
112
+
113
+ ### View Target Documentation
114
+ ```
115
+ What does the 'build' target do?
116
+ ```
117
+
118
+ ### Run Tests
119
+ ```
120
+ Please run the test target with a 2 minute timeout.
121
+ ```
122
+
123
+ ### View Makefile
124
+ ```
125
+ Show me the current Makefile content.
126
+ ```
127
+
128
+ ## Development
129
+
130
+ ### Local Development Setup
131
+ ```bash
132
+ # Clone repository
133
+ git clone https://github.com/modelcontextprotocol/mcp-server-make
134
+ cd mcp-server-make
135
+
136
+ # Create virtual environment
137
+ uv venv
138
+ source .venv/bin/activate # Unix/MacOS
139
+ .venv\Scripts\activate # Windows
140
+
141
+ # Install dependencies
142
+ make dev-setup
143
+
144
+ # Run tests and checks
145
+ make check
146
+ ```
147
+
148
+ ### Testing with MCP Inspector
149
+
150
+ Test the server using the MCP Inspector:
151
+ ```bash
152
+ npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-server-make run mcp-server-make --makefile-dir /path/to/project
153
+ ```
154
+
155
+ ## Security Features
156
+
157
+ Version 0.1.0 implements several security controls:
158
+
159
+ - Path validation and directory boundary enforcement
160
+ - Target name sanitization and command validation
161
+ - Resource and timeout limits for command execution
162
+ - Restricted environment access and cleanup
163
+ - Error isolation and safe propagation
164
+
165
+ ## Behavior Details
166
+
167
+ ### Resource Access
168
+ - Makefile content is read-only and validated
169
+ - Target listing includes names and documentation
170
+ - Full path validation prevents traversal attacks
171
+ - Resources require proper make:// URIs
172
+
173
+ ### Tool Execution
174
+ - Targets are sanitized and validated
175
+ - Execution occurs in controlled environment
176
+ - Timeouts prevent infinite execution
177
+ - Clear error messages for failures
178
+ - Resource cleanup after execution
179
+
180
+ ### Error Handling
181
+ - Type-safe error propagation
182
+ - Context-aware error messages
183
+ - Clean error isolation
184
+ - No error leakage
185
+
186
+ ## Known Limitations
187
+
188
+ Version 0.1.0 has the following scope limitations:
189
+
190
+ - Read-only Makefile access
191
+ - Single Makefile per working directory
192
+ - No support for include directives
193
+ - Basic target pattern matching only
194
+ - No variable expansion in documentation
195
+
196
+ ## Contributing
197
+
198
+ 1. Fork the repository
199
+ 2. Create a feature branch
200
+ 3. Add tests for new features
201
+ 4. Ensure all checks pass (`make check`)
202
+ 5. Submit a pull request
203
+
204
+ ## License
205
+
206
+ MIT - See LICENSE file for details.
207
+
208
+ ## Support
209
+
210
+ - GitHub Issues: Bug reports and feature requests
211
+ - GitHub Discussions: Questions and community help
212
+
213
+ ## Version History
214
+
215
+ ### 0.1.0
216
+ - Initial stable release
217
+ - Basic Makefile access and target execution
218
+ - Core security controls
219
+ - Claude Desktop integration
220
+ - Configurable Makefile directory
@@ -0,0 +1,189 @@
1
+ # mcp-server-make
2
+
3
+ MCP Server for GNU Make - providing controlled and secure access to Make systems from LLMs.
4
+
5
+ ## Features
6
+
7
+ ### Resources
8
+ - `make://current/makefile` - Access current Makefile content securely
9
+ - `make://targets` - List available Make targets with documentation
10
+
11
+ ### Tools
12
+ - `list-targets`: List available Make targets
13
+ - Returns target names and documentation
14
+ - Optional pattern filtering for searching targets
15
+ - `run-target`: Execute Make targets safely
16
+ - Required: target name
17
+ - Optional: timeout (1-3600 seconds, default 300)
18
+
19
+ ## Quick Start
20
+
21
+ ### Prerequisites
22
+ - Python 3.12+
23
+ - GNU Make
24
+ - pip or uv package manager
25
+
26
+ ### Installation
27
+
28
+ Using uv (recommended):
29
+ ```bash
30
+ uvx mcp-server-make
31
+ ```
32
+
33
+ Using pip:
34
+ ```bash
35
+ pip install mcp-server-make
36
+ ```
37
+
38
+ ### Claude Desktop Integration
39
+
40
+ Add to your Claude Desktop configuration:
41
+
42
+ MacOS:
43
+ ```bash
44
+ nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
45
+ ```
46
+
47
+ Windows:
48
+ ```bash
49
+ notepad %APPDATA%\Claude\claude_desktop_config.json
50
+ ```
51
+
52
+ Add configuration:
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "mcp-server-make": {
57
+ "command": "uv",
58
+ "args": [
59
+ "--directory",
60
+ "/path/to/server",
61
+ "run",
62
+ "mcp-server-make",
63
+ "--makefile-dir", "/path/to/project"
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ - `--directory`: Path to the installed mcp-server-make package
71
+ - `--makefile-dir`: Directory containing the Makefile to manage
72
+
73
+ Restart Claude Desktop to activate the Make server.
74
+
75
+ ## Usage Examples
76
+
77
+ ### List Available Targets
78
+ ```
79
+ I see you have a Makefile. Can you list the available targets?
80
+ ```
81
+
82
+ ### View Target Documentation
83
+ ```
84
+ What does the 'build' target do?
85
+ ```
86
+
87
+ ### Run Tests
88
+ ```
89
+ Please run the test target with a 2 minute timeout.
90
+ ```
91
+
92
+ ### View Makefile
93
+ ```
94
+ Show me the current Makefile content.
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ### Local Development Setup
100
+ ```bash
101
+ # Clone repository
102
+ git clone https://github.com/modelcontextprotocol/mcp-server-make
103
+ cd mcp-server-make
104
+
105
+ # Create virtual environment
106
+ uv venv
107
+ source .venv/bin/activate # Unix/MacOS
108
+ .venv\Scripts\activate # Windows
109
+
110
+ # Install dependencies
111
+ make dev-setup
112
+
113
+ # Run tests and checks
114
+ make check
115
+ ```
116
+
117
+ ### Testing with MCP Inspector
118
+
119
+ Test the server using the MCP Inspector:
120
+ ```bash
121
+ npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-server-make run mcp-server-make --makefile-dir /path/to/project
122
+ ```
123
+
124
+ ## Security Features
125
+
126
+ Version 0.1.0 implements several security controls:
127
+
128
+ - Path validation and directory boundary enforcement
129
+ - Target name sanitization and command validation
130
+ - Resource and timeout limits for command execution
131
+ - Restricted environment access and cleanup
132
+ - Error isolation and safe propagation
133
+
134
+ ## Behavior Details
135
+
136
+ ### Resource Access
137
+ - Makefile content is read-only and validated
138
+ - Target listing includes names and documentation
139
+ - Full path validation prevents traversal attacks
140
+ - Resources require proper make:// URIs
141
+
142
+ ### Tool Execution
143
+ - Targets are sanitized and validated
144
+ - Execution occurs in controlled environment
145
+ - Timeouts prevent infinite execution
146
+ - Clear error messages for failures
147
+ - Resource cleanup after execution
148
+
149
+ ### Error Handling
150
+ - Type-safe error propagation
151
+ - Context-aware error messages
152
+ - Clean error isolation
153
+ - No error leakage
154
+
155
+ ## Known Limitations
156
+
157
+ Version 0.1.0 has the following scope limitations:
158
+
159
+ - Read-only Makefile access
160
+ - Single Makefile per working directory
161
+ - No support for include directives
162
+ - Basic target pattern matching only
163
+ - No variable expansion in documentation
164
+
165
+ ## Contributing
166
+
167
+ 1. Fork the repository
168
+ 2. Create a feature branch
169
+ 3. Add tests for new features
170
+ 4. Ensure all checks pass (`make check`)
171
+ 5. Submit a pull request
172
+
173
+ ## License
174
+
175
+ MIT - See LICENSE file for details.
176
+
177
+ ## Support
178
+
179
+ - GitHub Issues: Bug reports and feature requests
180
+ - GitHub Discussions: Questions and community help
181
+
182
+ ## Version History
183
+
184
+ ### 0.1.0
185
+ - Initial stable release
186
+ - Basic Makefile access and target execution
187
+ - Core security controls
188
+ - Claude Desktop integration
189
+ - Configurable Makefile directory
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "mcp-server-make"
3
+ version = "0.1.0"
4
+ description = "MCP Server for GNU Make"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ keywords = ["mcp", "make", "build", "llm", "claude"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Build Tools",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+
21
+ dependencies = [
22
+ "mcp>=1.1.2",
23
+ "pydantic>=2.10.0",
24
+ ]
25
+
26
+ [[project.authors]]
27
+ name = "Joshua M. Dotson"
28
+ email = "contact@jmdots.com"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/modelcontextprotocol/mcp-server-make"
32
+ Documentation = "https://github.com/modelcontextprotocol/mcp-server-make#readme"
33
+ Repository = "https://github.com/modelcontextprotocol/mcp-server-make.git"
34
+ Changelog = "https://github.com/modelcontextprotocol/mcp-server-make/blob/main/CHANGELOG.md"
35
+
36
+ [project.scripts]
37
+ mcp-server-make = "mcp_server_make:main"
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build]
44
+ include = [
45
+ "src/**/*.py",
46
+ "README.md",
47
+ "LICENSE",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/mcp_server_make"]
52
+
53
+ [project.optional-dependencies]
54
+ dev = [
55
+ "ruff>=0.1.13",
56
+ "mypy>=1.8.0",
57
+ "pytest>=7.4.4",
58
+ "pytest-cov>=4.1.0",
59
+ "pytest-asyncio>=0.23.3",
60
+ ]
@@ -0,0 +1,18 @@
1
+ """MCP Server for GNU Make."""
2
+
3
+ import asyncio
4
+
5
+ from . import exceptions
6
+ from . import make
7
+ from . import security
8
+ from . import execution
9
+ from . import handlers
10
+ from .server import main as async_main
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = ["main", "exceptions", "make", "security", "execution", "handlers"]
14
+
15
+
16
+ def main():
17
+ """CLI entrypoint that runs the async main function."""
18
+ asyncio.run(async_main())
@@ -0,0 +1,13 @@
1
+ """Custom exceptions for the MCP Make Server."""
2
+
3
+
4
+ class MakefileError(Exception):
5
+ """Raised when there are issues with Makefile operations."""
6
+
7
+ pass
8
+
9
+
10
+ class SecurityError(Exception):
11
+ """Raised for security-related violations."""
12
+
13
+ pass
@@ -0,0 +1,77 @@
1
+ """Make target execution management with safety controls."""
2
+
3
+ import asyncio
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from .exceptions import MakefileError
8
+ from .make import VALID_TARGET_PATTERN
9
+ from .security import get_safe_environment, get_validated_path
10
+
11
+
12
+ class ExecutionManager:
13
+ """Manage Make target execution with safety controls."""
14
+
15
+ def __init__(self, base_dir: Path, timeout: int = 300):
16
+ """Initialize execution manager.
17
+
18
+ Args:
19
+ base_dir: Directory containing the Makefile
20
+ timeout: Maximum execution time in seconds
21
+ """
22
+ self.base_dir = base_dir
23
+ self.timeout = timeout
24
+ self.start_time = 0
25
+ self._original_cwd = None
26
+
27
+ async def __aenter__(self):
28
+ self.start_time = asyncio.get_event_loop().time()
29
+
30
+ # Store current directory and change to Makefile directory
31
+ self._original_cwd = Path.cwd()
32
+ makefile_dir = get_validated_path(self.base_dir)
33
+ os.chdir(str(makefile_dir))
34
+
35
+ return self
36
+
37
+ async def run_target(self, target: str) -> str:
38
+ """
39
+ Run a Make target with safety controls.
40
+
41
+ Args:
42
+ target: Name of the target to run
43
+
44
+ Returns:
45
+ Command output as string
46
+ """
47
+ if not VALID_TARGET_PATTERN.match(target):
48
+ raise ValueError(f"Invalid target name: {target}")
49
+
50
+ env = get_safe_environment()
51
+
52
+ try:
53
+ proc = await asyncio.create_subprocess_exec(
54
+ "make",
55
+ target,
56
+ env=env,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE,
59
+ )
60
+
61
+ stdout, stderr = await asyncio.wait_for(
62
+ proc.communicate(), timeout=self.timeout
63
+ )
64
+
65
+ if proc.returncode != 0:
66
+ error_msg = stderr.decode().strip()
67
+ raise MakefileError(f"Target execution failed: {error_msg}")
68
+
69
+ return stdout.decode()
70
+
71
+ except asyncio.TimeoutError:
72
+ raise MakefileError(f"Target execution exceeded {self.timeout}s timeout")
73
+
74
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
75
+ # Restore original working directory
76
+ if self._original_cwd:
77
+ os.chdir(str(self._original_cwd))
@@ -0,0 +1,228 @@
1
+ """MCP protocol handlers for Make server functionality."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+ from urllib.parse import quote, unquote, urlparse
7
+
8
+ from pydantic import AnyUrl
9
+ import mcp.types as types
10
+
11
+ from .exceptions import MakefileError, SecurityError
12
+ from .execution import ExecutionManager
13
+ from .make import parse_makefile_targets, read_makefile, validate_makefile_syntax
14
+ from .security import get_validated_path
15
+
16
+
17
+ def create_make_url(path: str) -> AnyUrl:
18
+ """Create a properly formatted MCP URI for the Make server.
19
+
20
+ Args:
21
+ path: Path without scheme (e.g. "current/makefile" or "targets")
22
+
23
+ Returns:
24
+ AnyUrl for Make server (e.g. "make://current/makefile")
25
+ """
26
+ # Clean and normalize path
27
+ path = path.strip().strip("/")
28
+
29
+ # Use "localhost" as host to ensure path is preserved
30
+ uri = f"make://localhost/{quote(path, safe='/')}"
31
+ return AnyUrl(uri)
32
+
33
+
34
+ def normalize_uri_path(uri: AnyUrl) -> str:
35
+ """Normalize a URI path for consistent comparison.
36
+
37
+ Args:
38
+ uri: URI to normalize path from
39
+
40
+ Returns:
41
+ Normalized path string
42
+
43
+ Raises:
44
+ ValueError: If URI has no valid path
45
+ """
46
+ parsed = urlparse(str(uri))
47
+ if not parsed.path or parsed.path == "/":
48
+ raise ValueError("URI must have a path component")
49
+
50
+ # Remove leading/trailing slashes and normalize
51
+ path = unquote(parsed.path.strip("/"))
52
+ return path.lower()
53
+
54
+
55
+ async def handle_list_resources(makefile_dir: Path) -> list[types.Resource]:
56
+ """List available Make-related resources.
57
+
58
+ Args:
59
+ makefile_dir: Directory containing the Makefile
60
+ """
61
+ resources = []
62
+
63
+ try:
64
+ # Add Makefile resource if it exists
65
+ makefile_path = get_validated_path(makefile_dir, "Makefile")
66
+ if makefile_path.exists():
67
+ resources.append(
68
+ types.Resource(
69
+ uri=create_make_url("current/makefile"),
70
+ name="Current Makefile",
71
+ description="Contents of the current Makefile",
72
+ mimeType="text/plain",
73
+ )
74
+ )
75
+
76
+ # Only add targets if we have a readable Makefile
77
+ targets = await parse_makefile_targets(makefile_dir)
78
+ if targets:
79
+ resources.append(
80
+ types.Resource(
81
+ uri=create_make_url("targets"),
82
+ name="Make Targets",
83
+ description="List of available Make targets",
84
+ mimeType="application/json",
85
+ )
86
+ )
87
+
88
+ except Exception as e:
89
+ print(f"Error listing resources: {e}")
90
+
91
+ return resources
92
+
93
+
94
+ async def handle_read_resource(uri: AnyUrl, makefile_dir: Path) -> str:
95
+ """Read Make-related resource content.
96
+
97
+ Args:
98
+ uri: Resource URI to read (e.g. "make://current/makefile")
99
+ makefile_dir: Directory containing the Makefile
100
+
101
+ Returns:
102
+ Resource content as string
103
+
104
+ Raises:
105
+ ValueError: If URI is invalid or resource not found
106
+ """
107
+ # Validate scheme before path normalization
108
+ parsed = urlparse(str(uri))
109
+ if parsed.scheme != "make":
110
+ raise ValueError(f"Unsupported URI scheme: {parsed.scheme}")
111
+
112
+ try:
113
+ # Normalize the path for consistent comparison
114
+ norm_path = normalize_uri_path(uri)
115
+
116
+ if norm_path == "current/makefile":
117
+ file_path = get_validated_path(makefile_dir, "Makefile")
118
+ content = await read_makefile(file_path)
119
+ validate_makefile_syntax(content)
120
+ return content
121
+
122
+ elif norm_path == "targets":
123
+ targets = await parse_makefile_targets(makefile_dir)
124
+ return str(targets)
125
+
126
+ raise ValueError(f"Unknown resource path: {norm_path}")
127
+
128
+ except (MakefileError, SecurityError) as e:
129
+ raise ValueError(str(e))
130
+ except Exception as e:
131
+ raise ValueError(f"Error reading resource: {str(e)}")
132
+
133
+
134
+ async def handle_list_tools() -> List[types.Tool]:
135
+ """List available Make-related tools."""
136
+ return [
137
+ types.Tool(
138
+ name="list-targets",
139
+ description="List available Make targets",
140
+ inputSchema={
141
+ "type": "object",
142
+ "properties": {
143
+ "pattern": {
144
+ "type": "string",
145
+ "description": "Optional filter pattern",
146
+ }
147
+ },
148
+ },
149
+ ),
150
+ types.Tool(
151
+ name="run-target",
152
+ description="Execute a Make target",
153
+ inputSchema={
154
+ "type": "object",
155
+ "properties": {
156
+ "target": {"type": "string", "description": "Target to execute"},
157
+ "timeout": {
158
+ "type": "integer",
159
+ "minimum": 1,
160
+ "maximum": 3600,
161
+ "default": 300,
162
+ "description": "Maximum execution time in seconds",
163
+ },
164
+ },
165
+ "required": ["target"],
166
+ },
167
+ ),
168
+ ]
169
+
170
+
171
+ async def handle_call_tool(
172
+ name: str,
173
+ arguments: Optional[dict],
174
+ makefile_dir: Path,
175
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
176
+ """Execute a Make-related tool.
177
+
178
+ Args:
179
+ name: Tool name to execute
180
+ arguments: Optional tool arguments
181
+ makefile_dir: Directory containing the Makefile
182
+
183
+ Returns:
184
+ List of tool result content
185
+
186
+ Raises:
187
+ ValueError: If tool not found or invalid arguments
188
+ """
189
+ if not arguments:
190
+ raise ValueError("Tool arguments required")
191
+
192
+ if name == "list-targets":
193
+ pattern = arguments.get("pattern", "*")
194
+ targets = await parse_makefile_targets(makefile_dir)
195
+
196
+ # Apply pattern filtering if specified
197
+ if pattern != "*":
198
+ try:
199
+ pattern_re = re.compile(pattern)
200
+ targets = [t for t in targets if pattern_re.search(t["name"])]
201
+ except re.error:
202
+ raise ValueError(f"Invalid pattern: {pattern}")
203
+
204
+ return [
205
+ types.TextContent(
206
+ type="text",
207
+ text="\n".join(
208
+ f"{t['name']}: {t['description'] or 'No description'}"
209
+ for t in targets
210
+ ),
211
+ )
212
+ ]
213
+
214
+ elif name == "run-target":
215
+ if "target" not in arguments:
216
+ raise ValueError("Target name required")
217
+
218
+ target = arguments["target"]
219
+ timeout = min(int(arguments.get("timeout", 300)), 3600)
220
+
221
+ try:
222
+ async with ExecutionManager(base_dir=makefile_dir, timeout=timeout) as mgr:
223
+ output = await mgr.run_target(target)
224
+ return [types.TextContent(type="text", text=output)]
225
+ except (MakefileError, SecurityError) as e:
226
+ raise ValueError(str(e))
227
+
228
+ raise ValueError(f"Unknown tool: {name}")
@@ -0,0 +1,99 @@
1
+ """Core Make functionality for the MCP Make Server."""
2
+
3
+ import asyncio
4
+ import re
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ from .exceptions import MakefileError
9
+ from .security import get_validated_path
10
+
11
+ # Regular expression for validating Make target names
12
+ VALID_TARGET_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
13
+
14
+
15
+ async def read_makefile(path: Path) -> str:
16
+ """
17
+ Safely read a Makefile's contents.
18
+
19
+ Args:
20
+ path: Path to the Makefile
21
+
22
+ Returns:
23
+ Contents of the Makefile as string
24
+ """
25
+ try:
26
+ async with asyncio.Lock(): # Ensure thread-safe file access
27
+ return path.read_text()
28
+ except Exception as e:
29
+ raise MakefileError(f"Failed to read Makefile: {str(e)}")
30
+
31
+
32
+ def validate_makefile_syntax(content: str) -> bool:
33
+ """
34
+ Perform basic Makefile syntax validation.
35
+
36
+ Args:
37
+ content: Makefile content to validate
38
+
39
+ Returns:
40
+ True if validation passes
41
+
42
+ Raises:
43
+ MakefileError: If validation fails
44
+ """
45
+ try:
46
+ # Basic syntax checks (can be expanded)
47
+ if not content.strip():
48
+ raise MakefileError("Empty Makefile")
49
+
50
+ # Check for basic format validity
51
+ lines = content.splitlines()
52
+ for i, line in enumerate(lines, 1):
53
+ if line.strip().startswith(".") and ":" not in line:
54
+ raise MakefileError(f"Invalid directive on line {i}: {line}")
55
+
56
+ return True
57
+ except Exception as e:
58
+ raise MakefileError(f"Makefile validation failed: {str(e)}")
59
+
60
+
61
+ async def parse_makefile_targets(makefile_dir: Path) -> List[dict]:
62
+ """
63
+ Parse Makefile to extract targets and their metadata.
64
+
65
+ Args:
66
+ makefile_dir: Directory containing the Makefile to parse
67
+
68
+ Returns:
69
+ List of target dictionaries with metadata
70
+ """
71
+ path = get_validated_path(makefile_dir, "Makefile")
72
+ content = await read_makefile(path)
73
+
74
+ targets = []
75
+ current_comment = []
76
+ current_description = None
77
+
78
+ for line in content.splitlines():
79
+ line = line.strip()
80
+
81
+ if line.startswith("#"):
82
+ current_comment.append(line[1:].strip())
83
+ elif ":" in line and not line.startswith("\t"):
84
+ target = line.split(":", 1)[0].strip()
85
+
86
+ # Extract description from ## comment if present
87
+ description_parts = line.split("##", 1)
88
+ if len(description_parts) > 1:
89
+ current_description = description_parts[1].strip()
90
+
91
+ if VALID_TARGET_PATTERN.match(target):
92
+ description = current_description or " ".join(current_comment) or None
93
+ targets.append({"name": target, "description": description})
94
+ current_comment = []
95
+ current_description = None
96
+ else:
97
+ current_comment = []
98
+
99
+ return targets
@@ -0,0 +1,52 @@
1
+ """Security controls for the MCP Make Server."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from .exceptions import SecurityError
7
+
8
+
9
+ def get_validated_path(base_dir: Path, subpath: Optional[str] = None) -> Path:
10
+ """
11
+ Validate and resolve a path within project boundaries.
12
+
13
+ Args:
14
+ base_dir: Base directory containing Makefile (project root)
15
+ subpath: Optional path relative to base_dir
16
+
17
+ Returns:
18
+ Resolved Path object
19
+
20
+ Raises:
21
+ SecurityError: If path validation fails
22
+ """
23
+ try:
24
+ # Resolve the base directory
25
+ base = base_dir.resolve()
26
+
27
+ # If no subpath, return base
28
+ if subpath is None:
29
+ return base
30
+
31
+ # Resolve requested path relative to base
32
+ requested = (base / subpath).resolve()
33
+
34
+ # Ensure path is within base directory tree
35
+ if not str(requested).startswith(str(base)):
36
+ raise SecurityError("Path access denied: outside project boundary")
37
+
38
+ return requested
39
+ except Exception as e:
40
+ raise SecurityError(f"Invalid path: {str(e)}")
41
+
42
+
43
+ def get_safe_environment() -> dict:
44
+ """Get a sanitized environment for Make execution."""
45
+ import os
46
+
47
+ env = os.environ.copy()
48
+ # Remove potentially dangerous variables
49
+ for key in list(env.keys()):
50
+ if key.startswith(("LD_", "DYLD_", "PATH")):
51
+ del env[key]
52
+ return env
@@ -0,0 +1,138 @@
1
+ """MCP Server for GNU Make - Core functionality."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ from pathlib import Path
6
+ from typing import Any, List
7
+ from urllib.parse import urlparse
8
+
9
+ from pydantic import AnyUrl
10
+ from mcp.server import (
11
+ NotificationOptions,
12
+ Server,
13
+ )
14
+ import mcp.server.stdio
15
+ import mcp.types as types
16
+ from mcp.server.models import InitializationOptions
17
+
18
+ from . import handlers
19
+ from .exceptions import SecurityError
20
+
21
+
22
+ class MakeServer(Server):
23
+ """MCP Server implementation for GNU Make functionality."""
24
+
25
+ def __init__(self, makefile_dir: Path | str | None = None):
26
+ """Initialize the Make server.
27
+
28
+ Args:
29
+ makefile_dir: Directory containing the Makefile to manage
30
+ """
31
+ super().__init__("mcp-server-make")
32
+
33
+ # Resolve and validate the Makefile directory
34
+ self.makefile_dir = Path(makefile_dir).resolve() if makefile_dir else Path.cwd()
35
+ if not (self.makefile_dir / "Makefile").exists():
36
+ raise SecurityError(f"No Makefile found in directory: {self.makefile_dir}")
37
+
38
+ self._init_handlers()
39
+
40
+ def _init_handlers(self) -> None:
41
+ """Initialize the handler functions."""
42
+
43
+ async def _list_resources() -> List[types.Resource]:
44
+ try:
45
+ return await handlers.handle_list_resources(self.makefile_dir)
46
+ except Exception as e:
47
+ raise ValueError(str(e))
48
+
49
+ async def _read_resource(uri: AnyUrl | str) -> str:
50
+ try:
51
+ # Handle invalid URI scheme early
52
+ if isinstance(uri, str):
53
+ parsed = urlparse(uri)
54
+ if parsed.scheme != "make":
55
+ raise ValueError(f"Unsupported URI scheme: {parsed.scheme}")
56
+ # Only remove prefix if it's the make:// scheme
57
+ uri = handlers.create_make_url(
58
+ uri[7:] if uri.startswith("make://") else uri
59
+ )
60
+
61
+ return await handlers.handle_read_resource(uri, self.makefile_dir)
62
+ except ValueError as e:
63
+ # Preserve original error messages
64
+ raise ValueError(str(e))
65
+ except Exception as e:
66
+ raise ValueError(f"Failed to read resource: {str(e)}")
67
+
68
+ async def _list_tools() -> List[types.Tool]:
69
+ try:
70
+ return await handlers.handle_list_tools()
71
+ except Exception as e:
72
+ raise ValueError(str(e))
73
+
74
+ async def _call_tool(
75
+ name: str,
76
+ arguments: dict | None,
77
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
78
+ try:
79
+ return await handlers.handle_call_tool(
80
+ name, arguments, self.makefile_dir
81
+ )
82
+ except Exception as e:
83
+ raise ValueError(str(e))
84
+
85
+ self._list_resources_handler = _list_resources
86
+ self._read_resource_handler = _read_resource
87
+ self._list_tools_handler = _list_tools
88
+ self._call_tool_handler = _call_tool
89
+
90
+ @property
91
+ def list_resources(self) -> Any:
92
+ """Return list_resources handler."""
93
+ return self._list_resources_handler
94
+
95
+ @property
96
+ def read_resource(self) -> Any:
97
+ """Return read_resource handler."""
98
+ return self._read_resource_handler
99
+
100
+ @property
101
+ def list_tools(self) -> Any:
102
+ """Return list_tools handler."""
103
+ return self._list_tools_handler
104
+
105
+ @property
106
+ def call_tool(self) -> Any:
107
+ """Return call_tool handler."""
108
+ return self._call_tool_handler
109
+
110
+
111
+ async def main():
112
+ """Run the server using stdin/stdout streams."""
113
+ # Parse command line arguments
114
+ parser = argparse.ArgumentParser(description="MCP Server for GNU Make")
115
+ parser.add_argument(
116
+ "--makefile-dir",
117
+ type=str,
118
+ help="Directory containing the Makefile to manage",
119
+ )
120
+ args = parser.parse_args()
121
+
122
+ # Create server instance with configured directory
123
+ server = MakeServer(makefile_dir=args.makefile_dir)
124
+
125
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
126
+ init_options = InitializationOptions(
127
+ server_name="mcp-server-make",
128
+ server_version="0.1.0",
129
+ capabilities=server.get_capabilities(
130
+ notification_options=NotificationOptions(),
131
+ experimental_capabilities={},
132
+ ),
133
+ )
134
+ await server.run(read_stream, write_stream, init_options)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ asyncio.run(main())