Compare commits
2 Commits
f625154aee
...
28639f9cbf
| Author | SHA1 | Date | |
|---|---|---|---|
| 28639f9cbf | |||
| 41a6038e50 |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,5 +3,5 @@
|
|||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.12" />
|
<option name="sdkName" value="Python 3.12" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (test1)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
2
.idea/solidstate-tools.iml
generated
2
.idea/solidstate-tools.iml
generated
@@ -2,7 +2,7 @@
|
|||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="jdk" jdkName="Python 3.12 (test1)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
50
mcp/lifespan.py
Normal file
50
mcp/lifespan.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import asyncssh
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
# --- 1. SSH 连接参数 ---
|
||||||
|
REMOTE_HOST = '202.121.182.208'
|
||||||
|
REMOTE_USER = 'koko125'
|
||||||
|
# 确保路径正确,Windows 路径建议使用正斜杠 '/'
|
||||||
|
PRIVATE_KEY_PATH = 'D:/tool/tool/id_rsa.txt'
|
||||||
|
INITIAL_WORKING_DIRECTORY = f'/cluster/home/{REMOTE_USER}/sandbox'
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2. 定义共享上下文 ---
|
||||||
|
# 这个数据类将持有所有模块需要共享的资源
|
||||||
|
@dataclass
|
||||||
|
class SharedAppContext:
|
||||||
|
"""在整个服务器生命周期内持有的共享资源"""
|
||||||
|
ssh_connection: asyncssh.SSHClientConnection
|
||||||
|
sandbox_path: str
|
||||||
|
|
||||||
|
|
||||||
|
# --- 3. 创建生命周期管理器 ---
|
||||||
|
# 这是整个架构的核心,负责在服务器启动时连接SSH,在关闭时断开
|
||||||
|
@asynccontextmanager
|
||||||
|
async def shared_lifespan(app: Starlette) -> AsyncIterator[SharedAppContext]:
|
||||||
|
"""
|
||||||
|
为整个应用管理共享资源的生命周期。
|
||||||
|
"""
|
||||||
|
print("主应用启动,正在建立共享 SSH 连接...")
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
# 建立 SSH 连接
|
||||||
|
conn = await asyncssh.connect(
|
||||||
|
REMOTE_HOST,
|
||||||
|
username=REMOTE_USER,
|
||||||
|
client_keys=[PRIVATE_KEY_PATH]
|
||||||
|
)
|
||||||
|
print(f"SSH 连接到 {REMOTE_HOST} 成功!")
|
||||||
|
|
||||||
|
# 使用 yield 将创建好的共享资源上下文传递给 Starlette 应用
|
||||||
|
# 服务器会在此处暂停并开始处理请求
|
||||||
|
yield {"shared_context": SharedAppContext(ssh_connection=conn, sandbox_path=INITIAL_WORKING_DIRECTORY)}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 当服务器关闭时,yield 之后的代码会被执行
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
await conn.wait_closed()
|
||||||
|
print("共享 SSH 连接已关闭。")
|
||||||
28
mcp/main.py
Normal file
28
mcp/main.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# main.py
|
||||||
|
import contextlib
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.routing import Mount
|
||||||
|
|
||||||
|
from test_tools import create_test_mcp
|
||||||
|
from system_tools import create_system_mcp # 如果暂时不用可先不挂
|
||||||
|
|
||||||
|
# 先创建 MCP 实例
|
||||||
|
test_mcp = create_test_mcp()
|
||||||
|
# system_mcp = create_system_mcp()
|
||||||
|
|
||||||
|
# 关键:在 Starlette 的 lifespan 中启动 MCP 的 session manager
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def lifespan(app: Starlette):
|
||||||
|
async with contextlib.AsyncExitStack() as stack:
|
||||||
|
await stack.enter_async_context(test_mcp.session_manager.run())
|
||||||
|
# await stack.enter_async_context(system_mcp.session_manager.run())
|
||||||
|
yield # 服务器运行期间
|
||||||
|
# 退出时自动清理
|
||||||
|
|
||||||
|
app = Starlette(
|
||||||
|
lifespan=lifespan,
|
||||||
|
routes=[
|
||||||
|
Mount("/test", app=test_mcp.streamable_http_app()),
|
||||||
|
# Mount("/system", app=system_mcp.streamable_http_app()),
|
||||||
|
],
|
||||||
|
)
|
||||||
114
mcp/system_tools.py
Normal file
114
mcp/system_tools.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# system_tools.py
|
||||||
|
import os
|
||||||
|
import asyncssh
|
||||||
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
from lifespan import SharedAppContext # 导入共享上下文的类型定义
|
||||||
|
|
||||||
|
|
||||||
|
def create_system_mcp() -> FastMCP:
|
||||||
|
"""创建一个包含系统操作工具的 MCP 实例。"""
|
||||||
|
|
||||||
|
system_mcp = FastMCP(
|
||||||
|
name="System Tools",
|
||||||
|
instructions="用于在远程服务器上进行基本文件和目录操作的工具集。",
|
||||||
|
# 将 MCP 实例的 HTTP 路径设置为根,以便挂载在 /system 下 [1]
|
||||||
|
streamable_http_path="/"
|
||||||
|
)
|
||||||
|
|
||||||
|
@system_mcp.tool()
|
||||||
|
async def list_files(ctx: Context[ServerSession, dict], path: str = ".") -> str:
|
||||||
|
"""
|
||||||
|
列出远程服务器安全沙箱路径下的文件和目录。
|
||||||
|
Args:
|
||||||
|
path: 要查看的相对路径,默认为当前沙箱目录。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从上下文中获取共享的 SSH 连接和沙箱路径 [1]
|
||||||
|
shared_ctx: SharedAppContext = ctx.request_context.lifespan_context["shared_context"]
|
||||||
|
|
||||||
|
conn = shared_ctx.ssh_connection
|
||||||
|
sandbox_path = shared_ctx.sandbox_path
|
||||||
|
|
||||||
|
# 安全检查:防止目录穿越攻击
|
||||||
|
if ".." in path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
# 安全地拼接路径
|
||||||
|
target_path = os.path.join(sandbox_path, path)
|
||||||
|
await ctx.info(f"正在列出安全路径: {target_path}")
|
||||||
|
|
||||||
|
# 使用 conn.run() 执行 ls -l 命令
|
||||||
|
command_to_run = f'ls -la {conn.escape(target_path)}'
|
||||||
|
result = await conn.run(command_to_run, check=True)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except asyncssh.ProcessError as e:
|
||||||
|
error_message = f"命令执行失败: {e.stderr.strip()}"
|
||||||
|
await ctx.error(error_message)
|
||||||
|
return error_message
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"执行 ls 命令时发生未知错误: {e}"
|
||||||
|
await ctx.error(error_message)
|
||||||
|
return error_message
|
||||||
|
|
||||||
|
@system_mcp.tool()
|
||||||
|
async def read_file(ctx: Context[ServerSession, dict], file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
读取远程服务器安全沙箱内指定文件的内容。
|
||||||
|
Args:
|
||||||
|
file_path: 相对于沙箱目录的文件路径。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
shared_ctx: SharedAppContext = ctx.request_context.lifespan_context["shared_context"]
|
||||||
|
|
||||||
|
conn = shared_ctx.ssh_connection
|
||||||
|
sandbox_path = shared_ctx.sandbox_path
|
||||||
|
# 安全检查:防止目录穿越
|
||||||
|
if ".." in file_path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
target_path = os.path.join(sandbox_path, file_path)
|
||||||
|
await ctx.info(f"正在读取远程文件: {target_path}")
|
||||||
|
|
||||||
|
async with conn.start_sftp_client() as sftp:
|
||||||
|
async with sftp.open(target_path, 'r') as f:
|
||||||
|
content = await f.read()
|
||||||
|
return content
|
||||||
|
except FileNotFoundError:
|
||||||
|
error_message = f"读取文件失败: 文件 '{file_path}' 不存在。"
|
||||||
|
await ctx.error(error_message)
|
||||||
|
return error_message
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.error(f"读取文件时发生未知错误: {e}")
|
||||||
|
return f"错误: {e}"
|
||||||
|
|
||||||
|
@system_mcp.tool()
|
||||||
|
async def write_file(ctx: Context[ServerSession, dict], file_path: str, content: str) -> str:
|
||||||
|
"""
|
||||||
|
向远程服务器安全沙箱内的文件写入内容。如果文件已存在,则会覆盖它。
|
||||||
|
Args:
|
||||||
|
file_path: 相对于沙箱目录的文件路径。
|
||||||
|
content: 要写入文件的文本内容。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
shared_ctx: SharedAppContext = ctx.request_context.lifespan_context["shared_context"]
|
||||||
|
|
||||||
|
conn = shared_ctx.ssh_connection
|
||||||
|
sandbox_path = shared_ctx.sandbox_path
|
||||||
|
|
||||||
|
# 安全检查
|
||||||
|
if ".." in file_path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
target_path = os.path.join(sandbox_path, file_path)
|
||||||
|
await ctx.info(f"正在向远程文件写入: {target_path}")
|
||||||
|
|
||||||
|
async with conn.start_sftp_client() as sftp:
|
||||||
|
async with sftp.open(target_path, 'w') as f:
|
||||||
|
await f.write(content)
|
||||||
|
return f"内容已成功写入到沙箱文件 '{file_path}'。"
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.error(f"写入文件时发生未知错误: {e}")
|
||||||
|
return f"错误: {e}"
|
||||||
|
|
||||||
|
return system_mcp
|
||||||
19
mcp/test_tools.py
Normal file
19
mcp/test_tools.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# test_tools.py
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_mcp() -> FastMCP:
|
||||||
|
"""创建一个只包含最简单工具的 MCP 实例,用于测试连接。"""
|
||||||
|
|
||||||
|
test_mcp = FastMCP(
|
||||||
|
name="Test Tools",
|
||||||
|
instructions="用于测试服务器连接是否通畅。",
|
||||||
|
streamable_http_path="/"
|
||||||
|
)
|
||||||
|
|
||||||
|
@test_mcp.tool()
|
||||||
|
def ping() -> str:
|
||||||
|
"""一个简单的工具,用于确认服务器是否响应。"""
|
||||||
|
return "pong"
|
||||||
|
|
||||||
|
return test_mcp
|
||||||
Reference in New Issue
Block a user