hunian commited on
Commit
bdcdaf4
·
1 Parent(s): e0fe9a9

feat: integrate PluginMCPRegistry and add MCP tools to all plugins

Browse files
app/main.py CHANGED
@@ -17,7 +17,9 @@ from app.config.settings import settings
17
  from app.api.routes import router as api_router
18
  from app.plugins.manager import plugin_manager, set_app_instance
19
  from app.plugins.loader import plugin_loader
20
- from app.mcp.server import mcp, get_mcp_app, get_temp_file
 
 
21
 
22
  # 配置日志
23
  _LOG_CONFIG = {
@@ -112,24 +114,26 @@ async def register_plugin_routes():
112
  @asynccontextmanager
113
  async def lifespan(app: FastAPI):
114
  """应用生命周期管理"""
115
- # 启动时执行
116
  logger.info(f"{settings.PROJECT_NAME} v{settings.VERSION} 正在启动...")
117
  logger.info(f"插件目录: {settings.PLUGINS_DIR}")
118
- logger.info(f"调试模式: {settings.DEBUG}")
119
 
120
- # 注册插件路由
121
  await register_plugin_routes()
122
 
123
- # 创建并挂载 MCP 服务
 
 
 
 
 
124
  mcp_app = get_mcp_app()
125
  app.mount("/mcp", mcp_app)
126
  logger.info("MCP服务已挂载到 /mcp 路径")
127
 
128
- # 启动 MCP session manager
129
  async with mcp.session_manager.run():
130
  yield
131
 
132
- # 关闭时执行
133
  logger.info(f"{settings.PROJECT_NAME} 正在关闭...")
134
 
135
 
@@ -152,29 +156,6 @@ app.include_router(api_router)
152
  set_app_instance(app)
153
 
154
 
155
- # MCP 临时文件下载端点
156
- @app.get("/mcp/file/{file_id}")
157
- async def download_mcp_temp_file(request: Request, file_id: str):
158
- """下载 MCP 工具上传的临时文件。"""
159
- from fastapi.responses import Response
160
-
161
- result = await get_temp_file(file_id)
162
- if result is None:
163
- context = {
164
- "request": request,
165
- "error": "文件不存在或已过期",
166
- "project_name": settings.PROJECT_NAME,
167
- }
168
- return templates.TemplateResponse(request, name="error.html", context=context, status_code=404)
169
-
170
- content, filename = result
171
- return Response(
172
- content=content,
173
- media_type="application/octet-stream",
174
- headers={"Content-Disposition": f"attachment; filename={filename}"},
175
- )
176
-
177
-
178
  # 异常处理 - 修复模板上下文
179
  @app.exception_handler(StarletteHTTPException)
180
  async def http_exception_handler(request: Request, exc: StarletteHTTPException):
 
17
  from app.api.routes import router as api_router
18
  from app.plugins.manager import plugin_manager, set_app_instance
19
  from app.plugins.loader import plugin_loader
20
+ from app.mcp.server import mcp, get_mcp_app
21
+ from app.mcp.plugin_registry import init_plugin_registry, get_plugin_registry
22
+ from app.mcp.admin_tools import * # 确保管理工具被注册
23
 
24
  # 配置日志
25
  _LOG_CONFIG = {
 
114
  @asynccontextmanager
115
  async def lifespan(app: FastAPI):
116
  """应用生命周期管理"""
 
117
  logger.info(f"{settings.PROJECT_NAME} v{settings.VERSION} 正在启动...")
118
  logger.info(f"插件目录: {settings.PLUGINS_DIR}")
 
119
 
120
+ # 1. 注册插件路由(现有逻辑)
121
  await register_plugin_routes()
122
 
123
+ # 2. 初始化插件MCP注册器并注册工具
124
+ registry = init_plugin_registry(mcp)
125
+ tool_count = registry.scan_and_register_all(settings.PLUGINS_DIR)
126
+ logger.info(f"已注册 {tool_count} 个插件MCP工具")
127
+
128
+ # 3. 挂载 MCP 应用
129
  mcp_app = get_mcp_app()
130
  app.mount("/mcp", mcp_app)
131
  logger.info("MCP服务已挂载到 /mcp 路径")
132
 
133
+ # 4. 启动 MCP session manager
134
  async with mcp.session_manager.run():
135
  yield
136
 
 
137
  logger.info(f"{settings.PROJECT_NAME} 正在关闭...")
138
 
139
 
 
156
  set_app_instance(app)
157
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  # 异常处理 - 修复模板上下文
160
  @app.exception_handler(StarletteHTTPException)
161
  async def http_exception_handler(request: Request, exc: StarletteHTTPException):
plugins/document_merge_split_plugin/core.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文档合并拆分核心逻辑 - 供 mcp.py 和 api.py 共用
3
+ """
4
+
5
+ import base64
6
+ from typing import List
7
+
8
+
9
+ async def do_merge(files_base64: List[str], output_format: str = "text") -> dict:
10
+ """合并多个文档。"""
11
+ contents = []
12
+ for f_b64 in files_base64:
13
+ try:
14
+ content = base64.b64decode(f_b64).decode('utf-8')
15
+ contents.append(content)
16
+ except Exception as e:
17
+ return {
18
+ "success": False,
19
+ "message": f"解码失败: {str(e)}"
20
+ }
21
+
22
+ separator = "\n\n---\n\n" if output_format == "markdown" else "\n\n"
23
+ merged = separator.join(contents)
24
+
25
+ output_base64 = base64.b64encode(merged.encode('utf-8')).decode('utf-8')
26
+
27
+ return {
28
+ "success": True,
29
+ "output_base64": output_base64,
30
+ "message": f"已合并 {len(files_base64)} 个文档"
31
+ }
32
+
33
+
34
+ async def do_split(content_base64: str, split_by: str = "line", chunk_size: int = 100) -> dict:
35
+ """拆分文档。"""
36
+ try:
37
+ content = base64.b64decode(content_base64).decode('utf-8')
38
+ except Exception as e:
39
+ return {
40
+ "success": False,
41
+ "message": f"解码失败: {str(e)}"
42
+ }
43
+
44
+ chunks = []
45
+ if split_by == "line":
46
+ chunks = content.split('\n')
47
+ elif split_by == "paragraph":
48
+ chunks = content.split('\n\n')
49
+ elif split_by == "chunk":
50
+ chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
51
+
52
+ chunks_base64 = [base64.b64encode(c.encode('utf-8')).decode('utf-8') for c in chunks]
53
+
54
+ return {
55
+ "success": True,
56
+ "chunks_base64": chunks_base64,
57
+ "message": f"已拆分为 {len(chunks)} 个片段"
58
+ }
plugins/document_merge_split_plugin/mcp.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文档合并拆分插件 MCP工具定义
3
+ """
4
+
5
+ from app.mcp.decorators import mcp_tool
6
+ from pydantic import BaseModel, Field
7
+ from typing import List
8
+
9
+ from .core import do_merge, do_split
10
+
11
+
12
+ class MergeInput(BaseModel):
13
+ """文档合并输入参数"""
14
+ files_base64: List[str] = Field(description="待合并文件的Base64编码列表")
15
+ output_format: str = Field(default="text", description="输出格式:text、markdown")
16
+
17
+
18
+ class SplitInput(BaseModel):
19
+ """文档拆分输入参数"""
20
+ content_base64: str = Field(description="待拆分文档的Base64编码")
21
+ split_by: str = Field(default="line", description="拆分方式:line、paragraph、chunk")
22
+ chunk_size: int = Field(default=100, ge=10, le=10000, description="chunk模式下的块大小")
23
+
24
+
25
+ class MergeOutput(BaseModel):
26
+ """合并输出结果"""
27
+ success: bool = Field(description="操作是否成功")
28
+ output_base64: str | None = Field(description="合并结果的Base64编码")
29
+ message: str = Field(description="结果说明")
30
+
31
+
32
+ class SplitOutput(BaseModel):
33
+ """拆分输出结果"""
34
+ success: bool = Field(description="操作是否成功")
35
+ chunks_base64: List[str] | None = Field(description="拆分片段的Base64编码列表")
36
+ message: str = Field(description="结果说明")
37
+
38
+
39
+ @mcp_tool(name="merge", title="文档合并", description="合并多个文档内容")
40
+ async def doc_merge(params: MergeInput) -> MergeOutput:
41
+ result = await do_merge(params.files_base64, params.output_format)
42
+ return MergeOutput(**result)
43
+
44
+
45
+ @mcp_tool(name="split", title="文档拆分", description="拆分文档为多个片段")
46
+ async def doc_split(params: SplitInput) -> SplitOutput:
47
+ result = await do_split(params.content_base64, params.split_by, params.chunk_size)
48
+ return SplitOutput(**result)
plugins/file_cache_plugin/core.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文件缓存核心逻辑 - 供 mcp.py 和 api.py 共用
3
+ """
4
+
5
+ import time
6
+ from typing import Optional
7
+
8
+ # 简化的内存缓存存储
9
+ _cache_store: dict = {}
10
+
11
+
12
+ async def do_set(key: str, value_base64: str, ttl_seconds: int = 3600) -> dict:
13
+ """设置缓存值。"""
14
+ _cache_store[key] = {
15
+ "value": value_base64,
16
+ "expires_at": time.time() + ttl_seconds
17
+ }
18
+ return {
19
+ "success": True,
20
+ "message": f"缓存已设置,TTL {ttl_seconds}秒"
21
+ }
22
+
23
+
24
+ async def do_get(key: str) -> dict:
25
+ """获取缓存值。"""
26
+ entry = _cache_store.get(key)
27
+ if not entry:
28
+ return {
29
+ "success": False,
30
+ "message": f"缓存键 {key} 不存在"
31
+ }
32
+
33
+ if time.time() > entry["expires_at"]:
34
+ del _cache_store[key]
35
+ return {
36
+ "success": False,
37
+ "message": f"缓存键 {key} 已过期"
38
+ }
39
+
40
+ return {
41
+ "success": True,
42
+ "value": entry["value"],
43
+ "message": "缓存获取成功"
44
+ }
45
+
46
+
47
+ async def do_delete(key: str) -> dict:
48
+ """删除缓存值。"""
49
+ if key in _cache_store:
50
+ del _cache_store[key]
51
+ return {
52
+ "success": True,
53
+ "message": f"缓存键 {key} 已删除"
54
+ }
55
+ return {
56
+ "success": False,
57
+ "message": f"缓存键 {key} 不存在"
58
+ }
plugins/file_cache_plugin/mcp.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文件缓存插件 MCP工具定义
3
+ """
4
+
5
+ from app.mcp.decorators import mcp_tool
6
+ from pydantic import BaseModel, Field
7
+
8
+ from .core import do_set, do_get, do_delete
9
+
10
+
11
+ class SetInput(BaseModel):
12
+ """设置缓存输入参数"""
13
+ key: str = Field(description="缓存键名")
14
+ value_base64: str = Field(description="缓存值的Base64编码")
15
+ ttl_seconds: int = Field(default=3600, ge=60, le=86400, description="过期时间(秒)")
16
+
17
+
18
+ class GetInput(BaseModel):
19
+ """获取缓存输入参数"""
20
+ key: str = Field(description="缓存键名")
21
+
22
+
23
+ class DeleteInput(BaseModel):
24
+ """删除缓存输入参数"""
25
+ key: str = Field(description="缓存键名")
26
+
27
+
28
+ class CacheOutput(BaseModel):
29
+ """缓存操作输出结果"""
30
+ success: bool = Field(description="操作是否成功")
31
+ value: str | None = Field(default=None, description="缓存值(仅get操作)")
32
+ message: str = Field(description="结果说明")
33
+
34
+
35
+ @mcp_tool(name="set", title="设置缓存", description="设置缓存值,支持TTL")
36
+ async def cache_set(params: SetInput) -> CacheOutput:
37
+ result = await do_set(params.key, params.value_base64, params.ttl_seconds)
38
+ return CacheOutput(**result)
39
+
40
+
41
+ @mcp_tool(name="get", title="获取缓存", description="获取缓存值")
42
+ async def cache_get(params: GetInput) -> CacheOutput:
43
+ result = await do_get(params.key)
44
+ return CacheOutput(**result)
45
+
46
+
47
+ @mcp_tool(name="delete", title="删除缓存", description="删除缓存值")
48
+ async def cache_delete(params: DeleteInput) -> CacheOutput:
49
+ result = await do_delete(params.key)
50
+ return CacheOutput(**result)
plugins/image_resizer_plugin/core.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 图片缩放核心逻辑 - 供 mcp.py 和 api.py 共用
3
+ """
4
+
5
+ import io
6
+ import base64
7
+ from PIL import Image
8
+
9
+
10
+ async def do_resize(file_base64: str, width: int, height: int, format: str) -> dict:
11
+ """执行图片缩放操作。
12
+
13
+ Args:
14
+ file_base64: 图片的Base64编码
15
+ width: 目标宽度
16
+ height: 目标高度
17
+ format: 输出格式(PNG、JPEG、WEBP)
18
+
19
+ Returns:
20
+ 包含缩放结果的字典
21
+ """
22
+ try:
23
+ # 解码Base64
24
+ image_data = base64.b64decode(file_base64)
25
+ image = Image.open(io.BytesIO(image_data))
26
+
27
+ # 检查源图片尺寸
28
+ if image.width < width or image.height < height:
29
+ return {
30
+ "success": False,
31
+ "message": f"源图片尺寸 ({image.width}x{image.height}) 小于目标尺寸 ({width}x{height})"
32
+ }
33
+
34
+ # 缩放图片
35
+ resized = image.resize((width, height), Image.Resampling.LANCZOS)
36
+
37
+ # 输出
38
+ output = io.BytesIO()
39
+ save_format = format.upper() if format.upper() != "JPEG" else "JPEG"
40
+ resized.save(output, format=save_format, quality=95)
41
+ output.seek(0)
42
+
43
+ # 编码输出
44
+ output_base64 = base64.b64encode(output.read()).decode('utf-8')
45
+
46
+ return {
47
+ "success": True,
48
+ "output_base64": output_base64,
49
+ "message": f"图片已缩放至 {width}x{height}"
50
+ }
51
+
52
+ except Exception as e:
53
+ return {
54
+ "success": False,
55
+ "message": f"缩放失败: {str(e)}"
56
+ }
plugins/image_resizer_plugin/mcp.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 图片缩放插件 MCP工具定义
3
+ """
4
+
5
+ from app.mcp.decorators import mcp_tool
6
+ from pydantic import BaseModel, Field
7
+
8
+ from .core import do_resize
9
+
10
+
11
+ # ============================================================================
12
+ # 输入输出模型
13
+ # ============================================================================
14
+
15
+ class ResizeInput(BaseModel):
16
+ """图片缩放工具输入参数"""
17
+ file_base64: str = Field(
18
+ description="图片文件的Base64编码",
19
+ examples=["iVBORw0KGgoAAAANSUhEUgAA..."]
20
+ )
21
+ width: int = Field(
22
+ ge=16, le=2048,
23
+ description="目标宽度(像素),范围16-2048"
24
+ )
25
+ height: int = Field(
26
+ ge=16, le=2048,
27
+ description="目标高度(像素),范围16-2048"
28
+ )
29
+ format: str = Field(
30
+ default="PNG",
31
+ description="输出格式:PNG、JPEG、WEBP"
32
+ )
33
+
34
+
35
+ class ResizeOutput(BaseModel):
36
+ """图片缩放工具输出结果"""
37
+ success: bool = Field(description="操作是否成功")
38
+ output_base64: str | None = Field(description="缩放后图片的Base64编码")
39
+ output_url: str | None = Field(default=None, description="临时下载链接(可选)")
40
+ message: str = Field(description="结果说明")
41
+
42
+
43
+ # ============================================================================
44
+ # 工具定义
45
+ # ============================================================================
46
+
47
+ @mcp_tool(
48
+ name="resize",
49
+ title="图片缩放",
50
+ description="缩放图片到指定尺寸,支持PNG、JPEG、WEBP格式输出",
51
+ annotations={
52
+ "readOnlyHint": False,
53
+ "destructiveHint": False,
54
+ }
55
+ )
56
+ async def resize_image(params: ResizeInput) -> ResizeOutput:
57
+ """将图片缩放到指定的宽度和高度。
58
+
59
+ 支持PNG、JPEG、WEBP格式输出。
60
+ 输入图片尺寸必须大于目标尺寸。
61
+
62
+ Args:
63
+ params: 包含Base64图片数据、目标宽高、输出格式
64
+
65
+ Returns:
66
+ ResizeOutput: 包含缩放后的图片数据或错误信息
67
+ """
68
+ result = await do_resize(params.file_base64, params.width, params.height, params.format)
69
+ return ResizeOutput(**result)
plugins/text_cleaner_plugin/core.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文本清理核心逻辑 - 供 mcp.py 和 api.py 共用
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ async def do_clean(text: str, operations: list) -> dict:
9
+ """执行文本清理操作。
10
+
11
+ Args:
12
+ text: 待清理的文本
13
+ operations: 清理操作列表(trim、remove_empty_lines、normalize_spaces等)
14
+
15
+ Returns:
16
+ 包含清理结果的字典
17
+ """
18
+ result = text
19
+
20
+ for op in operations:
21
+ if op == "trim":
22
+ result = result.strip()
23
+ elif op == "remove_empty_lines":
24
+ result = re.sub(r'\n\s*\n', '\n', result)
25
+ elif op == "normalize_spaces":
26
+ result = re.sub(r'[ \t]+', ' ', result)
27
+ elif op == "remove_extra_newlines":
28
+ result = re.sub(r'\n+', '\n', result)
29
+
30
+ return {
31
+ "success": True,
32
+ "output": result,
33
+ "message": f"已执行 {len(operations)} 个清理操作"
34
+ }
plugins/text_cleaner_plugin/mcp.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文本清理插件 MCP工具定义
3
+ """
4
+
5
+ from app.mcp.decorators import mcp_tool
6
+ from pydantic import BaseModel, Field
7
+ from typing import List
8
+
9
+ from .core import do_clean
10
+
11
+
12
+ class CleanInput(BaseModel):
13
+ """文本清理工具输入参数"""
14
+ text: str = Field(description="待清理的文本内容")
15
+ operations: List[str] = Field(
16
+ default=["trim", "normalize_spaces"],
17
+ description="清理操作列表:trim、remove_empty_lines、normalize_spaces、remove_extra_newlines"
18
+ )
19
+
20
+
21
+ class CleanOutput(BaseModel):
22
+ """文本清理工具输出结果"""
23
+ success: bool = Field(description="操作是否成功")
24
+ output: str | None = Field(description="清理后的文本")
25
+ message: str = Field(description="结果说明")
26
+
27
+
28
+ @mcp_tool(
29
+ name="clean",
30
+ title="文本清理",
31
+ description="清理文本内容,支持多种清理操作",
32
+ annotations={
33
+ "readOnlyHint": False,
34
+ "destructiveHint": False,
35
+ }
36
+ )
37
+ async def clean_text(params: CleanInput) -> CleanOutput:
38
+ """清理文本内容。
39
+
40
+ 支持的清理操作:
41
+ - trim: 去除首尾空白
42
+ - remove_empty_lines: 移除空行
43
+ - normalize_spaces: 规范化空格
44
+ - remove_extra_newlines: 移除多余换行
45
+
46
+ Args:
47
+ params: 包含文本和清理操作列表
48
+
49
+ Returns:
50
+ CleanOutput: 清理后的文本
51
+ """
52
+ result = await do_clean(params.text, params.operations)
53
+ return CleanOutput(**result)