@ -56,6 +56,11 @@ public class McpToolScanner {
@@ -56,6 +56,11 @@ public class McpToolScanner {
@Value ( "${apelet.mcp.default-user-id:0}" )
private Long defaultUserId ;
/ * *
* 管理类工具的名称前缀 ( 逗号分隔 ) , 命中者仅允许管理员调用 。 默认覆盖 OpenAPI 应用 / 接口配置管理工具 。
* /
@Value ( "${apelet.mcp.admin-only-tool-prefixes:openapi_app_,openapi_config_}" )
private String adminOnlyToolPrefixes ;
/ * *
* 已组装好的工具注册项 ( 供 spec . tools 批量注入 ) 。
* /
@Getter
@ -326,9 +331,18 @@ public class McpToolScanner {
@@ -326,9 +331,18 @@ public class McpToolScanner {
* /
private McpSchema . CallToolResult invoke ( Object bean , Method method , List < McpToolArgInfo > argInfos , Map < String , Object > args ) {
try {
//todo 这里要求带上token MCP 无 HTTP 登录态,注入默认用户 TokenData,避免服务内 takeFromRequest() 拿到 null 抛 NPE。
TokenData tokenData = new TokenData ( ) ;
tokenData . setUserId ( this . defaultUserId ) ;
// 优先复用当前请求上下文中的 TokenData —— HTTP 调用链路上,拦截器已完成鉴权并注入真实身份,
// 这里若再造一个空对象会把真实身份覆盖掉,导致工具以匿名身份执行、下游数据权限取不到会话。
// 只有在确实没有请求上下文时(如未经 HTTP 链路的直调),才回落默认用户,避免 takeFromRequest() 拿到 null 抛 NPE。
TokenData existing = TokenData . takeFromRequest ( ) ;
TokenData tokenData = existing ! = null & & existing . getUserId ( ) ! = null ? existing : this . buildDefaultTokenData ( ) ;
McpToolClass ann = method . getAnnotation ( McpToolClass . class ) ;
String toolName = ann = = null ? method . getName ( ) : ann . name ( ) ;
if ( ! this . isToolAllowed ( toolName , tokenData ) ) {
log . warn ( "MCP 工具 [{}] 调用被拒:当前身份无权调用管理类工具" , toolName ) ;
return new McpSchema . CallToolResult ( Collections . singletonList (
new McpSchema . TextContent ( "当前身份无权调用该工具!管理类工具仅允许管理员调用。" ) ) , true ) ;
}
return this . withRequestContext ( tokenData , ( ) - > {
Object [ ] invokeArgs = this . bindArgs ( args , argInfos , method . getParameterCount ( ) ) ;
Object result = method . invoke ( bean , invokeArgs ) ;
@ -343,6 +357,53 @@ public class McpToolScanner {
@@ -343,6 +357,53 @@ public class McpToolScanner {
}
/ * *
* 构造兜底 TokenData : 仅在请求上下文缺失时使用 , 保证服务内 { @code takeFromRequest ( ) } 不返回 null 。
*
* @return 仅带默认用户Id的 TokenData 。
* /
private TokenData buildDefaultTokenData ( ) {
TokenData tokenData = new TokenData ( ) ;
tokenData . setUserId ( this . defaultUserId ) ;
return tokenData ;
}
/ * *
* 工具级授权 : 管理类工具仅允许管理员调用 。
* < p > 存在这条约束的原因 : HTTP 调用链路上 , MCP 端点已从登录态拦截器排除 、 不再做 "菜单×URL" 越权校验 ,
* 若不在工具层补一道 , 任何能通过端点鉴权的身份 ( 含人员密钥 ) 都能调用应用 / 接口配置等管理类工具 ,
* 进而读出应用明文密钥 、 提权为应用级凭证 。 < / p >
* < p > 兜底 TokenData ( 匿名身份 ) 的 isAdmin 为 null , 同样会被拒绝 。 < / p >
*
* @param toolName 工具名称 。
* @param tokenData 当前调用身份 。
* @return 允许调用返回true , 否则false 。
* /
private boolean isToolAllowed ( String toolName , TokenData tokenData ) {
if ( ! this . isAdminOnlyTool ( toolName ) ) {
return true ;
}
return Boolean . TRUE . equals ( tokenData . getIsAdmin ( ) ) ;
}
/ * *
* 判断工具是否属于 "管理类" , 按配置的前缀匹配 。
*
* @param toolName 工具名称 。
* @return 属于管理类返回true , 否则false 。
* /
private boolean isAdminOnlyTool ( String toolName ) {
if ( StrUtil . isBlank ( this . adminOnlyToolPrefixes ) | | StrUtil . isBlank ( toolName ) ) {
return false ;
}
for ( String prefix : StrUtil . split ( this . adminOnlyToolPrefixes , ',' ) ) {
if ( StrUtil . isNotBlank ( prefix ) & & toolName . startsWith ( prefix . trim ( ) ) ) {
return true ;
}
}
return false ;
}
/ * *
* 在指定 TokenData 的请求上下文下执行动作 : 当前线程没有请求上下文时挂动态代理的 request / response ,
* 执行完恢复原样 。
* /