@fitchmultz/pi-mcp-adapter 8.0.11 → 8.0.13

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 CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 8.0.13 - 2026-11-03
11
+
12
+ <!-- npm-release:unreleased:start -->
13
+ <!-- npm-release:unreleased:end -->
14
+
15
+ - Enforce strict lint on MCP sampling bridge (#166) ([bb87eb1](https://github.com/fitchmultz/pi-mcp-adapter/commit/bb87eb14405d9231fb404dc8c50743b3c3757620))
16
+ - Enforce strict lint on native manager configuration tests (#168) ([998be45](https://github.com/fitchmultz/pi-mcp-adapter/commit/998be4523d16239614f4c0f0ca55006660488c23))
17
+
18
+ ### What's Changed
19
+
20
+ - Enforce strict lint on MCP sampling bridge by @fitchmultz in https://github.com/fitchmultz/pi-mcp-adapter/pull/166
21
+ - Enforce strict lint on native manager configuration tests by @fitchmultz in https://github.com/fitchmultz/pi-mcp-adapter/pull/168
22
+
23
+ **Full Changelog**: https://github.com/fitchmultz/pi-mcp-adapter/compare/v8.0.12...v8.0.13
24
+
25
+ ## 8.0.12 - 2026-11-03
26
+
27
+ <!-- npm-release:unreleased:start -->
28
+ <!-- npm-release:unreleased:end -->
29
+
30
+ ### What's Changed
31
+
32
+ - Enforce strict lint on HTTP callback lifetimes by @fitchmultz in https://github.com/fitchmultz/pi-mcp-adapter/pull/163
33
+ - Enforce strict lint on native command panels by @fitchmultz in https://github.com/fitchmultz/pi-mcp-adapter/pull/165
34
+
35
+ **Full Changelog**: https://github.com/fitchmultz/pi-mcp-adapter/compare/v8.0.11...v8.0.12
36
+
10
37
  ## 8.0.11 - 2026-11-03
11
38
 
12
39
  <!-- npm-release:unreleased:start -->
@@ -1,6 +1,6 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionUIContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
3
- import { type Client, type CreateMessageRequest, type CreateMessageResult } from "@modelcontextprotocol/client";
3
+ import type { Client, CreateMessageRequest, CreateMessageResult } from "@modelcontextprotocol/client";
4
4
  export interface SamplingHandlerOptions {
5
5
  serverName: string;
6
6
  autoApprove: boolean;
@@ -1,6 +1,5 @@
1
1
  import { truncateAtWord } from "./utils.js";
2
2
  import { throwIfAborted } from "./abort.js";
3
- import {} from "@modelcontextprotocol/client";
4
3
  export function registerSamplingHandler(client, options, own = (run) => run()) {
5
4
  // Own the handler's real promise, not the SDK caller's cancellable response facade.
6
5
  client.setRequestHandler("sampling/createMessage", (request) => own(() => handleSamplingRequest(options, request)));
@@ -9,51 +8,57 @@ export async function handleSamplingRequest(options, request) {
9
8
  const params = request.params;
10
9
  const signal = options.getSignal();
11
10
  throwIfAborted(signal);
11
+ validateSamplingFeatures(params);
12
+ const messages = params.messages.map(convertSamplingMessage);
13
+ const model = await resolveSamplingModel(options, params.modelPreferences);
14
+ throwIfAborted(signal);
15
+ await confirmSampling(options, "Approve MCP sampling request", formatRequestApproval(options.serverName, `${model.provider}/${model.id}`, params.systemPrompt, messages));
16
+ throwIfAborted(signal);
17
+ const result = await options.modelRegistry.complete(model, {
18
+ ...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
19
+ messages,
20
+ }, samplingCompletionOptions(model, params, signal));
21
+ const converted = convertAssistantResult(result);
22
+ throwIfAborted(signal);
23
+ await confirmSampling(options, "Return MCP sampling response", formatResponseApproval(options.serverName, converted));
24
+ return converted;
25
+ }
26
+ function validateSamplingFeatures(params) {
12
27
  if ("task" in params && params.task) {
13
28
  throw new Error("MCP sampling tasks are not supported");
14
29
  }
15
- if (params.includeContext && params.includeContext !== "none") {
30
+ if (params.includeContext !== undefined && params.includeContext !== "none") {
16
31
  throw new Error("MCP sampling context inclusion is not supported");
17
32
  }
18
- if (params.tools?.length) {
33
+ if (params.tools && params.tools.length > 0) {
19
34
  throw new Error("MCP sampling tool use is not supported");
20
35
  }
21
36
  if (params.toolChoice) {
22
37
  throw new Error("MCP sampling tool choice is not supported");
23
38
  }
24
- if (params.stopSequences?.length) {
39
+ if (params.stopSequences && params.stopSequences.length > 0) {
25
40
  throw new Error("MCP sampling stop sequences are not supported");
26
41
  }
27
- const messages = params.messages.map(convertSamplingMessage);
28
- const model = await resolveSamplingModel(options, params.modelPreferences);
29
- throwIfAborted(signal);
30
- await confirmSampling(options, "Approve MCP sampling request", formatRequestApproval(options.serverName, `${model.provider}/${model.id}`, params.systemPrompt, messages));
31
- throwIfAborted(signal);
42
+ }
43
+ function samplingCompletionOptions(model, params, signal) {
32
44
  const supportsTemperature = model.compat && "supportsTemperature" in model.compat
33
45
  ? model.compat.supportsTemperature !== false
34
46
  : !(model.id === "gpt-6-astra" &&
35
47
  (model.api === "openai-responses" || model.api === "openai-codex-responses"));
36
- const result = await options.modelRegistry.complete(model, {
37
- ...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
38
- messages,
39
- }, {
48
+ return {
40
49
  maxTokens: params.maxTokens,
41
50
  ...(supportsTemperature && params.temperature !== undefined
42
51
  ? { temperature: params.temperature }
43
52
  : {}),
44
53
  ...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
45
54
  ...(signal ? { signal } : {}),
46
- });
47
- const converted = convertAssistantResult(result);
48
- throwIfAborted(signal);
49
- await confirmSampling(options, "Return MCP sampling response", formatResponseApproval(options.serverName, converted));
50
- return converted;
55
+ };
51
56
  }
52
57
  function formatRequestApproval(serverName, modelName, systemPrompt, messages) {
53
58
  const lines = [
54
59
  `${serverName} wants to sample ${messages.length} message${messages.length === 1 ? "" : "s"} with ${modelName}.`,
55
60
  ];
56
- if (systemPrompt) {
61
+ if (systemPrompt !== undefined && systemPrompt !== "") {
57
62
  lines.push(`System: ${truncateAtWord(systemPrompt, 400)}`);
58
63
  }
59
64
  for (const [index, message] of messages.entries()) {
@@ -80,19 +85,16 @@ function messageText(message) {
80
85
  if (block.type === "thinking") {
81
86
  return "[thinking]";
82
87
  }
83
- if (block.type === "toolCall") {
84
- return `[tool call: ${block.name}]`;
85
- }
86
- return "[content]";
88
+ return `[tool call: ${block.name}]`;
87
89
  })
88
90
  .join("\n");
89
91
  }
90
- async function resolveSamplingModel(options, modelPreferences) {
92
+ function samplingCandidates(options, modelPreferences) {
91
93
  const candidates = [];
92
94
  const availableModels = options.modelRegistry.getAvailable();
93
95
  for (const hint of modelPreferences?.hints ?? []) {
94
- const normalizedHint = hint.name?.trim().toLowerCase();
95
- if (!normalizedHint) {
96
+ const normalizedHint = (hint.name ?? "").trim().toLowerCase();
97
+ if (normalizedHint === "") {
96
98
  continue;
97
99
  }
98
100
  for (const model of availableModels) {
@@ -109,6 +111,10 @@ async function resolveSamplingModel(options, modelPreferences) {
109
111
  for (const model of availableModels) {
110
112
  addSamplingCandidate(candidates, model);
111
113
  }
114
+ return candidates;
115
+ }
116
+ async function resolveSamplingModel(options, modelPreferences) {
117
+ const candidates = samplingCandidates(options, modelPreferences);
112
118
  const errors = [];
113
119
  const signal = options.getSignal();
114
120
  for (const model of candidates) {
@@ -188,14 +194,14 @@ function convertAssistantResult(message) {
188
194
  return block.text;
189
195
  }
190
196
  if (block.type === "thinking") {
191
- return undefined;
197
+ return;
192
198
  }
193
199
  throw new Error(`MCP sampling result ${block.type} content is not supported`);
194
200
  })
195
201
  .filter((value) => value !== undefined)
196
202
  .join("\n\n")
197
203
  .trim();
198
- if (!text) {
204
+ if (text === "") {
199
205
  throw new Error("MCP sampling result did not contain text content");
200
206
  }
201
207
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-mcp-adapter",
3
- "version": "8.0.11",
3
+ "version": "8.0.13",
4
4
  "description": "Token-efficient MCP adapter for the Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -45,6 +45,7 @@
45
45
  "lint:prompts": "oxlint --deny-warnings __tests__/prompts.test.ts __tests__/prompts-sdk-integration.test.ts __tests__/test-state.ts __tests__/test-extension-context.ts",
46
46
  "lint:runtime-tests": "oxlint --deny-warnings __tests__/runtime",
47
47
  "lint:tracing": "oxlint --deny-warnings mcp-trace.ts",
48
+ "lint:sampling": "oxlint --deny-warnings sampling-handler.ts",
48
49
  "lint:sdk-http": "oxlint --deny-warnings __tests__/sdk-v2-http.test.ts __tests__/fixtures/native-capture-extension.ts",
49
50
  "lint:lifecycle-tests": "oxlint --deny-warnings __tests__/index-lifecycle.test.ts __tests__/fixtures/extension-lifecycle.ts __tests__/test-extension-context.ts",
50
51
  "lint:oauth-recovery": "oxlint --deny-warnings __tests__/oauth-recovery-integration.test.ts __tests__/fixtures/oauth-recovery-http.ts __tests__/fixtures/oauth-recovery.ts __tests__/fixtures/panel-host.ts",