From 0d22d275fb930df58a4971c47b3ad34250fe813a Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 6 Jun 2026 23:26:21 +0800 Subject: [PATCH 1/5] fix: correct MCP template tool parameter handling for nested kwargs 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. --- gns3server/api/routes/mcp/templates.py | 29 ++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/gns3server/api/routes/mcp/templates.py b/gns3server/api/routes/mcp/templates.py index b5df86faf..e2cc71e76 100644 --- a/gns3server/api/routes/mcp/templates.py +++ b/gns3server/api/routes/mcp/templates.py @@ -52,12 +52,16 @@ def list_templates_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> def get_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: template_id = params.get("template_id") name = params.get("name") + if not template_id and not name: return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) template = conn.get_template(name=name, template_id=template_id) + if template is None: return {"error": "Template not found"} + return template @@ -66,26 +70,43 @@ def create_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> template_type = params.get("template_type") if not name or not template_type: return {"error": "name and template_type are required"} + conn = _get_connector(gns3_ctx) - return conn.create_template(**params) + + # Handle nested kwargs structure from MCP clients + if "kwargs" in params and isinstance(params["kwargs"], dict): + create_params = params["kwargs"] + else: + create_params = params + + return conn.create_template(**create_params) def update_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: template_id = params.get("template_id") name = params.get("name") + if not template_id and not name: return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) - return conn.update_template(name=name, template_id=template_id, **{ - k: v for k, v in params.items() if k not in ("template_id", "name") - }) + + # Extract update parameters - handle nested kwargs structure from MCP clients + if "kwargs" in params and isinstance(params["kwargs"], dict): + update_params = params["kwargs"] + else: + update_params = {k: v for k, v in params.items() if k not in ("template_id", "name", "kwargs")} + + return conn.update_template(name=name, template_id=template_id, **update_params) def delete_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: template_id = params.get("template_id") name = params.get("name") + if not template_id and not name: return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) conn.delete_template(name=name, template_id=template_id) return {"message": f"Template deleted"} From 3ba11c8bff7306438fb94cc2871fae601ce983b5 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 6 Jun 2026 23:31:32 +0800 Subject: [PATCH 2/5] fix: correct MCP nodes and links tool parameter handling for nested kwargs 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. --- gns3server/api/routes/mcp/links.py | 8 +++++++- gns3server/api/routes/mcp/nodes.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/api/routes/mcp/links.py index 2b9278ba0..32b203937 100644 --- a/gns3server/api/routes/mcp/links.py +++ b/gns3server/api/routes/mcp/links.py @@ -94,7 +94,13 @@ def update_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not project_id or not link_id: return {"error": "project_id and link_id are required"} conn = _get_connector(gns3_ctx) - update_data = {k: v for k, v in params.items() if k not in ("project_id", "link_id")} + + # Extract update parameters - handle nested kwargs structure from MCP clients + if "kwargs" in params and isinstance(params["kwargs"], dict): + update_data = params["kwargs"] + else: + update_data = {k: v for k, v in params.items() if k not in ("project_id", "link_id", "kwargs")} + url = f"{conn.base_url}/projects/{project_id}/links/{link_id}" return conn.http_call("put", url, json_data=update_data).json() diff --git a/gns3server/api/routes/mcp/nodes.py b/gns3server/api/routes/mcp/nodes.py index 7fef57021..7a16a59cd 100644 --- a/gns3server/api/routes/mcp/nodes.py +++ b/gns3server/api/routes/mcp/nodes.py @@ -132,7 +132,13 @@ def update_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not project_id or not node_id: return {"error": "project_id and node_id are required"} conn = _get_connector(gns3_ctx) - update_data = {k: v for k, v in params.items() if k not in ("project_id", "node_id")} + + # Extract update parameters - handle nested kwargs structure from MCP clients + if "kwargs" in params and isinstance(params["kwargs"], dict): + update_data = params["kwargs"] + else: + update_data = {k: v for k, v in params.items() if k not in ("project_id", "node_id", "kwargs")} + url = f"{conn.base_url}/projects/{project_id}/nodes/{node_id}" return conn.http_call("put", url, json_data=update_data).json() From eac6c9f17d139bccc62322825ec00b22a19fcebf Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 6 Jun 2026 23:46:22 +0800 Subject: [PATCH 3/5] docs: improve MCP link tools filter parameter descriptions 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. --- gns3server/api/routes/mcp/links.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/api/routes/mcp/links.py index 32b203937..1d5a87da6 100644 --- a/gns3server/api/routes/mcp/links.py +++ b/gns3server/api/routes/mcp/links.py @@ -153,7 +153,10 @@ LINK_TOOLS = [ }, }, "link_type": {"type": "string", "description": "Link type: ethernet or serial (optional)"}, - "filters": {"type": "object", "description": "Packet filters (optional)"}, + "filters": { + "type": "object", + "description": "Packet filters (optional). Must use array format: frequency_drop: [N], packet_loss: [rate], delay: [ms, jitter], corrupt: [rate], bpf: [expression]" + }, }, "required": ["project_id", "nodes"], }, @@ -181,7 +184,10 @@ LINK_TOOLS = [ "project_id": {"type": "string", "description": "Project UUID"}, "link_id": {"type": "string", "description": "Link UUID"}, "suspend": {"type": "boolean", "description": "Suspend the link (optional)"}, - "filters": {"type": "object", "description": "Packet filters (optional)"}, + "filters": { + "type": "object", + "description": "Packet filters (optional). Must use array format: frequency_drop: [N], packet_loss: [rate], delay: [ms, jitter], corrupt: [rate], bpf: [expression]. Example: {\"frequency_drop\": [10], \"packet_loss\": [5]}" + }, }, "required": ["project_id", "link_id"], }, From 1f985a2823dad661163ade3dab11e71f36fb741c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 7 Jun 2026 00:20:29 +0800 Subject: [PATCH 4/5] fix: update MCP link tools descriptions with detailed filter info 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. --- gns3server/api/routes/mcp/__init__.py | 32 ++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index 8cf8d0bcb..3cbc66b64 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -337,9 +337,19 @@ async def create_link( project_id: Annotated[str, Field(description="UUID of the project")], nodes: Annotated[list, Field(description="List of node connections, e.g. [{\"node_id\": \"...\", \"adapter_number\": 0, \"port_number\": 0}]")], link_type: Annotated[str, Field(description="Link type - ethernet or serial")] = "ethernet", - filters: Annotated[dict, Field(description="Optional packet filters")] = None, + filters: Annotated[dict, Field(description="Optional packet filters (must use array format): frequency_drop: [N], packet_loss: [rate], delay: [ms, jitter], corrupt: [rate], bpf: [expression]")] = None, ) -> list[dict[str, Any]]: - """Create a link between two nodes in a project.""" + """Create a link between two nodes in a project. + + Filters must use array format: + - frequency_drop: [N] - Drop every Nth packet (N: -1 to 32767) + - packet_loss: [rate] - Packet loss percentage (rate: 0 to 100) + - delay: [ms, jitter] - Latency and jitter in milliseconds + - corrupt: [rate] - Packet corruption percentage (rate: 0 to 100) + - bpf: [expression] - Berkeley Packet Filter expression + + Example: {"filters": {"delay": [100, 10], "packet_loss": [5]}} + """ params = {"project_id": project_id, "nodes": nodes, "link_type": link_type} if filters: params["filters"] = filters @@ -361,7 +371,23 @@ async def update_link( link_id: Annotated[str, Field(description="UUID of the link to update")], **kwargs: Any, ) -> list[dict[str, Any]]: - """Update a link's properties (suspend, filters, etc.).""" + """Update a link's properties (suspend, filters, etc.). + + Supported kwargs: + - suspend: boolean - Suspend or resume the link + - filters: dict - Packet filters (must use array format): + * frequency_drop: [N] - Drop every Nth packet (N: -1 to 32767) + * packet_loss: [rate] - Packet loss percentage (rate: 0 to 100) + * delay: [ms, jitter] - Latency and jitter in milliseconds + * corrupt: [rate] - Packet corruption percentage (rate: 0 to 100) + * bpf: [expression] - Berkeley Packet Filter expression + + Example filters: + {"filters": {"frequency_drop": [10]}} + {"filters": {"delay": [100, 10]}} + {"filters": {"packet_loss": [5]}} + {"filters": {"delay": [50, 5], "packet_loss": [2]}} + """ params = {"project_id": project_id, "link_id": link_id, **kwargs} return await asyncio.to_thread(_run_handler_sync, update_link_handler, params) From 012df5af57d4b3f1d429589429160b36a947f8df Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 7 Jun 2026 00:32:39 +0800 Subject: [PATCH 5/5] docs: add simplified MCP tool description guide to project memory Added a focused, concise memory document about MCP tool description location. Simplified from 180 lines to 45 lines to capture only the essential information needed for future conversations. Key points documented: - MCP tool descriptions are defined in @mcp.tool() functions in __init__.py - NOT in *_TOOLS arrays in individual module files - Must restart GNS3 server to see description updates - Description requirements: explicit formats, ranges, and examples This replaces the verbose 180-line version with a practical 45-line guide focused on answering: 'Where should I write tool descriptions so the AI can see them?' --- .claude/memory/MEMORY.md | 1 + .claude/memory/mcp-tool-description-guide.md | 43 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .claude/memory/mcp-tool-description-guide.md diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index e02b12fbc..59456d279 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -30,3 +30,4 @@ ### MCP Service - **[MCP Service Design](./mcp-service-design.md)** - MCP (Model Context Protocol) service architecture using FastMCP with SSE transport, JWT auth, 29 tools across 5 domains +- **[MCP Tool Description Location](./mcp-tool-description-guide.md)** - Where to define MCP tool descriptions: in `@mcp.tool()` functions in `__init__.py`, not in `*_TOOLS` arrays diff --git a/.claude/memory/mcp-tool-description-guide.md b/.claude/memory/mcp-tool-description-guide.md new file mode 100644 index 000000000..ca4ebcf42 --- /dev/null +++ b/.claude/memory/mcp-tool-description-guide.md @@ -0,0 +1,43 @@ +--- +name: mcp-tool-description-location +description: Where to define MCP tool descriptions so AI can see them +metadata: + type: reference +--- + +# MCP Tool Description Location + +## Key Point +MCP tool descriptions are defined in `@mcp.tool()` decorator functions in `__init__.py`, NOT in the `*_TOOLS` arrays in individual module files. + +## Correct Location +**File**: `gns3server/api/routes/mcp/__init__.py` + +**Example**: +```python +@mcp.tool() +async def update_link( + project_id: Annotated[str, Field(description="UUID of the project")], + link_id: Annotated[str, Field(description="UUID of the link to update")], + **kwargs: Any, +) -> list[dict[str, Any]]: + """Update a link's properties. + + Put detailed descriptions here, especially for complex parameters. + Include format requirements, ranges, and examples. + """ + # implementation +``` + +## Wrong Location +- ❌ `LINK_TOOLS` in `gns3server/api/routes/mcp/links.py` +- ❌ `TEMPLATE_TOOLS` in `gns3server/api/routes/mcp/templates.py` + +## Activation +**Must restart GNS3 server** for description updates to take effect. + +## Description Requirements +- Be explicit about data formats (arrays vs single values) +- Include parameter ranges and constraints +- Provide usage examples +- Prevent common errors in the description itself