agentscope-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. agentscope_runtime/__init__.py +4 -0
  2. agentscope_runtime/engine/__init__.py +9 -0
  3. agentscope_runtime/engine/agents/__init__.py +2 -0
  4. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
  5. agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
  6. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
  7. agentscope_runtime/engine/agents/agno_agent.py +220 -0
  8. agentscope_runtime/engine/agents/base_agent.py +29 -0
  9. agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
  10. agentscope_runtime/engine/agents/llm_agent.py +51 -0
  11. agentscope_runtime/engine/deployers/__init__.py +3 -0
  12. agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
  17. agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
  18. agentscope_runtime/engine/deployers/base.py +17 -0
  19. agentscope_runtime/engine/deployers/local_deployer.py +586 -0
  20. agentscope_runtime/engine/helpers/helper.py +127 -0
  21. agentscope_runtime/engine/llms/__init__.py +3 -0
  22. agentscope_runtime/engine/llms/base_llm.py +60 -0
  23. agentscope_runtime/engine/llms/qwen_llm.py +47 -0
  24. agentscope_runtime/engine/misc/__init__.py +0 -0
  25. agentscope_runtime/engine/runner.py +186 -0
  26. agentscope_runtime/engine/schemas/__init__.py +0 -0
  27. agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
  28. agentscope_runtime/engine/schemas/context.py +54 -0
  29. agentscope_runtime/engine/services/__init__.py +9 -0
  30. agentscope_runtime/engine/services/base.py +77 -0
  31. agentscope_runtime/engine/services/context_manager.py +129 -0
  32. agentscope_runtime/engine/services/environment_manager.py +50 -0
  33. agentscope_runtime/engine/services/manager.py +174 -0
  34. agentscope_runtime/engine/services/memory_service.py +270 -0
  35. agentscope_runtime/engine/services/sandbox_service.py +198 -0
  36. agentscope_runtime/engine/services/session_history_service.py +256 -0
  37. agentscope_runtime/engine/tracing/__init__.py +40 -0
  38. agentscope_runtime/engine/tracing/base.py +309 -0
  39. agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
  40. agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
  41. agentscope_runtime/engine/tracing/wrapper.py +321 -0
  42. agentscope_runtime/sandbox/__init__.py +14 -0
  43. agentscope_runtime/sandbox/box/__init__.py +0 -0
  44. agentscope_runtime/sandbox/box/base/__init__.py +0 -0
  45. agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
  46. agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
  47. agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
  48. agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
  50. agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
  51. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
  52. agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
  53. agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
  54. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
  55. agentscope_runtime/sandbox/box/sandbox.py +115 -0
  56. agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
  57. agentscope_runtime/sandbox/box/shared/app.py +44 -0
  58. agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
  59. agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
  60. agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
  61. agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
  62. agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
  64. agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
  65. agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
  66. agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
  67. agentscope_runtime/sandbox/box/training_box/base.py +120 -0
  68. agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
  69. agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
  70. agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
  71. agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
  72. agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
  73. agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
  74. agentscope_runtime/sandbox/build.py +213 -0
  75. agentscope_runtime/sandbox/client/__init__.py +5 -0
  76. agentscope_runtime/sandbox/client/http_client.py +527 -0
  77. agentscope_runtime/sandbox/client/training_client.py +265 -0
  78. agentscope_runtime/sandbox/constant.py +5 -0
  79. agentscope_runtime/sandbox/custom/__init__.py +16 -0
  80. agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
  81. agentscope_runtime/sandbox/custom/example.py +37 -0
  82. agentscope_runtime/sandbox/enums.py +68 -0
  83. agentscope_runtime/sandbox/manager/__init__.py +4 -0
  84. agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
  85. agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
  86. agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
  87. agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
  88. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
  89. agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
  90. agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
  91. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
  92. agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
  93. agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
  94. agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
  95. agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
  96. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
  97. agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
  98. agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
  99. agentscope_runtime/sandbox/manager/server/app.py +194 -0
  100. agentscope_runtime/sandbox/manager/server/config.py +68 -0
  101. agentscope_runtime/sandbox/manager/server/models.py +17 -0
  102. agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
  103. agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
  104. agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
  105. agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
  106. agentscope_runtime/sandbox/manager/utils.py +78 -0
  107. agentscope_runtime/sandbox/mcp_server.py +192 -0
  108. agentscope_runtime/sandbox/model/__init__.py +12 -0
  109. agentscope_runtime/sandbox/model/api.py +16 -0
  110. agentscope_runtime/sandbox/model/container.py +72 -0
  111. agentscope_runtime/sandbox/model/manager_config.py +158 -0
  112. agentscope_runtime/sandbox/registry.py +129 -0
  113. agentscope_runtime/sandbox/tools/__init__.py +12 -0
  114. agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
  115. agentscope_runtime/sandbox/tools/base/tool.py +52 -0
  116. agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
  117. agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
  118. agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
  119. agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
  120. agentscope_runtime/sandbox/tools/function_tool.py +321 -0
  121. agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
  122. agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
  123. agentscope_runtime/sandbox/tools/tool.py +123 -0
  124. agentscope_runtime/sandbox/tools/utils.py +68 -0
  125. agentscope_runtime/version.py +2 -0
  126. agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
  127. agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
  128. agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
  129. agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
  130. agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
  131. agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,87 @@
1
+ # -*- coding: utf-8 -*-
2
+ # pylint: disable=dangerous-default-value
3
+ from typing import Optional
4
+
5
+ from ...constant import IMAGE_TAG
6
+ from ...registry import SandboxRegistry
7
+ from ...enums import SandboxType
8
+ from ...box.sandbox import Sandbox
9
+
10
+
11
+ @SandboxRegistry.register(
12
+ f"agentscope/runtime-sandbox-filesystem:{IMAGE_TAG}",
13
+ sandbox_type=SandboxType.FILESYSTEM,
14
+ security_level="medium",
15
+ timeout=60,
16
+ description="Filesystem sandbox",
17
+ )
18
+ class FilesystemSandbox(Sandbox):
19
+ def __init__(
20
+ self,
21
+ sandbox_id: Optional[str] = None,
22
+ timeout: int = 3000,
23
+ base_url: Optional[str] = None,
24
+ bearer_token: Optional[str] = None,
25
+ ):
26
+ super().__init__(
27
+ sandbox_id,
28
+ timeout,
29
+ base_url,
30
+ bearer_token,
31
+ SandboxType.FILESYSTEM,
32
+ )
33
+
34
+ def read_file(self, path: str):
35
+ return self.call_tool("read_file", {"path": path})
36
+
37
+ def read_multiple_files(self, paths: list):
38
+ return self.call_tool("read_multiple_files", {"paths": paths})
39
+
40
+ def write_file(self, path: str, content: str):
41
+ return self.call_tool("write_file", {"path": path, "content": content})
42
+
43
+ def edit_file(self, path: str, edits: list, dry_run: bool = False):
44
+ return self.call_tool(
45
+ "edit_file",
46
+ {
47
+ "path": path,
48
+ "edits": edits,
49
+ "dryRun": dry_run,
50
+ },
51
+ )
52
+
53
+ def create_directory(self, path: str):
54
+ return self.call_tool("create_directory", {"path": path})
55
+
56
+ def list_directory(self, path: str):
57
+ return self.call_tool("list_directory", {"path": path})
58
+
59
+ def directory_tree(self, path: str):
60
+ return self.call_tool("directory_tree", {"path": path})
61
+
62
+ def move_file(self, source: str, destination: str):
63
+ return self.call_tool(
64
+ "move_file",
65
+ {"source": source, "destination": destination},
66
+ )
67
+
68
+ def search_files(
69
+ self,
70
+ path: str,
71
+ pattern: str,
72
+ exclude_patterns: list = [],
73
+ ):
74
+ return self.call_tool(
75
+ "search_files",
76
+ {
77
+ "path": path,
78
+ "pattern": pattern,
79
+ "excludePatterns": exclude_patterns,
80
+ },
81
+ )
82
+
83
+ def get_file_info(self, path: str):
84
+ return self.call_tool("get_file_info", {"path": path})
85
+
86
+ def list_allowed_directories(self):
87
+ return self.call_tool("list_allowed_directories", {})
@@ -0,0 +1,115 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+ from typing import Any, Optional
4
+
5
+ from ..enums import SandboxType
6
+ from ..manager.sandbox_manager import SandboxManager
7
+
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Sandbox:
14
+ """
15
+ Sandbox Interface.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ sandbox_id: Optional[str] = None,
21
+ timeout: int = 3000, # TODO: enable life circle management
22
+ base_url: Optional[str] = None,
23
+ bearer_token: Optional[str] = None, # TODO: support api_key
24
+ sandbox_type: SandboxType = SandboxType.BASE,
25
+ ) -> None:
26
+ """
27
+ Initialize the sandbox interface.
28
+ """
29
+ if base_url:
30
+ self.embed_mode = False
31
+ self.manager_api = SandboxManager(
32
+ base_url=base_url,
33
+ bearer_token=bearer_token,
34
+ ).__enter__()
35
+ else:
36
+ # Launch a local manager
37
+ self.embed_mode = True
38
+ self.manager_api = SandboxManager(
39
+ default_type=sandbox_type,
40
+ )
41
+
42
+ if sandbox_id is None:
43
+ logger.debug(
44
+ "You are using embed mode, it might take several seconds to "
45
+ "init the runtime, please wait.",
46
+ )
47
+
48
+ sandbox_id = self.manager_api.create_from_pool(
49
+ sandbox_type=sandbox_type.value,
50
+ )
51
+ if sandbox_id is None:
52
+ raise RuntimeError(
53
+ "No sandbox available. "
54
+ "Please check if sandbox images exist, build or pull "
55
+ "missing images in sandbox server.",
56
+ )
57
+ self._sandbox_id = sandbox_id
58
+
59
+ self._sandbox_id = sandbox_id
60
+ self.sandbox_type = sandbox_type
61
+ self.timeout = timeout
62
+
63
+ def __enter__(self):
64
+ return self
65
+
66
+ def __exit__(self, exc_type, exc_value, traceback):
67
+ # Remote not need to close the embed_manager
68
+ if self.embed_mode:
69
+ # Clean all
70
+ self.manager_api.__exit__(exc_type, exc_value, traceback)
71
+ else:
72
+ # Clean the specific sandbox
73
+ self.manager_api.release(self.sandbox_id)
74
+
75
+ @property
76
+ def sandbox_id(self) -> str:
77
+ """Get the sandbox ID."""
78
+ return self._sandbox_id
79
+
80
+ @sandbox_id.setter
81
+ def sandbox_id(self, value: str) -> None:
82
+ """Set the sandbox ID."""
83
+ if not value:
84
+ raise ValueError("Sandbox ID cannot be empty.")
85
+ self._sandbox_id = value
86
+
87
+ def get_info(self) -> dict:
88
+ return self.manager_api.get_info(self.sandbox_id)
89
+
90
+ def list_tools(self, tool_type: Optional[str] = None) -> dict:
91
+ return self.manager_api.list_tools(
92
+ self.sandbox_id,
93
+ tool_type=tool_type,
94
+ )
95
+
96
+ def call_tool(
97
+ self,
98
+ name: str,
99
+ arguments: Optional[dict[str, Any]] = None,
100
+ ) -> Any:
101
+ if arguments is None:
102
+ arguments = {}
103
+
104
+ return self.manager_api.call_tool(self.sandbox_id, name, arguments)
105
+
106
+ def add_mcp_servers(
107
+ self,
108
+ server_configs: dict,
109
+ overwrite=False,
110
+ ):
111
+ return self.manager_api.add_mcp_servers(
112
+ self.sandbox_id,
113
+ server_configs,
114
+ overwrite,
115
+ )
File without changes
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+
4
+ from fastapi import FastAPI, Response, Depends
5
+ from routers import (
6
+ generic_router,
7
+ mcp_router,
8
+ watcher_router,
9
+ workspace_router,
10
+ )
11
+ from dependencies import verify_secret_token
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI(
18
+ title="AgentScope Runtime Sandbox Server",
19
+ version="1.0",
20
+ description="Agentscope runtime sandbox server.",
21
+ )
22
+
23
+
24
+ @app.get(
25
+ "/healthz",
26
+ summary="Check the health of the API",
27
+ dependencies=[Depends(verify_secret_token)],
28
+ )
29
+ async def healthz():
30
+ return Response(content="OK", status_code=200)
31
+
32
+
33
+ app.include_router(mcp_router, dependencies=[Depends(verify_secret_token)])
34
+ app.include_router(generic_router, dependencies=[Depends(verify_secret_token)])
35
+ app.include_router(watcher_router, dependencies=[Depends(verify_secret_token)])
36
+ app.include_router(
37
+ workspace_router,
38
+ dependencies=[Depends(verify_secret_token)],
39
+ )
40
+
41
+ if __name__ == "__main__":
42
+ import uvicorn
43
+
44
+ uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .deps import verify_secret_token
3
+
4
+
5
+ __all__ = ["verify_secret_token"]
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+
4
+ from typing import Optional
5
+ from fastapi import Header, HTTPException, status
6
+
7
+ SECRET_TOKEN = os.getenv("SECRET_TOKEN", "secret_token123")
8
+
9
+
10
+ async def verify_secret_token(authorization: Optional[str] = Header(None)):
11
+ if authorization is None or not authorization.startswith("Bearer "):
12
+ raise HTTPException(
13
+ status_code=status.HTTP_403_FORBIDDEN,
14
+ detail="Missing or invalid authorization header",
15
+ )
16
+
17
+ token = authorization.split("Bearer ")[1]
18
+ if token != SECRET_TOKEN:
19
+ raise HTTPException(
20
+ status_code=status.HTTP_403_FORBIDDEN,
21
+ detail="Invalid secret token",
22
+ )
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .generic import generic_router
3
+ from .mcp import mcp_router
4
+ from .runtime_watcher import watcher_router
5
+ from .workspace import workspace_router
6
+
7
+ __all__ = [
8
+ "mcp_router",
9
+ "generic_router",
10
+ "watcher_router",
11
+ "workspace_router",
12
+ ]
@@ -0,0 +1,173 @@
1
+ # -*- coding: utf-8 -*-
2
+ import io
3
+ import logging
4
+ import subprocess
5
+ import traceback
6
+ from contextlib import redirect_stderr, redirect_stdout
7
+
8
+ from fastapi import APIRouter, Body, HTTPException
9
+ from IPython.core.interactiveshell import InteractiveShell
10
+ from mcp.types import CallToolResult, TextContent
11
+
12
+ SPLIT_OUTPUT_MODE = True
13
+
14
+
15
+ generic_router = APIRouter()
16
+
17
+ # Initialize IPython shell
18
+ ipy = InteractiveShell.instance()
19
+
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @generic_router.post(
25
+ "/tools/run_ipython_cell",
26
+ summary="Invoke a cell in a stateful IPython (Jupyter) kernel",
27
+ )
28
+ async def run_ipython_cell(
29
+ code: str = Body(
30
+ ...,
31
+ example="print('Hello World')",
32
+ embed=True,
33
+ ),
34
+ ):
35
+ """
36
+ Execute code in an IPython kernel and return the results.
37
+ """
38
+ try:
39
+ if not code:
40
+ raise HTTPException(status_code=400, detail="Code is required.")
41
+
42
+ # Capture stdout and stderr separately
43
+ stdout_buf = io.StringIO()
44
+ stderr_buf = io.StringIO()
45
+
46
+ with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
47
+ ipy.run_cell(code)
48
+
49
+ stdout_content = stdout_buf.getvalue()
50
+ stderr_content = stderr_buf.getvalue()
51
+
52
+ content_list = []
53
+
54
+ if SPLIT_OUTPUT_MODE:
55
+ content_list.append(
56
+ TextContent(
57
+ type="text",
58
+ text=stdout_content,
59
+ description="stdout",
60
+ ),
61
+ )
62
+
63
+ if stderr_content:
64
+ content_list.append(
65
+ TextContent(
66
+ type="text",
67
+ text=stderr_content,
68
+ description="stderr",
69
+ ),
70
+ )
71
+ else:
72
+ content_list.append(
73
+ TextContent(
74
+ type="text",
75
+ text=stdout_content + "\n" + stderr_content,
76
+ description="output",
77
+ ),
78
+ )
79
+
80
+ is_error = bool(stderr_content)
81
+
82
+ return CallToolResult(
83
+ content=content_list,
84
+ isError=is_error,
85
+ ).model_dump()
86
+
87
+ except Exception as e:
88
+ raise HTTPException(
89
+ status_code=500,
90
+ detail=f"{str(e)}: {traceback.format_exc()}",
91
+ ) from e
92
+
93
+
94
+ @generic_router.post(
95
+ "/tools/run_shell_command",
96
+ summary="Invoke a shell command.",
97
+ )
98
+ async def run_shell_command(
99
+ command: str = Body(
100
+ ...,
101
+ example="pwd",
102
+ embed=True,
103
+ ),
104
+ ):
105
+ """
106
+ Execute a shell command and return the results.
107
+ """
108
+ try:
109
+ if not command:
110
+ raise HTTPException(status_code=400, detail="Command is required.")
111
+
112
+ result = subprocess.run(
113
+ command,
114
+ shell=True,
115
+ stdout=subprocess.PIPE,
116
+ stderr=subprocess.PIPE,
117
+ text=True,
118
+ check=False,
119
+ )
120
+ stdout_content = result.stdout
121
+ stderr_content = result.stderr
122
+
123
+ content_list = []
124
+
125
+ if SPLIT_OUTPUT_MODE:
126
+ content_list.append(
127
+ TextContent(
128
+ type="text",
129
+ text=stdout_content,
130
+ description="stdout",
131
+ ),
132
+ )
133
+
134
+ if stderr_content:
135
+ content_list.append(
136
+ TextContent(
137
+ type="text",
138
+ text=stderr_content,
139
+ description="stderr",
140
+ ),
141
+ )
142
+ content_list.append(
143
+ TextContent(
144
+ type="text",
145
+ text=str(result.returncode),
146
+ description="returncode",
147
+ ),
148
+ )
149
+ else:
150
+ content_list.append(
151
+ TextContent(
152
+ type="text",
153
+ text=stdout_content
154
+ + "\n"
155
+ + stderr_content
156
+ + "\n"
157
+ + str(result.returncode),
158
+ description="output",
159
+ ),
160
+ )
161
+
162
+ is_error = bool(stderr_content)
163
+
164
+ return CallToolResult(
165
+ content=content_list,
166
+ isError=is_error,
167
+ ).model_dump()
168
+
169
+ except Exception as e:
170
+ raise HTTPException(
171
+ status_code=500,
172
+ detail=f"{str(e)}: {traceback.format_exc()}",
173
+ ) from e
@@ -0,0 +1,207 @@
1
+ # -*- coding: utf-8 -*-
2
+ import copy
3
+ import json
4
+ import logging
5
+ import os
6
+ import traceback
7
+
8
+ from fastapi import APIRouter, Body, HTTPException, Response
9
+
10
+ from .mcp_utils import MCPSessionHandler
11
+
12
+ mcp_router = APIRouter()
13
+
14
+ _MCP_SERVERS = {}
15
+ current_directory = os.path.dirname(os.path.abspath(__file__))
16
+ mcp_server_configs_path = os.path.abspath(
17
+ os.path.join(current_directory, "../mcp_server_configs.json"),
18
+ )
19
+
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ # NOTE: DO NOT use API-KEY Server in release version due to security issues
25
+ @mcp_router.post(
26
+ "/mcp/add_servers",
27
+ summary="Add and initialize MCP servers",
28
+ )
29
+ async def add_servers(
30
+ server_configs: dict = Body(
31
+ {},
32
+ embed=True,
33
+ ),
34
+ overwrite: bool = Body(
35
+ False,
36
+ embed=True,
37
+ ),
38
+ ):
39
+ global _MCP_SERVERS
40
+
41
+ try:
42
+ if not server_configs:
43
+ raise HTTPException(
44
+ status_code=400,
45
+ detail="server_configs is required.",
46
+ )
47
+
48
+ new_servers = [
49
+ MCPSessionHandler(name, config)
50
+ for name, config in server_configs["mcpServers"].items()
51
+ ]
52
+
53
+ fail_servers = []
54
+
55
+ # Initialize the servers
56
+ for server in new_servers:
57
+ if server.name in _MCP_SERVERS:
58
+ if not overwrite:
59
+ continue
60
+ # Cleanup old server
61
+ await _MCP_SERVERS.pop(server.name).cleanup()
62
+ try:
63
+ await server.initialize()
64
+ _MCP_SERVERS[server.name] = server
65
+ except Exception as e:
66
+ logging.error(f"Failed to initialize server: {e}")
67
+ fail_servers.append(server)
68
+ continue
69
+
70
+ if fail_servers:
71
+ for server in fail_servers:
72
+ await server.cleanup()
73
+ raise HTTPException(
74
+ status_code=500,
75
+ detail=f"Failed to initialize server: "
76
+ f"{[server.name for server in fail_servers]}",
77
+ )
78
+ return Response(content="OK", status_code=200)
79
+ except Exception as e:
80
+ raise HTTPException(
81
+ status_code=500,
82
+ detail=f"{str(e)}: {traceback.format_exc()}",
83
+ ) from e
84
+
85
+
86
+ @mcp_router.get(
87
+ "/mcp/list_tools",
88
+ summary="List MCP tools",
89
+ )
90
+ async def list_tools():
91
+ try:
92
+ mcp_tools = {}
93
+
94
+ for server_name, server in _MCP_SERVERS.items():
95
+ tools = await server.list_tools()
96
+ server_tools = {}
97
+ for tool in tools:
98
+ name = tool.name
99
+ if name in server_tools:
100
+ logging.warning(
101
+ f"Service function `{name}` already exists, "
102
+ f"skip adding it.",
103
+ )
104
+ else:
105
+ json_schema = {
106
+ "type": "function",
107
+ "function": {
108
+ "name": tool.name,
109
+ "description": tool.description,
110
+ "parameters": {
111
+ "type": "object",
112
+ "properties": tool.inputSchema.get(
113
+ "properties",
114
+ {},
115
+ ),
116
+ "required": tool.inputSchema.get(
117
+ "required",
118
+ [],
119
+ ),
120
+ },
121
+ },
122
+ }
123
+ server_tools[tool.name] = {
124
+ "name": tool.name,
125
+ "json_schema": json_schema,
126
+ }
127
+ mcp_tools[server_name] = copy.deepcopy(server_tools)
128
+ return mcp_tools
129
+ except Exception as e:
130
+ raise HTTPException(
131
+ status_code=500,
132
+ detail=f"{str(e)}: {traceback.format_exc()}",
133
+ ) from e
134
+
135
+
136
+ @mcp_router.post(
137
+ "/mcp/call_tool",
138
+ summary="Execute MCP tool",
139
+ )
140
+ async def call_tool(
141
+ tool_name: str = Body(
142
+ ...,
143
+ embed=True,
144
+ ),
145
+ arguments: dict = Body(
146
+ {},
147
+ embed=True,
148
+ ),
149
+ ) -> None:
150
+ try:
151
+ if not tool_name:
152
+ raise HTTPException(
153
+ status_code=400,
154
+ detail="tool_name is required.",
155
+ )
156
+
157
+ tools = await list_tools()
158
+ for server_name, server_tools in tools.items():
159
+ if tool_name not in server_tools:
160
+ continue
161
+ server = _MCP_SERVERS[server_name]
162
+ result = await server.call_tool(tool_name, arguments)
163
+ return result.model_dump()
164
+ raise ModuleNotFoundError(f"Tool '{tool_name}' not found.")
165
+ except Exception as e:
166
+ raise HTTPException(
167
+ status_code=500,
168
+ detail=f"{str(e)}: {traceback.format_exc()}",
169
+ ) from e
170
+
171
+
172
+ @mcp_router.on_event("shutdown")
173
+ async def cleanup_servers() -> None:
174
+ """Clean up all servers properly."""
175
+ global _MCP_SERVERS
176
+
177
+ for server in reversed(list(_MCP_SERVERS.values())):
178
+ try:
179
+ await server.cleanup()
180
+ except Exception as e:
181
+ logging.error(f"Failed to cleanup server: {e}")
182
+
183
+ _MCP_SERVERS = {}
184
+
185
+
186
+ @mcp_router.on_event("startup")
187
+ async def startup_event():
188
+ # Load MCP server configs
189
+ try:
190
+ with open(mcp_server_configs_path, "r", encoding="utf-8") as file:
191
+ mcp_server_configs = json.load(file)
192
+
193
+ except Exception as e:
194
+ logger.error(f"Failed to load MCP server configs: {e}")
195
+ mcp_server_configs = {}
196
+
197
+ # Call the add_servers function
198
+ if mcp_server_configs:
199
+ try:
200
+ await add_servers(
201
+ server_configs=mcp_server_configs,
202
+ overwrite=False,
203
+ )
204
+ except Exception as e:
205
+ logger.error(
206
+ f"Failed to add MCP servers: {e}, {traceback.format_exc()}",
207
+ )