pi-soly 1.9.3 → 1.11.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 (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,201 @@
1
+ // metadata-cache.ts - Persistent MCP metadata cache
2
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { getAgentPath } from "./agent-dir.ts";
5
+ import { createHash } from "node:crypto";
6
+ import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
7
+ import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.ts";
8
+ import { formatToolName, isToolExcluded } from "./types.ts";
9
+ import { resourceNameToToolName } from "./resource-tools.ts";
10
+ import { extractToolUiStreamMode, interpolateEnvRecord, resolveBearerToken, resolveConfigPath } from "./utils.ts";
11
+
12
+ const CACHE_VERSION = 1;
13
+ const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
+
15
+ export interface CachedTool {
16
+ name: string;
17
+ description?: string;
18
+ inputSchema?: unknown;
19
+ uiResourceUri?: string;
20
+ uiStreamMode?: "eager" | "stream-first";
21
+ }
22
+
23
+ export interface CachedResource {
24
+ uri: string;
25
+ name: string;
26
+ description?: string;
27
+ }
28
+
29
+ export interface ServerCacheEntry {
30
+ configHash: string;
31
+ tools: CachedTool[];
32
+ resources: CachedResource[];
33
+ cachedAt: number;
34
+ }
35
+
36
+ export interface MetadataCache {
37
+ version: number;
38
+ servers: Record<string, ServerCacheEntry>;
39
+ }
40
+
41
+ export function getMetadataCachePath(): string {
42
+ return getAgentPath("mcp-cache.json");
43
+ }
44
+
45
+ export function loadMetadataCache(): MetadataCache | null {
46
+ const cachePath = getMetadataCachePath();
47
+ if (!existsSync(cachePath)) return null;
48
+ try {
49
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
50
+ if (!raw || typeof raw !== "object") return null;
51
+ if (raw.version !== CACHE_VERSION) return null;
52
+ if (!raw.servers || typeof raw.servers !== "object") return null;
53
+ return raw as MetadataCache;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function saveMetadataCache(cache: MetadataCache): void {
60
+ const cachePath = getMetadataCachePath();
61
+ const dir = dirname(cachePath);
62
+ mkdirSync(dir, { recursive: true });
63
+
64
+ let merged: MetadataCache = { version: CACHE_VERSION, servers: {} };
65
+ try {
66
+ if (existsSync(cachePath)) {
67
+ const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as MetadataCache;
68
+ if (existing && existing.version === CACHE_VERSION && existing.servers) {
69
+ merged.servers = { ...existing.servers };
70
+ }
71
+ }
72
+ } catch {
73
+ // Ignore parse errors and proceed with empty cache
74
+ }
75
+
76
+ merged.version = CACHE_VERSION;
77
+ merged.servers = { ...merged.servers, ...cache.servers };
78
+
79
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
80
+ writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
81
+ renameSync(tmpPath, cachePath);
82
+ }
83
+
84
+ export function computeServerHash(definition: ServerEntry): string {
85
+ // Hash only fields that affect server identity and tool/resource output.
86
+ // Exclude lifecycle, idleTimeout, debug — those are runtime behavior settings
87
+ // that don't change which tools a server exposes.
88
+ const identity: Record<string, unknown> = {
89
+ command: definition.command,
90
+ args: definition.args,
91
+ env: interpolateEnvRecord(definition.env),
92
+ cwd: resolveConfigPath(definition.cwd),
93
+ url: definition.url,
94
+ headers: interpolateEnvRecord(definition.headers),
95
+ auth: definition.auth,
96
+ bearerToken: resolveBearerToken(definition),
97
+ bearerTokenEnv: definition.bearerTokenEnv,
98
+ exposeResources: definition.exposeResources,
99
+ excludeTools: definition.excludeTools,
100
+ };
101
+ const normalized = stableStringify(identity);
102
+ return createHash("sha256").update(normalized).digest("hex");
103
+ }
104
+
105
+ export function isServerCacheValid(
106
+ entry: ServerCacheEntry,
107
+ definition: ServerEntry,
108
+ maxAgeMs: number = CACHE_MAX_AGE_MS
109
+ ): boolean {
110
+ if (!entry || entry.configHash !== computeServerHash(definition)) return false;
111
+ if (!entry.cachedAt || typeof entry.cachedAt !== "number") return false;
112
+ if (maxAgeMs > 0 && Date.now() - entry.cachedAt > maxAgeMs) return false;
113
+ return true;
114
+ }
115
+
116
+ export function reconstructToolMetadata(
117
+ serverName: string,
118
+ entry: ServerCacheEntry,
119
+ prefix: "server" | "none" | "short",
120
+ definition: Pick<ServerEntry, "exposeResources" | "excludeTools">
121
+ ): ToolMetadata[] {
122
+ const metadata: ToolMetadata[] = [];
123
+
124
+ for (const tool of entry.tools ?? []) {
125
+ if (!tool?.name) continue;
126
+ if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
127
+ continue;
128
+ }
129
+
130
+ metadata.push({
131
+ name: formatToolName(tool.name, serverName, prefix),
132
+ originalName: tool.name,
133
+ description: tool.description ?? "",
134
+ inputSchema: tool.inputSchema,
135
+ uiResourceUri: tool.uiResourceUri,
136
+ uiStreamMode: tool.uiStreamMode,
137
+ });
138
+ }
139
+
140
+ if (definition.exposeResources !== false) {
141
+ for (const resource of entry.resources ?? []) {
142
+ if (!resource?.name || !resource?.uri) continue;
143
+ const baseName = `get_${resourceNameToToolName(resource.name)}`;
144
+ if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
145
+ continue;
146
+ }
147
+
148
+ metadata.push({
149
+ name: formatToolName(baseName, serverName, prefix),
150
+ originalName: baseName,
151
+ description: resource.description ?? `Read resource: ${resource.uri}`,
152
+ resourceUri: resource.uri,
153
+ });
154
+ }
155
+ }
156
+
157
+ return metadata;
158
+ }
159
+
160
+ export function serializeTools(tools: McpTool[]): CachedTool[] {
161
+ return tools
162
+ .filter(t => t?.name)
163
+ .map(t => ({
164
+ name: t.name,
165
+ description: t.description,
166
+ inputSchema: t.inputSchema,
167
+ uiResourceUri: tryGetToolUiResourceUri(t),
168
+ uiStreamMode: extractToolUiStreamMode(t._meta),
169
+ }));
170
+ }
171
+
172
+ export function serializeResources(resources: McpResource[]): CachedResource[] {
173
+ return resources
174
+ .filter(r => r?.name && r?.uri)
175
+ .map(r => ({
176
+ uri: r.uri,
177
+ name: r.name,
178
+ description: r.description,
179
+ }));
180
+ }
181
+
182
+ function stableStringify(value: unknown): string {
183
+ if (value === null || value === undefined || typeof value !== "object") {
184
+ const serialized = JSON.stringify(value);
185
+ return serialized === undefined ? "undefined" : serialized;
186
+ }
187
+ if (Array.isArray(value)) {
188
+ return `[${value.map(v => stableStringify(v)).join(",")}]`;
189
+ }
190
+ const obj = value as Record<string, unknown>;
191
+ const keys = Object.keys(obj).sort();
192
+ return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
193
+ }
194
+
195
+ function tryGetToolUiResourceUri(tool: McpTool): string | undefined {
196
+ try {
197
+ return getToolUiResourceUri({ _meta: tool._meta });
198
+ } catch {
199
+ return undefined;
200
+ }
201
+ }
package/mcp/notify.ts ADDED
@@ -0,0 +1,111 @@
1
+ // =============================================================================
2
+ // notify.ts — MCP-specific notification wrappers
3
+ // =============================================================================
4
+ //
5
+ // Thin layer over soly's notifyFramed() that:
6
+ // - Falls back gracefully if the UI doesn't have setWidget (print mode)
7
+ // - Adds MCP-specific keys so multiple MCP notifications don't clobber each other
8
+ // or soly's other widgets
9
+ // - Provides typed helpers for the most common MCP events: reconnect fail,
10
+ // session auto-recovery (UE5 bug workaround), tool errors
11
+ //
12
+ // Keys are namespaced (mcp-reconnect-<name>, mcp-session-recover-<name>) so
13
+ // they don't collide with soly's default "soly-notif" key.
14
+
15
+ import { notifyFramed, type NotifBg } from "../notification.ts";
16
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
17
+
18
+ const MCP_KEY_PREFIX = "mcp-";
19
+
20
+ function notifyBox(
21
+ ui: ExtensionUIContext,
22
+ key: string,
23
+ title: string,
24
+ lines: readonly string[],
25
+ bg: NotifBg,
26
+ autoClearMs = 8000,
27
+ ): void {
28
+ try {
29
+ notifyFramed(ui, title, lines, {
30
+ key: MCP_KEY_PREFIX + key,
31
+ bg,
32
+ autoClearMs,
33
+ });
34
+ } catch {
35
+ // Fallback: setWidget may fail in print mode / RPC. Use plain notify
36
+ // with a short title-only message. Don't crash the MCP handler.
37
+ const plain = lines.length > 0 ? `${title} — ${lines.join(" ")}` : title;
38
+ try {
39
+ ui.notify(plain, bg === "toolErrorBg" ? "error" : bg === "toolPendingBg" ? "warning" : "info");
40
+ } catch {
41
+ // no UI at all — silent
42
+ }
43
+ }
44
+ }
45
+
46
+ /** Reconnect failed — red box. Auto-clears after 10s (longer for errors). */
47
+ export function notifyReconnectFailed(
48
+ ui: ExtensionUIContext,
49
+ serverName: string,
50
+ error: string,
51
+ ): void {
52
+ notifyBox(
53
+ ui,
54
+ `reconnect-${serverName}`,
55
+ `🔌 MCP: ${serverName} reconnect failed`,
56
+ [error.length > 200 ? `${error.slice(0, 200)}…` : error],
57
+ "toolErrorBg",
58
+ 10_000,
59
+ );
60
+ }
61
+
62
+ /** Session auto-recovery succeeded (UE5 workaround worked). Green box. */
63
+ export function notifySessionRecovered(
64
+ ui: ExtensionUIContext,
65
+ serverName: string,
66
+ fromMethod: string,
67
+ ): void {
68
+ notifyBox(
69
+ ui,
70
+ `session-recover-${serverName}`,
71
+ `🔄 MCP: ${serverName} session auto-recovered`,
72
+ [
73
+ `${fromMethod} hit "Unknown session id"`,
74
+ `Reconnected silently — retry succeeded`,
75
+ ],
76
+ "toolSuccessBg",
77
+ 6_000,
78
+ );
79
+ }
80
+
81
+ /** Session auto-recovery failed after retry. Red box. */
82
+ export function notifySessionRecoveryFailed(
83
+ ui: ExtensionUIContext,
84
+ serverName: string,
85
+ error: string,
86
+ ): void {
87
+ notifyBox(
88
+ ui,
89
+ `session-recover-${serverName}`,
90
+ `🔄 MCP: ${serverName} session recovery failed`,
91
+ [error.length > 200 ? `${error.slice(0, 200)}…` : error],
92
+ "toolErrorBg",
93
+ 10_000,
94
+ );
95
+ }
96
+
97
+ /** Generic MCP info/warning (yellow) — for status updates like connecting. */
98
+ export function notifyMcpStatus(
99
+ ui: ExtensionUIContext,
100
+ serverName: string,
101
+ message: string,
102
+ ): void {
103
+ notifyBox(
104
+ ui,
105
+ `status-${serverName}`,
106
+ `🔌 MCP: ${serverName}`,
107
+ [message],
108
+ "toolPendingBg",
109
+ 5_000,
110
+ );
111
+ }