From 77cab00cb8c00e0bb907446b620cd5efe067ad60 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Wed, 3 Sep 2025 09:42:57 -0700 Subject: [PATCH] fix: try and patch the PATCH/update issue with MCP server URL [LET-3933] * fix: patch update path * autogenerate * update * update --------- Co-authored-by: Jin Peng Co-authored-by: Letta Bot Co-authored-by: jnjpng --- letta/schemas/mcp.py | 3 -- project.json | 5 +-- tests/mcp_tests/test_mcp.py | 65 +++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/letta/schemas/mcp.py b/letta/schemas/mcp.py index cf6ec776..e49f177a 100644 --- a/letta/schemas/mcp.py +++ b/letta/schemas/mcp.py @@ -84,7 +84,6 @@ class MCPServer(BaseMCPServer): class UpdateSSEMCPServer(LettaBase): """Update an SSE MCP server""" - server_name: Optional[str] = Field(None, description="The name of the server") server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)") token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)") custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs") @@ -93,7 +92,6 @@ class UpdateSSEMCPServer(LettaBase): class UpdateStdioMCPServer(LettaBase): """Update a Stdio MCP server""" - server_name: Optional[str] = Field(None, description="The name of the server") stdio_config: Optional[StdioServerConfig] = Field( None, description="The configuration for the server (MCP 'local' client will run this command)" ) @@ -102,7 +100,6 @@ class UpdateStdioMCPServer(LettaBase): class UpdateStreamableHTTPMCPServer(LettaBase): """Update a Streamable HTTP MCP server""" - server_name: Optional[str] = Field(None, description="The name of the server") server_url: Optional[str] = Field(None, description="The URL path for the streamable HTTP server (e.g., 'example/mcp')") auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')") auth_token: Optional[str] = Field(None, description="The authentication token or API key value") diff --git a/project.json b/project.json index fa824b60..89c4b422 100644 --- a/project.json +++ b/project.json @@ -26,10 +26,7 @@ "dev": { "executor": "@nxlv/python:run-commands", "options": { - "commands": [ - "./otel/start-otel-collector.sh", - "uv run letta server" - ], + "commands": ["./otel/start-otel-collector.sh", "uv run letta server"], "parallel": true, "cwd": "apps/core" } diff --git a/tests/mcp_tests/test_mcp.py b/tests/mcp_tests/test_mcp.py index 54fa786c..20649abb 100644 --- a/tests/mcp_tests/test_mcp.py +++ b/tests/mcp_tests/test_mcp.py @@ -375,3 +375,68 @@ async def test_streamable_http_mcp_server_update_schema_no_docstring_required(cl finally: client.tools.delete_mcp_server(mcp_server_name=mcp_server_name) assert mcp_server_name not in client.tools.list_mcp_servers() + + +@pytest.mark.asyncio +async def test_mcp_server_patch_updates_url(client, server_url): + """ + Test that PATCHing an MCP server actually updates the server URL. + """ + # Create a unique server name + mcp_server_name = f"test_patch_{uuid.uuid4().hex[:6]}" + original_url = "https://mcp.deepwiki.com/sse" + updated_url = "https://mcp2.deepwiki.com/sse" # Intentionally invalid URL + + # Create the initial SSE MCP server + sse_config = SSEServerConfig(server_name=mcp_server_name, server_url=original_url) + + try: + # Add the MCP server + client.tools.add_mcp_server(request=sse_config) + + # Verify the server was created with correct URL + servers = client.tools.list_mcp_servers() + assert mcp_server_name in servers + assert servers[mcp_server_name].server_url == original_url + print(f"✓ Server created with URL: {original_url}") + + # Import the update schema + from letta.schemas.mcp import UpdateSSEMCPServer + + # Now PATCH the server to update ONLY the URL (partial update) + update_request = UpdateSSEMCPServer(server_url=updated_url) + # Note: We're only setting server_url, not server_name + print(f"Updating server with request: {update_request.model_dump(exclude_unset=True)}") + + updated_server = client.tools.update_mcp_server(mcp_server_name=mcp_server_name, request=update_request) + + # Verify the server URL was actually updated + print(f"Server after update: URL={updated_server.server_url}") + assert updated_server.server_url == updated_url, f"Server URL not updated. Expected {updated_url}, got {updated_server.server_url}" + + # Also verify through the list endpoint + servers = client.tools.list_mcp_servers() + assert mcp_server_name in servers + assert servers[mcp_server_name].server_url == updated_url, ( + f"Server URL not updated in list. Got: {servers[mcp_server_name].server_url}" + ) + print(f"✓ Server URL successfully updated to: {updated_url}") + + # Now try to list tools with the updated (invalid) URL + # This should fail or return empty list if the patch actually worked + try: + tools = client.tools.list_mcp_tools_by_server(mcp_server_name=mcp_server_name) + # If we get here, either the tools listing succeeded (shouldn't with invalid URL) + # or it returned an empty list + assert len(tools) == 0, f"Expected no tools from invalid URL {updated_url}, but got {len(tools)} tools" + print("✓ Invalid URL correctly returns no tools") + except Exception: + # This is expected - the invalid URL should cause an error + # which confirms the PATCH actually updated the URL + print("✓ Invalid URL correctly causes error when listing tools") + + finally: + try: + client.tools.delete_mcp_server(mcp_server_name=mcp_server_name) + except Exception: + pass