@theia/ai-registry 1.73.0-next.10
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,248 @@
|
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { AIRegistryConfiguration } from '../ai-registry-configuration';
|
|
19
|
+
import { MCPRegistryEntryResolver, MCPRegistryEntryResolverImpl } from './mcp-registry-entry-resolver';
|
|
20
|
+
import { RegistryMCPServer } from './mcp-registry-types';
|
|
21
|
+
|
|
22
|
+
function createResolver(toolName: string = 'theia-ide'): MCPRegistryEntryResolver {
|
|
23
|
+
const resolver = new MCPRegistryEntryResolverImpl();
|
|
24
|
+
const configuration: AIRegistryConfiguration = Object.assign(new AIRegistryConfiguration(), {
|
|
25
|
+
getToolName(): string {
|
|
26
|
+
return toolName;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
Object.assign(resolver, { configuration });
|
|
30
|
+
return resolver;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('MCPRegistryEntryResolver.resolve', () => {
|
|
34
|
+
|
|
35
|
+
let resolver: MCPRegistryEntryResolver;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
resolver = createResolver();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('normalises a server with a single approval, install config and inner server, propagating configHash', () => {
|
|
42
|
+
const raw: RegistryMCPServer = {
|
|
43
|
+
serverId: 'io.github.example/example-mcp',
|
|
44
|
+
name: 'Example',
|
|
45
|
+
description: 'Example MCP server',
|
|
46
|
+
mcpRegistryVerified: true,
|
|
47
|
+
approvals: [{
|
|
48
|
+
organizationId: 'theia',
|
|
49
|
+
date: '2026-04-01',
|
|
50
|
+
version: '^1.0.0',
|
|
51
|
+
configHash: 'hash-v1',
|
|
52
|
+
installConfigs: [{
|
|
53
|
+
tool: 'theia-ide',
|
|
54
|
+
config: { servers: { example: { command: 'npx', args: ['-y', 'example-mcp'] } } }
|
|
55
|
+
}]
|
|
56
|
+
}]
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(resolver.resolve(raw)).to.deep.equal({
|
|
60
|
+
serverId: 'io.github.example/example-mcp',
|
|
61
|
+
name: 'Example',
|
|
62
|
+
description: 'Example MCP server',
|
|
63
|
+
localName: 'example',
|
|
64
|
+
config: { command: 'npx', args: ['-y', 'example-mcp'] },
|
|
65
|
+
version: '^1.0.0',
|
|
66
|
+
configHash: 'hash-v1',
|
|
67
|
+
mcpRegistryVerified: true
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('omits configHash when the approval has none - supports payloads pre-dating the field', () => {
|
|
72
|
+
const raw: RegistryMCPServer = {
|
|
73
|
+
serverId: 'io.github.example/legacy-mcp',
|
|
74
|
+
name: 'Legacy',
|
|
75
|
+
description: 'Legacy MCP server with no configHash',
|
|
76
|
+
mcpRegistryVerified: true,
|
|
77
|
+
approvals: [{
|
|
78
|
+
organizationId: 'theia',
|
|
79
|
+
date: '2026-04-01',
|
|
80
|
+
version: '^1.0.0',
|
|
81
|
+
installConfigs: [{
|
|
82
|
+
tool: 'theia-ide',
|
|
83
|
+
config: { servers: { legacy: { command: 'npx', args: ['-y', 'legacy-mcp'] } } }
|
|
84
|
+
}]
|
|
85
|
+
}]
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const resolved = resolver.resolve(raw);
|
|
89
|
+
expect(resolved).to.not.have.property('configHash');
|
|
90
|
+
expect(resolved?.version).to.equal('^1.0.0');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('picks the most recent approval when multiple organisations approved the same server', () => {
|
|
94
|
+
const raw: RegistryMCPServer = {
|
|
95
|
+
serverId: 'io.github.example/example-mcp',
|
|
96
|
+
name: 'Example',
|
|
97
|
+
description: 'Example MCP server',
|
|
98
|
+
mcpRegistryVerified: true,
|
|
99
|
+
approvals: [
|
|
100
|
+
{
|
|
101
|
+
organizationId: 'older-org',
|
|
102
|
+
date: '2025-01-01',
|
|
103
|
+
version: '^0.5.0',
|
|
104
|
+
installConfigs: [{
|
|
105
|
+
tool: 'theia-ide',
|
|
106
|
+
config: { servers: { example: { command: 'old-cmd' } } }
|
|
107
|
+
}]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
organizationId: 'newer-org',
|
|
111
|
+
date: '2026-04-01',
|
|
112
|
+
version: '^1.0.0',
|
|
113
|
+
installConfigs: [{
|
|
114
|
+
tool: 'theia-ide',
|
|
115
|
+
config: { servers: { example: { command: 'new-cmd' } } }
|
|
116
|
+
}]
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const resolved = resolver.resolve(raw);
|
|
122
|
+
expect(resolved?.version).to.equal('^1.0.0');
|
|
123
|
+
expect(resolved?.config).to.deep.equal({ command: 'new-cmd' });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns undefined when the server has no approvals', () => {
|
|
127
|
+
const raw: RegistryMCPServer = {
|
|
128
|
+
serverId: 'io.github.example/orphan',
|
|
129
|
+
name: 'Orphan',
|
|
130
|
+
description: 'No approvals',
|
|
131
|
+
mcpRegistryVerified: false,
|
|
132
|
+
approvals: []
|
|
133
|
+
};
|
|
134
|
+
expect(resolver.resolve(raw)).to.be.undefined;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns undefined when the picked approval has no usable install config', () => {
|
|
138
|
+
const raw: RegistryMCPServer = {
|
|
139
|
+
serverId: 'io.github.example/empty',
|
|
140
|
+
name: 'Empty',
|
|
141
|
+
description: 'Approval with no usable config',
|
|
142
|
+
mcpRegistryVerified: false,
|
|
143
|
+
approvals: [{
|
|
144
|
+
organizationId: 'theia',
|
|
145
|
+
date: '2026-04-01',
|
|
146
|
+
version: '^1.0.0',
|
|
147
|
+
installConfigs: [{ tool: 'theia-ide' }]
|
|
148
|
+
}]
|
|
149
|
+
};
|
|
150
|
+
expect(resolver.resolve(raw)).to.be.undefined;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('picks the install config matching the configured tool name when multiple are present', () => {
|
|
154
|
+
const productResolver = createResolver('my-product');
|
|
155
|
+
const raw: RegistryMCPServer = {
|
|
156
|
+
serverId: 'io.github.example/multi-tool',
|
|
157
|
+
name: 'Multi Tool',
|
|
158
|
+
description: 'Approval carrying configs for several tools',
|
|
159
|
+
mcpRegistryVerified: true,
|
|
160
|
+
approvals: [{
|
|
161
|
+
organizationId: 'theia',
|
|
162
|
+
date: '2026-04-01',
|
|
163
|
+
version: '^1.0.0',
|
|
164
|
+
installConfigs: [
|
|
165
|
+
{ tool: 'theia-ide', config: { servers: { example: { command: 'theia-cmd' } } } },
|
|
166
|
+
{ tool: 'my-product', config: { servers: { example: { command: 'product-cmd' } } } }
|
|
167
|
+
]
|
|
168
|
+
}]
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(productResolver.resolve(raw)?.config).to.deep.equal({ command: 'product-cmd' });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('accepts an untagged install config as a fallback when no tool-specific config matches', () => {
|
|
175
|
+
const productResolver = createResolver('my-product');
|
|
176
|
+
const raw: RegistryMCPServer = {
|
|
177
|
+
serverId: 'io.github.example/untagged',
|
|
178
|
+
name: 'Untagged',
|
|
179
|
+
description: 'Approval whose install config has no tool tag',
|
|
180
|
+
mcpRegistryVerified: true,
|
|
181
|
+
approvals: [{
|
|
182
|
+
organizationId: 'theia',
|
|
183
|
+
date: '2026-04-01',
|
|
184
|
+
version: '^1.0.0',
|
|
185
|
+
installConfigs: [
|
|
186
|
+
{ config: { servers: { example: { command: 'untagged-cmd' } } } }
|
|
187
|
+
]
|
|
188
|
+
}]
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
expect(productResolver.resolve(raw)?.config).to.deep.equal({ command: 'untagged-cmd' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("accepts every install config when the configured tool name is 'all'", () => {
|
|
195
|
+
const allResolver = createResolver('all');
|
|
196
|
+
const raw: RegistryMCPServer = {
|
|
197
|
+
serverId: 'io.github.example/all',
|
|
198
|
+
name: 'Any tool',
|
|
199
|
+
description: 'Approval whose install config is tagged for a different tool',
|
|
200
|
+
mcpRegistryVerified: true,
|
|
201
|
+
approvals: [{
|
|
202
|
+
organizationId: 'theia',
|
|
203
|
+
date: '2026-04-01',
|
|
204
|
+
version: '^1.0.0',
|
|
205
|
+
installConfigs: [
|
|
206
|
+
{ tool: 'other-tool', config: { servers: { example: { command: 'other-cmd' } } } }
|
|
207
|
+
]
|
|
208
|
+
}]
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
expect(allResolver.resolve(raw)?.config).to.deep.equal({ command: 'other-cmd' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('warns and picks the first key deterministically when the install config exposes multiple servers', () => {
|
|
215
|
+
const raw: RegistryMCPServer = {
|
|
216
|
+
serverId: 'io.github.example/multi-server',
|
|
217
|
+
name: 'Multi Server',
|
|
218
|
+
description: 'Approval whose install config exposes multiple servers',
|
|
219
|
+
mcpRegistryVerified: true,
|
|
220
|
+
approvals: [{
|
|
221
|
+
organizationId: 'theia',
|
|
222
|
+
date: '2026-04-01',
|
|
223
|
+
version: '^1.0.0',
|
|
224
|
+
installConfigs: [{
|
|
225
|
+
tool: 'theia-ide',
|
|
226
|
+
config: {
|
|
227
|
+
servers: {
|
|
228
|
+
primary: { command: 'first-cmd' },
|
|
229
|
+
secondary: { command: 'second-cmd' }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}]
|
|
233
|
+
}]
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const warnings: string[] = [];
|
|
237
|
+
const originalWarn = console.warn;
|
|
238
|
+
console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); };
|
|
239
|
+
try {
|
|
240
|
+
const resolved = resolver.resolve(raw);
|
|
241
|
+
expect(resolved?.localName).to.equal('primary');
|
|
242
|
+
expect(resolved?.config).to.deep.equal({ command: 'first-cmd' });
|
|
243
|
+
expect(warnings.some(w => w.includes('multiple servers'))).to.equal(true);
|
|
244
|
+
} finally {
|
|
245
|
+
console.warn = originalWarn;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { AIRegistryConfiguration } from '../ai-registry-configuration';
|
|
19
|
+
import { RegistryMCPServer, ResolvedRegistryEntry } from './mcp-registry-types';
|
|
20
|
+
|
|
21
|
+
export const MCPRegistryEntryResolver = Symbol('MCPRegistryEntryResolver');
|
|
22
|
+
export interface MCPRegistryEntryResolver {
|
|
23
|
+
/** Normalises a raw registry server entry into the single (slug, config, version) tuple the install path uses. */
|
|
24
|
+
resolve(raw: RegistryMCPServer): ResolvedRegistryEntry | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@injectable()
|
|
28
|
+
export class MCPRegistryEntryResolverImpl implements MCPRegistryEntryResolver {
|
|
29
|
+
|
|
30
|
+
@inject(AIRegistryConfiguration)
|
|
31
|
+
protected readonly configuration: AIRegistryConfiguration;
|
|
32
|
+
|
|
33
|
+
resolve(raw: RegistryMCPServer): ResolvedRegistryEntry | undefined {
|
|
34
|
+
const approval = [...raw.approvals].sort((a, b) => b.date.localeCompare(a.date))[0];
|
|
35
|
+
if (!approval) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
// Per-tool endpoints should already pre-filter install configs, but a registry
|
|
39
|
+
// may still emit several. Pick the one tagged for our configured tool name
|
|
40
|
+
// (or untagged, which we treat as "applies to all tools"); fall back to the
|
|
41
|
+
// first config so a registry that hasn't tagged its entries still works.
|
|
42
|
+
const toolName = this.configuration.getToolName();
|
|
43
|
+
const installConfig = approval.installConfigs.find(c => !c.tool || c.tool === toolName || toolName === 'all')
|
|
44
|
+
?? approval.installConfigs[0];
|
|
45
|
+
const servers = installConfig?.config?.servers ?? {};
|
|
46
|
+
const serverKeys = Object.keys(servers);
|
|
47
|
+
if (serverKeys.length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (serverKeys.length > 1) {
|
|
51
|
+
// Multi-server install configs aren't a Theia concept - we install one server
|
|
52
|
+
// per registry entry. Warn so the registry maintainer is aware their payload
|
|
53
|
+
// exposed more than we use, and pick the first slug deterministically.
|
|
54
|
+
console.warn(`AI registry entry ${raw.serverId} has multiple servers in its install config; using ${serverKeys[0]}.`);
|
|
55
|
+
}
|
|
56
|
+
const localName = serverKeys[0];
|
|
57
|
+
return {
|
|
58
|
+
serverId: raw.serverId,
|
|
59
|
+
name: raw.name,
|
|
60
|
+
description: raw.description,
|
|
61
|
+
localName,
|
|
62
|
+
config: servers[localName],
|
|
63
|
+
version: approval.version,
|
|
64
|
+
...(approval.configHash !== undefined && { configHash: approval.configHash }),
|
|
65
|
+
mcpRegistryVerified: raw.mcpRegistryVerified
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { MCPInstallEntryConfig } from '@theia/ai-mcp/lib/common/mcp-server-manager';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shape of `installConfigs[].config` for Theia: a `servers` map keyed by the local
|
|
21
|
+
* MCP server key chosen by the registry maintainer. Server entries use the canonical
|
|
22
|
+
* {@link MCPInstallEntryConfig} type from `@theia/ai-mcp` so the registry and the
|
|
23
|
+
* install path can never drift in shape.
|
|
24
|
+
*/
|
|
25
|
+
export interface RegistryMCPInstallConfigBlob {
|
|
26
|
+
servers: Record<string, MCPInstallEntryConfig>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A single install config inside an approval. Already filtered per-tool by the registry
|
|
31
|
+
* for the per-tool view (`<baseUrl>/<toolName>.json`).
|
|
32
|
+
*/
|
|
33
|
+
export interface RegistryInstallConfig {
|
|
34
|
+
tool?: string;
|
|
35
|
+
installUrl?: string;
|
|
36
|
+
openVsxUrl?: string;
|
|
37
|
+
config?: RegistryMCPInstallConfigBlob;
|
|
38
|
+
instructions?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One organization's approval of an MCP server entry, with the install configs for the
|
|
43
|
+
* tools that organization approved.
|
|
44
|
+
*/
|
|
45
|
+
export interface RegistryApproval {
|
|
46
|
+
organizationId: string;
|
|
47
|
+
date: string;
|
|
48
|
+
/** Pinned server version (per the AI-registry approval schema). Omitted means "use the latest from the Anthropic MCP registry". */
|
|
49
|
+
version?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Content hash of the approval produced by the registry's consolidation pipeline.
|
|
52
|
+
* Drives update detection on the client - when the hash differs from the locally
|
|
53
|
+
* stored `registryMetadata.configHash`, the registry has published a new approval
|
|
54
|
+
* and Theia offers an Update action. Optional for backwards compatibility with
|
|
55
|
+
* older payloads that pre-date the field.
|
|
56
|
+
*/
|
|
57
|
+
configHash?: string;
|
|
58
|
+
installConfigs: RegistryInstallConfig[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Top-level MCP server entry as returned by the registry's per-tool JSON endpoint.
|
|
63
|
+
*/
|
|
64
|
+
export interface RegistryMCPServer {
|
|
65
|
+
serverId: string;
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
mcpRegistryVerified: boolean;
|
|
69
|
+
approvals: RegistryApproval[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A registry MCP entry after resolving the (potentially multiple) approvals and install configs
|
|
74
|
+
* down to the single (key, config, version) tuple the install service operates on.
|
|
75
|
+
*
|
|
76
|
+
* Resolution lives in the fetch layer; the install service expects this normalised shape.
|
|
77
|
+
*/
|
|
78
|
+
export interface ResolvedRegistryEntry {
|
|
79
|
+
serverId: string;
|
|
80
|
+
name: string;
|
|
81
|
+
description: string;
|
|
82
|
+
/** The local preference key the registry maintainer chose (inner config.servers key). */
|
|
83
|
+
localName: string;
|
|
84
|
+
/** The config blob to write into `ai-features.mcp.mcpServers[localName]`. */
|
|
85
|
+
config: MCPInstallEntryConfig;
|
|
86
|
+
/** Registry-published version. Stored alongside installed entries for display only - update detection uses {@link configHash}. */
|
|
87
|
+
version?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Content hash of the chosen approval - compared against the local
|
|
90
|
+
* `registryMetadata.configHash` to decide whether an Update is available. Optional
|
|
91
|
+
* for backwards compatibility with older registry payloads - when absent the client
|
|
92
|
+
* does not offer Update, since "no hash" is not evidence of a new version.
|
|
93
|
+
*/
|
|
94
|
+
configHash?: string;
|
|
95
|
+
/** True if the entry is verified against the Anthropic MCP registry - drives the "verified only" search filter. */
|
|
96
|
+
mcpRegistryVerified: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Outcome of classifying an entry against the opposite side (registry -> local prefs
|
|
101
|
+
* for search/list views, or local prefs -> registry for the Installed view).
|
|
102
|
+
*
|
|
103
|
+
* Not every state is producible in both directions:
|
|
104
|
+
*
|
|
105
|
+
* - `not-installed` is only produced by `classifyRegistryEntry`.
|
|
106
|
+
* - `installed-user-added` is only produced by `classifyLocalServer`.
|
|
107
|
+
* - `installed-link-stale` is produced by both classifiers - `classifyLocalServer`
|
|
108
|
+
* when a linked local points to an unknown id, and `classifyRegistryEntry` when the
|
|
109
|
+
* key-matching local does so. The Installed and Search views show the same Unlink
|
|
110
|
+
* and Uninstall affordances in either case.
|
|
111
|
+
* - `installed-from-registry`, `installed-manually`, and `fix-config` are common.
|
|
112
|
+
*/
|
|
113
|
+
export type ClassificationResult =
|
|
114
|
+
| { kind: 'installed-from-registry'; updateAvailable: boolean }
|
|
115
|
+
| { kind: 'installed-manually' }
|
|
116
|
+
| { kind: 'fix-config' }
|
|
117
|
+
| { kind: 'not-installed' }
|
|
118
|
+
| { kind: 'installed-link-stale' }
|
|
119
|
+
| { kind: 'installed-user-added' };
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { Container } from '@theia/core/shared/inversify';
|
|
19
|
+
import { RequestContext, RequestOptions, RequestService } from '@theia/core/shared/@theia/request';
|
|
20
|
+
import { AIRegistryConfiguration } from './ai-registry-configuration';
|
|
21
|
+
import { MCPRegistryEntryResolver, MCPRegistryEntryResolverImpl } from './mcp/mcp-registry-entry-resolver';
|
|
22
|
+
import { RegistryFetchService, RegistryFetchServiceImpl } from './registry-fetch-service';
|
|
23
|
+
|
|
24
|
+
class FakeRequestService implements RequestService {
|
|
25
|
+
public lastUrl: string | undefined;
|
|
26
|
+
public callCount = 0;
|
|
27
|
+
constructor(private readonly responseBody: string, private readonly statusCode = 200) { }
|
|
28
|
+
async configure(): Promise<void> { /* no-op */ }
|
|
29
|
+
async resolveProxy(): Promise<string | undefined> { return undefined; }
|
|
30
|
+
async request(options: RequestOptions): Promise<RequestContext> {
|
|
31
|
+
this.lastUrl = options.url;
|
|
32
|
+
this.callCount += 1;
|
|
33
|
+
return {
|
|
34
|
+
url: options.url,
|
|
35
|
+
res: { headers: {}, statusCode: this.statusCode },
|
|
36
|
+
buffer: new TextEncoder().encode(this.responseBody)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class FakeConfiguration extends AIRegistryConfiguration {
|
|
42
|
+
constructor(private readonly toolName: string, private readonly baseUrl: string) { super(); }
|
|
43
|
+
override getToolName(): string { return this.toolName; }
|
|
44
|
+
override getBaseUrl(): string { return this.baseUrl; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function payload(): string {
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
organizations: [],
|
|
50
|
+
tools: [],
|
|
51
|
+
mcp: [{
|
|
52
|
+
serverId: 'io.github.example/example-mcp',
|
|
53
|
+
name: 'Example',
|
|
54
|
+
description: 'Example MCP server',
|
|
55
|
+
mcpRegistryVerified: true,
|
|
56
|
+
approvals: [{
|
|
57
|
+
organizationId: 'theia',
|
|
58
|
+
date: '2026-04-01',
|
|
59
|
+
version: '^1.0.0',
|
|
60
|
+
configHash: 'hash-v1',
|
|
61
|
+
installConfigs: [{
|
|
62
|
+
tool: 'theia-ide',
|
|
63
|
+
config: { servers: { example: { command: 'npx', args: ['-y', 'example-mcp'] } } }
|
|
64
|
+
}]
|
|
65
|
+
}]
|
|
66
|
+
}]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('RegistryFetchService', () => {
|
|
71
|
+
|
|
72
|
+
function buildContainer(requestService: RequestService, config: AIRegistryConfiguration): Container {
|
|
73
|
+
const container = new Container();
|
|
74
|
+
container.bind(RequestService).toConstantValue(requestService);
|
|
75
|
+
container.bind(AIRegistryConfiguration).toConstantValue(config);
|
|
76
|
+
container.bind(MCPRegistryEntryResolverImpl).toSelf().inSingletonScope();
|
|
77
|
+
container.bind(MCPRegistryEntryResolver).toService(MCPRegistryEntryResolverImpl);
|
|
78
|
+
container.bind(RegistryFetchServiceImpl).toSelf().inSingletonScope();
|
|
79
|
+
container.bind(RegistryFetchService).toService(RegistryFetchServiceImpl);
|
|
80
|
+
return container;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
it('fetches the per-tool JSON from <baseUrl>/<toolName>.json and returns resolved entries', async () => {
|
|
84
|
+
const request = new FakeRequestService(payload());
|
|
85
|
+
const config = new FakeConfiguration('theia-ide', 'https://example.test/api/v1/');
|
|
86
|
+
const service = buildContainer(request, config).get<RegistryFetchService>(RegistryFetchService);
|
|
87
|
+
|
|
88
|
+
const entries = await service.getEntries();
|
|
89
|
+
|
|
90
|
+
expect(request.lastUrl).to.equal('https://example.test/api/v1/theia-ide.json');
|
|
91
|
+
expect(entries).to.have.length(1);
|
|
92
|
+
expect(entries[0]).to.deep.equal({
|
|
93
|
+
serverId: 'io.github.example/example-mcp',
|
|
94
|
+
name: 'Example',
|
|
95
|
+
description: 'Example MCP server',
|
|
96
|
+
localName: 'example',
|
|
97
|
+
config: { command: 'npx', args: ['-y', 'example-mcp'] },
|
|
98
|
+
version: '^1.0.0',
|
|
99
|
+
configHash: 'hash-v1',
|
|
100
|
+
mcpRegistryVerified: true
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('serves cached entries on a second call without issuing a new request', async () => {
|
|
105
|
+
const request = new FakeRequestService(payload());
|
|
106
|
+
const service = buildContainer(request, new FakeConfiguration('theia-ide', 'https://example.test/api/v1/')).get<RegistryFetchService>(RegistryFetchService);
|
|
107
|
+
|
|
108
|
+
await service.getEntries();
|
|
109
|
+
await service.getEntries();
|
|
110
|
+
|
|
111
|
+
expect(request.callCount).to.equal(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('refetches when forceRefresh is true', async () => {
|
|
115
|
+
const request = new FakeRequestService(payload());
|
|
116
|
+
const service = buildContainer(request, new FakeConfiguration('theia-ide', 'https://example.test/api/v1/')).get<RegistryFetchService>(RegistryFetchService);
|
|
117
|
+
|
|
118
|
+
await service.getEntries();
|
|
119
|
+
await service.getEntries(true);
|
|
120
|
+
|
|
121
|
+
expect(request.callCount).to.equal(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throws a descriptive error when the server returns a non-success status', async () => {
|
|
125
|
+
const request = new FakeRequestService('', 404);
|
|
126
|
+
const service = buildContainer(request, new FakeConfiguration('theia-ide', 'https://example.test/api/v1/')).get<RegistryFetchService>(RegistryFetchService);
|
|
127
|
+
|
|
128
|
+
let caught: Error | undefined;
|
|
129
|
+
try {
|
|
130
|
+
await service.getEntries();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
caught = error as Error;
|
|
133
|
+
}
|
|
134
|
+
expect(caught?.message).to.match(/HTTP 404/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
19
|
+
import { RequestContext, RequestService } from '@theia/core/shared/@theia/request';
|
|
20
|
+
import { AIRegistryConfiguration } from './ai-registry-configuration';
|
|
21
|
+
import { MCPRegistryEntryResolver } from './mcp/mcp-registry-entry-resolver';
|
|
22
|
+
import { RegistryMCPServer, ResolvedRegistryEntry } from './mcp/mcp-registry-types';
|
|
23
|
+
|
|
24
|
+
interface RegistryResponse {
|
|
25
|
+
mcp?: RegistryMCPServer[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const RegistryFetchService = Symbol('RegistryFetchService');
|
|
29
|
+
export interface RegistryFetchService {
|
|
30
|
+
/** Fires whenever the cached set of resolved entries changes (initial load, manual refresh). */
|
|
31
|
+
readonly onDidChange: Event<void>;
|
|
32
|
+
/** Returns the resolved registry entries, fetching (and caching) them on first use or when `forceRefresh` is set. */
|
|
33
|
+
getEntries(forceRefresh?: boolean): Promise<ResolvedRegistryEntry[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@injectable()
|
|
37
|
+
export class RegistryFetchServiceImpl implements RegistryFetchService {
|
|
38
|
+
|
|
39
|
+
@inject(RequestService)
|
|
40
|
+
protected readonly requestService: RequestService;
|
|
41
|
+
|
|
42
|
+
@inject(AIRegistryConfiguration)
|
|
43
|
+
protected readonly configuration: AIRegistryConfiguration;
|
|
44
|
+
|
|
45
|
+
@inject(MCPRegistryEntryResolver)
|
|
46
|
+
protected readonly resolver: MCPRegistryEntryResolver;
|
|
47
|
+
|
|
48
|
+
protected cached: ResolvedRegistryEntry[] | undefined;
|
|
49
|
+
|
|
50
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
51
|
+
/** Fires whenever the cached set of resolved entries changes (initial load, manual refresh). */
|
|
52
|
+
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
|
53
|
+
|
|
54
|
+
async getEntries(forceRefresh: boolean = false): Promise<ResolvedRegistryEntry[]> {
|
|
55
|
+
if (this.cached && !forceRefresh) {
|
|
56
|
+
return this.cached;
|
|
57
|
+
}
|
|
58
|
+
const url = this.buildEndpointUrl();
|
|
59
|
+
const context = await this.requestService.request({ url });
|
|
60
|
+
if (!RequestContext.isSuccess(context)) {
|
|
61
|
+
throw new Error(`Failed to fetch AI registry from ${url}: HTTP ${context.res.statusCode ?? 'unknown'}`);
|
|
62
|
+
}
|
|
63
|
+
const data = RequestContext.asJson<RegistryResponse>(context);
|
|
64
|
+
const entries = (data.mcp ?? [])
|
|
65
|
+
.map(server => this.resolver.resolve(server))
|
|
66
|
+
.filter((entry): entry is ResolvedRegistryEntry => entry !== undefined);
|
|
67
|
+
this.cached = entries;
|
|
68
|
+
this.onDidChangeEmitter.fire();
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected buildEndpointUrl(): string {
|
|
73
|
+
const base = this.configuration.getBaseUrl();
|
|
74
|
+
const tool = this.configuration.getToolName();
|
|
75
|
+
const separator = base.endsWith('/') ? '' : '/';
|
|
76
|
+
return `${base}${separator}${tool}.json`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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
|
+
/* note: this bogus test file is required so that
|
|
18
|
+
we are able to run mocha unit tests on this
|
|
19
|
+
package, without having any actual unit tests in it.
|
|
20
|
+
This way a coverage report will be generated,
|
|
21
|
+
showing 0% coverage, instead of no report.
|
|
22
|
+
This file can be removed once we have real unit
|
|
23
|
+
tests in place. */
|
|
24
|
+
|
|
25
|
+
describe('ai-registry package', () => {
|
|
26
|
+
|
|
27
|
+
it('support code coverage statistics', () => true);
|
|
28
|
+
});
|