Skip to content

Commit 7133a11

Browse files
committed
feat: add custom profile creation capability with comprehensive guidelines
Add nettune.create_profile tool to enable creation of custom optimization profiles. Update documentation with detailed guidelines for profile creation, including risk level selection, sysctl parameter recommendations, buffer size calculations for high-BDP links, and qdisc selection criteria. Implement HTTP and MCP endpoints for profile creation with validation. - Add POST /profiles endpoint for creating custom profiles
1 parent 888625c commit 7133a11

7 files changed

Lines changed: 253 additions & 12 deletions

File tree

README.md

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Or use the NPM wrapper:
6161
| `nettune.snapshot_server` | Create a configuration snapshot for rollback |
6262
| `nettune.list_profiles` | List available optimization profiles |
6363
| `nettune.show_profile` | Show details of a specific profile |
64+
| `nettune.create_profile` | Create a custom optimization profile |
6465
| `nettune.apply_profile` | Apply a profile (dry_run or commit mode) |
6566
| `nettune.rollback` | Rollback to a previous snapshot |
6667
| `nettune.status` | Get current server status and configuration |
@@ -139,6 +140,7 @@ Flags:
139140
### Profile Endpoints
140141

141142
- `GET /profiles` - List profiles
143+
- `POST /profiles` - Create a new profile
142144
- `GET /profiles/:id` - Get profile details
143145

144146
### System Endpoints
@@ -205,23 +207,64 @@ Analyze the baseline results to classify the network situation:
205207
- Diagnosis: Current configuration is performing well
206208
- Recommended action: No changes needed; document current state
207209
208-
### Phase 3: Profile Selection
210+
### Phase 3: Profile Selection or Creation
209211
210212
Based on diagnosis:
211213
212214
1. Call `nettune.list_profiles` to see available profiles
213215
2. Call `nettune.show_profile` for candidate profiles to understand their settings
214-
3. Select the most appropriate profile based on:
215-
- User's stated goal (throughput vs latency vs balanced)
216-
- Diagnosed issue type
217-
- Server's current state
216+
3. Decide whether to use an existing profile or create a custom one:
217+
218+
**Use existing profile when:**
219+
- A built-in profile closely matches the diagnosed issue
220+
- User wants a conservative, well-tested configuration
221+
- The network situation fits a common pattern (Type A or B)
222+
223+
**Create custom profile when:**
224+
- Existing profiles don't address the specific issue
225+
- User has special requirements (e.g., specific buffer sizes, particular qdisc)
226+
- Fine-tuned parameters are needed based on measured BDP
227+
- Combining settings from multiple profiles would be beneficial
218228
219229
Profile selection guidelines:
220230
- For Type A issues: Start with `bbr-fq-tuned-32mb` (increased buffers)
221231
- For Type B issues: Start with `bbr-fq-default` (conservative, with FQ qdisc)
222-
- For high-BDP links (high bandwidth × high RTT): Prefer larger buffer profiles
232+
- For high-BDP links (high bandwidth × high RTT): Prefer larger buffer profiles or create custom with calculated buffer sizes
223233
- For low-latency requirements: Prefer profiles without aggressive buffering
224234
235+
### Creating Custom Profiles
236+
237+
When creating a custom profile with `nettune.create_profile`, follow these guidelines:
238+
239+
**Risk Level Selection:**
240+
- `low`: Only safe, widely-tested settings (e.g., enabling BBR, basic FQ)
241+
- `medium`: Moderate buffer increases, standard optimizations
242+
- `high`: Aggressive tuning, large buffers, experimental settings
243+
244+
**Sysctl Parameter Guidelines:**
245+
246+
| Parameter | Purpose | Conservative | Aggressive |
247+
|-----------|---------|--------------|------------|
248+
| `net.core.rmem_max` | Max receive buffer | 16MB | 64MB+ |
249+
| `net.core.wmem_max` | Max send buffer | 16MB | 64MB+ |
250+
| `net.ipv4.tcp_rmem` | TCP receive buffer (min/default/max) | "4096 131072 16777216" | "4096 524288 67108864" |
251+
| `net.ipv4.tcp_wmem` | TCP send buffer (min/default/max) | "4096 65536 16777216" | "4096 524288 67108864" |
252+
| `net.ipv4.tcp_congestion_control` | Congestion algorithm | bbr | bbr |
253+
| `net.ipv4.tcp_mtu_probing` | MTU discovery | 1 | 1 |
254+
| `net.ipv4.tcp_slow_start_after_idle` | Slow start behavior | 1 (safe) | 0 (better for persistent connections) |
255+
256+
**Buffer Size Calculation (for high-BDP links):**
257+
```
258+
Required buffer = Bandwidth (bytes/sec) × RTT (seconds) × 2
259+
Example: 1 Gbps link with 100ms RTT = 125MB/s × 0.1s × 2 = 25MB
260+
```
261+
262+
**Qdisc Selection:**
263+
- `fq` (Fair Queue): Best for BBR, provides flow isolation
264+
- `fq_codel`: Good for reducing bufferbloat, AQM built-in
265+
- `cake`: Advanced shaping, good for limited bandwidth scenarios
266+
- `pfifo_fast`: Default, minimal processing overhead
267+
225268
### Phase 4: Safe Application
226269
227270
1. **Create Snapshot**: Call `nettune.snapshot_server` BEFORE any changes
@@ -277,6 +320,13 @@ Provide a summary including:
277320
- Compare latency during load vs baseline
278321
- RTT inflation > 2x suggests buffering issues
279322
323+
### nettune.create_profile
324+
- Use when existing profiles don't match the diagnosed issue
325+
- Calculate buffer sizes based on measured bandwidth × RTT × 2
326+
- Start with `risk_level: "medium"` unless you have specific reasons
327+
- Always include a clear description explaining the profile's purpose
328+
- For high-BDP scenarios, set appropriate tcp_rmem/tcp_wmem based on BDP calculation
329+
280330
### nettune.apply_profile
281331
- ALWAYS use dry_run first
282332
- ALWAYS set auto_rollback_seconds for commit
@@ -317,11 +367,12 @@ When recommending changes:
317367
1. Runs baseline tests (status, RTT, throughput, latency-under-load)
318368
2. Analyzes results and classifies the issue
319369
3. Lists and reviews available profiles
320-
4. Creates a snapshot
321-
5. Does a dry-run and explains proposed changes
322-
6. After user approval, commits with auto-rollback
323-
7. Re-runs tests and compares results
324-
8. Provides a comprehensive summary
370+
4. Selects an existing profile OR creates a custom profile based on diagnosis
371+
5. Creates a snapshot
372+
6. Does a dry-run and explains proposed changes
373+
7. After user approval, commits with auto-rollback
374+
8. Re-runs tests and compares results
375+
9. Provides a comprehensive summary
325376

326377
## Troubleshooting
327378

internal/client/http/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,23 @@ func (c *Client) GetProfile(id string) (*types.Profile, error) {
207207
return &result, nil
208208
}
209209

210+
// CreateProfile calls POST /profiles to create a new profile
211+
func (c *Client) CreateProfile(profile *types.Profile) (*types.ProfileMeta, error) {
212+
resp, err := c.doRequest("POST", "/profiles", profile)
213+
if err != nil {
214+
return nil, err
215+
}
216+
if !resp.Success {
217+
return nil, resp.Error
218+
}
219+
220+
var result types.ProfileMeta
221+
if err := json.Unmarshal(resp.Data, &result); err != nil {
222+
return nil, err
223+
}
224+
return &result, nil
225+
}
226+
210227
// CreateSnapshot calls POST /sys/snapshot
211228
func (c *Client) CreateSnapshot() (*types.Snapshot, error) {
212229
resp, err := c.doRequest("POST", "/sys/snapshot", nil)

internal/client/mcp/server.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,50 @@ func (s *Server) registerTools() {
180180
),
181181
s.handleStatus,
182182
)
183+
184+
// Tool: nettune.create_profile
185+
s.mcpServer.AddTool(
186+
mcp.NewTool("nettune.create_profile",
187+
mcp.WithDescription("Create a new configuration profile for network optimization. The profile can then be applied using nettune.apply_profile."),
188+
mcp.WithString("id",
189+
mcp.Required(),
190+
mcp.Description("Unique profile ID (alphanumeric with hyphens, e.g., 'my-custom-profile')"),
191+
),
192+
mcp.WithString("name",
193+
mcp.Required(),
194+
mcp.Description("Human-readable profile name (e.g., 'Low Latency Gaming Profile')"),
195+
),
196+
mcp.WithString("description",
197+
mcp.Description("Detailed description of what this profile does and when to use it"),
198+
),
199+
mcp.WithString("risk_level",
200+
mcp.Required(),
201+
mcp.Description("Risk level of the profile"),
202+
mcp.Enum("low", "medium", "high"),
203+
),
204+
mcp.WithBoolean("requires_reboot",
205+
mcp.Description("Whether applying this profile requires a system reboot (default: false)"),
206+
),
207+
mcp.WithObject("sysctl",
208+
mcp.Description("Sysctl parameters to set. Keys are sysctl paths (e.g., 'net.core.rmem_max'), values are the desired settings."),
209+
),
210+
mcp.WithString("qdisc_type",
211+
mcp.Description("Queue discipline type for traffic control"),
212+
mcp.Enum("fq", "fq_codel", "cake", "pfifo_fast"),
213+
),
214+
mcp.WithString("qdisc_interfaces",
215+
mcp.Description("Which interfaces to apply qdisc to"),
216+
mcp.Enum("default-route", "all"),
217+
),
218+
mcp.WithObject("qdisc_params",
219+
mcp.Description("Additional qdisc parameters (type-specific, e.g., {'flow_limit': '10000'} for fq)"),
220+
),
221+
mcp.WithBoolean("systemd_ensure_qdisc_service",
222+
mcp.Description("Whether to create a systemd service to persist qdisc settings across reboots (default: false)"),
223+
),
224+
),
225+
s.handleCreateProfile,
226+
)
183227
}
184228

185229
// Tool handlers
@@ -338,6 +382,79 @@ func (s *Server) handleStatus(ctx context.Context, request mcp.CallToolRequest)
338382
})), nil
339383
}
340384

385+
func (s *Server) handleCreateProfile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
386+
args := parseArgs(request.Params.Arguments)
387+
388+
// Required fields
389+
id := getStringArg(args, "id", "")
390+
name := getStringArg(args, "name", "")
391+
riskLevel := getStringArg(args, "risk_level", "")
392+
393+
if id == "" {
394+
return mcp.NewToolResultError("Error: id is required"), nil
395+
}
396+
if name == "" {
397+
return mcp.NewToolResultError("Error: name is required"), nil
398+
}
399+
if riskLevel == "" {
400+
return mcp.NewToolResultError("Error: risk_level is required"), nil
401+
}
402+
403+
// Optional fields
404+
description := getStringArg(args, "description", "")
405+
requiresReboot := getBoolArg(args, "requires_reboot", false)
406+
407+
// Build profile
408+
profile := &types.Profile{
409+
ID: id,
410+
Name: name,
411+
Description: description,
412+
RiskLevel: riskLevel,
413+
RequiresReboot: requiresReboot,
414+
}
415+
416+
// Parse sysctl (map[string]interface{})
417+
if sysctl := getMapArg(args, "sysctl"); sysctl != nil {
418+
profile.Sysctl = sysctl
419+
}
420+
421+
// Parse qdisc config
422+
qdiscType := getStringArg(args, "qdisc_type", "")
423+
qdiscInterfaces := getStringArg(args, "qdisc_interfaces", "")
424+
if qdiscType != "" {
425+
profile.Qdisc = &types.QdiscConfig{
426+
Type: qdiscType,
427+
Interfaces: qdiscInterfaces,
428+
}
429+
if qdiscInterfaces == "" {
430+
profile.Qdisc.Interfaces = "default-route" // default
431+
}
432+
if qdiscParams := getMapArg(args, "qdisc_params"); qdiscParams != nil {
433+
profile.Qdisc.Params = qdiscParams
434+
}
435+
}
436+
437+
// Parse systemd config
438+
ensureQdiscService := getBoolArg(args, "systemd_ensure_qdisc_service", false)
439+
if ensureQdiscService {
440+
profile.Systemd = &types.SystemdConfig{
441+
EnsureQdiscService: true,
442+
}
443+
}
444+
445+
// Create profile via HTTP client
446+
result, err := s.client.CreateProfile(profile)
447+
if err != nil {
448+
return mcp.NewToolResultError(fmt.Sprintf("Error creating profile: %v", err)), nil
449+
}
450+
451+
return mcp.NewToolResultText(toJSON(map[string]interface{}{
452+
"success": true,
453+
"message": fmt.Sprintf("Profile '%s' created successfully", id),
454+
"profile": result,
455+
})), nil
456+
}
457+
341458
// Helper functions for argument parsing
342459

343460
// parseArgs converts the any type arguments to map[string]interface{}
@@ -404,6 +521,15 @@ func getBoolArg(args map[string]interface{}, key string, defaultVal bool) bool {
404521
return defaultVal
405522
}
406523

524+
func getMapArg(args map[string]interface{}, key string) map[string]interface{} {
525+
if v, ok := args[key]; ok {
526+
if m, ok := v.(map[string]interface{}); ok {
527+
return m
528+
}
529+
}
530+
return nil
531+
}
532+
407533
func toJSON(v interface{}) string {
408534
data, err := json.MarshalIndent(v, "", " ")
409535
if err != nil {

internal/server/api/handlers/profile.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,51 @@ func (h *ProfileHandler) Get(c *gin.Context) {
5454
success(c, profile)
5555
}
5656

57+
// CreateProfileRequest represents a request to create a new profile
58+
type CreateProfileRequest struct {
59+
ID string `json:"id" binding:"required"`
60+
Name string `json:"name" binding:"required"`
61+
Description string `json:"description,omitempty"`
62+
RiskLevel string `json:"risk_level" binding:"required,oneof=low medium high"`
63+
RequiresReboot bool `json:"requires_reboot,omitempty"`
64+
Sysctl map[string]interface{} `json:"sysctl,omitempty"`
65+
Qdisc *types.QdiscConfig `json:"qdisc,omitempty"`
66+
Systemd *types.SystemdConfig `json:"systemd,omitempty"`
67+
}
68+
69+
// Create handles POST /profiles
70+
func (h *ProfileHandler) Create(c *gin.Context) {
71+
var req CreateProfileRequest
72+
if err := c.ShouldBindJSON(&req); err != nil {
73+
badRequest(c, err.Error())
74+
return
75+
}
76+
77+
// Convert request to Profile
78+
profile := &types.Profile{
79+
ID: req.ID,
80+
Name: req.Name,
81+
Description: req.Description,
82+
RiskLevel: req.RiskLevel,
83+
RequiresReboot: req.RequiresReboot,
84+
Sysctl: req.Sysctl,
85+
Qdisc: req.Qdisc,
86+
Systemd: req.Systemd,
87+
}
88+
89+
// Save profile (validation happens inside Save)
90+
if err := h.profileService.Save(profile); err != nil {
91+
if errors.Is(err, types.ErrValidationFailed) {
92+
badRequest(c, err.Error())
93+
return
94+
}
95+
internalError(c, err.Error())
96+
return
97+
}
98+
99+
success(c, profile.ToMeta())
100+
}
101+
57102
func notFound(c *gin.Context, message string) {
58103
c.JSON(404, gin.H{"success": false, "error": gin.H{"code": "NOT_FOUND", "message": message}})
59104
}

internal/server/api/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func (s *Server) setupRouter() {
118118
profiles := authorized.Group("/profiles")
119119
{
120120
profiles.GET("", profileHandler.List)
121+
profiles.POST("", profileHandler.Create)
121122
profiles.GET("/:id", profileHandler.Get)
122123
}
123124

js/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Once connected, the following tools are available:
109109
| `nettune.snapshot_server` | Create a server state snapshot |
110110
| `nettune.list_profiles` | List available optimization profiles |
111111
| `nettune.show_profile` | Show details of a specific profile |
112+
| `nettune.create_profile` | Create a custom optimization profile |
112113
| `nettune.apply_profile` | Apply an optimization profile |
113114
| `nettune.rollback` | Rollback to a previous snapshot |
114115
| `nettune.status` | Get current server status |

js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jtsang/nettune-mcp",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"private": false,
55
"description": "MCP stdio wrapper for nettune - TCP network optimization tool",
66
"type": "module",

0 commit comments

Comments
 (0)