当前位置: 首页 > news >正文

MCP Java Sdk 添加key认证

api key认证

api key是一种用户认证的标识符,用户访问api时携带key来认证身份,api key一般是长期可用的
api key需要保证唯一性,随机性.唯一性是因为要和用户对应,随机性是为了避免被暴力猜测出来.

实现:
一般需要在数据库维护记录api key,并记录和用户,权限的关系.
需要提供重置api key,验证 key的接口.

api key微服务流转

由于项目是微服务项目,所以key传递到mcp服务后,mcp调用其他微服务也需要携带.一般情况,我们可以使用线程变量保存到每个线程,但是mcp sdk在调用tool时创建了新线程,需要我们手动维护sessionId和key的映射关系,通过sessionId传递正确的key到下游服务.

记录sessionId和key:

    @Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {response.setContentType("text/event-stream; charset=utf-8");HttpServletRequest httpRequest = (HttpServletRequest) request;//存储sessionId和key映射。并传递sessionId到tool调用线程。String sessionId = httpRequest.getParameter(SESSION_ID_PARAM);String key = httpRequest.getHeader(KEY_HEADER);if (sessionId != null && key != null) {SessionKeyMap.setKey(sessionId, key);log.debug("Set key for sessionId: {}", sessionId);HttpServletRequest wrappedRequest = new BodyRequestWrapper(httpRequest, sessionId);httpRequest = wrappedRequest;}String uri = httpRequest.getRequestURI();if (!uri.equals("/sse")) {chain.doFilter(httpRequest, response);return;}//处理sse响应,追加key后缀参数。HttpServletResponse httpRes = (HttpServletResponse)response;PrintWriter sseWriter = new SSEWriter(httpRes.getWriter(), key);HttpServletResponseWrapper sseResponse = new HttpServletResponseWrapper(httpRes) {@Overridepublic PrintWriter getWriter() {return sseWriter;}};chain.doFilter(httpRequest, sseResponse);}

BodyRequestWrapper


/*** @author twei*/
public class BodyRequestWrapper extends HttpServletRequestWrapper {private final byte[] body;public BodyRequestWrapper(HttpServletRequest request, String mcpSessionId) throws IOException {super(request);// 1. 获取原始bodyString oldBody = request.getReader().lines().reduce("", (accumulator, actual) -> accumulator + actual);// 2. 解析为MapObjectMapper mapper = new ObjectMapper();Map<String, Object> jsonMap;if (oldBody != null && !oldBody.isEmpty()) {jsonMap = mapper.readValue(oldBody, Map.class);} else {jsonMap = new java.util.HashMap<>();}// ======= 关键逻辑移到这里,跳过initialize =======Object methodObj = jsonMap.get("method");if (methodObj instanceof String && !((String) methodObj).equals("tools/call")) {// 如果包含 initialize,则直接保留原 body,不处理 paramsthis.body = oldBody.getBytes(StandardCharsets.UTF_8);return;}// 重点:把 mcpSessionId 放到 params 里Object paramsObj = jsonMap.get("params");if (paramsObj instanceof Map) {Map<String, Object> paramsMap = (Map<String, Object>) paramsObj;Object argumentsObj = paramsMap.get("arguments");if (argumentsObj instanceof Map) {((Map<String, Object>) argumentsObj).put("mcpSessionId", mcpSessionId);}}// 4. 重新序列化String newBody = mapper.writeValueAsString(jsonMap);body = newBody.getBytes(StandardCharsets.UTF_8);}@Overridepublic ServletInputStream getInputStream() {ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Override public boolean isFinished() { return bais.available() == 0; }@Override public boolean isReady() { return true; }@Override public void setReadListener(ReadListener listener) {}@Override public int read() { return bais.read(); }};}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream()));}
}

进入tool执行线程后,设置到线程变量MDC中.

  private void registerTool(McpSyncServer mcpSyncServer, Object bean, Method method, McpTool annotation) {String toolName = method.getName();String description = annotation.value().isEmpty() ? extractMethodDescription(method) : annotation.value();String jsonSchema = generateInputSchema(method);McpSchema.Tool tool = new McpSchema.Tool.Builder().name(toolName).description(description).inputSchema(jsonSchema).build();BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> call = (exchange,arguments) -> {try {String mcpSessionId = (String) arguments.get("mcpSessionId");MDC.put("McpSessionId",mcpSessionId);log.info("call " + bean + " " + method);// Convert arguments to match method parameter typesObject[] params = new Object[method.getParameterCount()];Parameter[] parameters = method.getParameters();for (int i = 0; i < parameters.length; i++) {String paramName = parameters[i].getName();Class<?> paramType = parameters[i].getType();Object argValue = arguments.get(paramName);if (argValue != null) {if (paramType == String.class) {params[i] = argValue.toString();} else if (paramType == Integer.class || paramType == int.class) {params[i] = Integer.parseInt(argValue.toString());} else if (paramType == Long.class || paramType == long.class) {params[i] = Long.parseLong(argValue.toString());} else if (paramType == Double.class || paramType == double.class) {params[i] = Double.parseDouble(argValue.toString());} else if (paramType == Boolean.class || paramType == boolean.class) {params[i] = Boolean.parseBoolean(argValue.toString());} else if (paramType == LocalDate.class) {params[i] = LocalDate.parse(argValue.toString());} else if (paramType == LocalDateTime.class) {params[i] = LocalDateTime.parse(argValue.toString());} else {throw new IllegalArgumentException("不支持的mcptool参数类型,只支持基础类型。【" + paramType.getSimpleName() + "】");}}}// 检查必填参数StringBuilder missingFields = new StringBuilder();for (int i = 0; i < parameters.length; i++) {Required required = parameters[i].getAnnotation(Required.class);if (required != null && params[i] == null) {if (missingFields.length() > 0) {missingFields.append(", ");}missingFields.append(parameters[i].getName());}}if (missingFields.length() > 0) {return new McpSchema.CallToolResult(missingFields.toString() + "为必填字段", false);}Object result = method.invoke(bean, params);log.info("call " + bean + " " + method + " " + params + " result: " + result);return new McpSchema.CallToolResult(result.toString(), false);} catch (Exception e) {return new McpSchema.CallToolResult(e.getMessage(), true);}};mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(tool, call));}

向下游发送feign请求时,获取线程变量中的sessionId映射得到key,并在请求头携带key

public class FeignInterceptorConfiguration implements RequestInterceptor {public RequestInterceptor requestInterceptor() {if ("mcp".equals(this.applicationName)) {traceId = MDC.get("McpSessionId");if (traceId != null) {String key = SessionKeyMap.getKey(traceId);if (key != null) {requestTemplate.header("Key", key);}}}}
}

mcp sdk兼容api key

兼容key使用见上文的fdoFilter,在访问sse接口时,向url末尾追加key信息.从而让client访问/mcp/message时能通过认证.

mcp java sdk

HttpServletSseServerTransportProvider是sdk中处理请求的核心类,doGet处理sso等方法,doPost会处理/mcp/message等方法.sendEvent用于发送一个sse事件.sessionFactory用于管理每个session.

@WebServlet(asyncSupported = true)
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider
http://www.dtcms.com/a/357048.html

相关文章:

  • CMake构建学习笔记22-libxml2库的构建
  • 【链表 - LeetCode】146. LRU 缓存
  • Prometheus+Grafana入门教程:从零搭建云原生服务器监控系统
  • 如何管理跨境电商多语种素材?数字资产本地化指南
  • nacos单机部署并开启鉴权
  • #医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(五)
  • 机器学习 - Kaggle项目实践(5)Quora Question Pairs 文本相似
  • OpenCV轮廓近似与Python命令行参数解析
  • 玳瑁的嵌入式日记D29-0829(进程间通信)
  • ZooKeeper 安装配置
  • idea2025.2中maven编译中文乱码
  • Altium Designer 22使用笔记(10)---PCB铺铜相关操作
  • c++ const 关键字
  • 聊聊Prompt Engineering (提示词工程)
  • 【工具类】得到多个数组中的相同元素
  • 考研数据结构Part3——二叉树知识点总结
  • Vue学习Ⅳ
  • 二手车估值查询-二手车估值api接口
  • el-table实现双击编辑-el-select选择框+输入框限制非负两位小数
  • HunyuanVideo-Foley视频音效生成模型介绍与部署
  • 非标设计 机架模板 misumi 设计组合案例
  • 浏览器自动化工具怎么选?MCP 控制浏览器 vs Selenium 深度对比
  • 预测模型及超参数:3.集成学习:[1]LightGBM
  • LangChain实战(三):深入理解Model I/O - Prompts模板
  • 顶会顶刊图像分类的云服务器训练方法
  • 闭包与内存泄漏:深度解析与应对策略
  • Spring boot 启用第二数据源
  • Java全栈工程师的实战面试:从基础到微服务架构
  • 【SOD】目标检测
  • 2025.8.29机械臂实战项目