@njuptlzf/mcp-adapter 2.29.0-0.0.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +1205 -0
  2. package/LICENSE +21 -0
  3. package/MAPPING.md +73 -0
  4. package/README.md +564 -0
  5. package/abort.ts +34 -0
  6. package/adapters/pi-renderer.ts +7 -0
  7. package/adapters/protocol-elicitation-forwarder.ts +164 -0
  8. package/adapters/protocol-sampling-forwarder.ts +108 -0
  9. package/adapters/universal-host.ts +213 -0
  10. package/agent-dir.ts +68 -0
  11. package/agent-plugin-loader.ts +377 -0
  12. package/app-bridge.bundle.js +67 -0
  13. package/banner.png +0 -0
  14. package/bin/mcp-server.ts +282 -0
  15. package/cli.js +392 -0
  16. package/commands.ts +735 -0
  17. package/config.ts +1284 -0
  18. package/consent-manager.ts +64 -0
  19. package/direct-tools.ts +619 -0
  20. package/dist/agent-dir.d.ts +10 -0
  21. package/dist/agent-dir.js +63 -0
  22. package/dist/agent-dir.js.map +1 -0
  23. package/dist/agent-plugin-loader.d.ts +8 -0
  24. package/dist/agent-plugin-loader.js +356 -0
  25. package/dist/agent-plugin-loader.js.map +1 -0
  26. package/dist/config.d.ts +123 -0
  27. package/dist/config.js +1087 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/metadata-cache.d.ts +19 -0
  30. package/dist/metadata-cache.js +319 -0
  31. package/dist/metadata-cache.js.map +1 -0
  32. package/dist/package-mcp-loader.d.ts +2 -0
  33. package/dist/package-mcp-loader.js +156 -0
  34. package/dist/package-mcp-loader.js.map +1 -0
  35. package/dist/resource-tools.d.ts +1 -0
  36. package/dist/resource-tools.js +15 -0
  37. package/dist/resource-tools.js.map +1 -0
  38. package/dist/types.d.ts +564 -0
  39. package/dist/types.js +287 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/ui-app-bridge-helpers.d.ts +6 -0
  42. package/dist/ui-app-bridge-helpers.js +37 -0
  43. package/dist/ui-app-bridge-helpers.js.map +1 -0
  44. package/dist/ui-stream-types.d.ts +104 -0
  45. package/dist/ui-stream-types.js +59 -0
  46. package/dist/ui-stream-types.js.map +1 -0
  47. package/dist/ui-tool-visibility.d.ts +4 -0
  48. package/dist/ui-tool-visibility.js +27 -0
  49. package/dist/ui-tool-visibility.js.map +1 -0
  50. package/dist/utils.d.ts +32 -0
  51. package/dist/utils.js +338 -0
  52. package/dist/utils.js.map +1 -0
  53. package/elicitation-handler.ts +348 -0
  54. package/error-signal.ts +21 -0
  55. package/errors.ts +223 -0
  56. package/failure-backoff.ts +23 -0
  57. package/glimpse-ui.ts +80 -0
  58. package/host-html-template.ts +461 -0
  59. package/index.ts +1291 -0
  60. package/init.ts +692 -0
  61. package/interfaces/agent-paths.ts +67 -0
  62. package/interfaces/host-types.ts +102 -0
  63. package/interfaces/sampling.ts +57 -0
  64. package/json-schema-validator.ts +69 -0
  65. package/lifecycle.ts +446 -0
  66. package/logger.ts +169 -0
  67. package/mcp-auth-flow.ts +1026 -0
  68. package/mcp-auth.ts +1000 -0
  69. package/mcp-bearer-store.ts +387 -0
  70. package/mcp-callback-server.ts +534 -0
  71. package/mcp-code.ts +360 -0
  72. package/mcp-keyring-helper.cjs +89 -0
  73. package/mcp-oauth-provider.ts +666 -0
  74. package/mcp-output-guard.ts +496 -0
  75. package/mcp-panel.ts +1044 -0
  76. package/mcp-probe.ts +201 -0
  77. package/mcp-references.ts +368 -0
  78. package/mcp-script-worker.mjs +130 -0
  79. package/mcp-server.mjs +2133 -0
  80. package/mcp-setup-panel.ts +667 -0
  81. package/mcp-status.ts +98 -0
  82. package/mcp-trace.ts +306 -0
  83. package/metadata-cache.ts +382 -0
  84. package/namespace-tools.ts +276 -0
  85. package/npx-resolver.ts +543 -0
  86. package/oauth-handler.ts +30 -0
  87. package/oauth.ts +48 -0
  88. package/onboarding-state.ts +76 -0
  89. package/package-mcp-loader.ts +168 -0
  90. package/package.json +225 -0
  91. package/panel-keys.ts +53 -0
  92. package/prompts.ts +353 -0
  93. package/proxy-modes.ts +1418 -0
  94. package/request-headers-command.ts +330 -0
  95. package/resource-tools.ts +17 -0
  96. package/runtime-owner.ts +91 -0
  97. package/sampling-handler.ts +284 -0
  98. package/search-ranking.ts +278 -0
  99. package/server-manager.ts +1318 -0
  100. package/session-recovery.ts +158 -0
  101. package/skills/README.md +64 -0
  102. package/skills/mcp-adapter/SKILL.md +332 -0
  103. package/skills/mcp-adapter/references/deploy-examples.md +259 -0
  104. package/skills/mcp-adapter/references/deploy.md +89 -0
  105. package/skills/mcp-adapter/references/generate.md +144 -0
  106. package/skills/mcp-adapter/references/resolver.md +105 -0
  107. package/skills/mcp-adapter/references/verify.md +157 -0
  108. package/skills/mcp-scripting/SKILL.md +38 -0
  109. package/skills/resolve-conflicts/SKILL.md +482 -0
  110. package/skills/resolve-conflicts/references/patterns.md +432 -0
  111. package/skills/resolve-conflicts/references/sample-plan.md +96 -0
  112. package/skills/resolve-conflicts/scripts/handle-deleted-modified.sh +183 -0
  113. package/skills/resolve-conflicts/scripts/validate-conflicts.sh +120 -0
  114. package/skills/upstream-merge/SKILL.md +486 -0
  115. package/skills/upstream-merge/references/pi-coupling-markers.md +129 -0
  116. package/skills/upstream-merge/references/special-cases.md +39 -0
  117. package/state.ts +60 -0
  118. package/tool-approval.ts +195 -0
  119. package/tool-metadata.ts +296 -0
  120. package/tool-registrar.ts +253 -0
  121. package/tool-result-renderer.ts +477 -0
  122. package/ts-shape.ts +149 -0
  123. package/types.ts +991 -0
  124. package/ui-app-bridge-helpers.ts +40 -0
  125. package/ui-resource-handler.ts +250 -0
  126. package/ui-server.ts +852 -0
  127. package/ui-session.ts +571 -0
  128. package/ui-stream-types.ts +89 -0
  129. package/ui-tool-visibility.ts +25 -0
  130. package/unix-socket-transport.ts +85 -0
  131. package/utils.ts +382 -0
package/abort.ts ADDED
@@ -0,0 +1,34 @@
1
+ export function throwIfAborted(signal?: AbortSignal): void {
2
+ if (!signal?.aborted) return;
3
+ throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "MCP request aborted"));
4
+ }
5
+
6
+ export async function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
7
+ if (!signal) return promise;
8
+ throwIfAborted(signal);
9
+ return await new Promise<T>((resolve, reject) => {
10
+ let settled = false;
11
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
12
+ const onAbort = () => {
13
+ if (settled) return;
14
+ settled = true;
15
+ cleanup();
16
+ reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "MCP request aborted")));
17
+ };
18
+ signal.addEventListener("abort", onAbort, { once: true });
19
+ promise.then(
20
+ value => {
21
+ if (settled) return;
22
+ settled = true;
23
+ cleanup();
24
+ resolve(value);
25
+ },
26
+ error => {
27
+ if (settled) return;
28
+ settled = true;
29
+ cleanup();
30
+ reject(error);
31
+ },
32
+ );
33
+ });
34
+ }
@@ -0,0 +1,7 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+
3
+ export type RenderOutput = string;
4
+
5
+ export function piRenderWrapper<T extends (...args: unknown[]) => RenderOutput>(fn: T) {
6
+ return (...args: Parameters<T>) => new Text(fn(...args), 0, 0);
7
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ProtocolElicitationForwarder — forwards MCP elicitation requests from
3
+ * downstream servers to the Agent Client via the MCP Server→Client
4
+ * `elicitation/create` reverse call.
5
+ *
6
+ * D-07: implements the `UISystem.form` interface.
7
+ *
8
+ * Threat model:
9
+ * T-12-02 (Information Disclosure): `form()` must NEVER log
10
+ * `result.content` — it may contain user PII. Only `result.action`
11
+ * ("accept"/"decline"/"cancel") may be logged.
12
+ *
13
+ * Note: This causes a double conversion (MCP → FormConfig → MCP → Agent →
14
+ * MCP → FormResult → ElicitResult) because `elicitation-handler.ts`
15
+ * already converts MCP schema to FormConfig. This is an accepted
16
+ * trade-off for architectural consistency. See RESEARCH Pitfall 3.
17
+ */
18
+
19
+ import type {
20
+ FormConfig,
21
+ FormField,
22
+ FormResult,
23
+ } from "../interfaces/host-types.ts";
24
+ import type {
25
+ ElicitRequestFormParams,
26
+ ElicitResult,
27
+ } from "@modelcontextprotocol/client";
28
+
29
+ /** Minimal Server→Client reverse-call surface the forwarder needs. */
30
+ type ElicitationForwardTarget = {
31
+ elicitInput(params: ElicitRequestFormParams): Promise<ElicitResult>;
32
+ };
33
+
34
+ /**
35
+ * Convert a `FormField` to a JSON Schema property definition.
36
+ *
37
+ * This is the reverse of `elicitation-handler.ts`'s
38
+ * `convertMcpSchemaToPiForm()`. It maps each FormField type to the
39
+ * corresponding JSON Schema property for `elicitInput`'s
40
+ * `requestedSchema`.
41
+ *
42
+ * Supported types:
43
+ * - text → { type: "string", minLength?, maxLength? }
44
+ * - number → { type: "number", minimum?, maximum? }
45
+ * - integer → { type: "integer", minimum?, maximum? }
46
+ * - boolean → { type: "boolean" }
47
+ * - select → { type: "string", enum: [...] }
48
+ * - multiSelect → { type: "array", items: { type: "string", enum: [...] } }
49
+ *
50
+ * `title` (from `field.label`) is always included when present.
51
+ * `description` (from `field.description`) is included when present.
52
+ */
53
+ export function convertFieldToSchema(field: FormField): Record<string, unknown> {
54
+ const type = field.type;
55
+ const label = field.label;
56
+ const description = (field as { description?: string }).description;
57
+
58
+ // Build the base object with optional title/description.
59
+ const buildBase = (): Record<string, unknown> => {
60
+ const base: Record<string, unknown> = {};
61
+ if (label !== undefined) base.title = label;
62
+ if (description !== undefined) base.description = description;
63
+ return base;
64
+ };
65
+
66
+ if (type === "select" || type === "multiSelect") {
67
+ const options = (field as { options?: Array<{ value: string; label?: string }> }).options ?? [];
68
+ if (type === "multiSelect") {
69
+ return {
70
+ type: "array",
71
+ items: { type: "string", enum: options.map((o) => o.value) },
72
+ ...buildBase(),
73
+ };
74
+ }
75
+ return {
76
+ type: "string",
77
+ enum: options.map((o) => o.value),
78
+ ...buildBase(),
79
+ };
80
+ }
81
+
82
+ if (type === "number" || type === "integer") {
83
+ const schema: Record<string, unknown> = { type, ...buildBase() };
84
+ const minimum = (field as { minimum?: number }).minimum;
85
+ const maximum = (field as { maximum?: number }).maximum;
86
+ if (minimum !== undefined) schema.minimum = minimum;
87
+ if (maximum !== undefined) schema.maximum = maximum;
88
+ return schema;
89
+ }
90
+
91
+ if (type === "boolean") {
92
+ return { type: "boolean", ...buildBase() };
93
+ }
94
+
95
+ // Default: text → string with optional minLength/maxLength
96
+ const schema: Record<string, unknown> = { type: "string", ...buildBase() };
97
+ const minLength = (field as { minLength?: number }).minLength;
98
+ const maxLength = (field as { maxLength?: number }).maxLength;
99
+ if (minLength !== undefined) schema.minLength = minLength;
100
+ if (maxLength !== undefined) schema.maxLength = maxLength;
101
+ return schema;
102
+ }
103
+
104
+ /**
105
+ * Forwards MCP elicitation requests to the Agent Client via
106
+ * `server.elicitInput()`.
107
+ *
108
+ * The forwarder is used when the connecting Agent Client declares
109
+ * `elicitation.form` capability. It implements the `UISystem.form`
110
+ * interface, converting `FormConfig` → `elicitInput` params and
111
+ * `ElicitResult` → `FormResult`.
112
+ *
113
+ * Action mapping:
114
+ * ElicitResult.action "accept" → FormResult.action "submit" (with values)
115
+ * ElicitResult.action "decline" → FormResult.action "secondary"
116
+ * ElicitResult.action "cancel" → FormResult.action "cancel"
117
+ */
118
+ export class ProtocolElicitationForwarder {
119
+ constructor(private readonly server: ElicitationForwardTarget) {}
120
+
121
+ /**
122
+ * Convert `FormConfig` to `ElicitRequestFormParams` and forward to
123
+ * the Agent Client via `server.elicitInput()`.
124
+ *
125
+ * T-12-02: does NOT log `result.content` (may contain user PII).
126
+ */
127
+ async form(config: FormConfig): Promise<FormResult> {
128
+ // Convert FormConfig fields back to JSON Schema for requestedSchema
129
+ const properties: Record<string, unknown> = {};
130
+ const required: string[] = [];
131
+
132
+ for (const field of config.fields) {
133
+ const schema = convertFieldToSchema(field);
134
+ properties[field.name] = schema;
135
+ if ((field as { required?: boolean }).required) {
136
+ required.push(field.name);
137
+ }
138
+ }
139
+
140
+ const result: ElicitResult = await this.server.elicitInput({
141
+ mode: "form",
142
+ message: config.message ?? "",
143
+ requestedSchema: {
144
+ type: "object" as const,
145
+ properties,
146
+ required: required.length > 0 ? required : undefined,
147
+ },
148
+ } as ElicitRequestFormParams);
149
+
150
+ // Map ElicitResult → FormResult
151
+ // T-12-02: only result.action is used for control flow — result.content
152
+ // is passed through as values without logging.
153
+ if (result.action === "accept") {
154
+ return {
155
+ action: "submit",
156
+ values: result.content as Record<string, unknown> | undefined,
157
+ };
158
+ }
159
+ if (result.action === "decline") {
160
+ return { action: "secondary" };
161
+ }
162
+ return { action: "cancel" };
163
+ }
164
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ProtocolSamplingForwarder — forwards MCP sampling requests from downstream
3
+ * servers to the Agent Client via the MCP Server→Client `sampling/createMessage`
4
+ * reverse call.
5
+ *
6
+ * D-06: implements the `SamplingProvider` interface.
7
+ * D-11: pure forwarding — no config check, no local approval. If the Agent
8
+ * Client declares `sampling` capability, mcp-adapter unconditionally
9
+ * forwards. The Agent handles user approval, LLM call, and result.
10
+ *
11
+ * Threat model:
12
+ * T-12-01 (Information Disclosure): `complete()` must NEVER log
13
+ * `request.messages` or `request.systemPrompt` — they may contain tokens
14
+ * or user content. Error paths only include the event name + error
15
+ * message, following the T-10-01 pattern from StoreAgentAdapter.
16
+ */
17
+
18
+ import type {
19
+ SamplingModel,
20
+ SamplingProvider,
21
+ SamplingRequest,
22
+ SamplingResponse,
23
+ } from "../interfaces/sampling.ts";
24
+ import type {
25
+ CreateMessageRequestParams,
26
+ CreateMessageResult,
27
+ ModelPreferences,
28
+ } from "@modelcontextprotocol/client";
29
+
30
+ /** Minimal Server→Client reverse-call surface the forwarder needs. */
31
+ type SamplingForwardTarget = {
32
+ createMessage(params: CreateMessageRequestParams): Promise<CreateMessageResult>;
33
+ };
34
+
35
+ /**
36
+ * Forwards MCP sampling requests to the Agent Client via
37
+ * `server.createMessage()`.
38
+ *
39
+ * The forwarder is injected into `McpServerManager.setSamplingConfig()`
40
+ * when the connecting Agent Client declares `sampling` capability.
41
+ */
42
+ export class ProtocolSamplingForwarder implements SamplingProvider {
43
+ constructor(private readonly server: SamplingForwardTarget) {}
44
+
45
+ /**
46
+ * Returns a placeholder model. In pure forwarding mode (D-11), the Agent
47
+ * Client handles actual model selection. The model returned here is not
48
+ * used for the LLM call — it's only passed to `complete()` which ignores it.
49
+ */
50
+ async resolveModel(_prefs?: ModelPreferences): Promise<SamplingModel | undefined> {
51
+ return { provider: "mcp-protocol", id: "forwarded" };
52
+ }
53
+
54
+ /**
55
+ * Forward the sampling request to the Agent Client via
56
+ * `server.createMessage()`.
57
+ *
58
+ * Converts `SamplingRequest.messages` to MCP `CreateMessageRequestParam`
59
+ * messages (string content → text content block), calls
60
+ * `server.createMessage()`, and maps `CreateMessageResult` to
61
+ * `SamplingResponse`.
62
+ *
63
+ * T-12-01: does NOT log `request.messages` or `request.systemPrompt`.
64
+ */
65
+ async complete(_model: SamplingModel, request: SamplingRequest): Promise<SamplingResponse> {
66
+ try {
67
+ // Convert SamplingRequest messages to MCP createMessage params.
68
+ // String content is wrapped as { type: "text", text }; array content
69
+ // (SamplingTextContent[]) is passed through.
70
+ const messages = request.messages.map((m) => ({
71
+ role: m.role,
72
+ content: typeof m.content === "string"
73
+ ? { type: "text" as const, text: m.content }
74
+ : m.content,
75
+ }));
76
+
77
+ const result: CreateMessageResult = await this.server.createMessage({
78
+ messages,
79
+ systemPrompt: request.systemPrompt,
80
+ // maxTokens is required by the MCP protocol; SamplingRequest.maxTokens
81
+ // is optional at the interface level but always present at runtime
82
+ // (the downstream server's createMessage request includes it).
83
+ maxTokens: request.maxTokens ?? 0,
84
+ });
85
+
86
+ // Map CreateMessageResult → SamplingResponse
87
+ const text = result.content.type === "text" ? result.content.text : "";
88
+ return {
89
+ text,
90
+ model: result.model,
91
+ stopReason: result.stopReason ?? "endTurn",
92
+ };
93
+ } catch (err) {
94
+ // T-12-01: only log event name + error message — never request payload.
95
+ throw new Error(
96
+ `[ProtocolSamplingForwarder] complete failed: ${(err as Error).message}`,
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * D-11: No local approval — the Agent Client handles user approval.
103
+ * Always returns `true` unconditionally.
104
+ */
105
+ async confirm(_title: string, _message: string): Promise<boolean> {
106
+ return true;
107
+ }
108
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * adapters/universal-host.ts — Universal MCP stdio host disguised as a Pi
3
+ * `ExtensionAPI`.
4
+ *
5
+ * Stage 2 (fork-host, not fork-engine): instead of the fork's parallel engine
6
+ * (`adapters/entry.ts`) driving registration through the fork `AgentAPI`
7
+ * abstraction, this class implements the *Pi* `ExtensionAPI` surface so the
8
+ * upstream `index.ts` engine (`installMcpAdapter`) can run unchanged against a
9
+ * non-Pi, MCP-stdio host.
10
+ *
11
+ * It is deliberately NOT declared as `implements ExtensionAPI` — the npm
12
+ * `@earendil-works/pi-coding-agent` types degrade `ExtensionAPI` to `any`, so
13
+ * the precise surface below is the fork-owned contract. The caller
14
+ * (`bin/mcp-server.ts`) casts to `ExtensionAPI` at the boundary.
15
+ *
16
+ * Surface implemented (evidence in upstream `index.ts`/`init.ts`/`utils.ts`):
17
+ * registerTool / registerCommand / registerFlag / getFlag / on / events /
18
+ * getAllTools / setActiveTools / getActiveTools / unregisterTool /
19
+ * sendMessage / exec (+ `tools`/`activeTools` stores for MCP ListTools
20
+ * filtering).
21
+ */
22
+
23
+ import { EventEmitter } from "node:events";
24
+ import type {
25
+ AgentChannel,
26
+ CommandConfig,
27
+ FlagConfig,
28
+ ToolInfo,
29
+ ToolRegistration,
30
+ } from "../interfaces/host-types.ts";
31
+
32
+ type EventHandler = (...args: unknown[]) => void | Promise<void>;
33
+
34
+ /** Shape of an MCP-status event bus; satisfies upstream's `McpStatusEventBus`. */
35
+ export type UniversalStatusEventBus = Pick<EventEmitter, "emit" | "on" | "removeAllListeners">;
36
+
37
+ /** The fork-owned `ExtensionAPI` contract that upstream `installMcpAdapter` needs. */
38
+ export interface UniversalHostSurface {
39
+ registerTool(tool: ToolRegistration): void;
40
+ registerCommand(name: string, config: CommandConfig): void;
41
+ registerFlag(name: string, config: FlagConfig): void;
42
+ getFlag(name: string): string | undefined;
43
+ on(event: string, handler: EventHandler): void;
44
+ getAllTools(): ToolInfo[];
45
+ setActiveTools(names: string[]): void;
46
+ getActiveTools(): string[];
47
+ unregisterTool(name: string): boolean;
48
+ sendMessage(message: unknown, options?: unknown): void;
49
+ exec(command: string, args: string[], options?: unknown): Promise<unknown>;
50
+ }
51
+
52
+ /**
53
+ * A host that presents the Pi `ExtensionAPI` surface backed by in-memory
54
+ * stores, so `index.ts`'s engine can register tools/commands/flags and wire
55
+ * session lifecycle events without a real Pi runtime.
56
+ */
57
+ export class UniversalMcpHost implements UniversalHostSurface {
58
+ /** Registered tools by name. Public so MCP `ListTools`/`CallTool` handlers can enumerate. */
59
+ readonly tools = new Map<string, ToolRegistration>();
60
+ /** Registered tools that are currently "active" (surface-visible). */
61
+ private activeTools = new Set<string>();
62
+ /** Registered commands by name (no-op surface for MCP stdio). */
63
+ readonly commands = new Map<string, CommandConfig>();
64
+ /** Registered flags by name. Value mutable so `getFlag` reflects later updates. */
65
+ readonly flags = new Map<string, FlagConfig & { value?: string }>();
66
+ /** Event handlers keyed by event name. */
67
+ private readonly handlers = new Map<string, Set<EventHandler>>();
68
+ /** Status event bus (upstream sets `state.statusEvents = pi.events`). */
69
+ readonly events: UniversalStatusEventBus = new EventEmitter();
70
+
71
+ private channel: AgentChannel | undefined;
72
+ private static readonly PREFIX = "[mcp-adapter]";
73
+
74
+ // ------------------------------------------------------------------
75
+ // ExtensionAPI surface
76
+ // ------------------------------------------------------------------
77
+
78
+ registerTool(tool: ToolRegistration): void {
79
+ this.tools.set(tool.name, tool);
80
+ // Pi auto-activates a registered tool; mirror that so `getActiveTools()`
81
+ // returns the full registered surface unless the engine explicitly
82
+ // `setActiveTools`/`unregisterTool` narrows it.
83
+ this.activeTools.add(tool.name);
84
+ }
85
+
86
+ registerCommand(name: string, config: CommandConfig): void {
87
+ this.commands.set(name, config);
88
+ }
89
+
90
+ registerFlag(name: string, config: FlagConfig): void {
91
+ this.flags.set(name, { ...config });
92
+ }
93
+
94
+ getFlag(name: string): string | undefined {
95
+ return this.flags.get(name)?.value;
96
+ }
97
+
98
+ on(event: string, handler: EventHandler): void {
99
+ let set = this.handlers.get(event);
100
+ if (!set) {
101
+ set = new Set();
102
+ this.handlers.set(event, set);
103
+ }
104
+ set.add(handler);
105
+ }
106
+
107
+ getAllTools(): ToolInfo[] {
108
+ return [...this.tools.values()].map((t) => ({ name: t.name }));
109
+ }
110
+
111
+ setActiveTools(names: string[]): void {
112
+ this.activeTools = new Set(names);
113
+ }
114
+
115
+ getActiveTools(): string[] {
116
+ return [...this.activeTools];
117
+ }
118
+
119
+ unregisterTool(name: string): boolean {
120
+ this.activeTools.delete(name);
121
+ return this.tools.delete(name);
122
+ }
123
+
124
+ sendMessage(message: unknown, _options?: unknown): void {
125
+ if (this.channel) {
126
+ void this.channel.send(message);
127
+ }
128
+ // No channel — no-op. stderr routing is handled by the channel in main().
129
+ }
130
+
131
+ /**
132
+ * Spawn a child process via node:child_process.spawn.
133
+ * Used by upstream `openUrl` (OAuth browser launch) and panels.
134
+ * No path from an untrusted MCP tool result to exec.
135
+ */
136
+ async exec(command: string, args: string[], _options?: unknown): Promise<unknown> {
137
+ const cp = (await import("node:child_process")) as typeof import("node:child_process");
138
+ return await new Promise<{
139
+ code: number | null;
140
+ stdout: string;
141
+ stderr: string;
142
+ }>((resolvePromise, reject) => {
143
+ const child = cp.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
144
+ const stdoutChunks: Buffer[] = [];
145
+ const stderrChunks: Buffer[] = [];
146
+ child.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
147
+ child.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
148
+ child.once("error", reject);
149
+ child.once("close", (code) => {
150
+ resolvePromise({
151
+ code,
152
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
153
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
154
+ });
155
+ });
156
+ });
157
+ }
158
+
159
+ // ------------------------------------------------------------------
160
+ // Companion methods (not part of ExtensionAPI)
161
+ // ------------------------------------------------------------------
162
+
163
+ /** Attach a universal AgentChannel for bidirectional communication. */
164
+ attachChannel(channel: AgentChannel): void {
165
+ this.channel = channel;
166
+ }
167
+
168
+ /** Drive a simulated `session_start` event with the supplied runtime context. */
169
+ async fireSessionStart(ctx: unknown): Promise<void> {
170
+ await this.fire("session_start", "session_start", ctx);
171
+ }
172
+
173
+ /** Drive a simulated `session_shutdown` event. */
174
+ async fireSessionShutdown(): Promise<void> {
175
+ await this.fire("session_shutdown");
176
+ }
177
+
178
+ /** Drive a simulated `input` event (keep-alive convergence hook). */
179
+ async fireInput(): Promise<void> {
180
+ await this.fire("input");
181
+ }
182
+
183
+ /** Drive a simulated `tool_result` event with the result details. */
184
+ async fireToolResult(details: unknown): Promise<void> {
185
+ await this.fire("tool_result", { details });
186
+ }
187
+
188
+ // ------------------------------------------------------------------
189
+ // Private helpers
190
+ // ------------------------------------------------------------------
191
+
192
+ /**
193
+ * Invoke every handler registered for `event`, catching handler errors so
194
+ * a single failing listener cannot break the host. Logs error prefix +
195
+ * event name + handler count only — never the event args.
196
+ */
197
+ private async fire(event: string, ...args: unknown[]): Promise<void> {
198
+ const set = this.handlers.get(event);
199
+ if (!set || set.size === 0) return;
200
+ const handlers = [...set];
201
+ await Promise.all(
202
+ handlers.map(async (h) => {
203
+ try {
204
+ await Promise.resolve(h(...args));
205
+ } catch (err) {
206
+ console.error(
207
+ `${UniversalMcpHost.PREFIX} handler error for event '${event}' (${set.size} handlers): ${(err as Error).message}`,
208
+ );
209
+ }
210
+ }),
211
+ );
212
+ }
213
+ }
package/agent-dir.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ export function getConfigDirName(): string {
6
+ const configDir = readPiConfig()?.configDir;
7
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
8
+ }
9
+
10
+ export function getAgentDir(): string {
11
+ const piConfig = readPiConfig();
12
+ const name = piConfig?.name;
13
+ const appName = typeof name === "string" && name.trim() ? name.trim() : "pi";
14
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
15
+ if (!configured) {
16
+ return join(homedir(), getConfigDirName(), "agent");
17
+ }
18
+ if (configured === "~") {
19
+ return homedir();
20
+ }
21
+ if (configured.startsWith("~/")) {
22
+ return resolve(homedir(), configured.slice(2));
23
+ }
24
+ return resolve(configured);
25
+ }
26
+
27
+ export function getAgentPath(...segments: string[]): string {
28
+ return join(getAgentDir(), ...segments);
29
+ }
30
+
31
+ /**
32
+ * What the host calls itself.
33
+ *
34
+ * pi supports rebranding through `piConfig.name` in the package.json that its
35
+ * `getPackageDir()` resolves, and distributions built on pi (arc, tau, …) point
36
+ * `PI_PACKAGE_DIR` at their own manifest. Read that manifest directly rather
37
+ * than importing pi: this package deliberately depends on pi-ai and pi-tui
38
+ * only, and `getAgentDir()` above reads its env var the same self-contained way.
39
+ *
40
+ * Falls back to "pi", which is what pi's own APP_NAME resolves to.
41
+ */
42
+ function readPiConfig(): { name?: unknown; configDir?: unknown; clientUri?: unknown } | undefined {
43
+ const dir = process.env.PI_PACKAGE_DIR?.trim()
44
+ if (!dir) return undefined
45
+ try {
46
+ const manifest = JSON.parse(readFileSync(join(resolve(dir), "package.json"), "utf8")) as {
47
+ piConfig?: { name?: unknown; configDir?: unknown; clientUri?: unknown }
48
+ }
49
+ return manifest.piConfig
50
+ } catch {
51
+ return undefined
52
+ }
53
+ }
54
+
55
+ export function getAppName(): string {
56
+ const name = readPiConfig()?.name
57
+ return typeof name === "string" && name.trim() ? name.trim() : "pi"
58
+ }
59
+
60
+ /**
61
+ * Home page the host declares for itself, via `piConfig.clientUri` in the same
62
+ * manifest that carries `piConfig.name`. Only the distribution knows its own
63
+ * URL, so this is the one place it can come from without guessing.
64
+ */
65
+ export function getAppClientUri(): string | undefined {
66
+ const uri = readPiConfig()?.clientUri
67
+ return typeof uri === "string" && uri.trim() ? uri.trim() : undefined
68
+ }