[ISSUE#13322] Refactor a2a registry implementation (#13794)

* Support a2a client api.

* Support query agent card with service type agent card.

* Refactor agent name encode to id by AgentIdCodec.

* Rename admin/console api parameter name -> agentName.

* Support agent name check.

* Fix pmd

* Fix unit test.

* Fix unit test.
This commit is contained in:
杨翊 SionYang 2025-09-11 16:05:11 +08:00 committed by GitHub
parent 2163a6b337
commit 9df5bcc1ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 2095 additions and 398 deletions

View File

@ -96,5 +96,13 @@ public class Constants {
public static final String SEARCH_BLUR = "blur";
public static final String SEARCH_ACCURATE = "accurate";
public static final String AGENT_ENDPOINT_GROUP = "agent-endpoints";
public static final String AGENT_ENDPOINT_PATH_KEY = "__nacos.agent.endpoint.path__";
public static final String AGENT_ENDPOINT_TRANSPORT_KEY = "__nacos.agent.endpoint.transport__";
public static final String NACOS_AGENT_ENDPOINT_SUPPORT_TLS = "__nacos.agent.endpoint.supportTls__";
}
}

View File

@ -21,7 +21,8 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
import com.alibaba.nacos.ai.service.A2aServerOperationService;
import com.alibaba.nacos.ai.param.AgentHttpParamExtractor;
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
@ -34,6 +35,7 @@ import com.alibaba.nacos.api.model.Page;
import com.alibaba.nacos.api.model.v2.Result;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.core.model.form.PageForm;
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.ApiType;
import com.alibaba.nacos.plugin.auth.constant.SignType;
@ -54,6 +56,7 @@ import java.util.List;
@NacosApi
@RestController
@RequestMapping(Constants.A2A.ADMIN_PATH)
@ExtractorManager.Extractor(httpExtractor = AgentHttpParamExtractor.class)
public class A2aAdminController {
private final A2aServerOperationService a2aServerOperationService;
@ -90,7 +93,8 @@ public class A2aAdminController {
public Result<AgentCardDetailInfo> getAgentCard(AgentForm form) throws NacosApiException {
form.validate();
return Result.success(
a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getName(), form.getVersion()));
a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getAgentName(), form.getVersion(),
form.getRegistrationType()));
}
/**
@ -121,7 +125,7 @@ public class A2aAdminController {
@Secured(action = ActionTypes.WRITE, signType = SignType.AI, apiType = ApiType.ADMIN_API)
public Result<String> deleteAgent(AgentForm form) throws NacosException {
form.validate();
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getName(), form.getVersion());
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getAgentName(), form.getVersion());
return Result.success("ok");
}
@ -140,7 +144,7 @@ public class A2aAdminController {
agentListForm.validate();
pageForm.validate();
return Result.success(
a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getName(),
a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
agentListForm.getSearch(), pageForm.getPageNo(), pageForm.getPageSize()));
}
@ -156,6 +160,6 @@ public class A2aAdminController {
public Result<List<AgentVersionDetail>> listAgentVersions(AgentForm agentForm) throws NacosException {
agentForm.validate();
return Result.success(
a2aServerOperationService.listAgentVersions(agentForm.getNamespaceId(), agentForm.getName()));
a2aServerOperationService.listAgentVersions(agentForm.getNamespaceId(), agentForm.getAgentName()));
}
}

View File

@ -80,7 +80,7 @@ public class AgentDetailForm extends AgentForm {
public void validate() throws NacosApiException {
fillDefaultNamespaceId();
if (StringUtils.isEmpty(super.getName())) {
if (StringUtils.isEmpty(super.getAgentName())) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter 'name' type String is not present");
}

View File

@ -40,7 +40,7 @@ public class AgentForm implements NacosForm {
private String namespaceId;
private String name;
private String agentName;
private String version;
@ -49,7 +49,7 @@ public class AgentForm implements NacosForm {
@Override
public void validate() throws NacosApiException {
fillDefaultNamespaceId();
if (StringUtils.isEmpty(name)) {
if (StringUtils.isEmpty(agentName)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter 'name' type String is not present");
}
@ -69,12 +69,12 @@ public class AgentForm implements NacosForm {
this.namespaceId = namespaceId;
}
public String getName() {
return name;
public String getAgentName() {
return agentName;
}
public void setName(String name) {
this.name = name;
public void setAgentName(String agentName) {
this.agentName = agentName;
}
public String getVersion() {
@ -99,13 +99,13 @@ public class AgentForm implements NacosForm {
return false;
}
AgentForm agentForm = (AgentForm) o;
return Objects.equals(namespaceId, agentForm.namespaceId) && Objects.equals(name, agentForm.name)
return Objects.equals(namespaceId, agentForm.namespaceId) && Objects.equals(agentName, agentForm.agentName)
&& Objects.equals(version, agentForm.version) && Objects.equals(registrationType,
agentForm.registrationType);
}
@Override
public int hashCode() {
return Objects.hash(namespaceId, name, version, registrationType);
return Objects.hash(namespaceId, agentName, version, registrationType);
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.param;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.common.paramcheck.ParamInfo;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.core.paramcheck.AbstractHttpParamExtractor;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
/**
* Nacos AI Agent or AgentCard param extractor.
*
* @author xiweng.yy
*/
public class AgentHttpParamExtractor extends AbstractHttpParamExtractor {
private static final String AGENT_CARD_PARAM = "agentCard";
@Override
public List<ParamInfo> extractParam(HttpServletRequest request) throws NacosException {
ParamInfo paramInfo = new ParamInfo();
paramInfo.setNamespaceId(request.getParameter("namespaceId"));
paramInfo.setAgentName(request.getParameter("agentName"));
if (request.getParameterMap().containsKey(AGENT_CARD_PARAM)) {
paramInfo.setAgentName(deserializeAndGetAgentName(request.getParameter(AGENT_CARD_PARAM)));
}
return Collections.singletonList(paramInfo);
}
private String deserializeAndGetAgentName(String agentCardJson) {
try {
AgentCard agentCard = JacksonUtils.toObj(agentCardJson, AgentCard.class);
return agentCard.getName();
} catch (Exception ignored) {
return StringUtils.EMPTY;
}
}
}

View File

@ -20,7 +20,6 @@ import com.alibaba.nacos.ai.index.McpServerIndex;
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
import com.alibaba.nacos.ai.service.McpServerOperationService;
import com.alibaba.nacos.ai.utils.McpRequestUtil;
import com.alibaba.nacos.ai.utils.McpRequestUtils;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.mcp.FrontEndpointConfig;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
@ -78,7 +77,7 @@ public class McpServerEndpointRequestHandler
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
public McpServerEndpointResponse handle(McpServerEndpointRequest request, RequestMeta meta) throws NacosException {
McpRequestUtils.fillNamespaceId(request);
McpRequestUtil.fillNamespaceId(request);
try {
checkParameters(request);
Instance instance = buildInstance(request);

View File

@ -19,7 +19,7 @@ package com.alibaba.nacos.ai.remote.handler;
import com.alibaba.nacos.ai.index.McpServerIndex;
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
import com.alibaba.nacos.ai.service.McpServerOperationService;
import com.alibaba.nacos.ai.utils.McpRequestUtils;
import com.alibaba.nacos.ai.utils.McpRequestUtil;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
import com.alibaba.nacos.api.ai.remote.request.QueryMcpServerRequest;
import com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse;
@ -58,7 +58,7 @@ public class QueryMcpServerRequestHandler extends RequestHandler<QueryMcpServerR
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
@Secured(action = ActionTypes.READ, signType = SignType.AI)
public QueryMcpServerResponse handle(QueryMcpServerRequest request, RequestMeta meta) throws NacosException {
McpRequestUtils.fillNamespaceId(request);
McpRequestUtil.fillNamespaceId(request);
if (StringUtils.isBlank(request.getMcpName())) {
QueryMcpServerResponse errorResponse = new QueryMcpServerResponse();
errorResponse.setErrorInfo(NacosException.INVALID_PARAM, "parameters `mcpName` can't be empty or null");

View File

@ -20,7 +20,7 @@ import com.alibaba.nacos.ai.index.McpServerIndex;
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
import com.alibaba.nacos.ai.service.McpEndpointOperationService;
import com.alibaba.nacos.ai.service.McpServerOperationService;
import com.alibaba.nacos.ai.utils.McpRequestUtils;
import com.alibaba.nacos.ai.utils.McpRequestUtil;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
@ -73,7 +73,7 @@ public class ReleaseMcpServerRequestHandler extends RequestHandler<ReleaseMcpSer
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
public ReleaseMcpServerResponse handle(ReleaseMcpServerRequest request, RequestMeta meta) throws NacosException {
McpRequestUtils.fillNamespaceId(request);
McpRequestUtil.fillNamespaceId(request);
try {
checkParameters(request);
return doHandler(request, meta);

View File

@ -0,0 +1,139 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.remote.handler.a2a;
import com.alibaba.nacos.ai.constant.Constants;
import com.alibaba.nacos.ai.service.a2a.identity.AgentIdCodecHolder;
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
import com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest;
import com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.api.model.v2.ErrorCode;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.api.remote.request.RequestMeta;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
import com.alibaba.nacos.core.remote.RequestHandler;
import com.alibaba.nacos.naming.core.v2.pojo.Service;
import com.alibaba.nacos.naming.core.v2.service.impl.EphemeralClientOperationServiceImpl;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Register or Deregister endpoint for agent to nacos AI module request handler.
*
* @author xiweng.yy
*/
@Component
public class AgentEndpointRequestHandler extends RequestHandler<AgentEndpointRequest, AgentEndpointResponse> {
private static final Logger LOGGER = LoggerFactory.getLogger(AgentEndpointRequestHandler.class);
private final EphemeralClientOperationServiceImpl clientOperationService;
private final AgentIdCodecHolder agentIdCodecHolder;
public AgentEndpointRequestHandler(EphemeralClientOperationServiceImpl clientOperationService,
AgentIdCodecHolder agentIdCodecHolder) {
this.clientOperationService = clientOperationService;
this.agentIdCodecHolder = agentIdCodecHolder;
}
@Override
@NamespaceValidation
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
public AgentEndpointResponse handle(AgentEndpointRequest request, RequestMeta meta) throws NacosException {
AgentEndpointResponse response = new AgentEndpointResponse();
AgentRequestUtil.fillNamespaceId(request);
try {
validateRequest(request);
Instance instance = transferInstance(request);
String serviceName =
agentIdCodecHolder.encode(request.getAgentName()) + "::" + request.getEndpoint().getVersion();
Service service = Service.newService(request.getNamespaceId(), Constants.A2A.AGENT_ENDPOINT_GROUP,
serviceName);
switch (request.getType()) {
case AiRemoteConstants.REGISTER_ENDPOINT:
doRegisterEndpoint(service, instance, meta);
break;
case AiRemoteConstants.DE_REGISTER_ENDPOINT:
doDeregisterEndpoint(service, instance, meta);
break;
default:
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_VALIDATE_ERROR,
String.format("parameter `type` should be %s or %s, but was %s",
AiRemoteConstants.REGISTER_ENDPOINT, AiRemoteConstants.DE_REGISTER_ENDPOINT,
request.getType()));
}
} catch (NacosApiException e) {
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
LOGGER.error("[{}] Register agent endpoint to agent {} error: {}", meta.getConnectionId(),
request.getAgentName(), e.getErrMsg());
}
return response;
}
private Instance transferInstance(AgentEndpointRequest request) throws NacosApiException {
Instance instance = new Instance();
AgentEndpoint endpoint = request.getEndpoint();
instance.setIp(endpoint.getAddress());
instance.setPort(endpoint.getPort());
String path = StringUtils.isBlank(endpoint.getPath()) ? StringUtils.EMPTY : endpoint.getPath();
Map<String, String> metadata = Map.of(Constants.A2A.AGENT_ENDPOINT_PATH_KEY, path,
Constants.A2A.AGENT_ENDPOINT_TRANSPORT_KEY, endpoint.getTransport(),
Constants.A2A.NACOS_AGENT_ENDPOINT_SUPPORT_TLS, String.valueOf(endpoint.isSupportTls()));
instance.setMetadata(metadata);
instance.validate();
return instance;
}
private void validateRequest(AgentEndpointRequest request) throws NacosApiException {
if (StringUtils.isBlank(request.getAgentName())) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter `agentName` can't be empty or null");
}
if (null == request.getEndpoint()) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter `endpoint` can't be null");
}
if (StringUtils.isBlank(request.getEndpoint().getVersion())) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter `endpoint.version` can't be empty or null");
}
}
private void doRegisterEndpoint(Service service, Instance instance, RequestMeta meta) throws NacosException {
clientOperationService.registerInstance(service, instance, meta.getConnectionId());
}
private void doDeregisterEndpoint(Service service, Instance instance, RequestMeta meta) {
clientOperationService.deregisterInstance(service, instance, meta.getConnectionId());
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.remote.handler.a2a;
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest;
import com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.remote.request.RequestMeta;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
import com.alibaba.nacos.core.remote.RequestHandler;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Nacos AI module query agent card request handler.
*
* @author xiweng.yy
*/
@Component
public class QueryAgentCardRequestHandler extends RequestHandler<QueryAgentCardRequest, QueryAgentCardResponse> {
private static final Logger LOGGER = LoggerFactory.getLogger(QueryAgentCardRequestHandler.class);
private final A2aServerOperationService a2aServerOperationService;
public QueryAgentCardRequestHandler(A2aServerOperationService a2aServerOperationService) {
this.a2aServerOperationService = a2aServerOperationService;
}
@Override
@NamespaceValidation
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
@Secured(action = ActionTypes.READ, signType = SignType.AI)
public QueryAgentCardResponse handle(QueryAgentCardRequest request, RequestMeta meta) throws NacosException {
AgentRequestUtil.fillNamespaceId(request);
if (StringUtils.isBlank(request.getAgentName())) {
QueryAgentCardResponse errorResponse = new QueryAgentCardResponse();
errorResponse.setErrorInfo(NacosException.INVALID_PARAM, "parameters `agentName` can't be empty or null");
return errorResponse;
}
return doHandler(request);
}
private QueryAgentCardResponse doHandler(QueryAgentCardRequest request) {
QueryAgentCardResponse response = new QueryAgentCardResponse();
try {
AgentCardDetailInfo result = a2aServerOperationService.getAgentCard(request.getNamespaceId(),
request.getAgentName(), request.getVersion(), request.getRegistrationType());
response.setAgentCardDetailInfo(result);
} catch (NacosException e) {
LOGGER.error("Query agent card for agent {} error: {}", request.getAgentName(), e.getErrMsg());
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
}
return response;
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.remote.handler.a2a;
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
import com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.api.model.v2.ErrorCode;
import com.alibaba.nacos.api.remote.request.RequestMeta;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
import com.alibaba.nacos.core.remote.RequestHandler;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Nacos AI module release agent card request handler.
*
* @author xiweng.yy
*/
@Component
public class ReleaseAgentCardRequestHandler extends RequestHandler<ReleaseAgentCardRequest, ReleaseAgentCardResponse> {
private static final Logger LOGGER = LoggerFactory.getLogger(ReleaseAgentCardRequestHandler.class);
private final A2aServerOperationService a2aServerOperationService;
public ReleaseAgentCardRequestHandler(A2aServerOperationService a2aServerOperationService) {
this.a2aServerOperationService = a2aServerOperationService;
}
@Override
@NamespaceValidation
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
public ReleaseAgentCardResponse handle(ReleaseAgentCardRequest request, RequestMeta meta) throws NacosException {
AgentRequestUtil.fillNamespaceId(request);
ReleaseAgentCardResponse response = new ReleaseAgentCardResponse();
try {
validateRequest(request);
doHandler(request, meta);
return response;
} catch (NacosException e) {
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
LOGGER.error("[{}] Release agent card {} error: {}", meta.getConnectionId(),
null == request.getAgentCard() ? null : JacksonUtils.toJson(request.getAgentCard()), e.getErrMsg());
}
return response;
}
private void validateRequest(ReleaseAgentCardRequest request) throws NacosApiException {
if (null == request.getAgentCard()) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `agentCard` can't be null");
}
AgentRequestUtil.validateAgentCard(request.getAgentCard());
}
private void doHandler(ReleaseAgentCardRequest request, RequestMeta meta) throws NacosException {
String namespaceId = request.getNamespaceId();
AgentCard agentCard = request.getAgentCard();
LOGGER.info("Release new agent {}, version {} into namespaceId {} from connectionId {}.", agentCard.getName(),
agentCard.getVersion(), namespaceId, meta.getConnectionId());
try {
AgentCardDetailInfo existAgentCard = a2aServerOperationService.getAgentCard(namespaceId,
agentCard.getName(), agentCard.getVersion(), StringUtils.EMPTY);
LOGGER.info("AgentCard {} and target version {} already exist.", existAgentCard.getName(),
existAgentCard.getVersion());
} catch (NacosApiException e) {
if (ErrorCode.AGENT_NOT_FOUND.getCode() == e.getDetailErrCode()) {
// agent card not found, create new agent card.
createAgentCard(namespaceId, agentCard, request.getRegistrationType());
LOGGER.info("AgentCard {} released.", agentCard.getName());
} else if (ErrorCode.AGENT_VERSION_NOT_FOUND.getCode() == e.getDetailErrCode()) {
// agent card found but version not found, update agent card.
createNewVersionAgentCard(namespaceId, agentCard, request.getRegistrationType(), request.isSetAsLatest());
LOGGER.info("AgentCard {} new version {} released.", agentCard.getName(), agentCard.getVersion());
} else {
LOGGER.error("AgentCard {} released failed.", agentCard.getName(), e);
throw e;
}
}
}
private void createAgentCard(String namespaceId, AgentCard agentCard, String registrationType)
throws NacosException {
a2aServerOperationService.registerAgent(agentCard, namespaceId, registrationType);
}
private void createNewVersionAgentCard(String namespaceId, AgentCard agentCard, String registrationType,
boolean setAsLatest) throws NacosException {
a2aServerOperationService.updateAgentCard(agentCard, namespaceId, registrationType, setAsLatest);
}
}

View File

@ -12,25 +12,26 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.alibaba.nacos.ai.service;
package com.alibaba.nacos.ai.service.a2a;
import com.alibaba.nacos.ai.constant.Constants;
import com.alibaba.nacos.ai.form.a2a.admin.AgentDetailForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentUpdateForm;
import com.alibaba.nacos.ai.service.SyncEffectService;
import com.alibaba.nacos.ai.service.a2a.identity.AgentIdCodecHolder;
import com.alibaba.nacos.ai.utils.AgentCardUtil;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentInterface;
import com.alibaba.nacos.api.ai.model.a2a.AgentVersionDetail;
import com.alibaba.nacos.api.config.ConfigType;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.api.model.Page;
import com.alibaba.nacos.api.model.v2.ErrorCode;
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.config.server.model.ConfigInfo;
@ -41,10 +42,12 @@ import com.alibaba.nacos.config.server.service.ConfigOperationService;
import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService;
import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest;
import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse;
import com.alibaba.nacos.config.server.utils.ParamUtils;
import com.alibaba.nacos.naming.core.v2.index.ServiceStorage;
import com.alibaba.nacos.naming.core.v2.pojo.Service;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import static com.alibaba.nacos.ai.constant.Constants.A2A.AGENT_GROUP;
import static com.alibaba.nacos.ai.constant.Constants.A2A.AGENT_VERSION_GROUP;
@ -65,24 +68,20 @@ public class A2aServerOperationService {
private final SyncEffectService syncEffectService;
private final ServiceStorage serviceStorage;
private final AgentIdCodecHolder agentIdCodecHolder;
public A2aServerOperationService(ConfigQueryChainService configQueryChainService,
ConfigOperationService configOperationService, ConfigDetailService configDetailService,
SyncEffectService syncEffectService) {
SyncEffectService syncEffectService, ServiceStorage serviceStorage,
AgentIdCodecHolder agentIdCodecHolder) {
this.configQueryChainService = configQueryChainService;
this.configOperationService = configOperationService;
this.configDetailService = configDetailService;
this.syncEffectService = syncEffectService;
}
/**
* Register agent.
*
* @param form agent detail form
* @throws NacosException nacos exception
*/
public void registerAgent(AgentDetailForm form) throws NacosException {
AgentCard agentCard = AgentCardUtil.buildAgentCard(form);
registerAgent(agentCard, form.getNamespaceId(), form.getRegistrationType());
this.serviceStorage = serviceStorage;
this.agentIdCodecHolder = agentIdCodecHolder;
}
/**
@ -120,7 +119,7 @@ public class A2aServerOperationService {
* @throws NacosException nacos exception
*/
public void deleteAgent(String namespaceId, String agentName, String version) throws NacosException {
String encodedName = ParamUtils.encodeName(agentName);
String encodedName = agentIdCodecHolder.encode(agentName);
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(encodedName, AGENT_GROUP,
namespaceId);
@ -173,17 +172,6 @@ public class A2aServerOperationService {
}
}
/**
* Update agent card.
*
* @param form agent update form
* @throws NacosException nacos exception
*/
public void updateAgentCard(AgentUpdateForm form) throws NacosException {
AgentCard agentCard = AgentCardUtil.buildAgentCard(form);
updateAgentCard(agentCard, form.getNamespaceId(), form.getRegistrationType(), form.getSetAsLatest());
}
/**
* Update agent card.
*
@ -256,7 +244,7 @@ public class A2aServerOperationService {
*/
public Page<AgentCardVersionInfo> listAgents(String namespaceId, String agentName, String search, int pageNo,
int pageSize) throws NacosException {
String encodedName = ParamUtils.encodeName(agentName);
String encodedName = agentIdCodecHolder.encode(agentName);
String dataId;
if (StringUtils.isEmpty(encodedName) || Constants.A2A.SEARCH_BLUR.equalsIgnoreCase(search)) {
@ -299,29 +287,30 @@ public class A2aServerOperationService {
* @param namespaceId namespaceId of agent
* @param agentName agent name
* @param version target version of want to query, if is null or empty, get latest version
* @param registrationType registration type
* @return agent card detail info
* @throws NacosApiException nacos api exception
*/
public AgentCardDetailInfo getAgentCard(String namespaceId, String agentName, String version)
throws NacosApiException {
public AgentCardDetailInfo getAgentCard(String namespaceId, String agentName, String version,
String registrationType) throws NacosApiException {
AgentCardVersionInfo agentCardVersionInfo = queryAgentCardVersionInfo(namespaceId, agentName);
return StringUtils.isEmpty(version) ? queryLatestVersion(agentCardVersionInfo, namespaceId)
: queryTargetVersion(agentCardVersionInfo, version, namespaceId);
return StringUtils.isEmpty(version) ? queryLatestVersion(agentCardVersionInfo, namespaceId, registrationType)
: queryTargetVersion(agentCardVersionInfo, version, namespaceId, registrationType);
}
private AgentCardDetailInfo queryLatestVersion(AgentCardVersionInfo agentCardVersionInfo, String namespaceId)
throws NacosApiException {
private AgentCardDetailInfo queryLatestVersion(AgentCardVersionInfo agentCardVersionInfo, String namespaceId,
String registrationType) throws NacosApiException {
String latestVersion = agentCardVersionInfo.getVersionDetails().stream().filter(AgentVersionDetail::isLatest)
.findFirst().orElseThrow(
() -> new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_VERSION_NOT_FOUND,
String.format("Agent %s latest version not found", agentCardVersionInfo.getName())))
.getVersion();
return queryTargetVersion(agentCardVersionInfo, latestVersion, namespaceId);
return queryTargetVersion(agentCardVersionInfo, latestVersion, namespaceId, registrationType);
}
private AgentCardDetailInfo queryTargetVersion(AgentCardVersionInfo agentCardVersionInfo, String version,
String namespaceId) throws NacosApiException {
String versionDataId = ParamUtils.encodeName(agentCardVersionInfo.getName()) + "-" + version;
String namespaceId, String registrationType) throws NacosApiException {
String versionDataId = agentIdCodecHolder.encode(agentCardVersionInfo.getName()) + "-" + version;
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(versionDataId,
AGENT_VERSION_GROUP, namespaceId);
ConfigQueryChainResponse response = configQueryChainService.handle(request);
@ -329,12 +318,45 @@ public class A2aServerOperationService {
throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_VERSION_NOT_FOUND,
String.format("Agent %s version %s not found.", agentCardVersionInfo.getName(), version));
}
return JacksonUtils.toObj(response.getContent(), AgentCardDetailInfo.class);
AgentCardDetailInfo result = JacksonUtils.toObj(response.getContent(), AgentCardDetailInfo.class);
if (StringUtils.isBlank(registrationType)) {
registrationType = result.getRegistrationType();
}
if (AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE.equalsIgnoreCase(registrationType)) {
injectEndpoint(result, namespaceId);
}
return result;
}
private void injectEndpoint(AgentCardDetailInfo agentCard, String namespaceId) {
String serviceName = agentIdCodecHolder.encode(agentCard.getName()) + "::" + agentCard.getVersion();
Service service = Service.newService(namespaceId, Constants.A2A.AGENT_ENDPOINT_GROUP, serviceName);
ServiceInfo serviceInfo = serviceStorage.getData(service);
if (serviceInfo.getHosts().isEmpty()) {
return;
}
List<AgentInterface> allAgentEndpoints = serviceInfo.getHosts().stream().map(AgentCardUtil::buildAgentInterface)
.toList();
agentCard.setAdditionalInterfaces(allAgentEndpoints);
List<AgentInterface> matchTransportEndpoints = allAgentEndpoints.stream()
.filter(agentInterface -> agentInterface.getTransport()
.equalsIgnoreCase(agentCard.getPreferredTransport())).toList();
AgentInterface randomPreferredTransportEndpoint = randomOne(
matchTransportEndpoints.isEmpty() ? allAgentEndpoints : matchTransportEndpoints);
agentCard.setUrl(randomPreferredTransportEndpoint.getUrl());
agentCard.setPreferredTransport(randomPreferredTransportEndpoint.getTransport());
}
/**
* TODO abstract a choose policy.
*/
private AgentInterface randomOne(List<AgentInterface> agentInterfaces) {
return agentInterfaces.get(ThreadLocalRandom.current().nextInt(agentInterfaces.size()));
}
private ConfigForm transferVersionInfoToConfigForm(AgentCardVersionInfo agentCardVersionInfo, String namespaceId) {
ConfigForm configForm = new ConfigForm();
String actualDataId = ParamUtils.encodeName(agentCardVersionInfo.getName());
String actualDataId = agentIdCodecHolder.encode(agentCardVersionInfo.getName());
configForm.setDataId(actualDataId);
configForm.setGroup(AGENT_GROUP);
configForm.setNamespaceId(namespaceId);
@ -349,7 +371,7 @@ public class A2aServerOperationService {
private ConfigForm transferAgentInfoToConfigForm(AgentCardDetailInfo storageInfo, String namespaceId) {
ConfigForm configForm = new ConfigForm();
String actualDataId = ParamUtils.encodeName(storageInfo.getName()) + "-" + storageInfo.getVersion();
String actualDataId = agentIdCodecHolder.encode(storageInfo.getName()) + "-" + storageInfo.getVersion();
configForm.setDataId(actualDataId);
configForm.setGroup(AGENT_VERSION_GROUP);
configForm.setNamespaceId(namespaceId);
@ -362,19 +384,15 @@ public class A2aServerOperationService {
return configForm;
}
private AgentCardVersionInfo queryAgentCardVersionInfo(AgentForm form) throws NacosApiException {
return queryAgentCardVersionInfo(form.getNamespaceId(), form.getName());
}
private AgentCardVersionInfo queryAgentCardVersionInfo(String namespaceId, String name) throws NacosApiException {
// Check if the agent exists
String actualDataId = ParamUtils.encodeName(name);
String actualDataId = agentIdCodecHolder.encode(name);
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(actualDataId,
AGENT_GROUP, namespaceId);
ConfigQueryChainResponse response = configQueryChainService.handle(request);
if (response.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND) {
throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_NOT_FOUND,
"Cannot update agent: Agent not found: " + name);
"Agent not found: " + name);
}
return JacksonUtils.toObj(response.getContent(), AgentCardVersionInfo.class);
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.service.a2a.identity;
/**
* Nacos AI module A2AAgent & AgentCardidentity Codec.
*
* <p>
* Agent and AgentCard allow user custom agent name without limit for now, but no limit means out of control and might cause un-expected behavior.
* So when storage in Nacos, it should be match some word limits.
* We need to encode and decode agent name as the identity to do storage.
* </p>
*
* @author xiweng.yy
*/
public interface AgentIdCodec {
/**
* Encode agent name to identity.
*
* @param agentName agent name
* @return identity encoded from agent name
*/
String encode(String agentName);
/**
* Decode agent id to agent name.
*
* @param agentId agent identity
* @return agent name
*/
String decode(String agentId);
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.service.a2a.identity;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
/**
* The Holder of {@link AgentIdCodec}.
*
* @author xiweng.yy
*/
@Component
public class AgentIdCodecHolder {
private final AgentIdCodec agentIdCodec;
public AgentIdCodecHolder(ObjectProvider<AgentIdCodec> agentIdCodecsProvider) {
this.agentIdCodec = agentIdCodecsProvider.getIfAvailable(AsciiAgentIdCodec::new);
}
/**
* Encode agent name to identity.
*
* @param agentName agent name
* @return identity encoded from agent name
*/
public String encode(String agentName) {
return agentIdCodec.encode(agentName);
}
/**
* Decode agent id to agent name.
*
* @param agentId agent identity
* @return agent name
*/
public String decode(String agentId) {
return agentIdCodec.decode(agentId);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.service.a2a.identity;
import com.alibaba.nacos.config.server.utils.ParamUtils;
import java.util.Set;
/**
* Agent Identity Codec implement by ASCII.
*
* @author xiweng.yy
*/
public class AsciiAgentIdCodec implements AgentIdCodec {
private static final String ENCODE_PREFIX = "_-.SYSENC:";
private static final char ENCODE_MARK_CHAR = '_';
/**
* Come From {@link ParamUtils#validChars} and remove {@link #ENCODE_MARK_CHAR}.
*/
private static final Set<Character> VALID_CHAR = Set.of('-', '.', ':');
@Override
public String encode(String agentName) {
if (ParamUtils.isValid(agentName)) {
return agentName;
}
StringBuilder sb = new StringBuilder(ENCODE_PREFIX);
for (char ch : agentName.toCharArray()) {
if (Character.isLetterOrDigit(ch) || VALID_CHAR.contains(ch)) {
// Keep letters, numbers, valid characters and non-underscores
sb.append(ch);
} else {
sb.append(ENCODE_MARK_CHAR).append(String.format("%04x", (int) ch));
}
}
return sb.toString();
}
@Override
public String decode(String agentId) {
if (!isEncoded(agentId)) {
return agentId;
}
String body = agentId.substring(ENCODE_PREFIX.length());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < body.length(); ) {
char ch = body.charAt(i);
if (ch == '_' && i + 5 <= body.length()) {
String hexPart = body.substring(i + 1, i + 5);
if (isHex(hexPart)) {
try {
int codePoint = Integer.parseInt(hexPart, 16);
sb.append((char) codePoint);
i += 5;
continue;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid encoded name");
}
}
}
sb.append(ch);
i++;
}
return sb.toString();
}
private boolean isHex(String s) {
for (char c : s.toCharArray()) {
if (!Character.isDigit(c) && !isHexLetter(c)) {
return false;
}
}
return s.length() == 4;
}
private boolean isHexLetter(char c) {
return (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
private boolean isEncoded(String name) {
return name != null && name.startsWith(ENCODE_PREFIX);
}
}

View File

@ -17,11 +17,13 @@
package com.alibaba.nacos.ai.utils;
import com.alibaba.nacos.ai.constant.Constants;
import com.alibaba.nacos.ai.form.a2a.admin.AgentDetailForm;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentInterface;
import com.alibaba.nacos.api.ai.model.a2a.AgentVersionDetail;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.common.utils.StringUtils;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@ -35,30 +37,7 @@ import java.util.Collections;
*/
public class AgentCardUtil {
/**
* Build Agent Card from Agent Detail form.
*
* @param form agent detail form
* @return Agent Card
*/
public static AgentCard buildAgentCard(AgentDetailForm form) {
AgentCard agentCard = new AgentCard();
injectAgentCardInfo(agentCard, form);
return agentCard;
}
/**
* Build Agent Card Storage Info from Agent Detail form.
*
* @param form agent detail form
* @return Agent Card Storage Info
*/
public static AgentCardDetailInfo buildAgentCardDetailInfo(AgentDetailForm form) {
AgentCardDetailInfo agentCardDetailInfo = new AgentCardDetailInfo();
injectAgentCardInfo(agentCardDetailInfo, form);
agentCardDetailInfo.setRegistrationType(form.getRegistrationType());
return agentCardDetailInfo;
}
private static final String AGENT_INTERFACE_URL_PATTERN = "%s://%s:%s";
/**
* Build Agent Card Storage Info from Agent Detail form.
@ -73,24 +52,6 @@ public class AgentCardUtil {
return agentCardDetailInfo;
}
/**
* Build Agent Card Storage Info from Agent Detail form.
*
* @param form agent detail form
* @param isLatest is latest version
* @return Agent Card Version Info
*/
public static AgentCardVersionInfo buildAgentCardVersionInfo(AgentDetailForm form, boolean isLatest) {
AgentCardVersionInfo agentCardVersionInfo = new AgentCardVersionInfo();
injectAgentCardInfo(agentCardVersionInfo, form);
agentCardVersionInfo.setRegistrationType(form.getRegistrationType());
if (isLatest) {
agentCardVersionInfo.setLatestPublishedVersion(form.getVersion());
}
agentCardVersionInfo.setVersionDetails(Collections.singletonList(buildAgentVersionDetail(form, isLatest)));
return agentCardVersionInfo;
}
/**
* Build Agent Card Storage Info from AgentCard.
*
@ -111,21 +72,6 @@ public class AgentCardUtil {
return agentCardVersionInfo;
}
/**
* Build Agent version detail from Agent Detail form.
*
* @param form agent detail form
* @return Agent Version Detail
*/
public static AgentVersionDetail buildAgentVersionDetail(AgentDetailForm form, boolean isLatest) {
AgentVersionDetail agentVersionDetail = new AgentVersionDetail();
agentVersionDetail.setCreatedAt(getCurrentTime());
agentVersionDetail.setUpdatedAt(getCurrentTime());
agentVersionDetail.setVersion(form.getVersion());
agentVersionDetail.setLatest(isLatest);
return agentVersionDetail;
}
/**
* Build Agent version detail from Agent Detail form.
*
@ -150,32 +96,33 @@ public class AgentCardUtil {
versionDetail.setUpdatedAt(getCurrentTime());
}
/**
* Build {@link AgentInterface} from service {@link Instance}.
*
* @param instance service instance.
* @return agent interface (endpoint)
*/
public static AgentInterface buildAgentInterface(Instance instance) {
AgentInterface agentInterface = new AgentInterface();
boolean isSupportTls = Boolean.parseBoolean(
instance.getMetadata().get(Constants.A2A.NACOS_AGENT_ENDPOINT_SUPPORT_TLS));
String protocol = isSupportTls ? Constants.PROTOCOL_TYPE_HTTPS : Constants.PROTOCOL_TYPE_HTTP;
String url = String.format(AGENT_INTERFACE_URL_PATTERN, protocol, instance.getIp(), instance.getPort());
String path = instance.getMetadata().get(Constants.A2A.AGENT_ENDPOINT_PATH_KEY);
if (StringUtils.isNotBlank(path)) {
url += path.startsWith("/") ? path : "/" + path;
}
agentInterface.setUrl(url);
agentInterface.setTransport(instance.getMetadata().get(Constants.A2A.AGENT_ENDPOINT_TRANSPORT_KEY));
return agentInterface;
}
private static String getCurrentTime() {
ZonedDateTime currentTime = ZonedDateTime.now(ZoneOffset.UTC);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Constants.RELEASE_DATE_FORMAT);
return currentTime.format(formatter);
}
private static void injectAgentCardInfo(AgentCard agentCard, AgentDetailForm form) {
agentCard.setProtocolVersion(form.getProtocolVersion());
agentCard.setName(form.getName());
agentCard.setDescription(form.getDescription());
agentCard.setUrl(form.getUrl());
agentCard.setVersion(form.getVersion());
agentCard.setPreferredTransport(form.getPreferredTransport());
agentCard.setAdditionalInterfaces(form.getAdditionalInterfaces());
agentCard.setIconUrl(form.getIconUrl());
agentCard.setProvider(form.getProvider());
agentCard.setCapabilities(form.getCapabilities());
agentCard.setSecuritySchemes(form.getSecuritySchemes());
agentCard.setSecurity(form.getSecurity());
agentCard.setDefaultInputModes(form.getDefaultInputModes());
agentCard.setDefaultOutputModes(form.getDefaultOutputModes());
agentCard.setSkills(form.getSkills());
agentCard.setSupportsAuthenticatedExtendedCard(form.getSupportsAuthenticatedExtendedCard());
agentCard.setDocumentationUrl(form.getDocumentationUrl());
}
private static void copyAgentCardInfo(AgentCard target, AgentCard source) {
target.setProtocolVersion(source.getProtocolVersion());
target.setName(source.getName());

View File

@ -17,7 +17,9 @@
package com.alibaba.nacos.ai.utils;
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.api.exception.runtime.NacosDeserializationException;
@ -48,10 +50,7 @@ public class AgentRequestUtil {
try {
AgentCard result = JacksonUtils.toObj(agentCardForm.getAgentCard(), new TypeReference<>() {
});
validateAgentCardField("name", result.getName());
validateAgentCardField("description", result.getDescription());
validateAgentCardField("version", result.getVersion());
validateAgentCardField("protocolVersion", result.getProtocolVersion());
validateAgentCard(result);
return result;
} catch (NacosDeserializationException e) {
LOGGER.error(String.format("Deserialize %s from %s failed, ", AgentCard.class.getSimpleName(),
@ -61,6 +60,29 @@ public class AgentRequestUtil {
}
}
/**
* Validate agent card is legal.
*
* @param agentCard agent card
* @throws NacosApiException if agent card is illegal.
*/
public static void validateAgentCard(AgentCard agentCard) throws NacosApiException {
validateAgentCardField("name", agentCard.getName());
validateAgentCardField("version", agentCard.getVersion());
validateAgentCardField("protocolVersion", agentCard.getProtocolVersion());
}
/**
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
*
* @param request agent request
*/
public static void fillNamespaceId(AbstractAgentRequest request) {
if (StringUtils.isEmpty(request.getNamespaceId())) {
request.setNamespaceId(AiConstants.A2a.A2A_DEFAULT_NAMESPACE);
}
}
private static void validateAgentCardField(String fieldName, String fieldValue) throws NacosApiException {
if (StringUtils.isEmpty(fieldValue)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,

View File

@ -23,6 +23,7 @@ import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServiceRef;
import com.alibaba.nacos.api.ai.model.mcp.McpTool;
import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.api.exception.runtime.NacosDeserializationException;
import com.alibaba.nacos.api.model.v2.ErrorCode;
@ -146,4 +147,15 @@ public class McpRequestUtil {
}
throw new IllegalArgumentException("input must be instance of McpServiceRef or Map");
}
/**
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
*
* @param request mcp request
*/
public static void fillNamespaceId(AbstractMcpRequest request) {
if (StringUtils.isEmpty(request.getNamespaceId())) {
request.setNamespaceId(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE);
}
}
}

View File

@ -14,4 +14,5 @@
# limitations under the License.
#
com.alibaba.nacos.ai.param.McpHttpParamExtractor
com.alibaba.nacos.ai.param.McpHttpParamExtractor
com.alibaba.nacos.ai.param.AgentHttpParamExtractor

View File

@ -0,0 +1,129 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.ai.service.a2a.identity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AsciiAgentIdCodecTest {
private AsciiAgentIdCodec agentIdCodec;
@BeforeEach
public void setUp() {
agentIdCodec = new AsciiAgentIdCodec();
}
// Does not encode when name is already valid
@Test
void testUsageDoesNotEncodeValidNames() {
String input = "abc123";
assertEquals(input, agentIdCodec.encode(input));
}
// Round-trip encode/decode when input contains a space
@Test
void testEncodeAndDecodeWhenNameContainsSpace() {
String input = "hello world";
String encoded = agentIdCodec.encode(input);
assertEquals("_-.SYSENC:hello_0020world", encoded);
assertEquals(input, agentIdCodec.decode(encoded));
}
// Valid special characters (._:-) should be preserved without encoding
@Test
void testValidSpecialCharsAreKept() {
String input = "name_ok.1:2";
assertEquals(input, agentIdCodec.encode(input));
}
// Round-trip encode/decode for mixed unicode letters and ASCII
@Test
void testRoundTripUnicodeChars() {
String input = " Ω test";
String encoded = agentIdCodec.encode(input);
assertEquals("_-.SYSENC:_0020Ω_0020test", encoded);
String decoded = agentIdCodec.decode(encoded);
assertEquals(input, decoded);
}
// Input starts with underscore followed by hex-like sequence; verify behavior policy
@Test
void testUnderscoreFollowedByHexAmbiguityHandledByPolicy() {
String original = "1 _abcd";
String encoded = agentIdCodec.encode(original);
assertEquals("_-.SYSENC:1_0020_005fabcd", encoded);
assertEquals(original, agentIdCodec.decode(encoded));
}
// Round-trip for extreme boundary code points (NUL and U+FFFF)
@Test
void testBoundaryCharacters() {
String input = "\u0000\uFFFF";
String encoded = agentIdCodec.encode(input);
assertEquals("_-.SYSENC:_0000_ffff", encoded);
String decoded = agentIdCodec.decode(encoded);
assertEquals(input, decoded);
}
// Encoding keeps empty string as-is and preserves a single underscore
@Test
void testEncodeKeepsEmptyAndUnderscore() {
String empty = "";
String encodedEmpty = agentIdCodec.encode(empty);
assertEquals("", encodedEmpty);
String underscoreOnly = "_";
String encodedUnderscore = agentIdCodec.encode(underscoreOnly);
assertEquals(underscoreOnly, encodedUnderscore);
}
// Encoding is idempotent for already-encoded output; decode restores original
@Test
void testAlreadyEncodedStringIsIdempotentOnEncode() {
String original = "with space and Ω and tab\t";
String first = agentIdCodec.encode(original);
String second = agentIdCodec.encode(first);
assertEquals("_-.SYSENC:with_0020space_0020and_0020Ω_0020and_0020tab_0009", first);
// encodeName should not double-encode an already valid string
assertEquals(first, second);
// decode should restore original
assertEquals(original, agentIdCodec.decode(first));
}
// Round-trip for mixture of ASCII, control (tab), and underscore suffix
@Test
void testMixedUnicodeAndControlCharactersRoundTrip() {
String original = "A B\tC_";
String encoded = agentIdCodec.encode(original);
assertEquals("_-.SYSENC:A_0020B_0009C_005f", encoded);
String decoded = agentIdCodec.decode(encoded);
assertEquals(original, decoded);
}
// Decoding a string with encoded prefix returns body; encoding leaves valid input unchanged
@Test
void testDecodeNameWithFakeEncodedPrefixBody() {
String fake = "_-.SYSENC:hello";
// This string is already valid; encodeName should return as-is
assertEquals(fake, agentIdCodec.encode(fake));
// decodeName should strip prefix and return body unchanged
assertEquals("hello", agentIdCodec.decode(fake));
}
}

View File

@ -27,10 +27,10 @@ class McpRequestUtilsTest {
@Test
void fillNamespaceId() {
QueryMcpServerRequest request = new QueryMcpServerRequest();
McpRequestUtils.fillNamespaceId(request);
McpRequestUtil.fillNamespaceId(request);
assertEquals(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, request.getNamespaceId());
request.setNamespaceId("test");
McpRequestUtils.fillNamespaceId(request);
McpRequestUtil.fillNamespaceId(request);
assertEquals("test", request.getNamespaceId());
}
}

View File

@ -53,6 +53,12 @@ public enum AbilityKey {
SERVER_MCP_REGISTRY("mcp", "Server whether support release mcp server and register endpoint for mcp server",
AbilityMode.SERVER),
/**
* For AI module Agent & Agent Card registry.
*/
SERVER_AGENT_REGISTRY("agent", "Server whether support release agent server and register endpoint for agent server",
AbilityMode.SERVER),
/**
* For fuzzy watch naming or config.
*/
@ -70,6 +76,12 @@ public enum AbilityKey {
SDK_MCP_REGISTRY("mcp", "Client whether support release mcp server and register endpoint for mcp server",
AbilityMode.SDK_CLIENT),
/**
* For AI module Agent & Agent Card registry.
*/
SDK_AGENT_REGISTRY("agent", "Client whether support release agent server and register endpoint for agent server",
AbilityMode.SDK_CLIENT),
/**
* For Test temporarily.
*/

View File

@ -48,6 +48,7 @@ public class SdkClientAbilities extends AbstractAbilityRegistry {
supportedAbilities.put(AbilityKey.SDK_CLIENT_FUZZY_WATCH, true);
supportedAbilities.put(AbilityKey.SDK_CLIENT_DISTRIBUTED_LOCK, true);
supportedAbilities.put(AbilityKey.SDK_MCP_REGISTRY, true);
supportedAbilities.put(AbilityKey.SDK_AGENT_REGISTRY, true);
}
/**.

View File

@ -49,6 +49,7 @@ public class ServerAbilities extends AbstractAbilityRegistry {
supportedAbilities.put(AbilityKey.SERVER_FUZZY_WATCH, true);
supportedAbilities.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, true);
supportedAbilities.put(AbilityKey.SERVER_MCP_REGISTRY, true);
supportedAbilities.put(AbilityKey.SERVER_AGENT_REGISTRY, true);
}
/**.

View File

@ -0,0 +1,231 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.utils.StringUtils;
/**
* Nacos AI A2A client service interface.
*
* @author xiweng.yy
*/
public interface A2aService {
/**
* Get agent card with nacos extension detail with latest version.
*
* @param agentName name of agent card
* @return agent card with nacos extension detail
* @throws NacosException if request parameter is invalid or agent card not found or handle error
*/
default AgentCardDetailInfo getAgentCard(String agentName) throws NacosException {
return getAgentCard(agentName, StringUtils.EMPTY);
}
/**
* Get agent card with nacos extension detail with target version.
*
* @param agentName name of agent card
* @param version target version, if null or empty, get latest version
* @return agent card with nacos extension detail
* @throws NacosException if request parameter is invalid or agent card not found or handle error
*/
default AgentCardDetailInfo getAgentCard(String agentName, String version) throws NacosException {
return getAgentCard(agentName, version, StringUtils.EMPTY);
}
/**
* Get agent card with nacos extension detail with target version.
*
* @param agentName name of agent card
* @param version target version, if null or empty, get latest version
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
* default is empty, means use agent card setting in nacos.
* @return agent card with nacos extension detail
* @throws NacosException if request parameter is invalid or agent card not found or handle error
*/
AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException;
/**
* Release new agent card or new version with default service type endpoint.
*
* <p>
* If current agent card and version exist, This API will do nothing.
* If current agent card exist but version not exist, This API will release new version.
* If current t agent card not exist, This API will release new agent card.
* </p>
*
* @param agentCard agent card need to release
* @throws NacosException if request parameter is invalid or handle error
*/
default void releaseAgentCard(AgentCard agentCard) throws NacosException {
releaseAgentCard(agentCard, AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE);
}
/**
* Release new agent card or new version.
*
* <p>
* If current agent card and version exist, This API will do nothing.
* If current agent card exist but version not exist, This API will release new version.
* If current t agent card not exist, This API will release new agent card.
* </p>
*
* @param agentCard agent card need to release
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
* @throws NacosException if request parameter is invalid or handle error
*/
default void releaseAgentCard(AgentCard agentCard, String registrationType) throws NacosException {
releaseAgentCard(agentCard, registrationType, false);
}
/**
* Release new agent card or new version.
*
* <p>
* If current agent card and version exist, This API will do nothing.
* If current agent card exist but version not exist, This API will release new version.
* If current t agent card not exist, This API will release new agent card.
* </p>
*
* @param agentCard agent card need to release
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
* @param setAsLatest whether set new version as latest, default is false. This parameter is only effect when new version is released.
* If current agent card not exist, whatever this parameter is, it will be set as latest.
* @throws NacosException if request parameter is invalid or handle error
*/
void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException;
/**
* Register endpoint to agent card.
*
* @param agentName name of agent
* @param version version of this endpoint
* @param address address for this endpoint
* @param port port of this endpoint
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
default void registerAgentEndpoint(String agentName, String version, String address, int port)
throws NacosException {
registerAgentEndpoint(agentName, version, address, port, AiConstants.A2a.A2A_ENDPOINT_DEFAULT_TRANSPORT);
}
/**
* Register endpoint to agent card.
*
* @param agentName name of agent
* @param version version of this endpoint
* @param address address for this endpoint
* @param port port of this endpoint
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport)
throws NacosException {
registerAgentEndpoint(agentName, version, address, port, transport, StringUtils.EMPTY);
}
/**
* Register endpoint to agent card.
*
* @param agentName name of agent
* @param version version of this endpoint
* @param address address for this endpoint
* @param port port of this endpoint
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
* @param path The path of endpoint request
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport,
String path) throws NacosException {
registerAgentEndpoint(agentName, version, address, port, transport, path, false);
}
/**
* Register endpoint to agent card.
*
* @param agentName name of agent
* @param version version of this endpoint
* @param address address for this endpoint
* @param port port of this endpoint
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
* @param path The path of endpoint request
* @param supportTls whether support tls
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport,
String path, boolean supportTls) throws NacosException {
AgentEndpoint agentEndpoint = new AgentEndpoint();
agentEndpoint.setAddress(address);
agentEndpoint.setPort(port);
agentEndpoint.setTransport(transport);
agentEndpoint.setPath(path);
agentEndpoint.setSupportTls(supportTls);
agentEndpoint.setVersion(version);
registerAgentEndpoint(agentName, agentEndpoint);
}
/**
* Register endpoint to agent card.
*
* @param agentName name of agent
* @param endpoint endpoint info
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException;
/**
* Deregister endpoint from agent card which registered by this client.
*
* <p>
* Only endpoint registered by this client can be deregistered.
* Other endpoint registered by other clients, call this API will no any effect.
* </p>
*
* @param agentName name of agent
* @param version version of this endpoint
* @param address address for this endpoint
* @param port port of this endpoint
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
default void deregisterAgentEndpoint(String agentName, String version, String address, int port) throws NacosException {
AgentEndpoint agentEndpoint = new AgentEndpoint();
agentEndpoint.setAddress(address);
agentEndpoint.setPort(port);
agentEndpoint.setVersion(version);
deregisterAgentEndpoint(agentName, agentEndpoint);
}
/**
* Deregister endpoint from agent card which registered by this client.
*
* <p>
* Only endpoint registered by this client can be deregistered.
* Other endpoint registered by other clients, call this API will no any effect.
* </p>
*
* @param agentName name of agent
* @param endpoint endpoint info
* @throws NacosException if request parameter is invalid or handle error or agent not found
*/
void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException;
}

View File

@ -28,7 +28,7 @@ import com.alibaba.nacos.api.exception.NacosException;
*
* @author xiweng.yy
*/
public interface AiService {
public interface AiService extends A2aService {
/**
* Get mcp server detail info for latest version.

View File

@ -63,5 +63,7 @@ public class AiConstants {
* Default endpoint type using `backend` service of agent when discovery a2a agent.
*/
public static final String A2A_ENDPOINT_TYPE_SERVICE = "SERVICE";
public static final String A2A_ENDPOINT_DEFAULT_TRANSPORT = "JSONRPC";
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.model.a2a;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.utils.StringUtils;
import java.util.Objects;
/**
* Agent endpoint for A2A protocol.
*
* <p>
* Details split version of {@link AgentInterface}.
* </p>
*
* @author xiweng.yy
*/
public class AgentEndpoint {
/**
* Same with {@link AgentInterface#transport}, Default `JSONRPC`.
*/
private String transport = AiConstants.A2a.A2A_ENDPOINT_DEFAULT_TRANSPORT;
/**
* Will be joined with {@link #port}, {@link #path}. Such as `<a href="http://address:port/path">...</a>`
*/
private String address;
private int port;
private String path = StringUtils.EMPTY;
/**
* If {@code true}, the target {@link AgentInterface} should be `https`, otherwise should be `http`. Default {@code false}.
*/
private boolean supportTls;
private String version;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getTransport() {
return transport;
}
public void setTransport(String transport) {
this.transport = transport;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isSupportTls() {
return supportTls;
}
public void setSupportTls(boolean supportTls) {
this.supportTls = supportTls;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
/**
* Only simple check address(IP or domain) and port.
*
* @param endpoint target endpoint
* @return {@code true} if is equal, otherwise {@code false}
*/
public boolean simpleEquals(AgentEndpoint endpoint) {
return Objects.equals(address, endpoint.address) && Objects.equals(port, endpoint.port);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AgentEndpoint that = (AgentEndpoint) o;
return port == that.port && supportTls == that.supportTls && Objects.equals(transport, that.transport)
&& Objects.equals(address, that.address) && Objects.equals(path, that.path) && Objects.equals(version,
that.version);
}
@Override
public int hashCode() {
return Objects.hash(transport, address, port, path, supportTls, version);
}
@Override
public String toString() {
return "AgentEndpoint{" + "transport='" + transport + '\'' + ", address='" + address + '\'' + ", port=" + port
+ ", path='" + path + '\'' + ", supportTls=" + supportTls + ", version='" + version + '\'' + '}';
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.request;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.remote.request.Request;
/**
* Nacos AI module agent request.
*
* @author xiweng.yy
*/
public class AbstractAgentRequest extends Request {
private String namespaceId;
private String agentName;
@Override
public String getModule() {
return Constants.AI.AI_MODULE;
}
public String getNamespaceId() {
return namespaceId;
}
public void setNamespaceId(String namespaceId) {
this.namespaceId = namespaceId;
}
public String getAgentName() {
return agentName;
}
public void setAgentName(String agentName) {
this.agentName = agentName;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.request;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
/**
* Register or Deregister endpoint for agent to nacos AI module request.
*
* @author xiweng.yy
*/
public class AgentEndpointRequest extends AbstractAgentRequest {
private AgentEndpoint endpoint;
/**
* Should be {@link AiRemoteConstants#REGISTER_ENDPOINT} or {@link AiRemoteConstants#DE_REGISTER_ENDPOINT}.
*/
private String type;
public AgentEndpoint getEndpoint() {
return endpoint;
}
public void setEndpoint(AgentEndpoint endpoint) {
this.endpoint = endpoint;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.request;
/**
* Nacos AI module query agent card request.
*
* @author xiweng.yy
*/
public class QueryAgentCardRequest extends AbstractAgentRequest {
private String version;
private String registrationType;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getRegistrationType() {
return registrationType;
}
public void setRegistrationType(String registrationType) {
this.registrationType = registrationType;
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.request;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
/**
* Nacos AI module release new agent card or new version of exist agent card request.
*
* @author xiweng.yy
*/
public class ReleaseAgentCardRequest extends AbstractAgentRequest {
private AgentCard agentCard;
private String registrationType = AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE;
private boolean setAsLatest;
public AgentCard getAgentCard() {
return agentCard;
}
public void setAgentCard(AgentCard agentCard) {
this.agentCard = agentCard;
}
public String getRegistrationType() {
return registrationType;
}
public void setRegistrationType(String registrationType) {
this.registrationType = registrationType;
}
public boolean isSetAsLatest() {
return setAsLatest;
}
public void setSetAsLatest(boolean setAsLatest) {
this.setAsLatest = setAsLatest;
}
}

View File

@ -14,27 +14,28 @@
* limitations under the License.
*/
package com.alibaba.nacos.ai.utils;
package com.alibaba.nacos.api.ai.remote.response;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
import com.alibaba.nacos.api.remote.response.Response;
/**
* Nacos Mcp server request utils.
* Register or Deregister endpoint for agent to nacos AI module response.
*
* @author xiweng.yy
*/
public class McpRequestUtils {
public class AgentEndpointResponse extends Response {
/**
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
*
* @param request mcp request
* Should be {@link AiRemoteConstants#REGISTER_ENDPOINT} or {@link AiRemoteConstants#DE_REGISTER_ENDPOINT}.
*/
public static void fillNamespaceId(AbstractMcpRequest request) {
if (StringUtils.isEmpty(request.getNamespaceId())) {
request.setNamespaceId(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE);
}
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.response;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.remote.response.Response;
/**
* Nacos AI module query agent card response.
*
* @author xiweng.yy
*/
public class QueryAgentCardResponse extends Response {
private AgentCardDetailInfo agentCardDetailInfo;
public AgentCardDetailInfo getAgentCardDetailInfo() {
return agentCardDetailInfo;
}
public void setAgentCardDetailInfo(AgentCardDetailInfo agentCardDetailInfo) {
this.agentCardDetailInfo = agentCardDetailInfo;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.ai.remote.response;
import com.alibaba.nacos.api.remote.response.Response;
/**
* Nacos AI module release new agent card or new version of exist agent card response.
*
* @author xiweng.yy
*/
public class ReleaseAgentCardResponse extends Response {
}

View File

@ -80,4 +80,11 @@ com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse
com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest
com.alibaba.nacos.api.ai.remote.response.ReleaseMcpServerResponse
com.alibaba.nacos.api.ai.remote.request.McpServerEndpointRequest
com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse
com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse
com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest
com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse
com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest
com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse
com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest
com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse

View File

@ -17,6 +17,9 @@
package com.alibaba.nacos.api.ai;
import com.alibaba.nacos.api.ai.listener.AbstractNacosMcpServerListener;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
@ -75,6 +78,28 @@ class AiServiceDefaultMethodTest {
invokeMark.set(true);
}
@Override
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException {
return null;
}
@Override
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException {
}
@Override
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint)
throws NacosException {
}
@Override
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint)
throws NacosException {
}
@Override
public void shutdown() throws NacosException {
}

View File

@ -48,22 +48,26 @@ class AbilityKeyTest {
enumMap.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, false);
enumMap.put(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC, false);
enumMap.put(AbilityKey.SERVER_MCP_REGISTRY, false);
enumMap.put(AbilityKey.SERVER_AGENT_REGISTRY, false);
stringBooleanMap = AbilityKey.mapStr(enumMap);
assertEquals(4, stringBooleanMap.size());
assertEquals(5, stringBooleanMap.size());
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_FUZZY_WATCH.getName()));
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_DISTRIBUTED_LOCK.getName()));
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC.getName()));
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_MCP_REGISTRY.getName()));
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_AGENT_REGISTRY.getName()));
enumMap.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, true);
enumMap.put(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC, true);
enumMap.put(AbilityKey.SERVER_MCP_REGISTRY, true);
enumMap.put(AbilityKey.SERVER_AGENT_REGISTRY, true);
stringBooleanMap = AbilityKey.mapStr(enumMap);
assertEquals(4, stringBooleanMap.size());
assertEquals(5, stringBooleanMap.size());
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_FUZZY_WATCH.getName()));
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_DISTRIBUTED_LOCK.getName()));
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC.getName()));
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_MCP_REGISTRY.getName()));
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_AGENT_REGISTRY.getName()));
}
@Test
@ -105,9 +109,9 @@ class AbilityKeyTest {
@Test
void testGetAllValues() {
Collection<AbilityKey> actual = AbilityKey.getAllValues(AbilityMode.SERVER);
assertEquals(4, actual.size());
assertEquals(5, actual.size());
actual = AbilityKey.getAllValues(AbilityMode.SDK_CLIENT);
assertEquals(3, actual.size());
assertEquals(4, actual.size());
actual = AbilityKey.getAllValues(AbilityMode.CLUSTER_CLIENT);
assertEquals(1, actual.size());
}
@ -115,9 +119,9 @@ class AbilityKeyTest {
@Test
void testGetAllNames() {
Collection<String> actual = AbilityKey.getAllNames(AbilityMode.SERVER);
assertEquals(4, actual.size());
assertEquals(5, actual.size());
actual = AbilityKey.getAllNames(AbilityMode.SDK_CLIENT);
assertEquals(3, actual.size());
assertEquals(4, actual.size());
actual = AbilityKey.getAllNames(AbilityMode.CLUSTER_CLIENT);
assertEquals(1, actual.size());
}

View File

@ -18,8 +18,12 @@ package com.alibaba.nacos.client.ai;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.ai.AiService;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.listener.AbstractNacosMcpServerListener;
import com.alibaba.nacos.api.ai.listener.NacosMcpServerEvent;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
@ -177,6 +181,73 @@ public class NacosAiService implements AiService {
}
}
@Override
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException {
if (StringUtils.isBlank(agentName)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `agentName` can't be empty or null");
}
return grpcClient.getAgentCard(agentName, version, registrationType);
}
@Override
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException {
if (null == agentCard) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `agentCard` can't be null");
}
validateAgentCardField("name", agentCard.getName());
validateAgentCardField("version", agentCard.getVersion());
validateAgentCardField("protocolVersion", agentCard.getProtocolVersion());
if (StringUtils.isBlank(registrationType)) {
registrationType = AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE;
}
grpcClient.releaseAgentCard(agentCard, registrationType, setAsLatest);
}
@Override
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
if (StringUtils.isBlank(agentName)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `agentName` can't be empty or null");
}
validateAgentEndpoint(endpoint);
grpcClient.registerAgentEndpoint(agentName, endpoint);
}
@Override
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint)
throws NacosException {
if (StringUtils.isBlank(agentName)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `agentName` can't be empty or null");
}
validateAgentEndpoint(endpoint);
grpcClient.deregisterAgentEndpoint(agentName, endpoint);
}
private void validateAgentEndpoint(AgentEndpoint endpoint) throws NacosApiException {
if (null == endpoint) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"parameters `endpoint` can't be null");
}
if (StringUtils.isBlank(endpoint.getVersion())) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter `endpoint.version` can't be empty or null");
}
Instance instance = new Instance();
instance.setIp(endpoint.getAddress());
instance.setPort(endpoint.getPort());
instance.validate();
}
private static void validateAgentCardField(String fieldName, String fieldValue) throws NacosApiException {
if (StringUtils.isEmpty(fieldValue)) {
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
"Required parameter `agentCard." + fieldName + "` not present");
}
}
@Override
public void shutdown() throws NacosException {
this.grpcClient.shutdown();

View File

@ -19,17 +19,27 @@ package com.alibaba.nacos.client.ai.remote;
import com.alibaba.nacos.api.ability.constant.AbilityKey;
import com.alibaba.nacos.api.ability.constant.AbilityStatus;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest;
import com.alibaba.nacos.api.ai.remote.request.McpServerEndpointRequest;
import com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest;
import com.alibaba.nacos.api.ai.remote.request.QueryMcpServerRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest;
import com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse;
import com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse;
import com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse;
import com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse;
import com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse;
import com.alibaba.nacos.api.ai.remote.response.ReleaseMcpServerResponse;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.exception.NacosException;
@ -284,6 +294,130 @@ public class AiGrpcClient implements Closeable {
mcpServerCacheHolder.removeMcpServerUpdateTask(mcpName, version);
}
/**
* Get agent card with nacos extension detail with target version.
*
* @param agentName name of agent card
* @param version target version, if null or empty, get latest version
* @param registrationType registration type
* @return agent card with nacos extension detail
* @throws NacosException if request parameter is invalid or agent card not found or handle error
*/
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType)
throws NacosException {
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
"Request Nacos server version is too low, not support agent registry feature.");
}
QueryAgentCardRequest request = new QueryAgentCardRequest();
request.setNamespaceId(this.namespaceId);
request.setAgentName(agentName);
request.setVersion(version);
request.setRegistrationType(registrationType);
QueryAgentCardResponse response = requestToServer(request, QueryAgentCardResponse.class);
return response.getAgentCardDetailInfo();
}
/**
* Release new agent card or new version.
*
* <p>
* If current agent card and version exist, This API will do nothing. If current agent card exist but version not
* exist, This API will release new version. If current t agent card not exist, This API will release new agent
* card.
* </p>
*
* @param agentCard agent card need to release
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or
* {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
* @param setAsLatest whether set new version as latest, default is false. This parameter is only effect when new version is released.
* If current agent card not exist, whatever this parameter is, it will be set as latest.
* @throws NacosException if request parameter is invalid or handle error
*/
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest)
throws NacosException {
LOGGER.info("[{}] Release Agent Card {}, version {}.", uuid, agentCard.getName(), agentCard.getVersion());
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
"Request Nacos server version is too low, not support agent registry feature.");
}
ReleaseAgentCardRequest request = new ReleaseAgentCardRequest();
request.setNamespaceId(this.namespaceId);
request.setAgentName(agentCard.getName());
request.setRegistrationType(registrationType);
request.setAgentCard(agentCard);
request.setSetAsLatest(setAsLatest);
requestToServer(request, ReleaseAgentCardResponse.class);
}
/**
* Register agent endpoint into agent.
*
* @param agentName agent name
* @param endpoint agent endpoint
* @throws NacosException if request parameter is invalid or handle error
*/
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
LOGGER.info("[{}] REGISTER Agent endpoint {} into agent {}", uuid, endpoint.toString(), agentName);
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
"Request Nacos server version is too low, not support agent registry feature.");
}
redoService.cachedAgentEndpointForRedo(agentName, endpoint);
doRegisterAgentEndpoint(agentName, endpoint);
}
/**
* Actual do register agent endpoint into agent.
*
* @param agentName agent name
* @param endpoint agent endpoint
* @throws NacosException if request parameter is invalid or handle error
*/
public void doRegisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
AgentEndpointRequest request = new AgentEndpointRequest();
request.setNamespaceId(this.namespaceId);
request.setAgentName(agentName);
request.setType(AiRemoteConstants.REGISTER_ENDPOINT);
request.setEndpoint(endpoint);
requestToServer(request, AgentEndpointResponse.class);
redoService.agentEndpointRegistered(agentName);
}
/**
* Deregister agent endpoint from agent.
*
* @param agentName agent name
* @param endpoint agent endpoint
* @throws NacosException if request parameter is invalid or handle error
*/
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
LOGGER.info("[{}] DE-REGISTER agent endpoint {} from agent {}", uuid, endpoint.toString(), agentName);
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
"Request Nacos server version is too low, not support agent registry feature.");
}
redoService.agentEndpointDeregister(agentName);
doDeregisterAgentEndpoint(agentName, endpoint);
}
/**
* Actual do deregister agent endpoint from agent.
*
* @param agentName agent name
* @param endpoint agent endpoint
* @throws NacosException if request parameter is invalid or handle error
*/
public void doDeregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
AgentEndpointRequest request = new AgentEndpointRequest();
request.setNamespaceId(this.namespaceId);
request.setAgentName(agentName);
request.setType(AiRemoteConstants.DE_REGISTER_ENDPOINT);
request.setEndpoint(endpoint);
requestToServer(request, AgentEndpointResponse.class);
redoService.agentEndpointDeregistered(agentName);
}
public boolean isEnable() {
return rpcClient.isRunning();
}
@ -302,8 +436,11 @@ public class AiGrpcClient implements Closeable {
Response response = null;
try {
if (request instanceof AbstractMcpRequest) {
request.putAllHeader(getSecurityHeaders(((AbstractMcpRequest) request).getNamespaceId(),
((AbstractMcpRequest) request).getMcpName()));
AbstractMcpRequest mcpRequest = (AbstractMcpRequest) request;
request.putAllHeader(getSecurityHeaders(mcpRequest.getNamespaceId(), mcpRequest.getMcpName()));
} else if (request instanceof AbstractAgentRequest) {
AbstractAgentRequest agentRequest = (AbstractAgentRequest) request;
request.putAllHeader(getSecurityHeaders(agentRequest.getNamespaceId(), agentRequest.getAgentName()));
} else {
throw new NacosException(400,
String.format("Unknown AI request type: %s", request.getClass().getSimpleName()));

View File

@ -0,0 +1,61 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.ai.remote.redo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.client.redo.data.RedoData;
import java.util.Objects;
/**
* Nacos AI module mcp server endpoint redo data.
*
* @author xiweng.yy
*/
public class AgentEndpointRedoData extends RedoData<AgentEndpoint> {
private final String agentName;
public AgentEndpointRedoData(String agentName, AgentEndpoint agentEndpoint) {
this.agentName = agentName;
this.set(agentEndpoint);
}
public String getAgentName() {
return agentName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
AgentEndpointRedoData that = (AgentEndpointRedoData) o;
return Objects.equals(agentName, that.agentName) && super.equals(o);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), agentName);
}
}

View File

@ -16,6 +16,7 @@
package com.alibaba.nacos.client.ai.remote.redo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.remote.RemoteConstants;
import com.alibaba.nacos.client.ai.remote.AiGrpcClient;
import com.alibaba.nacos.client.env.NacosClientProperties;
@ -90,4 +91,38 @@ public class AiGrpcRedoService extends AbstractRedoService {
result.set(mcpServerEndpoint);
return result;
}
public void cachedAgentEndpointForRedo(String agentName, AgentEndpoint agentEndpoint) {
AgentEndpointRedoData redoData = new AgentEndpointRedoData(agentName, agentEndpoint);
super.cachedRedoData(agentName, redoData, AgentEndpoint.class);
}
public void removeAgentEndpointForRedo(String agentName) {
super.removeRedoData(agentName, AgentEndpoint.class);
}
public void agentEndpointRegistered(String agentName) {
super.dataRegistered(agentName, AgentEndpoint.class);
}
public void agentEndpointDeregister(String agentName) {
super.dataDeregister(agentName, AgentEndpoint.class);
}
public void agentEndpointDeregistered(String agentName) {
super.dataDeregistered(agentName, AgentEndpoint.class);
}
public boolean isAgentEndpointRegistered(String agentName) {
return super.isDataRegistered(agentName, AgentEndpoint.class);
}
public Set<RedoData<AgentEndpoint>> findAgentEndpointRedoData() {
return super.findRedoData(AgentEndpoint.class);
}
public AgentEndpoint getAgentEndpoint(String agentName) {
RedoData<AgentEndpoint> redoData = super.getRedoData(agentName, AgentEndpoint.class);
return redoData == null ? null : redoData.get();
}
}

View File

@ -16,6 +16,7 @@
package com.alibaba.nacos.client.ai.remote.redo;
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.ai.remote.AiGrpcClient;
import com.alibaba.nacos.client.naming.remote.gprc.redo.data.NamingRedoData;
@ -44,16 +45,54 @@ public class AiRedoScheduledTask extends AbstractRedoTask<AiGrpcRedoService> {
protected void redoData() throws NacosException {
try {
redoForMcpSeverEndpoint();
redoForAgentEndpoint();
} catch (Exception e) {
LOGGER.warn("Redo task run with unexpected exception: ", e);
}
}
private void redoForAgentEndpoint() {
for (RedoData<AgentEndpoint> each : getRedoService().findAgentEndpointRedoData()) {
AgentEndpointRedoData redoData = (AgentEndpointRedoData) each;
try {
redoForAgentEndpoint(redoData);
} catch (NacosException e) {
LOGGER.error("Redo agent endpoint operation {} for {}} failed. ", each.getRedoType(),
redoData.getAgentName(), e);
}
}
}
private void redoForAgentEndpoint(AgentEndpointRedoData redoData) throws NacosException {
NamingRedoData.RedoType redoType = redoData.getRedoType();
String agentName = redoData.getAgentName();
LOGGER.info("Redo agent endpoint operation {} for {}.", redoType, agentName);
AgentEndpoint endpoint = redoData.get();
switch (redoType) {
case REGISTER:
if (!aiGrpcClient.isEnable()) {
return;
}
aiGrpcClient.doRegisterAgentEndpoint(agentName, endpoint);
break;
case UNREGISTER:
if (!aiGrpcClient.isEnable()) {
return;
}
aiGrpcClient.doDeregisterAgentEndpoint(agentName, endpoint);
break;
case REMOVE:
getRedoService().removeMcpServerEndpointForRedo(agentName);
break;
default:
}
}
private void redoForMcpSeverEndpoint() {
for (RedoData<McpServerEndpoint> each : getRedoService().findMcpServerEndpointRedoData()) {
McpServerEndpointRedoData redoData = (McpServerEndpointRedoData) each;
try {
redoForEndpoint(redoData);
redoForMcpServerEndpoint(redoData);
} catch (NacosException e) {
LOGGER.error("Redo mcp server endpoint operation {} for {}} failed. ", each.getRedoType(),
redoData.getMcpName(), e);
@ -61,7 +100,7 @@ public class AiRedoScheduledTask extends AbstractRedoTask<AiGrpcRedoService> {
}
}
private void redoForEndpoint(McpServerEndpointRedoData redoData) throws NacosException {
private void redoForMcpServerEndpoint(McpServerEndpointRedoData redoData) throws NacosException {
NamingRedoData.RedoType redoType = redoData.getRedoType();
String mcpName = redoData.getMcpName();
LOGGER.info("Redo mcp server endpoint operation {} for {}.", redoType, mcpName);

View File

@ -41,7 +41,7 @@ class ClientAbilityControlManagerTest {
assertEquals(1, actual.size());
assertTrue(actual.containsKey(AbilityMode.SDK_CLIENT));
// Current not define sdk ability.
assertEquals(3, actual.get(AbilityMode.SDK_CLIENT).size());
assertEquals(4, actual.get(AbilityMode.SDK_CLIENT).size());
}
@Test

View File

@ -47,6 +47,8 @@ public class DefaultParamChecker extends AbstractParamChecker {
private Pattern mcpNamePattern;
private Pattern agentNamePattern;
private static final String CHECKER_TYPE = "default";
private static final String MAX_METADATA_LENGTH_PROP_NAME = "nacos.naming.service.metadata.length";
@ -91,6 +93,7 @@ public class DefaultParamChecker extends AbstractParamChecker {
this.clusterPattern = Pattern.compile(this.paramCheckRule.clusterPatternString);
this.ipPattern = Pattern.compile(this.paramCheckRule.ipPatternString);
this.mcpNamePattern = Pattern.compile(this.paramCheckRule.clusterPatternString);
this.agentNamePattern = Pattern.compile(this.paramCheckRule.agentNamePatternString);
}
/**
@ -159,6 +162,10 @@ public class DefaultParamChecker extends AbstractParamChecker {
if (!paramCheckResponse.isSuccess()) {
return paramCheckResponse;
}
paramCheckResponse = checkAgentNameFormat(paramInfo.getAgentName());
if (!paramCheckResponse.isSuccess()) {
return paramCheckResponse;
}
paramCheckResponse.setSuccess(true);
return paramCheckResponse;
}
@ -438,7 +445,7 @@ public class DefaultParamChecker extends AbstractParamChecker {
}
/**
* Check data id format.
* Check mcp name format.
*
* @param mcpName the mcp name
* @return the param check response
@ -463,4 +470,31 @@ public class DefaultParamChecker extends AbstractParamChecker {
paramCheckResponse.setSuccess(true);
return paramCheckResponse;
}
/**
* Check agent name format.
*
* @param agentName agent name
* @return the param check response
*/
public ParamCheckResponse checkAgentNameFormat(String agentName) {
ParamCheckResponse paramCheckResponse = new ParamCheckResponse();
if (StringUtils.isBlank(agentName)) {
paramCheckResponse.setSuccess(true);
return paramCheckResponse;
}
if (agentName.length() > paramCheckRule.maxAgentNameLength) {
paramCheckResponse.setSuccess(false);
paramCheckResponse.setMessage(
String.format("Param 'agentName' is illegal, the param length should not exceed %d.", paramCheckRule.maxAgentNameLength));
return paramCheckResponse;
}
if (!agentNamePattern.matcher(agentName).matches()) {
paramCheckResponse.setSuccess(false);
paramCheckResponse.setMessage("Param 'agentName' is illegal, illegal characters should not appear in the param.");
return paramCheckResponse;
}
paramCheckResponse.setSuccess(true);
return paramCheckResponse;
}
}

View File

@ -57,5 +57,7 @@ public class ParamCheckRule {
public int maxMetadataLength = 1024;
public String agentNamePatternString = "^[\\x20-\\x7E]+$";
public int maxAgentNameLength = 64;
}

View File

@ -48,6 +48,8 @@ public class ParamInfo {
private String mcpId;
private String agentName;
public String getNamespaceShowName() {
return namespaceShowName;
}
@ -143,4 +145,12 @@ public class ParamInfo {
public void setMcpName(String mcpName) {
this.mcpName = mcpName;
}
public String getAgentName() {
return agentName;
}
public void setAgentName(String agentName) {
this.agentName = agentName;
}
}

View File

@ -51,8 +51,6 @@ public class ParamUtils {
private static final String ENCRYPTED_DATA_KEY = "encryptedDataKey";
private static final String ENCODE_PREFIX = "_-.SYSENC:";
/**
* Whitelist checks that valid parameters can only contain letters, Numbers, and characters in validChars, and
* cannot be empty.
@ -239,70 +237,4 @@ public class ParamUtils {
}
}
/**
* encode name.
*/
@SuppressWarnings("PMD.AvoidComplexConditionRule")
public static String encodeName(String name) {
if (isValid(name)) {
return name;
}
StringBuilder sb = new StringBuilder(ENCODE_PREFIX);
for (char ch : name.toCharArray()) {
if (Character.isLetterOrDigit(ch) || (isValidChar(ch) && ch != '_')) {
// Keep letters, numbers, valid characters and non-underscores
sb.append(ch);
} else {
sb.append("_").append(String.format("%04x", (int) ch));
}
}
return sb.toString();
}
public static boolean isEncoded(String name) {
return name != null && name.startsWith(ENCODE_PREFIX);
}
/**
* decode name.
*/
public static String decodeName(String encoded) {
if (!isEncoded(encoded)) {
return encoded;
}
String body = encoded.substring(ENCODE_PREFIX.length());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < body.length();) {
char ch = body.charAt(i);
if (ch == '_' && i + 5 <= body.length()) {
String hexPart = body.substring(i + 1, i + 5);
if (isHex(hexPart)) {
try {
int codePoint = Integer.parseInt(hexPart, 16);
sb.append((char) codePoint);
i += 5;
continue;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid encoded name");
}
}
}
sb.append(ch);
i++;
}
return sb.toString();
}
@SuppressWarnings("PMD.AvoidComplexConditionRule")
private static boolean isHex(String s) {
for (char c : s.toCharArray()) {
if (!Character.isDigit(c) && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) {
return false;
}
}
return s.length() == 4;
}
}

View File

@ -25,7 +25,6 @@ import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -302,117 +301,4 @@ class ParamUtilsTest {
assertDoesNotThrow(() -> ParamUtils.checkParam("dataId", "group", ""));
assertDoesNotThrow(() -> ParamUtils.checkParam("dataId", "group", UUID.randomUUID().toString()));
}
// Does not encode when name is already valid
@Test
void testUsageDoesNotEncodeValidNames() {
String input = "abc123";
assertTrue(ParamUtils.isValid(input));
String processed = input;
if (!ParamUtils.isValid(processed)) {
processed = ParamUtils.encodeName(processed);
}
assertEquals(input, processed);
}
// Round-trip encode/decode when input contains a space
@Test
void testEncodeAndDecodeWhenNameContainsSpace() {
String input = "hello world";
assertFalse(ParamUtils.isValid(input));
String encoded = ParamUtils.encodeName(input);
assertTrue(ParamUtils.isEncoded(encoded));
assertTrue(encoded.contains("_0020"));
assertEquals(input, ParamUtils.decodeName(encoded));
}
// Valid special characters (._:-) should be preserved without encoding
@Test
void testValidSpecialCharsAreKept() {
String input = "name_ok.1:2";
assertTrue(ParamUtils.isValid(input));
String processed = input;
if (!ParamUtils.isValid(processed)) {
processed = ParamUtils.encodeName(processed);
}
assertEquals(input, processed);
}
// Round-trip encode/decode for mixed unicode letters and ASCII
@Test
void testRoundTripUnicodeChars() {
String input = " Ω test";
assertFalse(ParamUtils.isValid(input));
String encoded = ParamUtils.encodeName(input);
String decoded = ParamUtils.decodeName(encoded);
assertEquals(input, decoded);
}
// Input starts with underscore followed by hex-like sequence; verify behavior policy
@Test
void testUnderscoreFollowedByHexAmbiguityHandledByPolicy() {
String original = "1 _abcd";
if (!ParamUtils.isValid(original)) {
String processed = ParamUtils.encodeName(original);
assertEquals(original, ParamUtils.decodeName(processed));
}
}
// Round-trip for extreme boundary code points (NUL and U+FFFF)
@Test
void testBoundaryCharacters() {
String input = "\u0000\uFFFF";
String encoded = ParamUtils.encodeName(input);
String decoded = ParamUtils.decodeName(encoded);
assertEquals(input, decoded);
}
// Encoding keeps empty string as-is and preserves a single underscore
@Test
void testEncodeKeepsEmptyAndUnderscore() {
String empty = "";
String encodedEmpty = ParamUtils.encodeName(empty);
assertEquals("", encodedEmpty);
assertFalse(ParamUtils.isEncoded(encodedEmpty));
String underscoreOnly = "_";
assertTrue(ParamUtils.isValid(underscoreOnly));
String encodedUnderscore = ParamUtils.encodeName(underscoreOnly);
assertEquals(underscoreOnly, encodedUnderscore);
assertFalse(ParamUtils.isEncoded(encodedUnderscore));
}
// Encoding is idempotent for already-encoded output; decode restores original
@Test
void testAlreadyEncodedStringIsIdempotentOnEncode() {
String original = "with space and Ω and tab\t";
String first = ParamUtils.encodeName(original);
assertTrue(ParamUtils.isEncoded(first));
String second = ParamUtils.encodeName(first);
// encodeName should not double-encode an already valid string
assertEquals(first, second);
// decode should restore original
assertEquals(original, ParamUtils.decodeName(first));
}
// Round-trip for mixture of ASCII, control (tab), and underscore suffix
@Test
void testMixedUnicodeAndControlCharactersRoundTrip() {
String original = "A B\tC_";
String encoded = ParamUtils.encodeName(original);
assertTrue(ParamUtils.isEncoded(encoded));
String decoded = ParamUtils.decodeName(encoded);
assertEquals(original, decoded);
}
// Decoding a string with encoded prefix returns body; encoding leaves valid input unchanged
@Test
void testDecodeNameWithFakeEncodedPrefixBody() {
String fake = "_-.SYSENC:hello";
// This string is already valid; encodeName should return as-is
assertTrue(ParamUtils.isValid(fake));
assertEquals(fake, ParamUtils.encodeName(fake));
// decodeName should strip prefix and return body unchanged
assertEquals("hello", ParamUtils.decodeName(fake));
}
}

View File

@ -56,7 +56,7 @@ class AgentDetail extends React.Component {
this.setState({ loading: true });
const params = new URLSearchParams();
params.append('name', agentName);
params.append('agentName', agentName);
params.append('namespaceId', namespaceId);
request({
@ -95,7 +95,7 @@ class AgentDetail extends React.Component {
}
const params = new URLSearchParams();
params.append('name', agentName);
params.append('agentName', agentName);
params.append('namespaceId', namespaceId);
request({

View File

@ -128,7 +128,7 @@ class AgentManagement extends React.Component {
const data = {
pageNo: pageNo,
pageSize: pageSize,
name: searchName || '',
agentName: searchName || '',
search: 'blur',
namespaceId: namespaceId,
};
@ -217,7 +217,7 @@ class AgentManagement extends React.Component {
const { locale = {} } = this.props;
const namespaceId = getParams('namespace') || '';
const params = new URLSearchParams();
params.append('name', record.name);
params.append('agentName', record.name);
if (namespaceId) {
params.append('namespaceId', namespaceId);
}
@ -281,7 +281,7 @@ class AgentManagement extends React.Component {
const deletePromises = selectedRows.map(row => {
const params = new URLSearchParams();
params.append('name', row.name);
params.append('agentName', row.name);
if (namespaceId) {
params.append('namespaceId', namespaceId);
}

View File

@ -74,7 +74,7 @@ class NewAgent extends React.Component {
this.setState({ loading: true });
const params = new URLSearchParams();
params.append('name', agentName);
params.append('agentName', agentName);
params.append('namespaceId', namespaceId);
request({
@ -236,7 +236,7 @@ class NewAgent extends React.Component {
// 准备请求数据
const requestData = {
namespaceId: namespaceId,
name: values.name,
agentName: values.name,
version: values.version,
registrationType: isEdit ? '' : 'URL', // 默认使用 url 类型
agentCard: JSON.stringify(agentCard),

View File

@ -21,6 +21,7 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
import com.alibaba.nacos.ai.param.AgentHttpParamExtractor;
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
@ -34,6 +35,7 @@ import com.alibaba.nacos.api.model.v2.Result;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.console.proxy.ai.A2aProxy;
import com.alibaba.nacos.core.model.form.PageForm;
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.ApiType;
import com.alibaba.nacos.plugin.auth.constant.SignType;
@ -54,6 +56,7 @@ import java.util.List;
@NacosApi
@RestController
@RequestMapping(Constants.A2A.CONSOLE_PATH)
@ExtractorManager.Extractor(httpExtractor = AgentHttpParamExtractor.class)
public class ConsoleA2aController {
private final A2aProxy a2aProxy;
@ -151,7 +154,7 @@ public class ConsoleA2aController {
@Secured(action = ActionTypes.READ, signType = SignType.AI, apiType = ApiType.ADMIN_API)
public Result<List<AgentVersionDetail>> listAgentVersions(AgentForm agentForm) throws NacosException {
agentForm.validate();
return Result.success(a2aProxy.listAgentVersions(agentForm.getNamespaceId(), agentForm.getName()));
return Result.success(a2aProxy.listAgentVersions(agentForm.getNamespaceId(), agentForm.getAgentName()));
}
}

View File

@ -21,7 +21,7 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
import com.alibaba.nacos.ai.service.A2aServerOperationService;
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
@ -61,12 +61,13 @@ public class A2aInnerHandler implements A2aHandler {
@Override
public AgentCardDetailInfo getAgentCardWithVersions(AgentForm form) throws NacosException {
return a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getName(), form.getVersion());
return a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getAgentName(), form.getVersion(),
form.getRegistrationType());
}
@Override
public void deleteAgent(AgentForm form) throws NacosException {
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getName(), form.getVersion());
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getAgentName(), form.getVersion());
}
@Override
@ -77,7 +78,7 @@ public class A2aInnerHandler implements A2aHandler {
@Override
public Page<AgentCardVersionInfo> listAgents(AgentListForm agentListForm, PageForm pageForm) throws NacosException {
return a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getName(),
return a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
agentListForm.getSearch(), pageForm.getPageNo(), pageForm.getPageSize());
}

View File

@ -64,12 +64,12 @@ public class A2aRemoteHandler implements A2aHandler {
@Override
public AgentCardDetailInfo getAgentCardWithVersions(AgentForm form) throws NacosException {
return clientHolder.getAiMaintainerService()
.getAgentCard(form.getName(), form.getNamespaceId(), form.getRegistrationType());
.getAgentCard(form.getAgentName(), form.getNamespaceId(), form.getRegistrationType());
}
@Override
public void deleteAgent(AgentForm form) throws NacosException {
clientHolder.getAiMaintainerService().deleteAgent(form.getName(), form.getNamespaceId());
clientHolder.getAiMaintainerService().deleteAgent(form.getAgentName(), form.getNamespaceId());
}
@Override
@ -82,9 +82,9 @@ public class A2aRemoteHandler implements A2aHandler {
public Page<AgentCardVersionInfo> listAgents(AgentListForm agentListForm, PageForm pageForm) throws NacosException {
AiMaintainerService aiMaintainerService = clientHolder.getAiMaintainerService();
return Constants.MCP_LIST_SEARCH_BLUR.equalsIgnoreCase(agentListForm.getSearch())
? aiMaintainerService.searchAgentCardsByName(agentListForm.getNamespaceId(), agentListForm.getName(),
? aiMaintainerService.searchAgentCardsByName(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
pageForm.getPageNo(), pageForm.getPageSize())
: aiMaintainerService.listAgentCards(agentListForm.getNamespaceId(), agentListForm.getName(),
: aiMaintainerService.listAgentCards(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
pageForm.getPageNo(), pageForm.getPageSize());
}

View File

@ -35,7 +35,7 @@
<link rel="stylesheet" type="text/css" href="console-ui/public/css/icon.css">
<link rel="stylesheet" type="text/css" href="console-ui/public/css/font-awesome.css">
<!-- 第三方css结束 -->
<link href="./css/main.css?a5493de22114f2937036" rel="stylesheet"></head>
<link href="./css/main.css?ff94db2b949ca2c50f35" rel="stylesheet"></head>
<body>
<div id="root" style="overflow:hidden"></div>
@ -56,6 +56,6 @@
<script src="console-ui/public/js/merge.js"></script>
<script src="console-ui/public/js/loader.js"></script>
<!-- 第三方js结束 -->
<script type="text/javascript" src="./js/main.js?a5493de22114f2937036"></script></body>
<script type="text/javascript" src="./js/main.js?ff94db2b949ca2c50f35"></script></body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
/*
* Copyright 1999-2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.core.paramcheck.impl;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.remote.request.Request;
import com.alibaba.nacos.common.paramcheck.ParamInfo;
import com.alibaba.nacos.core.paramcheck.AbstractRpcParamExtractor;
import java.util.List;
/**
* Nacos A2A(Agent & Agent Card) grpc request param extractor.
*
* @author xiweng.yy
*/
public class AgentRequestParamExtractor extends AbstractRpcParamExtractor {
@Override
public List<ParamInfo> extractParam(Request request) throws NacosException {
AbstractAgentRequest agentRequest = (AbstractAgentRequest) request;
ParamInfo paramInfo = new ParamInfo();
paramInfo.setNamespaceId(agentRequest.getNamespaceId());
paramInfo.setAgentName(agentRequest.getAgentName());
if (agentRequest instanceof ReleaseAgentCardRequest) {
ReleaseAgentCardRequest releaseAgentCardRequest = (ReleaseAgentCardRequest) agentRequest;
if (null != releaseAgentCardRequest.getAgentCard()) {
paramInfo.setAgentName(releaseAgentCardRequest.getAgentCard().getName());
}
}
return List.of(paramInfo);
}
}

View File

@ -22,4 +22,5 @@ com.alibaba.nacos.core.paramcheck.impl.PersistentInstanceRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.ConfigRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.ConfigBatchListenRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.BatchInstanceRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.McpServerRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.McpServerRequestParamExtractor
com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor

View File

@ -215,7 +215,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
Map<String, String> params = new HashMap<>(4);
params.put("agentCard", JacksonUtils.toJson(agentCard));
params.put("namespaceId", namespaceId);
params.put("name", agentCard.getName());
params.put("agentName", agentCard.getName());
params.put("registrationType", registrationType);
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.POST).setParamValue(params)
.setPath(Constants.AdminApiPath.AI_AGENT_ADMIN_PATH).build();
@ -232,7 +232,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
RequestResource resource = buildRequestResource(namespaceId, agentName);
Map<String, String> params = new HashMap<>(1);
params.put("name", agentName);
params.put("agentName", agentName);
params.put("namespaceId", namespaceId);
params.put("registrationType", registrationType);
@ -252,7 +252,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
Map<String, String> params = new HashMap<>(5);
params.put("agentCard", JacksonUtils.toJson(agentCard));
params.put("namespaceId", namespaceId);
params.put("name", agentCard.getName());
params.put("agentName", agentCard.getName());
params.put("setAsLatest", String.valueOf(setAsLatest));
params.put("registrationType", registrationType);
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.PUT).setParamValue(params)
@ -269,7 +269,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
RequestResource resource = buildRequestResource(namespaceId, agentName);
Map<String, String> params = new HashMap<>(1);
params.put("name", agentName);
params.put("agentName", agentName);
params.put("namespaceId", namespaceId);
params.put("version", version);
@ -287,7 +287,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
RequestResource resource = buildRequestResource(namespaceId, agentName);
Map<String, String> params = new HashMap<>(1);
params.put("name", agentName);
params.put("agentName", agentName);
params.put("namespaceId", namespaceId);
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.GET).setParamValue(params)
@ -315,7 +315,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
int pageSize, boolean isBlur) throws NacosException {
RequestResource resource = buildRequestResource(namespaceId, null);
Map<String, String> params = new HashMap<>(1);
params.put("name", agentName);
params.put("agentName", agentName);
params.put("namespaceId", namespaceId);
params.put("search", isBlur ? SEARCH_BLUR : SEARCH_ACCURATE);
params.put("pageNo", String.valueOf(pageNo));