Skip to content

Commit 530b4bd

Browse files
committed
✨ Adapt MCP protocal to different program language #416
[Specification Details] 1.Front-end modifications have been made, with the addition of a module for containerized MCP services and a container display and management module. 2.A new "container_id" field has been added in mcp_record_t to identify whether the mcp service is containerized and what the corresponding container id is. 3.The deployment pipeline has been modified, and a new MCP startup image has been added.
1 parent 7bbf165 commit 530b4bd

File tree

14 files changed

+893
-35
lines changed

14 files changed

+893
-35
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Docker Build MCP Images
2+
3+
concurrency:
4+
group: docker-build-mcp-dev-${{ github.ref }}
5+
cancel-in-progress: true
6+
7+
on:
8+
workflow_dispatch:
9+
pull_request:
10+
branches: [develop]
11+
paths:
12+
- 'backend/**'
13+
- 'sdk/**'
14+
- 'make/mcp/**'
15+
- '.github/workflows/**'
16+
push:
17+
branches: [develop]
18+
paths:
19+
- 'backend/**'
20+
- 'sdk/**'
21+
- 'make/mcp/**'
22+
- '.github/workflows/**'
23+
24+
jobs:
25+
build-mcp-amd64:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Checkout code
29+
uses: actions/checkout@v4
30+
- name: Build MCP image (amd64) and load locally
31+
run: |
32+
docker build --platform linux/amd64 -t nexent/nexent-mcp:dev-amd64 -f make/mcp/Dockerfile .
33+
34+
build-mcp-arm64:
35+
runs-on: ubuntu-24.04-arm
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v4
39+
- name: Build MCP image (arm64) and load locally
40+
run: |
41+
docker build --platform linux/arm64 -t nexent/nexent-mcp:dev-arm64 -f make/mcp/Dockerfile .
42+
43+

.github/workflows/docker-build-push-mainland.yml

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,58 @@ jobs:
244244
if: inputs.push_latest == 'true'
245245
run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:arm64
246246

247+
build-and-push-mcp-amd64:
248+
runs-on: ${{ fromJson(inputs.runner_label_json) }}
249+
steps:
250+
- name: Set up Docker Buildx
251+
run: |
252+
if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then
253+
docker buildx create --name nexent_builder --use
254+
else
255+
docker buildx use nexent_builder
256+
fi
257+
- name: Checkout code
258+
uses: actions/checkout@v4
259+
- name: Build MCP image (amd64) and load locally
260+
run: |
261+
docker buildx build --platform linux/amd64 --load -t ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-amd64 -f make/mcp/Dockerfile --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua .
262+
- name: Login to Tencent Cloud
263+
run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin
264+
- name: Push MCP image (amd64) to Tencent Cloud
265+
run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-amd64
266+
- name: Tag MCP image (amd64) as latest
267+
if: inputs.push_latest == 'true'
268+
run: docker tag ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-amd64 ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:amd64
269+
- name: Push latest MCP image (amd64) to Tencent Cloud
270+
if: inputs.push_latest == 'true'
271+
run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:amd64
272+
273+
build-and-push-mcp-arm64:
274+
runs-on: ${{ fromJson(inputs.runner_label_json) }}
275+
steps:
276+
- name: Set up Docker Buildx
277+
run: |
278+
if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then
279+
docker buildx create --name nexent_builder --use
280+
else
281+
docker buildx use nexent_builder
282+
fi
283+
- name: Checkout code
284+
uses: actions/checkout@v4
285+
- name: Build MCP image (arm64) and load locally
286+
run: |
287+
docker buildx build --platform linux/arm64 --load -t ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-arm64 -f make/mcp/Dockerfile --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua .
288+
- name: Login to Tencent Cloud
289+
run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin
290+
- name: Push MCP image (arm64) to Tencent Cloud
291+
run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-arm64
292+
- name: Tag MCP image (arm64) as latest
293+
if: inputs.push_latest == 'true'
294+
run: docker tag ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-arm64 ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:arm64
295+
- name: Push latest MCP image (arm64) to Tencent Cloud
296+
if: inputs.push_latest == 'true'
297+
run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:arm64
298+
247299
manifest-push-main:
248300
runs-on: ubuntu-latest
249301
needs:
@@ -330,4 +382,26 @@ jobs:
330382
docker manifest create ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:latest \
331383
ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:amd64 \
332384
ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:arm64
333-
docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:latest
385+
docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:latest
386+
387+
manifest-push-mcp:
388+
runs-on: ubuntu-latest
389+
needs:
390+
- build-and-push-mcp-amd64
391+
- build-and-push-mcp-arm64
392+
steps:
393+
- name: Login to Tencent Cloud
394+
run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin
395+
- name: Create and push manifest for mcp (Tencent Cloud)
396+
run: |
397+
docker manifest create ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }} \
398+
ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-amd64 \
399+
ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}-arm64
400+
docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${{ inputs.version }}
401+
- name: Create and push latest manifest for mcp (Tencent Cloud)
402+
if: inputs.push_latest == 'true'
403+
run: |
404+
docker manifest create ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:latest \
405+
ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:amd64 \
406+
ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:arm64
407+
docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:latest

.github/workflows/docker-build-push-overseas.yml

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,58 @@ jobs:
244244
if: inputs.push_latest == 'true'
245245
run: docker push nexent/nexent-ubuntu-terminal:arm64
246246

247+
build-and-push-mcp-amd64:
248+
runs-on: ${{ fromJson(inputs.runner_label_json) }}
249+
steps:
250+
- name: Set up Docker Buildx
251+
run: |
252+
if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then
253+
docker buildx create --name nexent_builder --use
254+
else
255+
docker buildx use nexent_builder
256+
fi
257+
- name: Checkout code
258+
uses: actions/checkout@v4
259+
- name: Build MCP image (amd64) and load locally
260+
run: |
261+
docker buildx build --platform linux/amd64 -t nexent/nexent-mcp:${{ inputs.version }}-amd64 --load -f make/mcp/Dockerfile .
262+
- name: Login to DockerHub
263+
run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin
264+
- name: Push MCP image (amd64) to DockerHub
265+
run: docker push nexent/nexent-mcp:${{ inputs.version }}-amd64
266+
- name: Tag MCP image (amd64) as latest
267+
if: inputs.push_latest == 'true'
268+
run: docker tag nexent/nexent-mcp:${{ inputs.version }}-amd64 nexent/nexent-mcp:amd64
269+
- name: Push latest MCP image (amd64) to DockerHub
270+
if: inputs.push_latest == 'true'
271+
run: docker push nexent/nexent-mcp:amd64
272+
273+
build-and-push-mcp-arm64:
274+
runs-on: ${{ fromJson(inputs.runner_label_json) }}
275+
steps:
276+
- name: Set up Docker Buildx
277+
run: |
278+
if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then
279+
docker buildx create --name nexent_builder --use
280+
else
281+
docker buildx use nexent_builder
282+
fi
283+
- name: Checkout code
284+
uses: actions/checkout@v4
285+
- name: Build MCP image (arm64) and load locally
286+
run: |
287+
docker buildx build --platform linux/arm64 -t nexent/nexent-mcp:${{ inputs.version }}-arm64 --load -f make/mcp/Dockerfile .
288+
- name: Login to DockerHub
289+
run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin
290+
- name: Push MCP image (arm64) to DockerHub
291+
run: docker push nexent/nexent-mcp:${{ inputs.version }}-arm64
292+
- name: Tag MCP image (arm64) as latest
293+
if: inputs.push_latest == 'true'
294+
run: docker tag nexent/nexent-mcp:${{ inputs.version }}-arm64 nexent/nexent-mcp:arm64
295+
- name: Push latest MCP image (arm64) to DockerHub
296+
if: inputs.push_latest == 'true'
297+
run: docker push nexent/nexent-mcp:arm64
298+
247299
manifest-push-main:
248300
runs-on: ubuntu-latest
249301
needs:
@@ -330,4 +382,26 @@ jobs:
330382
docker manifest create nexent/nexent-ubuntu-terminal:latest \
331383
nexent/nexent-ubuntu-terminal:amd64 \
332384
nexent/nexent-ubuntu-terminal:arm64
333-
docker manifest push nexent/nexent-ubuntu-terminal:latest
385+
docker manifest push nexent/nexent-ubuntu-terminal:latest
386+
387+
manifest-push-mcp:
388+
runs-on: ubuntu-latest
389+
needs:
390+
- build-and-push-mcp-amd64
391+
- build-and-push-mcp-arm64
392+
steps:
393+
- name: Login to DockerHub
394+
run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin
395+
- name: Create and push manifest for mcp (DockerHub)
396+
run: |
397+
docker manifest create nexent/nexent-mcp:${{ inputs.version }} \
398+
nexent/nexent-mcp:${{ inputs.version }}-amd64 \
399+
nexent/nexent-mcp:${{ inputs.version }}-arm64
400+
docker manifest push nexent/nexent-mcp:${{ inputs.version }}
401+
- name: Create and push latest manifest for mcp (DockerHub)
402+
if: inputs.push_latest == 'true'
403+
run: |
404+
docker manifest create nexent/nexent-mcp:latest \
405+
nexent/nexent-mcp:amd64 \
406+
nexent/nexent-mcp:arm64
407+
docker manifest push nexent/nexent-mcp:latest

backend/apps/remote_mcp_app.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
delete_remote_mcp_server_list,
1414
get_remote_mcp_server_list,
1515
check_mcp_health_and_update_db,
16+
delete_mcp_by_container_id,
1617
)
1718
from services.tool_configuration_service import get_tool_from_remote_mcp_server
1819
from services.mcp_container_service import MCPContainerManager
@@ -58,7 +59,8 @@ async def add_remote_proxies(
5859
await add_remote_mcp_server_list(tenant_id=tenant_id,
5960
user_id=user_id,
6061
remote_mcp_server=mcp_url,
61-
remote_mcp_server_name=service_name)
62+
remote_mcp_server_name=service_name,
63+
container_id=None)
6264
return JSONResponse(
6365
status_code=HTTPStatus.OK,
6466
content={"message": "Successfully added remote MCP proxy",
@@ -227,7 +229,8 @@ async def add_mcp_from_config(
227229
tenant_id=tenant_id,
228230
user_id=user_id,
229231
remote_mcp_server=container_info["mcp_url"],
230-
remote_mcp_server_name=service_name
232+
remote_mcp_server_name=service_name,
233+
container_id=container_info["container_id"],
231234
)
232235
except MCPNameIllegal:
233236
# If name already exists, try to stop the container we just created
@@ -297,18 +300,27 @@ async def stop_mcp_container(
297300
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
298301
detail="Docker service unavailable"
299302
)
300-
303+
301304
success = await container_manager.stop_mcp_container(container_id)
302-
305+
303306
if success:
307+
# Soft delete the corresponding MCP record (if any) by container ID
308+
await delete_mcp_by_container_id(
309+
tenant_id=tenant_id,
310+
user_id=user_id,
311+
container_id=container_id,
312+
)
304313
return JSONResponse(
305314
status_code=HTTPStatus.OK,
306-
content={"message": "Container stopped successfully", "status": "success"}
315+
content={
316+
"message": "Container and MCP service stopped successfully",
317+
"status": "success",
318+
},
307319
)
308320
else:
309321
return JSONResponse(
310322
status_code=HTTPStatus.NOT_FOUND,
311-
content={"message": "Container not found", "status": "error"}
323+
content={"message": "Container not found", "status": "error"},
312324
)
313325
except HTTPException:
314326
raise

backend/database/db_models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,15 @@ class McpRecord(TableBase):
300300
user_id = Column(String(100), doc="User ID")
301301
mcp_name = Column(String(100), doc="MCP name")
302302
mcp_server = Column(String(500), doc="MCP server address")
303-
status = Column(Boolean, default=None,
304-
doc="MCP server connection status, True=connected, False=disconnected, None=unknown")
303+
status = Column(
304+
Boolean,
305+
default=None,
306+
doc="MCP server connection status, True=connected, False=disconnected, None=unknown",
307+
)
308+
container_id = Column(
309+
String(200),
310+
doc="Docker container ID for MCP service, None for non-containerized MCP",
311+
)
305312

306313

307314
class UserTenant(TableBase):

backend/database/remote_mcp_db.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ def delete_mcp_record_by_name_and_url(mcp_name: str, mcp_server: str, tenant_id:
4646
).update({"delete_flag": "Y", "updated_by": user_id})
4747

4848

49+
def delete_mcp_record_by_container_id(container_id: str, tenant_id: str, user_id: str):
50+
"""
51+
Soft delete MCP record by container ID
52+
53+
:param container_id: Docker container ID
54+
:param tenant_id: Tenant ID
55+
:param user_id: User ID
56+
"""
57+
with get_db_session() as session:
58+
session.query(McpRecord).filter(
59+
McpRecord.container_id == container_id,
60+
McpRecord.tenant_id == tenant_id,
61+
McpRecord.delete_flag != 'Y'
62+
).update({"delete_flag": "Y", "updated_by": user_id})
63+
64+
4965
def update_mcp_status_by_name_and_url(mcp_name: str, mcp_server: str, tenant_id: str, user_id: str, status: bool):
5066
"""
5167
Update the status of MCP record by name and URL

backend/services/remote_mcp_service.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from database.remote_mcp_db import (
77
create_mcp_record,
88
delete_mcp_record_by_name_and_url,
9+
delete_mcp_record_by_container_id,
910
get_mcp_records_by_tenant,
1011
check_mcp_name_exists,
11-
update_mcp_status_by_name_and_url
12+
update_mcp_status_by_name_and_url,
1213
)
1314

1415
logger = logging.getLogger("remote_mcp_service")
@@ -26,10 +27,13 @@ async def mcp_server_health(remote_mcp_server: str) -> bool:
2627
raise MCPConnectionError("MCP connection failed")
2728

2829

29-
async def add_remote_mcp_server_list(tenant_id: str,
30-
user_id: str,
31-
remote_mcp_server: str,
32-
remote_mcp_server_name: str):
30+
async def add_remote_mcp_server_list(
31+
tenant_id: str,
32+
user_id: str,
33+
remote_mcp_server: str,
34+
remote_mcp_server_name: str,
35+
container_id: str | None = None,
36+
):
3337

3438
# check if MCP name already exists
3539
if check_mcp_name_exists(mcp_name=remote_mcp_server_name, tenant_id=tenant_id):
@@ -42,11 +46,13 @@ async def add_remote_mcp_server_list(tenant_id: str,
4246
raise MCPConnectionError("MCP connection failed")
4347

4448
# update the PG database record
45-
insert_mcp_data = {"mcp_name": remote_mcp_server_name,
46-
"mcp_server": remote_mcp_server,
47-
"status": True}
48-
create_mcp_record(
49-
mcp_data=insert_mcp_data, tenant_id=tenant_id, user_id=user_id)
49+
insert_mcp_data = {
50+
"mcp_name": remote_mcp_server_name,
51+
"mcp_server": remote_mcp_server,
52+
"status": True,
53+
"container_id": container_id,
54+
}
55+
create_mcp_record(mcp_data=insert_mcp_data, tenant_id=tenant_id, user_id=user_id)
5056

5157

5258
async def delete_remote_mcp_server_list(tenant_id: str,
@@ -88,3 +94,17 @@ async def check_mcp_health_and_update_db(mcp_url, service_name, tenant_id, user_
8894
status=status)
8995
if not status:
9096
raise MCPConnectionError("MCP connection failed")
97+
98+
99+
async def delete_mcp_by_container_id(tenant_id: str, user_id: str, container_id: str):
100+
"""
101+
Soft delete MCP record associated with a specific container ID.
102+
103+
This is used when stopping a containerized MCP so that the MCP record and
104+
its container are removed together.
105+
"""
106+
delete_mcp_record_by_container_id(
107+
container_id=container_id,
108+
tenant_id=tenant_id,
109+
user_id=user_id,
110+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE nexent.mcp_record_t
2+
ADD COLUMN IF NOT EXISTS container_id VARCHAR(200);
3+
4+
COMMENT ON COLUMN nexent.mcp_record_t.container_id IS 'Docker container ID for MCP service, NULL for non-containerized MCP';
5+
6+

0 commit comments

Comments
 (0)