@humain/terminal 0.1.1 → 0.2.2

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 (197) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
  4. package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
  5. package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-ADQYVZ6A.js} +483 -506
  6. package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
  7. package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
  9. package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
  10. package/dist/bundle/chunks/chunk-NMPIM7MN.js +2 -0
  11. package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
  12. package/dist/bundle/chunks/compat-CN7WN6DA.js +2 -0
  13. package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
  14. package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
  15. package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
  16. package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
  17. package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
  18. package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
  19. package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
  20. package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
  21. package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
  22. package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
  23. package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
  24. package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
  25. package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
  26. package/dist/bundle/cli.js +1 -1
  27. package/dist/bundle/index.js +1 -1
  28. package/dist/bundle/rpc-entry.js +1 -1
  29. package/dist/core/model-runtime.d.ts +21 -0
  30. package/dist/core/model-runtime.d.ts.map +1 -1
  31. package/dist/core/model-runtime.js +34 -4
  32. package/dist/core/model-runtime.js.map +1 -1
  33. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  34. package/dist/core/tools/file-mutation-queue.js +21 -8
  35. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  36. package/dist/core/tools/subagent-env.d.ts +20 -0
  37. package/dist/core/tools/subagent-env.d.ts.map +1 -0
  38. package/dist/core/tools/subagent-env.js +47 -0
  39. package/dist/core/tools/subagent-env.js.map +1 -0
  40. package/dist/core/tools/subagent-scheduler.d.ts +36 -0
  41. package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
  42. package/dist/core/tools/subagent-scheduler.js +213 -0
  43. package/dist/core/tools/subagent-scheduler.js.map +1 -0
  44. package/dist/core/tools/subagent.d.ts +83 -3
  45. package/dist/core/tools/subagent.d.ts.map +1 -1
  46. package/dist/core/tools/subagent.js +215 -76
  47. package/dist/core/tools/subagent.js.map +1 -1
  48. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  49. package/dist/humain/mcp-adapter/vendor/README.md +125 -19
  50. package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
  51. package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
  52. package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
  53. package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
  54. package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
  55. package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
  56. package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
  57. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
  58. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
  59. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
  60. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
  61. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
  62. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
  63. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
  64. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
  65. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
  66. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
  67. package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
  68. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  69. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
  70. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
  71. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
  72. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
  73. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
  74. package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
  75. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  76. package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
  77. package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
  78. package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
  79. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
  80. package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
  81. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
  82. package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
  83. package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
  84. package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
  85. package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
  86. package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
  87. package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
  88. package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
  89. package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
  90. package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
  91. package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
  92. package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
  93. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
  94. package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
  95. package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
  96. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
  97. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
  98. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
  99. package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
  100. package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
  101. package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
  102. package/dist/humain/mcp-adapter/vendor/package.json +13 -3
  103. package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
  104. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
  105. package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
  106. package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
  107. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
  108. package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
  109. package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
  110. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
  111. package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
  112. package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
  113. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
  114. package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
  115. package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
  116. package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
  117. package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
  118. package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
  119. package/dist/humain/web-access/vendor/abortable.js +22 -0
  120. package/dist/humain/web-access/vendor/abortable.js.map +1 -0
  121. package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
  122. package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
  123. package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
  124. package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
  125. package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
  126. package/dist/humain/web-access/vendor/credential-source.js +1 -0
  127. package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
  128. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  129. package/dist/humain/web-access/vendor/curator-page.js +20 -2
  130. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  131. package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
  132. package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
  133. package/dist/humain/web-access/vendor/curator-run.js +38 -0
  134. package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
  135. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  136. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  137. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  138. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  139. package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
  140. package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
  141. package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
  142. package/dist/humain/web-access/vendor/extract.d.ts +1 -1
  143. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  144. package/dist/humain/web-access/vendor/extract.js +58 -11
  145. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  146. package/dist/humain/web-access/vendor/fetch-params.js +1 -1
  147. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  148. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  149. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  150. package/dist/humain/web-access/vendor/gemini-search.js +12 -5
  151. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  152. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  153. package/dist/humain/web-access/vendor/index.js +23 -19
  154. package/dist/humain/web-access/vendor/index.js.map +1 -1
  155. package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
  156. package/dist/humain/web-access/vendor/openai-search.js +40 -14
  157. package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
  158. package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
  159. package/dist/humain/web-access/vendor/page-query.js +6 -2
  160. package/dist/humain/web-access/vendor/page-query.js.map +1 -1
  161. package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
  162. package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
  163. package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
  164. package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
  165. package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
  166. package/dist/humain/web-access/vendor/serpapi.js +215 -0
  167. package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
  168. package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
  169. package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
  170. package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
  171. package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
  172. package/dist/humain/web-access/vendor/storage.js +9 -0
  173. package/dist/humain/web-access/vendor/storage.js.map +1 -1
  174. package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
  175. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  176. package/dist/humain/web-access/vendor/summary-review.js +35 -24
  177. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  178. package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
  179. package/dist/humain/web-access/vendor/utils.js +5 -3
  180. package/dist/humain/web-access/vendor/utils.js.map +1 -1
  181. package/dist/modes/json-event.d.ts +8 -0
  182. package/dist/modes/json-event.d.ts.map +1 -1
  183. package/dist/modes/json-event.js +17 -0
  184. package/dist/modes/json-event.js.map +1 -1
  185. package/dist/modes/print-mode.d.ts.map +1 -1
  186. package/dist/modes/print-mode.js +3 -2
  187. package/dist/modes/print-mode.js.map +1 -1
  188. package/docs/environment-variables.md +9 -0
  189. package/docs/json.md +16 -0
  190. package/npm-shrinkwrap.json +6 -6
  191. package/package.json +2 -2
  192. package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
  193. package/src/humain/mcp-adapter/upstream.json +6 -4
  194. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  195. package/src/humain/web-access/UPSTREAM.md +18 -8
  196. package/src/humain/web-access/upstream.json +6 -5
  197. package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
@@ -0,0 +1,187 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { McpExtensionState } from "./state.ts";
3
+ import type { ToolMetadata } from "./types.ts";
4
+ import { logger } from "./logger.ts";
5
+ import { stableStringify } from "./utils.ts";
6
+
7
+ export const MCP_APPROVAL_CUSTOM_TYPE = "mcp-approval-v1";
8
+
9
+ export type SessionApprovalEntry =
10
+ | {
11
+ version: 1;
12
+ kind: "tool";
13
+ decision: "allow_for_session";
14
+ serverName: string;
15
+ originalToolName: string;
16
+ definitionHash: string;
17
+ argsHash: string;
18
+ }
19
+ | {
20
+ version: 1;
21
+ kind: "iframe";
22
+ decision: "allow" | "deny";
23
+ serverName: string;
24
+ };
25
+
26
+ export type SessionApprovalWriter = (record: SessionApprovalEntry) => void;
27
+
28
+ const TOOL_APPROVAL_KEYS = [
29
+ "version",
30
+ "kind",
31
+ "decision",
32
+ "serverName",
33
+ "originalToolName",
34
+ "definitionHash",
35
+ "argsHash",
36
+ ] as const;
37
+ const IFRAME_APPROVAL_KEYS = ["version", "kind", "decision", "serverName"] as const;
38
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
39
+
40
+ export function computeToolArgumentsHash(args: unknown): string {
41
+ return createHash("sha256").update(stableStringify(args ?? {})).digest("hex");
42
+ }
43
+
44
+ export function computeToolDefinitionHash(
45
+ toolMeta: Pick<ToolMetadata, "originalName" | "inputSchema" | "resourceUri" | "uiResourceUri">,
46
+ ): string {
47
+ return createHash("sha256").update(stableStringify({
48
+ originalName: toolMeta.originalName,
49
+ inputSchema: toolMeta.inputSchema ?? null,
50
+ resourceUri: toolMeta.resourceUri ?? null,
51
+ uiResourceUri: toolMeta.uiResourceUri ?? null,
52
+ })).digest("hex");
53
+ }
54
+
55
+ export function makeToolApprovalKey(
56
+ serverName: string,
57
+ originalToolName: string,
58
+ definitionHash: string,
59
+ argsHash: string,
60
+ ): string {
61
+ return `${serverName}\u0000${originalToolName}\u0000${definitionHash}\u0000${argsHash}`;
62
+ }
63
+
64
+ export function getToolApprovalIdentity(
65
+ serverName: string,
66
+ toolMeta: Pick<ToolMetadata, "originalName" | "inputSchema" | "resourceUri" | "uiResourceUri">,
67
+ args: unknown,
68
+ ): { definitionHash: string; argsHash: string; cacheKey: string } {
69
+ const definitionHash = computeToolDefinitionHash(toolMeta);
70
+ const argsHash = computeToolArgumentsHash(args);
71
+ return {
72
+ definitionHash,
73
+ argsHash,
74
+ cacheKey: makeToolApprovalKey(serverName, toolMeta.originalName, definitionHash, argsHash),
75
+ };
76
+ }
77
+
78
+ function isRecord(value: unknown): value is Record<string, unknown> {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+
82
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
83
+ const actualKeys = Object.keys(value);
84
+ return actualKeys.length === keys.length && keys.every(key => Object.hasOwn(value, key));
85
+ }
86
+
87
+ function isNonEmptyString(value: unknown): value is string {
88
+ return typeof value === "string" && value.length > 0;
89
+ }
90
+
91
+ function isSha256(value: unknown): value is string {
92
+ return typeof value === "string" && SHA256_HEX.test(value);
93
+ }
94
+
95
+ function isSessionApprovalEntry(value: unknown): value is SessionApprovalEntry {
96
+ if (!isRecord(value) || value.version !== 1 || !isNonEmptyString(value.serverName)) return false;
97
+
98
+ if (value.kind === "tool") {
99
+ return hasExactKeys(value, TOOL_APPROVAL_KEYS)
100
+ && value.decision === "allow_for_session"
101
+ && isNonEmptyString(value.originalToolName)
102
+ && isSha256(value.definitionHash)
103
+ && isSha256(value.argsHash);
104
+ }
105
+
106
+ if (value.kind === "iframe") {
107
+ return hasExactKeys(value, IFRAME_APPROVAL_KEYS)
108
+ && (value.decision === "allow" || value.decision === "deny");
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ function isApprovalCustomEntry(entry: unknown): entry is { type: "custom"; data?: unknown } {
115
+ return isRecord(entry)
116
+ && entry.type === "custom"
117
+ && entry.customType === MCP_APPROVAL_CUSTOM_TYPE;
118
+ }
119
+
120
+ export function createSessionApprovalWriter(
121
+ appendEntry: (customType: string, data?: unknown) => void,
122
+ canAppend: (() => boolean) | undefined = undefined,
123
+ ): SessionApprovalWriter {
124
+ return (record) => {
125
+ if (canAppend && !canAppend()) return;
126
+ try {
127
+ appendEntry(MCP_APPROVAL_CUSTOM_TYPE, record);
128
+ } catch (error) {
129
+ const detail = error instanceof Error ? error.message : String(error);
130
+ logger.debug(`MCP: failed to persist session approval: ${detail}`);
131
+ }
132
+ };
133
+ }
134
+
135
+ export function rememberToolApproval(
136
+ state: McpExtensionState,
137
+ serverName: string,
138
+ toolMeta: Pick<ToolMetadata, "originalName" | "inputSchema" | "resourceUri" | "uiResourceUri">,
139
+ args: unknown,
140
+ ): void {
141
+ const { definitionHash, argsHash, cacheKey } = getToolApprovalIdentity(serverName, toolMeta, args);
142
+ const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
143
+ if (approvedToolCalls.has(cacheKey)) return;
144
+
145
+ approvedToolCalls.set(cacheKey, true);
146
+ const record: SessionApprovalEntry = {
147
+ version: 1,
148
+ kind: "tool",
149
+ decision: "allow_for_session",
150
+ serverName,
151
+ originalToolName: toolMeta.originalName,
152
+ definitionHash,
153
+ argsHash,
154
+ };
155
+ try {
156
+ state.persistSessionApproval?.(record);
157
+ } catch (error) {
158
+ const detail = error instanceof Error ? error.message : String(error);
159
+ logger.debug(`MCP: session approval persistence callback failed: ${detail}`);
160
+ }
161
+ }
162
+
163
+ export function restoreSessionApprovalState(
164
+ state: McpExtensionState,
165
+ branchEntries: readonly unknown[],
166
+ ): void {
167
+ const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
168
+ approvedToolCalls.clear();
169
+ state.consentManager.clear();
170
+
171
+ for (const entry of branchEntries) {
172
+ if (!isApprovalCustomEntry(entry)) continue;
173
+ if (!isSessionApprovalEntry(entry.data)) continue;
174
+
175
+ if (entry.data.kind === "tool") {
176
+ approvedToolCalls.set(makeToolApprovalKey(
177
+ entry.data.serverName,
178
+ entry.data.originalToolName,
179
+ entry.data.definitionHash,
180
+ entry.data.argsHash,
181
+ ), true);
182
+ continue;
183
+ }
184
+
185
+ state.consentManager.restoreDecision(entry.data.serverName, entry.data.decision === "allow");
186
+ }
187
+ }
@@ -30,10 +30,18 @@ return result.data;
30
30
  2. Inspect the exact returned path with `await tools.describe({ path })`.
31
31
  3. Call it with `tools.call(path, args)`.
32
32
 
33
+ Descriptors include `inputTypeScript` (a compact parameter shape, or formatted schema fallback). When a compact shape would omit documented fields, `inputGuidance` preserves their descriptions, including formats and units. Undocumented inputs stay compact.
34
+
35
+ When advertised by the server, `outputSchema` is the original JSON Schema and `outputSchemaTarget` is `"data.structuredContent"`: it describes structured output inside the successful `{ ok: true, data }` call envelope, not the envelope itself. Inspect this schema for result fields and constraints; unsupported constructs remain intact rather than being presented as an approximate TypeScript type. Both output fields are absent when no output schema is advertised. Discovery and cache refresh preserve these optional schemas; old cache entries gain them on the next server metadata refresh. Ordinary search results do not include schemas.
36
+
33
37
  Calls resolve to `{ ok: true, data }` or `{ ok: false, error }`; handle failed calls instead of expecting them to stop the script. `emit(value)` adds user-visible output before the final `return` value. `console` output is captured too.
34
38
 
39
+ On success, `data` may still be the raw MCP `CallToolResult` envelope rather than the domain payload. Check `data.structuredContent` for the fields your script expects; if they are absent, inspect text blocks in `data.content` too (some servers emit newline-delimited JSON). If neither shape is understood, return or emit the envelope for inspection instead of coercing it to `[]` or `{}`.
40
+
35
41
  `tools` is a non-enumerable proxy: `Object.keys(tools)` throws. Always use `tools.search` for discovery. When a known flat path is a valid identifier, direct calls such as `tools.github_search_issues(args)` are supported; use bracket syntax for hyphenated names: `tools["server_tool-name"](args)`. `search`, `call`, `describe`, and promise/serialization names (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`) are reserved on the proxy; if a flat path collides with one, call it via `tools.call("exact-path", args)`.
36
42
 
37
- `tools.search` and `tools.describe` are asynchronous and must be awaited. The default script timeout is 30 seconds; the worker is terminated at the deadline, including for infinite loops. Every invocation still uses normal lazy connection, authentication, output guarding, and approval gates. Result details contain a concise `calls` trace with every search, describe, and call operation; each entry includes its query or path, outcome, and duration.
43
+ Successful intermediate data bypasses presentation truncation, summarization, and artifact spill until the script emits, logs, or returns it. A fixed, non-configurable **16 MiB cumulative UTF-8 JSON transfer budget per script** covers sequential and parallel calls. A result exceeding the remaining budget returns `{ ok: false, error: { code: "intermediate_result_too_large", message } }` with a failed call trace; rejected calls do not consume the budget, but upstream side effects may already have happened. Resource calls retain transformed text or `"(empty resource)"`; emitted, logged, or returned output and ordinary MCP calls remain guarded. This is not a memory limit: responses, serialization, copies, concurrency, and script values still allocate memory, and synchronous serialization can delay deadline handling.
44
+
45
+ `tools.search` and `tools.describe` are asynchronous and must be awaited. The default script timeout is 30 seconds; the worker is terminated at the deadline, including for infinite loops. Every invocation still uses normal lazy connection, authentication, and approval gates. Result details contain a concise `calls` trace with every search, describe, and call operation; each entry includes its query or path, outcome, and duration.
38
46
 
39
47
  Use plain JavaScript loops and Promise utilities for composition. Fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided.
@@ -7,6 +7,7 @@ import type { ToolMetadata, PromptMetadata, McpConfig, UiSessionMessages, UiStre
7
7
  import type { UiResourceHandler } from "./ui-resource-handler.ts";
8
8
  import type { McpRuntimeOwner } from "./runtime-owner.ts";
9
9
  import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
10
+ import type { SessionApprovalWriter } from "./session-approvals.ts";
10
11
 
11
12
  export interface CompletedUiSession {
12
13
  serverName: string;
@@ -32,6 +33,8 @@ export interface McpExtensionState {
32
33
  manager: McpServerManager;
33
34
  lifecycle: McpLifecycleManager;
34
35
  toolMetadata: Map<string, ToolMetadata[]>;
36
+ /** Number of tools currently registered directly with Pi, by server. */
37
+ directToolCounts: Map<string, number>;
35
38
  /** Resource counts retained separately because tool metadata includes resource tools. */
36
39
  resourceCounts: Map<string, number>;
37
40
  promptMetadata: Map<string, PromptMetadata[]>;
@@ -40,12 +43,18 @@ export interface McpExtensionState {
40
43
  serverInstructions: Map<string, string>;
41
44
  config: McpConfig;
42
45
  programmaticConfig?: boolean;
46
+ /** Install validations must not publish durable cache entries before config persistence. */
47
+ provisionalInstalls?: Set<string>;
43
48
  oauthRuntime: McpOAuthRuntime;
44
49
  authStorageOptions: AuthStorageOptions;
45
50
  failureTracker: Map<string, number>;
46
51
  failureMessages: Map<string, string>;
47
- /** Session-only approvals keyed by server and original tool name. */
52
+ /** Session-only approvals keyed by server, tool definition, and arguments. */
48
53
  approvedToolCalls: Map<string, true>;
54
+ /** Optional active-session sink for approval decisions. */
55
+ persistSessionApproval?: SessionApprovalWriter;
56
+ /** Session manager used to reject stale session-tree contexts. */
57
+ sessionManager?: ExtensionContext["sessionManager"];
49
58
  /** Shared event bus used by permission extensions to broker MCP approvals. */
50
59
  approvalEvents?: ExtensionAPI["events"];
51
60
  uiResourceHandler: UiResourceHandler;
@@ -1,7 +1,8 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { abortable } from "./abort.ts";
3
3
  import { combineAbortSignals } from "./runtime-owner.ts";
4
4
  import type { McpExtensionState } from "./state.ts";
5
+ import { getToolApprovalIdentity, rememberToolApproval } from "./session-approvals.ts";
5
6
  import {
6
7
  getToolNameCandidates,
7
8
  matchesToolPattern,
@@ -20,18 +21,6 @@ export type ToolCallApprovalResult =
20
21
  | { ok: true }
21
22
  | { ok: false; reason: "denied" | "approval_required_headless" };
22
23
 
23
- function stableStringify(value: unknown): string {
24
- if (value === null || value === undefined || typeof value !== "object") {
25
- const serialized = JSON.stringify(value);
26
- return serialized === undefined ? "undefined" : serialized;
27
- }
28
- if (Array.isArray(value)) {
29
- return `[${value.map(item => stableStringify(item)).join(",")}]`;
30
- }
31
- const object = value as Record<string, unknown>;
32
- return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
33
- }
34
-
35
24
  export function isToolCallApprovalRequired(
36
25
  config: McpConfig,
37
26
  serverName: string,
@@ -148,21 +137,20 @@ export async function ensureToolCallApproved(
148
137
  origin: McpToolApprovalOrigin = toolMeta.resourceUri ? "resource" : "proxy",
149
138
  approvalMetadata?: ReadonlyMap<string, readonly ToolMetadata[]>,
150
139
  ): Promise<ToolCallApprovalResult> {
151
- const argsHash = createHash("sha256").update(stableStringify(args ?? {})).digest("hex");
152
- const cacheKey = `${serverName}\u0000${toolMeta.originalName}\u0000${argsHash}`;
153
- const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
154
- if (approvedToolCalls.has(cacheKey)) {
155
- return { ok: true };
156
- }
157
-
158
140
  const brokerDecision = await requestBrokerApproval(state, serverName, toolMeta, args, origin, signal);
159
141
  if (brokerDecision === "allow_once") return { ok: true };
160
142
  if (brokerDecision === "allow_for_session") {
161
- approvedToolCalls.set(cacheKey, true);
143
+ rememberToolApproval(state, serverName, toolMeta, args);
162
144
  return { ok: true };
163
145
  }
164
146
  if (brokerDecision === "deny") return { ok: false, reason: "denied" };
165
147
 
148
+ const { cacheKey } = getToolApprovalIdentity(serverName, toolMeta, args);
149
+ const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
150
+ if (approvedToolCalls.has(cacheKey)) {
151
+ return { ok: true };
152
+ }
153
+
166
154
  if (!isToolCallApprovalRequired(state.config, serverName, toolMeta, approvalMetadata ?? state.toolMetadata)) {
167
155
  return { ok: true };
168
156
  }
@@ -188,7 +176,7 @@ export async function ensureToolCallApproved(
188
176
  return { ok: true };
189
177
  }
190
178
  if (decision === "Allow for session") {
191
- approvedToolCalls.set(cacheKey, true);
179
+ rememberToolApproval(state, serverName, toolMeta, args);
192
180
  return { ok: true };
193
181
  }
194
182
  return { ok: false, reason: "denied" };
@@ -108,6 +108,7 @@ export function buildToolMetadata(
108
108
  originalName: tool.name,
109
109
  description: tool.description ?? "",
110
110
  ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
111
+ ...(tool.outputSchema !== undefined ? { outputSchema: tool.outputSchema } : {}),
111
112
  ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
112
113
  ...(uiVisibility !== undefined ? { uiVisibility } : {}),
113
114
  ...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
@@ -159,6 +160,19 @@ export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: s
159
160
  return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
160
161
  }
161
162
 
163
+ /** Whether the schema formatter has field descriptions worth showing beside a compact shape. */
164
+ export function hasSchemaDescriptions(schema: unknown, includeRoot = false): boolean {
165
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
166
+ const s = schema as Record<string, unknown>;
167
+ if (includeRoot && typeof s.description === "string" && s.description.length > 0) return true;
168
+ const hasNestedDescription = (child: unknown) => hasSchemaDescriptions(child, true);
169
+ if (s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)
170
+ && Object.values(s.properties).some(hasNestedDescription)) return true;
171
+ return hasNestedDescription(s.items)
172
+ || (Array.isArray(s.anyOf) && s.anyOf.some(hasNestedDescription))
173
+ || (Array.isArray(s.oneOf) && s.oneOf.some(hasNestedDescription));
174
+ }
175
+
162
176
  export function formatSchema(schema: unknown, indent = " "): string {
163
177
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
164
178
  return `${indent}(no schema)`;
@@ -10,6 +10,7 @@ import type {
10
10
  import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
11
11
  import type { UiStreamMode, UiStreamSummary } from "./ui-stream-types.ts";
12
12
  import type { UiToolVisibility } from "./ui-tool-visibility.ts";
13
+ import { createHash } from "node:crypto";
13
14
 
14
15
  export type Transport = McpTransport;
15
16
 
@@ -38,6 +39,7 @@ export interface McpServerStatusSnapshot {
38
39
  readonly name: string;
39
40
  readonly status: McpServerRuntimeStatus;
40
41
  readonly toolCount: number;
42
+ readonly directToolCount: number;
41
43
  readonly resourceCount?: number;
42
44
  readonly failedAgoSeconds?: number;
43
45
  readonly disabled: boolean;
@@ -85,6 +87,7 @@ export interface McpTool {
85
87
  title?: SdkTool["title"];
86
88
  description?: SdkTool["description"];
87
89
  inputSchema?: SdkTool["inputSchema"]; // JSON Schema
90
+ outputSchema?: SdkTool["outputSchema"]; // JSON Schema for structuredContent
88
91
  _meta?: SdkTool["_meta"];
89
92
  }
90
93
 
@@ -386,13 +389,15 @@ export interface OAuthConfig {
386
389
  grantType?: "authorization_code" | "client_credentials";
387
390
  /** Pre-registered client ID (optional, dynamic registration used if not provided) */
388
391
  clientId?: string;
389
- /** Client secret for confidential clients */
392
+ /** Client secret for confidential clients; requires an explicit clientId when clientMetadataUrl is set. */
390
393
  clientSecret?: string;
394
+ /** Operator-supplied public HTTPS Client ID Metadata Document URL (SEP-991); opt-in, with DCR remaining the default. */
395
+ clientMetadataUrl?: string;
391
396
  /** Requested OAuth scopes */
392
397
  scope?: string;
393
398
  /** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
394
399
  authorizationParams?: Record<string, string>;
395
- /** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
400
+ /** Authorization-code redirect URI. Loopback URIs may use `{port}` for an OS-assigned port; HTTPS redirects use manual completion. */
396
401
  redirectUri?: string;
397
402
  /** Client display name for dynamic registration */
398
403
  clientName?: string;
@@ -426,9 +431,13 @@ export interface ServerEntry {
426
431
  /** Explicit rmcp-mux Unix-domain socket path. Mutually exclusive with command and url. */
427
432
  socket?: string;
428
433
  env?: Record<string, string>;
434
+ /** Inherit the adapter process environment for stdio servers. Defaults to true; false keeps SDK platform defaults plus explicit env overlays. */
435
+ inheritEnv?: boolean;
429
436
  cwd?: string;
430
437
  // HTTP fields
431
438
  url?: string;
439
+ /** PEM CA bundle replacing default roots for this HTTPS MCP origin only. */
440
+ caFile?: string;
432
441
  headers?: Record<string, string>;
433
442
  /** Add or replace HTTP headers by running a trusted command for each request. */
434
443
  requestHeadersCommand?: HttpRequestHeadersCommand;
@@ -456,7 +465,7 @@ export interface ServerEntry {
456
465
  // Resource handling
457
466
  exposeResources?: boolean;
458
467
  // Direct tool registration
459
- directTools?: boolean | string[];
468
+ directTools?: boolean | string[] | "search";
460
469
  // Override settings.toolPrefix for this server.
461
470
  toolPrefix?: ToolPrefix;
462
471
  // Include/exclude specific MCP tools/resources by original or prefixed name
@@ -510,6 +519,32 @@ export interface McpOutputGuardSettings {
510
519
 
511
520
  // Settings
512
521
  export type ToolPrefix = "server" | "none" | "short" | "mcp";
522
+
523
+ const ENCODED_SERVER_NAMESPACE_MARKER = "_mcpns_";
524
+ // Provider tool-name limit (64 for Bedrock, Anthropic, OpenAI) minus the `mcp__` proxy prefix.
525
+ const MAX_SERVER_NAMESPACE_LENGTH = 59;
526
+
527
+ export function formatServerNamespace(serverName: string): string {
528
+ const normalized = serverName.replace(/-/g, "_");
529
+ const safe = /^[A-Za-z0-9_]*$/.test(normalized) && !normalized.startsWith(ENCODED_SERVER_NAMESPACE_MARKER);
530
+ const body = safe ? normalized : encodeServerNamespace(normalized);
531
+ const namespace = safe ? body : `${ENCODED_SERVER_NAMESPACE_MARKER}${body}`;
532
+ if (namespace.length <= MAX_SERVER_NAMESPACE_LENGTH) return namespace;
533
+ // Hash the ASCII encoding, not the raw name: lone surrogates and U+FFFD share UTF-8 bytes.
534
+ const digest = createHash("sha256").update(namespace, "utf8").digest("hex").slice(0, 16);
535
+ // `_h_` cannot start an encoded body: `h` is neither `_` nor a hexadecimal digit.
536
+ const hashPrefix = `${ENCODED_SERVER_NAMESPACE_MARKER}_h_`;
537
+ const head = body.slice(0, MAX_SERVER_NAMESPACE_LENGTH - hashPrefix.length - digest.length - 1);
538
+ return `${hashPrefix}${head}_${digest}`;
539
+ }
540
+
541
+ // `_` becomes `__`, so `__` and `_<hex>_` form a prefix code and the encoding stays injective.
542
+ function encodeServerNamespace(name: string): string {
543
+ return Array.from(name, character => {
544
+ if (character === "_") return "__";
545
+ return /^[A-Za-z0-9]$/.test(character) ? character : `_${character.codePointAt(0)!.toString(16)}_`;
546
+ }).join("");
547
+ }
513
548
  export type HostConfigDiscovery = "off" | "prompt" | "on";
514
549
  export type McpFooterStatus = "full" | "compact" | "off";
515
550
 
@@ -551,11 +586,13 @@ export interface McpSettings {
551
586
  notifyOnStartupConnect?: boolean;
552
587
  /** Discover detected host-specific MCP configs only when explicitly enabled. */
553
588
  hostConfigDiscovery?: HostConfigDiscovery;
589
+ /** Trusted HOME-contained roots from which to discover ancestor project configs. */
590
+ ancestorConfigRoots?: string[];
554
591
  /** Agent Plugin package directories to load MCP servers from. */
555
592
  agentPluginPaths?: string[];
556
593
  idleTimeout?: number; // minutes, default 10, 0 to disable
557
594
  requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
558
- directTools?: boolean;
595
+ directTools?: boolean | "search";
559
596
  /**
560
597
  * Validate direct-tool inputs against the advertised schema after recovering
561
598
  * one JSON string layer for object and array properties. Defaults to false.
@@ -603,6 +640,8 @@ export interface McpSettings {
603
640
  * instruction when unset.
604
641
  */
605
642
  authRequiredMessage?: string;
643
+ /** Explicitly use AES-256-GCM files keyed by PI_MCP_ADAPTER_OAUTH_FILE_KEY instead of the OS credential store. */
644
+ oauthCredentialStore?: "encrypted-file";
606
645
  /**
607
646
  * Legacy OAuth tokens.json import directory.
608
647
  * Relative paths are resolved from the project root (cwd).
@@ -616,11 +655,21 @@ export interface McpSettings {
616
655
  oauthDir?: string;
617
656
  }
618
657
 
658
+ export interface ClaudePluginConfig {
659
+ /** Explicit local Claude plugin directory. File-based config resolves relative paths from the active project cwd; createMcpAdapter snapshots programmatic paths against process.cwd(). */
660
+ path: string;
661
+ /** Load the plugin's root .mcp.json as low-precedence MCP defaults. */
662
+ mcp?: boolean;
663
+ /** Expose the plugin's root skills/ directory to Pi resource discovery. */
664
+ skills?: boolean;
665
+ }
666
+
619
667
  // Root config
620
668
  export interface McpConfig {
621
669
  mcpServers: Record<string, ServerEntry>;
622
670
  imports?: ImportKind[];
623
671
  settings?: McpSettings;
672
+ claudePlugins?: ClaudePluginConfig[];
624
673
  }
625
674
 
626
675
  export interface McpAdapterOptions {
@@ -639,6 +688,7 @@ export interface ToolMetadata {
639
688
  uiResourceUri?: string; // For app-enabled tools: the UI resource URI
640
689
  uiVisibility?: UiToolVisibility[];
641
690
  inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
691
+ outputSchema?: unknown; // Server schema for structuredContent (stored for describe)
642
692
  uiStreamMode?: UiStreamMode;
643
693
  }
644
694
 
@@ -652,6 +702,8 @@ export interface PromptMetadata {
652
702
  }
653
703
 
654
704
  export interface DirectToolSpec {
705
+ /** Registered inactive; `mcp({ search })` activates it (directTools: "search"). */
706
+ lazy?: boolean;
655
707
  serverName: string;
656
708
  originalName: string;
657
709
  prefixedName: string;
@@ -677,6 +729,7 @@ export interface CachedTool {
677
729
  name: string;
678
730
  description?: string;
679
731
  inputSchema?: unknown;
732
+ outputSchema?: unknown;
680
733
  uiResourceUri?: string;
681
734
  uiVisibility?: UiToolVisibility[];
682
735
  uiStreamMode?: "eager" | "stream-first";
@@ -2,7 +2,7 @@ import http, { type IncomingMessage, type ServerResponse } from "node:http";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
- import { buildAllowAttribute } from "./ui-app-bridge-helpers.ts";
5
+ import { buildAllowAttribute, getToolUiResourceUri } from "./ui-app-bridge-helpers.ts";
6
6
  import {
7
7
  type CallToolRequest,
8
8
  type CallToolResult,
@@ -11,11 +11,12 @@ import { ContentBlockSchema } from "@modelcontextprotocol/core";
11
11
  import type { ConsentManager } from "./consent-manager.ts";
12
12
  import { ServerError, wrapError } from "./errors.ts";
13
13
  import { formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
14
- import { buildHostHtmlTemplate, buildCspMetaContent } from "./host-html-template.ts";
14
+ import { buildHostHtmlTemplate, buildSandboxResourceCsp } from "./host-html-template.ts";
15
15
  import {
16
16
  buildSandboxProxyCsp,
17
17
  buildSandboxProxyHtml,
18
18
  SANDBOX_PROXY_PATH,
19
+ SANDBOX_RESOURCE_PATH_PREFIX,
19
20
  } from "./sandbox-proxy-template.ts";
20
21
  import { logger } from "./logger.ts";
21
22
  import type { McpServerManager } from "./server-manager.ts";
@@ -85,7 +86,8 @@ export interface UiServerOptions {
85
86
 
86
87
  export async function startUiServer(options: UiServerOptions): Promise<UiServerHandle> {
87
88
  const sessionToken = options.sessionToken ?? randomUUID();
88
- const uiResourceToken = randomUUID();
89
+ const sandboxResourcePath = `${SANDBOX_RESOURCE_PATH_PREFIX}${randomUUID()}`;
90
+ const resourceAllow = buildAllowAttribute(options.resource.meta.permissions);
89
91
  const log = logger.child({
90
92
  component: "UiServer",
91
93
  server: options.serverName,
@@ -316,8 +318,12 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
316
318
  try {
317
319
  const method = req.method || "GET";
318
320
  const hostHeader = req.headers.host;
319
- const url = new URL(req.url || "/", `http://${hostHeader || "127.0.0.1"}`);
320
- if (hostHeader !== undefined && !isAllowedHost(url.hostname)) {
321
+ if (!hostHeader) {
322
+ sendText(res, 400, "Missing host");
323
+ return;
324
+ }
325
+ const url = new URL(req.url || "/", `http://${hostHeader}`);
326
+ if (!isAllowedHost(url.hostname)) {
321
327
  sendText(res, 403, "Invalid host");
322
328
  return;
323
329
  }
@@ -346,12 +352,11 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
346
352
 
347
353
  const html = buildHostHtmlTemplate({
348
354
  sessionToken,
349
- uiResourceToken,
350
355
  serverName: options.serverName,
351
356
  toolName: options.toolName,
352
357
  toolArgs: options.toolArgs,
353
358
  resource: options.resource,
354
- allowAttribute: buildAllowAttribute(options.resource.meta.permissions),
359
+ allowAttribute: resourceAllow,
355
360
  requireToolConsent: options.consentManager.requiresPrompt(options.serverName),
356
361
  cacheToolConsent: options.consentManager.shouldCacheConsent(),
357
362
  hostContext,
@@ -391,20 +396,6 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
391
396
  return;
392
397
  }
393
398
 
394
- if (method === "GET" && url.pathname === "/ui-app") {
395
- if (!validateTokenQuery(url, uiResourceToken, res, "resource")) return;
396
- touchHeartbeat();
397
- // Enforce host metadata independently of where app HTML places its document head.
398
- const cspContent = buildCspMetaContent(options.resource.meta.csp);
399
- res.writeHead(200, {
400
- "Content-Type": "text/html; charset=utf-8",
401
- "Cache-Control": "no-store",
402
- "Content-Security-Policy": cspContent,
403
- });
404
- res.end(options.resource.html);
405
- return;
406
- }
407
-
408
399
  if (method === "GET" && url.pathname === "/app-bridge.bundle.js") {
409
400
  // Serve the pre-bundled AppBridge module
410
401
  const bundlePath = path.join(import.meta.dirname, "app-bridge.bundle.js");
@@ -473,11 +464,18 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
473
464
  name: callParams.name,
474
465
  arguments: normalizedArguments,
475
466
  };
467
+ let uiResourceUri: string | undefined;
468
+ try {
469
+ uiResourceUri = getToolUiResourceUri({ _meta: toolDefinition._meta });
470
+ } catch {
471
+ // Preserve the endpoint's existing behavior for malformed declarations.
472
+ }
476
473
  const toolMeta = {
477
474
  name: callParams.name,
478
475
  originalName: callParams.name,
479
476
  description: toolDefinition?.description ?? "",
480
477
  ...(toolDefinition?.inputSchema !== undefined ? { inputSchema: toolDefinition.inputSchema } : {}),
478
+ ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
481
479
  ...(uiVisibility !== undefined ? { uiVisibility } : {}),
482
480
  };
483
481
  const approvalMetadata = new Map(options.state?.toolMetadata);
@@ -711,31 +709,41 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
711
709
  try {
712
710
  const method = req.method || "GET";
713
711
  const hostHeader = req.headers.host;
714
- const url = new URL(req.url || "/", `http://${hostHeader || "127.0.0.1"}`);
715
- if (hostHeader !== undefined && !isAllowedHost(url.hostname)) {
712
+ if (!hostHeader) {
713
+ sendText(res, 400, "Missing host");
714
+ return;
715
+ }
716
+ const url = new URL(req.url || "/", `http://${hostHeader}`);
717
+ if (!isAllowedHost(url.hostname)) {
716
718
  sendText(res, 403, "Invalid host");
717
719
  return;
718
720
  }
719
721
 
720
- if (method === "HEAD" && url.pathname === SANDBOX_PROXY_PATH) {
722
+ if ((method === "GET" || method === "HEAD") && url.pathname === SANDBOX_PROXY_PATH) {
721
723
  res.writeHead(200, {
722
724
  "Content-Type": "text/html; charset=utf-8",
723
725
  "Cache-Control": "no-store",
724
726
  "Content-Security-Policy": buildSandboxProxyCsp(),
727
+ "Referrer-Policy": "no-referrer",
728
+ "X-Content-Type-Options": "nosniff",
725
729
  });
726
- res.end();
730
+ res.end(method === "HEAD" ? undefined : buildSandboxProxyHtml({
731
+ parentOrigin,
732
+ resourcePath: sandboxResourcePath,
733
+ allowAttribute: resourceAllow,
734
+ }));
727
735
  return;
728
736
  }
729
737
 
730
- if (method === "GET" && url.pathname === SANDBOX_PROXY_PATH) {
738
+ if ((method === "GET" || method === "HEAD") && url.pathname === sandboxResourcePath) {
731
739
  res.writeHead(200, {
732
740
  "Content-Type": "text/html; charset=utf-8",
733
741
  "Cache-Control": "no-store",
734
- "Content-Security-Policy": buildSandboxProxyCsp(),
742
+ "Content-Security-Policy": buildSandboxResourceCsp(options.resource.meta.csp),
735
743
  "Referrer-Policy": "no-referrer",
736
744
  "X-Content-Type-Options": "nosniff",
737
745
  });
738
- res.end(buildSandboxProxyHtml({ parentOrigin }));
746
+ res.end(method === "HEAD" ? undefined : options.resource.html);
739
747
  return;
740
748
  }
741
749