Skip to content

Commit 61e8175

Browse files
committed
Update docs and add E2E tests for MCP APIs
Assisted-by: Claude Opus 4.6 Generated-by: Cursor Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com>
1 parent 068411f commit 61e8175

8 files changed

Lines changed: 216 additions & 7 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ This section describes the major functional components that make up LCore. Each
108108
- **Lifecycle Management**:
109109
- **Startup**: Load configuration, initialize Llama Stack client, load MCP server configuration and register all defined servers with Llama Stack to build the tools list, establish database connections
110110
- **Shutdown**: Clean up A2A storage resources (database connections and other resources are cleaned up automatically by Python's context managers)
111-
- **Router Registration**: Mount all endpoint routers (query, conversation, model info, auth, metrics, A2A, feedback, admin, mcp_auth)
111+
- **Router Registration**: Mount all endpoint routers (query, conversation, model info, auth, metrics, A2A, feedback, admin, mcp_auth, mcp_servers)
112112

113113
**Note:** All configured MCP servers must be running and accessible at startup time for LCore to initialize successfully.
114114

@@ -207,6 +207,9 @@ The system defines 30+ actions that can be authorized. Examples (see `docs/auth.
207207
**Agent-to-Agent Protocol:**
208208
- `A2A_JSONRPC` - A2A protocol access
209209

210+
**MCP Server Management:**
211+
- `REGISTER_MCP_SERVER`, `LIST_MCP_SERVERS`, `DELETE_MCP_SERVER`
212+
210213
**Metadata Operations:**
211214
- `LIST_MODELS`, `LIST_SHIELDS`, `LIST_TOOLS`, `LIST_PROVIDERS`
212215

@@ -325,7 +328,7 @@ MCP servers are remote HTTP services that expose tools/capabilities to LLMs (e.g
325328

326329
**How It Works:**
327330

328-
1. **Configuration:** MCP servers are defined in the config file with name, URL, and authorization headers
331+
1. **Configuration:** MCP servers are defined in the config file with name, URL, and authorization headers. Servers can also be registered dynamically at runtime via `POST /v1/mcp-servers`.
329332
2. **Registration at Startup:** LCore tells Llama Stack about each MCP server by calling `toolgroups.register()` - this makes the MCP server's tools available in Llama Stack's tool registry
330333
3. **Query Processing:** When processing a query, LCore determines which tools to make available to the LLM and finalizes authorization headers (e.g., merging client-provided tokens with configured headers)
331334
4. **Tool Execution:** When the LLM calls a tool, Llama Stack routes the request to the appropriate MCP server URL with the finalized authorization headers
@@ -489,6 +492,11 @@ This section documents the REST API endpoints exposed by LCore for client intera
489492
- Returns MCP servers that accept client-provided authentication tokens
490493
- Includes header names that need to be provided via MCP-HEADERS
491494

495+
**MCP Server Management:**
496+
- `POST /v1/mcp-servers` - Register a new MCP server at runtime
497+
- `GET /v1/mcp-servers` - List all registered MCP servers (static and dynamic)
498+
- `DELETE /v1/mcp-servers/{name}` - Unregister a dynamically registered MCP server
499+
492500
**List Shields:** `GET /shields`
493501
- Returns available guardrails
494502

docs/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ Global service configuration.
159159
| llama_stack | | This section contains Llama Stack configuration. Lightspeed Core Stack service can call Llama Stack in library mode or in server mode. |
160160
| user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). |
161161
| database | | Configuration for database to store conversation IDs and other runtime data |
162-
| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the llama-stack run.yaml are not accessible to lightspeed-core agents. |
162+
| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Servers can also be registered dynamically at runtime via the `POST /v1/mcp-servers` API endpoint. Only MCP servers defined in lightspeed-stack.yaml or registered via the API are available to the agents. Tools configured in the llama-stack run.yaml are not accessible to lightspeed-core agents. |
163163
| authentication | | Authentication configuration |
164164
| authorization | | Lightspeed Core Stack implements a modular authentication and authorization system with multiple authentication methods. Authorization is configurable through role-based access control. Authentication is handled through selectable modules configured via the module field in the authentication configuration. |
165165
| customization | | It is possible to customize Lightspeed Core Stack via this section. System prompt can be customized and also different parts of the service can be replaced by custom Python modules. |

docs/getting_started.md

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ mcp_servers:
240240
url: "http://localhost:3002"
241241
```
242242

243-
**Important**: Only MCP servers defined in the `lightspeed-stack.yaml` configuration are available to the AI agents. Tools configured in the llama-stack `run.yaml` are not accessible to LCS agents.
243+
**Important**: MCP servers defined in `lightspeed-stack.yaml` or registered dynamically via the API (see [Dynamic MCP Server Management](#dynamic-mcp-server-management-via-api)) are available to the AI agents. Tools configured in the llama-stack `run.yaml` are not accessible to LCS agents.
244244

245245
#### Step 3: Pass authentication or metadata via MCP headers (optional)
246246

@@ -255,4 +255,79 @@ curl -X POST "http://localhost:8080/v1/query" \
255255
```
256256

257257
#### Step 4: Verify connectivity
258-
After starting the MCP servers and updating `lightspeed-stack.yaml`, test by sending a prompt to the AI agent. LCS evaluates the prompt against available tools’ metadata, selects the appropriate tool, calls the corresponding MCP server, and uses the result to generate more accurate agent response.
258+
After starting the MCP servers and updating `lightspeed-stack.yaml`, test by sending a prompt to the AI agent. LCS evaluates the prompt against available tools' metadata, selects the appropriate tool, calls the corresponding MCP server, and uses the result to generate more accurate agent response.
259+
260+
### Dynamic MCP Server Management via API
261+
262+
In addition to static configuration in `lightspeed-stack.yaml`, MCP servers can be registered, listed, and removed at runtime through the REST API. This is useful for development, testing, or scenarios where MCP servers are provisioned dynamically.
263+
264+
#### Register an MCP server
265+
266+
```bash
267+
curl -X POST "http://localhost:8080/v1/mcp-servers" \
268+
-H "Content-Type: application/json" \
269+
-d '{
270+
"name": "my-dynamic-tools",
271+
"url": "http://localhost:9000/mcp",
272+
"provider_id": "model-context-protocol"
273+
}'
274+
```
275+
276+
Response (201 Created):
277+
```json
278+
{
279+
"name": "my-dynamic-tools",
280+
"url": "http://localhost:9000/mcp",
281+
"provider_id": "model-context-protocol",
282+
"message": "MCP server 'my-dynamic-tools' registered successfully"
283+
}
284+
```
285+
286+
Optional fields in the request body:
287+
- `authorization_headers` (object): Headers to send to the MCP server (e.g., `{"Authorization": "Bearer token123"}`).
288+
- `headers` (array): List of HTTP header names to forward from incoming requests to this MCP server.
289+
- `timeout` (integer): Request timeout in seconds.
290+
291+
#### List all MCP servers
292+
293+
```bash
294+
curl "http://localhost:8080/v1/mcp-servers"
295+
```
296+
297+
Response (200 OK):
298+
```json
299+
{
300+
"servers": [
301+
{
302+
"name": "filesystem-tools",
303+
"url": "http://localhost:3000",
304+
"provider_id": "model-context-protocol",
305+
"source": "config"
306+
},
307+
{
308+
"name": "my-dynamic-tools",
309+
"url": "http://localhost:9000/mcp",
310+
"provider_id": "model-context-protocol",
311+
"source": "api"
312+
}
313+
]
314+
}
315+
```
316+
317+
Each server has a `source` field indicating how it was registered: `"config"` for servers defined in `lightspeed-stack.yaml`, or `"api"` for servers registered via the REST API.
318+
319+
#### Delete a dynamically registered MCP server
320+
321+
```bash
322+
curl -X DELETE "http://localhost:8080/v1/mcp-servers/my-dynamic-tools"
323+
```
324+
325+
Response (200 OK):
326+
```json
327+
{
328+
"name": "my-dynamic-tools",
329+
"message": "MCP server 'my-dynamic-tools' unregistered successfully"
330+
}
331+
```
332+
333+
**Note:** Only dynamically registered servers (source `"api"`) can be deleted via the API. Attempting to delete a statically configured server returns 403 Forbidden. Dynamically registered servers do not persist across service restarts.

docs/openapi.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5363,7 +5363,7 @@
53635363
],
53645364
"summary": "Handle A2A Jsonrpc",
53655365
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
5366-
"operationId": "handle_a2a_jsonrpc_a2a_get",
5366+
"operationId": "handle_a2a_jsonrpc_a2a_post",
53675367
"responses": {
53685368
"200": {
53695369
"description": "Successful Response",
@@ -5381,7 +5381,7 @@
53815381
],
53825382
"summary": "Handle A2A Jsonrpc",
53835383
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
5384-
"operationId": "handle_a2a_jsonrpc_a2a_get",
5384+
"operationId": "handle_a2a_jsonrpc_a2a_post",
53855385
"responses": {
53865386
"200": {
53875387
"description": "Successful Response",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
@MCP
2+
Feature: MCP Server Management API tests
3+
4+
Tests for the dynamic MCP server management endpoints:
5+
POST /v1/mcp-servers, GET /v1/mcp-servers, DELETE /v1/mcp-servers/{name}
6+
7+
Background:
8+
Given The service is started locally
9+
And REST API service prefix is /v1
10+
11+
Scenario: List MCP servers returns pre-configured servers
12+
Given The system is in default state
13+
When I access REST API endpoint "mcp-servers" using HTTP GET method
14+
Then The status code of the response is 200
15+
And The body of the response contains mcp-oauth
16+
And The body of the response contains config
17+
18+
Scenario: List MCP servers response has expected structure
19+
Given The system is in default state
20+
When I access REST API endpoint "mcp-servers" using HTTP GET method
21+
Then The status code of the response is 200
22+
And the body of the response has the following structure
23+
"""
24+
{
25+
"servers": [
26+
{
27+
"name": "mcp-oauth",
28+
"url": "http://mock-mcp:3001",
29+
"provider_id": "model-context-protocol",
30+
"source": "config"
31+
}
32+
]
33+
}
34+
"""
35+
36+
Scenario: Register duplicate MCP server returns 409
37+
Given The system is in default state
38+
When I access REST API endpoint "mcp-servers" using HTTP POST method
39+
"""
40+
{"name": "mcp-oauth", "url": "http://mock-mcp:3001", "provider_id": "model-context-protocol"}
41+
"""
42+
Then The status code of the response is 409
43+
And The body of the response contains already exists
44+
45+
Scenario: Delete statically configured MCP server returns 403
46+
Given The system is in default state
47+
When I access REST API endpoint "mcp-servers/mcp-oauth" using HTTP DELETE method
48+
Then The status code of the response is 403
49+
And The body of the response contains statically configured
50+
51+
Scenario: Delete non-existent MCP server returns 404
52+
Given The system is in default state
53+
When I access REST API endpoint "mcp-servers/non-existent-server" using HTTP DELETE method
54+
Then The status code of the response is 404
55+
56+
Scenario: Register MCP server with invalid request body returns 422
57+
Given The system is in default state
58+
When I access REST API endpoint "mcp-servers" using HTTP POST method
59+
"""
60+
{"url": "http://mock-mcp:3001"}
61+
"""
62+
Then The status code of the response is 422
63+
64+
Scenario: Register MCP server with invalid URL scheme returns 422
65+
Given The system is in default state
66+
When I access REST API endpoint "mcp-servers" using HTTP POST method
67+
"""
68+
{"name": "bad-url-server", "url": "ftp://mock-mcp:3001"}
69+
"""
70+
Then The status code of the response is 422
71+
72+
@skip-in-library-mode
73+
Scenario: Register and delete MCP server lifecycle
74+
Given The system is in default state
75+
When I access REST API endpoint "mcp-servers" using HTTP POST method
76+
"""
77+
{"name": "e2e-lifecycle-server", "url": "http://mock-mcp:3001", "provider_id": "model-context-protocol"}
78+
"""
79+
Then The status code of the response is 201
80+
And The body of the response contains e2e-lifecycle-server
81+
And The body of the response contains registered successfully
82+
When I access REST API endpoint "mcp-servers" using HTTP GET method
83+
Then The status code of the response is 200
84+
And The body of the response contains e2e-lifecycle-server
85+
And The body of the response contains api
86+
When I access REST API endpoint "mcp-servers/e2e-lifecycle-server" using HTTP DELETE method
87+
Then The status code of the response is 200
88+
And The body of the response contains unregistered successfully
89+
When I access REST API endpoint "mcp-servers" using HTTP GET method
90+
Then The status code of the response is 200

tests/e2e/features/steps/common_http.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,19 @@ def access_rest_api_endpoint_post(context: Context, endpoint: str) -> None:
338338
)
339339

340340

341+
@when("I access REST API endpoint {endpoint} using HTTP DELETE method")
342+
def access_rest_api_endpoint_delete(context: Context, endpoint: str) -> None:
343+
"""Send DELETE HTTP request to tested service."""
344+
endpoint = normalize_endpoint(endpoint)
345+
base = f"http://{context.hostname}:{context.port}"
346+
path = f"{context.api_prefix}/{endpoint}".replace("//", "/")
347+
url = base + path
348+
headers = context.auth_headers if hasattr(context, "auth_headers") else {}
349+
context.response = None
350+
351+
context.response = requests.delete(url, headers=headers, timeout=DEFAULT_TIMEOUT)
352+
353+
341354
@when("I access REST API endpoint {endpoint} using HTTP PUT method")
342355
def access_rest_api_endpoint_put(context: Context, endpoint: str) -> None:
343356
"""Send PUT HTTP request with JSON payload to tested service.

tests/e2e/test_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ features/rlsapi_v1_errors.feature
1616
features/streaming_query.feature
1717
features/rest_api.feature
1818
features/mcp.feature
19+
features/mcp_servers_api.feature
1920
features/models.feature

tests/integration/test_openapi_json.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,17 @@ def test_servers_section_present_from_url(spec_from_url: dict[str, Any]) -> None
220220
"get",
221221
{"200", "401", "403", "404", "500", "503"},
222222
),
223+
(
224+
"/v1/mcp-servers",
225+
"post",
226+
{"201", "401", "403", "409", "500", "503"},
227+
),
228+
("/v1/mcp-servers", "get", {"200", "401", "403", "500"}),
229+
(
230+
"/v1/mcp-servers/{name}",
231+
"delete",
232+
{"200", "401", "403", "404", "500", "503"},
233+
),
223234
("/v1/query", "post", {"200", "401", "403", "404", "422", "429", "500", "503"}),
224235
(
225236
"/v1/streaming_query",
@@ -312,6 +323,17 @@ def test_paths_and_responses_exist_from_file(
312323
"get",
313324
{"200", "401", "403", "404", "500", "503"},
314325
),
326+
(
327+
"/v1/mcp-servers",
328+
"post",
329+
{"201", "401", "403", "409", "500", "503"},
330+
),
331+
("/v1/mcp-servers", "get", {"200", "401", "403", "500"}),
332+
(
333+
"/v1/mcp-servers/{name}",
334+
"delete",
335+
{"200", "401", "403", "404", "500", "503"},
336+
),
315337
("/v1/query", "post", {"200", "401", "403", "404", "422", "429", "500", "503"}),
316338
(
317339
"/v1/streaming_query",

0 commit comments

Comments
 (0)