@herbertgao/pi-extensions 2026.9.11 → 2026.9.13

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 (192) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  5. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  6. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  8. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  9. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  12. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  13. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  14. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  15. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  17. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  20. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  21. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  22. package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
  23. package/node_modules/pi-mcp-adapter/README.md +75 -7
  24. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
  25. package/node_modules/pi-mcp-adapter/cli.js +56 -5
  26. package/node_modules/pi-mcp-adapter/commands.ts +96 -5
  27. package/node_modules/pi-mcp-adapter/config.ts +73 -6
  28. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
  29. package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
  30. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
  31. package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
  32. package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
  33. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
  34. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
  35. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
  36. package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
  37. package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
  38. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
  40. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
  41. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
  42. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
  43. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
  44. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
  45. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
  46. package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
  47. package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
  48. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
  49. package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
  50. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
  51. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
  52. package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
  53. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
  54. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
  55. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
  56. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
  57. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
  58. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
  59. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
  60. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
  61. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
  62. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
  63. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
  64. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
  65. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
  66. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
  67. package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
  68. package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
  69. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
  70. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
  71. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
  72. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
  73. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
  74. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
  75. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
  76. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
  77. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
  78. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
  79. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
  80. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
  81. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
  82. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
  83. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
  84. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
  85. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
  86. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
  87. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
  88. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
  89. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
  90. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
  91. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
  92. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
  93. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
  94. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
  95. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  97. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
  98. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
  99. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
  100. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
  101. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
  102. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
  103. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
  104. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
  105. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
  106. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
  107. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
  108. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
  109. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
  110. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
  111. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
  112. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
  113. package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
  114. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
  115. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
  116. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
  117. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
  118. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
  119. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
  120. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
  121. package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
  122. package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
  123. package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
  124. package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
  125. package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
  126. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  127. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
  128. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
  129. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
  130. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
  131. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
  132. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
  133. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
  134. package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
  135. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  136. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
  137. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
  138. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
  139. package/node_modules/pi-mcp-adapter/index.ts +129 -6
  140. package/node_modules/pi-mcp-adapter/init.ts +6 -19
  141. package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
  142. package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
  143. package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
  144. package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
  145. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
  146. package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
  147. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
  148. package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
  149. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
  150. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
  151. package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
  152. package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
  153. package/node_modules/pi-mcp-adapter/package.json +17 -9
  154. package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
  155. package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
  156. package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
  157. package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
  158. package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
  159. package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
  160. package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
  161. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
  162. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  163. package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
  164. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
  165. package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
  166. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
  167. package/node_modules/pi-mcp-adapter/types.ts +53 -0
  168. package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
  169. package/node_modules/pi-mcp-adapter/utils.ts +16 -0
  170. package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
  171. package/node_modules/pi-multi-account/README.md +38 -11
  172. package/node_modules/pi-multi-account/index.ts +371 -96
  173. package/node_modules/pi-multi-account/package.json +5 -4
  174. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  175. package/node_modules/pi-multi-account/usage.ts +31 -2
  176. package/node_modules/pi-typesafe/README.md +3 -1
  177. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  178. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  179. package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
  180. package/node_modules/pi-typesafe/dist/backends.js +33 -0
  181. package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
  182. package/node_modules/pi-typesafe/dist/client.js +63 -39
  183. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  184. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  185. package/node_modules/pi-typesafe/dist/extension.js +4 -1
  186. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  187. package/node_modules/pi-typesafe/dist/index.js +1 -1
  188. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  189. package/node_modules/pi-typesafe/dist/login.js +17 -8
  190. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  191. package/node_modules/pi-typesafe/package.json +1 -1
  192. package/package.json +11 -10
@@ -11,6 +11,7 @@ import {
11
11
  import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
12
12
  import type { JsonSchemaType } from "@modelcontextprotocol/client";
13
13
  import open from "open";
14
+ import { abortable, throwIfAborted } from "./abort.ts";
14
15
 
15
16
  export type ElicitationValue = string | number | boolean | string[] | undefined;
16
17
  type FormProperty = ElicitRequestFormParams["requestedSchema"]["properties"][string];
@@ -35,49 +36,52 @@ export function registerElicitationHandler(client: Client, options: ElicitationH
35
36
  export async function handleElicitationRequest(
36
37
  options: ElicitationHandlerOptions,
37
38
  request: ElicitRequest,
39
+ signal?: AbortSignal,
38
40
  ): Promise<ElicitResult> {
41
+ throwIfAborted(signal);
39
42
  return request.params.mode === "url"
40
- ? handleUrlElicitation(options, request.params)
41
- : handleFormElicitation(options, request.params);
43
+ ? handleUrlElicitation(options, request.params, signal)
44
+ : handleFormElicitation(options, request.params, signal);
42
45
  }
43
46
 
44
47
  export async function handleFormElicitation(
45
48
  options: ElicitationHandlerOptions,
46
49
  params: ElicitRequestFormParams,
50
+ signal?: AbortSignal,
47
51
  ): Promise<ElicitResult> {
48
52
  const properties = Object.entries(params.requestedSchema.properties);
49
- const decision = await options.ui.select(
53
+ const decision = await abortable(options.ui.select(
50
54
  `MCP Input Request\nServer: ${options.serverName}\n\n${params.message}`,
51
55
  ["Continue", "Decline"],
52
- );
56
+ ), signal);
53
57
  if (decision === undefined) return { action: "cancel" };
54
58
  if (decision === "Decline") return { action: "decline" };
55
59
  if (properties.length === 0) return { action: "accept", content: {} };
56
60
 
57
61
  const values: Record<string, ElicitationValue> = {};
58
62
  for (const [name, schema] of properties) {
59
- const value = await collectValidField(options.ui, params, name, schema);
63
+ const value = await collectValidField(options.ui, params, name, schema, undefined, signal);
60
64
  if (value.status === "cancelled") return { action: "cancel" };
61
65
  values[name] = value.value;
62
66
  }
63
67
 
64
68
  while (true) {
65
69
  const content = coerceAndValidateFormValues(params, values);
66
- const action = await options.ui.select(
70
+ const action = await abortable(options.ui.select(
67
71
  formatReview(options.serverName, properties, content),
68
72
  ["Submit", "Edit", "Decline"],
69
- );
73
+ ), signal);
70
74
  if (action === undefined) return { action: "cancel" };
71
75
  if (action === "Decline") return { action: "decline" };
72
76
  if (action === "Submit") return { action: "accept", content };
73
77
 
74
78
  const labels = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)} (${name})`);
75
- const selected = await options.ui.select("Choose a field to edit", labels);
79
+ const selected = await abortable(options.ui.select("Choose a field to edit", labels), signal);
76
80
  if (selected === undefined) return { action: "cancel" };
77
81
  const property = properties[labels.indexOf(selected)];
78
82
  if (!property) continue;
79
83
  const [name, schema] = property;
80
- const value = await collectValidField(options.ui, params, name, schema, values[name]);
84
+ const value = await collectValidField(options.ui, params, name, schema, values[name], signal);
81
85
  if (value.status === "cancelled") return { action: "cancel" };
82
86
  values[name] = value.value;
83
87
  }
@@ -89,10 +93,11 @@ async function collectValidField(
89
93
  name: string,
90
94
  schema: FormProperty,
91
95
  current?: ElicitationValue,
96
+ signal?: AbortSignal,
92
97
  ): Promise<FieldCollectionResult> {
93
98
  const required = params.requestedSchema.required?.includes(name) === true;
94
99
  while (true) {
95
- const result = await collectField(ui, params, name, schema, current);
100
+ const result = await collectField(ui, params, name, schema, current, signal);
96
101
  if (result.status === "cancelled") return result;
97
102
  try {
98
103
  coerceAndValidateFormValues({
@@ -117,6 +122,7 @@ async function collectField(
117
122
  name: string,
118
123
  schema: FormProperty,
119
124
  current?: ElicitationValue,
125
+ signal?: AbortSignal,
120
126
  ): Promise<FieldCollectionResult> {
121
127
  const required = params.requestedSchema.required?.includes(name) === true;
122
128
  const title = [schema.title ?? humanizeName(name), required ? "(required)" : "", schema.description]
@@ -136,7 +142,7 @@ async function collectField(
136
142
  if (useDefault) actions.push(useDefault);
137
143
  const omit = required ? undefined : uniqueAction("Omit", actions);
138
144
  if (omit) actions.push(omit);
139
- const action = await ui.select(title, actions);
145
+ const action = await abortable(ui.select(title, actions), signal);
140
146
  if (action === undefined) return { status: "cancelled" };
141
147
  if (action === useDefault) return { status: "collected", value: schema.default };
142
148
  if (action === omit) return { status: "collected", value: undefined };
@@ -147,7 +153,7 @@ async function collectField(
147
153
  const actions = ["Yes", "No"];
148
154
  if (schema.default !== undefined) actions.push("Use default");
149
155
  if (!required) actions.push("Omit");
150
- const action = await ui.select(title, actions);
156
+ const action = await abortable(ui.select(title, actions), signal);
151
157
  if (action === undefined) return { status: "cancelled" };
152
158
  if (action === "Use default") return { status: "collected", value: schema.default };
153
159
  if (action === "Omit") return { status: "collected", value: undefined };
@@ -158,7 +164,7 @@ async function collectField(
158
164
  const actions = ["Choose values"];
159
165
  if (schema.default !== undefined) actions.push("Use default");
160
166
  if (!required) actions.push("Omit");
161
- const action = await ui.select(title, actions);
167
+ const action = await abortable(ui.select(title, actions), signal);
162
168
  if (action === undefined) return { status: "cancelled" };
163
169
  if (action === "Use default") return { status: "collected", value: schema.default };
164
170
  if (action === "Omit") return { status: "collected", value: undefined };
@@ -168,7 +174,7 @@ async function collectField(
168
174
  while (true) {
169
175
  const displays = uniqueLabels(choices.map(choice => selected.has(choice.value) ? `✓ ${choice.display}` : choice.display));
170
176
  const done = uniqueAction("Done", displays);
171
- const picked = await ui.select(title, [...displays, done]);
177
+ const picked = await abortable(ui.select(title, [...displays, done]), signal);
172
178
  if (picked === undefined) return { status: "cancelled" };
173
179
  if (picked === done) return { status: "collected", value: [...selected] };
174
180
  const choice = choices[displays.indexOf(picked)];
@@ -181,11 +187,11 @@ async function collectField(
181
187
  const actions = ["Enter value"];
182
188
  if (schema.default !== undefined) actions.push("Use default");
183
189
  if (!required) actions.push("Omit");
184
- const action = await ui.select(title, actions);
190
+ const action = await abortable(ui.select(title, actions), signal);
185
191
  if (action === undefined) return { status: "cancelled" };
186
192
  if (action === "Use default") return { status: "collected", value: schema.default };
187
193
  if (action === "Omit") return { status: "collected", value: undefined };
188
- const entered = await ui.input(title, current === undefined ? undefined : String(current));
194
+ const entered = await abortable(ui.input(title, current === undefined ? undefined : String(current)), signal);
189
195
  return entered === undefined ? { status: "cancelled" } : { status: "collected", value: entered };
190
196
  }
191
197
 
@@ -305,7 +311,9 @@ function formatReview(
305
311
  export async function handleUrlElicitation(
306
312
  options: ElicitationHandlerOptions,
307
313
  params: ElicitRequestURLParams,
314
+ signal?: AbortSignal,
308
315
  ): Promise<ElicitResult> {
316
+ throwIfAborted(signal);
309
317
  if (!options.allowUrl) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation is not supported");
310
318
 
311
319
  let parsed: URL;
@@ -318,7 +326,7 @@ export async function handleUrlElicitation(
318
326
  throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation only supports HTTP and HTTPS URLs");
319
327
  }
320
328
 
321
- const decision = await options.ui.select([
329
+ const decision = await abortable(options.ui.select([
322
330
  "MCP Browser Request",
323
331
  `Server: ${options.serverName}`,
324
332
  "",
@@ -328,16 +336,18 @@ export async function handleUrlElicitation(
328
336
  `Full URL: ${params.url}`,
329
337
  "",
330
338
  "Open this URL in your browser?",
331
- ].join("\n"), ["Open", "Decline"]);
339
+ ].join("\n"), ["Open", "Decline"]), signal);
332
340
  if (decision === undefined) return { action: "cancel" };
333
341
  if (decision === "Decline") return { action: "decline" };
334
342
 
335
343
  try {
336
- await open(params.url);
344
+ await abortable(open(params.url), signal);
337
345
  } catch (error) {
346
+ throwIfAborted(signal);
338
347
  options.ui.notify(`Could not open MCP elicitation URL: ${error instanceof Error ? error.message : String(error)}`, "error");
339
348
  return { action: "cancel" };
340
349
  }
350
+ throwIfAborted(signal);
341
351
  options.onUrlAccepted?.(params.elicitationId);
342
352
  options.ui.notify("Opened browser for MCP elicitation.", "info");
343
353
  return { action: "accept" };
@@ -0,0 +1,85 @@
1
+ const SAFE_OPERATIONS = new Set(["focus", "scroll"]);
2
+
3
+ export async function runAccessibilityLoop(tools, jev, options) {
4
+ const { observePath, actionPath, goal, allowedOperations, sources, maxSteps, maxMs, maxEvaluations } = options;
5
+ if (typeof observePath !== "string" || !observePath || typeof actionPath !== "string" || !actionPath || typeof goal !== "string" || !goal
6
+ || !Array.isArray(allowedOperations) || allowedOperations.length === 0 || allowedOperations.some(operation => !SAFE_OPERATIONS.has(operation))) {
7
+ throw new Error("Exact tool paths, a goal, and non-destructive focus/scroll operations are required");
8
+ }
9
+ if (![maxSteps, maxMs, maxEvaluations].every(value => Number.isInteger(value) && value > 0)) {
10
+ throw new Error("Positive step, time, and evaluation budgets are required");
11
+ }
12
+ if (!Array.isArray(sources) || sources.length === 0 || sources.some(source => typeof source !== "string" || !source)) {
13
+ throw new Error("Explicit MCP sources are required for accessibility data");
14
+ }
15
+ const startedAt = Date.now();
16
+ let evaluations = 0;
17
+ let previousFingerprint;
18
+
19
+ const observe = async () => {
20
+ const result = await tools.call(observePath, {});
21
+ if (!result.ok) return { error: "observation-failed" };
22
+ const tree = result.data?.structuredContent;
23
+ if (!tree || typeof tree.observationId !== "string" || !tree.observationId || !Array.isArray(tree.nodes)) {
24
+ return { error: "needs-information" };
25
+ }
26
+ return { tree };
27
+ };
28
+
29
+ for (let step = 0; step < maxSteps; step += 1) {
30
+ if (Date.now() - startedAt >= maxMs) return { status: "stop", reason: "time-budget" };
31
+ const snapshot = await observe();
32
+ if (snapshot.error) return { status: snapshot.error === "needs-information" ? "needs-information" : "stop", reason: snapshot.error };
33
+ const fingerprint = JSON.stringify(snapshot.tree.nodes);
34
+ if (fingerprint === previousFingerprint) return { status: "stop", reason: "no-progress" };
35
+ previousFingerprint = fingerprint;
36
+
37
+ const actions = snapshot.tree.nodes.flatMap(node =>
38
+ (node && typeof node.id === "string" && node.id && Array.isArray(node.actions) ? node.actions : [])
39
+ .filter(operation => typeof operation === "string" && allowedOperations.includes(operation))
40
+ .map(operation => ({ target: node.id, operation })),
41
+ ).slice(0, 120).map((action, index) => ({ ...action, label: `a${index}` }));
42
+ if (actions.length === 0) return { status: "no-match" };
43
+ if (evaluations >= maxEvaluations) return { status: "stop", reason: "evaluation-budget" };
44
+ evaluations += 1;
45
+ const evaluation = await jev.evaluate({
46
+ state: { goal, observationId: snapshot.tree.observationId, nodes: snapshot.tree.nodes },
47
+ sources,
48
+ questions: {
49
+ next: {
50
+ type: "choice",
51
+ instructions: "Choose complete only when the observation proves the goal. Otherwise choose one safe action, none, or needsInformation.",
52
+ criteria: Object.fromEntries([
53
+ ...actions.map(action => [action.label, { target: action.target, operation: action.operation }]),
54
+ ["complete", "Goal is observably complete"],
55
+ ["none", "No matching safe action"],
56
+ ["needsInformation", "User information is required"],
57
+ ]),
58
+ },
59
+ },
60
+ });
61
+ if (!evaluation.ok) return { status: "stop", reason: evaluation.error.code };
62
+ const answer = evaluation.data.answers.next;
63
+ if (answer.type !== "choice" || answer.confidence < 0.85) return { status: "stop", reason: "uncertain" };
64
+ if (answer.choice === "complete") return { status: "complete", observationId: snapshot.tree.observationId };
65
+ if (answer.choice === "none") return { status: "no-match" };
66
+ if (answer.choice === "needsInformation") return { status: "needs-information" };
67
+ const selected = actions.find(action => action.label === answer.choice);
68
+ if (!selected) return { status: "stop", reason: "invalid-action" };
69
+
70
+ if (Date.now() - startedAt >= maxMs) return { status: "stop", reason: "time-budget" };
71
+ const fresh = await observe();
72
+ if (fresh.error || fresh.tree.observationId === snapshot.tree.observationId) return { status: "stop", reason: "stale-candidate" };
73
+ const target = fresh.tree.nodes.find(node => node.id === selected.target);
74
+ if (!target || !Array.isArray(target.actions) || !target.actions.includes(selected.operation)) {
75
+ return { status: "stop", reason: "stale-candidate" };
76
+ }
77
+ const acted = await tools.call(actionPath, {
78
+ operation: selected.operation,
79
+ target: selected.target,
80
+ observationId: fresh.tree.observationId,
81
+ });
82
+ if (!acted.ok) return { status: "stop", reason: "action-failed-or-uncertain" };
83
+ }
84
+ return { status: "stop", reason: "step-budget" };
85
+ }
@@ -0,0 +1,34 @@
1
+ export async function filterIssuesForTriage(tools, jev) {
2
+ const found = await tools.search({
3
+ query: "list open customer issue reports",
4
+ server: "github",
5
+ searchMode: "semantic",
6
+ limit: 5,
7
+ });
8
+ const tool = found.items?.[0];
9
+ if (!tool) return { status: "no-match" };
10
+
11
+ const listed = await tools.call(tool.path, { state: "open", limit: 20 });
12
+ if (!listed.ok) return { status: "stop", error: listed.error };
13
+ const issues = listed.data?.structuredContent?.issues;
14
+ if (!Array.isArray(issues) || issues.length === 0) return { status: "no-match" };
15
+
16
+ const candidates = issues.slice(0, 20).map((issue, index) => ({
17
+ id: `issue${index}`,
18
+ title: String(issue.title ?? ""),
19
+ body: String(issue.body ?? "").slice(0, 2000),
20
+ }));
21
+ const questions = Object.fromEntries(candidates.flatMap(issue => [
22
+ [`relevant_${issue.id}`, { type: "noul", instructions: "Is this issue actionable and relevant?", criteria: { true: "Relevant", false: "Not relevant" } }],
23
+ [`regression_${issue.id}`, { type: "noul", instructions: "Does this issue contain evidence of a regression?", criteria: { true: "Regression", false: "No regression evidence" } }],
24
+ ]));
25
+ const evaluation = await jev.evaluate({ state: { candidates }, questions, sources: ["github"] });
26
+ if (!evaluation.ok) return { status: "stop", error: evaluation.error };
27
+
28
+ const useful = candidates.filter(issue => {
29
+ const relevant = evaluation.data.answers[`relevant_${issue.id}`];
30
+ const regression = evaluation.data.answers[`regression_${issue.id}`];
31
+ return relevant?.type === "noul" && regression?.type === "noul" && relevant.noul >= 0.8 && regression.noul >= 0.7;
32
+ });
33
+ return useful.length > 0 ? { status: "matched", issues: useful } : { status: "no-match" };
34
+ }
@@ -1,17 +1,19 @@
1
1
  import { withFileMutationQueue, type AgentToolUpdateCallback, type ExtensionAPI, type ExtensionContext, type ToolInfo } from "@earendil-works/pi-coding-agent";
2
2
  import { resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import type { McpExtensionState } from "./state.ts";
4
6
  import { isServerDisabled, type DirectToolSpec, type McpAdapterOptions, type McpConfig, type PromptMetadata, type ServerEntry } from "./types.ts";
5
7
  import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
6
8
  import { Type } from "typebox";
7
9
  import type { TSchema } from "typebox";
8
10
  import { cloneMcpConfig, discoverConfiguredClaudePluginSkills, getPiGlobalConfigPath, getProjectConfigPath, loadMcpConfig, resolveConfiguredClaudePluginMcp, writeProjectServerDisabledOverride, writeSharedServerEntry } from "./config.ts";
9
- import { buildProxyDescription, getMissingConfiguredDirectToolServers, prepareDirectToolArguments, resolveDirectTools } from "./direct-tool-surface.ts";
11
+ import { buildProxyDescription, getLargeDirectToolsAdvisory, getMissingConfiguredDirectToolServers, prepareDirectToolArguments, resolveDirectTools } from "./direct-tool-surface.ts";
10
12
  import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
11
13
  import { computeServerHash, isServerCacheValid, loadMetadataCache, parseDirectToolSelectors, type MetadataCache } from "./metadata-cache.ts";
12
14
  import { createPromptCommand, resolveCachedPrompts } from "./prompts.ts";
13
15
  import { logger } from "./logger.ts";
14
- import { formatTerminalError, getConfigPathFromArgv, normalizeDirectToolInputSchema, truncateAtWord } from "./utils.ts";
16
+ import { formatMcpFooterStatus, formatTerminalError, getConfigPathFromArgv, normalizeDirectToolInputSchema, truncateAtWord } from "./utils.ts";
15
17
  import { createMcpDirectToolCallRenderer, createMcpProxyToolCallRenderer, createMcpScriptToolCallRenderer, createMcpToolResultRenderer, resolveMcpToolRenderOptions } from "./tool-result-renderer.ts";
16
18
  import { toolErrorOverride } from "./error-signal.ts";
17
19
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError, type McpRuntimeOwner } from "./runtime-owner.ts";
@@ -348,6 +350,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
348
350
  let proxyToolRegistered = false;
349
351
  let proxyToolDescription: string | null = null;
350
352
  let directToolsFrozen = false;
353
+ let largeDirectToolsAdvisoryDelivered = false;
351
354
  // Session/runtime scoped server registrations from other extensions. They
352
355
  // survive session restarts within this install and die with the process.
353
356
  const runtimeServers = new Map<string, { definition: ServerEntry; entry: ServerEntry }>();
@@ -605,6 +608,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
605
608
  }
606
609
 
607
610
  function syncToolSurface(ctx?: ExtensionContext): void {
611
+ const notificationGeneration = lifecycleGeneration;
612
+ const notificationOwner = currentOwner;
608
613
  const config = state?.config ?? earlyConfig;
609
614
  const cache = loadToolSurfaceCache(config);
610
615
  const result = syncDirectTools(config, cache);
@@ -618,6 +623,13 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
618
623
  }
619
624
  syncProxyTool(config, cache, result.specs);
620
625
  syncNamespaceTools(config, cache, result.reservedDirectNames, result.activeDirectNames);
626
+ deliverLargeDirectToolsAdvisory(ctx, config, result.specs);
627
+ if (ctx && (notificationGeneration !== lifecycleGeneration
628
+ || notificationOwner !== currentOwner
629
+ || (notificationOwner && !notificationOwner.isActive()))) {
630
+ throw notificationOwner?.signal.reason ?? new Error("Stale MCP session after direct-tools advisory");
631
+ }
632
+ finalizationGuard?.();
621
633
  const changed = result.added.length + result.updated.length + result.deactivated.length;
622
634
  if (changed > 0 && ctx?.hasUI) {
623
635
  callReentrant(() => ctx.ui.notify(
@@ -627,6 +639,49 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
627
639
  }
628
640
  }
629
641
 
642
+ function deliverLargeDirectToolsAdvisory(
643
+ ctx: ExtensionContext | undefined,
644
+ config: McpConfig,
645
+ specs: readonly DirectToolSpec[],
646
+ ): void {
647
+ if (!ctx || largeDirectToolsAdvisoryDelivered) return;
648
+ const message = getLargeDirectToolsAdvisory(config, specs);
649
+ if (!message) return;
650
+ largeDirectToolsAdvisoryDelivered = true;
651
+ if (ctx.hasUI) {
652
+ callReentrant(() => ctx.ui.notify(message, "warning"));
653
+ } else {
654
+ console.warn(message);
655
+ }
656
+ }
657
+
658
+ function getDeferredSessionSnapshot(cwd: string | undefined): {
659
+ config: McpConfig;
660
+ specs: DirectToolSpec[];
661
+ enabledServerCount: number;
662
+ } | undefined {
663
+ const config = programmaticConfig
664
+ ? resolveConfiguredClaudePluginMcp(cloneMcpConfig(sessionConfig), cwd ?? process.cwd())
665
+ : loadMcpConfig(earlyConfigPath, cwd);
666
+ const cache = loadMetadataCache();
667
+ const enabledServers = Object.entries(config.mcpServers)
668
+ .filter(([, definition]) => !isServerDisabled(definition));
669
+ if (enabledServers.some(([, definition]) => definition.lifecycle === "eager" || definition.lifecycle === "keep-alive")) {
670
+ return undefined;
671
+ }
672
+ if (!cache || !enabledServers.every(([serverName, definition]) => {
673
+ const entry = cache.servers[serverName];
674
+ return entry !== undefined && isServerCacheValid(entry, definition);
675
+ })) return undefined;
676
+ if (envRaw !== undefined && envRaw !== "__none__"
677
+ && getMissingConfiguredDirectToolServers(config, cache, envDirectToolOverride).length > 0) return undefined;
678
+ return {
679
+ config,
680
+ specs: resolveCurrentDirectTools(config, cache),
681
+ enabledServerCount: enabledServers.length,
682
+ };
683
+ }
684
+
630
685
  function syncNamespaceTools(
631
686
  config: McpConfig,
632
687
  cache: MetadataCache | null,
@@ -1016,11 +1071,18 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1016
1071
  ? cloneMcpConfig(sessionConfig)
1017
1072
  : loadMcpConfig(earlyConfigPath, event.cwd);
1018
1073
  const skillPaths = discoverConfiguredClaudePluginSkills(resourceConfig, event.cwd);
1074
+ if (earlyConfig.settings?.scriptMode !== false) {
1075
+ const scriptingSkillPath = fileURLToPath(new URL("./skills/mcp-scripting/SKILL.md", import.meta.url));
1076
+ if (existsSync(scriptingSkillPath) && !skillPaths.includes(scriptingSkillPath)) {
1077
+ skillPaths.push(scriptingSkillPath);
1078
+ }
1079
+ }
1019
1080
  return skillPaths.length > 0 ? { skillPaths } : undefined;
1020
1081
  });
1021
1082
 
1022
1083
  pi.on("session_start", async (_event, ctx) => {
1023
1084
  const generation = ++lifecycleGeneration;
1085
+ largeDirectToolsAdvisoryDelivered = false;
1024
1086
  const previousState = state;
1025
1087
  const previousOwner = currentOwner;
1026
1088
  const previousOAuthRuntime = currentOAuthRuntime;
@@ -1049,7 +1111,24 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1049
1111
  if (state) return;
1050
1112
 
1051
1113
  if (!initPromise) {
1052
- if (deferSessionRuntime) return;
1114
+ const deferredSnapshot = deferSessionRuntime ? getDeferredSessionSnapshot(ctx.cwd) : undefined;
1115
+ if (deferredSnapshot) {
1116
+ deliverLargeDirectToolsAdvisory(ctx, deferredSnapshot.config, deferredSnapshot.specs);
1117
+ if (generation !== lifecycleGeneration || !owner.isActive() || currentOwner !== owner) return;
1118
+ const serverCount = Object.keys(deferredSnapshot.config.mcpServers).length;
1119
+ const formattedStatus = formatMcpFooterStatus(
1120
+ deferredSnapshot.config,
1121
+ deferredSnapshot.enabledServerCount,
1122
+ serverCount - deferredSnapshot.enabledServerCount,
1123
+ 0,
1124
+ );
1125
+ const theme = ctx.ui?.theme;
1126
+ const styledStatus = formattedStatus !== undefined && typeof theme?.fg === "function"
1127
+ ? theme.fg("accent", formattedStatus)
1128
+ : formattedStatus;
1129
+ ctx.ui?.setStatus("mcp", styledStatus);
1130
+ return;
1131
+ }
1053
1132
  startInitialization(ctx, owner, generation, "stale_session_start");
1054
1133
  }
1055
1134
 
@@ -1148,6 +1227,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1148
1227
  { value: "tools", label: "tools — List all tools" },
1149
1228
  { value: "prompts", label: "prompts — List all MCP prompts" },
1150
1229
  { value: "setup", label: "setup — Configure MCP servers" },
1230
+ { value: "jev", label: "jev setup — Configure Jev semantic search" },
1231
+ { value: "edit", label: "edit — Edit .mcp.json or the global config" },
1151
1232
  { value: "logout", label: "logout — Clear server credentials" },
1152
1233
  { value: "token", label: "token — Manage stored bearer tokens" },
1153
1234
  { value: "disable", label: "disable — Disable a server" },
@@ -1158,6 +1239,11 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1158
1239
  }
1159
1240
 
1160
1241
  const [, subcommand, argumentPrefix] = argumentMatch;
1242
+ if (subcommand === "jev") {
1243
+ return "setup".startsWith((argumentPrefix ?? "").trimStart())
1244
+ ? [{ value: "jev setup", label: "setup — Configure Jev semantic search" }]
1245
+ : null;
1246
+ }
1161
1247
  if (
1162
1248
  (subcommand !== "reconnect" && subcommand !== "logout" && subcommand !== "disable" && subcommand !== "enable" && subcommand !== "token")
1163
1249
  || argumentPrefix === undefined
@@ -1246,6 +1332,41 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1246
1332
  }
1247
1333
  break;
1248
1334
  }
1335
+ case "jev": {
1336
+ if (parts[1] !== "setup" || parts.length !== 2) {
1337
+ commandCtx.ui?.notify("Usage: /mcp jev setup", "error");
1338
+ break;
1339
+ }
1340
+ if (programmaticConfig) {
1341
+ commandCtx.ui?.notify("Jev setup is unavailable when config is supplied by createMcpAdapter().", "info");
1342
+ break;
1343
+ }
1344
+ commandOwner?.throwIfInactive();
1345
+ if (await commands.setupJevSemanticSearch(state, commandCtx, earlyConfigPath)) {
1346
+ commandOwner?.throwIfInactive();
1347
+ await commandReload();
1348
+ return;
1349
+ }
1350
+ break;
1351
+ }
1352
+ case "edit": {
1353
+ if (programmaticConfig) {
1354
+ commandCtx.ui?.notify("MCP edit is unavailable when config is supplied by createMcpAdapter().", "info");
1355
+ break;
1356
+ }
1357
+ const target = parts[1] ?? "project";
1358
+ if (target !== "project" && target !== "global") {
1359
+ commandCtx.ui?.notify("Usage: /mcp edit [project|global]", "error");
1360
+ return;
1361
+ }
1362
+ commandOwner?.throwIfInactive();
1363
+ if (await commands.editSharedConfig(commandCtx, target)) {
1364
+ commandOwner?.throwIfInactive();
1365
+ await commandReload();
1366
+ return;
1367
+ }
1368
+ break;
1369
+ }
1249
1370
  case "logout": {
1250
1371
  const serverName = rest;
1251
1372
  if (!serverName) {
@@ -1666,11 +1787,12 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1666
1787
  describe: Type.Optional(Type.String({ description: "Tool name to describe (shows parameters)" })),
1667
1788
  instructions: Type.Optional(Type.String({ description: "Server name to show that server's usage instructions" })),
1668
1789
  search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
1790
+ searchMode: Type.Optional(Type.String({ enum: ["lexical", "semantic"], description: "Search backend (default: lexical; semantic is available when a TypeSafe key is configured)" })),
1669
1791
  regex: Type.Optional(Type.Boolean({ description: "Treat search as regex (default: substring match)" })),
1670
1792
  includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
1671
1793
  limit: optionalNumber({ minimum: 1, description: "Maximum search results to return (default: 12)" }),
1672
1794
  offset: optionalNumber({ minimum: 0, description: "Search result offset (default: 0)" }),
1673
- server: Type.Optional(Type.String({ description: "Server name (filters/disambiguates calls and optionally names an install)" })),
1795
+ server: Type.Optional(Type.String({ description: "Server name: filters searches, disambiguates calls and describe operations, and optionally names an install" })),
1674
1796
  action: Type.Optional(Type.String({ description: "Action: 'install', 'ui-messages', 'auth-start', or 'auth-complete'" })),
1675
1797
  url: Type.Optional(Type.String({ description: "MCP endpoint URL for action: 'install'" })),
1676
1798
  target: Type.Optional(Type.String({ description: "Install target: 'global' (default) or 'project'" })),
@@ -1683,6 +1805,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1683
1805
  describe?: string;
1684
1806
  instructions?: string;
1685
1807
  search?: string;
1808
+ searchMode?: "lexical" | "semantic";
1686
1809
  regex?: boolean;
1687
1810
  includeSchemas?: boolean;
1688
1811
  limit?: number;
@@ -1811,13 +1934,13 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1811
1934
  return connectAndReport(proxyState, params.connect, signal, _ctx as ExtensionContext);
1812
1935
  }
1813
1936
  if (params.describe) {
1814
- return proxyModes.executeDescribe(proxyState, params.describe);
1937
+ return proxyModes.executeDescribe(proxyState, params.describe, params.server);
1815
1938
  }
1816
1939
  if (params.instructions) {
1817
1940
  return proxyModes.executeInstructions(proxyState, params.instructions);
1818
1941
  }
1819
1942
  if (params.search !== undefined) {
1820
- const result = proxyModes.executeSearch(proxyState, params.search, params.regex, params.server, params.includeSchemas, params.limit, params.offset);
1943
+ const result = await proxyModes.executeSearch(proxyState, params.search, params.regex, params.server, params.includeSchemas, params.limit, params.offset, params.searchMode, signal);
1821
1944
  if (lazyDirectTools.size === 0) return result;
1822
1945
  holdLazyToolsInactive();
1823
1946
  const matches = (result.details as { matches?: Array<{ server: string; tool: string }> } | undefined)?.matches ?? [];
@@ -24,7 +24,7 @@ import { McpServerManager, isTransientHttpConnectError } from "./server-manager.
24
24
  import { buildToolMetadata, totalToolCount } from "./tool-metadata.ts";
25
25
  import { resourceNameToToolName } from "./resource-tools.ts";
26
26
  import { UiResourceHandler } from "./ui-resource-handler.ts";
27
- import { formatMcpStatus, openUrl, parallelLimit, sanitizeTerminalText } from "./utils.ts";
27
+ import { formatMcpFooterStatus, formatMcpStatus, openUrl, parallelLimit, sanitizeTerminalText } from "./utils.ts";
28
28
  import { logger } from "./logger.ts";
29
29
  import { throwIfAborted } from "./abort.ts";
30
30
  import { getAuthStorageOptions } from "./mcp-auth.ts";
@@ -209,6 +209,7 @@ export async function initializeMcp(
209
209
  failureTracker,
210
210
  failureMessages,
211
211
  approvedToolCalls,
212
+ approvedServers: new Map(),
212
213
  ...(persistSessionApproval !== undefined ? { persistSessionApproval } : {}),
213
214
  ...(sessionManager !== undefined ? { sessionManager } : {}),
214
215
  approvalEvents: pi.events,
@@ -253,6 +254,9 @@ export async function initializeMcp(
253
254
  if (!owner.isActive()) return;
254
255
  updateStatusBar(state);
255
256
  });
257
+ owner.addCleanup(() => {
258
+ state.approvedServers = new Map();
259
+ });
256
260
  owner.addCleanup(() => lifecycle.gracefulShutdown());
257
261
  owner.addCleanup(() => {
258
262
  if (state.uiServer) {
@@ -642,28 +646,11 @@ export function updateStatusBar(state: McpExtensionState): void {
642
646
  const entries = Object.entries(state.config.mcpServers);
643
647
  const disabledCount = entries.filter(([, definition]) => isServerDisabled(definition)).length;
644
648
  const enabledCount = entries.length - disabledCount;
645
- if (entries.length === 0) {
646
- ui.setStatus("mcp", undefined);
647
- return;
648
- }
649
649
  const connectedCount = [...state.manager.getAllConnections()].filter(([name, connection]) => {
650
650
  const definition = state.config.mcpServers[name];
651
651
  return connection.status === "connected" && definition !== undefined && !isServerDisabled(definition);
652
652
  }).length;
653
- const footerStatus = state.config.settings?.mcpFooterStatus ?? "full";
654
- if (footerStatus === "off") {
655
- ui.setStatus("mcp", undefined);
656
- return;
657
- }
658
-
659
- let status = footerStatus === "compact"
660
- ? `MCP ${connectedCount}/${enabledCount}`
661
- : `${enabledCount} ${enabledCount === 1 ? "server" : "servers"} enabled`;
662
- if (footerStatus === "full") {
663
- if (connectedCount > 0) status += ` (${connectedCount} connected)`;
664
- if (disabledCount > 0) status += ` (${disabledCount} disabled)`;
665
- }
666
- const formattedStatus = footerStatus === "compact" ? status : formatMcpStatus(state.config, status);
653
+ const formattedStatus = formatMcpFooterStatus(state.config, enabledCount, disabledCount, connectedCount);
667
654
  if (formattedStatus === undefined) {
668
655
  ui.setStatus("mcp", undefined);
669
656
  return;