@ycodium-ai/plugin-connector-registry 0.2.0
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 +38 -0
- package/dist/index.js +209 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @ycodium-ai/plugin-connector-registry
|
|
2
|
+
|
|
3
|
+
Searches the MCP connector directory shown under Settings → Plugins → Directory.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The plugin exposes a `search` command. It merges a catalog shipped with the
|
|
8
|
+
package with a remote catalog, then filters locally. The remote URL comes from
|
|
9
|
+
the entry config (`url`), which the host fills from `YCODIUM_MCP_REGISTRY_URL`
|
|
10
|
+
and which the settings card can override.
|
|
11
|
+
|
|
12
|
+
If no URL is configured and the shipped catalog is empty, `search` returns
|
|
13
|
+
`{ status: "not-configured" }`. A fetch failure falls back to cache, then to
|
|
14
|
+
the shipped copy, and only fails when there is nothing to show.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
ycodium plugin add @ycodium-ai/plugin-connector-registry
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This plugin declares no extra permissions. Outbound fetches use the host HTTP
|
|
23
|
+
client, including the machine proxy.
|
|
24
|
+
|
|
25
|
+
Ycodium ships it enabled as `core.connector-registry`. Disabling it hides the
|
|
26
|
+
directory search. Uninstalling it writes a removal to the home patch so a
|
|
27
|
+
restart does not bring it back. Reinstalling the unversioned bundled package
|
|
28
|
+
restores the same entry without an npm fetch.
|
|
29
|
+
|
|
30
|
+
## Developing
|
|
31
|
+
|
|
32
|
+
Built from the Ycodium repository. The Node entry is bundled, but
|
|
33
|
+
`@ycodium-ai/plugin-api` stays external and is provided by the Ycodium host at
|
|
34
|
+
load time.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
UNLICENSED.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/catalog.ts
|
|
5
|
+
var BUNDLED_CATALOG = [];
|
|
6
|
+
var mergeCatalog = (bundled, fetched) => {
|
|
7
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const entry of bundled) byId.set(entry.catalogId, entry);
|
|
9
|
+
for (const entry of fetched) byId.set(entry.catalogId, entry);
|
|
10
|
+
return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
11
|
+
};
|
|
12
|
+
var filterCatalog = (entries, query) => {
|
|
13
|
+
const needle = query.trim().toLowerCase();
|
|
14
|
+
if (needle.length === 0) return entries;
|
|
15
|
+
return entries.filter(
|
|
16
|
+
(entry) => entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle)
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
22
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
23
|
+
var SEARCH_PAYLOAD_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: { query: { type: "string" } },
|
|
26
|
+
required: ["query"],
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
};
|
|
29
|
+
var STRING_MAP_SCHEMA = { type: "object", additionalProperties: { type: "string" } };
|
|
30
|
+
var TRANSPORT_SCHEMA = {
|
|
31
|
+
type: "object",
|
|
32
|
+
required: ["type"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties: {
|
|
35
|
+
type: { enum: ["stdio", "http", "sse"] },
|
|
36
|
+
command: { type: "string" },
|
|
37
|
+
args: { type: "array", items: { type: "string" } },
|
|
38
|
+
env: STRING_MAP_SCHEMA,
|
|
39
|
+
url: { type: "string" },
|
|
40
|
+
headers: STRING_MAP_SCHEMA
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var SEARCH_RESULT_SCHEMA = {
|
|
44
|
+
type: "object",
|
|
45
|
+
required: ["status"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
oneOf: [
|
|
48
|
+
{
|
|
49
|
+
properties: { status: { const: "not-configured" } },
|
|
50
|
+
required: ["status"],
|
|
51
|
+
additionalProperties: false
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
properties: { status: { const: "ok" }, items: {}, stale: {} },
|
|
55
|
+
required: ["status", "items", "stale"],
|
|
56
|
+
additionalProperties: false
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
properties: {
|
|
60
|
+
status: { enum: ["not-configured", "ok"] },
|
|
61
|
+
stale: { type: "boolean" },
|
|
62
|
+
items: {
|
|
63
|
+
type: "array",
|
|
64
|
+
items: {
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["catalogId", "name", "description", "transport", "oauth"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
properties: {
|
|
69
|
+
catalogId: { type: "string" },
|
|
70
|
+
name: { type: "string" },
|
|
71
|
+
description: { type: "string" },
|
|
72
|
+
transport: TRANSPORT_SCHEMA,
|
|
73
|
+
oauth: { type: "boolean" }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
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) => {
|
|
133
|
+
const value = config["url"];
|
|
134
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
135
|
+
};
|
|
136
|
+
var describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
137
|
+
var resolve = (fetched, query) => filterCatalog(mergeCatalog(BUNDLED_CATALOG, fetched), query);
|
|
138
|
+
var CATALOG_CACHE_KEY = "catalog";
|
|
139
|
+
var search = async (ctx, query) => {
|
|
140
|
+
const url = readUrl(ctx.config);
|
|
141
|
+
if (url === null) {
|
|
142
|
+
return BUNDLED_CATALOG.length === 0 ? { status: "not-configured" } : { status: "ok", items: [...filterCatalog(BUNDLED_CATALOG, query)], stale: false };
|
|
143
|
+
}
|
|
144
|
+
const cached = await ctx.cache.getJson(CATALOG_CACHE_KEY, CACHE_TTL_MS, true);
|
|
145
|
+
const cachedItems = cached === null ? null : parseRegistryItems(cached.value);
|
|
146
|
+
if (cachedItems !== null && cached !== null && !cached.stale) {
|
|
147
|
+
return { status: "ok", items: [...resolve(cachedItems, query)], stale: false };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const response = await ctx.http(url, {
|
|
151
|
+
headers: { accept: "application/json" },
|
|
152
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`connector registry responded with HTTP ${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
const items = parseRegistryItems(await response.json());
|
|
158
|
+
if (items === null) {
|
|
159
|
+
throw new Error("connector registry response did not match the expected shape");
|
|
160
|
+
}
|
|
161
|
+
await ctx.cache.setJson(CATALOG_CACHE_KEY, { items });
|
|
162
|
+
return { status: "ok", items: [...resolve(items, query)], stale: false };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const fallback = cachedItems ?? [];
|
|
165
|
+
ctx.log.warn("connector registry request failed; serving what is already on hand", {
|
|
166
|
+
detail: describeError(error)
|
|
167
|
+
});
|
|
168
|
+
const items = resolve(fallback, query);
|
|
169
|
+
if (items.length > 0 || BUNDLED_CATALOG.length > 0) {
|
|
170
|
+
return { status: "ok", items: [...items], stale: true };
|
|
171
|
+
}
|
|
172
|
+
throw error instanceof Error ? error : new Error(describeError(error));
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var connectorRegistryPlugin = definePlugin({
|
|
176
|
+
id: "connector-registry",
|
|
177
|
+
apiVersion: 2,
|
|
178
|
+
activate: (ctx) => {
|
|
179
|
+
ctx.command({
|
|
180
|
+
name: "search",
|
|
181
|
+
description: "Search the MCP connector catalog configured for this environment.",
|
|
182
|
+
scope: "read",
|
|
183
|
+
payloadSchema: SEARCH_PAYLOAD_SCHEMA,
|
|
184
|
+
resultSchema: SEARCH_RESULT_SCHEMA,
|
|
185
|
+
handle: (payload) => {
|
|
186
|
+
const query = typeof payload["query"] === "string" ? payload["query"] : "";
|
|
187
|
+
return search(ctx, query);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
ctx.settingsCard({
|
|
191
|
+
title: "Connector registry",
|
|
192
|
+
description: "Directory searched by Settings \u2192 Plugins \u2192 Connectors. Defaults to the YCODIUM_MCP_REGISTRY_URL environment variable.",
|
|
193
|
+
fields: [
|
|
194
|
+
{
|
|
195
|
+
key: "url",
|
|
196
|
+
kind: "text",
|
|
197
|
+
label: "Registry URL",
|
|
198
|
+
description: "Leave empty to fall back to the environment variable, if one is set.",
|
|
199
|
+
placeholder: "https://example.com/connectors/search"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
var index_default = connectorRegistryPlugin;
|
|
206
|
+
export {
|
|
207
|
+
CATALOG_CACHE_KEY,
|
|
208
|
+
index_default as default
|
|
209
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ycodium-ai/plugin-connector-registry",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Searches the directory behind Settings → Plugins → Directory.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"connector",
|
|
7
|
+
"mcp",
|
|
8
|
+
"ycodium",
|
|
9
|
+
"ycodium-plugin"
|
|
10
|
+
],
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@effect/vitest": "4.0.0-beta.103",
|
|
21
|
+
"esbuild": "0.28.1",
|
|
22
|
+
"vite-plus": "0.2.2",
|
|
23
|
+
"@ycodium-ai/plugin-api": "0.1.1"
|
|
24
|
+
},
|
|
25
|
+
"ycodium": {
|
|
26
|
+
"plugin": "./dist/index.js",
|
|
27
|
+
"displayName": "Connector directory",
|
|
28
|
+
"description": "Searches the directory behind Settings → Plugins → Directory.",
|
|
29
|
+
"icon": "blocks",
|
|
30
|
+
"permissions": []
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node build.mjs",
|
|
34
|
+
"typecheck": "tsgo --noEmit",
|
|
35
|
+
"test": "vp test run"
|
|
36
|
+
}
|
|
37
|
+
}
|