fiveclaw-agent 1.0.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,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: fiveclaw-agent
3
+ Version: 1.0.0
4
+ Summary: Local MCP agent for FiveClaw — FiveM AI development tools
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastmcp>=3.0
9
+ Provides-Extra: ssh
10
+ Requires-Dist: paramiko>=3.0; extra == "ssh"
11
+
12
+ # fiveclaw-agent
13
+
14
+ Local MCP agent for [FiveClaw](https://fiveclaw.com) — AI-powered FiveM development tools.
15
+
16
+ ## What it does
17
+
18
+ `fiveclaw-agent` runs on your machine alongside your FiveM server. It handles local operations (file search, logs, MySQL, txAdmin, SSH deploy) and relays AI analysis requests to the FiveClaw platform using your API key.
19
+
20
+ **Local tools** (run on your machine):
21
+ - Resource map generation and querying
22
+ - File search across Lua/JS resources
23
+ - Lua syntax checking
24
+ - Log reading
25
+ - MySQL query execution
26
+ - txAdmin server control (start/stop/restart resources, send console commands)
27
+ - SSH deployment to remote servers
28
+ - Persistent context memory
29
+
30
+ **Cloud tools** (powered by FiveClaw, gated by your plan):
31
+ - Resource validation and health checks
32
+ - Anti-pattern and duplicate code detection
33
+ - Export/event flow tracing
34
+ - AI code review and test generation
35
+ - FiveM native docs search
36
+ - Pattern library (scaffold new resources/features)
37
+
38
+ ## Requirements
39
+
40
+ - Python 3.10+
41
+ - A [FiveClaw](https://fiveclaw.com) account with an active subscription
42
+ - A FiveClaw API key (generate one at [fiveclaw.com/dashboard/keys](https://fiveclaw.com/dashboard/keys))
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install fiveclaw-agent
48
+
49
+ # With SSH deployment support
50
+ pip install fiveclaw-agent[ssh]
51
+ ```
52
+
53
+ ## Setup
54
+
55
+ 1. **Create a `.env` file** in your FiveM project root:
56
+
57
+ ```env
58
+ FIVECLAW_API_KEY=fc_live_your_key_here
59
+
60
+ # Optional — auto-detected if not set
61
+ # FIVEM_PROJECT_ROOT=/path/to/your/server
62
+
63
+ # Optional SSH deployment
64
+ # FIVEM_SSH_HOST=1.2.3.4
65
+ # FIVEM_SSH_USER=root
66
+ # FIVEM_SSH_KEY_PATH=~/.ssh/id_rsa
67
+ # FIVEM_REMOTE_RESOURCES=/server-data/resources
68
+ ```
69
+
70
+ 2. **Add to your Claude config** (`claude_desktop_config.json` or `.mcp.json`):
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "fiveclaw": {
76
+ "url": "http://localhost:5200/mcp"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ 3. **Start the agent** in your project directory:
83
+
84
+ ```bash
85
+ fiveclaw
86
+ ```
87
+
88
+ The agent runs on `http://localhost:5200/mcp` and stays running while you work. Restart it anytime without restarting Claude.
89
+
90
+ 4. **Verify** by asking Claude: *"Run mcp_health to check my FiveClaw connection."*
91
+
92
+ ## Configuration
93
+
94
+ SSH, txAdmin, and MySQL settings can also be configured on the [FiveClaw dashboard](https://fiveclaw.com/dashboard/server) — the agent fetches them automatically using your API key, so you don't need to duplicate them in `.env`.
95
+
96
+ ## Links
97
+
98
+ - [FiveClaw Dashboard](https://fiveclaw.com/dashboard)
99
+ - [Setup Guide](https://fiveclaw.com/dashboard/download)
100
+ - [Pricing](https://fiveclaw.com/pricing)
@@ -0,0 +1,89 @@
1
+ # fiveclaw-agent
2
+
3
+ Local MCP agent for [FiveClaw](https://fiveclaw.com) — AI-powered FiveM development tools.
4
+
5
+ ## What it does
6
+
7
+ `fiveclaw-agent` runs on your machine alongside your FiveM server. It handles local operations (file search, logs, MySQL, txAdmin, SSH deploy) and relays AI analysis requests to the FiveClaw platform using your API key.
8
+
9
+ **Local tools** (run on your machine):
10
+ - Resource map generation and querying
11
+ - File search across Lua/JS resources
12
+ - Lua syntax checking
13
+ - Log reading
14
+ - MySQL query execution
15
+ - txAdmin server control (start/stop/restart resources, send console commands)
16
+ - SSH deployment to remote servers
17
+ - Persistent context memory
18
+
19
+ **Cloud tools** (powered by FiveClaw, gated by your plan):
20
+ - Resource validation and health checks
21
+ - Anti-pattern and duplicate code detection
22
+ - Export/event flow tracing
23
+ - AI code review and test generation
24
+ - FiveM native docs search
25
+ - Pattern library (scaffold new resources/features)
26
+
27
+ ## Requirements
28
+
29
+ - Python 3.10+
30
+ - A [FiveClaw](https://fiveclaw.com) account with an active subscription
31
+ - A FiveClaw API key (generate one at [fiveclaw.com/dashboard/keys](https://fiveclaw.com/dashboard/keys))
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install fiveclaw-agent
37
+
38
+ # With SSH deployment support
39
+ pip install fiveclaw-agent[ssh]
40
+ ```
41
+
42
+ ## Setup
43
+
44
+ 1. **Create a `.env` file** in your FiveM project root:
45
+
46
+ ```env
47
+ FIVECLAW_API_KEY=fc_live_your_key_here
48
+
49
+ # Optional — auto-detected if not set
50
+ # FIVEM_PROJECT_ROOT=/path/to/your/server
51
+
52
+ # Optional SSH deployment
53
+ # FIVEM_SSH_HOST=1.2.3.4
54
+ # FIVEM_SSH_USER=root
55
+ # FIVEM_SSH_KEY_PATH=~/.ssh/id_rsa
56
+ # FIVEM_REMOTE_RESOURCES=/server-data/resources
57
+ ```
58
+
59
+ 2. **Add to your Claude config** (`claude_desktop_config.json` or `.mcp.json`):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "fiveclaw": {
65
+ "url": "http://localhost:5200/mcp"
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ 3. **Start the agent** in your project directory:
72
+
73
+ ```bash
74
+ fiveclaw
75
+ ```
76
+
77
+ The agent runs on `http://localhost:5200/mcp` and stays running while you work. Restart it anytime without restarting Claude.
78
+
79
+ 4. **Verify** by asking Claude: *"Run mcp_health to check my FiveClaw connection."*
80
+
81
+ ## Configuration
82
+
83
+ SSH, txAdmin, and MySQL settings can also be configured on the [FiveClaw dashboard](https://fiveclaw.com/dashboard/server) — the agent fetches them automatically using your API key, so you don't need to duplicate them in `.env`.
84
+
85
+ ## Links
86
+
87
+ - [FiveClaw Dashboard](https://fiveclaw.com/dashboard)
88
+ - [Setup Guide](https://fiveclaw.com/dashboard/download)
89
+ - [Pricing](https://fiveclaw.com/pricing)
@@ -0,0 +1,2 @@
1
+ """FiveClaw Agent — local MCP client for FiveClaw subscribers."""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,89 @@
1
+ """Configuration for the FiveClaw Agent."""
2
+
3
+ import os
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ def load_env_file():
9
+ for candidate in [Path.cwd() / ".env", Path(__file__).parent.parent / ".env"]:
10
+ if candidate.exists():
11
+ with open(candidate) as f:
12
+ for line in f:
13
+ line = line.strip()
14
+ if line and not line.startswith("#") and "=" in line:
15
+ key, value = line.split("=", 1)
16
+ if key not in os.environ:
17
+ os.environ[key] = value
18
+ break
19
+
20
+
21
+ class Config:
22
+ def __init__(self):
23
+ load_env_file()
24
+
25
+ self.api_key = os.getenv("FIVECLAW_API_KEY", "")
26
+ self.api_url = os.getenv("FIVECLAW_API_URL", "https://fiveclaw.gg").rstrip("/")
27
+
28
+ if not self.api_key:
29
+ raise RuntimeError(
30
+ "FIVECLAW_API_KEY is not set.\n"
31
+ "Add it to your .env file: FIVECLAW_API_KEY=fc_live_...\n"
32
+ "Get your key at https://fiveclaw.gg/dashboard/keys"
33
+ )
34
+
35
+ # Local project root (auto-detected or explicit)
36
+ env_root = os.getenv("FIVEM_PROJECT_ROOT", "")
37
+ if env_root and Path(env_root).exists():
38
+ self.project_root = Path(env_root)
39
+ else:
40
+ self.project_root = self._detect_project_root()
41
+
42
+ resources_override = os.getenv("FIVEM_RESOURCES_DIR", "")
43
+ self.resources_dir = (
44
+ Path(resources_override)
45
+ if resources_override
46
+ else self.project_root / "resources"
47
+ )
48
+
49
+ self.logs_dir = self.project_root / "logs"
50
+ self.context_dir = self.project_root / ".fiveclaw" / "context"
51
+ self.context_dir.mkdir(parents=True, exist_ok=True)
52
+
53
+ # SSH (for deploy_resource)
54
+ self.ssh = {
55
+ "host": os.getenv("FIVEM_SSH_HOST", ""),
56
+ "port": int(os.getenv("FIVEM_SSH_PORT", "22")),
57
+ "user": os.getenv("FIVEM_SSH_USER", ""),
58
+ "key_path": os.getenv("FIVEM_SSH_KEY", ""),
59
+ }
60
+
61
+ # MySQL (direct local connection)
62
+ self.mysql = {
63
+ "host": os.getenv("MYSQL_HOST", "127.0.0.1"),
64
+ "port": int(os.getenv("MYSQL_PORT", "3306")),
65
+ "user": os.getenv("MYSQL_USER", ""),
66
+ "password": os.getenv("MYSQL_PASSWORD", ""),
67
+ "database": os.getenv("MYSQL_DATABASE", ""),
68
+ }
69
+
70
+ # txAdmin
71
+ self.txadmin_url = os.getenv("TXADMIN_URL", "http://localhost:40120")
72
+ self.txadmin_token = os.getenv("TXADMIN_TOKEN", "")
73
+
74
+ def has_ssh(self) -> bool:
75
+ return bool(self.ssh["host"] and self.ssh["user"])
76
+
77
+ def has_mysql(self) -> bool:
78
+ return bool(self.mysql["user"] and self.mysql["database"])
79
+
80
+ def _detect_project_root(self) -> Path:
81
+ check = Path.cwd()
82
+ for _ in range(10):
83
+ if (check / "resources").is_dir() and list((check / "resources").glob("*/fxmanifest.lua")):
84
+ return check
85
+ parent = check.parent
86
+ if parent == check:
87
+ break
88
+ check = parent
89
+ return Path.cwd()
@@ -0,0 +1,422 @@
1
+ """
2
+ Local tool implementations — file I/O, SSH, MySQL, txAdmin.
3
+ These run entirely on the user's machine. No source logic is sent to the VPS.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from .config import Config
15
+
16
+
17
+ # ─── File collection helpers ──────────────────────────────────────────────────
18
+
19
+ def collect_resource_files(resources_dir: Path, resource_name: Optional[str] = None) -> dict[str, str]:
20
+ """
21
+ Collect Lua (and JS/TS) source files from one or all resources.
22
+ Returns {relative_path: content} — content capped at 50 KB per file.
23
+ """
24
+ MAX_FILE = 50_000
25
+ files: dict[str, str] = {}
26
+
27
+ dirs = (
28
+ [resources_dir / resource_name]
29
+ if resource_name
30
+ else [d for d in resources_dir.iterdir() if d.is_dir()]
31
+ )
32
+
33
+ for rdir in dirs:
34
+ if not rdir.exists():
35
+ continue
36
+ for ext in ("*.lua", "*.js", "*.ts"):
37
+ for f in rdir.rglob(ext):
38
+ try:
39
+ content = f.read_text(errors="ignore")
40
+ if len(content) > MAX_FILE:
41
+ content = content[:MAX_FILE] + "\n-- [truncated]"
42
+ rel = str(f.relative_to(resources_dir.parent))
43
+ files[rel] = content
44
+ except Exception:
45
+ pass
46
+
47
+ return files
48
+
49
+
50
+ # ─── RepoMap ──────────────────────────────────────────────────────────────────
51
+
52
+ class RepoMapTool:
53
+ def __init__(self, config: Config):
54
+ self.config = config
55
+ self._cache_file = config.context_dir / "repomap.json"
56
+
57
+ async def generate(self) -> str:
58
+ if not self.config.resources_dir.exists():
59
+ return json.dumps({"error": f"Resources directory not found: {self.config.resources_dir}"})
60
+
61
+ resources = {}
62
+ for rdir in self.config.resources_dir.iterdir():
63
+ if not rdir.is_dir():
64
+ continue
65
+ info: dict = {"name": rdir.name, "files": [], "exports": [], "events": []}
66
+ for f in rdir.rglob("*.lua"):
67
+ info["files"].append(str(f.relative_to(rdir)))
68
+ try:
69
+ text = f.read_text(errors="ignore")
70
+ info["exports"] += re.findall(r'exports\[[\'"](.*?)[\'"]\]', text)
71
+ info["events"] += re.findall(r'RegisterNetEvent\([\'"]([^\'"]+)[\'"]', text)
72
+ except Exception:
73
+ pass
74
+ resources[rdir.name] = info
75
+
76
+ result = {"resources": resources, "count": len(resources)}
77
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
78
+ self._cache_file.write_text(json.dumps(result, indent=2))
79
+ return json.dumps({"success": True, "resources_count": len(resources)})
80
+
81
+ async def query(self, query_type: str, filter: Optional[str] = None) -> str:
82
+ if not self._cache_file.exists():
83
+ return json.dumps({"error": "Run repomap_generate first."})
84
+ data = json.loads(self._cache_file.read_text())
85
+ resources = data.get("resources", {})
86
+
87
+ if filter:
88
+ resources = {k: v for k, v in resources.items() if filter.lower() in k.lower()}
89
+
90
+ if query_type == "exports":
91
+ out = {k: v.get("exports", []) for k, v in resources.items()}
92
+ elif query_type == "events":
93
+ out = {k: v.get("events", []) for k, v in resources.items()}
94
+ elif query_type == "files":
95
+ out = {k: v.get("files", []) for k, v in resources.items()}
96
+ else:
97
+ out = resources
98
+
99
+ return json.dumps(out, indent=2)
100
+
101
+ async def show(self) -> str:
102
+ if not self._cache_file.exists():
103
+ return json.dumps({"error": "Run repomap_generate first."})
104
+ return self._cache_file.read_text()
105
+
106
+
107
+ # ─── Search / File info ───────────────────────────────────────────────────────
108
+
109
+ class FileTool:
110
+ def __init__(self, config: Config):
111
+ self.config = config
112
+
113
+ async def search(self, pattern: str, path: Optional[str] = None) -> str:
114
+ search_path = Path(path) if path else self.config.resources_dir
115
+ if not search_path.exists():
116
+ return json.dumps({"error": f"Path not found: {search_path}"})
117
+
118
+ matches = []
119
+ for ext in ("*.lua", "*.js", "*.ts", "*.json"):
120
+ for f in search_path.rglob(ext):
121
+ try:
122
+ for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
123
+ if re.search(pattern, line, re.IGNORECASE):
124
+ matches.append({
125
+ "file": str(f.relative_to(search_path)),
126
+ "line": i,
127
+ "content": line.strip()[:200],
128
+ })
129
+ if len(matches) >= 100:
130
+ return json.dumps({"matches": matches, "truncated": True})
131
+ except Exception:
132
+ pass
133
+
134
+ return json.dumps({"matches": matches, "count": len(matches)})
135
+
136
+ async def file_info(self, file_path: str) -> str:
137
+ p = Path(file_path)
138
+ if not p.exists():
139
+ p = self.config.project_root / file_path
140
+ if not p.exists():
141
+ return json.dumps({"error": f"File not found: {file_path}"})
142
+
143
+ stat = p.stat()
144
+ try:
145
+ lines = len(p.read_text(errors="ignore").splitlines())
146
+ except Exception:
147
+ lines = None
148
+
149
+ return json.dumps({
150
+ "path": str(p),
151
+ "size_bytes": stat.st_size,
152
+ "lines": lines,
153
+ "modified": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)),
154
+ })
155
+
156
+ async def syntax_check(self, file_path: str) -> str:
157
+ p = Path(file_path)
158
+ if not p.exists():
159
+ p = self.config.resources_dir / file_path
160
+ if not p.exists():
161
+ return json.dumps({"error": f"File not found: {file_path}"})
162
+
163
+ result = subprocess.run(["luac", "-p", str(p)], capture_output=True, text=True)
164
+ if result.returncode == 0:
165
+ return json.dumps({"valid": True, "file": str(p)})
166
+ return json.dumps({"valid": False, "error": result.stderr.strip()})
167
+
168
+ async def read_logs(self, lines: int = 100, pattern: Optional[str] = None) -> str:
169
+ if not self.config.logs_dir.exists():
170
+ return json.dumps({"error": f"Logs directory not found: {self.config.logs_dir}"})
171
+
172
+ log_files = sorted(self.config.logs_dir.glob("*.log"), key=lambda f: f.stat().st_mtime, reverse=True)
173
+ if not log_files:
174
+ return json.dumps({"error": "No log files found."})
175
+
176
+ latest = log_files[0]
177
+ all_lines = latest.read_text(errors="ignore").splitlines()[-lines:]
178
+ if pattern:
179
+ all_lines = [l for l in all_lines if re.search(pattern, l, re.IGNORECASE)]
180
+
181
+ return json.dumps({"file": latest.name, "lines": all_lines, "count": len(all_lines)})
182
+
183
+
184
+ # ─── MySQL ────────────────────────────────────────────────────────────────────
185
+
186
+ class MySQLTool:
187
+ def __init__(self, config: Config):
188
+ self.config = config
189
+
190
+ async def query(self, query: str) -> str:
191
+ if not self.config.has_mysql():
192
+ return json.dumps({
193
+ "error": "MySQL not configured.",
194
+ "setup": "Set MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE in your .env",
195
+ })
196
+
197
+ result = subprocess.run(["which", "mysql"], capture_output=True)
198
+ if result.returncode != 0:
199
+ return json.dumps({"error": "mysql client not installed."})
200
+
201
+ db = self.config.mysql
202
+ cmd = [
203
+ "mysql",
204
+ "-h", db["host"],
205
+ "-P", str(db["port"]),
206
+ "-u", db["user"],
207
+ f"-p{db['password']}",
208
+ "-N", "-e", query, db["database"],
209
+ ]
210
+ result = subprocess.run(cmd, capture_output=True, text=True)
211
+ if result.returncode != 0:
212
+ return json.dumps({"error": result.stderr.strip()})
213
+
214
+ rows = [line.split("\t") for line in result.stdout.strip().splitlines() if line]
215
+ return json.dumps({"success": True, "rows": rows, "count": len(rows), "database": db["database"]})
216
+
217
+
218
+ # ─── txAdmin ──────────────────────────────────────────────────────────────────
219
+
220
+ class TxAdminTool:
221
+ def __init__(self, config: Config):
222
+ self.config = config
223
+
224
+ def _headers(self) -> dict:
225
+ h = {"Content-Type": "application/json"}
226
+ if self.config.txadmin_token:
227
+ h["X-TxAdmin-Token"] = self.config.txadmin_token
228
+ return h
229
+
230
+ async def server_status(self) -> str:
231
+ import urllib.request, urllib.error
232
+ try:
233
+ req = urllib.request.Request(
234
+ f"{self.config.txadmin_url}/api/server/status",
235
+ headers=self._headers(),
236
+ )
237
+ with urllib.request.urlopen(req, timeout=5) as r:
238
+ return r.read().decode()
239
+ except Exception as e:
240
+ return json.dumps({"error": str(e), "hint": f"Is txAdmin running at {self.config.txadmin_url}?"})
241
+
242
+ async def resource_control(self, action: str, resource_name: str) -> str:
243
+ import urllib.request
244
+ payload = json.dumps({"action": action, "resource": resource_name}).encode()
245
+ req = urllib.request.Request(
246
+ f"{self.config.txadmin_url}/api/server-control/command",
247
+ data=payload,
248
+ headers=self._headers(),
249
+ method="POST",
250
+ )
251
+ try:
252
+ with urllib.request.urlopen(req, timeout=10) as r:
253
+ return r.read().decode()
254
+ except Exception as e:
255
+ return json.dumps({"error": str(e)})
256
+
257
+ async def server_console(self, command: str) -> str:
258
+ import urllib.request
259
+ payload = json.dumps({"command": command}).encode()
260
+ req = urllib.request.Request(
261
+ f"{self.config.txadmin_url}/api/server-control/command",
262
+ data=payload,
263
+ headers=self._headers(),
264
+ method="POST",
265
+ )
266
+ try:
267
+ with urllib.request.urlopen(req, timeout=10) as r:
268
+ return r.read().decode()
269
+ except Exception as e:
270
+ return json.dumps({"error": str(e)})
271
+
272
+
273
+ # ─── Deploy ───────────────────────────────────────────────────────────────────
274
+
275
+ class DeployTool:
276
+ def __init__(self, config: Config):
277
+ self.config = config
278
+
279
+ async def deploy(self, resource_name: str, target: str = "production") -> str:
280
+ source = self.config.resources_dir / resource_name
281
+ if not source.exists():
282
+ return json.dumps({"error": f"Resource not found: {resource_name}"})
283
+
284
+ if self.config.has_ssh():
285
+ return await self._deploy_ssh(resource_name, source)
286
+
287
+ # Local copy
288
+ remote_res = os.getenv("FIVEM_REMOTE_RESOURCES_DIR", str(self.config.resources_dir))
289
+ target_path = (
290
+ Path(remote_res) / resource_name
291
+ if target in ("production", "txdata")
292
+ else Path(target) / resource_name
293
+ )
294
+
295
+ import shutil
296
+ backup_dir = self.config.project_root / "backups" / "deploy"
297
+ backup_dir.mkdir(parents=True, exist_ok=True)
298
+ backup_path = backup_dir / f"{resource_name}_{time.strftime('%Y%m%d_%H%M%S')}"
299
+
300
+ if target_path.exists():
301
+ shutil.copytree(target_path, backup_path)
302
+ shutil.rmtree(target_path)
303
+
304
+ shutil.copytree(source, target_path)
305
+ return json.dumps({
306
+ "success": True, "resource": resource_name,
307
+ "target": str(target_path), "method": "local",
308
+ })
309
+
310
+ async def _deploy_ssh(self, resource_name: str, source: Path) -> str:
311
+ try:
312
+ import paramiko
313
+ except ImportError:
314
+ return json.dumps({"error": "paramiko not installed. Run: pip install fiveclaw-agent[ssh]"})
315
+
316
+ remote_res = os.getenv("FIVEM_REMOTE_RESOURCES_DIR", "")
317
+ if not remote_res:
318
+ return json.dumps({"error": "FIVEM_REMOTE_RESOURCES_DIR not set."})
319
+
320
+ ssh_cfg = self.config.ssh
321
+ remote_target = f"{remote_res.rstrip('/')}/{resource_name}"
322
+
323
+ try:
324
+ client = paramiko.SSHClient()
325
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
326
+ kw: dict = {"hostname": ssh_cfg["host"], "port": ssh_cfg["port"], "username": ssh_cfg["user"]}
327
+ if ssh_cfg.get("key_path"):
328
+ kw["key_filename"] = ssh_cfg["key_path"]
329
+ client.connect(**kw, timeout=15)
330
+ sftp = client.open_sftp()
331
+
332
+ def _upload(local: Path, remote: str):
333
+ try: sftp.mkdir(remote)
334
+ except OSError: pass
335
+ for item in local.iterdir():
336
+ r = f"{remote}/{item.name}"
337
+ if item.is_dir(): _upload(item, r)
338
+ else: sftp.put(str(item), r)
339
+
340
+ client.exec_command(f"rm -rf '{remote_target}'")[1].channel.recv_exit_status()
341
+ _upload(source, remote_target)
342
+ sftp.close(); client.close()
343
+ return json.dumps({"success": True, "resource": resource_name, "target": remote_target,
344
+ "host": ssh_cfg["host"], "method": "ssh"})
345
+ except Exception as e:
346
+ return json.dumps({"error": f"SSH deploy failed: {e}"})
347
+
348
+ async def backup(self, resource_name: str) -> str:
349
+ source = self.config.resources_dir / resource_name
350
+ if not source.exists():
351
+ return json.dumps({"error": f"Resource not found: {resource_name}"})
352
+
353
+ import shutil
354
+ backup_dir = self.config.project_root / "backups" / "resources"
355
+ backup_dir.mkdir(parents=True, exist_ok=True)
356
+ ts = time.strftime("%Y%m%d_%H%M%S")
357
+ dest = backup_dir / f"{resource_name}_{ts}"
358
+ shutil.copytree(source, dest)
359
+ return json.dumps({"success": True, "backup": str(dest), "timestamp": ts})
360
+
361
+
362
+ # ─── Context memory ───────────────────────────────────────────────────────────
363
+
364
+ class ContextTool:
365
+ def __init__(self, config: Config):
366
+ self._file = config.context_dir / "knowledge.json"
367
+ self._ensure()
368
+
369
+ def _ensure(self):
370
+ self._file.parent.mkdir(parents=True, exist_ok=True)
371
+ if not self._file.exists():
372
+ self._file.write_text(json.dumps({"facts": {}, "history": []}))
373
+
374
+ def _load(self) -> dict:
375
+ return json.loads(self._file.read_text())
376
+
377
+ def _save(self, data: dict):
378
+ self._file.write_text(json.dumps(data, indent=2))
379
+
380
+ async def remember(self, key: str, value: str, category: str = "general") -> str:
381
+ data = self._load()
382
+ data["facts"][key] = {"value": value, "category": category, "ts": time.strftime("%Y-%m-%d %H:%M:%S")}
383
+ self._save(data)
384
+ return json.dumps({"saved": key})
385
+
386
+ async def recall(self, key: Optional[str] = None) -> str:
387
+ data = self._load()
388
+ if key:
389
+ return json.dumps(data["facts"].get(key, {"error": "Not found"}))
390
+ return json.dumps(data["facts"], indent=2)
391
+
392
+ async def search(self, query: str) -> str:
393
+ data = self._load()
394
+ q = query.lower()
395
+ hits = {k: v for k, v in data["facts"].items()
396
+ if q in k.lower() or q in str(v.get("value", "")).lower()}
397
+ return json.dumps(hits, indent=2)
398
+
399
+ async def forget(self, key: str) -> str:
400
+ data = self._load()
401
+ if key in data["facts"]:
402
+ del data["facts"][key]
403
+ self._save(data)
404
+ return json.dumps({"deleted": key})
405
+ return json.dumps({"error": f"Key not found: {key}"})
406
+
407
+ async def record(self, summary: str, tags: str) -> str:
408
+ data = self._load()
409
+ data.setdefault("history", []).append({
410
+ "summary": summary, "tags": tags.split(","),
411
+ "ts": time.strftime("%Y-%m-%d %H:%M:%S"),
412
+ })
413
+ data["history"] = data["history"][-200:]
414
+ self._save(data)
415
+ return json.dumps({"recorded": True})
416
+
417
+ async def history(self, limit: int = 10) -> str:
418
+ data = self._load()
419
+ return json.dumps(data.get("history", [])[-limit:], indent=2)
420
+
421
+ async def show(self) -> str:
422
+ return self._file.read_text()
@@ -0,0 +1,56 @@
1
+ """HTTP client — sends tool calls to the FiveClaw VPS for processing."""
2
+
3
+ import json
4
+ import urllib.request
5
+ import urllib.error
6
+ from typing import Any
7
+
8
+
9
+ class RemoteClient:
10
+ """Authenticated relay to the FiveClaw VPS intelligence API."""
11
+
12
+ def __init__(self, api_key: str, api_url: str):
13
+ self.api_key = api_key
14
+ self.api_url = api_url
15
+
16
+ def call(self, tool: str, params: dict, files: dict[str, str] | None = None) -> str:
17
+ """
18
+ POST to /api/mcp/tools with the tool name, params, and optional file content.
19
+ Returns the raw JSON string result.
20
+ """
21
+ payload = {
22
+ "tool": tool,
23
+ "params": params,
24
+ "files": files or {},
25
+ }
26
+
27
+ data = json.dumps(payload).encode()
28
+ req = urllib.request.Request(
29
+ f"{self.api_url}/api/mcp/tools",
30
+ data=data,
31
+ headers={
32
+ "Content-Type": "application/json",
33
+ "Authorization": f"Bearer {self.api_key}",
34
+ },
35
+ method="POST",
36
+ )
37
+
38
+ try:
39
+ with urllib.request.urlopen(req, timeout=120) as resp:
40
+ return resp.read().decode()
41
+ except urllib.error.HTTPError as e:
42
+ body = e.read().decode()
43
+ try:
44
+ err = json.loads(body)
45
+ except Exception:
46
+ err = {"error": body}
47
+
48
+ if e.code == 401:
49
+ return json.dumps({"error": "Invalid or expired API key. Check your FIVECLAW_API_KEY."})
50
+ if e.code == 402:
51
+ return json.dumps({"error": "Subscription required. Visit https://fiveclaw.gg/pricing"})
52
+ if e.code == 403:
53
+ return json.dumps({"error": err.get("error", "Access denied — your plan may not include this tool.")})
54
+ return json.dumps({"error": f"API error {e.code}: {err.get('error', body[:200])}"})
55
+ except Exception as e:
56
+ return json.dumps({"error": f"Could not reach FiveClaw API: {str(e)}"})
@@ -0,0 +1,302 @@
1
+ """
2
+ FiveClaw Agent — local MCP server.
3
+
4
+ Users run this on their machine and add it to their Claude Desktop config:
5
+ { "fiveclaw": { "url": "http://localhost:5200/mcp" } }
6
+
7
+ Local tools (file I/O, SSH, MySQL, txAdmin) run on the user's machine.
8
+ Intelligence tools (analysis, AI review, docs, patterns) relay to the FiveClaw VPS.
9
+ The VPS logic is never distributed — only results come back.
10
+ """
11
+
12
+ import os
13
+ from typing import Optional
14
+ from fastmcp import FastMCP
15
+
16
+ from .config import Config
17
+ from .local import RepoMapTool, FileTool, MySQLTool, TxAdminTool, DeployTool, ContextTool, collect_resource_files
18
+ from .remote import RemoteClient
19
+
20
+ # ─── Boot ─────────────────────────────────────────────────────────────────────
21
+
22
+ try:
23
+ config = Config()
24
+ except RuntimeError as e:
25
+ import sys
26
+ print(f"[FiveClaw Agent] Configuration error:\n{e}", file=sys.stderr)
27
+ sys.exit(1)
28
+
29
+ remote = RemoteClient(config.api_key, config.api_url)
30
+ repomap = RepoMapTool(config)
31
+ files = FileTool(config)
32
+ mysql = MySQLTool(config)
33
+ txadmin = TxAdminTool(config)
34
+ deploy = DeployTool(config)
35
+ context = ContextTool(config)
36
+
37
+ server = FastMCP("fiveclaw-agent")
38
+
39
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
40
+
41
+ def _resource_files(resource: Optional[str] = None) -> dict:
42
+ """Collect local source files to send to the VPS for analysis."""
43
+ return collect_resource_files(config.resources_dir, resource)
44
+
45
+ # =============================================================================
46
+ # LOCAL TOOLS — run on the user's machine
47
+ # =============================================================================
48
+
49
+ @server.tool()
50
+ async def repomap_generate() -> str:
51
+ """Scan your FiveM resources directory and build a map of all resources,
52
+ their files, exports, and event handlers. Run this first."""
53
+ return await repomap.generate()
54
+
55
+ @server.tool()
56
+ async def repomap_query(query_type: str, filter: Optional[str] = None) -> str:
57
+ """Query the resource map. query_type: 'exports', 'events', 'files', or 'all'."""
58
+ return await repomap.query(query_type, filter)
59
+
60
+ @server.tool()
61
+ async def repomap_show() -> str:
62
+ """Show the full resource map JSON."""
63
+ return await repomap.show()
64
+
65
+ @server.tool()
66
+ async def tool_search(pattern: str, path: Optional[str] = None) -> str:
67
+ """Search for a regex pattern across all Lua/JS files in your resources."""
68
+ return await files.search(pattern, path)
69
+
70
+ @server.tool()
71
+ async def tool_file_info(file_path: str) -> str:
72
+ """Get file size, line count, and last modified time."""
73
+ return await files.file_info(file_path)
74
+
75
+ @server.tool()
76
+ async def tool_syntax_check(file_path: str) -> str:
77
+ """Check Lua syntax using luac. Requires luac installed locally."""
78
+ return await files.syntax_check(file_path)
79
+
80
+ @server.tool()
81
+ async def read_latest_logs(lines: int = 100, pattern: Optional[str] = None) -> str:
82
+ """Read the latest FiveM server log file."""
83
+ return await files.read_logs(lines, pattern)
84
+
85
+ @server.tool()
86
+ async def tool_mysql_query(query: str) -> str:
87
+ """Execute a SQL query against your locally configured MySQL database."""
88
+ return await mysql.query(query)
89
+
90
+ @server.tool()
91
+ async def tool_server_status() -> str:
92
+ """Check FiveM server status via your txAdmin panel."""
93
+ return await txadmin.server_status()
94
+
95
+ @server.tool()
96
+ async def tool_resource_control(action: str, resource_name: str) -> str:
97
+ """Start, stop, or restart a resource via txAdmin. action: 'start'|'stop'|'restart'."""
98
+ return await txadmin.resource_control(action, resource_name)
99
+
100
+ @server.tool()
101
+ async def tool_server_console(command: str) -> str:
102
+ """Send a raw command to the FiveM server console via txAdmin."""
103
+ return await txadmin.server_console(command)
104
+
105
+ @server.tool()
106
+ async def deploy_resource(resource_name: str, target: str = "production") -> str:
107
+ """Deploy a resource to your FiveM server.
108
+ Uses SSH if FIVEM_SSH_HOST is configured, otherwise local file copy.
109
+ target: 'production' or an absolute path."""
110
+ return await deploy.deploy(resource_name, target)
111
+
112
+ @server.tool()
113
+ async def backup_resource(resource_name: str) -> str:
114
+ """Create a timestamped backup of a resource in your project's backups/ folder."""
115
+ return await deploy.backup(resource_name)
116
+
117
+ # Context memory
118
+ @server.tool()
119
+ async def context_remember(key: str, value: str, category: str = "general") -> str:
120
+ """Save a fact to persistent local memory."""
121
+ return await context.remember(key, value, category)
122
+
123
+ @server.tool()
124
+ async def context_recall(key: Optional[str] = None) -> str:
125
+ """Recall a saved fact, or list all facts if no key given."""
126
+ return await context.recall(key)
127
+
128
+ @server.tool()
129
+ async def context_search(query: str) -> str:
130
+ """Search your saved facts."""
131
+ return await context.search(query)
132
+
133
+ @server.tool()
134
+ async def context_forget(key: str) -> str:
135
+ """Delete a saved fact."""
136
+ return await context.forget(key)
137
+
138
+ @server.tool()
139
+ async def context_record(summary: str, tags: str) -> str:
140
+ """Record a session note (comma-separated tags)."""
141
+ return await context.record(summary, tags)
142
+
143
+ @server.tool()
144
+ async def context_history(limit: int = 10) -> str:
145
+ """Show recent session history."""
146
+ return await context.history(limit)
147
+
148
+ # =============================================================================
149
+ # REMOTE TOOLS — relay to FiveClaw VPS (your logic stays server-side)
150
+ # =============================================================================
151
+
152
+ @server.tool()
153
+ async def tool_validate_resource(resource_name: str) -> str:
154
+ """Validate a FiveM resource — checks manifest, syntax, structure, and best practices."""
155
+ return remote.call("validate_resource", {"resource_name": resource_name},
156
+ files=_resource_files(resource_name))
157
+
158
+ @server.tool()
159
+ async def resource_health_check(resource_name: str) -> str:
160
+ """Comprehensive health check: manifest, syntax, NUI build, TODOs, dependencies."""
161
+ return remote.call("resource_health_check", {"resource_name": resource_name},
162
+ files=_resource_files(resource_name))
163
+
164
+ @server.tool()
165
+ async def detect_anti_patterns(resource: Optional[str] = None) -> str:
166
+ """Detect common FiveM anti-patterns: busy waits, missing locals, performance issues."""
167
+ return remote.call("detect_anti_patterns", {"resource": resource},
168
+ files=_resource_files(resource))
169
+
170
+ @server.tool()
171
+ async def detect_duplicate_code(min_lines: int = 10, resource: Optional[str] = None) -> str:
172
+ """Find duplicate or copy-pasted code blocks across your resources."""
173
+ return remote.call("detect_duplicate_code", {"min_lines": min_lines, "resource": resource},
174
+ files=_resource_files(resource))
175
+
176
+ @server.tool()
177
+ async def find_exports(export_name: Optional[str] = None, resource: Optional[str] = None) -> str:
178
+ """Find where exports are defined and called across your resources."""
179
+ return remote.call("find_exports", {"export_name": export_name, "resource": resource},
180
+ files=_resource_files(resource))
181
+
182
+ @server.tool()
183
+ async def find_event_handlers(event_name: Optional[str] = None, resource: Optional[str] = None) -> str:
184
+ """Find RegisterNetEvent and AddEventHandler calls."""
185
+ return remote.call("find_event_handlers", {"event_name": event_name, "resource": resource},
186
+ files=_resource_files(resource))
187
+
188
+ @server.tool()
189
+ async def find_triggers(pattern: Optional[str] = None, resource: Optional[str] = None) -> str:
190
+ """Find TriggerServerEvent and TriggerClientEvent calls."""
191
+ return remote.call("find_triggers", {"pattern": pattern, "resource": resource},
192
+ files=_resource_files(resource))
193
+
194
+ @server.tool()
195
+ async def nui_build_check(resource_name: str) -> str:
196
+ """Check if a NUI resource needs to be rebuilt."""
197
+ return remote.call("nui_build_check", {"resource_name": resource_name},
198
+ files=_resource_files(resource_name))
199
+
200
+ @server.tool()
201
+ async def test_resource(resource_name: str, mode: str = "auto") -> str:
202
+ """Test a FiveM resource using the FiveClaw test engine. mode: 'auto' or 'analyze'."""
203
+ return remote.call("test_resource", {"resource_name": resource_name, "mode": mode},
204
+ files=_resource_files(resource_name))
205
+
206
+ @server.tool()
207
+ async def test_generate(resource_name: str) -> str:
208
+ """Generate test cases for a resource without running them."""
209
+ return remote.call("test_generate", {"resource_name": resource_name},
210
+ files=_resource_files(resource_name))
211
+
212
+ @server.tool()
213
+ async def trace_event_flow(event_name: str, max_depth: int = 3) -> str:
214
+ """Trace how an event flows through your resources."""
215
+ return remote.call("trace_event_flow", {"event_name": event_name, "max_depth": max_depth},
216
+ files=_resource_files())
217
+
218
+ @server.tool()
219
+ async def analyze_export_usage(caller: str, target: str, export_name: Optional[str] = None) -> str:
220
+ """Analyze how a resource uses another resource's exports."""
221
+ return remote.call("analyze_export_usage",
222
+ {"caller": caller, "target": target, "export_name": export_name},
223
+ files=_resource_files())
224
+
225
+ @server.tool()
226
+ async def mysql_visualize_schema() -> str:
227
+ """Visualize your database schema as an ASCII diagram."""
228
+ return remote.call("mysql_visualize_schema", {})
229
+
230
+ @server.tool()
231
+ async def pattern_list() -> str:
232
+ """List all available FiveM code patterns in the FiveClaw pattern library."""
233
+ return remote.call("pattern_list", {})
234
+
235
+ @server.tool()
236
+ async def pattern_show(pattern_name: str) -> str:
237
+ """Show the full code for a pattern from the FiveClaw library."""
238
+ return remote.call("pattern_show", {"pattern_name": pattern_name})
239
+
240
+ @server.tool()
241
+ async def pattern_apply(pattern_name: str, variables_json: str,
242
+ output_dir: Optional[str] = None, dry_run: bool = False) -> str:
243
+ """Apply a FiveClaw code pattern to scaffold a new resource or feature."""
244
+ return remote.call("pattern_apply",
245
+ {"pattern_name": pattern_name, "variables_json": variables_json,
246
+ "output_dir": output_dir, "dry_run": dry_run})
247
+
248
+ @server.tool()
249
+ async def fivem_docs(query: str) -> str:
250
+ """Search the FiveM native documentation, scripting guides, and framework references."""
251
+ return remote.call("fivem_docs", {"query": query})
252
+
253
+ @server.tool()
254
+ async def fivem_native(native_name: str) -> str:
255
+ """Get detailed information about a FiveM native function."""
256
+ return remote.call("fivem_native", {"native_name": native_name})
257
+
258
+ @server.tool()
259
+ async def mcp_health() -> str:
260
+ """Check agent status and verify your FiveClaw API key is valid."""
261
+ import json as _json
262
+ result = remote.call("mcp_health", {})
263
+ try:
264
+ data = _json.loads(result)
265
+ data["agent_version"] = "1.0.0"
266
+ data["project_root"] = str(config.project_root)
267
+ data["resources_dir"] = str(config.resources_dir)
268
+ data["ssh_configured"] = config.has_ssh()
269
+ data["mysql_configured"] = config.has_mysql()
270
+ return _json.dumps(data, indent=2)
271
+ except Exception:
272
+ return result
273
+
274
+ # =============================================================================
275
+ # Entry point
276
+ # =============================================================================
277
+
278
+ def main():
279
+ import signal, sys
280
+
281
+ def _stop(sig, frame):
282
+ print("\n[FiveClaw Agent] Shutting down.", file=sys.stderr)
283
+ sys.exit(0)
284
+
285
+ signal.signal(signal.SIGINT, _stop)
286
+ signal.signal(signal.SIGTERM, _stop)
287
+
288
+ transport = os.getenv("TRANSPORT", "streamable-http")
289
+ host = os.getenv("HOST", "127.0.0.1") # localhost only by default
290
+ port = int(os.getenv("PORT", "5200"))
291
+
292
+ print(f"[FiveClaw Agent] Starting on {transport}://{host}:{port}/mcp", file=sys.stderr)
293
+ print(f"[FiveClaw Agent] Project root: {config.project_root}", file=sys.stderr)
294
+
295
+ if transport == "streamable-http":
296
+ server.run(transport="streamable-http", host=host, port=port)
297
+ else:
298
+ server.run(transport="stdio")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: fiveclaw-agent
3
+ Version: 1.0.0
4
+ Summary: Local MCP agent for FiveClaw — FiveM AI development tools
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastmcp>=3.0
9
+ Provides-Extra: ssh
10
+ Requires-Dist: paramiko>=3.0; extra == "ssh"
11
+
12
+ # fiveclaw-agent
13
+
14
+ Local MCP agent for [FiveClaw](https://fiveclaw.com) — AI-powered FiveM development tools.
15
+
16
+ ## What it does
17
+
18
+ `fiveclaw-agent` runs on your machine alongside your FiveM server. It handles local operations (file search, logs, MySQL, txAdmin, SSH deploy) and relays AI analysis requests to the FiveClaw platform using your API key.
19
+
20
+ **Local tools** (run on your machine):
21
+ - Resource map generation and querying
22
+ - File search across Lua/JS resources
23
+ - Lua syntax checking
24
+ - Log reading
25
+ - MySQL query execution
26
+ - txAdmin server control (start/stop/restart resources, send console commands)
27
+ - SSH deployment to remote servers
28
+ - Persistent context memory
29
+
30
+ **Cloud tools** (powered by FiveClaw, gated by your plan):
31
+ - Resource validation and health checks
32
+ - Anti-pattern and duplicate code detection
33
+ - Export/event flow tracing
34
+ - AI code review and test generation
35
+ - FiveM native docs search
36
+ - Pattern library (scaffold new resources/features)
37
+
38
+ ## Requirements
39
+
40
+ - Python 3.10+
41
+ - A [FiveClaw](https://fiveclaw.com) account with an active subscription
42
+ - A FiveClaw API key (generate one at [fiveclaw.com/dashboard/keys](https://fiveclaw.com/dashboard/keys))
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install fiveclaw-agent
48
+
49
+ # With SSH deployment support
50
+ pip install fiveclaw-agent[ssh]
51
+ ```
52
+
53
+ ## Setup
54
+
55
+ 1. **Create a `.env` file** in your FiveM project root:
56
+
57
+ ```env
58
+ FIVECLAW_API_KEY=fc_live_your_key_here
59
+
60
+ # Optional — auto-detected if not set
61
+ # FIVEM_PROJECT_ROOT=/path/to/your/server
62
+
63
+ # Optional SSH deployment
64
+ # FIVEM_SSH_HOST=1.2.3.4
65
+ # FIVEM_SSH_USER=root
66
+ # FIVEM_SSH_KEY_PATH=~/.ssh/id_rsa
67
+ # FIVEM_REMOTE_RESOURCES=/server-data/resources
68
+ ```
69
+
70
+ 2. **Add to your Claude config** (`claude_desktop_config.json` or `.mcp.json`):
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "fiveclaw": {
76
+ "url": "http://localhost:5200/mcp"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ 3. **Start the agent** in your project directory:
83
+
84
+ ```bash
85
+ fiveclaw
86
+ ```
87
+
88
+ The agent runs on `http://localhost:5200/mcp` and stays running while you work. Restart it anytime without restarting Claude.
89
+
90
+ 4. **Verify** by asking Claude: *"Run mcp_health to check my FiveClaw connection."*
91
+
92
+ ## Configuration
93
+
94
+ SSH, txAdmin, and MySQL settings can also be configured on the [FiveClaw dashboard](https://fiveclaw.com/dashboard/server) — the agent fetches them automatically using your API key, so you don't need to duplicate them in `.env`.
95
+
96
+ ## Links
97
+
98
+ - [FiveClaw Dashboard](https://fiveclaw.com/dashboard)
99
+ - [Setup Guide](https://fiveclaw.com/dashboard/download)
100
+ - [Pricing](https://fiveclaw.com/pricing)
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ fiveclaw_agent/__init__.py
4
+ fiveclaw_agent/config.py
5
+ fiveclaw_agent/local.py
6
+ fiveclaw_agent/remote.py
7
+ fiveclaw_agent/server.py
8
+ fiveclaw_agent.egg-info/PKG-INFO
9
+ fiveclaw_agent.egg-info/SOURCES.txt
10
+ fiveclaw_agent.egg-info/dependency_links.txt
11
+ fiveclaw_agent.egg-info/entry_points.txt
12
+ fiveclaw_agent.egg-info/requires.txt
13
+ fiveclaw_agent.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fiveclaw = fiveclaw_agent.server:main
@@ -0,0 +1,4 @@
1
+ fastmcp>=3.0
2
+
3
+ [ssh]
4
+ paramiko>=3.0
@@ -0,0 +1 @@
1
+ fiveclaw_agent
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fiveclaw-agent"
7
+ version = "1.0.0"
8
+ description = "Local MCP agent for FiveClaw — FiveM AI development tools"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "fastmcp>=3.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ ssh = ["paramiko>=3.0"]
18
+
19
+ [project.scripts]
20
+ fiveclaw = "fiveclaw_agent.server:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["."]
24
+ include = ["fiveclaw_agent*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+