Spaces:
Running
Running
File size: 5,760 Bytes
5ef3828 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | # Contributing to Universal MCP Hub
Thanks for considering a contribution. Read this before opening a PR.
---
## Before You Start
Open an issue first for anything non-trivial. Describe what you want to change and why. This avoids situations where you spend time on a PR that conflicts with the architecture or is out of scope.
For typos, doc fixes, or obvious bugs β just open the PR directly.
---
## The Non-Negotiables
These rules are not up for debate in PRs. If you disagree with them, open an issue and make the case.
**1. Don't break the sandbox.**
`app/*` must never import from `fundaments/`, read `os.environ`, or access `.env` directly. The Guardian pattern (`main.py` injects services, `app/*` receives them) is the core security guarantee of this project. PRs that route around it will be closed.
**2. No secrets in code or config.**
`app/.pyfun` references ENV variable names only β never values. If your PR puts an API key, token, or credential anywhere in the codebase, it gets closed immediately.
**3. `db_query` stays SELECT-only.**
The read-only enforcement in `db_sync.py` is not a suggestion. Don't submit PRs that add write access through the MCP tool layer.
**4. New providers need a dummy too.**
If you add a new LLM or search provider, add a commented-out dummy class following the existing pattern. Future contributors should be able to add the next provider by copying your dummy.
---
## How to Add a New LLM Provider
Three files, nothing else:
**1. `app/providers.py`** β add your class (copy the OpenAI dummy as starting point):
```python
class YourProvider(BaseProvider):
"""Your Provider API β brief description."""
async def complete(self, prompt: str, model: str = None, max_tokens: int = 1024) -> str:
data = await self._post(
f"{self.base_url}/chat/completions", # adjust endpoint
headers={
"Authorization": f"Bearer {self.key}",
"content-type": "application/json",
},
payload={
"model": model or self.model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}],
},
)
return data["choices"][0]["message"]["content"] # adjust response parsing
```
**2. `app/providers.py`** β register in `_PROVIDER_CLASSES`:
```python
_PROVIDER_CLASSES = {
...
"yourprovider": YourProvider,
}
```
**3. `app/.pyfun`** β add provider block:
```ini
[LLM_PROVIDER.yourprovider]
active = "true"
base_url = "https://api.yourprovider.com/v1"
env_key = "YOUR_PROVIDER_API_KEY"
default_model = "your-default-model"
models = "your-default-model, other-model"
fallback_to = ""
[LLM_PROVIDER.yourprovider_END]
```
Then add a commented dummy of your class above the `_PROVIDER_CLASSES` dict with the `.pyfun` block as a comment β same style as OpenAI/Mistral/xAI dummies already there.
Most OpenAI-compatible APIs (Mistral, xAI, Together, etc.) need zero changes to the class body β just different `base_url` and `env_key`. Providers with non-standard auth (like Gemini's `?key=` param) need their own `complete()` implementation.
---
## How to Add a New Search Provider
Search providers follow the same pattern as LLM providers but register in `_SEARCH_REGISTRY` (not yet implemented β first search provider PR sets the pattern). Look at how `BraveProvider` and `TavilyProvider` are planned in `.pyfun` for reference.
---
## How to Add a New Tool
Tools are defined in `app/.pyfun` under `[TOOLS]` and registered in `app/mcp.py`. A tool needs:
- A `[TOOL.name]` block in `.pyfun` with `provider_type`, `default_provider`, `system_prompt` (for LLM tools)
- Registration in `mcp.py` via the appropriate `_register_*_tools()` function
- Execution logic in `app/tools.py`
Tools that require an API key must gate their registration β if the key isn't set, the tool must not register. No key, no tool, no crash. See existing tool registration in `mcp.py` for the pattern.
---
## Code Style
- Match the existing style β same header format, same section separators, same logging pattern
- One logger per module: `logger = logging.getLogger("module_name")`
- Async where the code is already async (`app/*`), sync where it's sync (`fundaments/*`)
- Comments explain *why*, not *what* β the code shows what
---
## Testing Your Changes
Before submitting:
```bash
# install dependencies
pip install -r requirements.txt
# start with minimal config (no keys needed for basic startup test)
python main.py
# check that startup completes without errors
# check that list_active_tools returns expected results
# check that missing keys don't crash the server
```
If you're adding a provider: test with a real key in a local `.env`. Document the response format you're parsing from in your PR description.
---
## PR Checklist
- [ ] Sandbox rules not broken (`app/*` has no new `os.environ` or `fundaments/` imports)
- [ ] No credentials or secrets in any file
- [ ] New provider includes dummy class + `.pyfun` block
- [ ] Startup still works with zero API keys configured
- [ ] Code follows existing style and header format
- [ ] PR description explains what changed and why
---
## What Won't Be Merged
- Features that add attack surface without clear security justification
- AI-generated code the author can't explain in review
- PRs that touch `fundaments/` without a very good reason β that layer is stable by design
- Dependency additions without justification β the stack is intentionally lean
---
*Questions? Open an issue. The architecture docs in `README.md`, `SECURITY.md`, and `docs/` cover most of the "why" behind the design decisions.*
|