@ycodium-ai/plugin-connector-registry 0.2.3766 → 0.2.3799
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -11
- package/dist/index.js +250 -118
- package/locales/en.json +5 -6
- package/locales/zh-CN.json +6 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
# @ycodium-ai/plugin-connector-registry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lets you pick MCP servers from the official MCP registry
|
|
4
|
+
(`https://registry.modelcontextprotocol.io`) in the Add server dialog under
|
|
5
|
+
machine Settings → MCP & skills. Anyone can list a server in that registry and
|
|
6
|
+
entries are not reviewed.
|
|
4
7
|
|
|
5
8
|
## What it does
|
|
6
9
|
|
|
7
|
-
The plugin exposes a `search` command
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
and
|
|
10
|
+
The plugin exposes a `search` command and an agent tool of the same name
|
|
11
|
+
(`connector-registry_search`). Both query `GET <registry>/v0/servers` with
|
|
12
|
+
`search`, `limit` and `version=latest`, keep entries the registry marks active
|
|
13
|
+
and latest, and turn each into a draft the dialog can fill in:
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
+
- an npm package runs as `npx -y <package>@<version>`, a PyPI package as
|
|
16
|
+
`uvx <package>==<version>`; a remote server becomes an HTTP or SSE transport
|
|
17
|
+
with its headers;
|
|
18
|
+
- secrets are left empty, and every value the user still has to supply is
|
|
19
|
+
listed in `needs`;
|
|
20
|
+
- entries that only ship other package types (OCI images, NuGet, MCPB) come
|
|
21
|
+
back as `manual`.
|
|
22
|
+
|
|
23
|
+
The registry address comes from the entry config (`url`), which the host fills
|
|
24
|
+
from `YCODIUM_MCP_REGISTRY_URL` and the settings card can override. When it is
|
|
25
|
+
empty the official registry is used. Another registry must serve the same
|
|
26
|
+
`/v0/servers` interface.
|
|
27
|
+
|
|
28
|
+
Results are cached per query for ten minutes. When the registry cannot be
|
|
29
|
+
reached, `search` returns the last cached result for that query with
|
|
30
|
+
`stale: true`, and fails only when there is none.
|
|
15
31
|
|
|
16
32
|
## Install
|
|
17
33
|
|
|
@@ -23,9 +39,9 @@ This plugin declares no extra permissions. Outbound fetches use the host HTTP
|
|
|
23
39
|
client, including the machine proxy.
|
|
24
40
|
|
|
25
41
|
Ycodium ships it enabled as `core.connector-registry`. Disabling it hides the
|
|
26
|
-
|
|
27
|
-
restart does not bring it back. Reinstalling the
|
|
28
|
-
restores the same entry without an npm fetch.
|
|
42
|
+
registry picker in the Add server dialog. Uninstalling it writes a removal to
|
|
43
|
+
the home patch so a restart does not bring it back. Reinstalling the
|
|
44
|
+
unversioned bundled package restores the same entry without an npm fetch.
|
|
29
45
|
|
|
30
46
|
## Developing
|
|
31
47
|
|
package/dist/index.js
CHANGED
|
@@ -1,202 +1,335 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
// src/registry.ts
|
|
5
|
+
var DEFAULT_REGISTRY_URL = "https://registry.modelcontextprotocol.io";
|
|
6
|
+
var OFFICIAL_META_KEY = "io.modelcontextprotocol.registry/official";
|
|
7
|
+
var FALLBACK_SERVER_NAME = "mcp-server";
|
|
8
|
+
var RESERVED_SERVER_NAME = "ycodium";
|
|
9
|
+
var PLACEHOLDER = /\{[^{}]*\}/;
|
|
10
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
var str = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
12
|
+
var records = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
|
|
13
|
+
var serversEndpoint = (base) => {
|
|
14
|
+
const trimmed = base.trim().replace(/\/+$/, "");
|
|
15
|
+
return trimmed.endsWith("/servers") ? trimmed : `${trimmed}/v0/servers`;
|
|
16
|
+
};
|
|
17
|
+
var searchUrl = (base, query, limit) => {
|
|
18
|
+
const url = new URL(serversEndpoint(base));
|
|
19
|
+
url.searchParams.set("limit", String(limit));
|
|
20
|
+
url.searchParams.set("version", "latest");
|
|
21
|
+
const needle = query.trim();
|
|
22
|
+
if (needle.length > 0) url.searchParams.set("search", needle);
|
|
23
|
+
return url.toString();
|
|
24
|
+
};
|
|
25
|
+
var suggestServerName = (registryName) => {
|
|
26
|
+
const last = registryName.split("/").pop() ?? "";
|
|
27
|
+
const slug = last.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, 64);
|
|
28
|
+
return slug.length === 0 || slug === RESERVED_SERVER_NAME ? FALLBACK_SERVER_NAME : slug;
|
|
29
|
+
};
|
|
30
|
+
var argumentParts = (argument) => {
|
|
31
|
+
const required = argument["isRequired"] === true;
|
|
32
|
+
const value = str(argument["value"]) ?? (required ? str(argument["default"]) : void 0);
|
|
33
|
+
if (value === void 0 && !required) return [];
|
|
34
|
+
if (argument["type"] === "named") {
|
|
35
|
+
const name = str(argument["name"]);
|
|
36
|
+
if (name === void 0) return [];
|
|
37
|
+
const flag = name.startsWith("-") ? name : `--${name}`;
|
|
38
|
+
return [flag, value ?? `<${str(argument["valueHint"]) ?? name.replace(/^-+/, "")}>`];
|
|
39
|
+
}
|
|
40
|
+
return [value ?? `<${str(argument["valueHint"]) ?? str(argument["name"]) ?? "value"}>`];
|
|
41
|
+
};
|
|
42
|
+
var argumentsOf = (value) => records(value).flatMap(argumentParts);
|
|
43
|
+
var WHOLE_ARGUMENT_PLACEHOLDER = /^<[^<>]+>$/;
|
|
44
|
+
var TEMPLATES = /\{[^{}]*\}/g;
|
|
45
|
+
var argumentNeeds = (args) => args.flatMap(
|
|
46
|
+
(arg) => WHOLE_ARGUMENT_PLACEHOLDER.test(arg) ? [arg] : arg.match(TEMPLATES) ?? []
|
|
47
|
+
);
|
|
48
|
+
var fillDeclared = (declared) => {
|
|
49
|
+
const values = {};
|
|
50
|
+
const needs = [];
|
|
51
|
+
const optional = [];
|
|
52
|
+
for (const entry of records(declared)) {
|
|
53
|
+
const name = str(entry["name"]);
|
|
54
|
+
if (name === void 0) continue;
|
|
55
|
+
const secret = entry["isSecret"] === true;
|
|
56
|
+
const required = entry["isRequired"] === true;
|
|
57
|
+
const given = str(entry["value"]) ?? str(entry["default"]);
|
|
58
|
+
if (!secret && !required && given === void 0) continue;
|
|
59
|
+
const value = secret ? given !== void 0 && PLACEHOLDER.test(given) ? given : "" : given ?? "";
|
|
60
|
+
values[name] = value;
|
|
61
|
+
if (required || PLACEHOLDER.test(value)) needs.push(name);
|
|
62
|
+
else if (secret) optional.push(name);
|
|
63
|
+
}
|
|
64
|
+
return { values, needs, optional };
|
|
65
|
+
};
|
|
66
|
+
var packageSpec = (identifier, version, separator) => version === void 0 || version === "latest" ? identifier : `${identifier}${separator}${version}`;
|
|
67
|
+
var stdioSetup = (pkg, name, runner) => {
|
|
68
|
+
const identifier = str(pkg["identifier"]);
|
|
69
|
+
if (identifier === void 0) return null;
|
|
70
|
+
const spec = packageSpec(identifier, str(pkg["version"]), runner === "npx" ? "@" : "==");
|
|
71
|
+
const runtime = argumentsOf(pkg["runtimeArguments"]);
|
|
72
|
+
const confirm = runner === "npx" && !runtime.includes("-y") && !runtime.includes("--yes");
|
|
73
|
+
const args = [
|
|
74
|
+
...confirm ? ["-y"] : [],
|
|
75
|
+
...runtime,
|
|
76
|
+
spec,
|
|
77
|
+
...argumentsOf(pkg["packageArguments"])
|
|
78
|
+
];
|
|
79
|
+
const env = fillDeclared(pkg["environmentVariables"]);
|
|
80
|
+
return {
|
|
81
|
+
kind: "ready",
|
|
82
|
+
name,
|
|
83
|
+
transport: {
|
|
84
|
+
type: "stdio",
|
|
85
|
+
command: runner,
|
|
86
|
+
args,
|
|
87
|
+
...Object.keys(env.values).length > 0 ? { env: env.values } : {}
|
|
88
|
+
},
|
|
89
|
+
needs: [...env.needs, ...argumentNeeds(args)],
|
|
90
|
+
optional: env.optional
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
var remoteSetup = (remote, type, name) => {
|
|
94
|
+
const url = str(remote["url"]);
|
|
95
|
+
if (url === void 0 || PLACEHOLDER.test(url) || !/^https?:\/\//.test(url)) return null;
|
|
96
|
+
const headers = fillDeclared(remote["headers"]);
|
|
97
|
+
return {
|
|
98
|
+
kind: "ready",
|
|
99
|
+
name,
|
|
100
|
+
transport: {
|
|
101
|
+
type,
|
|
102
|
+
url,
|
|
103
|
+
...Object.keys(headers.values).length > 0 ? { headers: headers.values } : {}
|
|
104
|
+
},
|
|
105
|
+
needs: headers.needs,
|
|
106
|
+
optional: headers.optional
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
var isStdioPackage = (pkg, registryType) => pkg["registryType"] === registryType && isRecord(pkg["transport"]) && pkg["transport"]["type"] === "stdio";
|
|
110
|
+
var setupOf = (server) => {
|
|
111
|
+
const name = suggestServerName(str(server["name"]) ?? "");
|
|
112
|
+
const packages = records(server["packages"]);
|
|
113
|
+
const remotes = records(server["remotes"]);
|
|
114
|
+
const candidates = [
|
|
115
|
+
...packages.filter((pkg) => isStdioPackage(pkg, "npm")).map((pkg) => () => stdioSetup(pkg, name, "npx")),
|
|
116
|
+
...remotes.filter((remote) => remote["type"] === "streamable-http").map((remote) => () => remoteSetup(remote, "http", name)),
|
|
117
|
+
...remotes.filter((remote) => remote["type"] === "sse").map((remote) => () => remoteSetup(remote, "sse", name)),
|
|
118
|
+
...packages.filter((pkg) => isStdioPackage(pkg, "pypi")).map((pkg) => () => stdioSetup(pkg, name, "uvx"))
|
|
119
|
+
];
|
|
120
|
+
for (const candidate of candidates) {
|
|
121
|
+
const setup = candidate();
|
|
122
|
+
if (setup !== null) return setup;
|
|
123
|
+
}
|
|
124
|
+
return { kind: "manual" };
|
|
125
|
+
};
|
|
126
|
+
var httpsUrl = (value) => {
|
|
127
|
+
const url = str(value);
|
|
128
|
+
return url !== void 0 && url.startsWith("https://") ? url : void 0;
|
|
129
|
+
};
|
|
130
|
+
var isCurrent = (meta) => {
|
|
131
|
+
if (!isRecord(meta)) return false;
|
|
132
|
+
const official = meta[OFFICIAL_META_KEY];
|
|
133
|
+
return isRecord(official) && official["status"] === "active" && official["isLatest"] === true;
|
|
134
|
+
};
|
|
135
|
+
var toServer = (entry) => {
|
|
136
|
+
const server = entry["server"];
|
|
137
|
+
if (!isRecord(server) || !isCurrent(entry["_meta"])) return null;
|
|
138
|
+
const id = str(server["name"]);
|
|
139
|
+
const version = str(server["version"]);
|
|
140
|
+
if (id === void 0 || version === void 0) return null;
|
|
141
|
+
const slash = id.indexOf("/");
|
|
142
|
+
const repository = isRecord(server["repository"]) ? server["repository"]["url"] : void 0;
|
|
143
|
+
const homepage = httpsUrl(server["websiteUrl"]) ?? httpsUrl(repository);
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
title: str(server["title"]) ?? id.slice(slash + 1),
|
|
147
|
+
publisher: slash > 0 ? id.slice(0, slash) : id,
|
|
148
|
+
description: str(server["description"]) ?? "",
|
|
149
|
+
version,
|
|
150
|
+
...homepage === void 0 ? {} : { homepage },
|
|
151
|
+
setup: setupOf(server)
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
var parseRegistryResponse = (body) => {
|
|
155
|
+
if (!isRecord(body) || !Array.isArray(body["servers"])) return null;
|
|
156
|
+
return records(body["servers"]).flatMap((entry) => {
|
|
157
|
+
const server = toServer(entry);
|
|
158
|
+
return server === null ? [] : [server];
|
|
159
|
+
});
|
|
18
160
|
};
|
|
19
161
|
|
|
20
162
|
// src/index.ts
|
|
21
163
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
22
164
|
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
165
|
+
var SEARCH_LIMIT = 30;
|
|
23
166
|
var SEARCH_PAYLOAD_SCHEMA = {
|
|
24
167
|
type: "object",
|
|
25
|
-
properties: { query: { type: "string" } },
|
|
168
|
+
properties: { query: { type: "string", maxLength: 200 } },
|
|
26
169
|
required: ["query"],
|
|
27
170
|
additionalProperties: false
|
|
28
171
|
};
|
|
29
172
|
var STRING_MAP_SCHEMA = { type: "object", additionalProperties: { type: "string" } };
|
|
173
|
+
var STRING_LIST_SCHEMA = { type: "array", items: { type: "string" } };
|
|
30
174
|
var TRANSPORT_SCHEMA = {
|
|
31
175
|
type: "object",
|
|
32
176
|
required: ["type"],
|
|
33
177
|
additionalProperties: false,
|
|
178
|
+
oneOf: [
|
|
179
|
+
{
|
|
180
|
+
properties: { type: { const: "stdio" }, command: {}, args: {}, env: {} },
|
|
181
|
+
required: ["type", "command"],
|
|
182
|
+
additionalProperties: false
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
properties: { type: { enum: ["http", "sse"] }, url: {}, headers: {} },
|
|
186
|
+
required: ["type", "url"],
|
|
187
|
+
additionalProperties: false
|
|
188
|
+
}
|
|
189
|
+
],
|
|
34
190
|
properties: {
|
|
35
191
|
type: { enum: ["stdio", "http", "sse"] },
|
|
36
192
|
command: { type: "string" },
|
|
37
|
-
args:
|
|
193
|
+
args: STRING_LIST_SCHEMA,
|
|
38
194
|
env: STRING_MAP_SCHEMA,
|
|
39
195
|
url: { type: "string" },
|
|
40
196
|
headers: STRING_MAP_SCHEMA
|
|
41
197
|
}
|
|
42
198
|
};
|
|
43
|
-
var
|
|
199
|
+
var SETUP_SCHEMA = {
|
|
44
200
|
type: "object",
|
|
45
|
-
required: ["
|
|
201
|
+
required: ["kind"],
|
|
46
202
|
additionalProperties: false,
|
|
47
203
|
oneOf: [
|
|
48
204
|
{
|
|
49
|
-
properties: {
|
|
50
|
-
required: ["
|
|
205
|
+
properties: { kind: { const: "ready" }, name: {}, transport: {}, needs: {}, optional: {} },
|
|
206
|
+
required: ["kind", "name", "transport", "needs", "optional"],
|
|
51
207
|
additionalProperties: false
|
|
52
208
|
},
|
|
53
209
|
{
|
|
54
|
-
properties: {
|
|
55
|
-
required: ["
|
|
210
|
+
properties: { kind: { const: "manual" } },
|
|
211
|
+
required: ["kind"],
|
|
56
212
|
additionalProperties: false
|
|
57
213
|
}
|
|
58
214
|
],
|
|
59
215
|
properties: {
|
|
60
|
-
|
|
216
|
+
kind: { enum: ["ready", "manual"] },
|
|
217
|
+
name: { type: "string", pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" },
|
|
218
|
+
transport: TRANSPORT_SCHEMA,
|
|
219
|
+
needs: STRING_LIST_SCHEMA,
|
|
220
|
+
optional: STRING_LIST_SCHEMA
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var SEARCH_RESULT_SCHEMA = {
|
|
224
|
+
type: "object",
|
|
225
|
+
required: ["status", "stale", "items"],
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
properties: {
|
|
228
|
+
status: { const: "ok" },
|
|
61
229
|
stale: { type: "boolean" },
|
|
62
230
|
items: {
|
|
63
231
|
type: "array",
|
|
64
232
|
items: {
|
|
65
233
|
type: "object",
|
|
66
|
-
required: ["
|
|
234
|
+
required: ["id", "title", "publisher", "description", "version", "setup"],
|
|
67
235
|
additionalProperties: false,
|
|
68
236
|
properties: {
|
|
69
|
-
|
|
70
|
-
|
|
237
|
+
id: { type: "string" },
|
|
238
|
+
title: { type: "string" },
|
|
239
|
+
publisher: { type: "string" },
|
|
71
240
|
description: { type: "string" },
|
|
72
|
-
|
|
73
|
-
|
|
241
|
+
version: { type: "string" },
|
|
242
|
+
homepage: { type: "string" },
|
|
243
|
+
setup: SETUP_SCHEMA
|
|
74
244
|
}
|
|
75
245
|
}
|
|
76
246
|
}
|
|
77
247
|
}
|
|
78
248
|
};
|
|
79
|
-
var
|
|
80
|
-
var isStringRecord = (value) => isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
81
|
-
var isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
82
|
-
var parseTransport = (value) => {
|
|
83
|
-
if (!isRecord(value)) return null;
|
|
84
|
-
const type = value["type"];
|
|
85
|
-
if (type === "stdio") {
|
|
86
|
-
const command = value["command"];
|
|
87
|
-
if (typeof command !== "string" || command.length === 0) return null;
|
|
88
|
-
const args = value["args"];
|
|
89
|
-
const env = value["env"];
|
|
90
|
-
if (args !== void 0 && !isStringArray(args)) return null;
|
|
91
|
-
if (env !== void 0 && !isStringRecord(env)) return null;
|
|
92
|
-
return {
|
|
93
|
-
type: "stdio",
|
|
94
|
-
command,
|
|
95
|
-
...args === void 0 ? {} : { args },
|
|
96
|
-
...env === void 0 ? {} : { env }
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
if (type === "http" || type === "sse") {
|
|
100
|
-
const url = value["url"];
|
|
101
|
-
if (typeof url !== "string" || url.length === 0) return null;
|
|
102
|
-
const headers = value["headers"];
|
|
103
|
-
if (headers !== void 0 && !isStringRecord(headers)) return null;
|
|
104
|
-
return { type, url, ...headers === void 0 ? {} : { headers } };
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
};
|
|
108
|
-
var parseItem = (value) => {
|
|
109
|
-
if (!isRecord(value)) return null;
|
|
110
|
-
const catalogId = value["catalogId"];
|
|
111
|
-
const name = value["name"];
|
|
112
|
-
const description = value["description"];
|
|
113
|
-
const oauth = value["oauth"];
|
|
114
|
-
const transport = parseTransport(value["transport"]);
|
|
115
|
-
if (typeof catalogId !== "string" || catalogId.trim().length === 0 || typeof name !== "string" || name.trim().length === 0 || typeof description !== "string" || typeof oauth !== "boolean" || transport === null) {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
return { catalogId, name, description, transport, oauth };
|
|
119
|
-
};
|
|
120
|
-
var parseRegistryItems = (body) => {
|
|
121
|
-
if (!isRecord(body)) return null;
|
|
122
|
-
const items = body["items"];
|
|
123
|
-
if (!Array.isArray(items)) return null;
|
|
124
|
-
const parsed = [];
|
|
125
|
-
for (const raw of items) {
|
|
126
|
-
const item = parseItem(raw);
|
|
127
|
-
if (item === null) return null;
|
|
128
|
-
parsed.push(item);
|
|
129
|
-
}
|
|
130
|
-
return parsed;
|
|
131
|
-
};
|
|
132
|
-
var readUrl = (config) => {
|
|
249
|
+
var registryBase = (config) => {
|
|
133
250
|
const value = config["url"];
|
|
134
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() :
|
|
251
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : DEFAULT_REGISTRY_URL;
|
|
135
252
|
};
|
|
136
253
|
var describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
137
|
-
var
|
|
138
|
-
var CATALOG_CACHE_KEY = "catalog";
|
|
254
|
+
var searchCacheKey = (base, query) => `search:${serversEndpoint(base)}:${query.trim().toLowerCase()}`;
|
|
139
255
|
var search = async (ctx, query) => {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const cached = await ctx.cache.getJson(CATALOG_CACHE_KEY, CACHE_TTL_MS, true);
|
|
145
|
-
const cachedItems = cached === null ? null : parseRegistryItems(cached.value);
|
|
256
|
+
const base = registryBase(ctx.config);
|
|
257
|
+
const key = searchCacheKey(base, query);
|
|
258
|
+
const cached = await ctx.cache.getJson(key, CACHE_TTL_MS, true);
|
|
259
|
+
const cachedItems = cached === null ? null : parseRegistryResponse(cached.value);
|
|
146
260
|
if (cachedItems !== null && cached !== null && !cached.stale) {
|
|
147
|
-
return { status: "ok",
|
|
261
|
+
return { status: "ok", stale: false, items: cachedItems };
|
|
148
262
|
}
|
|
149
263
|
try {
|
|
150
|
-
const response = await ctx.http(
|
|
264
|
+
const response = await ctx.http(searchUrl(base, query, SEARCH_LIMIT), {
|
|
151
265
|
headers: { accept: "application/json" },
|
|
152
266
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
153
267
|
});
|
|
154
268
|
if (!response.ok) {
|
|
155
|
-
throw new Error(`
|
|
269
|
+
throw new Error(`MCP registry responded with HTTP ${response.status}`);
|
|
156
270
|
}
|
|
157
|
-
const
|
|
271
|
+
const body = await response.json();
|
|
272
|
+
const items = parseRegistryResponse(body);
|
|
158
273
|
if (items === null) {
|
|
159
|
-
throw new Error("
|
|
274
|
+
throw new Error("MCP registry response did not match the expected shape");
|
|
160
275
|
}
|
|
161
|
-
await ctx.cache.setJson(
|
|
162
|
-
return { status: "ok",
|
|
276
|
+
await ctx.cache.setJson(key, body);
|
|
277
|
+
return { status: "ok", stale: false, items };
|
|
163
278
|
} catch (error) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (items.length > 0 || BUNDLED_CATALOG.length > 0) {
|
|
170
|
-
return { status: "ok", items: [...items], stale: true };
|
|
279
|
+
if (cachedItems !== null) {
|
|
280
|
+
ctx.log.warn("MCP registry request failed; serving the cached result", {
|
|
281
|
+
detail: describeError(error)
|
|
282
|
+
});
|
|
283
|
+
return { status: "ok", stale: true, items: cachedItems };
|
|
171
284
|
}
|
|
172
285
|
throw error instanceof Error ? error : new Error(describeError(error));
|
|
173
286
|
}
|
|
174
287
|
};
|
|
288
|
+
var queryOf = (payload) => typeof payload["query"] === "string" ? payload["query"] : "";
|
|
289
|
+
var AGENT_TOOL_DESCRIPTION = "Search the official MCP registry for MCP servers. Anyone can list a server there and entries are not reviewed, so tell the user who published one before adding it. Pass a short query; an empty query lists the first entries. Each item has the registry id, title, publisher, description, version and an optional homepage. When setup.kind is ready, setup.name and setup.transport describe a server you can add with Ycodium's MCP registration tool after the user agrees. setup.needs names what the user must supply before it works: required environment variables or headers that are empty or still hold a {template}, and the placeholders still in the arguments (<valueHint> or {template}). setup.optional names secrets the server accepts but does not require; offer them, and leave out any the user does not give rather than setting an empty value. Never invent values. When setup.kind is manual the server cannot be added in one step; point the user to its homepage. stale: true means the registry was unreachable and the result is an earlier cached copy.";
|
|
175
290
|
var connectorRegistryPlugin = definePlugin({
|
|
176
291
|
id: "connector-registry",
|
|
177
|
-
apiVersion:
|
|
292
|
+
apiVersion: 60,
|
|
178
293
|
activate: (ctx) => {
|
|
179
294
|
ctx.command({
|
|
180
295
|
name: "search",
|
|
181
|
-
description: "Search the MCP
|
|
296
|
+
description: "Search the MCP registry configured for this machine.",
|
|
182
297
|
scope: "read",
|
|
183
298
|
payloadSchema: SEARCH_PAYLOAD_SCHEMA,
|
|
184
299
|
resultSchema: SEARCH_RESULT_SCHEMA,
|
|
185
|
-
handle: (payload) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
handle: (payload) => search(ctx, queryOf(payload))
|
|
301
|
+
});
|
|
302
|
+
ctx.toolkit({
|
|
303
|
+
name: "connector-registry",
|
|
304
|
+
tools: [
|
|
305
|
+
{
|
|
306
|
+
name: "search",
|
|
307
|
+
description: AGENT_TOOL_DESCRIPTION,
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
query: { type: "string", description: "Words to search for, such as github." }
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
handle: async (input) => {
|
|
315
|
+
try {
|
|
316
|
+
return { text: JSON.stringify(await search(ctx, queryOf(input))) };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
text: `The MCP registry could not be searched: ${describeError(error)}`,
|
|
320
|
+
isError: true
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
]
|
|
189
326
|
});
|
|
190
327
|
ctx.command({
|
|
191
328
|
name: "effectiveSettings",
|
|
192
|
-
description: "Settings card: what
|
|
329
|
+
description: "Settings card: what the registry search reads when the URL is empty.",
|
|
193
330
|
scope: "read",
|
|
194
331
|
payloadSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
195
|
-
handle: async () => ({
|
|
196
|
-
url: {
|
|
197
|
-
detailKey: BUNDLED_CATALOG.length === 0 ? "settings.url.none" : "settings.url.bundled"
|
|
198
|
-
}
|
|
199
|
-
})
|
|
332
|
+
handle: async () => ({ url: { detailKey: "settings.url.official" } })
|
|
200
333
|
});
|
|
201
334
|
ctx.settingsCard({
|
|
202
335
|
titleKey: "settings.card.title",
|
|
@@ -216,6 +349,5 @@ var connectorRegistryPlugin = definePlugin({
|
|
|
216
349
|
});
|
|
217
350
|
var index_default = connectorRegistryPlugin;
|
|
218
351
|
export {
|
|
219
|
-
CATALOG_CACHE_KEY,
|
|
220
352
|
index_default as default
|
|
221
353
|
};
|
package/locales/en.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"presentation": {
|
|
3
3
|
"name": "Connector catalog",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Lets you pick MCP servers from the official MCP registry when adding a server."
|
|
5
5
|
},
|
|
6
6
|
"settings": {
|
|
7
7
|
"card": {
|
|
8
8
|
"title": "Connector registry",
|
|
9
|
-
"description": "Where
|
|
9
|
+
"description": "Where Add server looks up MCP servers. Defaults to the YCODIUM_MCP_REGISTRY_URL environment variable, then to the official MCP registry."
|
|
10
10
|
},
|
|
11
11
|
"url": {
|
|
12
12
|
"label": "Registry URL",
|
|
13
|
-
"description": "Leave empty to use
|
|
14
|
-
"placeholder": "https://
|
|
15
|
-
"
|
|
16
|
-
"bundled": "Only the catalog bundled with this version"
|
|
13
|
+
"description": "Leave empty to use the official MCP registry. Another registry must offer the same /v0/servers interface.",
|
|
14
|
+
"placeholder": "https://registry.modelcontextprotocol.io",
|
|
15
|
+
"official": "Using the official MCP registry"
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
}
|
package/locales/zh-CN.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"presentation": {
|
|
3
3
|
"name": "连接器目录",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "添加服务器时可以从 MCP 官方清单里挑。"
|
|
5
5
|
},
|
|
6
6
|
"settings": {
|
|
7
7
|
"card": {
|
|
8
8
|
"title": "连接器目录",
|
|
9
|
-
"description": "
|
|
9
|
+
"description": "「添加服务器」从哪里查 MCP 服务器。默认取环境变量 YCODIUM_MCP_REGISTRY_URL,没有则用 MCP 官方清单。"
|
|
10
10
|
},
|
|
11
11
|
"url": {
|
|
12
|
-
"label": "
|
|
13
|
-
"description": "
|
|
14
|
-
"placeholder": "https://
|
|
15
|
-
"
|
|
16
|
-
"bundled": "只用随版本自带的目录"
|
|
12
|
+
"label": "清单地址",
|
|
13
|
+
"description": "留空用 MCP 官方清单。换成别的清单时,它要提供同样的 /v0/servers 接口。",
|
|
14
|
+
"placeholder": "https://registry.modelcontextprotocol.io",
|
|
15
|
+
"official": "正在用 MCP 官方清单"
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycodium-ai/plugin-connector-registry",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.3799",
|
|
4
|
+
"description": "Lets you pick MCP servers from the official MCP registry when adding a server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"connector",
|
|
7
7
|
"mcp",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"@effect/vitest": "4.0.0-beta.103",
|
|
23
23
|
"esbuild": "0.28.1",
|
|
24
24
|
"vite-plus": "0.2.2",
|
|
25
|
-
"@ycodium-ai/plugin-api": "0.2.
|
|
25
|
+
"@ycodium-ai/plugin-api": "0.2.3799"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|
|
28
28
|
"plugin": "./dist/index.js",
|
|
29
29
|
"displayName": "Connector catalog",
|
|
30
30
|
"displayNameKey": "presentation.name",
|
|
31
|
-
"description": "
|
|
31
|
+
"description": "Lets you pick MCP servers from the official MCP registry when adding a server.",
|
|
32
32
|
"descriptionKey": "presentation.description",
|
|
33
33
|
"icon": "./assets/icon.svg",
|
|
34
34
|
"permissions": []
|