adapt to new registry api (#13849)

This commit is contained in:
Xin Luo 2025-09-23 15:36:33 +08:00 committed by GitHub
parent 2b9bfd4c07
commit a8e9f19de8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 154 additions and 181 deletions

View File

@ -398,26 +398,12 @@ public class McpServerTransformService {
out.setName(registryServer.getName());
out.setDescription(registryServer.getDescription());
// Map status from registry to nacos model (default to active)
out.setStatus(normalizeStatus(registryServer.getStatus()));
out.setStatus(registryServer.getStatus());
if (registryServer.getRepository() != null) {
out.setRepository(registryServer.getRepository());
}
}
/**
* Normalize registry status to nacos supported values.
*/
private String normalizeStatus(String status) {
if (status == null) {
return AiConstants.Mcp.MCP_STATUS_ACTIVE;
}
String s = status.trim().toLowerCase();
if (AiConstants.Mcp.MCP_STATUS_ACTIVE.equals(s) || AiConstants.Mcp.MCP_STATUS_DEPRECATED.equals(s)) {
return s;
}
return AiConstants.Mcp.MCP_STATUS_ACTIVE;
}
/**
* Derive registry id from _meta.official.id or repository.id.
*/
@ -426,9 +412,9 @@ public class McpServerTransformService {
McpRegistryServerDetail detail = (McpRegistryServerDetail) registryServer;
Meta meta = detail.getMeta();
if (meta != null && meta.getOfficial() != null) {
String id = meta.getOfficial().getId();
if (StringUtils.isNotBlank(id)) {
return id;
String serverId = meta.getOfficial().getServerId();
if (StringUtils.isNotBlank(serverId)) {
return serverId;
}
}
}
@ -451,10 +437,7 @@ public class McpServerTransformService {
if (v == null) {
v = new ServerVersionDetail();
}
String release = detail.getPublishedAt();
if (StringUtils.isBlank(release) && official != null) {
release = official.getPublishedAt();
}
String release = official != null ? official.getPublishedAt() : null;
if (StringUtils.isNotBlank(release)) {
v.setRelease_date(release);
}
@ -521,7 +504,7 @@ public class McpServerTransformService {
// Without packages, try transportType from the first remote
if (detail.getRemotes() != null && !detail.getRemotes().isEmpty()) {
Remote first = detail.getRemotes().get(0);
String tt = first != null ? first.getTransportType() : null;
String tt = first != null ? first.getType() : null;
if (tt != null) {
String lower = tt.trim().toLowerCase();
if (AiConstants.Mcp.OFFICIAL_TRANSPORT_SSE.equals(lower)) {
@ -591,7 +574,7 @@ public class McpServerTransformService {
FrontEndpointConfig cfg = new FrontEndpointConfig();
cfg.setEndpointData(endpointData);
cfg.setPath(StringUtils.isNotBlank(path) ? path : "/");
cfg.setType(remote.getTransportType());
cfg.setType(remote.getType());
cfg.setProtocol(isHttps ? "https" : AiConstants.Mcp.MCP_PROTOCOL_HTTP);
cfg.setEndpointType(AiConstants.Mcp.MCP_ENDPOINT_TYPE_DIRECT);
cfg.setHeaders(remote.getHeaders());

View File

@ -45,7 +45,7 @@ class McpServerTransformServiceTest {
@Test
void testTransformMcpRegistryServerList() throws Exception {
String registryJson = "{\"servers\":[{\"_meta\":{\"io.modelcontextprotocol.registry/official\":"
+ "{\"id\":\"4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0\"}},"
+ "{\"serverId\":\"4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0\"}},"
+ "\"name\":\"io.github.21st-dev/magic-mcp\","
+ "\"description\":\"It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server\","
+ "\"repository\":{\"url\":\"https://github.com/21st-dev/magic-mcp\",\"source\":\"github\",\"id\":\"935450522\"},"
@ -73,7 +73,7 @@ class McpServerTransformServiceTest {
@Test
void testTransformSingleMcpRegistryServer() throws Exception {
String registryJson = "{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"id\":\"d3669201-252f-403c-944b-c3ec0845782b\"}},"
String registryJson = "{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"serverId\":\"d3669201-252f-403c-944b-c3ec0845782b\"}},"
+ "\"name\":\"io.github.adfin-engineering/mcp-server-adfin\","
+ "\"description\":\"A Model Context Protocol Server for connecting with Adfin APIs\","
+ "\"repository\":{\"url\":\"https://github.com/Adfin-Engineering/mcp-server-adfin\",\"source\":\"github\",\"id\":\"951338147\"},"
@ -107,7 +107,7 @@ class McpServerTransformServiceTest {
@Test
void testTransformLegacyFormat() throws Exception {
String legacyJson = "{\"servers\":[{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"id\":\"legacy-server\"}}"
String legacyJson = "{\"servers\":[{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"serverId\":\"legacy-server\"}}"
+ ",\"name\":\"Legacy MCP Server\","
+ "\"description\":\"A legacy format server\"}]}";
@ -191,7 +191,7 @@ class McpServerTransformServiceTest {
@Test
void testUrlValidationWithValidPackage() throws Exception {
// Test with valid package format that doesn't trigger URL validation issues
String jsonWithValidPackage = "{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"id\":\"valid-server\"}},"
String jsonWithValidPackage = "{\"_meta\":{\"io.modelcontextprotocol.registry/official\":{\"serverId\":\"valid-server\"}},"
+ "\"name\":\"Valid Server\","
+ "\"repository\":{\"url\":\"https://github.com/test/valid-server\",\"source\":\"github\",\"id\":\"123\"},"
+ "\"version\":\"1.0.0\","

View File

@ -31,14 +31,12 @@ public class Input {
private String description;
@JsonProperty("is_required")
private Boolean isRequired;
private String format;
private String value;
@JsonProperty("is_secret")
private Boolean isSecret;
@JsonProperty("default")

View File

@ -16,8 +16,6 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* KeyValueInput used for headers / env vars.
*
@ -25,7 +23,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*/
public class KeyValueInput extends InputWithVariables {
@JsonProperty("name")
private String name;
public String getName() {

View File

@ -17,7 +17,6 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* McpRegistryServer (renamed from Server) to align with registry package naming.
@ -37,15 +36,8 @@ public class McpRegistryServer {
private String version;
@JsonProperty("website_url")
private String websiteUrl;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
public String getName() {
return name;
}
@ -93,20 +85,4 @@ public class McpRegistryServer {
public void setWebsiteUrl(String websiteUrl) {
this.websiteUrl = websiteUrl;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@ -39,9 +39,6 @@ public class McpRegistryServerDetail extends McpRegistryServer {
@JsonProperty("_meta")
private Meta meta;
@JsonProperty("published_at")
private String publishedAt;
public String getSchema() {
return schema;
}
@ -74,11 +71,4 @@ public class McpRegistryServerDetail extends McpRegistryServer {
this.meta = meta;
}
public String getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(String publishedAt) {
this.publishedAt = publishedAt;
}
}

View File

@ -16,11 +16,9 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
/**
* NamedArgument per components.schemas.NamedArgument.
@ -36,15 +34,8 @@ public class NamedArgument extends InputWithVariables implements Argument {
private String name;
@JsonProperty("is_repeated")
private Boolean isRepeated;
/**
* Optional UI/UX hint for value input; accept any JSON type to be forward-compatible.
*/
@JsonProperty("value_hint")
private JsonNode valueHint;
public String getType() {
return type;
}
@ -69,11 +60,4 @@ public class NamedArgument extends InputWithVariables implements Argument {
this.isRepeated = isRepeated;
}
public JsonNode getValueHint() {
return valueHint;
}
public void setValueHint(JsonNode valueHint) {
this.valueHint = valueHint;
}
}

View File

@ -17,7 +17,6 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Official metadata inside _meta.
@ -27,23 +26,30 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OfficialMeta {
private String id;
private String serverId;
private String versionId;
@JsonProperty("published_at")
private String publishedAt;
@JsonProperty("updated_at")
private String updatedAt;
@JsonProperty("is_latest")
private Boolean isLatest;
public String getId() {
return id;
public String getServerId() {
return serverId;
}
public void setId(String id) {
this.id = id;
public void setServerId(String serverId) {
this.serverId = serverId;
}
public String getVersionId() {
return versionId;
}
public void setVersionId(String versionId) {
this.versionId = versionId;
}
public String getPublishedAt() {

View File

@ -17,7 +17,6 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@ -29,29 +28,22 @@ import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Package {
@JsonProperty("registry_type")
private String registryType;
@JsonProperty("registry_base_url")
private String registryBaseUrl;
private String identifier;
private String version;
@JsonProperty("file_sha256")
private String fileSha256;
@JsonProperty("runtime_hint")
private String runtimeHint;
@JsonProperty("runtime_arguments")
private List<Argument> runtimeArguments;
@JsonProperty("package_arguments")
private List<Argument> packageArguments;
@JsonProperty("environment_variables")
private List<KeyValueInput> environmentVariables;
public String getRegistryType() {

View File

@ -17,7 +17,6 @@
package com.alibaba.nacos.api.ai.model.mcp.registry;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
/**
@ -31,10 +30,8 @@ public class PositionalArgument extends InputWithVariables implements Argument {
private String type = "positional";
@JsonProperty("value_hint")
private String valueHint;
@JsonProperty("is_repeated")
private Boolean isRepeated;
public String getType() {

View File

@ -30,20 +30,20 @@ import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Remote {
@JsonProperty("transport_type")
@JsonAlias("type")
private String transportType;
@JsonProperty("type")
@JsonAlias({"transport_type"})
private String type;
private String url;
private List<KeyValueInput> headers;
public String getTransportType() {
return transportType;
public String getType() {
return type;
}
public void setTransportType(String transportType) {
this.transportType = transportType;
public void setType(String type) {
this.type = type;
}
public String getUrl() {

View File

@ -38,39 +38,43 @@ class NacosMcpRegistryServerDetailTest extends BasicRequestTest {
mcpRegistryServerDetail.setDescription("test mcp registry server object");
mcpRegistryServerDetail.setRepository(new Repository());
mcpRegistryServerDetail.setVersion("1.0.0");
mcpRegistryServerDetail.setCreatedAt("2022-01-01T00:00:00Z");
mcpRegistryServerDetail.setUpdatedAt("2022-01-01T00:00:00Z");
mcpRegistryServerDetail.setPublishedAt("2022-01-01T00:00:00Z");
Meta meta = new Meta();
OfficialMeta official = new OfficialMeta();
official.setPublishedAt("2022-01-01T00:00:00Z");
meta.setOfficial(official);
mcpRegistryServerDetail.setMeta(meta);
mcpRegistryServerDetail.setRemotes(Collections.singletonList(new Remote()));
mcpRegistryServerDetail.getRemotes().get(0).setUrl("127.0.0.1:8848/sse");
mcpRegistryServerDetail.getRemotes().get(0).setTransportType("https");
mcpRegistryServerDetail.getRemotes().get(0).setType("https");
String json = mapper.writeValueAsString(mcpRegistryServerDetail);
assertNotNull(json);
assertTrue(json.contains("\"name\":\"testRegistryServer\""));
assertTrue(json.contains("\"description\":\"test mcp registry server object\""));
assertTrue(json.contains("\"repository\":{}"));
assertTrue(json.contains("\"version\":\"1.0.0\""));
assertTrue(json.contains("\"published_at\":\"2022-01-01T00:00:00Z\""));
assertTrue(json.contains("\"io.modelcontextprotocol.registry/official\""));
assertTrue(json.contains("\"publishedAt\":\"2022-01-01T00:00:00Z\""));
assertTrue(json.contains("\"remotes\":[{"));
assertTrue(json.contains("\"url\":\"127.0.0.1:8848/sse\""));
assertTrue(json.contains("\"transport_type\":\"https\""));
assertTrue(json.contains("\"type\":\"https\""));
}
@Test
void testDeserialize() throws JsonProcessingException {
String json = "{\"name\":\"testRegistryServer\",\"description\":\"test mcp registry server object\","
+ "\"repository\":{},\"version\":\"1.0.0\",\"remotes\":[{\"transport_type\":\"https\","
+ "\"url\":\"127.0.0.1:8848/sse\"}],\"published_at\":\"2022-01-01T00:00:00Z\"}";
+ "\"repository\":{},\"version\":\"1.0.0\",\"remotes\":[{\"type\":\"https\","
+ "\"url\":\"127.0.0.1:8848/sse\"}],\"_meta\":{\"io.modelcontextprotocol.registry/official\":"
+ "{\"publishedAt\":\"2022-01-01T00:00:00Z\"}}}";
McpRegistryServerDetail mcpRegistryServerDetail = mapper.readValue(json, McpRegistryServerDetail.class);
assertNotNull(mcpRegistryServerDetail);
assertEquals("testRegistryServer", mcpRegistryServerDetail.getName());
assertEquals("test mcp registry server object", mcpRegistryServerDetail.getDescription());
assertNotNull(mcpRegistryServerDetail.getRepository());
assertEquals("1.0.0", mcpRegistryServerDetail.getVersion());
assertEquals("2022-01-01T00:00:00Z", mcpRegistryServerDetail.getPublishedAt());
assertEquals("2022-01-01T00:00:00Z", mcpRegistryServerDetail.getMeta().getOfficial().getPublishedAt());
assertNotNull(mcpRegistryServerDetail.getRemotes());
assertEquals(1, mcpRegistryServerDetail.getRemotes().size());
assertEquals("https", mcpRegistryServerDetail.getRemotes().get(0).getTransportType());
assertEquals("https", mcpRegistryServerDetail.getRemotes().get(0).getType());
assertEquals("127.0.0.1:8848/sse", mcpRegistryServerDetail.getRemotes().get(0).getUrl());
}
}

View File

@ -412,8 +412,9 @@ class McpDetail extends React.Component {
serverName = 'mcp-server';
}
const serverConfig = {};
if (packageDef.runtime_hint) {
serverConfig.command = packageDef.runtime_hint;
const runtimeHint = packageDef.runtimeHint || packageDef.runtime_hint;
if (runtimeHint) {
serverConfig.command = runtimeHint;
} else if (this.getRegistryType(packageDef) === 'npm') {
serverConfig.command = 'npx';
} else {
@ -433,8 +434,9 @@ class McpDetail extends React.Component {
// 检查是否已经有runtime_arguments包含了包名
let hasPackageInRuntimeArgs = false;
if (packageDef.runtime_arguments && Array.isArray(packageDef.runtime_arguments)) {
for (const arg of packageDef.runtime_arguments) {
const runtimeArguments = packageDef.runtimeArguments || packageDef.runtime_arguments || [];
if (runtimeArguments && Array.isArray(runtimeArguments)) {
for (const arg of runtimeArguments) {
if (arg.value && arg.value.includes(pkgName)) {
hasPackageInRuntimeArgs = true;
break;
@ -443,8 +445,8 @@ class McpDetail extends React.Component {
}
// 先添加运行时参数
if (packageDef.runtime_arguments && Array.isArray(packageDef.runtime_arguments)) {
packageDef.runtime_arguments.forEach(arg => {
if (runtimeArguments && Array.isArray(runtimeArguments)) {
runtimeArguments.forEach(arg => {
args.push(...this.processArgument(arg));
});
}
@ -484,8 +486,9 @@ class McpDetail extends React.Component {
}
// 添加包参数
if (packageDef.package_arguments && Array.isArray(packageDef.package_arguments)) {
packageDef.package_arguments.forEach(arg => {
const packageArguments = packageDef.packageArguments || packageDef.package_arguments || [];
if (packageArguments && Array.isArray(packageArguments)) {
packageArguments.forEach(arg => {
args.push(...this.processArgument(arg));
});
}
@ -493,9 +496,11 @@ class McpDetail extends React.Component {
serverConfig.args = args;
// 处理环境变量
if (packageDef.environment_variables && Array.isArray(packageDef.environment_variables)) {
const environmentVariables =
packageDef.environmentVariables || packageDef.environment_variables || [];
if (environmentVariables && Array.isArray(environmentVariables)) {
const env = {};
packageDef.environment_variables.forEach(envVar => {
environmentVariables.forEach(envVar => {
if (envVar.name) {
let value = envVar.value || envVar.default;
if (!value) {
@ -542,8 +547,8 @@ class McpDetail extends React.Component {
case 'positional':
if (arg.value) {
result.push(this.replaceVariables(arg.value, arg.variables));
} else if (arg.value_hint) {
result.push(`<${arg.value_hint}>`);
} else if (arg.value_hint || arg.valueHint) {
result.push(`<${arg.value_hint || arg.valueHint}>`);
} else if (arg.default) {
result.push(this.replaceVariables(arg.default, arg.variables));
}
@ -607,9 +612,13 @@ class McpDetail extends React.Component {
const isTabsExpanded = this.state.packageTabsExpanded[index];
// 统计各类参数数量
const runtimeArgsCount = packageDef.runtime_arguments?.length || 0;
const packageArgsCount = packageDef.package_arguments?.length || 0;
const envVarsCount = packageDef.environment_variables?.length || 0;
const runtimeArguments = packageDef.runtimeArguments || packageDef.runtime_arguments || [];
const packageArguments = packageDef.packageArguments || packageDef.package_arguments || [];
const environmentVariables =
packageDef.environmentVariables || packageDef.environment_variables || [];
const runtimeArgsCount = runtimeArguments?.length || 0;
const packageArgsCount = packageArguments?.length || 0;
const envVarsCount = environmentVariables?.length || 0;
const totalParamsCount = runtimeArgsCount + packageArgsCount + envVarsCount;
return (
@ -731,7 +740,7 @@ class McpDetail extends React.Component {
{this.getRegistryType(packageDef)}
</p>
</Col>
{packageDef.runtime_hint && (
{(packageDef.runtimeHint || packageDef.runtime_hint) && (
<Col span={24} style={{ display: 'flex', marginBottom: '8px' }}>
<p style={{ minWidth: 120, fontWeight: 'bold', color: '#000' }}>
{locale.runtimeHint || '运行时提示'}:
@ -745,7 +754,7 @@ class McpDetail extends React.Component {
color: '#000',
}}
>
{packageDef.runtime_hint}
{packageDef.runtimeHint || packageDef.runtime_hint}
</p>
</Col>
)}
@ -831,14 +840,14 @@ class McpDetail extends React.Component {
</div>
{this.state.parameterContainersExpanded[index]?.runtime && (
<div style={{ padding: '8px 16px' }}>
{packageDef.runtime_arguments.map((arg, argIndex) => (
{runtimeArguments.map((arg, argIndex) => (
<div
key={argIndex}
style={{
marginBottom: '8px',
paddingBottom: '8px',
borderBottom:
argIndex < packageDef.runtime_arguments.length - 1
argIndex < runtimeArguments.length - 1
? '1px solid #e6e6e6'
: 'none',
}}
@ -916,14 +925,14 @@ class McpDetail extends React.Component {
</div>
{this.state.parameterContainersExpanded[index]?.package && (
<div style={{ padding: '8px 16px' }}>
{packageDef.package_arguments.map((arg, argIndex) => (
{packageArguments.map((arg, argIndex) => (
<div
key={argIndex}
style={{
marginBottom: '8px',
paddingBottom: '8px',
borderBottom:
argIndex < packageDef.package_arguments.length - 1
argIndex < packageArguments.length - 1
? '1px solid #e6e6e6'
: 'none',
}}
@ -999,14 +1008,14 @@ class McpDetail extends React.Component {
</div>
{this.state.parameterContainersExpanded[index]?.env && (
<div style={{ padding: '8px 16px' }}>
{packageDef.environment_variables.map((envVar, envIndex) => (
{environmentVariables.map((envVar, envIndex) => (
<div
key={envIndex}
style={{
marginBottom: '8px',
paddingBottom: '8px',
borderBottom:
envIndex < packageDef.environment_variables.length - 1
envIndex < environmentVariables.length - 1
? '1px solid #e6e6e6'
: 'none',
}}
@ -1047,7 +1056,7 @@ class McpDetail extends React.Component {
{envVar.value || envVar.default || '<未设置>'}
</span>
<div style={{ display: 'flex', gap: '6px' }}>
{envVar.is_required && (
{(envVar.isRequired || envVar.is_required) && (
<span
style={{
backgroundColor: '#ff4d4f',
@ -1061,7 +1070,7 @@ class McpDetail extends React.Component {
必填
</span>
)}
{envVar.is_secret && (
{(envVar.isSecret || envVar.is_secret) && (
<span
style={{
backgroundColor: '#faad14',
@ -1120,7 +1129,7 @@ class McpDetail extends React.Component {
// 注册表类型优先 registry_type兼容旧 registry_name
getRegistryType = packageDef => {
if (!packageDef) return '';
return packageDef.registry_type || packageDef.registry_name || '';
return packageDef.registryType || packageDef.registry_type || packageDef.registry_name || '';
};
// 包名显示与链接用优先 identifier兼容旧 name
@ -1131,10 +1140,10 @@ class McpDetail extends React.Component {
// 获取包名对应的仓库链接
getPackageRepositoryUrl = packageDef => {
const registry_name = this.getRegistryType(packageDef);
const registryType = this.getRegistryType(packageDef);
const name = (packageDef && (packageDef.identifier || packageDef.name)) || '';
switch (registry_name) {
switch (registryType) {
case 'npm':
return `https://www.npmjs.com/package/${name}`;
case 'docker':
@ -1247,7 +1256,7 @@ class McpDetail extends React.Component {
>
{header.name}
</span>
{header.is_required && (
{(header.isRequired || header.is_required) && (
<span
style={{
backgroundColor: '#ff4d4f',
@ -1262,7 +1271,7 @@ class McpDetail extends React.Component {
必填
</span>
)}
{header.is_secret && (
{(header.isSecret || header.is_secret) && (
<span
style={{
backgroundColor: '#faad14',
@ -1468,7 +1477,7 @@ class McpDetail extends React.Component {
const versionSelections = [];
for (let i = 0; i < versions.length; i++) {
const item = versions[i];
if (item.is_latest) {
if (item.isLatest || item.is_latest) {
versionSelections.push({
label: item.version + ` (` + locale.versionIsPublished + ')',
value: item.version,

View File

@ -132,7 +132,10 @@ class NewMcpServer extends React.Component {
const allPublishedVersions = [];
for (let i = 0; i < allVersions.length; i++) {
if (i === allVersions.length - 1 && !allVersions[i].is_latest) {
if (
i === allVersions.length - 1 &&
!(allVersions[i].isLatest || allVersions[i].is_latest)
) {
break;
}
allPublishedVersions.push(allVersions[i].version);
@ -140,7 +143,7 @@ class NewMcpServer extends React.Component {
this.setState({
currentVersion: versionDetail.version,
isLatestVersion: versionDetail.is_latest,
isLatestVersion: versionDetail.isLatest || versionDetail.is_latest,
versionsList: allPublishedVersions,
});
@ -250,21 +253,21 @@ class NewMcpServer extends React.Component {
}
const pkg = {
registry_type: this.inferRegistryType(parsedCommand),
registryType: this.inferRegistryType(parsedCommand),
identifier: this.extractPackageNameFromArgs(parsedArgs, parsedCommand),
version: this.extractPackageVersionFromArgs(parsedArgs),
};
// 处理 runtime hint runtime arguments
if (parsedCommand && parsedCommand !== pkg.identifier) {
pkg.runtime_hint = parsedCommand;
pkg.runtimeHint = parsedCommand;
// args 中提取 runtime_arguments package_arguments
if (parsedArgs && Array.isArray(parsedArgs)) {
const { runtimeArgs, packageArgs } = this.separateArguments(parsedArgs, pkg.identifier);
if (runtimeArgs.length > 0) {
pkg.runtime_arguments = runtimeArgs.map(arg => ({
pkg.runtimeArguments = runtimeArgs.map(arg => ({
type: 'positional',
value: arg,
format: 'string',
@ -272,7 +275,7 @@ class NewMcpServer extends React.Component {
}
if (packageArgs.length > 0) {
pkg.package_arguments = packageArgs.map(arg => ({
pkg.packageArguments = packageArgs.map(arg => ({
type: 'positional',
value: arg,
format: 'string',
@ -281,7 +284,7 @@ class NewMcpServer extends React.Component {
}
} else if (parsedArgs && Array.isArray(parsedArgs)) {
// 如果 command 就是包名所有 args 都是 package_arguments
pkg.package_arguments = parsedArgs.map(arg => ({
pkg.packageArguments = parsedArgs.map(arg => ({
type: 'positional',
value: arg,
format: 'string',
@ -290,7 +293,7 @@ class NewMcpServer extends React.Component {
// 处理环境变量
if (config.env && typeof config.env === 'object') {
pkg.environment_variables = Object.entries(config.env).map(([name, value]) => ({
pkg.environmentVariables = Object.entries(config.env).map(([name, value]) => ({
name: name,
value: value,
format: 'string',
@ -1088,7 +1091,9 @@ class NewMcpServer extends React.Component {
let hasDraftVersion = false;
if (versions.length > 0) {
hasDraftVersion = !versions[versions.length - 1].is_latest;
hasDraftVersion = !(
versions[versions.length - 1].isLatest || versions[versions.length - 1].is_latest
);
}
let currentVersionExist = versions

View File

@ -217,7 +217,8 @@ public class NacosMcpRegistryService {
if (detail == null) {
return;
}
detail.setStatus(AiConstants.Mcp.MCP_STATUS_ACTIVE);
// Align with enum-based status in registry model
detail.setStatus("active");
detail.setSchema("https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json");
Meta meta = detail.getMeta();
if (meta == null) {
@ -227,10 +228,19 @@ public class NacosMcpRegistryService {
if (official == null) {
official = new OfficialMeta();
}
official.setId(id);
official.setPublishedAt(detail.getPublishedAt());
official.setUpdatedAt(detail.getUpdatedAt());
official.setIsLatest(detail.getUpdatedAt() != null && detail.getUpdatedAt().equals(detail.getPublishedAt()));
official.setServerId(id);
// published/updated timestamps should be carried in official meta rather than top-level
// Keep existing values if already set
if (official.getPublishedAt() == null && detail.getMeta() != null && detail.getMeta().getOfficial() != null) {
official.setPublishedAt(detail.getMeta().getOfficial().getPublishedAt());
}
if (official.getUpdatedAt() == null && detail.getMeta() != null && detail.getMeta().getOfficial() != null) {
official.setUpdatedAt(detail.getMeta().getOfficial().getUpdatedAt());
}
// If still null, do not synthesize values here
if (official.getIsLatest() == null && detail.getMeta() != null && detail.getMeta().getOfficial() != null) {
official.setIsLatest(detail.getMeta().getOfficial().getIsLatest());
}
meta.setOfficial(official);
detail.setMeta(meta);
}
@ -266,7 +276,7 @@ public class NacosMcpRegistryService {
}
return endpoints.stream().map((item) -> {
Remote remote = new Remote();
remote.setTransportType(transport);
remote.setType(transport);
remote.setUrl(String.format("%s://%s:%d%s", Constants.PROTOCOL_TYPE_HTTP, item.getAddress(),
item.getPort(), item.getPath()));
KeyValueInput headerAuth = new KeyValueInput();
@ -310,9 +320,20 @@ public class NacosMcpRegistryService {
if (mcpServerDetail.getVersionDetail() != null) {
result.setVersion(mcpServerDetail.getVersionDetail().getVersion());
String iso = toRfc3339(mcpServerDetail.getVersionDetail().getRelease_date());
result.setCreatedAt(iso);
result.setUpdatedAt(iso);
result.setPublishedAt(iso);
Meta meta = result.getMeta();
if (meta == null) {
meta = new Meta();
}
OfficialMeta official = meta.getOfficial();
if (official == null) {
official = new OfficialMeta();
}
official.setPublishedAt(iso);
official.setUpdatedAt(iso);
// mark latest when release equals update in our simple synthesis
official.setIsLatest(Boolean.TRUE);
meta.setOfficial(official);
result.setMeta(meta);
}
enrich(result, id, mcpServerDetail.getFrontProtocol(),
mcpServerDetail.getFrontendEndpoints(), mcpServerDetail.getBackendEndpoints());

View File

@ -18,6 +18,8 @@ package com.alibaba.nacos.mcpregistry.controller;
import com.alibaba.nacos.api.ai.model.mcp.registry.McpRegistryServerDetail;
import com.alibaba.nacos.api.ai.model.mcp.registry.McpRegistryServerList;
import com.alibaba.nacos.api.ai.model.mcp.registry.Meta;
import com.alibaba.nacos.api.ai.model.mcp.registry.OfficialMeta;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import com.alibaba.nacos.mcpregistry.form.GetServerForm;
import com.alibaba.nacos.mcpregistry.service.NacosMcpRegistryService;
@ -150,9 +152,12 @@ class McpRegistryControllerTest {
McpRegistryServerDetail d = new McpRegistryServerDetail();
d.setName(id + "-name");
d.setDescription("desc-" + id);
d.setPublishedAt(publishedAt);
d.setCreatedAt(publishedAt);
d.setUpdatedAt(updatedAt);
Meta meta = new Meta();
OfficialMeta official = new OfficialMeta();
official.setPublishedAt(publishedAt);
official.setUpdatedAt(updatedAt);
meta.setOfficial(official);
d.setMeta(meta);
return d;
}

View File

@ -236,7 +236,9 @@ class NacosMcpRegistryServiceTest {
assertEquals("Description:" + RANDOM_NAMESPACE_ID, result.getDescription());
assertNull(result.getRepository());
assertEquals("1.0.0", result.getVersion());
assertEquals("2025-06-10T02:29:17Z", result.getPublishedAt());
assertNotNull(result.getMeta());
assertNotNull(result.getMeta().getOfficial());
assertEquals("2025-06-10T02:29:17Z", result.getMeta().getOfficial().getPublishedAt());
assertNull(result.getRemotes());
}
@ -254,10 +256,12 @@ class NacosMcpRegistryServiceTest {
assertEquals("Description:" + RANDOM_NAMESPACE_ID, result.getDescription());
assertNull(result.getRepository());
assertEquals("1.0.0", result.getVersion());
assertEquals("2025-06-10T02:29:17Z", result.getPublishedAt());
assertNotNull(result.getMeta());
assertNotNull(result.getMeta().getOfficial());
assertEquals("2025-06-10T02:29:17Z", result.getMeta().getOfficial().getPublishedAt());
assertNotNull(result.getRemotes());
assertEquals(1, result.getRemotes().size());
assertEquals("sse", result.getRemotes().get(0).getTransportType());
assertEquals("sse", result.getRemotes().get(0).getType());
assertEquals("http://127.0.0.1:8080/api/path", result.getRemotes().get(0).getUrl());
}
@ -296,15 +300,17 @@ class NacosMcpRegistryServiceTest {
for (int i = 0; i < actualSize; i++) {
McpServerBasicInfo basicInfo = mockMcpServerBasicInfo(i, namespaceId);
mockPage.getPageItems().add(basicInfo);
// ensure getServer won't return null by mocking index and detail lookup for each generated id
// ensure getServer won't return null by mocking index and detail lookup for
// each generated id
McpServerIndexData indexData = new McpServerIndexData();
indexData.setId(basicInfo.getId());
indexData.setNamespaceId(namespaceId);
Mockito.lenient().when(mcpServerIndex.getMcpServerById(basicInfo.getId())).thenReturn(indexData);
// default detail mocks without backend endpoints or tools
try {
Mockito.lenient().when(mcpServerOperationService.getMcpServerDetail(namespaceId, basicInfo.getId(), null, null))
.thenReturn(mockMcpServerDetailInfo(basicInfo.getId(), namespaceId, false, false));
Mockito.lenient()
.when(mcpServerOperationService.getMcpServerDetail(namespaceId, basicInfo.getId(), null, null))
.thenReturn(mockMcpServerDetailInfo(basicInfo.getId(), namespaceId, false, false));
} catch (NacosException e) {
throw new RuntimeException(e);
}