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.
- mcp_server_make-0.1.0/.gitignore +10 -0
- mcp_server_make-0.1.0/PKG-INFO +220 -0
- mcp_server_make-0.1.0/README.md +189 -0
- mcp_server_make-0.1.0/pyproject.toml +60 -0
- mcp_server_make-0.1.0/src/mcp_server_make/__init__.py +18 -0
- mcp_server_make-0.1.0/src/mcp_server_make/exceptions.py +13 -0
- mcp_server_make-0.1.0/src/mcp_server_make/execution.py +77 -0
- mcp_server_make-0.1.0/src/mcp_server_make/handlers.py +228 -0
- mcp_server_make-0.1.0/src/mcp_server_make/make.py +99 -0
- mcp_server_make-0.1.0/src/mcp_server_make/security.py +52 -0
- mcp_server_make-0.1.0/src/mcp_server_make/server.py +138 -0
|
@@ -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())
|