@humain/terminal 0.1.0 → 0.2.1
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.
- package/CHANGELOG.md +33 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/chunk-EY4DWHQU.js +2 -0
- package/dist/bundle/chunks/{chunk-RM336IEI.js → chunk-I47ZWR55.js} +486 -509
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-QCV26HNS.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli.js +1 -5
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +18 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +43 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +31 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +203 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +214 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/index.d.ts.map +1 -1
- package/dist/humain/index.js +2 -0
- package/dist/humain/index.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/node-http.d.ts +3 -0
- package/dist/humain/node-http.d.ts.map +1 -0
- package/dist/humain/node-http.js +119 -0
- package/dist/humain/node-http.js.map +1 -0
- package/dist/humain/node-provider.d.ts +4 -0
- package/dist/humain/node-provider.d.ts.map +1 -0
- package/dist/humain/node-provider.js +359 -0
- package/dist/humain/node-provider.js.map +1 -0
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/index.js +1 -51
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +7 -2
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +46 -21
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +10 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +8 -0
- package/docs/humain-terminal.md +6 -0
- package/docs/json.md +16 -0
- package/docs/providers.md +19 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
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:
|
|
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
|
-
|
|
715
|
-
|
|
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 ===
|
|
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":
|
|
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(
|
|
746
|
+
res.end(method === "HEAD" ? undefined : options.resource.html);
|
|
739
747
|
return;
|
|
740
748
|
}
|
|
741
749
|
|