@skydiveai/pi-extensions 0.1.253-beta.7 → 0.1.253

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 (2) hide show
  1. package/dist/index.mjs +138 -13
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1169,7 +1169,58 @@ function estimateToolTokens(tool) {
1169
1169
  return Math.ceil(chars / 4);
1170
1170
  }
1171
1171
  //#endregion
1172
- //#region src/extensions/mcp/tool-name.ts
1172
+ //#region src/extensions/mcp/legacy-names.ts
1173
+ /**
1174
+ * Rewrites `mcp_<server>_<tool>` names in replayed history to the current
1175
+ * `mcp__<server>__<tool>` registration. Runs on pi's `context` hook, which
1176
+ * hands us a per-call clone that is never written back to the session file,
1177
+ * so the stored history keeps the names the model actually emitted while the
1178
+ * model only ever sees the live shape (and so does not mimic the old one).
1179
+ *
1180
+ * Returns the input array untouched when nothing matched. `renamed` lists
1181
+ * the legacy names that were mapped, for one-shot logging by the caller.
1182
+ */
1183
+ function rewriteLegacyMcpToolNames({ messages, legacyNames }) {
1184
+ const renamed = /* @__PURE__ */ new Set();
1185
+ if (legacyNames.size === 0) return {
1186
+ messages,
1187
+ renamed
1188
+ };
1189
+ const out = messages.map((message) => {
1190
+ if (message.role === "assistant") {
1191
+ let changed = false;
1192
+ const content = message.content.map((block) => {
1193
+ if (block.type !== "toolCall") return block;
1194
+ const current = legacyNames.get(block.name);
1195
+ if (current === void 0) return block;
1196
+ renamed.add(block.name);
1197
+ changed = true;
1198
+ return {
1199
+ ...block,
1200
+ name: current
1201
+ };
1202
+ });
1203
+ return changed ? {
1204
+ ...message,
1205
+ content
1206
+ } : message;
1207
+ }
1208
+ if (message.role === "toolResult") {
1209
+ const current = legacyNames.get(message.toolName);
1210
+ if (current === void 0) return message;
1211
+ renamed.add(message.toolName);
1212
+ return {
1213
+ ...message,
1214
+ toolName: current
1215
+ };
1216
+ }
1217
+ return message;
1218
+ });
1219
+ return {
1220
+ messages: renamed.size > 0 ? out : messages,
1221
+ renamed
1222
+ };
1223
+ }
1173
1224
  const MAX_LENGTH = 128;
1174
1225
  /**
1175
1226
  * Deterministically coerce `raw` into the Anthropic tool-name alphabet.
@@ -1185,17 +1236,46 @@ function sanitizeToolName(raw) {
1185
1236
  return stripped === "" ? "tool" : stripped;
1186
1237
  }
1187
1238
  /**
1188
- * First free `name`, `name_2`, `name_3`, ... within the 128-char budget.
1189
- * Suffixes come out of the kept base so the whole string stays <= 128.
1239
+ * First free `name`, `name_2`, `name_3`, ... within `maxLength`. Suffixes
1240
+ * come out of the kept base so the whole string stays <= `maxLength`; the
1241
+ * caller passes the budget its names were built to (64 for MCP names, 128
1242
+ * for the Anthropic-wide limit) so a collision cannot push a name past it.
1190
1243
  */
1191
- function uniqueSanitizedToolName(base, used) {
1244
+ function uniqueSanitizedToolName({ base, used, maxLength }) {
1192
1245
  if (!used.has(base)) return base;
1193
1246
  for (let n = 2;; n++) {
1194
1247
  const suffix = `_${n}`;
1195
- const candidate = base.slice(0, MAX_LENGTH - suffix.length) + suffix;
1248
+ const candidate = base.slice(0, maxLength - suffix.length) + suffix;
1196
1249
  if (!used.has(candidate)) return candidate;
1197
1250
  }
1198
1251
  }
1252
+ /**
1253
+ * MCP tools register under Claude Code's `mcp__<server>__<tool>` shape: the
1254
+ * double-underscore separators are what Anthropic's models were trained on,
1255
+ * and they make the name splittable again (`mcp_stripe_stripe_planner` cannot
1256
+ * be split; `mcp__stripe__stripe_planner` can). Cap is 64, not Anthropic's
1257
+ * 128: `tool-history.ts` hashes replayed names over 64 and OpenAI-compatible
1258
+ * providers enforce 64, so a longer name would be unusable on replay anyway.
1259
+ */
1260
+ const MCP_TOOL_PREFIX = "mcp__";
1261
+ const MCP_SEPARATOR = "__";
1262
+ const MCP_SERVER_SEGMENT_MAX_LENGTH = 32;
1263
+ function sanitizeServerSegment(serverName) {
1264
+ return sanitizeToolName(serverName).replace(/_{2,}/g, "_").slice(0, MCP_SERVER_SEGMENT_MAX_LENGTH).replace(/_+$/, "");
1265
+ }
1266
+ function buildMcpToolName({ serverName, toolName }) {
1267
+ const head = `${MCP_TOOL_PREFIX}${sanitizeServerSegment(serverName)}${MCP_SEPARATOR}`;
1268
+ const tool = sanitizeToolName(toolName).slice(0, 64 - head.length).replace(/_+$/, "");
1269
+ return `${head}${tool === "" ? "tool" : tool}`;
1270
+ }
1271
+ /**
1272
+ * The pre-2026-10 registered shape (`mcp_<server>_<tool>`, sanitized, no
1273
+ * collision suffix). Only used to map names in replayed history onto the
1274
+ * current registration; delete with the compat window.
1275
+ */
1276
+ function legacyMcpToolName({ serverName, toolName }) {
1277
+ return sanitizeToolName(`mcp_${serverName}_${toolName}`);
1278
+ }
1199
1279
  //#endregion
1200
1280
  //#region src/extensions/mcp/mcp-config.ts
1201
1281
  /**
@@ -1296,16 +1376,23 @@ async function readConfigMtimeMs(configPath) {
1296
1376
  }
1297
1377
  }
1298
1378
  /**
1299
- * The registered tool name must satisfy Anthropic's tools[].name pattern
1300
- * (`^[a-zA-Z0-9_-]{1,128}$`) or the WHOLE Messages request hard-400s — real
1301
- * servers emit names that fail it (e.g. granola's `granola.meetings.list
1302
- * (v2)`). Sanitize here at registration time; execution still calls the
1303
- * server with the ORIGINAL name, and model tool_use round-trips on the
1304
- * sanitized one, so no reverse mapping is needed. `used` dedupes collisions
1305
- * (two distinct raw names can sanitize to the same string).
1379
+ * Registered as Claude Code's `mcp__<server>__<tool>` (see tool-name.ts), in
1380
+ * the Anthropic tools[].name alphabet or the WHOLE Messages request hard-400s
1381
+ * — real servers emit names that fail it (e.g. granola's
1382
+ * `granola.meetings.list (v2)`). Execution still calls the server with the
1383
+ * ORIGINAL name, and model tool_use round-trips on the registered one, so no
1384
+ * reverse mapping is needed. `used` dedupes collisions (two distinct raw
1385
+ * names can sanitize to the same string).
1306
1386
  */
1307
1387
  function makeToolName(serverId, toolName, used) {
1308
- return uniqueSanitizedToolName(sanitizeToolName(`mcp_${serverId}_${toolName}`), used);
1388
+ return uniqueSanitizedToolName({
1389
+ base: buildMcpToolName({
1390
+ serverName: serverId,
1391
+ toolName
1392
+ }),
1393
+ used,
1394
+ maxLength: 64
1395
+ });
1309
1396
  }
1310
1397
  function toMcpArguments(params) {
1311
1398
  const out = {};
@@ -1410,6 +1497,9 @@ var McpExtension = class {
1410
1497
  mcpClients = /* @__PURE__ */ new Map();
1411
1498
  registeredMcpToolNames = /* @__PURE__ */ new Set();
1412
1499
  usedSanitizedToolNames = /* @__PURE__ */ new Set();
1500
+ legacyToolNames = /* @__PURE__ */ new Map();
1501
+ usedLegacyToolNames = /* @__PURE__ */ new Set();
1502
+ loggedLegacyToolNames = /* @__PURE__ */ new Set();
1413
1503
  lastConfigMtimeMs = 0;
1414
1504
  pendingMcpUpdate = null;
1415
1505
  /** Non-consuming peek used by `shouldStopAfterTurn`. */
@@ -1468,6 +1558,23 @@ var McpExtension = class {
1468
1558
  }));
1469
1559
  this.registeredMcpToolNames.add(name);
1470
1560
  this.usedSanitizedToolNames.add(name);
1561
+ this.recordLegacyToolName({
1562
+ serverId,
1563
+ toolName: tool.name,
1564
+ name
1565
+ });
1566
+ }
1567
+ recordLegacyToolName({ serverId, toolName, name }) {
1568
+ const legacy = uniqueSanitizedToolName({
1569
+ base: legacyMcpToolName({
1570
+ serverName: serverId,
1571
+ toolName
1572
+ }),
1573
+ used: this.usedLegacyToolNames,
1574
+ maxLength: 128
1575
+ });
1576
+ this.usedLegacyToolNames.add(legacy);
1577
+ this.legacyToolNames.set(legacy, name);
1471
1578
  }
1472
1579
  async reconcile({ pi, configPath, connectTimeoutMs, contextWindow }) {
1473
1580
  let config;
@@ -1494,6 +1601,8 @@ var McpExtension = class {
1494
1601
  const desiredIds = new Set(Object.keys(config.servers));
1495
1602
  this.registeredMcpToolNames.clear();
1496
1603
  this.usedSanitizedToolNames.clear();
1604
+ this.legacyToolNames.clear();
1605
+ this.usedLegacyToolNames.clear();
1497
1606
  for (const [id, existing] of [...this.mcpClients.entries()]) if (!desiredIds.has(id)) {
1498
1607
  await closeConnected(existing);
1499
1608
  this.mcpClients.delete(id);
@@ -1852,6 +1961,22 @@ var McpExtension = class {
1852
1961
  }, "MCP reconcile failed");
1853
1962
  }
1854
1963
  });
1964
+ pi.on("context", (event) => {
1965
+ const { messages, renamed } = rewriteLegacyMcpToolNames({
1966
+ messages: event.messages,
1967
+ legacyNames: this.legacyToolNames
1968
+ });
1969
+ for (const legacy of renamed) {
1970
+ if (this.loggedLegacyToolNames.has(legacy)) continue;
1971
+ this.loggedLegacyToolNames.add(legacy);
1972
+ log$13.info({
1973
+ event: "mcp_tool_legacy_name",
1974
+ legacy_name: legacy,
1975
+ tool_name: this.legacyToolNames.get(legacy)
1976
+ }, "rewrote legacy MCP tool name in replayed history");
1977
+ }
1978
+ return { messages };
1979
+ });
1855
1980
  pi.on("tool_result", async (_event, ctx) => {
1856
1981
  const configPath = join(ctx.cwd, MCP_CONFIG_FILENAME);
1857
1982
  let mtime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.253-beta.7",
3
+ "version": "0.1.253",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",