pi-mcp-adapter 3.3.0 → 4.0.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.
@@ -14,6 +14,7 @@ import type { ContentBlock, McpSettings } from "./types.ts";
14
14
  export const DEFAULT_MCP_OUTPUT_MAX_BYTES = DEFAULT_MAX_BYTES;
15
15
  export const DEFAULT_MCP_OUTPUT_MAX_LINES = DEFAULT_MAX_LINES;
16
16
  export const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;
17
+ const SCRIPT_PIPE_HINT_MIN_BYTES = 8 * 1024;
17
18
 
18
19
  const CONTENT_SUMMARY_LIMIT = 20;
19
20
  const KEY_PREVIEW_LIMIT = 20;
@@ -63,6 +64,8 @@ export interface McpOutputGuardOptions {
63
64
  enabled?: boolean;
64
65
  prefix?: string;
65
66
  suffix?: string;
67
+ /** Optional guidance appended when it fits the output limits. */
68
+ footer?: string;
66
69
  emptyTextFallback?: string;
67
70
  maxBytes?: number;
68
71
  maxLines?: number;
@@ -93,6 +96,18 @@ export function resolveMcpOutputGuardOptions(settings?: McpSettings): Pick<McpOu
93
96
  };
94
97
  }
95
98
 
99
+ /**
100
+ * Models retype large results into the next call's arguments unless told otherwise at the moment
101
+ * they see the result; the same wording in tool descriptions did not change that.
102
+ */
103
+ export function scriptPipeHint(scriptTool: boolean | undefined, content: ContentBlock[]): { footer?: string } {
104
+ if (scriptTool !== true) return {};
105
+ const bytes = content.reduce((total, block) => total + (block.type === "text" ? byteLength(block.text) : 0), 0);
106
+ return bytes >= SCRIPT_PIPE_HINT_MIN_BYTES
107
+ ? { footer: "\n\n[Use mcpScript to pass this result to another MCP call without copying it through the conversation.]" }
108
+ : {};
109
+ }
110
+
96
111
  /** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */
97
112
  export function guardedMcpDetails(guarded: GuardedMcpOutput): Record<string, unknown> {
98
113
  return {
@@ -115,6 +130,7 @@ export async function guardMcpOutput(
115
130
  const detailsMaxBytes = options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES;
116
131
  const prefix = options.prefix ?? "";
117
132
  const suffix = options.suffix ?? "";
133
+ const footer = options.footer ?? "";
118
134
 
119
135
  const normalizedContent = withEmptyTextFallback(
120
136
  content.length > 0
@@ -125,7 +141,7 @@ export async function guardMcpOutput(
125
141
 
126
142
  if (options.enabled === false) {
127
143
  return {
128
- content: addAffixes(normalizedContent, prefix, suffix),
144
+ content: addAffixes(normalizedContent, prefix, `${suffix}${footer}`),
129
145
  ...(options.rawMcpResult !== undefined ? { mcpResult: options.rawMcpResult } : {}),
130
146
  };
131
147
  }
@@ -136,15 +152,18 @@ export async function guardMcpOutput(
136
152
  .map((block) => (block as { text: string }).text)
137
153
  .join("\n");
138
154
  const composedOutput = `${prefix}${textOutput}${suffix}`;
139
- const truncation = truncateHead(composedOutput, { maxBytes, maxLines });
155
+ const truncation = truncateHead(`${composedOutput}${footer}`, { maxBytes, maxLines });
140
156
 
141
- let guardedContent: ContentBlock[] = addAffixes(normalizedContent, prefix, suffix);
157
+ let guardedContent: ContentBlock[] = addAffixes(normalizedContent, prefix, `${suffix}${footer}`);
142
158
  let outputGuard: McpOutputGuardDetails | undefined;
143
159
 
144
160
  if (truncation.truncated) {
145
161
  const { path: fullOutputPath, error: writeError } = await saveArtifact("output", composedOutput);
146
162
  const initialNotice = formatTruncationNotice(truncation, fullOutputPath, writeError);
147
- const previewBudget = reserveBudget(maxBytes, maxLines, initialNotice);
163
+ // Footers are optional guidance: drop one that would not fit beside the notice rather than exceed the limits.
164
+ const noticeWithFooter = textStats(`\n\n${initialNotice}${footer}`);
165
+ const keptFooter = noticeWithFooter.bytes < maxBytes && noticeWithFooter.lines < maxLines ? footer : "";
166
+ const previewBudget = reserveBudget(maxBytes, maxLines, `${initialNotice}${keptFooter}`);
148
167
  const preview = truncateHead(composedOutput, {
149
168
  maxBytes: previewBudget.maxBytes,
150
169
  maxLines: previewBudget.maxLines,
@@ -154,7 +173,7 @@ export async function guardMcpOutput(
154
173
  fullOutputPath,
155
174
  writeError,
156
175
  );
157
- const finalText = `${preview.content}\n\n${notice}`;
176
+ const finalText = `${preview.content}\n\n${notice}${keptFooter}`;
158
177
  const finalStats = textStats(finalText);
159
178
 
160
179
  guardedContent = [{ type: "text" as const, text: finalText }, ...imageBlocks];
@@ -129,11 +129,15 @@ const PRELUDE_SOURCE = `(function (bridge) {
129
129
  get(_target, property) {
130
130
  if (property === "search") return (input) => request("search", { input });
131
131
  if (property === "describe") return (input) => request("describe", { input });
132
- if (property === "call") return (path, args) => {
132
+ if (property === "call") return (path, args, options) => {
133
133
  if (typeof path !== "string" || path.trim() === "") {
134
134
  return Promise.resolve({ ok: false, error: { code: "invalid_tool_path", message: "tools.call(path, args) requires a non-empty tool path." } });
135
135
  }
136
- return request("call", { path, args });
136
+ const server = options?.server;
137
+ if (server !== undefined && (typeof server !== "string" || server.trim() === "")) {
138
+ return Promise.resolve({ ok: false, error: { code: "invalid_tool_server", message: "tools.call(path, args, { server }) requires a non-empty server name." } });
139
+ }
140
+ return request("call", server === undefined ? { path, args } : { path, args, server });
137
141
  };
138
142
  if (typeof property !== "string" || reserved.has(property)) return undefined;
139
143
  return (args) => request("call", { path: property, args });