pi-soly 1.9.3 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,216 @@
1
+ import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
+ import type { McpExtensionState } from "./state.ts";
3
+ import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.ts";
4
+ import { formatToolName, isToolExcluded } from "./types.ts";
5
+ import { resourceNameToToolName } from "./resource-tools.ts";
6
+ import { extractToolUiStreamMode } from "./utils.ts";
7
+
8
+ export function buildToolMetadata(
9
+ tools: McpTool[],
10
+ resources: McpResource[],
11
+ definition: ServerEntry,
12
+ serverName: string,
13
+ prefix: "server" | "none" | "short"
14
+ ): { metadata: ToolMetadata[]; failedTools: string[] } {
15
+ const metadata: ToolMetadata[] = [];
16
+ const failedTools: string[] = [];
17
+
18
+ for (const tool of tools) {
19
+ if (!tool?.name) {
20
+ failedTools.push("(unnamed)");
21
+ continue;
22
+ }
23
+ if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
24
+ continue;
25
+ }
26
+
27
+ let uiResourceUri: string | undefined;
28
+ try {
29
+ uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
30
+ } catch {
31
+ failedTools.push(tool.name);
32
+ }
33
+ metadata.push({
34
+ name: formatToolName(tool.name, serverName, prefix),
35
+ originalName: tool.name,
36
+ description: tool.description ?? "",
37
+ inputSchema: tool.inputSchema,
38
+ uiResourceUri,
39
+ uiStreamMode: extractToolUiStreamMode(tool._meta),
40
+ });
41
+ }
42
+
43
+ if (definition.exposeResources !== false) {
44
+ for (const resource of resources) {
45
+ const baseName = `get_${resourceNameToToolName(resource.name)}`;
46
+ if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
47
+ continue;
48
+ }
49
+
50
+ metadata.push({
51
+ name: formatToolName(baseName, serverName, prefix),
52
+ originalName: baseName,
53
+ description: resource.description ?? `Read resource: ${resource.uri}`,
54
+ resourceUri: resource.uri,
55
+ });
56
+ }
57
+ }
58
+
59
+ return { metadata, failedTools };
60
+ }
61
+
62
+ export function getToolNames(state: McpExtensionState, serverName: string): string[] {
63
+ return state.toolMetadata.get(serverName)?.map(m => m.name) ?? [];
64
+ }
65
+
66
+ export function totalToolCount(state: McpExtensionState): number {
67
+ let count = 0;
68
+ for (const metadata of state.toolMetadata.values()) {
69
+ count += metadata.length;
70
+ }
71
+ return count;
72
+ }
73
+
74
+ export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined {
75
+ if (!metadata) return undefined;
76
+ const exact = metadata.find(m => m.name === toolName);
77
+ if (exact) return exact;
78
+ const normalized = toolName.replace(/-/g, "_");
79
+ return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
80
+ }
81
+
82
+ export function formatSchema(schema: unknown, indent = " "): string {
83
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
84
+ return `${indent}(no schema)`;
85
+ }
86
+
87
+ const s = schema as Record<string, unknown>;
88
+
89
+ if (s.type === "object" && s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) {
90
+ const props = s.properties as Record<string, unknown>;
91
+ const required = Array.isArray(s.required) ? s.required.filter((name): name is string => typeof name === "string") : [];
92
+
93
+ if (Object.keys(props).length === 0) {
94
+ return `${indent}(no parameters)`;
95
+ }
96
+
97
+ const lines: string[] = [];
98
+ for (const [name, propSchema] of Object.entries(props)) {
99
+ lines.push(...formatProperty(name, propSchema, required.includes(name), indent));
100
+ }
101
+ return lines.join("\n");
102
+ }
103
+
104
+ const lines = formatNestedSchema(s, indent);
105
+ if (lines.length > 0) {
106
+ return lines.join("\n");
107
+ }
108
+
109
+ const typeStr = formatType(s);
110
+ if (typeStr) {
111
+ return `${indent}(${typeStr})`;
112
+ }
113
+
114
+ return `${indent}(complex schema)`;
115
+ }
116
+
117
+ function formatProperty(name: string, schema: unknown, required: boolean, indent: string): string[] {
118
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
119
+ return [`${indent}${name}${required ? " *required*" : ""}`];
120
+ }
121
+
122
+ const s = schema as Record<string, unknown>;
123
+ const parts = [`${indent}${name}`];
124
+ const typeStr = formatType(s);
125
+ if (typeStr) parts.push(`(${typeStr})`);
126
+ if (required) parts.push("*required*");
127
+ appendSchemaAnnotations(parts, s);
128
+
129
+ return [parts.join(" "), ...formatNestedSchema(s, `${indent} `)];
130
+ }
131
+
132
+ function formatNestedSchema(schema: Record<string, unknown>, indent: string): string[] {
133
+ const lines: string[] = [];
134
+
135
+ if (Array.isArray(schema.anyOf)) {
136
+ lines.push(...formatVariants("anyOf", schema.anyOf, indent));
137
+ }
138
+ if (Array.isArray(schema.oneOf)) {
139
+ lines.push(...formatVariants("oneOf", schema.oneOf, indent));
140
+ }
141
+ if (schema.items !== undefined) {
142
+ lines.push(...formatProperty("items", schema.items, false, indent));
143
+ }
144
+ if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) {
145
+ const required = Array.isArray(schema.required) ? schema.required.filter((name): name is string => typeof name === "string") : [];
146
+ for (const [name, propSchema] of Object.entries(schema.properties as Record<string, unknown>)) {
147
+ lines.push(...formatProperty(name, propSchema, required.includes(name), indent));
148
+ }
149
+ }
150
+
151
+ return lines;
152
+ }
153
+
154
+ function formatVariants(keyword: "anyOf" | "oneOf", variants: unknown[], indent: string): string[] {
155
+ const lines = [`${indent}${keyword}:`];
156
+
157
+ for (const variant of variants) {
158
+ if (!variant || typeof variant !== "object" || Array.isArray(variant)) {
159
+ lines.push(`${indent} - ${JSON.stringify(variant)}`);
160
+ continue;
161
+ }
162
+
163
+ const s = variant as Record<string, unknown>;
164
+ const typeStr = formatType(s) || "schema";
165
+ const parts = [`${indent} - ${typeStr}`];
166
+ appendSchemaAnnotations(parts, s);
167
+ lines.push(parts.join(" "));
168
+ lines.push(...formatNestedSchema(s, `${indent} `));
169
+ }
170
+
171
+ return lines;
172
+ }
173
+
174
+ function formatType(schema: Record<string, unknown>): string {
175
+ if (Object.hasOwn(schema, "const")) {
176
+ return `const ${JSON.stringify(schema.const)}`;
177
+ }
178
+
179
+ if (Array.isArray(schema.enum)) {
180
+ return `enum: ${schema.enum.map(v => JSON.stringify(v)).join(", ")}`;
181
+ }
182
+
183
+ if (Array.isArray(schema.type)) {
184
+ return schema.type.map(type => String(type)).join(" | ");
185
+ }
186
+
187
+ if (schema.type) {
188
+ return String(schema.type);
189
+ }
190
+
191
+ if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) {
192
+ return "object";
193
+ }
194
+
195
+ if (schema.items !== undefined) {
196
+ return "array";
197
+ }
198
+
199
+ return "";
200
+ }
201
+
202
+ function appendSchemaAnnotations(parts: string[], schema: Record<string, unknown>): void {
203
+ if (schema.description && typeof schema.description === "string") {
204
+ parts.push(`- ${schema.description}`);
205
+ }
206
+
207
+ for (const key of ["minLength", "maxLength", "minimum", "maximum", "minItems", "maxItems", "format", "pattern"] as const) {
208
+ if (schema[key] !== undefined) {
209
+ parts.push(`[${key}: ${JSON.stringify(schema[key])}]`);
210
+ }
211
+ }
212
+
213
+ if (schema.default !== undefined) {
214
+ parts.push(`[default: ${JSON.stringify(schema.default)}]`);
215
+ }
216
+ }
@@ -0,0 +1,46 @@
1
+ // tool-registrar.ts - MCP content transformation
2
+ // NOTE: Tools are NOT registered with Pi - only the unified `mcp` proxy tool is registered.
3
+ // This keeps the LLM context small (1 tool instead of 100s).
4
+
5
+ import type { McpContent, ContentBlock } from "./types.ts";
6
+
7
+ /**
8
+ * Transform MCP content types to Pi content blocks.
9
+ */
10
+ export function transformMcpContent(content: McpContent[]): ContentBlock[] {
11
+ return content.map(c => {
12
+ if (c.type === "text") {
13
+ return { type: "text" as const, text: c.text ?? "" };
14
+ }
15
+ if (c.type === "image") {
16
+ return {
17
+ type: "image" as const,
18
+ data: c.data ?? "",
19
+ mimeType: c.mimeType ?? "image/png",
20
+ };
21
+ }
22
+ if (c.type === "resource") {
23
+ const resourceUri = c.resource?.uri ?? "(no URI)";
24
+ const resourceContent = c.resource?.text ?? (c.resource ? JSON.stringify(c.resource) : "(no content)");
25
+ return {
26
+ type: "text" as const,
27
+ text: `[Resource: ${resourceUri}]\n${resourceContent}`,
28
+ };
29
+ }
30
+ if (c.type === "resource_link") {
31
+ const linkName = c.name ?? c.uri ?? "unknown";
32
+ const linkUri = c.uri ?? "(no URI)";
33
+ return {
34
+ type: "text" as const,
35
+ text: `[Resource Link: ${linkName}]\nURI: ${linkUri}`,
36
+ };
37
+ }
38
+ if (c.type === "audio") {
39
+ return {
40
+ type: "text" as const,
41
+ text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
42
+ };
43
+ }
44
+ return { type: "text" as const, text: JSON.stringify(c) };
45
+ });
46
+ }
@@ -0,0 +1,161 @@
1
+ import type { AgentToolResult, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+
4
+ type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
5
+ type McpToolContentBlock = AgentToolResult<McpToolResultDetails>["content"][number];
6
+
7
+ interface RenderTheme {
8
+ fg: (name: string, text: string) => string;
9
+ bold?: (text: string) => string;
10
+ }
11
+
12
+ export interface McpProxyToolCallInput {
13
+ tool?: string;
14
+ args?: string;
15
+ connect?: string;
16
+ describe?: string;
17
+ search?: string;
18
+ regex?: boolean;
19
+ includeSchemas?: boolean;
20
+ server?: string;
21
+ action?: string;
22
+ }
23
+
24
+ interface McpToolRenderContext {
25
+ isError: boolean;
26
+ }
27
+
28
+ export interface McpToolResultDisplay {
29
+ lines: string[];
30
+ truncated: boolean;
31
+ }
32
+
33
+ const DEFAULT_MAX_CALL_INPUT_CHARS = 1500;
34
+
35
+ function truncateText(value: string, maxChars: number): string {
36
+ if (value.length <= maxChars) return value;
37
+ return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
38
+ }
39
+
40
+ function formatJsonish(value: unknown, maxChars: number): string {
41
+ if (typeof value === "string") {
42
+ try {
43
+ return truncateText(JSON.stringify(JSON.parse(value), null, 2), maxChars);
44
+ } catch {
45
+ return truncateText(value, maxChars);
46
+ }
47
+ }
48
+
49
+ try {
50
+ return truncateText(JSON.stringify(value, null, 2), maxChars);
51
+ } catch {
52
+ return truncateText(String(value), maxChars);
53
+ }
54
+ }
55
+
56
+ function hasUsefulObjectContent(value: unknown): boolean {
57
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
58
+ }
59
+
60
+ export function formatMcpProxyToolCallLines(
61
+ args: McpProxyToolCallInput,
62
+ maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS,
63
+ ): string[] {
64
+ if (args.action === "ui-messages") return [`mcp ${args.action}`];
65
+
66
+ if (args.tool) {
67
+ const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
68
+ const lines = [`mcp call ${target}`];
69
+ if (args.args) lines.push(formatJsonish(args.args, maxInputChars));
70
+ return lines;
71
+ }
72
+
73
+ if (args.connect) return [`mcp connect ${args.connect}`];
74
+ if (args.describe) return [`mcp describe ${args.describe}`];
75
+
76
+ if (args.search) {
77
+ let line = `mcp search ${args.search}`;
78
+ if (args.server) line += ` @ ${args.server}`;
79
+ if (args.regex === true) line += " (regex)";
80
+ if (args.includeSchemas === false) line += " (schemas hidden)";
81
+ return [line];
82
+ }
83
+
84
+ if (args.server) return [`mcp list ${args.server}`];
85
+ if (args.action) return [`mcp ${args.action}`];
86
+
87
+ return ["mcp status"];
88
+ }
89
+
90
+ export function formatMcpDirectToolCallLines(
91
+ displayName: string,
92
+ args: Record<string, unknown>,
93
+ maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS,
94
+ ): string[] {
95
+ if (!hasUsefulObjectContent(args)) return [displayName];
96
+ return [displayName, formatJsonish(args, maxInputChars)];
97
+ }
98
+
99
+ function renderToolCallLines(lines: string[], theme: RenderTheme) {
100
+ const [title = "mcp", ...rest] = lines;
101
+ const styledTitle = theme.fg("toolTitle", theme.bold ? theme.bold(title) : title);
102
+ const styledRest = rest.map(line => theme.fg("muted", line));
103
+ return new Text([styledTitle, ...styledRest].join("\n"), 0, 0);
104
+ }
105
+
106
+ export function renderMcpProxyToolCall(args: McpProxyToolCallInput, theme: RenderTheme) {
107
+ return renderToolCallLines(formatMcpProxyToolCallLines(args), theme);
108
+ }
109
+
110
+ export function createMcpDirectToolCallRenderer(displayName: string) {
111
+ return (args: Record<string, unknown>, theme: RenderTheme) => {
112
+ return renderToolCallLines(formatMcpDirectToolCallLines(displayName, args), theme);
113
+ };
114
+ }
115
+
116
+ function blockToLines(block: McpToolContentBlock): string[] {
117
+ if (block.type === "text") {
118
+ return block.text.split("\n");
119
+ }
120
+ return [`[image: ${block.mimeType}]`];
121
+ }
122
+
123
+ export function formatMcpToolResultLines(
124
+ result: Pick<AgentToolResult<McpToolResultDetails>, "content">,
125
+ expanded: boolean,
126
+ maxCollapsedLines = 3,
127
+ ): McpToolResultDisplay {
128
+ const allLines = result.content.flatMap(blockToLines);
129
+ const lines = allLines.length > 0 ? allLines : ["(empty result)"];
130
+
131
+ if (expanded || lines.length <= maxCollapsedLines) {
132
+ return { lines, truncated: false };
133
+ }
134
+
135
+ return {
136
+ lines: [...lines.slice(0, maxCollapsedLines), "…"],
137
+ truncated: true,
138
+ };
139
+ }
140
+
141
+ export function renderMcpToolResult(
142
+ result: AgentToolResult<McpToolResultDetails>,
143
+ options: ToolRenderResultOptions,
144
+ theme: RenderTheme,
145
+ context?: McpToolRenderContext,
146
+ ) {
147
+ if (options.isPartial) {
148
+ return new Text(theme.fg("warning", "Running MCP tool..."), 0, 0);
149
+ }
150
+
151
+ const hasErrorDetails = Boolean(result.details.error);
152
+ const display = formatMcpToolResultLines(result, options.expanded || context?.isError === true || hasErrorDetails);
153
+ const output = display.lines
154
+ .map((line) => line === "…" ? theme.fg("muted", line) : theme.fg("toolOutput", line))
155
+ .join("\n");
156
+ const hint = display.truncated && !options.expanded
157
+ ? `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
158
+ : "";
159
+
160
+ return new Text(`${output}${hint}`, 0, 0);
161
+ }