Skip to content

Commit 42bfa9d

Browse files
committed
Unified mcp registration endpoints
1 parent d0b65f5 commit 42bfa9d

8 files changed

Lines changed: 184 additions & 76 deletions

File tree

docs/openapi.json

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,14 @@
12291229
"response": "Configuration is not loaded"
12301230
}
12311231
}
1232+
},
1233+
"mcp server registration": {
1234+
"value": {
1235+
"detail": {
1236+
"cause": "Could not register the MCP server with the remote service.",
1237+
"response": "Failed to register MCP server"
1238+
}
1239+
}
12321240
}
12331241
}
12341242
}
@@ -1397,30 +1405,18 @@
13971405
"response": "User does not have permission to access this endpoint"
13981406
}
13991407
}
1400-
}
1401-
},
1402-
"schema": {
1403-
"$ref": "#/components/schemas/ForbiddenResponse"
1404-
}
1405-
}
1406-
}
1407-
},
1408-
"404": {
1409-
"description": "Resource not found",
1410-
"content": {
1411-
"application/json": {
1412-
"examples": {
1413-
"mcp server": {
1408+
},
1409+
"mcp server static": {
14141410
"value": {
14151411
"detail": {
1416-
"cause": "Mcp Server with ID test-mcp-server does not exist",
1417-
"response": "Mcp Server not found"
1412+
"cause": "MCP server 'my-mcp' is defined in configuration and cannot be removed via the API.",
1413+
"response": "Cannot delete statically configured MCP server"
14181414
}
14191415
}
14201416
}
14211417
},
14221418
"schema": {
1423-
"$ref": "#/components/schemas/NotFoundResponse"
1419+
"$ref": "#/components/schemas/ForbiddenResponse"
14241420
}
14251421
}
14261422
}
@@ -13221,6 +13217,13 @@
1322113217
"response": "This instance does not permit overriding model/provider in the query request (missing permission: model_override). Please remove the model and provider fields from your request."
1322213218
},
1322313219
"label": "model override"
13220+
},
13221+
{
13222+
"detail": {
13223+
"cause": "MCP server 'my-mcp' is defined in configuration and cannot be removed via the API.",
13224+
"response": "Cannot delete statically configured MCP server"
13225+
},
13226+
"label": "mcp server static"
1322413227
}
1322513228
]
1322613229
},
@@ -13514,6 +13517,13 @@
1351413517
"response": "Internal server error"
1351513518
},
1351613519
"label": "invalid cluster version"
13520+
},
13521+
{
13522+
"detail": {
13523+
"cause": "Could not register the MCP server with the remote service.",
13524+
"response": "Failed to register MCP server"
13525+
},
13526+
"label": "mcp server registration"
1351713527
}
1351813528
]
1351913529
},

src/app/endpoints/mcp_servers.py

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from typing import Annotated, Any
44

55
from fastapi import APIRouter, Depends, HTTPException, Request, status
6-
from llama_stack_client import APIConnectionError
6+
from llama_stack_api.common.errors import ToolGroupNotFoundError
7+
from llama_stack_client import APIConnectionError, APIStatusError
78

89
from authentication import get_auth_dependency
910
from authentication.interface import AuthTuple
@@ -17,7 +18,6 @@
1718
ConflictResponse,
1819
ForbiddenResponse,
1920
InternalServerErrorResponse,
20-
NotFoundResponse,
2121
ServiceUnavailableResponse,
2222
UnauthorizedResponse,
2323
)
@@ -39,7 +39,9 @@
3939
401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES),
4040
403: ForbiddenResponse.openapi_response(examples=["endpoint"]),
4141
409: ConflictResponse.openapi_response(examples=["mcp server"]),
42-
500: InternalServerErrorResponse.openapi_response(examples=["configuration"]),
42+
500: InternalServerErrorResponse.openapi_response(
43+
examples=["configuration", "mcp server registration"]
44+
),
4345
503: ServiceUnavailableResponse.openapi_response(
4446
examples=["llama stack", "kubernetes api"]
4547
),
@@ -109,10 +111,7 @@ async def register_mcp_server_handler(
109111
except Exception as e: # pylint: disable=broad-exception-caught
110112
configuration.remove_mcp_server(body.name)
111113
logger.error("Failed to register MCP toolgroup: %s", e)
112-
error_response = InternalServerErrorResponse(
113-
response="Failed to register MCP server",
114-
cause=str(e),
115-
)
114+
error_response = InternalServerErrorResponse.mcp_server_registration_failed()
116115
raise HTTPException(**error_response.model_dump()) from e
117116

118117
logger.info("Dynamically registered MCP server: %s at %s", body.name, body.url)
@@ -178,8 +177,7 @@ async def list_mcp_servers_handler(
178177
delete_responses: dict[int | str, dict[str, Any]] = {
179178
200: MCPServerDeleteResponse.openapi_response(),
180179
401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES),
181-
403: ForbiddenResponse.openapi_response(examples=["endpoint"]),
182-
404: NotFoundResponse.openapi_response(examples=["mcp server"]),
180+
403: ForbiddenResponse.openapi_response(examples=["endpoint", "mcp server static"]),
183181
500: InternalServerErrorResponse.openapi_response(examples=["configuration"]),
184182
503: ServiceUnavailableResponse.openapi_response(
185183
examples=["llama stack", "kubernetes api"]
@@ -218,46 +216,30 @@ async def delete_mcp_server_handler(
218216
check_configuration_loaded(configuration)
219217

220218
if not configuration.is_dynamic_mcp_server(name):
221-
found = any(s.name == name for s in configuration.mcp_servers)
222-
if found:
223-
response = ForbiddenResponse(
224-
response="Cannot delete statically configured MCP server",
225-
cause=f"MCP server '{name}' was configured in lightspeed-stack.yaml "
226-
"and cannot be removed via the API.",
227-
)
228-
else:
229-
response = NotFoundResponse(resource="MCP server", resource_id=name)
230-
raise HTTPException(**response.model_dump())
219+
static_mcp_names = {s.name for s in configuration.mcp_servers}
220+
if name in static_mcp_names:
221+
response = ForbiddenResponse.mcp_server_static_config(name)
222+
raise HTTPException(**response.model_dump())
223+
224+
try:
225+
configuration.remove_mcp_server(name)
226+
local_deleted = True
227+
except ValueError as e:
228+
logger.error("Failed to remove MCP server from configuration: %s", e)
229+
local_deleted = False
231230

232231
try:
233232
client = AsyncLlamaStackClientHolder().get_client()
234233
await client.toolgroups.unregister( # pyright: ignore[reportDeprecated]
235234
toolgroup_id=name
236235
)
237236
except APIConnectionError as e:
238-
logger.error("Failed to unregister MCP toolgroup from Llama Stack: %s", e)
237+
logger.error("Failed to connect to Llama Stack: %s", e)
239238
svc_response = ServiceUnavailableResponse(
240239
backend_name="Llama Stack", cause=str(e)
241240
)
242241
raise HTTPException(**svc_response.model_dump()) from e
243-
except Exception as e: # pylint: disable=broad-exception-caught
244-
logger.warning(
245-
"Llama Stack toolgroup unregister failed for '%s', "
246-
"proceeding with local removal: %s",
247-
name,
248-
e,
249-
)
250-
251-
try:
252-
configuration.remove_mcp_server(name)
253-
except ValueError as e:
254-
logger.error("Failed to remove MCP server from configuration: %s", e)
255-
response = NotFoundResponse(resource="MCP server", resource_id=name)
256-
raise HTTPException(**response.model_dump()) from e
242+
except (APIStatusError, ToolGroupNotFoundError):
243+
logger.warning("MCP server not found, treating as already deleted.")
257244

258-
logger.info("Dynamically unregistered MCP server: %s", name)
259-
260-
return MCPServerDeleteResponse(
261-
name=name,
262-
message=f"MCP server '{name}' unregistered successfully",
263-
)
245+
return MCPServerDeleteResponse(deleted=local_deleted, name=name)

src/models/api/responses/error/forbidden.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ class ForbiddenResponse(AbstractErrorResponse):
8585
),
8686
},
8787
},
88+
{
89+
"label": "mcp server static",
90+
"detail": {
91+
"response": "Cannot delete statically configured MCP server",
92+
"cause": (
93+
"MCP server 'my-mcp' is defined in configuration "
94+
"and cannot be removed via the API."
95+
),
96+
},
97+
},
8898
]
8999
}
90100
}
@@ -156,6 +166,25 @@ def model_override(cls) -> Self:
156166
"to override model/provider."
157167
),
158168
)
169+
170+
@classmethod
171+
def mcp_server_static_config(cls, server_name: str) -> Self:
172+
"""Create a ForbiddenResponse indicating the client tried to delete a statically configured MCP server.
173+
174+
Args:
175+
server_name: The name of the MCP server that is statically configured.
176+
177+
Returns:
178+
Error response with response "Cannot delete statically configured MCP server" and cause
179+
"MCP server 'server_name' is defined in configuration and cannot be removed via the API."
180+
"""
181+
return cls(
182+
response="Cannot delete statically configured MCP server",
183+
cause=(
184+
f"MCP server '{server_name}' is defined in configuration "
185+
"and cannot be removed via the API."
186+
),
187+
)
159188

160189
def __init__(self, *, response: str, cause: str) -> None:
161190
"""Construct a ForbiddenResponse with a public message and an internal cause.

src/models/api/responses/error/internal.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ class InternalServerErrorResponse(AbstractErrorResponse):
8080
"cause": "Missing or invalid 'clusterID' in ClusterVersion",
8181
},
8282
},
83+
{
84+
"label": "mcp server registration",
85+
"detail": {
86+
"response": "Failed to register MCP server",
87+
"cause": (
88+
"Could not register the MCP server with the remote service."
89+
),
90+
},
91+
},
8392
]
8493
}
8594
}
@@ -173,6 +182,20 @@ def database_error(cls) -> Self:
173182
response="Database query failed",
174183
cause="Failed to query the database",
175184
)
185+
186+
@classmethod
187+
def mcp_server_registration_failed(cls) -> Self:
188+
"""
189+
Create an InternalServerErrorResponse representing a failed MCP server registration.
190+
191+
Returns:
192+
Error response with response "Failed to register MCP server" and cause
193+
"Could not register the MCP server with the remote service."
194+
"""
195+
return cls(
196+
response="Failed to register MCP server",
197+
cause=("Could not register the MCP server with the remote service."),
198+
)
176199

177200
def __init__(self, *, response: str, cause: str) -> None:
178201
"""Initialize the error response for internal server errors and set the HTTP status code.

tests/e2e/features/mcp_servers_api.feature

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,18 @@ Feature: MCP Server Management API tests
4848
Then The status code of the response is 403
4949
And The body of the response contains statically configured
5050

51-
Scenario: Delete non-existent MCP server returns 404
51+
Scenario: Delete non-existent MCP server returns 200 with not-found outcome
5252
When I access REST API endpoint "mcp-servers/non-existent-server" using HTTP DELETE method
53-
Then The status code of the response is 404
54-
And The body of the response contains Mcp Server not found
53+
Then The status code of the response is 200
54+
And The body of the response is the following
55+
"""
56+
{
57+
"deleted": false,
58+
"name": "non-existent-server",
59+
"response": "MCP server not found",
60+
"success": true
61+
}
62+
"""
5563

5664
Scenario: Register MCP server with missing required fields returns 422
5765
When I access REST API endpoint "mcp-servers" using HTTP POST method
@@ -100,9 +108,18 @@ Feature: MCP Server Management API tests
100108
When I access REST API endpoint "mcp-servers/e2e-lifecycle-server" using HTTP DELETE method
101109
Then The status code of the response is 200
102110
And The body of the response contains e2e-lifecycle-server
103-
And The body of the response contains unregistered successfully
111+
And The body of the response contains deleted successfully
104112
When I access REST API endpoint "mcp-servers/e2e-lifecycle-server" using HTTP DELETE method
105-
Then The status code of the response is 404
113+
Then The status code of the response is 200
114+
And The body of the response is the following
115+
"""
116+
{
117+
"deleted": false,
118+
"name": "e2e-lifecycle-server",
119+
"response": "MCP server not found",
120+
"success": true
121+
}
122+
"""
106123
When I access REST API endpoint "mcp-servers" using HTTP GET method
107124
Then The status code of the response is 200
108125
And the body of the response has the following structure

tests/integration/test_openapi_json.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def test_servers_section_present_from_url(spec_from_url: dict[str, Any]) -> None
237237
(
238238
"/v1/mcp-servers/{name}",
239239
"delete",
240-
{"200", "401", "403", "404", "500", "503"},
240+
{"200", "401", "403", "500", "503"},
241241
),
242242
("/v1/query", "post", {"200", "401", "403", "404", "422", "429", "500", "503"}),
243243
(
@@ -341,7 +341,7 @@ def test_paths_and_responses_exist_from_file(
341341
(
342342
"/v1/mcp-servers/{name}",
343343
"delete",
344-
{"200", "401", "403", "404", "500", "503"},
344+
{"200", "401", "403", "500", "503"},
345345
),
346346
("/v1/query", "post", {"200", "401", "403", "404", "422", "429", "500", "503"}),
347347
(

0 commit comments

Comments
 (0)