@2kw/ai-mcp-server 6.3.0-dev.75 → 6.3.0-dev.84

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.
@@ -25,6 +25,26 @@ export interface PendingToolCall {
25
25
  callId: string;
26
26
  arguments: unknown;
27
27
  }
28
+ /**
29
+ * A connector the run waits for the user to connect, allow or reconnect in chat.2kw.ai: an open
30
+ * `backbone:connector_auth_request` (#807 R12). The caller never answers the connect call; the
31
+ * continuation re-checks access itself.
32
+ */
33
+ export interface PendingConnection {
34
+ serverLabel: string;
35
+ host: string;
36
+ /** `connect`, `allow` or `reconnect`. */
37
+ reason: string;
38
+ /** The egress difference an allow is re-asked for; absent otherwise. */
39
+ destinations?: string[];
40
+ }
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
+ /**
44
+ * 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
+ */
47
+ export declare function chatUrlFor(baseUrl: string | undefined, env?: NodeJS.ProcessEnv): string;
28
48
  /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
29
49
  export type ConversationMode = "plan" | "ask" | "auto";
30
50
  export declare const CONVERSATION_MODES: readonly ConversationMode[];
@@ -55,6 +75,7 @@ export interface RunEnvelope {
55
75
  toolCalls: ToolCallSummary[];
56
76
  pendingApprovals: PendingApproval[];
57
77
  pendingToolCalls: PendingToolCall[];
78
+ pendingConnections: PendingConnection[];
58
79
  incompleteReason: string | null;
59
80
  usage: {
60
81
  inputTokens: number;
@@ -95,7 +116,9 @@ export declare function parseAgentModel(model?: string): {
95
116
  /**
96
117
  * @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
97
118
  * the decision continues on the same label and model (D11). Falls back to the echoed agent name.
119
+ * @param chatUrl the chat web host of the API ({@link chatUrlFor}); a connect pause's `next` names its
120
+ * Connectors page.
98
121
  */
99
- export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string): RunEnvelope;
122
+ export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string, chatUrl?: string): RunEnvelope;
100
123
  export {};
101
124
  //# sourceMappingURL=agent-run.d.ts.map
@@ -3,6 +3,33 @@
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
5
  */
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
+ };
16
+ /**
17
+ * 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.
19
+ */
20
+ export function chatUrlFor(baseUrl, env = process.env) {
21
+ const override = env.AI_2KW_CHAT_URL?.trim();
22
+ if (override)
23
+ 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
+ }
32
+ }
6
33
  export const CONVERSATION_MODES = ["plan", "ask", "auto"];
7
34
  /**
8
35
  * Who may change the mode (#656 D8), stated in both tool descriptions: the mode outlives the request
@@ -67,11 +94,73 @@ function parseArguments(raw) {
67
94
  return raw;
68
95
  }
69
96
  }
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
+ /**
138
+ * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
139
+ * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
140
+ */
141
+ const DECODED_ITEM_TYPES = new Set([
142
+ "message",
143
+ "reasoning",
144
+ "function_call",
145
+ "function_call_output",
146
+ "backbone:approval_request",
147
+ "backbone:connector_auth_request",
148
+ "mcp_call",
149
+ "mcp_list_tools",
150
+ ]);
151
+ function undecodedPause(output) {
152
+ const types = [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
153
+ return types.length > 0
154
+ ? `This server cannot show or answer ${types.join(", ")}.`
155
+ : "This server cannot tell what the run waits for.";
156
+ }
70
157
  /**
71
158
  * @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
72
159
  * the decision continues on the same label and model (D11). Falls back to the echoed agent name.
160
+ * @param chatUrl the chat web host of the API ({@link chatUrlFor}); a connect pause's `next` names its
161
+ * Connectors page.
73
162
  */
74
- export function buildRunEnvelope(result, agentRef) {
163
+ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
75
164
  const output = result?.output ?? [];
76
165
  const { agent, version } = parseAgentModel(result?.model);
77
166
  // call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
@@ -92,7 +181,8 @@ export function buildRunEnvelope(result, agentRef) {
92
181
  policyClass: String(i.policy_class),
93
182
  reason: typeof i.reason === "string" && i.reason ? i.reason : null,
94
183
  }));
95
- const withheldCallIds = new Set(pendingApprovals.map((a) => a.callId));
184
+ const pendingConnections = pendingConnectionsOf(output);
185
+ const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, pendingConnections)]);
96
186
  const toolCalls = [];
97
187
  const pendingToolCalls = [];
98
188
  for (const item of output) {
@@ -116,17 +206,38 @@ export function buildRunEnvelope(result, agentRef) {
116
206
  status = "incomplete";
117
207
  break;
118
208
  case "requires_action":
119
- status = pendingToolCalls.length > 0 || pendingApprovals.length === 0 ? "requires_tool_output" : "requires_approval";
209
+ // A connect pause is requires_tool_output too (#807 R12): no tool here can answer it.
210
+ status =
211
+ pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
212
+ ? "requires_tool_output"
213
+ : "requires_approval";
120
214
  break;
121
215
  default:
122
216
  throw new Error(`Unexpected response status: ${result?.status}`);
123
217
  }
124
218
  const responseId = result?.id ?? null;
125
219
  const ref = agentRef ?? agent;
126
- // Never an "approve all" hint: the assistant decides only what the user decided (D12).
127
- const next = status === "requires_approval" && ref && responseId
128
- ? `Show the pending approvals to the user; after they decide each one, call 2kw_decide_agent_approvals with agent ${JSON.stringify(ref)} and responseId ${JSON.stringify(responseId)}.`
129
- : null;
220
+ let next = null;
221
+ if (status === "requires_approval" && ref && responseId) {
222
+ // Never an "approve all" hint: the assistant decides only what the user decided (D12).
223
+ next = `Show the pending approvals to the user; after they decide each one, call 2kw_decide_agent_approvals with agent ${JSON.stringify(ref)} and responseId ${JSON.stringify(responseId)}.`;
224
+ }
225
+ else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
226
+ // No tool here takes previous_response_id, and a connect pause cannot be continued by
227
+ // conversation (the gateway answers 400), so the honest ways on are a fresh request or a
228
+ // client that continues by response id.
229
+ next =
230
+ `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors. ` +
231
+ `This server cannot continue response ${JSON.stringify(responseId)}: once they have connected, send the request again ` +
232
+ "with 2kw_create_response without `conversation` (a connect pause cannot be continued by conversation), " +
233
+ "or continue the response by its id from n8n (Previous Response ID) or any client that sends previous_response_id.";
234
+ }
235
+ else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
236
+ // Paused on nothing the envelope decodes: say so, rather than hand back a pause with no way on.
237
+ next =
238
+ `${undecodedPause(output)} Response ${JSON.stringify(responseId)} stays paused: tell the user to answer it ` +
239
+ `in ${chatUrl}/, or continue it from a client that sends previous_response_id.`;
240
+ }
130
241
  return {
131
242
  status,
132
243
  mode: responseMode(result),
@@ -138,6 +249,7 @@ export function buildRunEnvelope(result, agentRef) {
138
249
  toolCalls,
139
250
  pendingApprovals,
140
251
  pendingToolCalls,
252
+ pendingConnections,
141
253
  incompleteReason: result?.incomplete_details?.reason ?? null,
142
254
  usage: result?.usage
143
255
  ? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
3
  import { continueWithDecisions, fetchPendingApprovals, planDecisions, resolveAgentId, splitAgentRef } from "../lib/agent-decide.js";
4
- import { buildRunEnvelope, MODE_RULE } from "../lib/agent-run.js";
4
+ import { buildRunEnvelope, chatUrlFor, MODE_RULE } from "../lib/agent-run.js";
5
5
  const modelSchema = z
6
6
  .string()
7
7
  .min(1)
@@ -537,7 +537,8 @@ export function register(server, client) {
537
537
  server.tool("2kw_decide_agent_approvals", "Answer the tool approvals a paused agent run is waiting for (a 2kw_create_response envelope with status requires_approval), then continue the run. "
538
538
  + "Decide only what the user decided: show them each pending approval (tool, arguments, policy class) first and never approve on your own. "
539
539
  + "Every pending approval of the response must be decided in this one call. Pass `agent` exactly as the run was started (keep '@label' and '#model'). "
540
- + "Returns the continuation's run envelope, which can pause again."
540
+ + "Returns the continuation's run envelope, which can pause again: for approval, or on a connector the user must connect "
541
+ + "in chat.2kw.ai → Connectors (`pendingConnections`; follow its `next`)."
541
542
  + " " + MODE_RULE, {
542
543
  agent: z.string().min(1).describe("Agent id or name as the run used it: 'ref[@label][#model]'"),
543
544
  responseId: z.string().min(1).describe("The paused response's id (envelope `responseId`)"),
@@ -567,7 +568,7 @@ export function register(server, client) {
567
568
  const pending = await fetchPendingApprovals(client, agentId, responseId);
568
569
  const items = planDecisions(pending, { decisions, decideAll, reason, remember }, responseId);
569
570
  const result = await continueWithDecisions(client, agentId, responseId, items, label, model, mode);
570
- const envelope = buildRunEnvelope(result, agent);
571
+ const envelope = buildRunEnvelope(result, agent, chatUrlFor(client._config?.baseUrl));
571
572
  return { content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }] };
572
573
  }
573
574
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
- import { buildRunEnvelope, MODE_RULE, modeItem } from "../lib/agent-run.js";
3
+ import { buildRunEnvelope, chatUrlFor, MODE_RULE, modeItem } from "../lib/agent-run.js";
4
4
  /**
5
5
  * The `model` value for an agent run: `agent/<ref>[@<label>]`, plus `#<model>` when the
6
6
  * request switches to another entry of the version's `models` list (#626, spec #591 §4.1).
@@ -93,7 +93,7 @@ export function register(server, client) {
93
93
  }
94
94
  });
95
95
  // ── create_response ─────────────────────────────────────────────────────
96
- server.tool("2kw_create_response", "Send a request through Backbone's OpenAI-compatible OpenResponses endpoint (POST /v1/responses). Model format: 'provider/model' for a direct gateway call via `model`, or use `agent` to invoke a stored agent by id or name (optionally 'ref@label', e.g. 'support-bot@latest'). With `agent`, `agentModel` runs this one request on another model from the agent version's `models` list. With `agent`, the reply is the run envelope as JSON (status, text, tool calls, pending approvals, response id, next step) followed by the model that answered; answer a pause with 2kw_decide_agent_approvals. Always non-streaming. " + MODE_RULE, {
96
+ server.tool("2kw_create_response", "Send a request through Backbone's OpenAI-compatible OpenResponses endpoint (POST /v1/responses). Model format: 'provider/model' for a direct gateway call via `model`, or use `agent` to invoke a stored agent by id or name (optionally 'ref@label', e.g. 'support-bot@latest'). With `agent`, `agentModel` runs this one request on another model from the agent version's `models` list. With `agent`, the reply is the run envelope as JSON (status, text, tool calls, pending approvals, response id, next step) followed by the model that answered; answer an approval pause with 2kw_decide_agent_approvals. A run that needs the user's own sign-in to a connector pauses with status requires_tool_output and `pendingConnections` (label, host, reason): no tool here can answer that; tell the user to connect it in chat.2kw.ai → Connectors, as the envelope's `next` says. Always non-streaming. " + MODE_RULE, {
97
97
  input: z.string().min(1).describe("The input text (sent as a single user message)"),
98
98
  agent: z
99
99
  .string()
@@ -148,7 +148,7 @@ export function register(server, client) {
148
148
  if (agent) {
149
149
  // The reference the run used, so the envelope's `next` keeps its label and model (D11).
150
150
  const agentRef = agentModel !== undefined ? `${agent}#${agentModel}` : agent;
151
- const envelope = buildRunEnvelope(result, agentRef);
151
+ const envelope = buildRunEnvelope(result, agentRef, chatUrlFor(client._config?.baseUrl));
152
152
  const content = [{ type: "text", text: JSON.stringify(envelope, null, 2) }];
153
153
  // The echoed model is 'agent/<name>@<version>', plus '#<model>' when the request switched it (#591).
154
154
  if (typeof result.model === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0-dev.75",
3
+ "version": "6.3.0-dev.84",
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": [