@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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/dist/index.js +209 -0
  3. 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
+ }