@2kw/ai-mcp-server 6.3.0-dev.32 → 6.3.0-dev.40

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.
@@ -1,6 +1,6 @@
1
1
  import type { ApiClient } from "../client.js";
2
2
  import type { components } from "../generated/openapi.js";
3
- import type { ResponsesResult } from "./agent-run.js";
3
+ import { type ConversationMode, type ResponsesResult } from "./agent-run.js";
4
4
  /**
5
5
  * Decide the approvals a paused agent run is waiting for (#667; spec 2026-09-14-cli-agents-design.md
6
6
  * §4.2, D10, D11). Mirrors `cli/src/lib/agent-decide.ts` and `agent-lookup.ts`, with a per-approval
@@ -48,6 +48,7 @@ export declare function planDecisions(pending: ApprovalRow[], input: DecideInput
48
48
  * Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
49
49
  * instructions and model from this reference, so label and model must be the run's (D11). Never the
50
50
  * echoed `name@<versionNumber>` — it would 404 as a label.
51
+ * With `mode`, the `backbone:mode` item follows the decisions (#656).
51
52
  */
52
- export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, items: ApprovalItem[], label?: string, model?: string): Promise<ResponsesResult>;
53
+ export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, items: ApprovalItem[], label?: string, model?: string, mode?: ConversationMode): Promise<ResponsesResult>;
53
54
  //# sourceMappingURL=agent-decide.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { modeItem } from "./agent-run.js";
1
2
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
3
  const PAGE_SIZE = 100;
3
4
  const MAX_PAGES = 50;
@@ -105,13 +106,15 @@ export function planDecisions(pending, input, responseId) {
105
106
  * Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
106
107
  * instructions and model from this reference, so label and model must be the run's (D11). Never the
107
108
  * echoed `name@<versionNumber>` — it would 404 as a label.
109
+ * With `mode`, the `backbone:mode` item follows the decisions (#656).
108
110
  */
109
- export async function continueWithDecisions(client, agentId, responseId, items, label, model) {
111
+ export async function continueWithDecisions(client, agentId, responseId, items, label, model, mode) {
110
112
  const body = {
111
113
  model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
112
114
  previous_response_id: responseId,
113
115
  stream: false,
114
- input: items,
116
+ // With `mode`, the backbone:mode item follows the decisions (#656 D7); without it, nothing is added (D2).
117
+ input: mode ? [...items, modeItem(mode)] : items,
115
118
  };
116
119
  // The generated body type does not model backbone: extension items, hence the cast.
117
120
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -25,10 +25,28 @@ export interface PendingToolCall {
25
25
  callId: string;
26
26
  arguments: unknown;
27
27
  }
28
+ /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
29
+ export type ConversationMode = "plan" | "ask" | "auto";
30
+ export declare const CONVERSATION_MODES: readonly ConversationMode[];
31
+ /**
32
+ * Who may change the mode (#656 D8), stated in both tool descriptions: the mode outlives the request
33
+ * that sets it, so a model that switched on its own would lift the user's choice for every later turn.
34
+ */
35
+ export declare const MODE_RULE: string;
36
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
37
+ export declare function modeItem(mode: ConversationMode): {
38
+ type: "backbone:mode";
39
+ mode: ConversationMode;
40
+ };
41
+ /**
42
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
43
+ * and any value other than the three read as null, which also means "none set".
44
+ */
45
+ export declare function responseMode(result: ResponsesResult | undefined): ConversationMode | null;
28
46
  export interface RunEnvelope {
29
47
  status: RunStatus;
30
- /** Reserved for the server-side conversation mode (#656); always null until then. */
31
- mode: null;
48
+ /** The conversation mode the request ran under (`conversation_mode`); null when none is set. */
49
+ mode: ConversationMode | null;
32
50
  agent: string | null;
33
51
  version: number | null;
34
52
  responseId: string | null;
@@ -61,6 +79,7 @@ export interface ResponsesResult {
61
79
  incomplete_details?: {
62
80
  reason?: string;
63
81
  } | null;
82
+ conversation_mode?: string | null;
64
83
  }
65
84
  /** The assistant's text: every `output_text` part of every `message` item, in order. */
66
85
  export declare function extractResponseText(result: ResponsesResult | undefined): string;
@@ -3,6 +3,30 @@
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
+ export const CONVERSATION_MODES = ["plan", "ask", "auto"];
7
+ /**
8
+ * Who may change the mode (#656 D8), stated in both tool descriptions: the mode outlives the request
9
+ * that sets it, so a model that switched on its own would lift the user's choice for every later turn.
10
+ */
11
+ export const MODE_RULE = "Set `mode` only when the user explicitly asks for that mode. Never set it to retry a refused approval " +
12
+ "or to leave plan mode on your own: ask the user first. plan = read-only; ask = no automatic approver, " +
13
+ "calls that need approval wait for a person; auto = the operator's policy as written. Leave it out to " +
14
+ "keep the conversation's mode.";
15
+ function isConversationMode(value) {
16
+ return typeof value === "string" && CONVERSATION_MODES.includes(value);
17
+ }
18
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
19
+ export function modeItem(mode) {
20
+ return { type: "backbone:mode", mode };
21
+ }
22
+ /**
23
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
24
+ * and any value other than the three read as null, which also means "none set".
25
+ */
26
+ export function responseMode(result) {
27
+ const raw = result?.conversation_mode;
28
+ return isConversationMode(raw) ? raw : null;
29
+ }
6
30
  /** The assistant's text: every `output_text` part of every `message` item, in order. */
7
31
  export function extractResponseText(result) {
8
32
  const parts = [];
@@ -105,7 +129,7 @@ export function buildRunEnvelope(result, agentRef) {
105
129
  : null;
106
130
  return {
107
131
  status,
108
- mode: null,
132
+ mode: responseMode(result),
109
133
  agent,
110
134
  version,
111
135
  responseId,
@@ -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 } from "../lib/agent-run.js";
4
+ import { buildRunEnvelope, MODE_RULE } from "../lib/agent-run.js";
5
5
  const modelSchema = z
6
6
  .string()
7
7
  .min(1)
@@ -533,7 +533,8 @@ export function register(server, client) {
533
533
  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. "
534
534
  + "Decide only what the user decided: show them each pending approval (tool, arguments, policy class) first and never approve on your own. "
535
535
  + "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'). "
536
- + "Returns the continuation's run envelope, which can pause again.", {
536
+ + "Returns the continuation's run envelope, which can pause again."
537
+ + " " + MODE_RULE, {
537
538
  agent: z.string().min(1).describe("Agent id or name as the run used it: 'ref[@label][#model]'"),
538
539
  responseId: z.string().min(1).describe("The paused response's id (envelope `responseId`)"),
539
540
  decisions: z
@@ -548,7 +549,11 @@ export function register(server, client) {
548
549
  decideAll: z.enum(["approve", "reject"]).optional().describe("Decide every pending approval the same way"),
549
550
  reason: z.string().optional().describe("Only with `decideAll`: reason stored on every decision"),
550
551
  remember: z.boolean().optional().describe("Only with `decideAll`: remember every approval for the conversation"),
551
- }, async ({ agent, responseId, decisions, decideAll, reason, remember }) => {
552
+ mode: z
553
+ .enum(["plan", "ask", "auto"])
554
+ .optional()
555
+ .describe("The conversation mode this continuation and later turns run under. Only on the user's explicit request."),
556
+ }, async ({ agent, responseId, decisions, decideAll, reason, remember, mode }) => {
552
557
  try {
553
558
  if (decisions !== undefined && (reason !== undefined || remember !== undefined)) {
554
559
  throw new Error("With `decisions`, give `reason` and `remember` per entry.");
@@ -557,7 +562,7 @@ export function register(server, client) {
557
562
  const agentId = await resolveAgentId(client, name);
558
563
  const pending = await fetchPendingApprovals(client, agentId, responseId);
559
564
  const items = planDecisions(pending, { decisions, decideAll, reason, remember }, responseId);
560
- const result = await continueWithDecisions(client, agentId, responseId, items, label, model);
565
+ const result = await continueWithDecisions(client, agentId, responseId, items, label, model, mode);
561
566
  const envelope = buildRunEnvelope(result, agent);
562
567
  return { content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }] };
563
568
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
- import { buildRunEnvelope } from "../lib/agent-run.js";
3
+ import { buildRunEnvelope, 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.", {
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, {
97
97
  input: z.string().min(1).describe("The input text (sent as a single user message)"),
98
98
  agent: z
99
99
  .string()
@@ -109,7 +109,11 @@ export function register(server, client) {
109
109
  .optional()
110
110
  .describe("Model identifier in 'provider/model' format. Mutually exclusive with `agent`."),
111
111
  conversation: z.string().optional().describe("Conversation ID this response belongs to"),
112
- }, async ({ input, agent, agentModel, model, conversation }) => {
112
+ mode: z
113
+ .enum(["plan", "ask", "auto"])
114
+ .optional()
115
+ .describe("Only with `agent`: the conversation mode for this and later turns (plan, ask or auto). Only on the user's explicit request."),
116
+ }, async ({ input, agent, agentModel, model, conversation, mode }) => {
113
117
  try {
114
118
  if (agent && model) {
115
119
  throw new Error("Use either `agent` or `model`, not both.");
@@ -123,13 +127,16 @@ export function register(server, client) {
123
127
  if (agentModel !== undefined && agent?.includes("#")) {
124
128
  throw new Error("Give the model switch once: either 'ref#model' in `agent` or `agentModel`.");
125
129
  }
130
+ if (mode !== undefined && !agent) {
131
+ throw new Error("`mode` applies to agent runs; it needs `agent`.");
132
+ }
126
133
  const resolvedModel = agentModelReference(agent, agentModel) ?? model;
127
134
  const body = {
128
135
  model: resolvedModel,
129
- // The wire `input` field also accepts a bare string (shorthand for
130
- // a single user message; see ResponseItemInputDeserializer), which
131
- // the generated type does not model.
132
- input,
136
+ // Without `mode` the wire `input` is the bare string (shorthand for one user message; see
137
+ // ResponseItemInputDeserializer), which the generated type does not model. With it, the
138
+ // backbone:mode item follows the message (#656).
139
+ input: mode ? [{ type: "message", role: "user", content: input }, modeItem(mode)] : input,
133
140
  stream: false,
134
141
  ...(conversation !== undefined && { conversation }),
135
142
  };
@@ -134,6 +134,32 @@ export function register(server, client) {
134
134
  };
135
135
  }
136
136
  });
137
+ // ── cancel_conversation_turn ─────────────────────────────────────────────
138
+ server.tool("2kw_cancel_conversation_turn", "Ask a running turn of a conversation to stop. turnId is the value the client sent as the Backbone-Turn-Id header on the responses call. The stop is accepted, not confirmed: the turn's own response reports status cancelled, or completed when the stop arrived too late.", {
139
+ conversationId: z.string().describe("The conversation ID"),
140
+ turnId: z
141
+ .string()
142
+ .regex(/^[A-Za-z0-9_-]{1,64}$/)
143
+ .describe("The turn id from the Backbone-Turn-Id header: 1-64 characters of A-Z, a-z, 0-9, '_' or '-'"),
144
+ }, async ({ conversationId, turnId }) => {
145
+ try {
146
+ await client.POST("/v1/conversations/{conversationId}/cancel", {
147
+ params: { path: { conversationId } },
148
+ body: { turn_id: turnId },
149
+ });
150
+ return {
151
+ content: [
152
+ { type: "text", text: `Stop requested for turn ${turnId} of conversation ${conversationId}.` },
153
+ ],
154
+ };
155
+ }
156
+ catch (error) {
157
+ return {
158
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
159
+ isError: true,
160
+ };
161
+ }
162
+ });
137
163
  // ── list_conversation_items ──────────────────────────────────────────────
138
164
  server.tool("2kw_list_conversation_items", "Fetch a conversation's items in replay order, oldest first.", { conversationId: z.string().describe("The conversation ID") }, async ({ conversationId }) => {
139
165
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0-dev.32",
3
+ "version": "6.3.0-dev.40",
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": [