@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.
- package/dist/lib/agent-decide.js +20 -2
- package/dist/lib/agent-run.d.ts +5 -4
- package/dist/lib/agent-run.js +45 -69
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +149 -0
- package/dist/tools/agents.js +12 -6
- package/dist/tools/index.js +1 -1
- package/package.json +3 -1
package/dist/lib/agent-decide.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/lib/agent-run.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
export
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
197
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
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
|
package/dist/tools/agents.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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"),
|
package/dist/tools/index.js
CHANGED
|
@@ -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.
|
|
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
|
},
|