castrel-proxy 0.1.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,403 @@
1
+ """
2
+ Command Whitelist Management Module
3
+
4
+ Responsible for loading and managing command whitelist, checking if commands are allowed to execute
5
+ """
6
+
7
+ import importlib.resources
8
+ import logging
9
+ import os
10
+ import re
11
+ from typing import List, Set, Tuple
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Whitelist configuration file path
17
+ WHITELIST_FILE_PATH = os.path.join(os.path.expanduser("~"), ".castrel", "whitelist.conf")
18
+
19
+ # Cache default whitelist (read from package data file, won't change, can be cached)
20
+ _default_whitelist_cache: List[str] = []
21
+ _default_whitelist_loaded: bool = False
22
+
23
+
24
+ def _load_default_whitelist() -> List[str]:
25
+ """
26
+ Load default whitelist command list from package data file
27
+
28
+ Returns:
29
+ List[str]: Default whitelist command list
30
+ """
31
+ global _default_whitelist_cache, _default_whitelist_loaded
32
+
33
+ if _default_whitelist_loaded:
34
+ return _default_whitelist_cache
35
+
36
+ commands = []
37
+
38
+ try:
39
+ # Use importlib.resources to read package data file
40
+ # Python 3.9+ recommends using files() API
41
+ data_files = importlib.resources.files("castrel_proxy.data")
42
+ whitelist_file = data_files.joinpath("default_whitelist.txt")
43
+
44
+ content = whitelist_file.read_text(encoding="utf-8")
45
+
46
+ for line in content.splitlines():
47
+ line = line.strip()
48
+ # Skip empty lines and comment lines
49
+ if not line or line.startswith("#"):
50
+ continue
51
+ commands.append(line)
52
+
53
+ logger.debug(f"[WHITELIST] Loaded {len(commands)} default commands from package data")
54
+
55
+ except Exception as e:
56
+ logger.error(f"[WHITELIST] Failed to load default whitelist from package: {e}")
57
+ # If loading fails, return basic command list as fallback
58
+ commands = [
59
+ "ls",
60
+ "cat",
61
+ "head",
62
+ "tail",
63
+ "grep",
64
+ "find",
65
+ "pwd",
66
+ "cd",
67
+ "mkdir",
68
+ "echo",
69
+ "git",
70
+ "python",
71
+ "python3",
72
+ "pip",
73
+ "pip3",
74
+ "node",
75
+ "npm",
76
+ ]
77
+
78
+ _default_whitelist_cache = commands
79
+ _default_whitelist_loaded = True
80
+
81
+ return commands
82
+
83
+
84
+ def get_default_whitelist() -> List[str]:
85
+ """
86
+ Get default whitelist command list
87
+
88
+ Returns:
89
+ List[str]: Default whitelist command list
90
+ """
91
+ return _load_default_whitelist()
92
+
93
+
94
+ def _ensure_whitelist_file_exists() -> None:
95
+ """
96
+ Ensure whitelist configuration file exists, copy default config from package data file if not exists
97
+ """
98
+ if os.path.exists(WHITELIST_FILE_PATH):
99
+ return
100
+
101
+ # Ensure directory exists
102
+ whitelist_dir = os.path.dirname(WHITELIST_FILE_PATH)
103
+ os.makedirs(whitelist_dir, exist_ok=True)
104
+
105
+ try:
106
+ # Read default config from package data file
107
+ data_files = importlib.resources.files("castrel_proxy.data")
108
+ whitelist_file = data_files.joinpath("default_whitelist.txt")
109
+ content = whitelist_file.read_text(encoding="utf-8")
110
+
111
+ # Write to user configuration file
112
+ with open(WHITELIST_FILE_PATH, "w", encoding="utf-8") as f:
113
+ f.write(content)
114
+
115
+ logger.info(f"[WHITELIST] Created default whitelist file from package data: {WHITELIST_FILE_PATH}")
116
+
117
+ except Exception as e:
118
+ logger.error(f"[WHITELIST] Failed to copy default whitelist from package: {e}")
119
+ # If reading from package fails, create using basic command list
120
+ basic_commands = get_default_whitelist()
121
+ content = "# Castrel Command Whitelist Configuration File\n"
122
+ content += "# One command name per line, lines starting with # are comments\n\n"
123
+ content += "\n".join(basic_commands) + "\n"
124
+
125
+ with open(WHITELIST_FILE_PATH, "w", encoding="utf-8") as f:
126
+ f.write(content)
127
+
128
+ logger.info(f"[WHITELIST] Created basic whitelist file: {WHITELIST_FILE_PATH}")
129
+
130
+
131
+ def load_whitelist() -> Set[str]:
132
+ """
133
+ Load whitelist configuration (re-read file on each call, supports dynamic modification)
134
+
135
+ Returns:
136
+ Set[str]: Whitelist command set
137
+ """
138
+ # Ensure configuration file exists
139
+ _ensure_whitelist_file_exists()
140
+
141
+ whitelist: Set[str] = set()
142
+
143
+ try:
144
+ with open(WHITELIST_FILE_PATH, "r", encoding="utf-8") as f:
145
+ for line in f:
146
+ # Remove leading and trailing whitespace
147
+ line = line.strip()
148
+
149
+ # Skip empty lines and comment lines
150
+ if not line or line.startswith("#"):
151
+ continue
152
+
153
+ # Add to whitelist
154
+ whitelist.add(line)
155
+
156
+ logger.debug(f"[WHITELIST] Loaded {len(whitelist)} commands from whitelist: {WHITELIST_FILE_PATH}")
157
+
158
+ except Exception as e:
159
+ logger.error(f"[WHITELIST] Failed to load whitelist file: {e}")
160
+ # If loading fails, use default whitelist
161
+ whitelist = set(get_default_whitelist())
162
+ logger.warning(f"[WHITELIST] Using default whitelist with {len(whitelist)} commands")
163
+
164
+ return whitelist
165
+
166
+
167
+ def _get_base_command(command: str) -> str:
168
+ """
169
+ Extract base command name from a single command
170
+
171
+ Args:
172
+ command: Single command string
173
+
174
+ Returns:
175
+ str: Base command name
176
+ """
177
+ if not command or not command.strip():
178
+ return ""
179
+
180
+ # Remove leading and trailing whitespace
181
+ command = command.strip()
182
+
183
+ # Skip environment variable assignment prefix (e.g., VAR=value cmd)
184
+ while "=" in command.split()[0] if command.split() else False:
185
+ parts = command.split(None, 1)
186
+ if len(parts) > 1:
187
+ command = parts[1]
188
+ else:
189
+ return ""
190
+
191
+ # Extract first word of command
192
+ parts = command.split()
193
+ if not parts:
194
+ return ""
195
+
196
+ base_command = parts[0]
197
+
198
+ # Handle path-style commands (e.g., /usr/bin/ls -> ls)
199
+ base_command = os.path.basename(base_command)
200
+
201
+ return base_command
202
+
203
+
204
+ def _remove_quoted_strings(command: str) -> str:
205
+ """
206
+ Remove quoted content from command to avoid operators inside quotes being split
207
+
208
+ Args:
209
+ command: Command string
210
+
211
+ Returns:
212
+ str: Command with quoted content removed
213
+ """
214
+ result = []
215
+ i = 0
216
+ in_single_quote = False
217
+ in_double_quote = False
218
+ escape_next = False
219
+
220
+ while i < len(command):
221
+ char = command[i]
222
+
223
+ if escape_next:
224
+ escape_next = False
225
+ i += 1
226
+ continue
227
+
228
+ if char == "\\":
229
+ escape_next = True
230
+ i += 1
231
+ continue
232
+
233
+ if char == "'" and not in_double_quote:
234
+ in_single_quote = not in_single_quote
235
+ i += 1
236
+ continue
237
+
238
+ if char == '"' and not in_single_quote:
239
+ in_double_quote = not in_double_quote
240
+ i += 1
241
+ continue
242
+
243
+ if not in_single_quote and not in_double_quote:
244
+ result.append(char)
245
+
246
+ i += 1
247
+
248
+ return "".join(result)
249
+
250
+
251
+ def _extract_command_substitutions(command: str) -> List[str]:
252
+ """
253
+ Extract commands from command substitutions: $(...) and `...`
254
+
255
+ Args:
256
+ command: Command string
257
+
258
+ Returns:
259
+ List[str]: List of commands from command substitutions
260
+ """
261
+ substitutions = []
262
+
263
+ # Match $(...) - need to handle nested parentheses
264
+ # Simplified handling: use regex to match non-nested cases
265
+ dollar_paren_pattern = r"\$\(([^()]*)\)"
266
+ for match in re.finditer(dollar_paren_pattern, command):
267
+ inner_cmd = match.group(1).strip()
268
+ if inner_cmd:
269
+ substitutions.append(inner_cmd)
270
+
271
+ # Match `...` backticks
272
+ backtick_pattern = r"`([^`]*)`"
273
+ for match in re.finditer(backtick_pattern, command):
274
+ inner_cmd = match.group(1).strip()
275
+ if inner_cmd:
276
+ substitutions.append(inner_cmd)
277
+
278
+ return substitutions
279
+
280
+
281
+ def _extract_commands(full_command: str) -> List[str]:
282
+ """
283
+ Extract all subcommands from compound command
284
+
285
+ Handled operators:
286
+ - Command separators: &&, ||, ;, & (background execution)
287
+ - Pipes: |, |&
288
+ - Command substitutions: $(...), `...`
289
+
290
+ Args:
291
+ full_command: Complete command string
292
+
293
+ Returns:
294
+ List[str]: List of all subcommands
295
+ """
296
+ if not full_command or not full_command.strip():
297
+ return []
298
+
299
+ commands = []
300
+
301
+ # First extract commands from command substitutions
302
+ substitutions = _extract_command_substitutions(full_command)
303
+ for sub in substitutions:
304
+ # Recursively extract subcommands from command substitutions
305
+ commands.extend(_extract_commands(sub))
306
+
307
+ # Remove quoted content to avoid operators inside quotes being split
308
+ cleaned_command = _remove_quoted_strings(full_command)
309
+
310
+ # Use regex to split commands
311
+ # Match: &&, ||, |&, |, ;, & (but not single characters in && or |&)
312
+ # Note order: match longer operators first
313
+ split_pattern = r"\s*(?:&&|\|\||;\s*|\|&|\||\s+&\s+|&\s*$)\s*"
314
+
315
+ # Split commands
316
+ parts = re.split(split_pattern, cleaned_command)
317
+
318
+ for part in parts:
319
+ part = part.strip()
320
+ if part:
321
+ # Remove redirection parts (but keep the command itself)
322
+ # Match: >, >>, <, <<, 2>, 2>>, &>, >&, etc.
323
+ # Only remove redirection symbols and following file paths
324
+ redirect_pattern = r"\s*(?:\d*>>?|<<?|&>|>&)\s*\S+"
325
+ part_no_redirect = re.sub(redirect_pattern, "", part).strip()
326
+
327
+ if part_no_redirect:
328
+ commands.append(part_no_redirect)
329
+
330
+ return commands
331
+
332
+
333
+ def is_command_allowed(full_command: str) -> Tuple[bool, List[str]]:
334
+ """
335
+ Check if command is in whitelist
336
+
337
+ Parse all subcommands in compound command, ensure each subcommand is in whitelist.
338
+
339
+ Args:
340
+ full_command: Complete command string
341
+
342
+ Returns:
343
+ Tuple[bool, List[str]]: (Whether execution is allowed, List of commands not in whitelist)
344
+ """
345
+ if not full_command or not full_command.strip():
346
+ return False, ["(empty command)"]
347
+
348
+ # Extract all subcommands
349
+ commands = _extract_commands(full_command)
350
+
351
+ if not commands:
352
+ # If no commands extracted, try to get base command directly
353
+ base_cmd = _get_base_command(full_command)
354
+ if base_cmd:
355
+ commands = [full_command]
356
+ else:
357
+ return False, ["(empty command)"]
358
+
359
+ # Load whitelist
360
+ whitelist = load_whitelist()
361
+
362
+ # Check each subcommand
363
+ blocked_commands = []
364
+ for cmd in commands:
365
+ base_cmd = _get_base_command(cmd)
366
+ if not base_cmd:
367
+ continue
368
+
369
+ if base_cmd not in whitelist:
370
+ blocked_commands.append(base_cmd)
371
+
372
+ is_allowed = len(blocked_commands) == 0
373
+
374
+ if not is_allowed:
375
+ logger.warning(
376
+ f"[WHITELIST] Commands not in whitelist: blocked={blocked_commands}, full_command={full_command[:200]}"
377
+ )
378
+
379
+ return is_allowed, blocked_commands
380
+
381
+
382
+ def get_whitelist_file_path() -> str:
383
+ """
384
+ Get whitelist configuration file path
385
+
386
+ Returns:
387
+ str: Full path to whitelist configuration file
388
+ """
389
+ return WHITELIST_FILE_PATH
390
+
391
+
392
+ def init_whitelist_file() -> str:
393
+ """
394
+ Initialize whitelist configuration file
395
+
396
+ If file does not exist, copy default config from package data file to user directory.
397
+ If file already exists, do nothing.
398
+
399
+ Returns:
400
+ str: Whitelist configuration file path
401
+ """
402
+ _ensure_whitelist_file_exists()
403
+ return WHITELIST_FILE_PATH
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.4
2
+ Name: castrel-proxy
3
+ Version: 0.1.0
4
+ Summary: A lightweight remote command execution bridge client with MCP integration
5
+ Project-URL: Homepage, https://github.com/castrel-ai/castrel-bridge-proxy
6
+ Project-URL: Documentation, https://github.com/castrel-ai/castrel-bridge-proxy#readme
7
+ Project-URL: Repository, https://github.com/castrel-ai/castrel-bridge-proxy
8
+ Project-URL: Issues, https://github.com/castrel-ai/castrel-bridge-proxy/issues
9
+ Project-URL: Changelog, https://github.com/castrel-ai/castrel-bridge-proxy/blob/main/CHANGELOG.md
10
+ Author: Castrel Team
11
+ License: MIT License
12
+
13
+ Copyright (c) 2025 Cloudwise
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: bridge,mcp,proxy,remote-execution,websocket
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Topic :: System :: Networking
45
+ Requires-Python: >=3.10
46
+ Requires-Dist: aiohttp>=3.9.0
47
+ Requires-Dist: langchain-mcp-adapters>=0.2.1
48
+ Requires-Dist: mcp>=1.0.0
49
+ Requires-Dist: pyyaml>=6.0.1
50
+ Requires-Dist: typer>=0.20.1
51
+ Provides-Extra: dev
52
+ Requires-Dist: black>=23.0.0; extra == 'dev'
53
+ Requires-Dist: flake8>=6.0.0; extra == 'dev'
54
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
55
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
56
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
57
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
58
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
59
+ Description-Content-Type: text/markdown
60
+
61
+ # Castrel Bridge Proxy
62
+
63
+ [![CI](https://github.com/castrel-ai/castrel-bridge-proxy/workflows/CI/badge.svg)](https://github.com/castrel-ai/castrel-bridge-proxy/actions)
64
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
65
+ [![Python Version](https://img.shields.io/pypi/pyversions/castrel-proxy)](https://pypi.org/project/castrel-proxy/)
66
+
67
+ A lightweight remote command execution bridge client that connects to a server via WebSocket to receive and execute commands, with MCP (Model Context Protocol) integration.
68
+
69
+ ## ✨ Features
70
+
71
+ - ✅ **Secure Pairing**: Pair with server using verification codes
72
+ - ✅ **Persistent Configuration**: Configuration saved in `~/.castrel/config.yaml`
73
+ - ✅ **Unique Identifier**: Generate stable client ID based on machine characteristics
74
+ - ✅ **WebSocket Connection**: Real-time bidirectional communication
75
+ - ✅ **Command Execution**: Execute shell commands with whitelist security
76
+ - ✅ **Document Operations**: Read, write, and edit files remotely
77
+ - ✅ **Auto Reconnect**: Automatically reconnect when connection is lost
78
+ - ✅ **Timeout Control**: Command execution timeout protection
79
+ - ✅ **MCP Integration**: Connect to local MCP services and sync tools information
80
+
81
+ ## 📦 Installation
82
+
83
+ ### Via pip
84
+
85
+ ```bash
86
+ pip install castrel-proxy
87
+ ```
88
+
89
+ ### From source
90
+
91
+ ```bash
92
+ git clone https://github.com/castrel-ai/castrel-bridge-proxy.git
93
+ cd castrel-bridge-proxy
94
+ pip install -e .
95
+ ```
96
+
97
+ ## 🚀 Quick Start
98
+
99
+ ### 1. Pair with Server
100
+
101
+ ```bash
102
+ castrel-proxy pair <verification_code> <server_url>
103
+ ```
104
+
105
+ Example:
106
+ ```bash
107
+ castrel-proxy pair eyJ0cyI6MTczNTA4ODQwMCwid2lkIjoiZGVmYXVsdCIsInJhbmQiOiIxMjM0NTYifQ https://server.example.com
108
+ ```
109
+
110
+ ### 2. Start Bridge Service
111
+
112
+ ```bash
113
+ # Run in background (default)
114
+ castrel-proxy start
115
+
116
+ # Run in foreground
117
+ castrel-proxy start --foreground
118
+
119
+ # Press Ctrl+C to stop (foreground mode only)
120
+ ```
121
+
122
+ ### 3. Stop Bridge Service
123
+
124
+ ```bash
125
+ # Stop background daemon
126
+ castrel-proxy stop
127
+ ```
128
+
129
+ ### 4. Check Status
130
+
131
+ ```bash
132
+ castrel-proxy status
133
+ ```
134
+
135
+ ### 5. View Logs
136
+
137
+ ```bash
138
+ # View last 50 lines (default)
139
+ castrel-proxy logs
140
+
141
+ # View last 100 lines
142
+ castrel-proxy logs -n 100
143
+
144
+ # Follow logs in real-time
145
+ castrel-proxy logs -f
146
+ ```
147
+
148
+ ## 📖 Documentation
149
+
150
+ - [Installation Guide](docs/installation.md)
151
+ - [Configuration Guide](docs/configuration.md)
152
+ - [Daemon Mode Guide](docs/daemon-mode.md)
153
+ - [MCP Integration](docs/mcp-integration.md)
154
+ - [API Reference](docs/api-reference.md)
155
+ - [Protocol Specification](docs/protocol.md)
156
+ - [Migration Guide](MIGRATION_GUIDE.md)
157
+
158
+ ## 🔧 Configuration
159
+
160
+ ### Bridge Configuration (`~/.castrel/config.yaml`)
161
+
162
+ Pairing information is saved automatically:
163
+
164
+ ```yaml
165
+ server_url: "https://server.example.com"
166
+ verification_code: "ABC123"
167
+ client_id: "a1b2c3d4e5f6"
168
+ workspace_id: "default"
169
+ paired_at: "2025-12-22T10:30:00Z"
170
+ ```
171
+
172
+ ### MCP Configuration (`~/.castrel/mcp.json`)
173
+
174
+ Configure MCP services (optional):
175
+
176
+ ```json
177
+ {
178
+ "mcpServers": {
179
+ "filesystem": {
180
+ "transport": "stdio",
181
+ "command": "npx",
182
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
183
+ "env": {}
184
+ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ See `examples/mcp.json.example` for more examples.
190
+
191
+ ### Command Whitelist (`~/.castrel/whitelist.conf`)
192
+
193
+ Configure allowed commands for security:
194
+
195
+ ```
196
+ # Add commands one per line
197
+ ls
198
+ cat
199
+ git
200
+ python
201
+ # etc.
202
+ ```
203
+
204
+ ## 🏗️ Architecture
205
+
206
+ ```
207
+ ┌─────────────────────────────────────────────────┐
208
+ │ Bridge Client (Local) │
209
+ │ │
210
+ │ ┌──────────────────────────────────────────┐ │
211
+ │ │ CLI Commands │ │
212
+ │ └──────────────────────────────────────────┘ │
213
+ │ │ │
214
+ │ ┌─────────────┼─────────────┐ │
215
+ │ │ │ │ │
216
+ │ ┌───▼────┐ ┌────▼─────┐ ┌───▼─────┐ │
217
+ │ │ Core │ │ MCP │ │ Network │ │
218
+ │ │ Config │ │ Manager │ │ Client │ │
219
+ │ └────────┘ └──────────┘ └──────────┘ │
220
+ │ │ │ │
221
+ │ ┌─────▼─────┐ │ │
222
+ │ │ MCP │ │ │
223
+ │ │ Servers │ │ │
224
+ │ └───────────┘ ┌────▼─────┐ │
225
+ │ │ Command │ │
226
+ │ │ Executor │ │
227
+ │ └──────────┘ │
228
+ └─────────────────────────────────────────────────┘
229
+
230
+ │ WebSocket
231
+
232
+ ┌─────────────────────────────────────────────────┐
233
+ │ Bridge Server (Remote) │
234
+ │ /api/v1/bridge/ws?client_id=xxx&code=yyy │
235
+ └─────────────────────────────────────────────────┘
236
+ ```
237
+
238
+ ## 🛠️ Development
239
+
240
+ ### Setup Development Environment
241
+
242
+ ```bash
243
+ # Clone repository
244
+ git clone https://github.com/castrel-ai/castrel-bridge-proxy.git
245
+ cd castrel-bridge-proxy
246
+
247
+ # Create virtual environment
248
+ python -m venv venv
249
+ source venv/bin/activate # On Windows: venv\Scripts\activate
250
+
251
+ # Install dependencies
252
+ pip install -e ".[dev]"
253
+ ```
254
+
255
+ ### Run Tests
256
+
257
+ ```bash
258
+ # Run all tests
259
+ pytest
260
+
261
+ # Run with coverage
262
+ pytest --cov=castrel_proxy
263
+
264
+ # Run specific test file
265
+ pytest tests/test_core.py
266
+ ```
267
+
268
+ ### Code Quality
269
+
270
+ ```bash
271
+ # Format code
272
+ black src/
273
+
274
+ # Lint code
275
+ flake8 src/
276
+
277
+ # Type checking
278
+ mypy src/
279
+ ```
280
+
281
+ ## 🤝 Contributing
282
+
283
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
284
+
285
+ ## 📝 License
286
+
287
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
288
+
289
+ ## 🔒 Security
290
+
291
+ For security concerns, please see [SECURITY.md](SECURITY.md) or contact security@example.com.
292
+
293
+ ## 📮 Contact
294
+
295
+ - Issues: [GitHub Issues](https://github.com/castrel-ai/castrel-bridge-proxy/issues)
296
+ - Discussions: [GitHub Discussions](https://github.com/castrel-ai/castrel-bridge-proxy/discussions)
297
+
298
+ ## 🙏 Acknowledgments
299
+
300
+ - Built with [Typer](https://typer.tiangolo.com/) for CLI
301
+ - Uses [aiohttp](https://docs.aiohttp.org/) for async WebSocket communication
302
+ - Integrates with [MCP](https://modelcontextprotocol.io/) for tool protocols