@theia/ai-registry 1.73.0-next.2
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 +40 -0
- package/lib/browser/ai-registry-frontend-module.d.ts +5 -0
- package/lib/browser/ai-registry-frontend-module.d.ts.map +1 -0
- package/lib/browser/ai-registry-frontend-module.js +45 -0
- package/lib/browser/ai-registry-frontend-module.js.map +1 -0
- package/lib/browser/ai-registry-toolbar-contribution.d.ts +9 -0
- package/lib/browser/ai-registry-toolbar-contribution.d.ts.map +1 -0
- package/lib/browser/ai-registry-toolbar-contribution.js +53 -0
- package/lib/browser/ai-registry-toolbar-contribution.js.map +1 -0
- package/lib/browser/mcp/mcp-entries.d.ts +40 -0
- package/lib/browser/mcp/mcp-entries.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-entries.js +143 -0
- package/lib/browser/mcp/mcp-entries.js.map +1 -0
- package/lib/browser/mcp/mcp-extensions-contribution.d.ts +45 -0
- package/lib/browser/mcp/mcp-extensions-contribution.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-extensions-contribution.js +198 -0
- package/lib/browser/mcp/mcp-extensions-contribution.js.map +1 -0
- package/lib/browser/mcp/mcp-extensions-contribution.spec.d.ts +2 -0
- package/lib/browser/mcp/mcp-extensions-contribution.spec.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-extensions-contribution.spec.js +266 -0
- package/lib/browser/mcp/mcp-extensions-contribution.spec.js.map +1 -0
- package/lib/browser/mcp/mcp-install-service.d.ts +72 -0
- package/lib/browser/mcp/mcp-install-service.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-install-service.js +255 -0
- package/lib/browser/mcp/mcp-install-service.js.map +1 -0
- package/lib/browser/mcp/mcp-install-service.spec.d.ts +2 -0
- package/lib/browser/mcp/mcp-install-service.spec.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-install-service.spec.js +604 -0
- package/lib/browser/mcp/mcp-install-service.spec.js.map +1 -0
- package/lib/browser/mcp/mcp-registry-ui-bridge-impl.d.ts +27 -0
- package/lib/browser/mcp/mcp-registry-ui-bridge-impl.d.ts.map +1 -0
- package/lib/browser/mcp/mcp-registry-ui-bridge-impl.js +136 -0
- package/lib/browser/mcp/mcp-registry-ui-bridge-impl.js.map +1 -0
- package/lib/common/ai-registry-configuration.d.ts +28 -0
- package/lib/common/ai-registry-configuration.d.ts.map +1 -0
- package/lib/common/ai-registry-configuration.js +56 -0
- package/lib/common/ai-registry-configuration.js.map +1 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.d.ts +12 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.d.ts.map +1 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.js +68 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.js.map +1 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.spec.d.ts +2 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.spec.d.ts.map +1 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.spec.js +230 -0
- package/lib/common/mcp/mcp-registry-entry-resolver.spec.js.map +1 -0
- package/lib/common/mcp/mcp-registry-types.d.ts +105 -0
- package/lib/common/mcp/mcp-registry-types.d.ts.map +1 -0
- package/lib/common/mcp/mcp-registry-types.js +18 -0
- package/lib/common/mcp/mcp-registry-types.js.map +1 -0
- package/lib/common/registry-fetch-service.d.ts +24 -0
- package/lib/common/registry-fetch-service.d.ts.map +1 -0
- package/lib/common/registry-fetch-service.js +72 -0
- package/lib/common/registry-fetch-service.js.map +1 -0
- package/lib/common/registry-fetch-service.spec.d.ts +2 -0
- package/lib/common/registry-fetch-service.spec.d.ts.map +1 -0
- package/lib/common/registry-fetch-service.spec.js +129 -0
- package/lib/common/registry-fetch-service.spec.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +26 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +50 -0
- package/src/browser/ai-registry-frontend-module.ts +48 -0
- package/src/browser/ai-registry-toolbar-contribution.ts +51 -0
- package/src/browser/mcp/mcp-entries.tsx +288 -0
- package/src/browser/mcp/mcp-extensions-contribution.spec.ts +294 -0
- package/src/browser/mcp/mcp-extensions-contribution.ts +199 -0
- package/src/browser/mcp/mcp-install-service.spec.ts +673 -0
- package/src/browser/mcp/mcp-install-service.ts +300 -0
- package/src/browser/mcp/mcp-registry-ui-bridge-impl.ts +130 -0
- package/src/browser/style/mcp-entries.css +56 -0
- package/src/common/ai-registry-configuration.ts +52 -0
- package/src/common/mcp/mcp-registry-entry-resolver.spec.ts +248 -0
- package/src/common/mcp/mcp-registry-entry-resolver.ts +68 -0
- package/src/common/mcp/mcp-registry-types.ts +119 -0
- package/src/common/registry-fetch-service.spec.ts +136 -0
- package/src/common/registry-fetch-service.ts +78 -0
- package/src/package.spec.ts +28 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { PreferenceScope, PreferenceService } from '@theia/core';
|
|
19
|
+
import {
|
|
20
|
+
isLocalMCPServerDescription,
|
|
21
|
+
isRemoteMCPServerDescription,
|
|
22
|
+
MCPInstallEntryConfig,
|
|
23
|
+
MCPRegistryMetadata,
|
|
24
|
+
MCPServerDescription
|
|
25
|
+
} from '@theia/ai-mcp/lib/common/mcp-server-manager';
|
|
26
|
+
import { MCP_SERVERS_PREF } from '@theia/ai-mcp/lib/common/mcp-preferences';
|
|
27
|
+
import { MCPInstallOverrides, MCPServerEditor } from '@theia/ai-mcp/lib/browser/mcp-server-editor';
|
|
28
|
+
import { ClassificationResult, ResolvedRegistryEntry } from '../../common/mcp/mcp-registry-types';
|
|
29
|
+
|
|
30
|
+
export { MCPInstallOverrides };
|
|
31
|
+
|
|
32
|
+
type StoredEntry = MCPInstallEntryConfig & {
|
|
33
|
+
autostart?: boolean;
|
|
34
|
+
registryMetadata?: MCPRegistryMetadata;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type StoredServers = Record<string, StoredEntry>;
|
|
38
|
+
|
|
39
|
+
export const MCPInstallService = Symbol('MCPInstallService');
|
|
40
|
+
export interface MCPInstallService {
|
|
41
|
+
/** Installs a registry entry, applying any user-supplied overrides collected by the install dialog. */
|
|
42
|
+
install(entry: ResolvedRegistryEntry, overrides?: MCPInstallOverrides): Promise<void>;
|
|
43
|
+
/** Restores a drifted entry's registry-owned config fields while preserving user-owned ones. */
|
|
44
|
+
fixConfig(entry: ResolvedRegistryEntry): Promise<void>;
|
|
45
|
+
/** Applies a newer registry approval to an installed entry, preserving user-supplied additions. */
|
|
46
|
+
update(entry: ResolvedRegistryEntry): Promise<void>;
|
|
47
|
+
/** Links an existing local server to a registry entry by stamping its registry metadata. */
|
|
48
|
+
link(entry: ResolvedRegistryEntry): Promise<void>;
|
|
49
|
+
/** Drops the registry link from a local server while keeping its config intact. */
|
|
50
|
+
unlink(name: string): Promise<void>;
|
|
51
|
+
/** Removes an installed server entry by its local preference key. */
|
|
52
|
+
uninstall(name: string): Promise<void>;
|
|
53
|
+
/** Classifies a locally stored server against the registry (for the Installed view). */
|
|
54
|
+
classifyLocalServer(local: MCPServerDescription, registryEntries: ResolvedRegistryEntry[]): ClassificationResult;
|
|
55
|
+
/** Classifies a registry entry against the locally stored servers (for the Search view). */
|
|
56
|
+
classifyRegistryEntry(entry: ResolvedRegistryEntry, locals: MCPServerDescription[], registryEntries: ResolvedRegistryEntry[]): ClassificationResult;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@injectable()
|
|
60
|
+
export class MCPInstallServiceImpl implements MCPInstallService {
|
|
61
|
+
|
|
62
|
+
@inject(PreferenceService)
|
|
63
|
+
protected readonly preferenceService: PreferenceService;
|
|
64
|
+
|
|
65
|
+
@inject(MCPServerEditor)
|
|
66
|
+
protected readonly editor: MCPServerEditor;
|
|
67
|
+
|
|
68
|
+
/** Delegates to the generic editor so both registry installs and `install-mcp` URL handlers go through the same code path. */
|
|
69
|
+
async install(entry: ResolvedRegistryEntry, overrides?: MCPInstallOverrides): Promise<void> {
|
|
70
|
+
await this.editor.installFromEntry(entry, overrides);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async fixConfig(entry: ResolvedRegistryEntry): Promise<void> {
|
|
74
|
+
// Overwrite the registry-owned config fields, but carry forward user-owned ones
|
|
75
|
+
// (today: autostart). The registry has no opinion on `autostart`, so wiping it on
|
|
76
|
+
// fix-config would silently discard a user preference. Once the registry grows a
|
|
77
|
+
// formal notion of "user-configurable parameters", extend the forwarded set here.
|
|
78
|
+
const existing = this.readServers()[entry.localName];
|
|
79
|
+
const overrides: MCPInstallOverrides = {};
|
|
80
|
+
if (existing?.autostart !== undefined) {
|
|
81
|
+
overrides.autostart = existing.autostart;
|
|
82
|
+
}
|
|
83
|
+
await this.install(entry, overrides);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async update(entry: ResolvedRegistryEntry): Promise<void> {
|
|
87
|
+
const current = this.readServers();
|
|
88
|
+
const existing = current[entry.localName];
|
|
89
|
+
if (!existing) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// If the registry switched the entry's transport (e.g. local stdio -> remote URL),
|
|
93
|
+
// drop the previous side's fields so settings.json doesn't end up carrying both.
|
|
94
|
+
const sanitized: StoredEntry = { ...existing };
|
|
95
|
+
if (entry.config.serverUrl !== undefined) {
|
|
96
|
+
delete sanitized.command;
|
|
97
|
+
delete sanitized.args;
|
|
98
|
+
delete sanitized.env;
|
|
99
|
+
} else if (entry.config.command !== undefined) {
|
|
100
|
+
delete sanitized.serverUrl;
|
|
101
|
+
delete sanitized.serverAuthToken;
|
|
102
|
+
delete sanitized.serverAuthTokenHeader;
|
|
103
|
+
delete sanitized.headers;
|
|
104
|
+
}
|
|
105
|
+
// Preserve user-added env keys; registry values win for keys the registry sets.
|
|
106
|
+
// Asymmetry: keys the registry previously set and has since dropped are also
|
|
107
|
+
// preserved here, because we cannot distinguish them from user-added keys without
|
|
108
|
+
// tracking provenance. A registry approval that published e.g. `LOG_LEVEL` in v1
|
|
109
|
+
// and removes it in v2 will leave the stale key in the local entry. This will be
|
|
110
|
+
// addressed alongside the planned "user-configurable parameters" work, which
|
|
111
|
+
// introduces an explicit registry-set vs. user-set distinction.
|
|
112
|
+
const mergedEnv = (entry.config.env || sanitized.env)
|
|
113
|
+
? { ...sanitized.env, ...(entry.config.env ?? {}) }
|
|
114
|
+
: undefined;
|
|
115
|
+
// Preserve user-supplied additions across updates. Today we only carry the auth
|
|
116
|
+
// token forward - registries should not ship secrets, so the new approval will
|
|
117
|
+
// either omit `serverAuthToken` entirely or carry it as an empty slot, both of
|
|
118
|
+
// which would otherwise wipe a token the user previously entered in the install
|
|
119
|
+
// dialog. A broader policy for user-additions belongs with the planned parameter
|
|
120
|
+
// configuration work.
|
|
121
|
+
const userAdditions = sanitized.serverAuthToken !== undefined
|
|
122
|
+
? { serverAuthToken: sanitized.serverAuthToken }
|
|
123
|
+
: {};
|
|
124
|
+
const updated: StoredEntry = {
|
|
125
|
+
...sanitized,
|
|
126
|
+
...entry.config,
|
|
127
|
+
...userAdditions,
|
|
128
|
+
...(mergedEnv && { env: mergedEnv }),
|
|
129
|
+
registryMetadata: this.metadata(entry)
|
|
130
|
+
};
|
|
131
|
+
await this.writeServers({ ...current, [entry.localName]: updated });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async link(entry: ResolvedRegistryEntry): Promise<void> {
|
|
135
|
+
const current = this.readServers();
|
|
136
|
+
const existing = current[entry.localName];
|
|
137
|
+
if (!existing) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await this.writeServers({
|
|
141
|
+
...current,
|
|
142
|
+
[entry.localName]: { ...existing, registryMetadata: this.metadata(entry) }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Drop the registry link from a local server while keeping its config intact.
|
|
148
|
+
* Used to convert a stale-linked entry (registry no longer lists the serverId)
|
|
149
|
+
* into a plain user-added entry without losing the user's running server config.
|
|
150
|
+
*/
|
|
151
|
+
async unlink(name: string): Promise<void> {
|
|
152
|
+
const current = this.readServers();
|
|
153
|
+
const existing = current[name];
|
|
154
|
+
if (!existing || existing.registryMetadata === undefined) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const next: StoredEntry = { ...existing };
|
|
158
|
+
delete next.registryMetadata;
|
|
159
|
+
await this.writeServers({ ...current, [name]: next });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async uninstall(name: string): Promise<void> {
|
|
163
|
+
const current = this.readServers();
|
|
164
|
+
if (!(name in current)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const next: StoredServers = { ...current };
|
|
168
|
+
delete next[name];
|
|
169
|
+
await this.writeServers(next);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected readServers(): StoredServers {
|
|
173
|
+
return this.preferenceService.get<StoredServers>(MCP_SERVERS_PREF, {}) ?? {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected async writeServers(next: StoredServers): Promise<void> {
|
|
177
|
+
await this.preferenceService.set(MCP_SERVERS_PREF, next, PreferenceScope.User);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Registry-managed metadata block written onto every linked server entry. */
|
|
181
|
+
protected metadata(entry: ResolvedRegistryEntry): MCPRegistryMetadata {
|
|
182
|
+
return {
|
|
183
|
+
serverId: entry.serverId,
|
|
184
|
+
...(entry.version !== undefined && { version: entry.version }),
|
|
185
|
+
...(entry.configHash !== undefined && { configHash: entry.configHash })
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
classifyLocalServer(local: MCPServerDescription, registryEntries: ResolvedRegistryEntry[]): ClassificationResult {
|
|
190
|
+
const linkedId = local.registryMetadata?.serverId;
|
|
191
|
+
if (linkedId) {
|
|
192
|
+
const byServerId = registryEntries.find(e => e.serverId === linkedId);
|
|
193
|
+
if (!byServerId) {
|
|
194
|
+
return { kind: 'installed-link-stale' };
|
|
195
|
+
}
|
|
196
|
+
return this.classifyLinked(byServerId, local);
|
|
197
|
+
}
|
|
198
|
+
const matchingEntry = registryEntries.find(e => e.localName === local.name);
|
|
199
|
+
if (!matchingEntry) {
|
|
200
|
+
return { kind: 'installed-user-added' };
|
|
201
|
+
}
|
|
202
|
+
// Unlinked + same key: always offer Link, regardless of config drift. Drift is only
|
|
203
|
+
// considered actionable (fix-config) once the user has opted in by linking.
|
|
204
|
+
return { kind: 'installed-manually' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
classifyRegistryEntry(
|
|
208
|
+
entry: ResolvedRegistryEntry,
|
|
209
|
+
locals: MCPServerDescription[],
|
|
210
|
+
registryEntries: ResolvedRegistryEntry[]
|
|
211
|
+
): ClassificationResult {
|
|
212
|
+
const local = locals.find(l => l.name === entry.localName);
|
|
213
|
+
if (!local) {
|
|
214
|
+
return { kind: 'not-installed' };
|
|
215
|
+
}
|
|
216
|
+
const linkedId = local.registryMetadata?.serverId;
|
|
217
|
+
if (linkedId === entry.serverId) {
|
|
218
|
+
return this.classifyLinked(entry, local);
|
|
219
|
+
}
|
|
220
|
+
// Local links to a registry id that doesn't exist in the registry - the link is
|
|
221
|
+
// stale. Surface the same state Installed shows so the user sees the Unlink and
|
|
222
|
+
// Uninstall affordances in both views instead of a misleading Link button.
|
|
223
|
+
if (linkedId && !registryEntries.some(e => e.serverId === linkedId)) {
|
|
224
|
+
return { kind: 'installed-link-stale' };
|
|
225
|
+
}
|
|
226
|
+
// Same key but not linked (no registryMetadata, or pointing to a different valid
|
|
227
|
+
// id): offer Link before surfacing any drift - drift handling is a post-link concern.
|
|
228
|
+
return { kind: 'installed-manually' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Classify a local server that is already linked to a registry entry. Either the
|
|
233
|
+
* registry config matches (eligible for Update when the registry has published a new
|
|
234
|
+
* approval - detected via `configHash`) or it has drifted away (`fix-config`).
|
|
235
|
+
*
|
|
236
|
+
* Update detection uses `registryMetadata.configHash` exclusively.
|
|
237
|
+
* `registryMetadata.version` is kept on the local entry for display only; the
|
|
238
|
+
* registry may publish a new version without changing the install config, in which
|
|
239
|
+
* case we still want to offer Update.
|
|
240
|
+
*/
|
|
241
|
+
protected classifyLinked(entry: ResolvedRegistryEntry, local: MCPServerDescription): ClassificationResult {
|
|
242
|
+
if (!this.matchesByConfig(entry, local)) {
|
|
243
|
+
return { kind: 'fix-config' };
|
|
244
|
+
}
|
|
245
|
+
const updateAvailable = this.isUpdateAvailable(entry, local);
|
|
246
|
+
return { kind: 'installed-from-registry', updateAvailable };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* True when the registry's `configHash` differs from the locally stored
|
|
251
|
+
* `registryMetadata.configHash`. Returns false when the registry has no `configHash`
|
|
252
|
+
* to compare against (older payloads) - without a hash we cannot make a confident
|
|
253
|
+
* "update available" decision and prefer not to nag the user.
|
|
254
|
+
*/
|
|
255
|
+
protected isUpdateAvailable(entry: ResolvedRegistryEntry, local: MCPServerDescription): boolean {
|
|
256
|
+
if (entry.configHash === undefined) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return local.registryMetadata?.configHash !== entry.configHash;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected matchesByConfig(entry: ResolvedRegistryEntry, local: MCPServerDescription): boolean {
|
|
263
|
+
if (entry.config.command !== undefined) {
|
|
264
|
+
if (!isLocalMCPServerDescription(local)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (local.command !== entry.config.command) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const entryArgs = entry.config.args ?? [];
|
|
271
|
+
const localArgs = local.args ?? [];
|
|
272
|
+
if (entryArgs.length !== localArgs.length) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (!entryArgs.every((value, index) => value === localArgs[index])) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return this.envMatches(entry.config.env, local.env);
|
|
279
|
+
}
|
|
280
|
+
if (entry.config.serverUrl !== undefined) {
|
|
281
|
+
if (!isRemoteMCPServerDescription(local)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
return local.serverUrl === entry.config.serverUrl;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected envMatches(registryEnv: Record<string, string> | undefined, localEnv: Record<string, string> | undefined): boolean {
|
|
290
|
+
if (!registryEnv) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
for (const key of Object.keys(registryEnv)) {
|
|
294
|
+
if (localEnv?.[key] !== registryEnv[key]) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { Emitter, Event } from '@theia/core';
|
|
18
|
+
import { ApplicationShell, WidgetManager } from '@theia/core/lib/browser';
|
|
19
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
20
|
+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
21
|
+
import { MCPRegistryUiBridge } from '@theia/ai-mcp/lib/browser/mcp-registry-ui-bridge';
|
|
22
|
+
import { MCPInstallEntry } from '@theia/ai-mcp/lib/browser/mcp-server-editor';
|
|
23
|
+
import { VSXExtensionsViewContainer } from '@theia/vsx-registry/lib/browser/vsx-extensions-view-container';
|
|
24
|
+
import { VSXExtensionsSearchModel } from '@theia/vsx-registry/lib/browser/vsx-extensions-search-model';
|
|
25
|
+
import { ResolvedRegistryEntry } from '../../common/mcp/mcp-registry-types';
|
|
26
|
+
import { RegistryFetchService } from '../../common/registry-fetch-service';
|
|
27
|
+
|
|
28
|
+
@injectable()
|
|
29
|
+
export class MCPRegistryUiBridgeImpl implements MCPRegistryUiBridge {
|
|
30
|
+
|
|
31
|
+
@inject(RegistryFetchService)
|
|
32
|
+
protected readonly fetchService: RegistryFetchService;
|
|
33
|
+
|
|
34
|
+
@inject(WidgetManager)
|
|
35
|
+
protected readonly widgetManager: WidgetManager;
|
|
36
|
+
|
|
37
|
+
@inject(ApplicationShell)
|
|
38
|
+
protected readonly shell: ApplicationShell;
|
|
39
|
+
|
|
40
|
+
@inject(VSXExtensionsSearchModel)
|
|
41
|
+
protected readonly searchModel: VSXExtensionsSearchModel;
|
|
42
|
+
|
|
43
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
44
|
+
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
|
45
|
+
|
|
46
|
+
protected knownServerIds = new Set<string>();
|
|
47
|
+
protected entriesByServerId = new Map<string, ResolvedRegistryEntry>();
|
|
48
|
+
|
|
49
|
+
/** Resolves after `refreshCache` has run for the first time, success or failure. */
|
|
50
|
+
protected readonly readyDeferred = new Deferred<void>();
|
|
51
|
+
|
|
52
|
+
@postConstruct()
|
|
53
|
+
protected init(): void {
|
|
54
|
+
// Re-sync our id cache whenever the registry data changes (initial load, refresh).
|
|
55
|
+
this.fetchService.onDidChange(() => this.refreshCache());
|
|
56
|
+
// Prime the cache eagerly so the very first `hasServer` check after the UI mounts
|
|
57
|
+
// returns the right answer instead of erring on the side of "still unknown".
|
|
58
|
+
this.refreshCache();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ready(): Promise<void> {
|
|
62
|
+
return this.readyDeferred.promise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
hasServer(serverId: string): boolean {
|
|
66
|
+
return this.knownServerIds.has(serverId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getInstallEntry(serverId: string): MCPInstallEntry | undefined {
|
|
70
|
+
const entry = this.entriesByServerId.get(serverId);
|
|
71
|
+
if (!entry) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
localName: entry.localName,
|
|
76
|
+
config: { ...entry.config },
|
|
77
|
+
serverId: entry.serverId,
|
|
78
|
+
...(entry.version !== undefined && { version: entry.version }),
|
|
79
|
+
...(entry.configHash !== undefined && { configHash: entry.configHash })
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async openRegistry(serverId?: string): Promise<void> {
|
|
84
|
+
// Pre-populate the search only for known-good ids so the entry is in focus when
|
|
85
|
+
// the view opens. For revoked ids the server appears in the Installed section
|
|
86
|
+
// (with a warning + Unlink / Uninstall); jumping to an empty search hides it, so
|
|
87
|
+
// we just show the view and let the user see the warning in Installed.
|
|
88
|
+
if (serverId && this.hasServer(serverId)) {
|
|
89
|
+
this.searchModel.query = serverId;
|
|
90
|
+
}
|
|
91
|
+
// Reveal-and-activate rather than toggle: a second click on "Browse AI registry"
|
|
92
|
+
// must keep the view visible, not collapse it again.
|
|
93
|
+
const widget = await this.widgetManager.getOrCreateWidget(VSXExtensionsViewContainer.ID);
|
|
94
|
+
await this.shell.revealWidget(widget.id);
|
|
95
|
+
await this.shell.activateWidget(widget.id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected async refreshCache(): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
const entries = await this.fetchService.getEntries();
|
|
101
|
+
const next = new Set(entries.map(entry => entry.serverId));
|
|
102
|
+
// Refresh the per-id lookup table regardless of whether the id set itself
|
|
103
|
+
// changed - the *contents* of an entry (config, configHash) may have moved
|
|
104
|
+
// even when the id list is unchanged, and `getInstallEntry` callers expect
|
|
105
|
+
// the latest values.
|
|
106
|
+
this.entriesByServerId = new Map(entries.map(entry => [entry.serverId, entry]));
|
|
107
|
+
if (!areSetsEqual(next, this.knownServerIds)) {
|
|
108
|
+
this.knownServerIds = next;
|
|
109
|
+
this.onDidChangeEmitter.fire();
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Leave the cache as-is on failure; surfaced errors are the fetch service's job.
|
|
113
|
+
} finally {
|
|
114
|
+
// Signal readiness regardless of outcome - failed fetch is still a "we tried".
|
|
115
|
+
this.readyDeferred.resolve();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function areSetsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
121
|
+
if (a.size !== b.size) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
for (const value of a) {
|
|
125
|
+
if (!b.has(value)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
/** MCP-specific tweaks layered on top of the shared `.theia-vsx-extension` card. */
|
|
18
|
+
|
|
19
|
+
.theia-vsx-extension-icon.theia-mcp-extension-icon {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
background-image: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.theia-vsx-extension-icon.theia-mcp-extension-icon .codicon {
|
|
27
|
+
font-size: calc(var(--theia-vsx-extension-icon-size) * 0.65);
|
|
28
|
+
color: var(--theia-descriptionForeground);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.theia-mcp-extension-actions {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
/* Keep the action controls at full width when a long publisher string competes for space. */
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Invisible spacer reserving the same width VSX cards use for their context-menu gear,
|
|
39
|
+
so MCP and VSX action bars line up across the unified Extensions view. */
|
|
40
|
+
.theia-mcp-extension-gear-placeholder {
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.theia-mcp-extension-warning {
|
|
46
|
+
color: var(--theia-editorWarning-foreground, var(--theia-notificationsWarningIcon-foreground));
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.theia-mcp-extension-link-stale-message {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: calc(var(--theia-ui-padding) / 2);
|
|
54
|
+
font-size: 90%;
|
|
55
|
+
color: var(--theia-editorWarning-foreground, var(--theia-notificationsWarningIcon-foreground));
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Endpoint and tool identity for the AI registry integration.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to the public Eclipse-hosted AI registry with tool name `'all'`. Products
|
|
23
|
+
* that want a different registry or a tool-specific filter rebind this class in their
|
|
24
|
+
* frontend module:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* rebind(AIRegistryConfiguration).toConstantValue(new (class extends AIRegistryConfiguration {
|
|
28
|
+
* override getToolName(): string { return 'my-product'; }
|
|
29
|
+
* override getBaseUrl(): string { return 'https://internal.example/registry/'; }
|
|
30
|
+
* })());
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Values are intentionally not exposed as user preferences - a user must not be able
|
|
34
|
+
* to redirect the IDE to a different registry URL or change the tool identity (both
|
|
35
|
+
* are trust-relevant decisions).
|
|
36
|
+
*/
|
|
37
|
+
@injectable()
|
|
38
|
+
export class AIRegistryConfiguration {
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tool identifier used to scope which approvals apply. `'all'` is the safe default
|
|
42
|
+
* for any Theia-based product; rebind this in product code to filter the registry
|
|
43
|
+
* down to a tool-specific approval set.
|
|
44
|
+
*/
|
|
45
|
+
getToolName(): string {
|
|
46
|
+
return 'all';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getBaseUrl(): string {
|
|
50
|
+
return 'https://eclipsefdn-ai-registry.github.io/ai-registry-core/api/v1/';
|
|
51
|
+
}
|
|
52
|
+
}
|