Previously compute_id was typed as str, so 'local' would pass MCP validation
and reach the controller API where it crashed. Now uuid.UUID type ensures
Pydantic rejects any non-UUID string before the handler runs.
The end /v3/computes/{compute_id} expects the compute_id to be a
valid UUID. Previously the MCP tool defaulted to the string 'local',
which caused a ValueError in the database layer. Now compute_id is
required and callers must use compute_list first to resolve names to UUIDs.
- Move node file operations (list_files, delete_file) from Gns3Connector to Node class
- Add link capture/reset operations (reset, start_capture, stop_capture) to Link class
- Convert all MCP handlers to use conn.http_call() directly instead of
Gns3Connector/Node/Link abstraction methods
- Register 4 new MCP tools: reset_link, start_capture, stop_capture,
download_capture_file
- Add list_node_files, get_node_file, write_node_file, delete_node_file methods to Gns3Connector
- Add MCP handlers with offset/limit line-based reading for get_node_file
- Auto-truncate files >50KB with truncated flag in response
- Rich metadata returned (total_lines, total_bytes, has_more, etc.)
- Tool docstrings guide AI to check file sizes before reading chunked
Rootful Docker recreates volume mount points as root on start,
preventing the GNS3 process from writing files into node directories
while the container is running. self._fix_permissions() would resolve
this but is currently only called at container stop time.
- _fix_permissions: capture stderr, check returncode, only set
_permissions_fixed on success instead of silently marking as fixed
- list_node_files: wrap os.scandir in try-except to handle
PermissionError gracefully
Create async_iterable_to_stream() in gns3server.utils.asyncio that
converts an async iterable to an aiohttp StreamReader via a background
feeder task. This bypasses aiohttp's AsyncIterablePayload which can
cause 'Connection reset by peer' with certain HTTP servers.
Use it in _run_http_query for the __aiter__ data path.
Both is_safe_path rejection and PermissionError were returning 403
without a detail message, making them indistinguishable in logs.
Add specific detail strings to each:
- is_safe_path: 'Path is outside the project directory'
- PermissionError (write): 'Permission denied writing to ...'
- PermissionError (delete): 'Permission denied deleting ...'
The inner try-except caught OSError/UnicodeEncodeError with 'pass',
silently swallowing all write failures and returning HTTP 204 as if
the file was written successfully.
Remove the nested try-except and let errors propagate properly:
- OSError → 500 with error detail
- PermissionError → 403 (already handled)
- FileNotFoundError → 404 (already handled)
- Stream file GET/POST through controller without buffering in memory
- Add recursive and subdirectory filtering to node file listing
- Replace file extension with magic-based file type detection
- Add DELETE endpoint for node and project files
- Include directories in listing response
- Add params and stream support to http_query
- Fix lambda closures, streamer exception scope, and delete error codes
Only description strings changed to tell AI the content is Markdown
format (.md). The actual handler still reads/writes README.txt for
Web UI compatibility.
Updated the actual MCP tool definitions in __init__.py with detailed
filter parameter descriptions. The previous update to links.py was incorrect
because MCP tools are defined via @mcp.tool() decorators in __init__.py.
Changes:
- Updated update_link tool docstring with comprehensive filter information
- Updated create_link tool filters parameter description
- Added all 5 filter types with proper array format requirements
- Added parameter ranges and usage examples
Filters now properly documented:
- frequency_drop: [N] (N: -1 to 32767)
- packet_loss: [rate] (rate: 0 to 100)
- delay: [ms, jitter] (milliseconds)
- corrupt: [rate] (rate: 0 to 100)
- bpf: [expression] (BPF syntax)
This will prevent TypeError: 'int' object is not iterable errors and
help users understand the correct filter format.
Updated the filters parameter description in create_link and update_link
MCP tools to specify the required array format and provide examples.
Changes:
- Updated create_link filters description with array format requirements
- Updated update_link filters description with array format and example
- Added specific filter types: frequency_drop, packet_loss, delay, corrupt, bpf
This helps users understand the correct format:
- frequency_drop: [N] (drop every Nth packet)
- packet_loss: [rate] (packet loss percentage)
- delay: [ms, jitter] (latency and jitter in milliseconds)
- corrupt: [rate] (packet corruption percentage)
- bpf: [expression] (Berkeley Packet Filter)
Prevents TypeError: 'int' object is not iterable errors.
Extended the kwargs parameter handling fix to nodes and links MCP tools,
which had the same nested kwargs structure issue as templates.
Changes:
- Modified update_node_handler to extract params from nested kwargs
- Modified update_link_handler to extract params from nested kwargs
This ensures that node and link updates through MCP tools work correctly,
allowing proper modification of node and link properties.
Fixed an issue where MCP template tools (update_template, create_template)
were not correctly handling nested kwargs parameter structure from MCP clients.
The problem occurred when MCP clients passed parameters in the format:
{'template_id': 'xxx', 'kwargs': {'adapters': 3}}
The original code was passing the entire kwargs dictionary as a parameter,
instead of extracting the actual update parameters from within it.
Changes:
- Modified update_template_handler to extract params from nested kwargs
- Modified create_template_handler to handle the same issue
This fix ensures that template updates through MCP tools now work correctly,
allowing proper modification of template properties like adapters count.
Replace the global variable + lock + polling mechanism with asyncio.Event
pattern for MCP server ready state tracking.
Benefits:
- Eliminates race conditions (Event.set() is thread-safe)
- Event-driven notification instead of polling (no 50x sleep overhead)
- Fixes test contamination from global state
- Reduces code from 54 lines to 30 lines
- Uses standard asyncio primitive for this pattern
Instead of allowing connections to proceed when server initialization
times out, return HTTP 503 Service Unavailable to prevent MCP
protocol initialization errors.
This prevents the original "Received request before initialization
was complete" errors when GNS3 server startup takes longer than
the 5-second timeout.
Add server ready state tracking for MCP service to prevent
"Received request before initialization was complete" errors
when clients connect before GNS3 server completes startup.
Changes:
- Add MCP server ready state management with wait/notify mechanism
- Modify auth wrapper to wait for server initialization before accepting connections
- Set MCP server ready flag after GNS3 startup completes
This ensures MCP protocol initialization handshake only occurs
after GNS3 server is fully initialized, preventing race
conditions during server startup.
When GNS3 server is configured to listen on 0.0.0.0 (all interfaces),
_server_url() was hardcoding 127.0.0.1, making the WebSocket console
URL unreachable from remote MCP clients.
Use the UDP connect trick (connect to 8.8.8.8:80 without sending data)
to discover the default route interface IP, which is the address remote
clients can actually reach.
The MCP library's TransportSecurityMiddleware only supports exact host
matches or "host:*" port wildcards. It does NOT support a standalone "*"
wildcard to mean "allow all hosts" — setting allowed_hosts=["*"] would
reject every connection because no Host header equals "*".
Worse, when transport_security=None was passed to FastMCP while its default
host is "127.0.0.1", FastMCP would auto-enable protection with strict
localhost-only rules, overriding GNS3's intent to allow all hosts.
Root cause analysis:
- FastMCP auto-enables DNS rebinding protection when host is localhost
and no explicit TransportSecuritySettings is provided
- GNS3 was passing transport_security=None (indirectly via FastMCP's default)
when protection was disabled, triggering the auto-enable
- The TransportSecuritySettings "allowed_hosts" list does NOT support "*"
as a catch-all wildcard
This fix:
1. Always pass an explicit TransportSecuritySettings to FastMCP
- Disabled: TransportSecuritySettings(enable_dns_rebinding_protection=False)
- Enabled: TransportSecuritySettings(enable_dns_rebinding_protection=True, ...)
2. Restore mcp_allowed_hosts and mcp_allowed_origins config fields
3. Set mcp_enable_dns_rebinding_protection default to False (allow all hosts)
Behaviour:
- Default (no config change): all hosts can connect to MCP server
- With mcp_enable_dns_rebinding_protection=true: only configured hosts
- Aligns with GNS3 server's 0.0.0.0 binding policy
Add MCP transport security configuration to gns3_server.conf with permissive
defaults that align with GNS3's design philosophy and VM distribution requirements.
## Changes
### 1. Configuration Schema (gns3server/schemas/config.py)
- Added MCP transport security fields to ServerSettings class:
- mcp_enable_dns_rebinding_protection (bool, default: True)
- mcp_allowed_hosts (list[str], default: ["*"])
- mcp_allowed_origins (list[str], default: ["*"])
- Added field validators to handle comma-separated string input
### 2. MCP Server Initialization (gns3server/api/routes/mcp/__init__.py)
- Import TransportSecuritySettings from mcp.server.transport_security
- Added _create_mcp_server() function to read configuration
- Updated FastMCP instantiation to use configured security settings
### 3. Configuration Sample (gns3server/config_samples/gns3_server.conf)
- Added MCP transport security settings section
- Documented default behavior and security options
- Provided examples for different use cases
## Design Philosophy
**Default: Allow All Hosts** (matches GNS3's 0.0.0.0 binding):
- VM distribution works out-of-the-box
- Users can access from any network location
- Security-conscious users can restrict when needed
**Security: Optional Restriction**:
Users can configure specific hosts for enhanced security:
``ini
mcp_allowed_hosts = 127.0.0.1:*,localhost:*,192.168.1.3:*
mcp_allowed_origins = http://127.0.0.1:*,http://localhost:*,http://192.168.1.3:*
```
## Benefits
- Flexible: Users can configure based on security requirements
- User-friendly: Default matches GNS3's 0.0.0.0 binding philosophy
- Maintainable: No code changes needed for different deployment scenarios
- Secure: DNS rebinding protection remains enabled with configurable hosts
## Related
- Issue #2771
- FastMCP DNS rebinding protection design
- Existing skills configuration in ServerSettings
The feature directory contains network planning and design functionalities
(e.g., topology_planner), not device-specific features. These were not being
loaded because load_device_skills() only scanned the device directory.
Changes:
- Added new load_feature_skills() method in SkillsLoader
- Modified reload_skills() to load both device and feature directories
- Device skills: device-specific configurations (e.g., VPCS)
- Feature skills: network planning functionalities (e.g., topology planner)
- Both are now properly loaded into SKILLS_REGISTRY
This ensures that network planning features like topology_planner are available
via the device_skills tool with proper category classification.
- Replace Args: docstring blocks with Annotated[str, Field(description=...)]
so parameter descriptions appear in inputSchema.properties.*.description
- mcp.server.fastmcp does not parse Args: blocks from docstrings;
only Annotated with pydantic Field injects descriptions into the
structured JSON Schema visible to AI clients via tools/list
- Remove redundant Args: blocks from docstrings (info moved to Field)
- Restore full 4-step websocat workflow in get_node_console_info docstring
with connection, command sending, response receiving, and timeout