pikiloop 0.4.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * MCP OAuth 2.1 + Dynamic Client Registration subsystem.
3
+ *
4
+ * Implements the MCP auth spec flow for remote HTTP MCP servers:
5
+ * 1. Discover auth server (Protected Resource Metadata + WWW-Authenticate header).
6
+ * 2. Fetch Authorization Server Metadata.
7
+ * 3. Register client dynamically (RFC 7591) if no pre-registered client_id.
8
+ * 4. Drive authorization_code + PKCE — returns auth URL for user's browser.
9
+ * 5. Exchange code for tokens on callback.
10
+ * 6. Refresh tokens when expired.
11
+ *
12
+ * Tokens are persisted via the token store (setting.json `extensions.mcpTokens`).
13
+ * State for in-flight authorizations is kept in memory only.
14
+ */
15
+ import crypto from 'node:crypto';
16
+ import { loadUserConfig, saveUserConfig, } from '../../core/config/user-config.js';
17
+ // ---------------------------------------------------------------------------
18
+ // Token store (setting.json-backed)
19
+ // ---------------------------------------------------------------------------
20
+ export function getMcpToken(serverId) {
21
+ const cfg = loadUserConfig();
22
+ return cfg.extensions?.mcpTokens?.[serverId];
23
+ }
24
+ export function saveMcpToken(serverId, token) {
25
+ const cfg = loadUserConfig();
26
+ const extensions = cfg.extensions ?? {};
27
+ const mcpTokens = { ...(extensions.mcpTokens ?? {}) };
28
+ mcpTokens[serverId] = token;
29
+ saveUserConfig({ ...cfg, extensions: { ...extensions, mcpTokens } });
30
+ }
31
+ export function deleteMcpToken(serverId) {
32
+ const cfg = loadUserConfig();
33
+ const tokens = { ...(cfg.extensions?.mcpTokens ?? {}) };
34
+ if (!(serverId in tokens))
35
+ return false;
36
+ delete tokens[serverId];
37
+ saveUserConfig({ ...cfg, extensions: { ...cfg.extensions, mcpTokens: tokens } });
38
+ return true;
39
+ }
40
+ export function hasValidMcpToken(serverId) {
41
+ const token = getMcpToken(serverId);
42
+ if (!token?.accessToken)
43
+ return false;
44
+ if (token.expiresAt && token.expiresAt < Date.now() + 30_000)
45
+ return false;
46
+ return true;
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // PKCE helpers
50
+ // ---------------------------------------------------------------------------
51
+ function randomUrlSafe(bytes) {
52
+ return crypto.randomBytes(bytes).toString('base64url');
53
+ }
54
+ function pkceChallenge(verifier) {
55
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Discovery
59
+ // ---------------------------------------------------------------------------
60
+ async function fetchJson(url, init, timeoutMs = 8_000) {
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
63
+ try {
64
+ const res = await fetch(url, { ...init, signal: controller.signal });
65
+ if (!res.ok)
66
+ throw new Error(`${url} responded ${res.status}`);
67
+ return await res.json();
68
+ }
69
+ finally {
70
+ clearTimeout(timer);
71
+ }
72
+ }
73
+ /**
74
+ * Discover the authorization server for a remote MCP resource and the
75
+ * canonical `resource` indicator the AS will validate against.
76
+ *
77
+ * Strategy:
78
+ * 1. Try GET {resourceBase}/.well-known/oauth-protected-resource (RFC 9728).
79
+ * 2. Fall back to probing the MCP endpoint for a 401 with WWW-Authenticate
80
+ * and follow the resource_metadata link.
81
+ * 3. Last resort: treat the origin as the AS and the input URL as the
82
+ * canonical resource.
83
+ */
84
+ async function discoverAuthorizationServer(resourceUrl) {
85
+ const origin = new URL(resourceUrl).origin;
86
+ const adopt = (meta) => {
87
+ const as = meta.authorization_servers?.[0];
88
+ if (!as)
89
+ return null;
90
+ return {
91
+ authorizationServer: as,
92
+ canonicalResource: meta.resource || resourceUrl,
93
+ };
94
+ };
95
+ // Strategy 1: Protected Resource Metadata at the origin
96
+ try {
97
+ const meta = await fetchJson(`${origin}/.well-known/oauth-protected-resource`);
98
+ const out = adopt(meta);
99
+ if (out)
100
+ return out;
101
+ }
102
+ catch { /* try next */ }
103
+ // Strategy 2: probe resource for WWW-Authenticate, follow resource_metadata
104
+ try {
105
+ const controller = new AbortController();
106
+ const timer = setTimeout(() => controller.abort(), 8_000);
107
+ const res = await fetch(resourceUrl, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
110
+ body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'initialize', params: {} }),
111
+ signal: controller.signal,
112
+ });
113
+ clearTimeout(timer);
114
+ if (res.status === 401) {
115
+ const auth = res.headers.get('www-authenticate') || '';
116
+ // A header may declare resource_metadata twice; prefer the most specific
117
+ // (last) match since servers commonly emit both origin-level and
118
+ // per-resource paths in one header.
119
+ const matches = [...auth.matchAll(/resource_metadata="([^"]+)"/gi)];
120
+ const link = matches.length ? matches[matches.length - 1][1] : null;
121
+ if (link) {
122
+ const meta = await fetchJson(link);
123
+ const out = adopt(meta);
124
+ if (out)
125
+ return out;
126
+ }
127
+ }
128
+ }
129
+ catch { /* fall through */ }
130
+ // Last resort: assume the resource origin is itself the authorization server
131
+ return { authorizationServer: origin, canonicalResource: resourceUrl };
132
+ }
133
+ async function fetchAuthorizationServerMetadata(issuer) {
134
+ // Try OAuth AS metadata first (RFC 8414), then OIDC fallback
135
+ const candidates = [
136
+ `${issuer.replace(/\/$/, '')}/.well-known/oauth-authorization-server`,
137
+ `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`,
138
+ ];
139
+ let lastErr;
140
+ for (const url of candidates) {
141
+ try {
142
+ return await fetchJson(url);
143
+ }
144
+ catch (e) {
145
+ lastErr = e;
146
+ }
147
+ }
148
+ throw new Error(`no AS metadata at ${issuer}: ${lastErr}`);
149
+ }
150
+ async function registerClient(registrationEndpoint, redirectUri, clientName) {
151
+ const body = {
152
+ client_name: clientName,
153
+ redirect_uris: [redirectUri],
154
+ grant_types: ['authorization_code', 'refresh_token'],
155
+ response_types: ['code'],
156
+ token_endpoint_auth_method: 'none',
157
+ application_type: 'native',
158
+ };
159
+ return fetchJson(registrationEndpoint, {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify(body),
163
+ });
164
+ }
165
+ async function resolveEndpoints(auth, resourceUrl) {
166
+ if (auth.type !== 'mcp-oauth')
167
+ throw new Error('not an mcp-oauth auth spec');
168
+ if (auth.authorizationEndpoint && auth.tokenEndpoint) {
169
+ return {
170
+ authorizationEndpoint: auth.authorizationEndpoint,
171
+ tokenEndpoint: auth.tokenEndpoint,
172
+ registrationEndpoint: auth.registrationEndpoint,
173
+ canonicalResource: resourceUrl,
174
+ };
175
+ }
176
+ const discovered = await discoverAuthorizationServer(resourceUrl);
177
+ const meta = await fetchAuthorizationServerMetadata(discovered.authorizationServer);
178
+ return {
179
+ authorizationEndpoint: meta.authorization_endpoint,
180
+ tokenEndpoint: meta.token_endpoint,
181
+ registrationEndpoint: meta.registration_endpoint,
182
+ issuer: meta.issuer,
183
+ canonicalResource: discovered.canonicalResource,
184
+ };
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // Pending state (in-memory, short-lived)
188
+ // ---------------------------------------------------------------------------
189
+ const pendingFlows = new Map();
190
+ const PENDING_TTL_MS = 10 * 60 * 1000;
191
+ function sweepPending() {
192
+ const now = Date.now();
193
+ for (const [state, flow] of pendingFlows) {
194
+ if (now - flow.createdAt > PENDING_TTL_MS)
195
+ pendingFlows.delete(state);
196
+ }
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Public API: start authorization, handle callback, refresh
200
+ // ---------------------------------------------------------------------------
201
+ /**
202
+ * Kick off an OAuth flow for the given MCP server.
203
+ *
204
+ * @param serverId The catalog id / installed-extension key.
205
+ * @param auth The auth spec from registry.
206
+ * @param resourceUrl The MCP server URL (http transport url).
207
+ * @param redirectUri Callback URL (http://localhost:<port>/api/extensions/mcp/oauth/callback).
208
+ * @param clientName Human-readable client name shown to the provider.
209
+ */
210
+ export async function startAuthorization(opts) {
211
+ sweepPending();
212
+ const { serverId, auth, resourceUrl, redirectUri, clientName } = opts;
213
+ if (auth.type !== 'mcp-oauth')
214
+ throw new Error(`server ${serverId} is not mcp-oauth`);
215
+ const endpoints = await resolveEndpoints(auth, resourceUrl);
216
+ const canonicalResource = endpoints.canonicalResource;
217
+ let clientId = auth.clientId;
218
+ let clientSecret;
219
+ if (!clientId) {
220
+ if (!endpoints.registrationEndpoint) {
221
+ throw new Error(`server ${serverId} has no client_id and no dynamic registration endpoint`);
222
+ }
223
+ const reg = await registerClient(endpoints.registrationEndpoint, redirectUri, clientName);
224
+ clientId = reg.client_id;
225
+ clientSecret = reg.client_secret;
226
+ }
227
+ const state = randomUrlSafe(32);
228
+ const codeVerifier = randomUrlSafe(48);
229
+ const codeChallenge = pkceChallenge(codeVerifier);
230
+ const params = new URLSearchParams({
231
+ response_type: 'code',
232
+ client_id: clientId,
233
+ redirect_uri: redirectUri,
234
+ state,
235
+ code_challenge: codeChallenge,
236
+ code_challenge_method: 'S256',
237
+ resource: canonicalResource,
238
+ });
239
+ if (auth.scopes?.length)
240
+ params.set('scope', auth.scopes.join(' '));
241
+ const authUrl = `${endpoints.authorizationEndpoint}${endpoints.authorizationEndpoint.includes('?') ? '&' : '?'}${params.toString()}`;
242
+ pendingFlows.set(state, {
243
+ state,
244
+ codeVerifier,
245
+ serverId,
246
+ resource: canonicalResource,
247
+ clientId,
248
+ clientSecret,
249
+ tokenEndpoint: endpoints.tokenEndpoint,
250
+ authorizationEndpoint: endpoints.authorizationEndpoint,
251
+ registrationEndpoint: endpoints.registrationEndpoint,
252
+ redirectUri,
253
+ createdAt: Date.now(),
254
+ });
255
+ return { authUrl, state };
256
+ }
257
+ /**
258
+ * Exchange the authorization code for tokens and persist the result.
259
+ */
260
+ export async function completeAuthorization(opts) {
261
+ const flow = pendingFlows.get(opts.state);
262
+ if (!flow)
263
+ throw new Error('unknown or expired oauth state');
264
+ pendingFlows.delete(opts.state);
265
+ const body = new URLSearchParams({
266
+ grant_type: 'authorization_code',
267
+ code: opts.code,
268
+ redirect_uri: flow.redirectUri,
269
+ client_id: flow.clientId,
270
+ code_verifier: flow.codeVerifier,
271
+ resource: flow.resource,
272
+ });
273
+ if (flow.clientSecret)
274
+ body.set('client_secret', flow.clientSecret);
275
+ const res = await fetch(flow.tokenEndpoint, {
276
+ method: 'POST',
277
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
278
+ body: body.toString(),
279
+ });
280
+ if (!res.ok) {
281
+ const text = await res.text().catch(() => '');
282
+ throw new Error(`token exchange failed (${res.status}): ${text.slice(0, 200)}`);
283
+ }
284
+ const token = await res.json();
285
+ const record = {
286
+ accessToken: token.access_token,
287
+ tokenType: token.token_type || 'Bearer',
288
+ refreshToken: token.refresh_token,
289
+ expiresAt: token.expires_in ? Date.now() + (token.expires_in * 1000) : undefined,
290
+ scope: token.scope,
291
+ clientId: flow.clientId,
292
+ clientSecret: flow.clientSecret,
293
+ authorizationEndpoint: flow.authorizationEndpoint,
294
+ tokenEndpoint: flow.tokenEndpoint,
295
+ registrationEndpoint: flow.registrationEndpoint,
296
+ resource: flow.resource,
297
+ };
298
+ saveMcpToken(flow.serverId, record);
299
+ return { ok: true, serverId: flow.serverId };
300
+ }
301
+ /**
302
+ * Refresh an expired token. Returns the updated record or null if refresh failed
303
+ * (e.g., no refresh_token available, or refresh rejected).
304
+ */
305
+ export async function refreshMcpToken(serverId) {
306
+ const token = getMcpToken(serverId);
307
+ if (!token?.refreshToken)
308
+ return null;
309
+ const body = new URLSearchParams({
310
+ grant_type: 'refresh_token',
311
+ refresh_token: token.refreshToken,
312
+ client_id: token.clientId,
313
+ resource: token.resource,
314
+ });
315
+ if (token.clientSecret)
316
+ body.set('client_secret', token.clientSecret);
317
+ try {
318
+ const res = await fetch(token.tokenEndpoint, {
319
+ method: 'POST',
320
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
321
+ body: body.toString(),
322
+ });
323
+ if (!res.ok)
324
+ return null;
325
+ const next = await res.json();
326
+ const updated = {
327
+ ...token,
328
+ accessToken: next.access_token,
329
+ tokenType: next.token_type || token.tokenType,
330
+ refreshToken: next.refresh_token || token.refreshToken,
331
+ expiresAt: next.expires_in ? Date.now() + (next.expires_in * 1000) : undefined,
332
+ scope: next.scope || token.scope,
333
+ };
334
+ saveMcpToken(serverId, updated);
335
+ return updated;
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
341
+ /**
342
+ * Inject Authorization: Bearer headers for remote MCP configs that have a token.
343
+ * Called by bridge.ts before handing merged config to agents.
344
+ */
345
+ export function injectOAuthHeaders(name, config) {
346
+ const token = getMcpToken(name);
347
+ if (!token?.accessToken)
348
+ return config.headers || {};
349
+ return {
350
+ ...(config.headers || {}),
351
+ Authorization: `${token.tokenType || 'Bearer'} ${token.accessToken}`,
352
+ };
353
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * MCP extension registry — curated recommended servers, recommended skills, community search.
3
+ *
4
+ * Schema supports three transport × auth combinations:
5
+ * - stdio + none (local, zero-config)
6
+ * - stdio + credentials (local, needs API key/token)
7
+ * - http + none (rare)
8
+ * - http + credentials (remote with API key)
9
+ * - http + mcp-oauth (remote SaaS via MCP OAuth spec)
10
+ *
11
+ * For MCP-OAuth servers, `authorizationEndpoint` / `tokenEndpoint` can be
12
+ * pre-declared (fast path) or omitted (discovered via MCP Protected Resource
13
+ * Metadata at `<url>/.well-known/oauth-protected-resource`).
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // Recommended MCP servers + skill repos — data lives in src/catalog/
17
+ //
18
+ // This module owns the *types* and helper functions. Edit
19
+ // `src/catalog/mcp-servers.ts` and `src/catalog/skill-repos.ts` to add or hide
20
+ // entries; the arrays below are just pointers back at that catalog.
21
+ // ---------------------------------------------------------------------------
22
+ import { MCP_SERVERS, SKILL_REPOS } from '../../catalog/index.js';
23
+ const RECOMMENDED_MCP_SERVERS = MCP_SERVERS;
24
+ const RECOMMENDED_SKILL_REPOS = SKILL_REPOS;
25
+ // ---------------------------------------------------------------------------
26
+ // Public API
27
+ // ---------------------------------------------------------------------------
28
+ export function getRecommendedMcpServers() {
29
+ return RECOMMENDED_MCP_SERVERS;
30
+ }
31
+ export function getRecommendedMcpServer(id) {
32
+ return RECOMMENDED_MCP_SERVERS.find(s => s.id === id);
33
+ }
34
+ export function getRecommendedSkillRepos() {
35
+ return RECOMMENDED_SKILL_REPOS;
36
+ }
37
+ /**
38
+ * Search the official MCP Registry API for servers.
39
+ * Falls back to npm search if the registry is unreachable.
40
+ */
41
+ export async function searchMcpServers(query, limit = 20) {
42
+ if (!query.trim())
43
+ return [];
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), 8_000);
46
+ try {
47
+ const url = `https://registry.modelcontextprotocol.io/v0.1/servers?search=${encodeURIComponent(query)}&limit=${limit}`;
48
+ const res = await fetch(url, { signal: controller.signal });
49
+ clearTimeout(timer);
50
+ if (res.ok) {
51
+ const data = await res.json();
52
+ if (Array.isArray(data?.servers)) {
53
+ return data.servers.map((s) => ({
54
+ name: s.name || s.display_name || '',
55
+ description: s.description || '',
56
+ npmPackage: s.npm_package || undefined,
57
+ source: s.repository || s.url || undefined,
58
+ stars: typeof s.stars === 'number' ? s.stars : undefined,
59
+ })).filter((s) => s.name);
60
+ }
61
+ if (Array.isArray(data)) {
62
+ return data.slice(0, limit).map((s) => ({
63
+ name: s.name || '',
64
+ description: s.description || '',
65
+ source: s.repository || s.url || undefined,
66
+ })).filter((s) => s.name);
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ clearTimeout(timer);
72
+ }
73
+ try {
74
+ const npmUrl = `https://registry.npmjs.org/-/v1/search?text=mcp+server+${encodeURIComponent(query)}&size=${limit}`;
75
+ const controller2 = new AbortController();
76
+ const timer2 = setTimeout(() => controller2.abort(), 5_000);
77
+ const res = await fetch(npmUrl, { signal: controller2.signal });
78
+ clearTimeout(timer2);
79
+ if (res.ok) {
80
+ const data = await res.json();
81
+ if (Array.isArray(data.objects)) {
82
+ return data.objects.map((o) => ({
83
+ name: o.package?.name || '',
84
+ description: o.package?.description || '',
85
+ npmPackage: o.package?.name,
86
+ })).filter((s) => s.name);
87
+ }
88
+ }
89
+ }
90
+ catch { /* fallback failed */ }
91
+ return [];
92
+ }
93
+ /**
94
+ * Search for skills via npm search (`agent skill` keywords).
95
+ */
96
+ export async function searchSkills(query, limit = 20) {
97
+ if (!query.trim())
98
+ return [];
99
+ try {
100
+ const npmUrl = `https://registry.npmjs.org/-/v1/search?text=agent+skill+${encodeURIComponent(query)}&size=${limit}`;
101
+ const controller = new AbortController();
102
+ const timer = setTimeout(() => controller.abort(), 5_000);
103
+ const res = await fetch(npmUrl, { signal: controller.signal });
104
+ clearTimeout(timer);
105
+ if (!res.ok)
106
+ return [];
107
+ const data = await res.json();
108
+ if (Array.isArray(data.objects)) {
109
+ return data.objects.map((o) => ({
110
+ name: o.package?.name || '',
111
+ description: o.package?.description || '',
112
+ source: o.package?.links?.repository || o.package?.name || '',
113
+ author: o.package?.publisher?.username,
114
+ })).filter((s) => s.name);
115
+ }
116
+ }
117
+ catch { /* unreachable */ }
118
+ return [];
119
+ }