Files
xiaoqu_mcp_service/xiaoqu-agent-service
..

xiaoqu-agent-service

面向 MCP 客户端(Kimi Code / Claude 等)的小趣业务数据服务:独立 Spring Boot 应用,只读查询现有三库(m0 权限库 / m1 商城库 / m2 业务库),以 MCP(Streamable HTTP)形式对外暴露工具。

设计方案见 ../xiaoqu-franchisee/doc/agent-mcp-service-design.md。当前进度:P3 首批工具完成(P1 骨架 + P2 权限层 + agent.list.query / property.list.query 完整版,均经 curl 实测)。管理后台为 P4,未实现。

技术栈与已验证版本组合

组件 版本
JDK 17(Temurin 17.0.20.1)
Spring Boot 3.5.16
Spring AI(MCP server webmvc) 1.1.8
MyBatis-Plus 3.5.12(mybatis-plus-spring-boot3-starter)
MySQL 驱动 mysql-connector-j(Boot 管理)
Redis spring-boot-starter-data-redis(db5)

⚠️ 版本组合说明:Spring Boot 3.5.x + Spring AI 1.0.0 的 streamable 传输存在 NPE(spring-projects/spring-ai#6274),本工程选用已修复的 Spring AI 1.1.8,2026-09-03 实测 STREAMABLE 握手(initialize → tools/list → tools/call)通过。

工程结构

xiaoqu-agent-service/
├── mvnw / .mvn/wrapper/              # Maven Wrapper(本机无全局 mvn)
├── pom.xml
├── sql/
│   ├── V1__init.sql                  # 自有库 xiaoqu_agent_svc_d 建表(手工执行,未接 flyway)
│   └── seed-dev.sql                  # dev 种子数据(测试 ak + 运营作用域授权)
├── src/main/java/me/iiv/agent/
│   ├── AgentServiceApplication.java
│   ├── config/                       # M0/M1/M2/SvcDataSourceConfig(独立 SqlSessionFactory + MapperScan)、McpToolConfig
│   ├── mcp/                          # ToolObject 标记、@XqTool 元数据、ToolSyncService(启动同步 tool 表,只插不删)
│   │   ├── GuardedToolCallback(Provider)  # 统一包装:scope 强制校验 + 审计
│   │   └── tool/                     # @Tool 工具(SysTools: sys.ping;AgentTools: agent.count / agent.list.query)
│   ├── security/                     # AkAuthFilter(/mcp ak→401/AuthContext)、AuthService(ak 解析+用户加载)、ToolGuard
│   ├── permission/                   # PermissionService:行级数据权限引擎(移植 getByIsAgent)
│   ├── common/                       # AuditService(audit_log 落库)
│   ├── admin/                        # P4 占位包
│   ├── entity/m0、m2、svc  mapper/m0、m1、m2、svc   # codegen 生成物(提交进仓库)
├── src/main/resources/
│   ├── application.yml               # MCP server: protocol=STREAMABLE, type=SYNC, 端点 /mcp;默认 dev profile
│   └── application-dev.yml           # 四数据源 + Redis,密码用环境变量占位符带默认值
└── src/test/java/me/iiv/agent/codegen/CodeGenerator.java   # run-once 代码生成器

数据源分包约定:entity/m0|m1|m2|svc 与 mapper/... 一一对应各自的 SqlSessionFactory(@MapperScan + sqlSessionFactoryRef)。m0/m1/m2 只读,svc(自有库)读写。

鉴权(P2)

  • /mcp 全路径要求 Authorization: Bearer <ak>,缺失/无效/禁用/过期 → 401 JSON。
  • 判定链(设计文档 §3.2):ak → SHA-256 hex → agent_api_key 匹配 → 解析出人(Redis db5 entity:User:{id} 热缓存优先,fastjson camelCase JSON 用 Jackson 解析;读不到回源 m0 user 表)→ AuthContext 存 ThreadLocal → 工具调用前 scope 校验 → 审计落库。
  • ThreadLocal 实测结论:spring.ai.mcp.server.type=SYNC(application.yml 已显式设置)时,Spring AI 1.1.8 的工具回调在 servlet 请求线程(nio-8088-exec-*)上同步执行,ThreadLocal 的 AuthContext 与 RequestContextHolder 均有效(已用真实 curl 验证:agent.list.query 按人过滤、audit_log 正确落 user_id/token_id/client_ip)。AkAuthFilter 在请求结束 finally 清理。
  • 已知限制:tools/list 暂不按 key 过滤工具列表,所有工具可见;tools/call 时的 scope 强制校验是底线。
  • client_type 识别:X-Client-Type 头优先,否则 User-Agent 含 kimi→mcp-kimi / claude→mcp-claude / curl→cli,其余 unknown。

行级数据权限引擎(permission/PermissionService)

移植老系统 WrapperUtils.getByIsAgent 语义,输出中性 id 集合,调用方拼 wrapper:

方法 老系统来源 语义
isAgentAuthority(authorityId) AuthorityServiceImpl.checkAuthorityIsagent 权限组 isagent=1 → 代理商视角
visibleAgentIds(user) getByIsAgent 主重载 isagent!=1 返回 null(不过滤);否则 自己 + 直属下级(supervisor_id 一层)+ m2 warehouse_agent_auth 代管 manager_agent_id
isSalesNonLeader(user) saleFlag 判定(:703-710) Redis db5 Dictionary hash 的 authDeptId 命中 ",deptId," 且 authDeptPostId 不含 "postId,";字典缺失按不限制处理并告警
applyPropertyRowFilter 销售分支 saleFlag 分支(:716-735,互斥 return) 1:1 复刻(2026-09-03 决策):id IN (create_id=自己 ∪ [代管授权非空时] agent_id IN (下级+代管移除自己) 的物业),空集合则 id IN (0);命中后不再叠加 agent_id/代管物业 OR 结构
isPropertyUser(user) enterprise_type=2 规则(§3.3 通用规则,保留备用) 物业用户;property.list.query 不使用本规则(物业用户不查此列表,与老 /property/listByPage 一致,2026-09-03 决策)

agent.list.query 的行权限照搬老系统 AgentController.listByPage:isagent=1 → supervisor_id = 用户agentId(只查直属下级);isagent=0 → 不过滤;入参传 id → 精确查询(可查到自身,不加 supervisor 限制)。

MCP 工具

工具名 分类 读写 说明
sys.ping system read 服务自检:服务名、版本、当前时间、Redis PING
agent.count system read 查 m0 库 agent 表 soft_delete=0 总数
agent.list.query customer read 代理商分页查询。入参 id/name/level/channelNo/province/city/serialnumber/page/rows;出参 id/name/level/channelNo/serialnumber/supervisorName/province/city/countyseat/contact/phone/agencyarea/memo/createTime。行权限:isagent=1 只查直属下级,id 精确查可看自身。supervisorName 批量 IN 富化
property.list.query customer read 物业(客户)分页查询(m0 properyt 表)。入参 name/agentId/serialnumber/province/city/contact/phone/page/rows;出参 id/name/serialnumber/agentName/createName/province/city/countyseat/address/contact/phone/createTime。行权限走 PermissionService 完整版 getByIsAgent 语义(不含 enterprise_type=2 限制,与老 /property/listByPage 一致)。agentName/createName 批量 IN 富化

分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 notice 字段提示。

工具名使用点号分隔(MCP 规范允许 [a-zA-Z0-9._-],实测 Spring AI 1.1.8 与握手流程均正常)。工具的 category/kind 由代码 @XqTool 注解声明,启动时同步进 tool 表(只插不删,管理员停用状态不会被覆盖)。新工具默认不属于任何作用域 = 任何 token 调不到,需管理员配 scope。

启动

# 首次:建自有库 + 种子数据(dev)
mysql -h192.168.1.181 -uroot -p < sql/V1__init.sql
# 启动一次让 ToolSyncService 同步 tool 表后:
mysql -h192.168.1.181 -uroot -p xiaoqu_agent_svc_d < sql/seed-dev.sql

./mvnw clean package -DskipTests
java -jar target/xiaoqu-agent-service.jar
# 或
./mvnw spring-boot:run
# 后台常驻(本机兼任 dev 部署机时):
nohup java -jar target/xiaoqu-agent-service.jar > target/app.log 2>&1 &
  • 默认 spring.profiles.active=dev,端口 8088,MCP 端点 http://localhost:8088/mcp。
  • 密码默认值仅供本地 dev,环境变量覆盖:M0_DB_PASSWORD / M1_DB_PASSWORD / M2_DB_PASSWORD / SVC_DB_PASSWORD / REDIS_PASSWORD。

测试 ak(仅 dev,明文只在此处与 sql/seed-dev.sql 出现,服务端只存 SHA-256)

用途 用户 ak
总部视角(isagent=0) 46 代理商管理端 xqak_1d21057ad83203dbeb767eb5069701046b27debdc08a733c
代理商视角(isagent=1,agent_id=1,26 个直属下级) 1000 斤斤计较 xqak_7df02ccee2133dc2cf951db12e3958557bdf2d30a91e7f08
无任何 scope(验证拒绝) 1000 斤斤计较 xqak_5bdc0eac8c1cab535f22dc3e9757f72b3127de43a14997a7
销售非负责人(dept 4/post 103,代管 agent 19) 1727 北京销售1 xqak_cb006faf86c0db6f2f47b3eb8872f8b5d495b0c05a6fb52c

curl 验证(Streamable HTTP 握手)

# 1) initialize(从响应头取 Mcp-Session-Id)
curl -i -X POST http://localhost:8088/mcp \
  -H 'Authorization: Bearer <ak>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'

# 2) notifications/initialized → 3) tools/list → 4) tools/call,均带:
#    -H 'Authorization: Bearer <ak>' -H 'Mcp-Session-Id: <session-id>'
curl -X POST http://localhost:8088/mcp \
  -H 'Authorization: Bearer <ak>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Mcp-Session-Id: <session-id>' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"agent.list.query","arguments":{"rows":100}}}'

响应为 SSE 帧(event: message / data: {...})。2026-09-03 实测:无 ak/无效 ak → 401;总部 ak 查 agent.list.query total=62(全量);代理商 ak total=26(与 SELECT COUNT(*) FROM agent WHERE soft_delete=0 AND supervisor_id=1 一致);无 scope 的 ak → isError:true + "该 token 的作用域不包含工具 [agent.list.query],请联系管理员在对应作用域开通";audit_log 记录 success/denied 与 client_type。

property.list.query 权限隔离实测(2026-09-03,均与直连 SQL 核对一致):

条件 总部 ak 代理商 ak
无过滤 total=141 total=73
province=北京市 total=38 total=31
name 模糊 '%测试%' — total=7(与 SQL 一致)

agent.list.query 抽测:channel_no='D' + province='北京市' → 总部 11 / 代理商 9(均与 SQL 一致);id=79 精确查 → 1;rows=500 → 截断为 100 并返回 notice。

客户端接入(dev 阶段裸跑 HTTP)

dev 服务地址示例:http://192.168.4.236:8088/mcp(本机内网 IP;本机自测可用 localhost)。生产必须 HTTPS(Nginx 终结 TLS)。

Kimi Code — 项目级 .kimi-code/mcp.json:

{
  "mcpServers": {
    "xiaoqu": {
      "url": "http://192.168.4.236:8088/mcp",
      "bearerTokenEnvVar": "XQ_API_KEY"
    }
  }
}

shell 里 export XQ_API_KEY=<测试ak> 后启动 Kimi Code 即可自发现工具。

Claude Code:

export XQ_API_KEY=<测试ak>
claude mcp add --transport http xiaoqu http://192.168.4.236:8088/mcp \
  --header "Authorization: Bearer $XQ_API_KEY"

codegen(run-once)

表白名单:m0 user, agent, authority, properyt, agent_manage_district_auth;m2 warehouse_agent_auth;svc agent_api_key, agent_scope, tool, scope_tool, token_scope, audit_log, admin_user(注:物业表真实表名为老系统遗留拼写 properyt)。生成物落在 src/main/java 对应分包并提交进仓库。

# 生成(依赖已下载后执行;连 dev 库)
./mvnw -q dependency:build-classpath -Dmdep.includeScope=test -Dmdep.outputFile=target/cp.txt
javac -cp "$(cat target/cp.txt)" -d target/codegen-classes src/test/java/me/iiv/agent/codegen/CodeGenerator.java
java -cp "target/codegen-classes:$(cat target/cp.txt)" me.iiv.agent.codegen.CodeGenerator

重新生成前请先删除目标 entity/mapper 文件,避免覆盖人工改动。