init
This commit is contained in:
20
README.md
20
README.md
@@ -15,22 +15,18 @@
|
|||||||
| `search_course` | 搜索课程信息 | `search_course("数据结构")` |
|
| `search_course` | 搜索课程信息 | `search_course("数据结构")` |
|
||||||
| `get_course_schedule` | 获取学生课表 | `get_course_schedule("B21010101", "2024-2025-1")` |
|
| `get_course_schedule` | 获取学生课表 | `get_course_schedule("B21010101", "2024-2025-1")` |
|
||||||
| `search_library_book` | 搜索图书馆藏书 | `search_library_book("Python", "title")` |
|
| `search_library_book` | 搜索图书馆藏书 | `search_library_book("Python", "title")` |
|
||||||
| `get_campus_info` | 获取校园信息 | `get_campus_info("contacts")` |
|
|
||||||
|
|
||||||
### 📚 Resources(资源)
|
### 📚 Resources(资源)
|
||||||
|
|
||||||
| 资源 URI | 描述 |
|
| 资源 URI | 描述 |
|
||||||
|----------|------|
|
|--------|-----|
|
||||||
| `njupt://announcements` | 最新公告列表 |
|
| `...` | ... |
|
||||||
| `njupt://academic-calendar` | 校历信息 |
|
|
||||||
| `njupt://departments` | 学院/部门列表 |
|
|
||||||
|
|
||||||
### 💬 Prompts(提示词模板)
|
### 💬 Prompts(提示词模板)
|
||||||
|
|
||||||
| Prompt | 用途 |
|
| Prompt | 用途 |
|
||||||
|--------|------|
|
|--------|-----|
|
||||||
| `academic_advisor_query` | 学业咨询助手 |
|
| `...` | ... |
|
||||||
| `campus_guide_query` | 校园导航助手 |
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -68,13 +64,13 @@ python -m njupt_mcp.server
|
|||||||
#### 方式二:SSE 模式(网络服务)
|
#### 方式二:SSE 模式(网络服务)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run njupt-mcp --transport sse --host 0.0.0.0 --port 8000
|
uv run njupt-mcp --transport sse
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 方式三:streamable-http 模式
|
#### 方式三:streamable-http 模式
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run njupt-mcp --transport streamable-http --port 8000
|
uv run njupt-mcp --transport streamable-http
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ 客户端配置
|
## ⚙️ 客户端配置
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ classifiers = [
|
|||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"beautifulsoup4>=4.14.3",
|
||||||
|
"fastapi>=0.135.2",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"mcp[cli]>=1.7.0",
|
"mcp[cli]>=1.7.0",
|
||||||
"pydantic>=2.0",
|
"pydantic>=2.0",
|
||||||
|
"python-dotenv>=1.2.2",
|
||||||
|
"uvicorn>=0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
32
src/njupt_mcp/resources/course_schedule/B240423-22.html
Normal file
32
src/njupt_mcp/resources/course_schedule/B240423-22.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<table id="Table6" class="blacktab" rules="all" border="1" height="132" width="100%">
|
||||||
|
<tbody><tr>
|
||||||
|
<td colspan="2" rowspan="1" width="2%">时间</td><td align="Center" width="14%">星期一</td><td align="Center" width="14%">星期二</td><td align="Center" width="14%">星期三</td><td align="Center" width="14%">星期四</td><td align="Center" width="14%">星期五</td><td align="Center" width="14%">星期六</td><td align="Center" width="14%">星期日</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td colspan="2">早晨</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td rowspan="5" width="1%">上午</td><td width="1%">第1节</td><td align="Center" rowspan="2" width="7%">计算机问题求解:使用算法(混合式)<br>1-17(1,2)<br>毛毅<br>教4-312<br></td><td align="Center" width="7%"> </td><td align="Center" width="7%"> </td><td align="Center" rowspan="2" width="7%">人工智能导论及其Python应用实践<br>1-17(1,2)<br>李平<br>教3-208<br></td><td align="Center" rowspan="2" width="7%">概率论与数理统计<br>1-17单(1,2)<br>王雪红<br>教3-520<br></td><td align="Center" width="7%"> </td><td align="Center" width="7%"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第2节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第3节</td><td align="Center" rowspan="2">计算机系统基础Ⅰ(混合式)<br>1-17(3,4)<br>李维维<br>教4-312<br></td><td align="Center" rowspan="2">概率论与数理统计<br>1-17(3,4)<br>王雪红<br>教3-520<br></td><td align="Center" rowspan="2">操作系统基础(混合式)<br>1-17(3,4)<br>王波(女)<br>教3-511<br></td><td align="Center" rowspan="2">大学英语IV<br>2节/双周<br> <br> <br><br><br>大学英语IV<br>2节/单周<br> <br> <br></td><td align="Center" rowspan="2">计算机系统基础Ⅰ(混合式)<br>2-16双(3,4)<br>李维维<br>教3-512<br><br><br>操作系统基础(混合式)<br>1-17单(3,4)<br>王波(女)<br>教3-201<br></td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第4节</td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第5节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td rowspan="4">下午</td><td>第6节</td><td align="Center" rowspan="2">数据库系统基础<br>1-17(6,7)<br>黄楠<br>教4-102<br><br><br>数据库系统基础<br>1-17(6,7)<br>李博文<br>教4-102<br></td><td align="Center" rowspan="2">人工智能导论及其Python应用实践<br>1-17单(6,7)<br>李平<br>教3-408<br><br><br>大学英语IV<br>2节/双周<br> <br> <br></td><td align="Center"> </td><td align="Center"> </td><td align="Center" rowspan="2">体育 IV<br>2节/周<br> <br> <br></td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第7节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第8节</td><td align="Center" rowspan="2">数学建模<br>1-17(8,9)<br>王敏敏<br>教3-102<br></td><td align="Center" rowspan="2">现代管理科学基础<br>1-17(8,9)<br>葛伟<br>教2-304<br></td><td align="Center" rowspan="2">中国文化概论<br>1-17(8,9)<br>葛辉<br>教4-101<br></td><td align="Center" rowspan="2">计算机问题求解:使用算法(混合式)<br>2-16双(8,9)<br>毛毅<br>教3-310<br></td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第9节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td rowspan="3">晚上</td><td>第10节</td><td align="Center" rowspan="3">形势与政策IV<br>6-8(10,11,12)<br>陈骏<br>教4-101<br></td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第11节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr><tr>
|
||||||
|
<td>第12节</td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td><td align="Center"> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
1
src/njupt_mcp/resources/types/__init__.py
Normal file
1
src/njupt_mcp/resources/types/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .course import Course, course_dict_serializer
|
||||||
34
src/njupt_mcp/resources/types/course.py
Normal file
34
src/njupt_mcp/resources/types/course.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Course:
|
||||||
|
"""
|
||||||
|
Course 是对课程表中的 **某一节课** 的抽象。
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
1-17周,星期一,1-2节,数据结构,是一个 Course 对象;
|
||||||
|
|
||||||
|
1-17周,星期三,3-4节,数据结构,是另一个 Course 对象;
|
||||||
|
|
||||||
|
1-17周中的单周,星期四,3-4节,英语,是一个 Course 对象;
|
||||||
|
|
||||||
|
1-17周中的双周,星期四,3-4节,物理,是另一个 Course 对象。
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
weeks: list[int]
|
||||||
|
day: int
|
||||||
|
classes: list[int]
|
||||||
|
teacher: str | None
|
||||||
|
classroom: str | None
|
||||||
|
|
||||||
|
|
||||||
|
def course_dict_serializer(course: Course) -> dict[str, str | list[int] | int | None]:
|
||||||
|
return {
|
||||||
|
"name": course.name,
|
||||||
|
"weeks": course.weeks,
|
||||||
|
"day": course.day,
|
||||||
|
"classes": course.classes,
|
||||||
|
"teacher": course.teacher,
|
||||||
|
"classroom": course.classroom,
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
"""NJUPT MCP Server 主入口
|
"""NJUPT MCP Server 主入口
|
||||||
|
|
||||||
基于 FastMCP 实现的南京邮电大学 MCP 服务器。
|
基于 FastMCP + FastAPI 实现的南京邮电大学 MCP 服务器。
|
||||||
支持 stdio 和 SSE 两种传输方式。
|
支持 stdio 和 SSE 两种传输方式。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import AsyncIterator
|
from typing import AsyncIterator
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from mcp.server.fastmcp.server import Settings
|
from mcp.types import ToolAnnotations
|
||||||
|
|
||||||
|
from njupt_mcp.resources.types import course_dict_serializer
|
||||||
|
from njupt_mcp.tools import create_course_schedule
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -21,14 +27,24 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger("njupt-mcp")
|
logger = logging.getLogger("njupt-mcp")
|
||||||
|
|
||||||
|
|
||||||
|
def findHtml():
|
||||||
|
load_dotenv()
|
||||||
|
html_path = os.environ.get('COURSE_SCHEDULE')
|
||||||
|
if html_path is None:
|
||||||
|
logger.error('未知课表 HTML 的位置,调用错误。')
|
||||||
|
logger.error('回退到芒果帆帆的大二下学期课表~')
|
||||||
|
html_path = './src/njupt_mcp/resources/course_schedule/B240423-22.html'
|
||||||
|
return html_path
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
|
async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
|
||||||
"""应用生命周期管理
|
"""应用生命周期管理
|
||||||
|
|
||||||
处理服务器启动和关闭时的资源初始化和清理。
|
处理服务器启动和关闭时的资源初始化和清理。
|
||||||
"""
|
"""
|
||||||
logger.info("🚀 NJUPT MCP Server 启动中...")
|
logger.info("🚀 NJUPT MCP Server 启动中...")
|
||||||
|
|
||||||
# 初始化共享资源(如数据库连接、HTTP 客户端等)
|
# 初始化共享资源(如数据库连接、HTTP 客户端等)
|
||||||
try:
|
try:
|
||||||
# 这里可以初始化一些全局资源
|
# 这里可以初始化一些全局资源
|
||||||
@@ -44,166 +60,150 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
|
|||||||
logger.info("✅ 资源清理完成")
|
logger.info("✅ 资源清理完成")
|
||||||
|
|
||||||
|
|
||||||
# 创建 MCP 服务器实例
|
# 1. 创建标准的 FastMCP 实例
|
||||||
# lifespan: 应用生命周期管理
|
mcp = FastMCP("njupt-mcp", lifespan=app_lifespan)
|
||||||
mcp = FastMCP(
|
|
||||||
"njupt-mcp",
|
# 2. 创建 FastAPI 实例并配置 CORS
|
||||||
lifespan=app_lifespan,
|
app = FastAPI()
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 允许所有来源,方便 Inspector 连接
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 3. 将 FastMCP 的 SSE 应用挂载到 FastAPI
|
||||||
|
# sse_app() 返回 Starlette ASGI 应用,内部路由为 /sse 和 /messages
|
||||||
|
# 挂载到根路径,使 SSE 端点可直接通过 /sse 访问
|
||||||
|
app.mount("/", mcp.sse_app())
|
||||||
|
|
||||||
|
|
||||||
# ==================== Tools ====================
|
# ==================== Tools ====================
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(
|
||||||
async def search_course(keyword: str, limit: int = 10) -> str:
|
name="get_course_schedule_json",
|
||||||
"""搜索南京邮电大学课程信息
|
title="获取课表 JSON",
|
||||||
|
description="以 JSON 格式获取整学期整周全部课表数据",
|
||||||
根据关键词搜索课程名称、课程代码、开课学院等信息。
|
annotations=ToolAnnotations(
|
||||||
|
title="获取课表 JSON",
|
||||||
Args:
|
readOnlyHint=True, # 只读取数据,不修改
|
||||||
keyword: 搜索关键词(课程名称、课程代码等)
|
destructiveHint=False, # 非破坏性操作
|
||||||
limit: 返回结果数量限制,默认 10 条
|
idempotentHint=True, # 幂等:重复调用结果相同
|
||||||
|
openWorldHint=False, # 不依赖外部世界状态
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def get_course_schedule_json() -> list[dict]:
|
||||||
|
"""以 JSON 格式获取整学期整周全部课表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
课程信息列表的 JSON 字符串
|
课程列表,每个课程包含以下字段:
|
||||||
|
- name: 课程名称
|
||||||
Example:
|
- teacher: 授课教师(可能为 None)
|
||||||
search_course("数据结构") -> 返回数据结构相关课程
|
- classroom: 教室(可能为 None)
|
||||||
search_course("CS101", limit=5) -> 返回课程代码包含 CS101 的课程
|
- weeks: 周数,list[int]
|
||||||
|
- day: 星期几,int
|
||||||
|
- classes: 当日第几节课,list[int]
|
||||||
|
失败时返回空列表
|
||||||
"""
|
"""
|
||||||
# TODO: 实现实际的课程搜索逻辑
|
html_path = findHtml()
|
||||||
# 这里返回示例数据
|
if html_path is None:
|
||||||
courses = [
|
return []
|
||||||
{
|
|
||||||
"course_code": "CS2101",
|
courses = create_course_schedule(html_path)
|
||||||
"name": f"{keyword}基础",
|
logger.debug('解析得到的课表如下:')
|
||||||
"credit": 3.0,
|
logger.debug(courses)
|
||||||
"department": "计算机学院",
|
|
||||||
"description": f"这是一门关于{keyword}的基础课程",
|
final_courses = []
|
||||||
},
|
for course in courses:
|
||||||
{
|
final_courses.append(course_dict_serializer(course))
|
||||||
"course_code": "CS3102",
|
return final_courses
|
||||||
"name": f"高级{keyword}",
|
|
||||||
"credit": 4.0,
|
|
||||||
"department": "计算机学院",
|
|
||||||
"description": f"深入探讨{keyword}的高级主题",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
import json
|
|
||||||
return json.dumps(courses[:limit], ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(
|
||||||
async def get_course_schedule(student_id: str, semester: str = "2024-2025-1") -> str:
|
name="get_week_course_schedule_json",
|
||||||
"""获取学生课表信息
|
title="获取指定周课表",
|
||||||
|
description="以 JSON 格式获取指定教学周的全部课表数据",
|
||||||
根据学号和学期获取个人课表。
|
annotations=ToolAnnotations(
|
||||||
|
title="获取指定周课表",
|
||||||
|
readOnlyHint=True, # 只读取数据,不修改
|
||||||
|
destructiveHint=False, # 非破坏性操作
|
||||||
|
idempotentHint=True, # 幂等:重复调用结果相同
|
||||||
|
openWorldHint=False, # 不依赖外部世界状态
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def get_week_course_schedule_json(week: int) -> list[dict]:
|
||||||
|
"""以 JSON 格式获取指定教学周的全部课表
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
student_id: 学生学号(如 B21010101)
|
week: 教学周数,范围通常为 1-20
|
||||||
semester: 学期代码,格式为 "YYYY-YYYY-S"(如 2024-2025-1)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
课表信息的 JSON 字符串
|
指定周的课程列表,每个课程包含以下字段:
|
||||||
|
- name: 课程名称
|
||||||
Example:
|
- teacher: 授课教师(可能为 None)
|
||||||
get_course_schedule("B21010101", "2024-2025-1")
|
- classroom: 教室(可能为 None)
|
||||||
|
- weeks: 周数,list[int]
|
||||||
|
- day: 星期几,int (1-7)
|
||||||
|
- classes: 当日第几节课,list[int]
|
||||||
|
该周无课程或参数错误时返回空列表
|
||||||
"""
|
"""
|
||||||
# TODO: 实现实际的课表查询逻辑
|
html_path = findHtml()
|
||||||
import json
|
if html_path is None:
|
||||||
schedule = {
|
return []
|
||||||
"student_id": student_id,
|
|
||||||
"semester": semester,
|
courses = create_course_schedule(html_path)
|
||||||
"courses": [
|
logger.debug('解析得到的课表如下:')
|
||||||
{
|
logger.debug(courses)
|
||||||
"day": 1,
|
|
||||||
"period": [1, 2],
|
final_courses = []
|
||||||
"name": "数据结构",
|
for course in courses:
|
||||||
"location": "教1-101",
|
if week in course.weeks:
|
||||||
"teacher": "张三教授",
|
final_courses.append(course_dict_serializer(course))
|
||||||
},
|
return final_courses
|
||||||
{
|
|
||||||
"day": 3,
|
|
||||||
"period": [3, 4],
|
|
||||||
"name": "计算机网络",
|
|
||||||
"location": "教2-205",
|
|
||||||
"teacher": "李四副教授",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
return json.dumps(schedule, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(
|
||||||
async def search_library_book(keyword: str, search_type: str = "title") -> str:
|
name="get_week_day_course_schedule_json",
|
||||||
"""搜索图书馆藏书
|
title="获取指定周星期课表",
|
||||||
|
description="以 JSON 格式获取指定教学周和星期的课表数据",
|
||||||
在南京邮电大学图书馆搜索图书。
|
annotations=ToolAnnotations(
|
||||||
|
title="获取指定周星期课表",
|
||||||
|
readOnlyHint=True, # 只读取数据,不修改
|
||||||
|
destructiveHint=False, # 非破坏性操作
|
||||||
|
idempotentHint=True, # 幂等:重复调用结果相同
|
||||||
|
openWorldHint=False, # 不依赖外部世界状态
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def get_week_day_course_schedule_json(week: int, day: int) -> list[dict]:
|
||||||
|
"""以 JSON 格式获取指定教学周和星期的课表
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
keyword: 搜索关键词
|
week: 教学周数,范围通常为 1-20
|
||||||
search_type: 搜索类型,可选 "title"(书名), "author"(作者), "isbn"(ISBN)
|
day: 星期几,1=星期一,2=星期二,...,7=星期日
|
||||||
|
|
||||||
Returns:
|
|
||||||
图书信息列表的 JSON 字符串
|
|
||||||
|
|
||||||
Example:
|
|
||||||
search_library_book("Python", "title")
|
|
||||||
search_library_book("鲁迅", "author")
|
|
||||||
"""
|
|
||||||
# TODO: 实现实际的图书馆搜索逻辑
|
|
||||||
import json
|
|
||||||
books = [
|
|
||||||
{
|
|
||||||
"title": f"{keyword}编程实战",
|
|
||||||
"author": "王某某",
|
|
||||||
"publisher": "清华大学出版社",
|
|
||||||
"isbn": "978-7-302-12345-6",
|
|
||||||
"available": True,
|
|
||||||
"location": "计算机科学阅览室",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": f"{keyword}入门指南",
|
|
||||||
"author": "李某某",
|
|
||||||
"publisher": "人民邮电出版社",
|
|
||||||
"isbn": "978-7-115-78901-2",
|
|
||||||
"available": False,
|
|
||||||
"location": "基础科学阅览室",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return json.dumps(books, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_campus_info(info_type: str = "overview") -> str:
|
|
||||||
"""获取校园信息
|
|
||||||
|
|
||||||
获取南京邮电大学的基本信息、办事指南等。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
info_type: 信息类型,可选 "overview"(概况), "map"(地图),
|
|
||||||
"contacts"(联系方式), "calendar"(校历)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
校园信息的字符串
|
指定周和星期的课程列表,每个课程包含以下字段:
|
||||||
|
- name: 课程名称
|
||||||
Example:
|
- teacher: 授课教师(可能为 None)
|
||||||
get_campus_info("overview") -> 学校概况
|
- classroom: 教室(可能为 None)
|
||||||
get_campus_info("contacts") -> 各部门联系方式
|
- weeks: 周数,list[int]
|
||||||
|
- day: 星期几,int (1-7)
|
||||||
|
- classes: 当日第几节课,list[int]
|
||||||
|
该时段无课程或参数错误时返回空列表
|
||||||
"""
|
"""
|
||||||
info_data = {
|
html_path = findHtml()
|
||||||
"overview": """南京邮电大学(Nanjing University of Posts and Telecommunications,
|
if html_path is None:
|
||||||
简称 NJUPT)是国家“双一流”建设高校和江苏高水平大学高峰计划 A 类建设高校。
|
return []
|
||||||
学校坐落于历史文化名城南京,现有仙林、三牌楼、锁金村、江宁四个校区。""",
|
|
||||||
"contacts": """教务处:025-85866250
|
courses = create_course_schedule(html_path)
|
||||||
学生工作处:025-85866255
|
logger.debug('解析得到的课表如下:')
|
||||||
图书馆:025-85866270
|
logger.debug(courses)
|
||||||
信息化建设与管理办公室:025-85866280""",
|
|
||||||
"map": "请访问 https://map.njupt.edu.cn 查看校园地图",
|
final_courses = []
|
||||||
"calendar": "2024-2025学年第一学期:2024年9月2日 - 2025年1月12日",
|
for course in courses:
|
||||||
}
|
if (week in course.weeks) and (day == course.day):
|
||||||
return info_data.get(info_type, info_data["overview"])
|
final_courses.append(course_dict_serializer(course))
|
||||||
|
return final_courses
|
||||||
|
|
||||||
|
|
||||||
# ==================== Resources ====================
|
# ==================== Resources ====================
|
||||||
@@ -249,39 +249,6 @@ async def get_academic_calendar() -> str:
|
|||||||
- 暑假:2025年6月30日起"""
|
- 暑假:2025年6月30日起"""
|
||||||
|
|
||||||
|
|
||||||
@mcp.resource("njupt://departments")
|
|
||||||
async def get_departments() -> str:
|
|
||||||
"""获取南京邮电大学学院/部门列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
学院和部门列表
|
|
||||||
"""
|
|
||||||
return """🏫 南京邮电大学学院设置:
|
|
||||||
|
|
||||||
通信与信息工程学院
|
|
||||||
电子与光学工程学院/柔性电子(未来技术)学院
|
|
||||||
集成电路科学与工程学院(产教融合学院)
|
|
||||||
计算机学院/软件学院/网络空间安全学院
|
|
||||||
自动化学院/人工智能学院
|
|
||||||
材料科学与工程学院
|
|
||||||
化学与生命科学学院
|
|
||||||
物联网学院
|
|
||||||
理学院
|
|
||||||
地理与生物信息学院
|
|
||||||
现代邮政学院
|
|
||||||
传媒与艺术学院
|
|
||||||
管理学院
|
|
||||||
经济学院
|
|
||||||
马克思主义学院
|
|
||||||
社会与人口学院/社会工作学院
|
|
||||||
外国语学院
|
|
||||||
教育科学与技术学院
|
|
||||||
|
|
||||||
📋 主要职能部门:
|
|
||||||
教务处、学生工作处、研究生工作部、科学技术处、
|
|
||||||
人事处、财务处、审计处、保卫处、后勤管理处等"""
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Prompts ====================
|
# ==================== Prompts ====================
|
||||||
|
|
||||||
@mcp.prompt()
|
@mcp.prompt()
|
||||||
@@ -295,7 +262,7 @@ def academic_advisor_query(question: str, student_major: str = "") -> str:
|
|||||||
student_major: 学生专业(可选)
|
student_major: 学生专业(可选)
|
||||||
"""
|
"""
|
||||||
major_context = f"该学生专业为:{student_major}。" if student_major else ""
|
major_context = f"该学生专业为:{student_major}。" if student_major else ""
|
||||||
|
|
||||||
return f"""你是南京邮电大学的学业咨询助手,专门帮助学生解决学习和课程相关的问题。
|
return f"""你是南京邮电大学的学业咨询助手,专门帮助学生解决学习和课程相关的问题。
|
||||||
|
|
||||||
{major_context}
|
{major_context}
|
||||||
@@ -320,9 +287,9 @@ def campus_guide_query(location: str, query_type: str = "location") -> str:
|
|||||||
"route": f"请提供前往南京邮电大学'{location}'的详细路线指引。",
|
"route": f"请提供前往南京邮电大学'{location}'的详细路线指引。",
|
||||||
"facility": f"请介绍南京邮电大学'{location}'的设施情况和开放时间。",
|
"facility": f"请介绍南京邮电大学'{location}'的设施情况和开放时间。",
|
||||||
}
|
}
|
||||||
|
|
||||||
base_prompt = prompts.get(query_type, prompts["location"])
|
base_prompt = prompts.get(query_type, prompts["location"])
|
||||||
|
|
||||||
return f"""你是南京邮电大学校园导航助手,熟悉校园的各个位置和设施。
|
return f"""你是南京邮电大学校园导航助手,熟悉校园的各个位置和设施。
|
||||||
|
|
||||||
{base_prompt}
|
{base_prompt}
|
||||||
@@ -342,34 +309,41 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host",
|
"--host",
|
||||||
default="127.0.0.1",
|
default="127.0.0.1",
|
||||||
help="SSE/HTTP 模式下的监听地址 (默认: 127.0.0.1)",
|
help="服务器主机地址 (默认: 127.0.0.1)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port",
|
"--port",
|
||||||
type=int,
|
type=int,
|
||||||
default=8000,
|
default=8000,
|
||||||
help="SSE/HTTP 模式下的监听端口 (默认: 8000)",
|
help="服务器端口 (默认: 8000)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug",
|
"--debug",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="启用调试模式",
|
help="启用调试模式",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
logger.debug("调试模式已启用")
|
logger.debug("调试模式已启用")
|
||||||
|
|
||||||
logger.info(f"启动 NJUPT MCP Server,传输方式: {args.transport}")
|
logger.info(f"启动 NJUPT MCP Server,传输方式: {args.transport}")
|
||||||
|
|
||||||
if args.transport == "stdio":
|
if args.transport == "stdio":
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
elif args.transport == "sse":
|
elif args.transport == "sse":
|
||||||
mcp.run(transport="sse", host=args.host, port=args.port)
|
# 使用 uvicorn 启动 FastAPI 应用(SSE 模式)
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host=args.host, port=args.port)
|
||||||
elif args.transport == "streamable-http":
|
elif args.transport == "streamable-http":
|
||||||
mcp.run(transport="streamable-http", host=args.host, port=args.port)
|
# 创建独立的 streamable-http 应用
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
http_app = mcp.streamable_http_app()
|
||||||
|
uvicorn.run(http_app, host=args.host, port=args.port)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
4. 处理异常情况并返回友好的错误信息
|
4. 处理异常情况并返回友好的错误信息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 工具函数将在这里定义,也可以在单独的模块中定义后在此导入
|
from .course import create_course_schedule
|
||||||
|
|||||||
1
src/njupt_mcp/tools/course/__init__.py
Normal file
1
src/njupt_mcp/tools/course/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .create_course_schedule import create_course_schedule
|
||||||
201
src/njupt_mcp/tools/course/create_course_schedule.py
Normal file
201
src/njupt_mcp/tools/course/create_course_schedule.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from njupt_mcp.resources.types import Course
|
||||||
|
|
||||||
|
|
||||||
|
# 可能需要配合修改的方法
|
||||||
|
def normalize_course_str(course_str: str) -> str:
|
||||||
|
"""规范化课程字符串,确保 create_course 能正确解析。"""
|
||||||
|
parts = course_str.split("<br>")
|
||||||
|
while parts and parts[0] == "":
|
||||||
|
parts.pop(0)
|
||||||
|
while len(parts) < 4:
|
||||||
|
parts.append(" ")
|
||||||
|
for i in range(2, 4):
|
||||||
|
if parts[i] == "":
|
||||||
|
parts[i] = " "
|
||||||
|
return "<br>".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_schedule(html_path: str) -> list[Course]:
|
||||||
|
"""
|
||||||
|
解析给定 HTML 文件,返回包含数个 Course 对象的列表。
|
||||||
|
Args:
|
||||||
|
html_path: HTML 文件路径。该文件中应该有且只有一个 <table> 标签,其中是课程表数据。
|
||||||
|
|
||||||
|
Returns: list[Course]
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open(html_path, encoding="utf-8") as f:
|
||||||
|
html = f.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
table = soup.find("table")
|
||||||
|
rows = table.find_all("tr")
|
||||||
|
|
||||||
|
courses: list[Course] = []
|
||||||
|
rowspan_map: dict[int, int] = {}
|
||||||
|
|
||||||
|
# 解析第一行表头,建立列索引到星期几的映射
|
||||||
|
# 表头格式:第1列是"时间"(colspan=2),然后是 星期一 到 星期日
|
||||||
|
day_map: dict[int, int] = {} # col_idx -> day (1-7)
|
||||||
|
if rows:
|
||||||
|
header_cells = rows[0].find_all(["td", "th"])
|
||||||
|
col_idx = 0
|
||||||
|
for cell in header_cells:
|
||||||
|
text = cell.get_text(strip=True)
|
||||||
|
colspan = int(cell.get("colspan", 1))
|
||||||
|
|
||||||
|
# 跳过"时间"单元格
|
||||||
|
if text != "时间":
|
||||||
|
# 映射星期几到数字
|
||||||
|
day_mapping = {
|
||||||
|
"星期一": 1, "星期二": 2, "星期三": 3, "星期四": 4,
|
||||||
|
"星期五": 5, "星期六": 6, "星期日": 7, "星期天": 7
|
||||||
|
}
|
||||||
|
day = day_mapping.get(text)
|
||||||
|
if day is not None:
|
||||||
|
for c in range(col_idx, col_idx + colspan):
|
||||||
|
day_map[c] = day
|
||||||
|
|
||||||
|
col_idx += colspan
|
||||||
|
|
||||||
|
for row_idx, row in enumerate(rows):
|
||||||
|
if row_idx == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cells = row.find_all(["td", "th"])
|
||||||
|
col_idx = 0
|
||||||
|
class_start: int | None = None
|
||||||
|
|
||||||
|
for cell in cells:
|
||||||
|
while col_idx in rowspan_map and rowspan_map[col_idx] > 0:
|
||||||
|
rowspan_map[col_idx] -= 1
|
||||||
|
if rowspan_map[col_idx] == 0:
|
||||||
|
del rowspan_map[col_idx]
|
||||||
|
col_idx += 1
|
||||||
|
|
||||||
|
text = cell.get_text(strip=True)
|
||||||
|
colspan = int(cell.get("colspan", 1))
|
||||||
|
rowspan = int(cell.get("rowspan", 1))
|
||||||
|
|
||||||
|
if text.startswith("第") and text.endswith("节"):
|
||||||
|
class_start = int(text[1:-1])
|
||||||
|
if rowspan > 1:
|
||||||
|
for c in range(col_idx, col_idx + colspan):
|
||||||
|
rowspan_map[c] = rowspan - 1
|
||||||
|
col_idx += colspan
|
||||||
|
continue
|
||||||
|
|
||||||
|
if text in ("早晨", "上午", "下午", "晚上"):
|
||||||
|
if rowspan > 1:
|
||||||
|
for c in range(col_idx, col_idx + colspan):
|
||||||
|
rowspan_map[c] = rowspan - 1
|
||||||
|
col_idx += colspan
|
||||||
|
continue
|
||||||
|
|
||||||
|
td_str = str(cell)
|
||||||
|
start = td_str.find(">") + 1
|
||||||
|
end = td_str.rfind("</td>")
|
||||||
|
inner_html = td_str[start:end]
|
||||||
|
|
||||||
|
if " " not in inner_html and inner_html.strip():
|
||||||
|
inner_html = re.sub(r"<br\s*/?>", "<br>", inner_html)
|
||||||
|
course_strs = [
|
||||||
|
s.strip()
|
||||||
|
for s in re.split(r"(?:<br>){2,}", inner_html)
|
||||||
|
if s.strip() and " " not in s
|
||||||
|
]
|
||||||
|
# 获取当前列对应的星期几
|
||||||
|
day = day_map.get(col_idx, 1) # 默认为1(星期一)
|
||||||
|
for course_str in course_strs:
|
||||||
|
course_str = normalize_course_str(course_str)
|
||||||
|
courses.append(create_course(course_str, day, default_classes_start=class_start))
|
||||||
|
|
||||||
|
if rowspan > 1:
|
||||||
|
for c in range(col_idx, col_idx + colspan):
|
||||||
|
rowspan_map[c] = rowspan - 1
|
||||||
|
|
||||||
|
col_idx += colspan
|
||||||
|
|
||||||
|
return courses
|
||||||
|
|
||||||
|
|
||||||
|
def create_course(raw: str, day: int, default_classes_start: int | None = None) -> Course:
|
||||||
|
"""
|
||||||
|
根据从 HTML 中提取出的原字符串解析课程信息
|
||||||
|
Args:
|
||||||
|
raw: 原字符串,以 <br> 作为换行符
|
||||||
|
day: 周内的星期几
|
||||||
|
default_classes_start: 如果没有解析出课程的 classes,则使用此参数。此参数应当从表格的行标题解析
|
||||||
|
|
||||||
|
Returns: Course
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 0 1 2 3 4
|
||||||
|
# ['概率论与数理统计', '1-17单(1,2)', '王雪红', '教3-520', '']
|
||||||
|
raw_list = raw.split("<br>")
|
||||||
|
|
||||||
|
# 首先去除列表头部的所有空字符串
|
||||||
|
while True:
|
||||||
|
if raw_list[0] == '':
|
||||||
|
raw_list.pop(0)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 对于大部分课程,raw_list[1] 都是形如以下格式
|
||||||
|
# 1-17(3,4)
|
||||||
|
# 1-17单(1,2) *(也可能是双)
|
||||||
|
# 2节/周
|
||||||
|
# 2节/单周 *(也可能是双)
|
||||||
|
raw_time = raw_list[1]
|
||||||
|
weeks = []
|
||||||
|
classes = []
|
||||||
|
single = False # 内部变量
|
||||||
|
double = False # 内部变量
|
||||||
|
# 处理前两种形式
|
||||||
|
if '-' in raw_time:
|
||||||
|
# 也可能是 '1-17单'
|
||||||
|
t = raw_time.split('(') # ['1-17', '3-4)']
|
||||||
|
# 也可能是 '17单'
|
||||||
|
start, end = t[0].split('-') # ['1', '17']
|
||||||
|
if end.endswith('单'):
|
||||||
|
end = end[:-1]
|
||||||
|
single = True
|
||||||
|
elif end.endswith('双'):
|
||||||
|
end = end[:-1]
|
||||||
|
double = True
|
||||||
|
for i in range(int(start), int(end) + 1):
|
||||||
|
if single and i % 2 == 0:
|
||||||
|
continue
|
||||||
|
if double and i % 2 == 1:
|
||||||
|
continue
|
||||||
|
weeks.append(i)
|
||||||
|
raw_classes = t[1].removesuffix(')')
|
||||||
|
classes = [int(i) for i in raw_classes.split(',')]
|
||||||
|
# 处理后两种形式
|
||||||
|
elif '/' in raw_time:
|
||||||
|
# 默认学期 1-16 周
|
||||||
|
if '/单周' in raw_time:
|
||||||
|
single = True
|
||||||
|
elif '/双周' in raw_time:
|
||||||
|
double = True
|
||||||
|
for i in range(1, 17):
|
||||||
|
if single and i % 2 == 0:
|
||||||
|
continue
|
||||||
|
if double and i % 2 == 1:
|
||||||
|
continue
|
||||||
|
weeks.append(i)
|
||||||
|
|
||||||
|
# 获取多少节课
|
||||||
|
_num = int(raw_time.split('节')[0])
|
||||||
|
for i in range(0, _num):
|
||||||
|
classes.append(default_classes_start + i)
|
||||||
|
|
||||||
|
teacher = raw_list[2] if raw_list[2] != ' ' else None
|
||||||
|
classroom = raw_list[3] if raw_list[3] != ' ' else None
|
||||||
|
|
||||||
|
return Course(raw_list[0], weeks, day, classes, teacher, classroom)
|
||||||
87
tests/test_tools.py
Normal file
87
tests/test_tools.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"""测试工具函数"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from njupt_mcp.resources.types import Course
|
||||||
|
from njupt_mcp.tools.course.create_course_schedule import (
|
||||||
|
create_course,
|
||||||
|
create_course_schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_HTML_PATH = Path(__file__).parent.parent / "src" / "njupt_mcp" / "resources" / "course_schedule" / "B240423-22.html"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_course_basic():
|
||||||
|
"""测试 create_course 解析常规课程字符串"""
|
||||||
|
course = create_course("概率论与数理统计<br>1-17单(1,2)<br>王雪红<br>教3-520<br>")
|
||||||
|
assert course.name == "概率论与数理统计"
|
||||||
|
assert course.weeks == [1, 3, 5, 7, 9, 11, 13, 15, 17]
|
||||||
|
assert course.classes == [1, 2]
|
||||||
|
assert course.teacher == "王雪红"
|
||||||
|
assert course.classroom == "教3-520"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_course_double_weeks():
|
||||||
|
"""测试 create_course 解析双周课程"""
|
||||||
|
course = create_course("计算机系统基础Ⅰ(混合式)<br>2-16双(3,4)<br>李维维<br>教3-512<br>")
|
||||||
|
assert course.name == "计算机系统基础Ⅰ(混合式)"
|
||||||
|
assert course.weeks == [2, 4, 6, 8, 10, 12, 14, 16]
|
||||||
|
assert course.classes == [3, 4]
|
||||||
|
assert course.teacher == "李维维"
|
||||||
|
assert course.classroom == "教3-512"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_course_with_default_classes():
|
||||||
|
"""测试 create_course 在需要 default_classes_start 时的解析"""
|
||||||
|
course = create_course("大学英语IV<br>2节/双周<br> <br> <br>", default_classes_start=3)
|
||||||
|
assert course.name == "大学英语IV"
|
||||||
|
assert course.weeks == [2, 4, 6, 8, 10, 12, 14, 16]
|
||||||
|
assert course.classes == [3, 4]
|
||||||
|
assert course.teacher is None
|
||||||
|
assert course.classroom is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_course_schedule_from_html():
|
||||||
|
"""测试从 HTML 文件解析课程表"""
|
||||||
|
courses = create_course_schedule(str(TEST_HTML_PATH))
|
||||||
|
|
||||||
|
assert isinstance(courses, list)
|
||||||
|
assert len(courses) == 20
|
||||||
|
assert all(isinstance(c, Course) for c in courses)
|
||||||
|
|
||||||
|
# 验证几个典型课程
|
||||||
|
course_names = [c.name for c in courses]
|
||||||
|
assert "计算机问题求解:使用算法(混合式)" in course_names
|
||||||
|
assert "概率论与数理统计" in course_names
|
||||||
|
assert "形势与政策IV" in course_names
|
||||||
|
|
||||||
|
# 查找并验证特定课程
|
||||||
|
xingshi = next(c for c in courses if c.name == "形势与政策IV")
|
||||||
|
assert xingshi.weeks == [6, 7, 8]
|
||||||
|
assert xingshi.classes == [10, 11, 12]
|
||||||
|
assert xingshi.teacher == "陈骏"
|
||||||
|
assert xingshi.classroom == "教4-101"
|
||||||
|
|
||||||
|
# 验证单周课程
|
||||||
|
gailv = [c for c in courses if c.name == "概率论与数理统计" and c.classes == [1, 2]][0]
|
||||||
|
assert gailv.weeks == [1, 3, 5, 7, 9, 11, 13, 15, 17]
|
||||||
|
|
||||||
|
# 验证合并单元格中的多课程(数据库系统基础出现两次,不同老师)
|
||||||
|
db_courses = [c for c in courses if c.name == "数据库系统基础"]
|
||||||
|
assert len(db_courses) == 2
|
||||||
|
teachers = {c.teacher for c in db_courses}
|
||||||
|
assert teachers == {"黄楠", "李博文"}
|
||||||
|
|
||||||
|
# 验证无教师/教室的课程(大学英语IV、体育 IV)
|
||||||
|
english_courses = [c for c in courses if c.name == "大学英语IV"]
|
||||||
|
assert len(english_courses) == 3
|
||||||
|
for c in english_courses:
|
||||||
|
assert c.teacher is None
|
||||||
|
assert c.classroom is None
|
||||||
|
|
||||||
|
tiyu = [c for c in courses if c.name == "体育 IV"][0]
|
||||||
|
assert tiyu.teacher is None
|
||||||
|
assert tiyu.classroom is None
|
||||||
|
assert tiyu.classes == [6, 7]
|
||||||
46
uv.lock
generated
46
uv.lock
generated
@@ -42,6 +42,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.2.25"
|
version = "2026.2.25"
|
||||||
@@ -201,6 +214,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.135.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -349,9 +378,13 @@ name = "njupt-mcp"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
{ name = "fastapi" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "mcp", extra = ["cli"] },
|
{ name = "mcp", extra = ["cli"] },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -367,11 +400,15 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
||||||
|
{ name = "fastapi", specifier = ">=0.135.2" },
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.7.0" },
|
{ name = "mcp", extras = ["cli"], specifier = ">=1.7.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.0" },
|
{ name = "pydantic", specifier = ">=2.0" },
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" },
|
||||||
|
{ name = "uvicorn", specifier = ">=0.42.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev"]
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
@@ -789,6 +826,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sse-starlette"
|
name = "sse-starlette"
|
||||||
version = "3.3.4"
|
version = "3.3.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user