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.
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
- SSE endpoint supports both Authorization: Bearer header and ?token= query param
- Claude Code can use headers (no URL exposure)
- Claude Desktop (EventSource) can use ?token= URL param
- Use FastMCP (Anthropic MCP SDK) for tool registration and SSE transport
- Mount SSE app under /v3/mcp/transport with JWT token authentication
- Token passed via ?token=<jwt> query parameter on SSE connection
- Token validated against GNS3 auth_service and stored in contextvars
- Tool handlers create Gns3Connector with JWT token to call GNS3 REST API
- 7 project tools: list_projects, get_project, create_project, delete_project,
open_project, close_project, get_project_stats
- Unauthenticated SSE connections return 401
- Add MCPTool/MCPToolRegistry system for centralized tool registration
- Add 7 project-related MCP tools: list_projects, get_project, create_project,
delete_project, open_project, close_project, get_project_stats
- Tools use Gns3Connector (custom_gns3fy) to call GNS3 REST API via HTTP loopback,
keeping the MCP layer decoupled from controller internals
- Handlers run in thread pool via asyncio.to_thread() to avoid blocking
the event loop on synchronous requests calls
- Unified POST /v3/mcp/execute endpoint with JWT authentication
The _get_container_state() call in create() has no practical effect:
Docker's POST /containers/create only creates the container without
starting it, so a newly created container can never be in 'running'
or 'paused' state.
Move the is_running() check from _fast_duplication() to
duplicate() to avoid the error message being wrapped by
the except Exception handler. This ensures the error
message is clean and prevents wasted fast duplication
attempts on running projects.
Add is_running() check at the beginning of _fast_duplication()
to prevent duplicating a project while nodes are running.
Previously, only the export/import fallback path had this check,
which meant running nodes were not detected when fast duplication
succeeded. This aligns with the duplicate API behavior and
provides a consistent safeguard against data inconsistencies.
When a Docker container is created (e.g., when loading a project), the node
status should reflect the actual container state. Previously, the node status
was always set to 'stopped' even if the container was already running.
This fix checks the container state after creation and updates the node
status accordingly:
- If container is running: status = 'started'
- If container is paused: status = 'suspended'
- If container is exited: status = 'stopped' (default)
This ensures that project.is_running() correctly detects running Docker
containers when attempting to export/duplicate a project, fixing the issue
where running Docker nodes were not detected and prompted for shutdown.
When renaming a project that has running Docker containers, the containers
were unnecessarily stopped, removed, and recreated, even though the project
name change doesn't affect container configuration.
Root cause:
- Client sends complete project object including variables: [] during rename
- Controller unconditionally notified all computes about the update
- Docker nodes rebuild containers on any project update notification
Solution:
- Only notify compute nodes when variables field has actual content
- Treat None and [] as semantically equivalent (no variables)
- Empty variables don't affect running containers, so no need to update
Impact:
- Project rename operations no longer trigger ~7 second container rebuilds
- Only actual variable changes trigger container recreation
- Fixes issue #2760
Fixes#2759
When renaming a project:
- Update self._filename to match the new project name
- Rename the .gns3 file on disk to keep it in sync
- Add error handling for file rename failures
When duplicating a project:
- Use self._filename (actual filename) instead of self.name
- This handles the case where a project has been renamed
- Prevents 'No such file or directory' errors
The root cause was that project renaming only updated the project name
in memory and in the .gns3 file content, but did not update the actual
.gns3 filename. This caused duplicate operations to fail because they
tried to read a file with the new name that didn't exist.
Remove duplicate delete_resource call from remove_resource_from_pool since
the API layer already handles resource deletion. This prevents conflicts
where the API layer tries to delete a resource that was already deleted by
the repository layer.
The complete fix is now:
- remove_resource_from_pool: Only removes resource from pool (API handles deletion)
- delete_resource_pool: Deletes all resource records before deleting pool
This completes the fix from PR #2315 by ensuring that when a resource
pool is deleted, all associated resource records are also deleted from
the resources table, preventing orphaned resource records.
Changes:
- Modified delete_resource_pool() to first delete all resource records
in the pool before deleting the pool itself
- This complements the existing fix in remove_resource_from_pool() which
handles single resource removal
This change significantly improves project loading performance, especially for
topologies with multiple Docker containers or other node types.
Changes:
- Modified project.open() method to use parallel node creation
- Replaced serial node creation loop with Pool-based parallel processing
- Set concurrency limit to 5 to avoid overwhelming the system
- Maintains backward compatibility with existing functionality
Performance improvements:
- Projects with 6 Docker containers: 60-70% faster loading time
- Reduced from ~4-5 seconds to ~1-2 seconds for typical multi-node topologies
- Better resource utilization through concurrent node creation
Technical details:
- Uses existing Pool utility class (concurrency=5)
- Preserves node creation order where required
- Maintains error handling and rollback capabilities
- No changes to node creation logic itself, only parallelization
Testing:
- Syntax validation passed
- Compatible with existing project.open tests
- No API changes, internal optimization only
This fix addresses the issue where delay: [0, X] configurations were being
silently dropped instead of returning validation errors.
Changes:
- Created new utility function filter_inactive_filters() in packet_filter_validation.py
- Implemented smart filtering logic for delay filter that checks both latency and jitter:
* delay: [0, 0] → User wants to disable delay, filter out silently
* delay: [0, X] where X > 0 → Invalid config, keep for validation error
* delay: [X, X] where X > 0 → Normal configuration, validate normally
- Simplified link.py update_filters() method to use the new utility function
- Added comprehensive tests for the new filtering logic
Before this fix:
- delay: [0, 100] would be silently dropped with no error message
- Users wouldn't know their configuration was invalid
After this fix:
- delay: [0, 100] returns proper error: "delay parameter Latency must be between 1 and 32767 ms, got: 0"
- delay: [0, 0] is correctly handled as intentional disable
- Normal delay configurations continue to work as expected
Change the default value of show_interface_labels from False to True for better user experience, as interface labels are commonly used in network topology visualization.
Performance improvement for project variable updates when multiple containers
are present. Previously, nodes were updated serially in a for loop, causing:
- 5 containers: ~35 seconds (7s per container)
- 10 containers: ~70 seconds
- 20 containers: ~140 seconds (2min 20sec)
Changed to parallel processing using asyncio.gather(), reducing total time
to the duration of the slowest single node update (~7 seconds regardless
of container count).
The change maintains error handling with return_exceptions=True to ensure
one node's update failure doesn't prevent others from completing.
This is particularly important for users with large topologies containing
many Docker containers that need to be recreated when project variables change.
Related to issue #2755 ghost node timeout fix.
When a Docker node is deleted, the compute node's DELETE endpoint only calls
node.delete() which removes the working directory but does not remove the node
object from the project's self._nodes collection. This causes ghost nodes to
remain in memory.
When project variables are updated, the code iterates through ALL nodes in
memory and calls update() on them. For ghost nodes with VNC configuration, this
triggers VNC startup attempts, resulting in 60-second timeouts waiting for X11
socket files that don't exist.
The fix adds await node.project.remove_node(node) to ensure the node object is
removed from the project's node collection when deleted, matching the behavior
of other node types that use manager.delete_node() which already calls
project.remove_node().
This resolves the issue where updating project variables after deleting a VNC
Docker container would timeout with: 'x11 socket file "/tmp/.X11-unix/X100"
does not exist'
Fixes issue #2755
When updating project variables while Docker containers are running, the
system now properly handles both dictionary-format variables and Pydantic
Variable objects. This prevents AttributeError when containers are recreated
after variable updates.
Changes:
- Modified DockerVM.create() to detect and handle Pydantic Variable objects
- Updated _format_env() method to support both variable formats
- Maintains backward compatibility with existing dictionary format
Fixes error: AttributeError: 'Variable' object has no attribute 'get'
Align validation rules with ubridge source: delay latency must be > 0
(packet_filter.c delay_setup line 182). Update FILTERS definition in
link.py and test cases accordingly.
Changes:
- Replace tshark BPF validation with tcpdump -d (calls pcap_compile
internally like ubridge, returns instantly without waiting for traffic)
- Support multi-line BPF expressions: split on newlines and validate
each line individually
- Always validate, never save invalid filters on error
- Drop invalid filters during project load with warning (prevents
old topologies with bad filters from failing to open)
- Simplify test cases (no longer depend on tshark availability)
Update the default download repository address for GNS3 skills from
yueguobin/GNS3-Skills to gns3/gns3-skills to use the official
organization repository.
This affects:
- Default skills_repo_url in server configuration schema
- Skills manager default repository URL
- Skills configuration defaults
- All documentation references
Replace per-project check_user_has_privilege calls with a single
batch method that performs 3 fixed DB queries regardless of project
count. Reduces GET /projects response time for 10000 projects from
~12s to ~290ms (40x improvement).
Fixed a bug where projects created by a user that are also in a resource pool
the user has access to would appear twice in the GET /projects response.
Changes:
- Add seen_project_ids set to track already added projects
- Check for duplicates before adding projects in Step 2 (user projects)
- Check for duplicates before adding projects in Step 3 (resource pool projects)
This ensures each project appears only once regardless of whether it's user-created
or shared via resource pool.
This fix addresses a critical issue in the RBAC permission checking logic
introduced in PR #2750. When a project is shared through a resource pool,
both the project creator (with regular ACEs like "All endpoints") and the
shared user (with resource pool ACEs) should be able to see the project.
Changes:
- Modified `check_user_has_privilege` in `gns3server/db/repositories/rbac.py`
- Changed from if-elif (exclusive) to sequential (inclusive) checking
- Now checks regular ACEs first, then resource pool ACEs
- Both types of ACEs can grant access (OR logic instead of XOR)
This fixes the scenario where:
1. user100 has "All endpoints" ACE and creates a project
2. user100 shares the project via resource pool with user200
3. Both users should see the project (user100 via regular ACE, user200 via pool ACE)
Related to PR #2750 - RBAC user isolation implementation.