Skip to content

Commit 79e97e5

Browse files
committed
refactor: Make prefix_sep configurable instead of class constant
- Change PREFIX_SEP from ClassVar to prefix_sep field on MaggConfig - Add MAGG_PREFIX_SEP env var support (default: "_") - Update ServerManager to use config.prefix_sep - Remove unused generate_prefix_from_name() method - Update docs to explain configurable separator - Add tests for prefix_sep configuration Allows users to customize the separator between prefix and tool name.
1 parent 1c98a90 commit 79e97e5

File tree

8 files changed

+177
-49
lines changed

8 files changed

+177
-49
lines changed

docs/index.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,86 @@ Check logs for:
842842
- Tool delegation errors
843843
- Configuration problems
844844

845+
### Tool Naming and Prefix System
846+
847+
#### Understanding Tool Names
848+
849+
Magg organizes tools from multiple servers using a prefix system. Every tool name follows this pattern:
850+
851+
```
852+
{prefix}{separator}{tool_name}
853+
```
854+
855+
Where:
856+
- `prefix` is the namespace for a server (configurable per server)
857+
- `separator` is configurable via `prefix_sep` field or `MAGG_PREFIX_SEP` env var (default: `_`)
858+
- `tool_name` is the original tool name from the server
859+
860+
Examples:
861+
- `calc_add` - "add" tool from server with "calc" prefix
862+
- `pw_screenshot` - "screenshot" tool from server with "pw" prefix
863+
- `magg_list_servers` - "list_servers" tool from Magg itself
864+
- `read_file` - "read_file" tool from server with no prefix
865+
866+
#### Configuring Server Prefixes
867+
868+
When adding a server, specify the `prefix` field:
869+
870+
```json
871+
{
872+
"name": "calculator",
873+
"source": "...",
874+
"prefix": "calc" // All tools will be calc_*
875+
}
876+
```
877+
878+
Special cases:
879+
- `"prefix": ""` or `"prefix": null` - No prefix, tools keep original names
880+
- Omitting prefix field - Server name is used as default prefix
881+
- Multiple servers can share the same prefix (tools are merged)
882+
883+
#### Configuring Magg's Prefix
884+
885+
Magg's own tools use the prefix from `MAGG_SELF_PREFIX`:
886+
887+
```bash
888+
# Default: magg_list_servers, magg_add_server, etc.
889+
export MAGG_SELF_PREFIX=magg
890+
891+
# Custom: myapp_list_servers, myapp_add_server, etc.
892+
export MAGG_SELF_PREFIX=myapp
893+
894+
# No prefix: list_servers, add_server, etc.
895+
export MAGG_SELF_PREFIX=""
896+
```
897+
898+
#### Configuring the Separator
899+
900+
You can change the separator between prefix and tool name:
901+
902+
```bash
903+
# Default underscore separator: calc_add, magg_list_servers
904+
export MAGG_PREFIX_SEP="_"
905+
906+
# Dash separator: calc-add, magg-list-servers
907+
export MAGG_PREFIX_SEP="-"
908+
909+
# Dot separator: calc.add, magg.list.servers
910+
export MAGG_PREFIX_SEP="."
911+
912+
# No separator: calcadd, magglistservers
913+
export MAGG_PREFIX_SEP=""
914+
```
915+
916+
#### Prefix Validation Rules
917+
918+
- Must be valid Python identifiers (alphanumeric, not starting with digit)
919+
- Cannot contain the separator character (default: underscore, but configurable)
920+
- Case-sensitive
921+
- Empty string allowed (results in no prefix)
922+
923+
Note: While the separator is configurable, server prefixes currently still cannot contain underscores due to Python identifier constraints.
924+
845925
### Performance Optimization
846926

847927
1. **Disable unused servers** to reduce memory usage

magg/server/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def save_config(self, config: MaggConfig):
6060
@cached_property
6161
def prefix_separator(self) -> str:
6262
"""Get the prefix separator for this Magg server - cannot be changed during process lifetime."""
63-
return ServerConfig.PREFIX_SEP
63+
return self.config.prefix_sep
6464

6565
@cached_property
6666
def self_prefix(self) -> str:

magg/settings.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import os
55
from functools import cached_property
66
from pathlib import Path
7-
from typing import Any, ClassVar, Callable, Coroutine, TYPE_CHECKING
7+
from typing import Any, Callable, Coroutine, TYPE_CHECKING
88

99
if TYPE_CHECKING:
10-
from .reload import ConfigReloader, ConfigChange
10+
from .reload import ConfigChange
1111

1212
from pydantic import field_validator, Field, model_validator, AnyUrl
1313
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -114,13 +114,11 @@ class ServerConfig(BaseSettings):
114114
arbitrary_types_allowed=True,
115115
)
116116

117-
PREFIX_SEP: ClassVar[str] = "_"
118-
119117
name: str = Field(..., description="Unique server name - can contain any characters")
120118
source: str = Field(..., description="URL/URI/path of the server package, repository, or listing")
121119
prefix: str | None = Field(
122120
default=None,
123-
description=f"Tool prefix for this server - must be a valid Python identifier without {PREFIX_SEP!r}."
121+
description="Tool prefix for this server - must be a valid Python identifier without underscores."
124122
)
125123
notes: str | None = Field(None, description="Setup notes for LLM and humans")
126124

@@ -147,9 +145,9 @@ def validate_prefix(cls, v: str | None) -> str | None:
147145
raise ValueError(
148146
f"Server prefix {v!r} must be a valid Python identifier (letters and numbers only, not starting with a number)"
149147
)
150-
if cls.PREFIX_SEP in v:
148+
if "_" in v:
151149
raise ValueError(
152-
f"Server prefix {v!r} must be a valid Python identifier and cannot contain {cls.PREFIX_SEP!r}"
150+
f"Server prefix {v!r} must be a valid Python identifier and cannot contain underscores"
153151
)
154152
return v
155153

@@ -165,31 +163,6 @@ def validate_uri(cls, v: str | None) -> str | None:
165163
AnyUrl(v) # Validate as URL
166164
return v
167165

168-
@classmethod
169-
def generate_prefix_from_name(cls, name: str) -> str:
170-
"""Generate a valid prefix from a server name.
171-
172-
Removes special characters and ensures it's a valid Python identifier
173-
without underscores.
174-
"""
175-
prefix = (
176-
name.replace('-', '')
177-
.replace('_', '')
178-
.replace('.', '')
179-
.replace(' ', '')
180-
.replace(cls.PREFIX_SEP, '')
181-
)
182-
183-
prefix = ''.join(c for c in prefix if c.isalnum())
184-
185-
if prefix and prefix[0].isdigit():
186-
prefix = 'srv' + prefix
187-
188-
if not prefix or not prefix.isidentifier():
189-
prefix = 'server'
190-
191-
return prefix.lower()[:30]
192-
193166

194167
class MaggConfig(BaseSettings):
195168
"""Main Magg configuration."""
@@ -214,6 +187,10 @@ class MaggConfig(BaseSettings):
214187
default="magg",
215188
description="Prefix for Magg tools and commands - must be a valid Python identifier without underscores (env: MAGG_SELF_PREFIX)"
216189
)
190+
prefix_sep: str = Field(
191+
default="_",
192+
description="Separator between prefix and tool name (env: MAGG_PREFIX_SEP)"
193+
)
217194
auto_reload: bool = Field(default=True, description="Enable automatic config reloading on file changes (env: MAGG_AUTO_RELOAD)")
218195
reload_poll_interval: float = Field(default=1.0, description="Config file poll interval in seconds (env: MAGG_RELOAD_POLL_INTERVAL)")
219196
reload_use_watchdog: bool | None = Field(default=None, description="Use file system notifications if available, None=auto-detect (env: MAGG_RELOAD_USE_WATCHDOG)")

magg/test/test_config.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,13 @@ def test_server_prefix_validation(self):
6666
ServerConfig(name="test", source="test", prefix="validprefix")
6767

6868
# Invalid prefixes - no underscores allowed
69-
with pytest.raises(ValueError, match="cannot contain '_'"):
69+
with pytest.raises(ValueError, match="cannot contain underscores"):
7070
ServerConfig(name="test", source="test", prefix="invalid_prefix")
7171

7272
# Invalid prefixes - must be identifier
7373
with pytest.raises(ValueError, match="must be a valid Python identifier"):
7474
ServerConfig(name="test", source="test", prefix="123invalid")
7575

76-
def test_prefix_generation(self):
77-
"""Test the generate_prefix_from_name helper method."""
78-
# Test various name patterns
79-
assert ServerConfig.generate_prefix_from_name("simple") == "simple"
80-
assert ServerConfig.generate_prefix_from_name("weather-mcp") == "weathermcp"
81-
assert ServerConfig.generate_prefix_from_name("test_server") == "testserver"
82-
assert ServerConfig.generate_prefix_from_name("my.package.name") == "mypackagename"
83-
assert ServerConfig.generate_prefix_from_name("123-start") == "srv123start"
84-
assert ServerConfig.generate_prefix_from_name("@namespace/package") == "namespacepackage"
85-
assert ServerConfig.generate_prefix_from_name("UPPERCASE") == "uppercase"
86-
assert ServerConfig.generate_prefix_from_name("") == "server"
87-
assert ServerConfig.generate_prefix_from_name("!!!") == "server"
8876

8977

9078
class TestMaggConfig:

magg/test/test_prefix_handling.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Test prefix and separator handling."""
2+
3+
import pytest
4+
import os
5+
from magg.settings import MaggConfig, ServerConfig
6+
from magg.server.server import MaggServer
7+
from fastmcp import Client
8+
9+
10+
class TestPrefixHandling:
11+
"""Test custom prefix and separator handling."""
12+
13+
def test_prefix_separator_on_magg_config(self):
14+
"""Test that prefix_sep is a configurable field on MaggConfig."""
15+
config = MaggConfig()
16+
assert hasattr(config, 'prefix_sep')
17+
assert config.prefix_sep == "_" # Default value
18+
19+
# Test it can be configured
20+
config2 = MaggConfig(prefix_sep="-")
21+
assert config2.prefix_sep == "-"
22+
23+
def test_server_config_uses_separator(self):
24+
"""Test that ServerConfig validation uses the separator."""
25+
# Valid prefix
26+
server = ServerConfig(name="test", source="test", prefix="myprefix")
27+
assert server.prefix == "myprefix"
28+
29+
# Invalid prefix with underscore
30+
with pytest.raises(ValueError, match="cannot contain underscores"):
31+
ServerConfig(name="test", source="test", prefix="my_prefix")
32+
33+
@pytest.mark.asyncio
34+
async def test_custom_self_prefix(self, tmp_path, monkeypatch):
35+
"""Test that custom self_prefix is used correctly."""
36+
# Set custom prefix via environment
37+
monkeypatch.setenv("MAGG_SELF_PREFIX", "myapp")
38+
39+
config_path = tmp_path / "config.json"
40+
server = MaggServer(config_path, enable_config_reload=False)
41+
42+
# Check configuration
43+
assert server.self_prefix == "myapp"
44+
assert server.self_prefix_ == "myapp_"
45+
46+
await server.setup()
47+
48+
# Verify tools have the correct prefix
49+
async with Client(server.mcp) as client:
50+
tools = await client.list_tools()
51+
tool_names = [tool.name for tool in tools]
52+
53+
# All Magg tools should have myapp_ prefix
54+
myapp_tools = [t for t in tool_names if t.startswith("myapp_")]
55+
assert len(myapp_tools) > 0
56+
assert "myapp_list_servers" in tool_names
57+
assert "myapp_add_server" in tool_names
58+
assert "myapp_status" in tool_names
59+
60+
# Should not have any magg_ tools
61+
magg_tools = [t for t in tool_names if t.startswith("magg_")]
62+
assert len(magg_tools) == 0
63+
64+
def test_prefix_separator_consistency(self):
65+
"""Test that prefix separator is used consistently."""
66+
# Create a server with default Magg config
67+
config = MaggConfig()
68+
assert config.self_prefix == "magg"
69+
assert config.prefix_sep == "_"
70+
71+
# Server config validation should use the separator
72+
server1 = ServerConfig(name="test1", source="test", prefix="myprefix")
73+
assert server1.prefix == "myprefix"
74+
75+
# Empty prefix is allowed
76+
server2 = ServerConfig(name="test2", source="test", prefix="")
77+
assert server2.prefix == ""
78+
79+
# None prefix is allowed
80+
server3 = ServerConfig(name="test3", source="test", prefix=None)
81+
assert server3.prefix is None

magg/test/test_tool_delegation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ def test_tool2(value: int) -> int:
145145
assert "test_test_tool2" in tool_names
146146

147147
# Verify we have tools from both servers
148-
magg_tools = [t for t in tool_names if t.startswith("magg_")]
148+
server_prefix = server.self_prefix_
149+
magg_tools = [t for t in tool_names if t.startswith(server_prefix)]
149150
test_tools = [t for t in tool_names if t.startswith("test_")]
150151

151152
assert len(magg_tools) >= 2 # At least magg_list_servers and magg_add_server

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ Magg supports several environment variables for configuration:
281281
- `MAGG_RELOAD_POLL_INTERVAL` - Config polling interval in seconds (default: 1.0)
282282
- `MAGG_READ_ONLY=true` - Run in read-only mode
283283
- `MAGG_QUIET=true` - Suppress output unless errors occur
284-
- `MAGG_SELF_PREFIX` - Prefix for Magg tools (default: "magg")
284+
- `MAGG_SELF_PREFIX` - Prefix for Magg tools (default: "magg"). Tools will be named as `{prefix}{sep}{tool}` (e.g., `magg_list_servers`)
285+
- `MAGG_PREFIX_SEP` - Separator between prefix and tool name (default: "_")
285286

286287
Example configuration:
287288
```json

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)