@2kw/ai-mcp-server 6.3.0-dev.117 → 6.3.0-dev.118

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.
@@ -144,7 +144,22 @@ export function planContinuation(pending, input, responseId) {
144
144
  const outputItems = toToolOutputItems(outputs);
145
145
  if (pending.length === 0) {
146
146
  const hasList = input.decisions !== undefined && input.decisions.length > 0;
147
+ // Both given is refused the same way planDecisions refuses it, regardless of pending state (#1254
148
+ // review): pending === 0 licenses only the P1 no-op, never a malformed pair of approval inputs.
149
+ if (hasList && input.decideAll !== undefined) {
150
+ throw new Error("Give either `decisions` or `decideAll` (exactly one): one decision per pending approval, or one for all. " +
151
+ "Answer relayed tool calls with `outputs`.");
152
+ }
147
153
  if (hasList) {
154
+ const seen = new Set();
155
+ const twice = new Set();
156
+ for (const d of input.decisions) {
157
+ if (seen.has(d.approvalId))
158
+ twice.add(d.approvalId);
159
+ seen.add(d.approvalId);
160
+ }
161
+ if (twice.size)
162
+ throw new Error(`Approvals decided more than once: ${[...twice].join(", ")}`);
148
163
  throw new Error(`Not pending on ${responseId}: ${input.decisions.map((d) => d.approvalId).join(", ")}`);
149
164
  }
150
165
  return { outputs: outputItems, approvals: [] };
@@ -152,7 +167,7 @@ export function planContinuation(pending, input, responseId) {
152
167
  const hasApprovalInput = (input.decisions !== undefined && input.decisions.length > 0) || input.decideAll !== undefined;
153
168
  if (!hasApprovalInput) {
154
169
  throw new Error(`${responseId} also waits for approvals: every relayed tool call and every pending approval must be answered ` +
155
- `in the same call as \`outputs\`. Undecided: ${pending.map((r) => r.id).join(", ")}`);
170
+ `in the same call as \`outputs\`. Undecided: ${pending.map((r) => String(r.id)).join(", ")}`);
156
171
  }
157
172
  return { outputs: outputItems, approvals: planDecisions(pending, input, responseId) };
158
173
  }
@@ -192,7 +207,10 @@ export function decideErrorText(error) {
192
207
  const text = formatErrorForMcp(error);
193
208
  if (!(error instanceof BackboneApiError))
194
209
  return text;
195
- const hint = RELAY_ERROR_HINTS[error.errorType];
210
+ // hasOwn: an errorType named after an Object.prototype key (e.g. "toString") must not resolve to
211
+ // an inherited member — the gateway's error code is server-controlled, but never trusted as a key
212
+ // into a plain object (#1254 review; same guard as chatUrlFor in agent-run.ts).
213
+ const hint = Object.hasOwn(RELAY_ERROR_HINTS, error.errorType) ? RELAY_ERROR_HINTS[error.errorType] : undefined;
196
214
  return hint ? `${text}\nHint: ${hint}` : text;
197
215
  }
198
216
  //# sourceMappingURL=agent-decide.js.map
@@ -2,7 +2,9 @@
2
2
  * The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
3
3
  * from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
4
4
  * only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
5
+ * The connect pause is not copied by hand: `connect-pause.ts` is a guarded byte copy of the CLI's (#1086).
5
6
  */
7
+ import { DEFAULT_CHAT_URL } from "./connect-pause.js";
6
8
  type AnyRecord = Record<string, any>;
7
9
  export type RunStatus = "completed" | "requires_approval" | "requires_tool_output" | "incomplete";
8
10
  export interface PendingApproval {
@@ -38,11 +40,11 @@ export interface PendingConnection {
38
40
  /** The egress difference an allow is re-asked for; absent otherwise. */
39
41
  destinations?: string[];
40
42
  }
41
- /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
42
- export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
43
+ export { DEFAULT_CHAT_URL };
43
44
  /**
44
45
  * The chat web host that belongs to the server's API base URL (`AI_2KW_BASE_URL`), where a member
45
- * connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped.
46
+ * connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped. The
47
+ * mapping itself is the shared `connect-pause.ts`'s {@link chatOriginFor} (#1086).
46
48
  */
47
49
  export declare function chatUrlFor(baseUrl: string | undefined, env?: NodeJS.ProcessEnv): string;
48
50
  /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
@@ -120,5 +122,4 @@ export declare function parseAgentModel(model?: string): {
120
122
  * Connectors page.
121
123
  */
122
124
  export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string, chatUrl?: string): RunEnvelope;
123
- export {};
124
125
  //# sourceMappingURL=agent-run.d.ts.map
@@ -2,33 +2,20 @@
2
2
  * The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
3
3
  * from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
4
4
  * only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
5
+ * The connect pause is not copied by hand: `connect-pause.ts` is a guarded byte copy of the CLI's (#1086).
5
6
  */
6
- /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
7
- export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
8
- /** Known API-host → chat web host mappings, as the CLI's `CHAT_URL_BY_API_HOST` (#807 R11). */
9
- const CHAT_URL_BY_API_HOST = {
10
- "api.2kw.ai": "https://chat.2kw.ai",
11
- "api-dev.2kw.ai": "https://chat-dev.2kw.ai",
12
- "backbone.manfred-kunze.dev": "https://chat.2kw.ai",
13
- "localhost:8080": "http://localhost:3000",
14
- "127.0.0.1:8080": "http://localhost:3000",
15
- };
7
+ import { chatOriginFor, connectCallIds, connectionsPhrase, DEFAULT_CHAT_URL, pendingConnectionsOf, } from "./connect-pause.js";
8
+ export { DEFAULT_CHAT_URL };
16
9
  /**
17
10
  * The chat web host that belongs to the server's API base URL (`AI_2KW_BASE_URL`), where a member
18
- * connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped.
11
+ * connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped. The
12
+ * mapping itself is the shared `connect-pause.ts`'s {@link chatOriginFor} (#1086).
19
13
  */
20
14
  export function chatUrlFor(baseUrl, env = process.env) {
21
15
  const override = env.AI_2KW_CHAT_URL?.trim();
22
16
  if (override)
23
17
  return override.replace(/\/+$/, "");
24
- try {
25
- const host = new URL(baseUrl ?? "").host;
26
- // hasOwn: a host named after an Object.prototype key must not resolve.
27
- return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] : DEFAULT_CHAT_URL;
28
- }
29
- catch {
30
- return DEFAULT_CHAT_URL;
31
- }
18
+ return chatOriginFor(baseUrl);
32
19
  }
33
20
  export const CONVERSATION_MODES = ["plan", "ask", "auto"];
34
21
  /**
@@ -94,46 +81,6 @@ function parseArguments(raw) {
94
81
  return raw;
95
82
  }
96
83
  }
97
- /** An open connector consent request; a continuation's projection carries another status. */
98
- function isOpenConnectRequest(item) {
99
- return item.type === "backbone:connector_auth_request" && (item.status === undefined || item.status === "in_progress");
100
- }
101
- function pendingConnectionsOf(output) {
102
- return output.filter(isOpenConnectRequest).map((i) => ({
103
- serverLabel: String(i.server_label),
104
- host: String(i.host),
105
- reason: String(i.reason),
106
- ...(Array.isArray(i.destinations) && i.destinations.length > 0
107
- ? { destinations: i.destinations.map((d) => String(d)) }
108
- : {}),
109
- }));
110
- }
111
- /**
112
- * The call ids a connect pause withholds from the caller: the call each request names, and every
113
- * `mcp__<label>__connect` call for a pending connector (one request stands for all of them).
114
- */
115
- function connectCallIds(output, connections) {
116
- const connectNames = new Set(connections.map((c) => `mcp__${c.serverLabel}__connect`));
117
- const ids = new Set();
118
- for (const item of output) {
119
- if (isOpenConnectRequest(item) || (item.type === "function_call" && connectNames.has(String(item.name)))) {
120
- ids.add(String(item.call_id));
121
- }
122
- }
123
- return ids;
124
- }
125
- function connectionPhrase(c) {
126
- const target = `${c.serverLabel} (${c.host})`;
127
- if (c.reason === "allow")
128
- return `allow the agent to use ${target}`;
129
- if (c.reason === "reconnect")
130
- return `reconnect ${target}`;
131
- return `connect ${target}`;
132
- }
133
- function connectionsPhrase(connections) {
134
- const phrases = connections.map(connectionPhrase);
135
- return phrases.length > 1 ? `${phrases.slice(0, -1).join(", ")} and ${phrases.at(-1)}` : phrases[0] ?? "";
136
- }
137
84
  /**
138
85
  * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
139
86
  * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
@@ -160,8 +107,12 @@ function relayNext(pendingToolCalls, ref, responseId) {
160
107
  `Then call 2kw_decide_agent_approvals with agent ${JSON.stringify(ref)}, responseId ${JSON.stringify(responseId)} and one ` +
161
108
  "`outputs` entry per call.");
162
109
  }
110
+ /** Output item types this server does not decode into anything the envelope shows (e.g. a connector-tool approval's `mcp_approval_request`). */
111
+ function extraUndecodedTypes(output) {
112
+ return [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
113
+ }
163
114
  function undecodedPause(output) {
164
- const types = [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
115
+ const types = extraUndecodedTypes(output);
165
116
  return types.length > 0
166
117
  ? `This server cannot show or answer ${types.join(", ")}.`
167
118
  : "This server cannot tell what the run waits for.";
@@ -193,8 +144,15 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
193
144
  policyClass: String(i.policy_class),
194
145
  reason: typeof i.reason === "string" && i.reason ? i.reason : null,
195
146
  }));
196
- const pendingConnections = pendingConnectionsOf(output);
197
- const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, pendingConnections)]);
147
+ // The one decoding rule (#1086): only an open request with its id, call id, label and host.
148
+ const connectRequests = pendingConnectionsOf(output);
149
+ const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
150
+ serverLabel,
151
+ host,
152
+ reason,
153
+ ...(destinations ? { destinations } : {}),
154
+ }));
155
+ const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
198
156
  const toolCalls = [];
199
157
  const pendingToolCalls = [];
200
158
  for (const item of output) {
@@ -238,17 +196,35 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
238
196
  // The engine answers a connect call itself on the continuation, so connecting first is enough
239
197
  // to also resolve a relay + connect pause (#1254 M7).
240
198
  const relay = relayNext(pendingToolCalls, ref, responseId);
199
+ // The relay window started at the pause, not at the connection (#1254 review): connecting takes
200
+ // however long it takes, and does not reset the clock the relay text below names.
241
201
  const connectPrefix = pendingConnections.length > 0
242
- ? `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors; once they have, continue as follows. `
202
+ ? `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors; once they have, continue as follows ` +
203
+ "without delay — the window below started at the pause, not at the connection. "
204
+ : "";
205
+ // Never a decideAll hint: the assistant decides only what the user decided (D12, #667). The
206
+ // approval also has to fit inside the relay window named above (#1254 review).
207
+ const approvalsSuffix = pendingApprovals.length > 0
208
+ ? " Show the pending approvals to the user and pass their decisions in the same call, within the same window."
209
+ : "";
210
+ // A connector-tool approval (mcp_approval_request) never decodes into pendingApprovals, so this
211
+ // envelope can under-report what is actually pending; say so rather than sound falsely complete
212
+ // (#1254 review). The continuation itself still refuses an incomplete answer (400
213
+ // incomplete_tool_outputs, decideErrorText's hint), this only tells the assistant to expect that.
214
+ const undecodedTypes = extraUndecodedTypes(output);
215
+ const undecodedSuffix = undecodedTypes.length > 0
216
+ ? ` This server also cannot show or answer ${undecodedTypes.join(", ")} on this response — it may hold more than ` +
217
+ "the relayed calls above (for example another pending approval); if the continuation answers " +
218
+ "400 incomplete_tool_outputs, that is why."
243
219
  : "";
244
- // Never a decideAll hint: the assistant decides only what the user decided (D12, #667).
245
- const approvalsSuffix = pendingApprovals.length > 0 ? " Show the pending approvals to the user and pass their decisions in the same call." : "";
246
- next = `${connectPrefix}${relay}${approvalsSuffix}`;
220
+ next = `${connectPrefix}${relay}${approvalsSuffix}${undecodedSuffix}`;
247
221
  }
248
222
  else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
249
- // No tool here takes previous_response_id, and a connect pause cannot be continued by
250
- // conversation (the gateway answers 400), so the honest ways on are a fresh request or a
251
- // client that continues by response id.
223
+ // A connector-only pause (no relayed call, so 2kw_decide_agent_approvals has nothing to decide,
224
+ // spec M11) has no tool here to continue it by id, and a connect pause cannot be continued by
225
+ // conversation (the gateway answers 400), so the honest ways on are a fresh request or a client
226
+ // that continues by response id (#1254 review: continueWithDecisions does send previous_response_id
227
+ // for every other pause this server can decide).
252
228
  next =
253
229
  `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors. ` +
254
230
  `This server cannot continue response ${JSON.stringify(responseId)}: once they have connected, send the request again ` +
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
3
+ * `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
4
+ * connect, allow or reconnect in chat.2kw.ai.
5
+ *
6
+ * This file is canonical. `mcp/src/lib/connect-pause.ts` and
7
+ * `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
8
+ * here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
9
+ * Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
10
+ * mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
11
+ *
12
+ * Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
13
+ * scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
14
+ * `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
15
+ */
16
+ /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
17
+ export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
18
+ /** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
19
+ export declare const CHAT_URL_BY_API_HOST: Readonly<Record<string, string>>;
20
+ /**
21
+ * The chat web host that belongs to an API base URL, where a member connects a connector
22
+ * (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
23
+ */
24
+ export declare function chatOriginFor(baseUrl: unknown): string;
25
+ /** What the user is asked to do: connect, allow the agent, or reconnect. */
26
+ export type ConnectReason = "connect" | "allow" | "reconnect";
27
+ export declare const CONNECT_REASONS: readonly ConnectReason[];
28
+ /**
29
+ * One open connect request. `id` (`cauth_<call id>`) and `callId` (the first connect call for
30
+ * this connector) are what a client needs to withhold the connect calls; the CLI and the MCP
31
+ * server drop both from what they publish.
32
+ */
33
+ export interface PendingConnection {
34
+ id: string;
35
+ callId: string;
36
+ serverLabel: string;
37
+ /** The MCP server's host: what the user is asked to connect to. */
38
+ host: string;
39
+ reason: ConnectReason;
40
+ /** The egress difference an allow is asked again for (#807 R9); absent otherwise, never empty. */
41
+ destinations?: string[];
42
+ }
43
+ /** Enough of a connection to name it in a sentence. */
44
+ export interface ConnectTarget {
45
+ serverLabel: string;
46
+ host: string;
47
+ reason: string;
48
+ }
49
+ /** The synthetic tool a connector's connect pause is raised through (#806). */
50
+ export declare function connectToolName(serverLabel: string): string;
51
+ /**
52
+ * The connectors a response's `output` still waits on: every `in_progress` connect request whose
53
+ * id no other-status projection in the same list resolved, one per id, in order. An item without
54
+ * a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
55
+ * `reason` outside the three reads as `connect`, the action that always applies; only the string
56
+ * entries of `destinations` are kept, and none at all omits the field.
57
+ */
58
+ export declare function pendingConnectionsOf(output: unknown): PendingConnection[];
59
+ /**
60
+ * The call ids a connect pause withholds from the caller: the call each pending request names;
61
+ * the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
62
+ * describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
63
+ * never the client's (R7); and every `mcp__<label>__connect` call for a connector in
64
+ * `connections` (the model may call it more than once; one request stands for all of them).
65
+ */
66
+ export declare function connectCallIds(output: unknown, connections: readonly Pick<PendingConnection, "callId" | "serverLabel">[]): Set<string>;
67
+ /** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
68
+ export declare function connectionPhrase(c: ConnectTarget): string;
69
+ /** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
70
+ export declare function connectionsPhrase(connections: readonly ConnectTarget[]): string;
71
+ //# sourceMappingURL=connect-pause.d.ts.map
@@ -0,0 +1,149 @@
1
+ // GENERATED COPY of cli/src/lib/connect-pause.ts (#1086). Do not edit here: edit the CLI's file,
2
+ // then run `npm run sync:connect-pause` in mcp/. `npm run check:connect-pause` fails on any drift.
3
+ /**
4
+ * The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
5
+ * `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
6
+ * connect, allow or reconnect in chat.2kw.ai.
7
+ *
8
+ * This file is canonical. `mcp/src/lib/connect-pause.ts` and
9
+ * `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
10
+ * here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
11
+ * Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
12
+ * mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
13
+ *
14
+ * Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
15
+ * scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
16
+ * `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
17
+ */
18
+ /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
19
+ export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
20
+ /** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
21
+ export const CHAT_URL_BY_API_HOST = {
22
+ "api.2kw.ai": "https://chat.2kw.ai",
23
+ "api-dev.2kw.ai": "https://chat-dev.2kw.ai",
24
+ "backbone.manfred-kunze.dev": "https://chat.2kw.ai",
25
+ "localhost:8080": "http://localhost:3000",
26
+ "127.0.0.1:8080": "http://localhost:3000",
27
+ };
28
+ /**
29
+ * The chat web host that belongs to an API base URL, where a member connects a connector
30
+ * (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
31
+ */
32
+ export function chatOriginFor(baseUrl) {
33
+ try {
34
+ const host = new URL(String(baseUrl ?? "")).host;
35
+ // hasOwn, not a bare index: a host named after an Object.prototype key must not resolve.
36
+ return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] ?? DEFAULT_CHAT_URL : DEFAULT_CHAT_URL;
37
+ }
38
+ catch {
39
+ return DEFAULT_CHAT_URL;
40
+ }
41
+ }
42
+ export const CONNECT_REASONS = ["connect", "allow", "reconnect"];
43
+ const CONNECT_REQUEST = "backbone:connector_auth_request";
44
+ function itemsOf(output) {
45
+ if (!Array.isArray(output))
46
+ return [];
47
+ return output.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
48
+ }
49
+ function text(value) {
50
+ return typeof value === "string" && value.length > 0 ? value : undefined;
51
+ }
52
+ /** The synthetic tool a connector's connect pause is raised through (#806). */
53
+ export function connectToolName(serverLabel) {
54
+ return `mcp__${serverLabel}__connect`;
55
+ }
56
+ /**
57
+ * The connectors a response's `output` still waits on: every `in_progress` connect request whose
58
+ * id no other-status projection in the same list resolved, one per id, in order. An item without
59
+ * a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
60
+ * `reason` outside the three reads as `connect`, the action that always applies; only the string
61
+ * entries of `destinations` are kept, and none at all omits the field.
62
+ */
63
+ export function pendingConnectionsOf(output) {
64
+ const items = itemsOf(output);
65
+ const resolved = new Set();
66
+ for (const item of items) {
67
+ const id = text(item.id);
68
+ if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
69
+ resolved.add(id);
70
+ }
71
+ const pending = new Map();
72
+ for (const item of items) {
73
+ if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
74
+ continue;
75
+ const id = text(item.id);
76
+ const callId = text(item.call_id);
77
+ const serverLabel = text(item.server_label);
78
+ const host = text(item.host);
79
+ if (!id || !callId || !serverLabel || !host || resolved.has(id) || pending.has(id))
80
+ continue;
81
+ const reason = text(item.reason);
82
+ const destinations = Array.isArray(item.destinations)
83
+ ? item.destinations.filter((entry) => typeof entry === "string")
84
+ : [];
85
+ pending.set(id, {
86
+ id,
87
+ callId,
88
+ serverLabel,
89
+ host,
90
+ reason: reason && CONNECT_REASONS.includes(reason) ? reason : "connect",
91
+ ...(destinations.length > 0 ? { destinations } : {}),
92
+ });
93
+ }
94
+ return [...pending.values()];
95
+ }
96
+ /**
97
+ * The call ids a connect pause withholds from the caller: the call each pending request names;
98
+ * the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
99
+ * describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
100
+ * never the client's (R7); and every `mcp__<label>__connect` call for a connector in
101
+ * `connections` (the model may call it more than once; one request stands for all of them).
102
+ */
103
+ export function connectCallIds(output, connections) {
104
+ const names = new Set(connections.map((c) => connectToolName(c.serverLabel)));
105
+ const ids = new Set(connections.map((c) => c.callId));
106
+ const items = itemsOf(output);
107
+ // Every open connect request's own call id is withheld, even one pendingConnectionsOf could
108
+ // not fully describe (a missing server_label or host): the paired function_call must never be
109
+ // left for the caller to fabricate an output for (R7).
110
+ const resolved = new Set();
111
+ for (const item of items) {
112
+ const id = text(item.id);
113
+ if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
114
+ resolved.add(id);
115
+ }
116
+ for (const item of items) {
117
+ if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
118
+ continue;
119
+ const id = text(item.id);
120
+ if (id && resolved.has(id))
121
+ continue;
122
+ const callId = text(item.call_id);
123
+ if (callId)
124
+ ids.add(callId);
125
+ }
126
+ for (const item of items) {
127
+ const callId = text(item.call_id);
128
+ if (item.type === "function_call" && callId && typeof item.name === "string" && names.has(item.name))
129
+ ids.add(callId);
130
+ }
131
+ return ids;
132
+ }
133
+ /** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
134
+ export function connectionPhrase(c) {
135
+ const target = `${c.serverLabel} (${c.host})`;
136
+ if (c.reason === "allow")
137
+ return `allow the agent to use ${target}`;
138
+ if (c.reason === "reconnect")
139
+ return `reconnect ${target}`;
140
+ return `connect ${target}`;
141
+ }
142
+ /** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
143
+ export function connectionsPhrase(connections) {
144
+ const phrases = connections.map(connectionPhrase);
145
+ if (phrases.length < 2)
146
+ return phrases[0] ?? "";
147
+ return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
148
+ }
149
+ //# sourceMappingURL=connect-pause.js.map
@@ -555,13 +555,14 @@ export function register(server, client) {
555
555
  + "to the user first and run it only after they agree; never pass the arguments unchecked into a shell or another tool. "
556
556
  + "Otherwise answer it with `failed: true` and output \"not available in this client\". Answer within one hour of the pause. "
557
557
  + "Pass `agent` exactly as the run was started (keep '@label' and '#model'). "
558
- + "Returns the continuation's run envelope, which can pause again: for approval, or on a connector the user must connect "
559
- + "in chat.2kw.ai → Connectors (`pendingConnections`; follow its `next`)."
558
+ + "Returns the continuation's run envelope, which can pause again: for approval, on another relayed tool call (answer it the "
559
+ + "same way), or on a connector the user must connect in chat.2kw.ai → Connectors (`pendingConnections`; follow its `next`)."
560
560
  + " " + MODE_RULE, {
561
561
  agent: z.string().min(1).describe("Agent id or name as the run used it: 'ref[@label][#model]'"),
562
562
  responseId: z.string().min(1).describe("The paused response's id (envelope `responseId`)"),
563
563
  outputs: z
564
- .array(z.object({
564
+ .array(z
565
+ .object({
565
566
  callId: z.string().min(1).describe("Envelope `pendingToolCalls[].callId`"),
566
567
  output: z
567
568
  .string()
@@ -570,17 +571,22 @@ export function register(server, client) {
570
571
  .boolean()
571
572
  .optional()
572
573
  .describe("The call failed or was declined: the tool's span ends ERROR and the agent sees `output` as the error"),
573
- }))
574
+ })
575
+ // Strict: an unrecognized key (e.g. `error`/`isError`/`status` instead of `failed`) must
576
+ // refuse the call, not silently drop the very field that tells the tool's span ERROR (#1254 review).
577
+ .strict())
574
578
  .optional()
575
579
  .describe("One entry per relayed tool call of the paused response (envelope `pendingToolCalls`), within one hour of the pause. " +
576
580
  "Combine with `decisions` or `decideAll` when approvals are pending too."),
577
581
  decisions: z
578
- .array(z.object({
582
+ .array(z
583
+ .object({
579
584
  approvalId: z.string().min(1).describe("Envelope `pendingApprovals[].approvalId`"),
580
585
  decision: z.enum(["approve", "reject"]),
581
586
  reason: z.string().optional().describe("Stored with the decision; the model sees it on a reject"),
582
587
  remember: z.boolean().optional().describe("Approve this tool for the rest of the conversation; not on destructive tools"),
583
- }))
588
+ })
589
+ .strict())
584
590
  .optional()
585
591
  .describe("One entry per pending approval. Mutually exclusive with `decideAll`."),
586
592
  decideAll: z.enum(["approve", "reject"]).optional().describe("Decide every pending approval the same way"),
@@ -35,7 +35,7 @@ export const TOOL_GROUPS = [
35
35
  {
36
36
  id: "agents",
37
37
  title: "Agents",
38
- summary: "Manage agents, their versions, labels and tool catalogs, and decide paused approvals. See [Running agents](#running-agents).",
38
+ summary: "Manage agents, their versions, labels and tool catalogs, and decide a paused run's approvals and relayed tool calls. See [Running agents](#running-agents).",
39
39
  register: (s, d) => agents.register(s, d.client),
40
40
  },
41
41
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0-dev.117",
3
+ "version": "6.3.0-dev.118",
4
4
  "description": "MCP server for 2kw.ai — EU-hosted AI platform: OpenAI-compatible LLM gateway, schema-driven document extraction, transcription, agents with a knowledge base, and cost observability. 158 tools for Claude Code, Cursor, and Windsurf.",
5
5
  "mcpName": "ai.2kw/mcp-server",
6
6
  "keywords": [
@@ -43,6 +43,8 @@
43
43
  "check:coverage": "tsx ../cli/openapi/scripts/check-coverage.ts",
44
44
  "docs:tools": "tsx scripts/tool-docs.ts",
45
45
  "check:tool-docs": "tsx scripts/tool-docs.ts --check",
46
+ "sync:connect-pause": "tsx scripts/sync-connect-pause.ts",
47
+ "check:connect-pause": "tsx scripts/sync-connect-pause.ts --check",
46
48
  "test": "tsx --test tests/*.test.ts",
47
49
  "typecheck": "tsc --noEmit"
48
50
  },