@docyrus/docyrus 0.0.19 → 0.0.21

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 (111) hide show
  1. package/agent-loader.js +37 -3
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +498 -93
  4. package/main.js.map +4 -4
  5. package/package.json +14 -4
  6. package/resources/chrome-tools/browser-content.js +103 -0
  7. package/resources/chrome-tools/browser-cookies.js +35 -0
  8. package/resources/chrome-tools/browser-eval.js +53 -0
  9. package/resources/chrome-tools/browser-hn-scraper.js +108 -0
  10. package/resources/chrome-tools/browser-nav.js +44 -0
  11. package/resources/chrome-tools/browser-pick.js +162 -0
  12. package/resources/chrome-tools/browser-screenshot.js +34 -0
  13. package/resources/chrome-tools/browser-start.js +86 -0
  14. package/resources/pi-agent/extensions/answer.ts +532 -0
  15. package/resources/pi-agent/extensions/context.ts +578 -0
  16. package/resources/pi-agent/extensions/control.ts +1779 -0
  17. package/resources/pi-agent/extensions/diff.ts +218 -0
  18. package/resources/pi-agent/extensions/files.ts +199 -0
  19. package/resources/pi-agent/extensions/loop.ts +446 -0
  20. package/resources/pi-agent/extensions/multi-edit.ts +835 -0
  21. package/resources/pi-agent/extensions/notify.ts +88 -0
  22. package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
  23. package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
  24. package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
  25. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
  26. package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
  27. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
  28. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
  29. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
  30. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
  54. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
  55. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
  56. package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
  57. package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
  58. package/resources/pi-agent/extensions/redraws.ts +24 -0
  59. package/resources/pi-agent/extensions/review.ts +2160 -0
  60. package/resources/pi-agent/extensions/todos.ts +2076 -0
  61. package/resources/pi-agent/extensions/tps.ts +47 -0
  62. package/resources/pi-agent/extensions/whimsical.ts +474 -0
  63. package/resources/pi-agent/prompts/coder-system.md +106 -0
  64. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  65. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  66. package/resources/pi-agent/skills/docyrus-platform/SKILL.md +71 -0
  67. package/resources/pi-agent/skills/docyrus-platform/references/ai-capabilities.md +43 -0
  68. package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +35 -0
  69. package/resources/pi-agent/skills/docyrus-platform/references/automation-and-workflows.md +30 -0
  70. package/resources/pi-agent/skills/docyrus-platform/references/core-building-blocks.md +53 -0
  71. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/data-source-query-guide.md +32 -28
  72. package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +28 -0
  73. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +554 -0
  74. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/formula-design-guide-llm.md +15 -23
  75. package/resources/pi-agent/skills/docyrus-platform/references/integrations-and-events.md +60 -0
  76. package/resources/pi-agent/skills/docyrus-platform/references/platform-services.md +58 -0
  77. package/resources/pi-agent/skills/docyrus-platform/references/querying-and-data-operations.md +27 -0
  78. package/resources/pi-agent/prompts/coder-append-system.md +0 -19
  79. package/resources/pi-agent/skills/docyrus-ai/SKILL.md +0 -28
  80. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +0 -161
  81. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +0 -349
  82. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +0 -238
  83. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +0 -592
  84. package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +0 -70
  85. package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +0 -588
  86. package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +0 -159
  87. package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +0 -275
  88. package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +0 -352
  89. package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +0 -2059
  90. package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +0 -320
  91. package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +0 -525
  92. package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +0 -466
  93. package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +0 -602
  94. package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +0 -463
  95. package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +0 -242
  96. package/resources/pi-agent/skills/docyrus-apps/SKILL.md +0 -54
  97. package/resources/pi-agent/skills/docyrus-architect/SKILL.md +0 -174
  98. package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +0 -410
  99. package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +0 -2059
  100. package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +0 -320
  101. package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +0 -145
  102. package/resources/pi-agent/skills/docyrus-auth/SKILL.md +0 -100
  103. package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +0 -279
  104. package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +0 -532
  105. package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +0 -248
  106. package/resources/pi-agent/skills/docyrus-curl/SKILL.md +0 -32
  107. package/resources/pi-agent/skills/docyrus-discover/SKILL.md +0 -63
  108. package/resources/pi-agent/skills/docyrus-ds/SKILL.md +0 -95
  109. package/resources/pi-agent/skills/docyrus-env/SKILL.md +0 -21
  110. package/resources/pi-agent/skills/docyrus-studio/SKILL.md +0 -369
  111. package/resources/pi-agent/skills/docyrus-tui/SKILL.md +0 -15
@@ -0,0 +1,319 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { McpExtensionState } from "./state.js";
3
+ import type { ToolMetadata } from "./types.js";
4
+ import { existsSync } from "node:fs";
5
+ import { loadMcpConfig } from "./config.js";
6
+ import { ConsentManager } from "./consent-manager.js";
7
+ import { McpLifecycleManager } from "./lifecycle.js";
8
+ import {
9
+ computeServerHash,
10
+ getMetadataCachePath,
11
+ isServerCacheValid,
12
+ loadMetadataCache,
13
+ reconstructToolMetadata,
14
+ saveMetadataCache,
15
+ serializeResources,
16
+ serializeTools,
17
+ type ServerCacheEntry,
18
+ } from "./metadata-cache.js";
19
+ import { McpServerManager } from "./server-manager.js";
20
+ import { buildToolMetadata, totalToolCount } from "./tool-metadata.js";
21
+ import { UiResourceHandler } from "./ui-resource-handler.js";
22
+ import { openUrl, parallelLimit } from "./utils.js";
23
+ import { logger } from "./logger.js";
24
+
25
+ const FAILURE_BACKOFF_MS = 60 * 1000;
26
+
27
+ export async function initializeMcp(
28
+ pi: ExtensionAPI,
29
+ ctx: ExtensionContext
30
+ ): Promise<McpExtensionState> {
31
+ const configPath = pi.getFlag("mcp-config") as string | undefined;
32
+ const config = loadMcpConfig(configPath);
33
+
34
+ const manager = new McpServerManager();
35
+ const lifecycle = new McpLifecycleManager(manager);
36
+ const toolMetadata = new Map<string, ToolMetadata[]>();
37
+ const failureTracker = new Map<string, number>();
38
+ const uiResourceHandler = new UiResourceHandler(manager);
39
+ const consentManager = new ConsentManager("once-per-server");
40
+ const ui = ctx.hasUI ? ctx.ui : undefined;
41
+ const state: McpExtensionState = {
42
+ manager,
43
+ lifecycle,
44
+ toolMetadata,
45
+ config,
46
+ failureTracker,
47
+ uiResourceHandler,
48
+ consentManager,
49
+ uiServer: null,
50
+ completedUiSessions: [],
51
+ openBrowser: (url: string) => openUrl(pi, url, process.env.BROWSER),
52
+ ui,
53
+ sendMessage: (message, options) => pi.sendMessage(message, options),
54
+ };
55
+
56
+ const serverEntries = Object.entries(config.mcpServers);
57
+ if (serverEntries.length === 0) {
58
+ return state;
59
+ }
60
+
61
+ const idleSetting = typeof config.settings?.idleTimeout === "number" ? config.settings.idleTimeout : 10;
62
+ lifecycle.setGlobalIdleTimeout(idleSetting);
63
+
64
+ const cachePath = getMetadataCachePath();
65
+ const cacheFileExists = existsSync(cachePath);
66
+ let cache = loadMetadataCache();
67
+ let bootstrapAll = false;
68
+
69
+ if (!cacheFileExists) {
70
+ bootstrapAll = true;
71
+ saveMetadataCache({ version: 1, servers: {} });
72
+ } else if (!cache) {
73
+ cache = { version: 1, servers: {} };
74
+ saveMetadataCache(cache);
75
+ }
76
+
77
+ const prefix = config.settings?.toolPrefix ?? "server";
78
+
79
+ for (const [name, definition] of serverEntries) {
80
+ const lifecycleMode = definition.lifecycle ?? "lazy";
81
+ const idleOverride = definition.idleTimeout ?? (lifecycleMode === "eager" ? 0 : undefined);
82
+ lifecycle.registerServer(
83
+ name,
84
+ definition,
85
+ idleOverride !== undefined ? { idleTimeout: idleOverride } : undefined
86
+ );
87
+ if (lifecycleMode === "keep-alive") {
88
+ lifecycle.markKeepAlive(name, definition);
89
+ }
90
+
91
+ if (cache?.servers?.[name] && isServerCacheValid(cache.servers[name], definition)) {
92
+ const metadata = reconstructToolMetadata(name, cache.servers[name], prefix, definition.exposeResources);
93
+ toolMetadata.set(name, metadata);
94
+ }
95
+ }
96
+
97
+ const startupServers = bootstrapAll
98
+ ? serverEntries
99
+ : serverEntries.filter(([, definition]) => {
100
+ const mode = definition.lifecycle ?? "lazy";
101
+ return mode === "keep-alive" || mode === "eager";
102
+ });
103
+
104
+ if (ctx.hasUI && startupServers.length > 0) {
105
+ ctx.ui.setStatus("mcp", `MCP: connecting to ${startupServers.length} servers...`);
106
+ }
107
+
108
+ const results = await parallelLimit(startupServers, 10, async ([name, definition]) => {
109
+ try {
110
+ const connection = await manager.connect(name, definition);
111
+ return { name, definition, connection, error: null };
112
+ } catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ return { name, definition, connection: null, error: message };
115
+ }
116
+ });
117
+
118
+ for (const { name, definition, connection, error } of results) {
119
+ if (error || !connection) {
120
+ if (ctx.hasUI) {
121
+ ctx.ui.notify(`MCP: Failed to connect to ${name}: ${error}`, "error");
122
+ }
123
+ console.error(`MCP: Failed to connect to ${name}: ${error}`);
124
+ continue;
125
+ }
126
+
127
+ const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
128
+ toolMetadata.set(name, metadata);
129
+ updateMetadataCache(state, name);
130
+
131
+ if (failedTools.length > 0 && ctx.hasUI) {
132
+ ctx.ui.notify(
133
+ `MCP: ${name} - ${failedTools.length} tools skipped`,
134
+ "warning"
135
+ );
136
+ }
137
+ }
138
+
139
+ const connectedCount = results.filter(r => r.connection).length;
140
+ const failedCount = results.filter(r => r.error).length;
141
+ if (ctx.hasUI && connectedCount > 0) {
142
+ const totalTools = totalToolCount(state);
143
+ const msg = failedCount > 0
144
+ ? `MCP: ${connectedCount}/${startupServers.length} servers connected (${totalTools} tools)`
145
+ : `MCP: ${connectedCount} servers connected (${totalTools} tools)`;
146
+ ctx.ui.notify(msg, "info");
147
+ }
148
+
149
+ const envDirect = process.env.MCP_DIRECT_TOOLS;
150
+ if (envDirect !== "__none__") {
151
+ const missingCacheServers: string[] = [];
152
+ const currentCache = loadMetadataCache();
153
+ for (const [name, definition] of serverEntries) {
154
+ const hasDirect = definition.directTools !== undefined
155
+ ? !!definition.directTools
156
+ : !!config.settings?.directTools;
157
+ if (!hasDirect) continue;
158
+ const entry = currentCache?.servers?.[name];
159
+ if (!entry || !isServerCacheValid(entry, definition)) {
160
+ missingCacheServers.push(name);
161
+ }
162
+ }
163
+
164
+ if (missingCacheServers.length > 0) {
165
+ const bootstrapResults = await parallelLimit(
166
+ missingCacheServers.filter(name => !results.some(r => r.name === name && r.connection)),
167
+ 10,
168
+ async (name) => {
169
+ const definition = config.mcpServers[name];
170
+ try {
171
+ const connection = await manager.connect(name, definition);
172
+ const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
173
+ toolMetadata.set(name, metadata);
174
+ updateMetadataCache(state, name);
175
+ return { name, ok: true };
176
+ } catch {
177
+ return { name, ok: false };
178
+ }
179
+ },
180
+ );
181
+ const bootstrapped = bootstrapResults.filter(r => r.ok).map(r => r.name);
182
+ if (bootstrapped.length > 0 && ctx.hasUI) {
183
+ ctx.ui.notify(`MCP: direct tools for ${bootstrapped.join(", ")} will be available after restart`, "info");
184
+ }
185
+ }
186
+ }
187
+
188
+ lifecycle.setReconnectCallback((serverName) => {
189
+ updateServerMetadata(state, serverName);
190
+ updateMetadataCache(state, serverName);
191
+ state.failureTracker.delete(serverName);
192
+ updateStatusBar(state);
193
+ });
194
+
195
+ lifecycle.setIdleShutdownCallback((serverName) => {
196
+ const idleMinutes = getEffectiveIdleTimeoutMinutes(state, serverName);
197
+ logger.debug(`${serverName} shut down (idle ${idleMinutes}m)`);
198
+ updateStatusBar(state);
199
+ });
200
+
201
+ lifecycle.startHealthChecks();
202
+
203
+ return state;
204
+ }
205
+
206
+ export function updateServerMetadata(state: McpExtensionState, serverName: string): void {
207
+ const connection = state.manager.getConnection(serverName);
208
+ if (!connection || connection.status !== "connected") return;
209
+
210
+ const definition = state.config.mcpServers[serverName];
211
+ if (!definition) return;
212
+
213
+ const prefix = state.config.settings?.toolPrefix ?? "server";
214
+
215
+ const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
216
+ state.toolMetadata.set(serverName, metadata);
217
+ }
218
+
219
+ export function updateMetadataCache(state: McpExtensionState, serverName: string): void {
220
+ const connection = state.manager.getConnection(serverName);
221
+ if (!connection || connection.status !== "connected") return;
222
+
223
+ const definition = state.config.mcpServers[serverName];
224
+ if (!definition) return;
225
+
226
+ const configHash = computeServerHash(definition);
227
+ const existing = loadMetadataCache();
228
+ const existingEntry = existing?.servers?.[serverName];
229
+
230
+ const tools = serializeTools(connection.tools);
231
+ let resources = definition.exposeResources === false ? [] : serializeResources(connection.resources);
232
+
233
+ if (
234
+ definition.exposeResources !== false &&
235
+ resources.length === 0 &&
236
+ existingEntry?.resources?.length &&
237
+ existingEntry.configHash === configHash
238
+ ) {
239
+ resources = existingEntry.resources;
240
+ }
241
+
242
+ const entry: ServerCacheEntry = {
243
+ configHash,
244
+ tools,
245
+ resources,
246
+ cachedAt: Date.now(),
247
+ };
248
+
249
+ saveMetadataCache({ version: 1, servers: { [serverName]: entry } });
250
+ }
251
+
252
+ export function flushMetadataCache(state: McpExtensionState): void {
253
+ for (const [name, connection] of state.manager.getAllConnections()) {
254
+ if (connection.status === "connected") {
255
+ updateMetadataCache(state, name);
256
+ }
257
+ }
258
+ }
259
+
260
+ export function updateStatusBar(state: McpExtensionState): void {
261
+ const ui = state.ui;
262
+ if (!ui) return;
263
+ const total = Object.keys(state.config.mcpServers).length;
264
+ if (total === 0) {
265
+ ui.setStatus("mcp", "");
266
+ return;
267
+ }
268
+ const connectedCount = state.manager.getAllConnections().size;
269
+ ui.setStatus("mcp", ui.theme.fg("accent", `MCP: ${connectedCount}/${total} servers`));
270
+ }
271
+
272
+ export function getFailureAgeSeconds(state: McpExtensionState, serverName: string): number | null {
273
+ const failedAt = state.failureTracker.get(serverName);
274
+ if (!failedAt) return null;
275
+ const ageMs = Date.now() - failedAt;
276
+ if (ageMs > FAILURE_BACKOFF_MS) return null;
277
+ return Math.round(ageMs / 1000);
278
+ }
279
+
280
+ export async function lazyConnect(state: McpExtensionState, serverName: string): Promise<boolean> {
281
+ const connection = state.manager.getConnection(serverName);
282
+ if (connection?.status === "connected") {
283
+ updateServerMetadata(state, serverName);
284
+ return true;
285
+ }
286
+
287
+ const failedAgo = getFailureAgeSeconds(state, serverName);
288
+ if (failedAgo !== null) return false;
289
+
290
+ const definition = state.config.mcpServers[serverName];
291
+ if (!definition) return false;
292
+
293
+ try {
294
+ if (state.ui) {
295
+ state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
296
+ }
297
+ await state.manager.connect(serverName, definition);
298
+ state.failureTracker.delete(serverName);
299
+ updateServerMetadata(state, serverName);
300
+ updateMetadataCache(state, serverName);
301
+ updateStatusBar(state);
302
+ return true;
303
+ } catch {
304
+ state.failureTracker.set(serverName, Date.now());
305
+ updateStatusBar(state);
306
+ return false;
307
+ }
308
+ }
309
+
310
+ function getEffectiveIdleTimeoutMinutes(state: McpExtensionState, serverName: string): number {
311
+ const definition = state.config.mcpServers[serverName];
312
+ if (!definition) {
313
+ return typeof state.config.settings?.idleTimeout === "number" ? state.config.settings.idleTimeout : 10;
314
+ }
315
+ if (typeof definition.idleTimeout === "number") return definition.idleTimeout;
316
+ const mode = definition.lifecycle ?? "lazy";
317
+ if (mode === "eager") return 0;
318
+ return typeof state.config.settings?.idleTimeout === "number" ? state.config.settings.idleTimeout : 10;
319
+ }
@@ -0,0 +1,93 @@
1
+ import type { ServerDefinition } from "./types.js";
2
+ import type { McpServerManager } from "./server-manager.js";
3
+ import { logger } from "./logger.js";
4
+
5
+ export type ReconnectCallback = (serverName: string) => void;
6
+
7
+ export class McpLifecycleManager {
8
+ private manager: McpServerManager;
9
+ private keepAliveServers = new Map<string, ServerDefinition>();
10
+ private allServers = new Map<string, ServerDefinition>();
11
+ private serverSettings = new Map<string, { idleTimeout?: number }>();
12
+ private globalIdleTimeout: number = 10 * 60 * 1000;
13
+ private healthCheckInterval?: NodeJS.Timeout;
14
+ private onReconnect?: ReconnectCallback;
15
+ private onIdleShutdown?: (serverName: string) => void;
16
+
17
+ constructor(manager: McpServerManager) {
18
+ this.manager = manager;
19
+ }
20
+
21
+ /**
22
+ * Set callback to be invoked after a successful auto-reconnect.
23
+ * Use this to update tool metadata when a server reconnects.
24
+ */
25
+ setReconnectCallback(callback: ReconnectCallback): void {
26
+ this.onReconnect = callback;
27
+ }
28
+
29
+ markKeepAlive(name: string, definition: ServerDefinition): void {
30
+ this.keepAliveServers.set(name, definition);
31
+ }
32
+
33
+ registerServer(name: string, definition: ServerDefinition, settings?: { idleTimeout?: number }): void {
34
+ this.allServers.set(name, definition);
35
+ if (settings?.idleTimeout !== undefined) {
36
+ this.serverSettings.set(name, settings);
37
+ }
38
+ }
39
+
40
+ setGlobalIdleTimeout(minutes: number): void {
41
+ this.globalIdleTimeout = minutes * 60 * 1000;
42
+ }
43
+
44
+ setIdleShutdownCallback(callback: (serverName: string) => void): void {
45
+ this.onIdleShutdown = callback;
46
+ }
47
+
48
+ startHealthChecks(intervalMs = 30000): void {
49
+ this.healthCheckInterval = setInterval(() => {
50
+ this.checkConnections();
51
+ }, intervalMs);
52
+ this.healthCheckInterval.unref();
53
+ }
54
+
55
+ private async checkConnections(): Promise<void> {
56
+ for (const [name, definition] of this.keepAliveServers) {
57
+ const connection = this.manager.getConnection(name);
58
+
59
+ if (!connection || connection.status !== "connected") {
60
+ try {
61
+ await this.manager.connect(name, definition);
62
+ logger.debug(`Reconnected to ${name}`);
63
+ // Notify extension to update metadata
64
+ this.onReconnect?.(name);
65
+ } catch (error) {
66
+ console.error(`MCP: Failed to reconnect to ${name}:`, error);
67
+ }
68
+ }
69
+ }
70
+
71
+ for (const [name] of this.allServers) {
72
+ if (this.keepAliveServers.has(name)) continue;
73
+ const timeout = this.getIdleTimeout(name);
74
+ if (timeout > 0 && this.manager.isIdle(name, timeout)) {
75
+ await this.manager.close(name);
76
+ this.onIdleShutdown?.(name);
77
+ }
78
+ }
79
+ }
80
+
81
+ private getIdleTimeout(name: string): number {
82
+ const perServer = this.serverSettings.get(name)?.idleTimeout;
83
+ if (perServer !== undefined) return perServer * 60 * 1000;
84
+ return this.globalIdleTimeout;
85
+ }
86
+
87
+ async gracefulShutdown(): Promise<void> {
88
+ if (this.healthCheckInterval) {
89
+ clearInterval(this.healthCheckInterval);
90
+ }
91
+ await this.manager.closeAll();
92
+ }
93
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Centralized logging for MCP UI operations.
3
+ * Provides structured, contextual logs with levels.
4
+ */
5
+
6
+ export type LogLevel = "debug" | "info" | "warn" | "error";
7
+
8
+ export interface LogContext {
9
+ server?: string;
10
+ session?: string;
11
+ tool?: string;
12
+ uri?: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export interface LogEntry {
17
+ level: LogLevel;
18
+ message: string;
19
+ context?: LogContext;
20
+ error?: Error;
21
+ timestamp: Date;
22
+ }
23
+
24
+ type LogHandler = (entry: LogEntry) => void;
25
+
26
+ const LEVEL_PRIORITY: Record<LogLevel, number> = {
27
+ debug: 0,
28
+ info: 1,
29
+ warn: 2,
30
+ error: 3,
31
+ };
32
+
33
+ const LEVEL_PREFIX: Record<LogLevel, string> = {
34
+ debug: "[MCP-UI:DEBUG]",
35
+ info: "[MCP-UI]",
36
+ warn: "[MCP-UI:WARN]",
37
+ error: "[MCP-UI:ERROR]",
38
+ };
39
+
40
+ class Logger {
41
+ private minLevel: LogLevel = "info";
42
+ private handlers: LogHandler[] = [];
43
+ private defaultContext: LogContext = {};
44
+
45
+ setLevel(level: LogLevel): void {
46
+ this.minLevel = level;
47
+ }
48
+
49
+ setDefaultContext(context: LogContext): void {
50
+ this.defaultContext = context;
51
+ }
52
+
53
+ addHandler(handler: LogHandler): void {
54
+ this.handlers.push(handler);
55
+ }
56
+
57
+ clearHandlers(): void {
58
+ this.handlers = [];
59
+ }
60
+
61
+ private shouldLog(level: LogLevel): boolean {
62
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.minLevel];
63
+ }
64
+
65
+ private emit(level: LogLevel, message: string, context?: LogContext, error?: Error): void {
66
+ if (!this.shouldLog(level)) return;
67
+
68
+ const entry: LogEntry = {
69
+ level,
70
+ message,
71
+ context: { ...this.defaultContext, ...context },
72
+ error,
73
+ timestamp: new Date(),
74
+ };
75
+
76
+ // Default console output
77
+ const prefix = LEVEL_PREFIX[level];
78
+ const contextStr = formatContext(entry.context);
79
+ const fullMessage = contextStr ? `${prefix} ${message} ${contextStr}` : `${prefix} ${message}`;
80
+
81
+ if (level === "error") {
82
+ console.error(fullMessage, error ?? "");
83
+ } else if (level === "warn") {
84
+ console.warn(fullMessage);
85
+ } else if (level === "debug") {
86
+ console.debug(fullMessage);
87
+ } else {
88
+ console.log(fullMessage);
89
+ }
90
+
91
+ // Custom handlers
92
+ for (const handler of this.handlers) {
93
+ try {
94
+ handler(entry);
95
+ } catch {
96
+ // Ignore handler errors
97
+ }
98
+ }
99
+ }
100
+
101
+ debug(message: string, context?: LogContext): void {
102
+ this.emit("debug", message, context);
103
+ }
104
+
105
+ info(message: string, context?: LogContext): void {
106
+ this.emit("info", message, context);
107
+ }
108
+
109
+ warn(message: string, context?: LogContext): void {
110
+ this.emit("warn", message, context);
111
+ }
112
+
113
+ error(message: string, error?: Error, context?: LogContext): void {
114
+ this.emit("error", message, context, error);
115
+ }
116
+
117
+ /**
118
+ * Create a child logger with additional default context.
119
+ */
120
+ child(context: LogContext): ChildLogger {
121
+ return new ChildLogger(this, context);
122
+ }
123
+ }
124
+
125
+ class ChildLogger {
126
+ constructor(
127
+ private parent: Logger,
128
+ private context: LogContext
129
+ ) {}
130
+
131
+ debug(message: string, context?: LogContext): void {
132
+ this.parent.debug(message, { ...this.context, ...context });
133
+ }
134
+
135
+ info(message: string, context?: LogContext): void {
136
+ this.parent.info(message, { ...this.context, ...context });
137
+ }
138
+
139
+ warn(message: string, context?: LogContext): void {
140
+ this.parent.warn(message, { ...this.context, ...context });
141
+ }
142
+
143
+ error(message: string, error?: Error, context?: LogContext): void {
144
+ this.parent.error(message, error, { ...this.context, ...context });
145
+ }
146
+
147
+ child(context: LogContext): ChildLogger {
148
+ return new ChildLogger(this.parent, { ...this.context, ...context });
149
+ }
150
+ }
151
+
152
+ function formatContext(context?: LogContext): string {
153
+ if (!context || Object.keys(context).length === 0) return "";
154
+ const parts: string[] = [];
155
+ for (const [key, value] of Object.entries(context)) {
156
+ if (value !== undefined && value !== null) {
157
+ parts.push(`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
158
+ }
159
+ }
160
+ return parts.length > 0 ? `(${parts.join(", ")})` : "";
161
+ }
162
+
163
+ // Singleton instance
164
+ export const logger = new Logger();
165
+
166
+ // Enable debug mode via environment variable
167
+ if (process.env.MCP_UI_DEBUG === "1" || process.env.MCP_UI_DEBUG === "true") {
168
+ logger.setLevel("debug");
169
+ }