up-cli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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,474 @@
1
+ """MCP (Model Context Protocol) server templates."""
2
+
3
+ from pathlib import Path
4
+ from datetime import date
5
+
6
+
7
+ def create_mcp_config(target_dir: Path, ai_target: str, force: bool = False) -> None:
8
+ """Create MCP server configuration files.
9
+
10
+ Creates:
11
+ - .mcp/config.json - MCP server configuration
12
+ - .mcp/tools/ - Custom tool definitions
13
+ """
14
+ mcp_dir = target_dir / ".mcp"
15
+ mcp_dir.mkdir(parents=True, exist_ok=True)
16
+
17
+ _create_mcp_config_json(mcp_dir, force)
18
+ _create_mcp_tools_dir(mcp_dir, force)
19
+ _create_mcp_readme(mcp_dir, force)
20
+
21
+
22
+ def _write_file(path: Path, content: str, force: bool) -> None:
23
+ """Write file if it doesn't exist or force is True."""
24
+ if path.exists() and not force:
25
+ return
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(content)
28
+
29
+
30
+ def _create_mcp_config_json(mcp_dir: Path, force: bool) -> None:
31
+ """Create MCP configuration file."""
32
+ content = """{
33
+ "$schema": "https://modelcontextprotocol.io/schema/config.json",
34
+ "version": "1.0.0",
35
+ "servers": {
36
+ "project-tools": {
37
+ "description": "Project-specific tools for AI assistants",
38
+ "command": "python",
39
+ "args": ["-m", "mcp_server"],
40
+ "env": {},
41
+ "tools": [
42
+ "project_status",
43
+ "run_tests",
44
+ "lint_code",
45
+ "search_code"
46
+ ]
47
+ }
48
+ },
49
+ "resources": {
50
+ "docs": {
51
+ "uri": "file://./docs/",
52
+ "description": "Project documentation"
53
+ },
54
+ "context": {
55
+ "uri": "file://./docs/CONTEXT.md",
56
+ "description": "Current project context"
57
+ }
58
+ },
59
+ "prompts": {
60
+ "project-overview": {
61
+ "description": "Get a quick project overview",
62
+ "template": "Read docs/CONTEXT.md and provide a summary of the current project state."
63
+ }
64
+ }
65
+ }
66
+ """
67
+ _write_file(mcp_dir / "config.json", content, force)
68
+
69
+
70
+ def _create_mcp_tools_dir(mcp_dir: Path, force: bool) -> None:
71
+ """Create MCP tools directory with sample tools."""
72
+ tools_dir = mcp_dir / "tools"
73
+ tools_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Project status tool
76
+ status_tool = """{
77
+ "name": "project_status",
78
+ "description": "Get current project status including loop state and circuit breakers",
79
+ "inputSchema": {
80
+ "type": "object",
81
+ "properties": {
82
+ "verbose": {
83
+ "type": "boolean",
84
+ "description": "Include detailed information",
85
+ "default": false
86
+ }
87
+ }
88
+ }
89
+ }
90
+ """
91
+ _write_file(tools_dir / "project_status.json", status_tool, force)
92
+
93
+ # Run tests tool
94
+ tests_tool = """{
95
+ "name": "run_tests",
96
+ "description": "Run project tests with optional filtering",
97
+ "inputSchema": {
98
+ "type": "object",
99
+ "properties": {
100
+ "path": {
101
+ "type": "string",
102
+ "description": "Test file or directory path"
103
+ },
104
+ "pattern": {
105
+ "type": "string",
106
+ "description": "Test name pattern to match"
107
+ },
108
+ "verbose": {
109
+ "type": "boolean",
110
+ "default": false
111
+ }
112
+ }
113
+ }
114
+ }
115
+ """
116
+ _write_file(tools_dir / "run_tests.json", tests_tool, force)
117
+
118
+ # Lint code tool
119
+ lint_tool = """{
120
+ "name": "lint_code",
121
+ "description": "Run linter on specified files or directories",
122
+ "inputSchema": {
123
+ "type": "object",
124
+ "properties": {
125
+ "path": {
126
+ "type": "string",
127
+ "description": "File or directory to lint"
128
+ },
129
+ "fix": {
130
+ "type": "boolean",
131
+ "description": "Automatically fix issues",
132
+ "default": false
133
+ }
134
+ }
135
+ }
136
+ }
137
+ """
138
+ _write_file(tools_dir / "lint_code.json", lint_tool, force)
139
+
140
+ # Search code tool
141
+ search_tool = """{
142
+ "name": "search_code",
143
+ "description": "Search for patterns in the codebase",
144
+ "inputSchema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "pattern": {
148
+ "type": "string",
149
+ "description": "Search pattern (regex supported)"
150
+ },
151
+ "file_pattern": {
152
+ "type": "string",
153
+ "description": "File glob pattern to search",
154
+ "default": "**/*"
155
+ },
156
+ "context_lines": {
157
+ "type": "integer",
158
+ "description": "Number of context lines",
159
+ "default": 2
160
+ }
161
+ },
162
+ "required": ["pattern"]
163
+ }
164
+ }
165
+ """
166
+ _write_file(tools_dir / "search_code.json", search_tool, force)
167
+
168
+
169
+ def _create_mcp_readme(mcp_dir: Path, force: bool) -> None:
170
+ """Create MCP README with usage instructions."""
171
+ content = """# MCP (Model Context Protocol) Configuration
172
+
173
+ This directory contains MCP server configuration for enhanced AI assistant integration.
174
+
175
+ ## Overview
176
+
177
+ MCP provides a standardized way to expose tools, resources, and prompts to AI assistants
178
+ like Claude and Cursor.
179
+
180
+ ## Structure
181
+
182
+ ```
183
+ .mcp/
184
+ ├── config.json # Main MCP configuration
185
+ ├── tools/ # Tool definitions
186
+ │ ├── project_status.json
187
+ │ ├── run_tests.json
188
+ │ ├── lint_code.json
189
+ │ └── search_code.json
190
+ └── README.md # This file
191
+ ```
192
+
193
+ ## Configuration
194
+
195
+ ### config.json
196
+
197
+ The main configuration file defines:
198
+
199
+ - **servers**: MCP server definitions
200
+ - **resources**: Read-only data sources
201
+ - **prompts**: Pre-defined prompt templates
202
+
203
+ ### Tools
204
+
205
+ Each tool in `tools/` is a JSON file with:
206
+
207
+ - `name`: Tool identifier
208
+ - `description`: What the tool does
209
+ - `inputSchema`: JSON Schema for parameters
210
+
211
+ ## Usage with Claude Code
212
+
213
+ Claude Code automatically detects MCP configuration.
214
+ Tools become available as `/tool_name` commands.
215
+
216
+ ## Usage with Cursor
217
+
218
+ Add to your Cursor settings:
219
+
220
+ ```json
221
+ {
222
+ "mcp.servers": {
223
+ "project": {
224
+ "configPath": ".mcp/config.json"
225
+ }
226
+ }
227
+ }
228
+ ```
229
+
230
+ ## Implementing Tool Handlers
231
+
232
+ Create a Python MCP server:
233
+
234
+ ```python
235
+ from mcp import Server, Tool
236
+
237
+ server = Server("project-tools")
238
+
239
+ @server.tool("project_status")
240
+ async def project_status(verbose: bool = False):
241
+ # Implementation
242
+ return {"status": "healthy"}
243
+
244
+ if __name__ == "__main__":
245
+ server.run()
246
+ ```
247
+
248
+ ## Resources
249
+
250
+ - [MCP Documentation](https://modelcontextprotocol.io/docs)
251
+ - [MCP Python SDK](https://github.com/anthropics/mcp-python-sdk)
252
+ """
253
+ _write_file(mcp_dir / "README.md", content, force)
254
+
255
+
256
+ def create_mcp_server_stub(target_dir: Path, force: bool = False) -> None:
257
+ """Create a stub MCP server implementation."""
258
+ content = '''#!/usr/bin/env python3
259
+ """
260
+ MCP Server for Project Tools
261
+
262
+ A Model Context Protocol server that provides project-specific tools
263
+ to AI assistants.
264
+
265
+ Usage:
266
+ python -m mcp_server
267
+
268
+ Or install as a package and run:
269
+ up-mcp-server
270
+ """
271
+
272
+ import json
273
+ import subprocess
274
+ from pathlib import Path
275
+ from typing import Any
276
+
277
+
278
+ class MCPServer:
279
+ """Simple MCP server implementation."""
280
+
281
+ def __init__(self, workspace: Path = None):
282
+ self.workspace = workspace or Path.cwd()
283
+ self.tools = {
284
+ "project_status": self.project_status,
285
+ "run_tests": self.run_tests,
286
+ "lint_code": self.lint_code,
287
+ "search_code": self.search_code,
288
+ }
289
+
290
+ async def project_status(self, verbose: bool = False) -> dict:
291
+ """Get project status."""
292
+ status = {"healthy": True, "systems": {}}
293
+
294
+ # Check loop state
295
+ loop_file = self.workspace / ".loop_state.json"
296
+ if loop_file.exists():
297
+ try:
298
+ data = json.loads(loop_file.read_text())
299
+ status["systems"]["loop"] = {
300
+ "iteration": data.get("iteration", 0),
301
+ "phase": data.get("phase", "UNKNOWN"),
302
+ }
303
+ except json.JSONDecodeError:
304
+ status["systems"]["loop"] = {"error": "Invalid state"}
305
+
306
+ # Check context budget
307
+ context_file = self.workspace / ".claude/context_budget.json"
308
+ if context_file.exists():
309
+ try:
310
+ data = json.loads(context_file.read_text())
311
+ status["systems"]["context"] = {
312
+ "usage_percent": data.get("usage_percent", 0),
313
+ "status": data.get("status", "OK"),
314
+ }
315
+ except json.JSONDecodeError:
316
+ status["systems"]["context"] = {"error": "Invalid state"}
317
+
318
+ if verbose:
319
+ status["workspace"] = str(self.workspace)
320
+ status["initialized"] = (self.workspace / ".claude").exists()
321
+
322
+ return status
323
+
324
+ async def run_tests(
325
+ self,
326
+ path: str = None,
327
+ pattern: str = None,
328
+ verbose: bool = False
329
+ ) -> dict:
330
+ """Run tests."""
331
+ cmd = ["pytest"]
332
+
333
+ if path:
334
+ cmd.append(path)
335
+
336
+ if pattern:
337
+ cmd.extend(["-k", pattern])
338
+
339
+ if verbose:
340
+ cmd.append("-v")
341
+ else:
342
+ cmd.append("-q")
343
+
344
+ try:
345
+ result = subprocess.run(
346
+ cmd,
347
+ cwd=self.workspace,
348
+ capture_output=True,
349
+ text=True,
350
+ timeout=120,
351
+ )
352
+ return {
353
+ "success": result.returncode == 0,
354
+ "output": result.stdout,
355
+ "errors": result.stderr,
356
+ }
357
+ except subprocess.TimeoutExpired:
358
+ return {"success": False, "error": "Test timeout"}
359
+ except FileNotFoundError:
360
+ return {"success": False, "error": "pytest not found"}
361
+
362
+ async def lint_code(self, path: str = None, fix: bool = False) -> dict:
363
+ """Run linter."""
364
+ cmd = ["ruff", "check"]
365
+
366
+ if path:
367
+ cmd.append(path)
368
+ else:
369
+ cmd.append(".")
370
+
371
+ if fix:
372
+ cmd.append("--fix")
373
+
374
+ try:
375
+ result = subprocess.run(
376
+ cmd,
377
+ cwd=self.workspace,
378
+ capture_output=True,
379
+ text=True,
380
+ timeout=60,
381
+ )
382
+ return {
383
+ "success": result.returncode == 0,
384
+ "output": result.stdout,
385
+ "errors": result.stderr,
386
+ }
387
+ except FileNotFoundError:
388
+ return {"success": False, "error": "ruff not found"}
389
+
390
+ async def search_code(
391
+ self,
392
+ pattern: str,
393
+ file_pattern: str = "**/*",
394
+ context_lines: int = 2
395
+ ) -> dict:
396
+ """Search code for pattern."""
397
+ cmd = [
398
+ "rg", # ripgrep
399
+ "-n", # line numbers
400
+ f"-C{context_lines}", # context
401
+ "-g", file_pattern,
402
+ pattern,
403
+ ]
404
+
405
+ try:
406
+ result = subprocess.run(
407
+ cmd,
408
+ cwd=self.workspace,
409
+ capture_output=True,
410
+ text=True,
411
+ timeout=30,
412
+ )
413
+
414
+ matches = []
415
+ if result.stdout:
416
+ for line in result.stdout.strip().split("\\n"):
417
+ matches.append(line)
418
+
419
+ return {
420
+ "success": True,
421
+ "match_count": len([m for m in matches if ":" in m]),
422
+ "matches": matches[:100], # Limit results
423
+ }
424
+ except FileNotFoundError:
425
+ return {"success": False, "error": "ripgrep not found"}
426
+
427
+ async def handle_request(self, request: dict) -> dict:
428
+ """Handle incoming MCP request."""
429
+ method = request.get("method")
430
+ params = request.get("params", {})
431
+
432
+ if method == "tools/list":
433
+ return {
434
+ "tools": [
435
+ {"name": name, "description": func.__doc__}
436
+ for name, func in self.tools.items()
437
+ ]
438
+ }
439
+
440
+ if method == "tools/call":
441
+ tool_name = params.get("name")
442
+ tool_args = params.get("arguments", {})
443
+
444
+ if tool_name in self.tools:
445
+ result = await self.tools[tool_name](**tool_args)
446
+ return {"result": result}
447
+ else:
448
+ return {"error": f"Unknown tool: {tool_name}"}
449
+
450
+ return {"error": f"Unknown method: {method}"}
451
+
452
+
453
+ async def main():
454
+ """Run MCP server."""
455
+ import sys
456
+
457
+ server = MCPServer()
458
+
459
+ # Simple stdin/stdout protocol for demonstration
460
+ # In production, use the official MCP SDK
461
+ for line in sys.stdin:
462
+ try:
463
+ request = json.loads(line)
464
+ response = await server.handle_request(request)
465
+ print(json.dumps(response), flush=True)
466
+ except json.JSONDecodeError:
467
+ print(json.dumps({"error": "Invalid JSON"}), flush=True)
468
+
469
+
470
+ if __name__ == "__main__":
471
+ import asyncio
472
+ asyncio.run(main())
473
+ '''
474
+ _write_file(target_dir / "mcp_server.py", content, force)