mcp-change
This commit is contained in:
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.11" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (test1)" 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.11" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.12 (test1)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
227
mcp/server.py
227
mcp/server.py
@@ -1,56 +1,221 @@
|
|||||||
# server.py
|
import asyncssh
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import asyncssh # 用于 SSH 连接的库
|
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
# --- 预设的服务器连接信息 ---
|
# --- 1. 使用您提供的参数 ---
|
||||||
# 警告:在生产环境中,不要将密钥和密码硬编码在代码里。
|
REMOTE_HOST = '202.121.182.208'
|
||||||
# 最好使用环境变量、配置文件或专门的密钥管理服务。
|
REMOTE_USER = 'koko125'
|
||||||
REMOTE_HOST = '202.121.182.208' # 替换为你的服务器地址
|
PRIVATE_KEY_PATH = 'D:/tool/tool/id_rsa.txt' # Windows 路径建议使用 /
|
||||||
REMOTE_USER = 'koko125' # 替换为你的用户名
|
INITIAL_WORKING_DIRECTORY = '/cluster/home/koko125/sandbox'
|
||||||
PRIVATE_KEY_PATH = 'D:/tool/tool/id_rsa.txt' # 替换为你的私钥文件路径
|
|
||||||
|
|
||||||
# 创建一个 MCP 服务器实例
|
|
||||||
mcp = FastMCP("远程服务器工具")
|
|
||||||
|
|
||||||
|
# --- 2. 生命周期管理部分 (高级功能) ---
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppContext:
|
||||||
|
"""用于在服务器生命周期内持有共享资源的上下文对象"""
|
||||||
|
ssh_connection: asyncssh.SSHClientConnection
|
||||||
|
current_path: str
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
||||||
|
"""在服务器启动时建立SSH连接,在关闭时断开。"""
|
||||||
|
print("服务器启动中,正在建立SSH连接...")
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = await asyncssh.connect(
|
||||||
|
REMOTE_HOST,
|
||||||
|
username=REMOTE_USER,
|
||||||
|
client_keys=[PRIVATE_KEY_PATH]
|
||||||
|
)
|
||||||
|
print("SSH 连接成功!")
|
||||||
|
|
||||||
|
# <<< 在登录时执行 source bashrc 命令 >>>
|
||||||
|
# 注意:bashrc 的效果(如环境变量)只会对这一个 conn.run() 调用生效。
|
||||||
|
# 对于非交互式shell,更好的做法是把需要执行的命令串联起来。
|
||||||
|
# 但如果只是为了加载路径等,可以在后续命令中体现。
|
||||||
|
print("正在加载 .bashrc...")
|
||||||
|
# 为了让 bashrc 生效,我们需要在一个交互式 shell 中执行命令
|
||||||
|
await conn.run('source /cluster/home/koko125/.bashrc', check=True)
|
||||||
|
print(".bashrc 加载完成。")
|
||||||
|
|
||||||
|
print(f"初始工作目录设置为: {INITIAL_WORKING_DIRECTORY}")
|
||||||
|
yield AppContext(ssh_connection=conn, current_path=INITIAL_WORKING_DIRECTORY)
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
await conn.wait_closed()
|
||||||
|
print("SSH 连接已关闭。")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 3. 创建 MCP 服务器实例,并应用生命周期管理 ---
|
||||||
|
mcp = FastMCP("远程服务器工具", lifespan=app_lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
# --- 4. 保留的调试工具 (不安全) ---
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def execute_remote_command(command: str, ctx: Context) -> str:
|
async def execute_remote_command(command: str, ctx: Context) -> str:
|
||||||
"""
|
"""
|
||||||
在远程服务器上执行一个shell命令并返回其输出。
|
【调试用】在远程服务器上执行一个任意的shell命令并返回其输出。
|
||||||
|
警告:此工具有安全风险,请勿在生产环境中使用。
|
||||||
Args:
|
|
||||||
command: 要在远程服务器上执行的命令字符串。
|
|
||||||
"""
|
"""
|
||||||
await ctx.info(f"准备在 {REMOTE_HOST} 上执行命令: '{command}'")
|
await ctx.info(f"准备在 {REMOTE_HOST} 上执行调试命令: '{command}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 建立 SSH 连接
|
# 每次都创建一个新连接,以确保环境隔离
|
||||||
async with asyncssh.connect(REMOTE_HOST, username=REMOTE_USER, client_keys=[PRIVATE_KEY_PATH]) as conn:
|
async with asyncssh.connect(REMOTE_HOST, username=REMOTE_USER, client_keys=[PRIVATE_KEY_PATH]) as conn:
|
||||||
# 执行命令
|
# 在执行命令前先 source bashrc
|
||||||
result = await conn.run(command, check=True)
|
full_command = f'source /cluster/home/koko125/.bashrc && {command}'
|
||||||
|
result = await conn.run(full_command, check=True)
|
||||||
# 成功执行,返回标准输出
|
|
||||||
output = result.stdout.strip()
|
output = result.stdout.strip()
|
||||||
await ctx.info(f"命令成功执行,返回输出。")
|
await ctx.info("调试命令成功执行。")
|
||||||
await ctx.debug(f"输出内容: {output}")
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
except asyncssh.ProcessError as e:
|
except asyncssh.ProcessError as e:
|
||||||
# 命令执行出错(例如,命令本身返回了非零退出码)
|
|
||||||
error_message = f"命令执行失败: {e.stderr.strip()}"
|
error_message = f"命令执行失败: {e.stderr.strip()}"
|
||||||
await ctx.error(error_message)
|
await ctx.error(error_message)
|
||||||
return error_message
|
return error_message
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 其他连接错误等
|
|
||||||
error_message = f"发生未知错误: {str(e)}"
|
error_message = f"发生未知错误: {str(e)}"
|
||||||
await ctx.error(error_message)
|
await ctx.error(error_message)
|
||||||
return error_message
|
return error_message
|
||||||
|
|
||||||
|
|
||||||
# 这部分使得你可以直接运行 `python server.py`
|
# --- 5. 新增的规范、安全的工具 ---
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def list_files_in_sandbox(ctx: Context[ServerSession, AppContext], path: str = ".") -> str:
|
||||||
|
"""
|
||||||
|
【安全】列出远程服务器安全沙箱路径下的文件和目录。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: 要查看的相对路径,默认为当前沙箱目录。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_ctx = ctx.request_context.lifespan_context
|
||||||
|
conn = app_ctx.ssh_connection
|
||||||
|
|
||||||
|
# 防止目录穿越攻击
|
||||||
|
if ".." in path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
# 安全地拼接路径
|
||||||
|
target_path = os.path.join(app_ctx.current_path, path)
|
||||||
|
|
||||||
|
await ctx.info(f"正在列出安全路径: {target_path}")
|
||||||
|
|
||||||
|
# 为了让 .bashrc 的设置(如别名)生效,最好也加上 source
|
||||||
|
command_to_run = f'source /cluster/home/koko125/.bashrc && ls -l {conn.escape(target_path)}'
|
||||||
|
result = await conn.run(command_to_run, check=True)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.error(f"执行 ls 命令失败: {e}")
|
||||||
|
return f"错误: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def create_directory_in_sandbox(ctx: Context[ServerSession, AppContext], directory_name: str) -> str:
|
||||||
|
"""
|
||||||
|
【安全】在远程服务器的安全沙箱内创建一个新目录。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory_name: 要创建的目录的名称。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_ctx = ctx.request_context.lifespan_context
|
||||||
|
conn = app_ctx.ssh_connection
|
||||||
|
|
||||||
|
# 安全检查:确保目录名不包含斜杠或 "..",以防止创建深层目录或进行目录穿越
|
||||||
|
if '/' in directory_name or ".." in directory_name:
|
||||||
|
return "错误: 目录名无效。不能包含 '/' 或 '..'。"
|
||||||
|
|
||||||
|
target_path = os.path.join(app_ctx.current_path, directory_name)
|
||||||
|
|
||||||
|
await ctx.info(f"正在创建远程目录: {target_path}")
|
||||||
|
|
||||||
|
command_to_run = f'source /cluster/home/koko125/.bashrc && mkdir {conn.escape(target_path)}'
|
||||||
|
await conn.run(command_to_run, check=True)
|
||||||
|
|
||||||
|
return f"目录 '{directory_name}' 在沙箱中成功创建。"
|
||||||
|
except asyncssh.ProcessError as e:
|
||||||
|
error_message = f"创建目录失败: {e.stderr.strip()}"
|
||||||
|
await ctx.error(error_message)
|
||||||
|
return error_message
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.error(f"创建目录时发生未知错误: {e}")
|
||||||
|
return f"错误: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def read_file_in_sandbox(ctx: Context[ServerSession, AppContext], file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
【安全】读取远程服务器安全沙箱内指定文件的内容。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于沙箱目录的文件路径。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_ctx = ctx.request_context.lifespan_context
|
||||||
|
conn = app_ctx.ssh_connection
|
||||||
|
|
||||||
|
# 安全检查:防止目录穿越
|
||||||
|
if ".." in file_path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
target_path = os.path.join(app_ctx.current_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}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def write_file_in_sandbox(ctx: Context[ServerSession, AppContext], file_path: str, content: str) -> str:
|
||||||
|
"""
|
||||||
|
【安全】向远程服务器安全沙箱内的文件写入内容。如果文件已存在,则会覆盖它。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于沙箱目录的文件路径。
|
||||||
|
content: 要写入文件的文本内容。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_ctx = ctx.request_context.lifespan_context
|
||||||
|
conn = app_ctx.ssh_connection
|
||||||
|
normalized_file_path = file_path.replace("\\", "/")
|
||||||
|
# 安全检查
|
||||||
|
if ".." in file_path.split('/'):
|
||||||
|
return "错误: 不允许使用 '..' 访问上级目录。"
|
||||||
|
|
||||||
|
target_path = os.path.join(app_ctx.current_path, normalized_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}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- 运行服务器 ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# mcp.run() # 这是默认的 stdio 模式
|
mcp.run(transport="streamable-http")
|
||||||
mcp.run(transport="streamable-http") # 改为 streamable-http 模式
|
|
||||||
Reference in New Issue
Block a user