@2kw/ai-mcp-server 6.3.0-dev.3 → 6.3.0-dev.31

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/README.md CHANGED
@@ -42,6 +42,7 @@ Get an API key from your [2kw.ai dashboard](https://2kw.ai) — paid plans start
42
42
  |----------|----------|---------|-------------|
43
43
  | `AI_2KW_API_KEY` | Yes | — | API key for the 2kw.ai platform (legacy alias: `BACKBONE_API_KEY`) |
44
44
  | `AI_2KW_BASE_URL` | No | 2kw.ai cloud API | Point at a self-hosted deployment (legacy alias: `BACKBONE_BASE_URL`) |
45
+ | `AI_2KW_MEMORY` | No | off | `true` makes agent runs through `2kw_create_response` and `2kw_decide_agent_approvals` opt into agent memory (`X-Backbone-Memory: enabled`) |
45
46
  | `MCP_TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` |
46
47
  | `MCP_HTTP_PORT` | No | `3100` | Port for HTTP transport |
47
48
 
@@ -56,10 +57,11 @@ Get an API key from your [2kw.ai dashboard](https://2kw.ai) — paid plans start
56
57
  | **Schemas** | CRUD, versioning, labels, validation, and testing against sample documents |
57
58
  | **Prompts** | Versioned prompt management, labels, compilation, testing |
58
59
  | **Datasets & experiments** | Build datasets, run experiments, manage evaluators, record scores |
59
- | **Agents** | CRUD, versioning, labels, tool-approval history, and tool-catalog sync history |
60
+ | **Agents** | CRUD, versioning, labels, runs with the full result envelope, approval decisions, tool-approval history, and tool-catalog sync history |
60
61
  | **Conversations** | Create/inspect/delete conversations and replay their items for the responses API |
61
62
  | **Knowledge base** | CRUD, document upload/attach/versioning, hybrid search, and citation resolution |
62
63
  | **Files** | Upload, list, download, and delete files (agent input or knowledge documents) |
64
+ | **Memory** | List, read, write and delete your own agent memory files, forget everything; admins see member totals and erase a member |
63
65
  | **Observability** | Analytics (spend, quality, providers, errors), traces, billing limits |
64
66
  | **API docs** | Browse the platform API documentation by section |
65
67
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import createClient from "openapi-fetch";
1
+ import createClient, { type Middleware } from "openapi-fetch";
2
2
  import type { paths } from "./generated/openapi.js";
3
3
  export type ApiClient = ReturnType<typeof createClient<paths>> & {
4
4
  /** Exposed config for raw fetch calls (e.g. multipart uploads). */
@@ -7,8 +7,15 @@ export type ApiClient = ReturnType<typeof createClient<paths>> & {
7
7
  apiKey: string;
8
8
  };
9
9
  };
10
+ /** The per-request opt-in to agent memory for API-key runs (agent memory spec §3.3, D6, D7). */
11
+ export declare const MEMORY_HEADER = "X-Backbone-Memory";
12
+ /** Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses` only, the call that starts or resumes a run. */
13
+ export declare const memoryHeaderMiddleware: Middleware;
10
14
  /**
11
15
  * Create a typed openapi-fetch client with Bearer auth and error middleware.
16
+ * `memory: true` opts agent runs into agent memory (see {@link memoryHeaderMiddleware}).
12
17
  */
13
- export declare function createApiClient(baseUrl: string, apiKey: string): ApiClient;
18
+ export declare function createApiClient(baseUrl: string, apiKey: string, options?: {
19
+ memory?: boolean;
20
+ }): ApiClient;
14
21
  //# sourceMappingURL=client.d.ts.map
package/dist/client.js CHANGED
@@ -34,10 +34,22 @@ const errorMiddleware = {
34
34
  });
35
35
  },
36
36
  };
37
+ /** The per-request opt-in to agent memory for API-key runs (agent memory spec §3.3, D6, D7). */
38
+ export const MEMORY_HEADER = "X-Backbone-Memory";
39
+ /** Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses` only, the call that starts or resumes a run. */
40
+ export const memoryHeaderMiddleware = {
41
+ onRequest({ request }) {
42
+ if (request.method === "POST" && new URL(request.url).pathname.endsWith("/v1/responses")) {
43
+ request.headers.set(MEMORY_HEADER, "enabled");
44
+ }
45
+ return request;
46
+ },
47
+ };
37
48
  /**
38
49
  * Create a typed openapi-fetch client with Bearer auth and error middleware.
50
+ * `memory: true` opts agent runs into agent memory (see {@link memoryHeaderMiddleware}).
39
51
  */
40
- export function createApiClient(baseUrl, apiKey) {
52
+ export function createApiClient(baseUrl, apiKey, options = {}) {
41
53
  const client = createClient({
42
54
  baseUrl: baseUrl.replace(/\/+$/, ""),
43
55
  headers: {
@@ -45,6 +57,8 @@ export function createApiClient(baseUrl, apiKey) {
45
57
  },
46
58
  });
47
59
  client.use(errorMiddleware);
60
+ if (options.memory)
61
+ client.use(memoryHeaderMiddleware);
48
62
  // Expose config for raw fetch calls (multipart uploads)
49
63
  const enriched = client;
50
64
  enriched._config = { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import * as agents from "./tools/agents.js";
31
31
  import * as conversations from "./tools/conversations.js";
32
32
  import * as knowledge from "./tools/knowledge.js";
33
33
  import * as files from "./tools/files.js";
34
+ import * as memory from "./tools/memory.js";
34
35
  // Read the canonical KW_* env var; fall back to legacy BACKBONE_* for
35
36
  // backwards compatibility. Warn once on stderr when the legacy name supplied
36
37
  // the value.
@@ -67,7 +68,8 @@ async function main() {
67
68
  const baseUrl = readEnv("AI_2KW_BASE_URL", "BACKBONE_BASE_URL") ?? DEFAULT_BASE_URL;
68
69
  const transport = process.env.MCP_TRANSPORT ?? "stdio";
69
70
  const httpPort = parseInt(process.env.MCP_HTTP_PORT ?? "3100", 10);
70
- const client = createApiClient(baseUrl, apiKey);
71
+ // Opt API-key runs into agent memory (X-Backbone-Memory: enabled on responses calls, #721).
72
+ const client = createApiClient(baseUrl, apiKey, { memory: process.env.AI_2KW_MEMORY === "true" });
71
73
  const server = new McpServer({
72
74
  name: "2kw",
73
75
  version: pkg.version,
@@ -98,6 +100,7 @@ async function main() {
98
100
  conversations.register(server, client);
99
101
  knowledge.register(server, client);
100
102
  files.register(server, client);
103
+ memory.register(server, client);
101
104
  if (transport === "http") {
102
105
  const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
103
106
  const httpServer = createServer(async (req, res) => {
@@ -0,0 +1,53 @@
1
+ import type { ApiClient } from "../client.js";
2
+ import type { components } from "../generated/openapi.js";
3
+ import type { ResponsesResult } from "./agent-run.js";
4
+ /**
5
+ * Decide the approvals a paused agent run is waiting for (#667; spec 2026-09-14-cli-agents-design.md
6
+ * §4.2, D10, D11). Mirrors `cli/src/lib/agent-decide.ts` and `agent-lookup.ts`, with a per-approval
7
+ * input instead of the CLI's flags.
8
+ */
9
+ export type ApprovalRow = components["schemas"]["ToolApprovalDTO"];
10
+ export interface DecisionInput {
11
+ approvalId: string;
12
+ decision: "approve" | "reject";
13
+ reason?: string;
14
+ remember?: boolean;
15
+ }
16
+ export interface DecideInput {
17
+ decisions?: DecisionInput[];
18
+ decideAll?: "approve" | "reject";
19
+ /** Only with `decideAll`: the reason stored on every decision. */
20
+ reason?: string;
21
+ /** Only with `decideAll`: remember every approval for the conversation. */
22
+ remember?: boolean;
23
+ }
24
+ /** The #660 wire item, byte for byte what n8n Decide Approval and `bb agents decide` send. */
25
+ export interface ApprovalItem {
26
+ type: "backbone:approval_response";
27
+ approval_id: string;
28
+ decision: "approve" | "reject";
29
+ hmac: string;
30
+ reason?: string;
31
+ remember?: "conversation";
32
+ }
33
+ /** `name[@label][#model]`, split like the server: the model at the last `#`, then the label at the last `@`. */
34
+ export declare function splitAgentRef(ref: string): {
35
+ name: string;
36
+ label: string | undefined;
37
+ model: string | undefined;
38
+ };
39
+ /** A UUID is taken as the id; a name is matched exactly (the REST API only offers a substring `search`). */
40
+ export declare function resolveAgentId(client: ApiClient, name: string): Promise<string>;
41
+ export declare function fetchPendingApprovals(client: ApiClient, agentId: string, responseId: string): Promise<ApprovalRow[]>;
42
+ /**
43
+ * The backend refuses a continuation that leaves any pending approval of the paused response
44
+ * undecided (400 incomplete_tool_outputs, D10), so the whole set is checked here before sending.
45
+ */
46
+ export declare function planDecisions(pending: ApprovalRow[], input: DecideInput, responseId: string): ApprovalItem[];
47
+ /**
48
+ * Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
49
+ * instructions and model from this reference, so label and model must be the run's (D11). Never the
50
+ * echoed `name@<versionNumber>` — it would 404 as a label.
51
+ */
52
+ export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, items: ApprovalItem[], label?: string, model?: string): Promise<ResponsesResult>;
53
+ //# sourceMappingURL=agent-decide.d.ts.map
@@ -0,0 +1,121 @@
1
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ const PAGE_SIZE = 100;
3
+ const MAX_PAGES = 50;
4
+ /** `name[@label][#model]`, split like the server: the model at the last `#`, then the label at the last `@`. */
5
+ export function splitAgentRef(ref) {
6
+ const hash = ref.lastIndexOf("#");
7
+ const model = hash >= 0 ? ref.slice(hash + 1) : undefined;
8
+ const base = hash >= 0 ? ref.slice(0, hash) : ref;
9
+ const at = base.lastIndexOf("@");
10
+ if (at <= 0)
11
+ return { name: base, label: undefined, model };
12
+ return { name: base.slice(0, at), label: base.slice(at + 1), model };
13
+ }
14
+ /** A UUID is taken as the id; a name is matched exactly (the REST API only offers a substring `search`). */
15
+ export async function resolveAgentId(client, name) {
16
+ if (UUID.test(name))
17
+ return name;
18
+ for (let page = 0; page < MAX_PAGES; page++) {
19
+ const { data } = await client.GET("/v1/agents", {
20
+ // A stable sort: offset paging over an unordered result can skip rows between pages.
21
+ params: { query: { search: name, pageable: { page, size: PAGE_SIZE, sort: ["id,asc"] } } },
22
+ });
23
+ // Typed loosely, as 2kw_list_agents does: the generated page type of this endpoint is not a PageAgentDTO.
24
+ const pageData = data;
25
+ const content = pageData?.content ?? [];
26
+ const hit = content.find((a) => a.name === name);
27
+ if (hit?.id)
28
+ return String(hit.id);
29
+ if (pageData?.last !== false || content.length === 0)
30
+ break;
31
+ }
32
+ throw new Error(`Agent '${name}' not found. List agents with 2kw_list_agents.`);
33
+ }
34
+ export async function fetchPendingApprovals(client, agentId, responseId) {
35
+ const rows = [];
36
+ for (let page = 0; page < MAX_PAGES; page++) {
37
+ const { data } = await client.GET("/v1/agents/{agentId}/approvals", {
38
+ params: { path: { agentId }, query: { status: "pending", pageable: { page, size: PAGE_SIZE } } },
39
+ });
40
+ const content = data?.content ?? [];
41
+ rows.push(...content.filter((r) => r.responseId === responseId));
42
+ if (data?.last !== false || content.length === 0)
43
+ break;
44
+ }
45
+ return rows;
46
+ }
47
+ /**
48
+ * The backend refuses a continuation that leaves any pending approval of the paused response
49
+ * undecided (400 incomplete_tool_outputs, D10), so the whole set is checked here before sending.
50
+ */
51
+ export function planDecisions(pending, input, responseId) {
52
+ if (pending.length === 0) {
53
+ throw new Error(`No pending approvals for ${responseId} (already decided or superseded).`);
54
+ }
55
+ const hasList = input.decisions !== undefined && input.decisions.length > 0;
56
+ if (hasList === (input.decideAll !== undefined)) {
57
+ throw new Error("Give either `decisions` or `decideAll` (exactly one): one decision per pending approval, or one for all.");
58
+ }
59
+ const chosen = new Map();
60
+ if (input.decideAll !== undefined) {
61
+ for (const r of pending) {
62
+ chosen.set(String(r.id), { approvalId: String(r.id), decision: input.decideAll, reason: input.reason, remember: input.remember });
63
+ }
64
+ }
65
+ else {
66
+ const twice = new Set();
67
+ for (const d of input.decisions) {
68
+ if (chosen.has(d.approvalId))
69
+ twice.add(d.approvalId);
70
+ chosen.set(d.approvalId, d);
71
+ }
72
+ if (twice.size)
73
+ throw new Error(`Approvals decided more than once: ${[...twice].join(", ")}`);
74
+ const pendingIds = new Set(pending.map((r) => String(r.id)));
75
+ const foreign = [...chosen.keys()].filter((id) => !pendingIds.has(id));
76
+ if (foreign.length)
77
+ throw new Error(`Not pending on ${responseId}: ${foreign.join(", ")}`);
78
+ const undecided = [...pendingIds].filter((id) => !chosen.has(id));
79
+ if (undecided.length) {
80
+ throw new Error(`Every pending approval of a response must be decided in one call. Undecided: ${undecided.join(", ")}`);
81
+ }
82
+ }
83
+ return pending.map((r) => {
84
+ const id = String(r.id);
85
+ const d = chosen.get(id);
86
+ if (d.remember && d.decision !== "approve") {
87
+ throw new Error(`\`remember\` applies only to an approve: ${id}`);
88
+ }
89
+ if (d.remember && (r.policyClass ?? "").toUpperCase() === "DESTRUCTIVE") {
90
+ throw new Error(`\`remember\` cannot be used on a destructive tool call: ${id}`);
91
+ }
92
+ if (!r.hmac)
93
+ throw new Error(`Approval ${id} carries no hmac and cannot be decided here.`);
94
+ return {
95
+ type: "backbone:approval_response",
96
+ approval_id: id,
97
+ decision: d.decision,
98
+ hmac: r.hmac,
99
+ ...(d.reason ? { reason: d.reason } : {}),
100
+ ...(d.remember ? { remember: "conversation" } : {}),
101
+ };
102
+ });
103
+ }
104
+ /**
105
+ * Continues on `agent/<agentId>[@<label>][#<model>]`: the server resolves the continuation's tools,
106
+ * instructions and model from this reference, so label and model must be the run's (D11). Never the
107
+ * echoed `name@<versionNumber>` — it would 404 as a label.
108
+ */
109
+ export async function continueWithDecisions(client, agentId, responseId, items, label, model) {
110
+ const body = {
111
+ model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
112
+ previous_response_id: responseId,
113
+ stream: false,
114
+ input: items,
115
+ };
116
+ // The generated body type does not model backbone: extension items, hence the cast.
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ const { data } = await client.POST("/v1/responses", { body });
119
+ return data;
120
+ }
121
+ //# sourceMappingURL=agent-decide.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
3
+ * from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
4
+ * only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
5
+ */
6
+ type AnyRecord = Record<string, any>;
7
+ export type RunStatus = "completed" | "requires_approval" | "requires_tool_output" | "incomplete";
8
+ export interface PendingApproval {
9
+ approvalId: string;
10
+ callId: string;
11
+ tool: string;
12
+ arguments: unknown;
13
+ policyClass: string;
14
+ /** Why the automatic approver escalated this request to a human (#634); null when it did not. */
15
+ reason: string | null;
16
+ }
17
+ export interface ToolCallSummary {
18
+ tool: string;
19
+ callId: string;
20
+ /** `incomplete` when the server-side tool run failed (its `function_call_output` carries that status). */
21
+ status: "completed" | "incomplete";
22
+ }
23
+ export interface PendingToolCall {
24
+ tool: string;
25
+ callId: string;
26
+ arguments: unknown;
27
+ }
28
+ export interface RunEnvelope {
29
+ status: RunStatus;
30
+ /** Reserved for the server-side conversation mode (#656); always null until then. */
31
+ mode: null;
32
+ agent: string | null;
33
+ version: number | null;
34
+ responseId: string | null;
35
+ conversationId: string | null;
36
+ text: string;
37
+ toolCalls: ToolCallSummary[];
38
+ pendingApprovals: PendingApproval[];
39
+ pendingToolCalls: PendingToolCall[];
40
+ incompleteReason: string | null;
41
+ usage: {
42
+ inputTokens: number;
43
+ outputTokens: number;
44
+ } | null;
45
+ next: string | null;
46
+ }
47
+ /** Loose wire shape of a `POST /v1/responses` result; the OpenAPI spec types it as `object`. */
48
+ export interface ResponsesResult {
49
+ id?: string;
50
+ status?: string;
51
+ model?: string;
52
+ output?: AnyRecord[];
53
+ usage?: {
54
+ input_tokens?: number;
55
+ output_tokens?: number;
56
+ total_tokens?: number;
57
+ };
58
+ conversation?: {
59
+ id?: string;
60
+ } | null;
61
+ incomplete_details?: {
62
+ reason?: string;
63
+ } | null;
64
+ }
65
+ /** The assistant's text: every `output_text` part of every `message` item, in order. */
66
+ export declare function extractResponseText(result: ResponsesResult | undefined): string;
67
+ /**
68
+ * The response echoes `model` as `agent/{name}@{versionNumber}` (all digits), optionally
69
+ * followed by a `#<model>` override. Only a trailing all-digit `@N` is read as a version;
70
+ * any other `@` suffix stays part of the name.
71
+ */
72
+ export declare function parseAgentModel(model?: string): {
73
+ agent: string | null;
74
+ version: number | null;
75
+ };
76
+ /**
77
+ * @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
78
+ * the decision continues on the same label and model (D11). Falls back to the echoed agent name.
79
+ */
80
+ export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string): RunEnvelope;
81
+ export {};
82
+ //# sourceMappingURL=agent-run.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
3
+ * from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
4
+ * only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
5
+ */
6
+ /** The assistant's text: every `output_text` part of every `message` item, in order. */
7
+ export function extractResponseText(result) {
8
+ const parts = [];
9
+ for (const item of result?.output ?? []) {
10
+ if (item.type !== "message")
11
+ continue;
12
+ for (const part of item.content ?? []) {
13
+ if (part.type === "output_text" && part.text)
14
+ parts.push(part.text);
15
+ }
16
+ }
17
+ return parts.join("\n");
18
+ }
19
+ /**
20
+ * The response echoes `model` as `agent/{name}@{versionNumber}` (all digits), optionally
21
+ * followed by a `#<model>` override. Only a trailing all-digit `@N` is read as a version;
22
+ * any other `@` suffix stays part of the name.
23
+ */
24
+ export function parseAgentModel(model) {
25
+ // Strip the `#<model>` override (from the last `#`, as the server splits it) before matching.
26
+ const hash = (model ?? "").lastIndexOf("#");
27
+ const ref = hash >= 0 ? (model ?? "").slice(0, hash) : model ?? "";
28
+ const withVersion = /^agent\/(.+)@(\d+)$/.exec(ref);
29
+ if (withVersion)
30
+ return { agent: withVersion[1], version: Number(withVersion[2]) };
31
+ const bare = /^agent\/(.+)$/.exec(ref);
32
+ if (bare)
33
+ return { agent: bare[1], version: null };
34
+ return { agent: null, version: null };
35
+ }
36
+ function parseArguments(raw) {
37
+ if (typeof raw !== "string")
38
+ return raw ?? {};
39
+ try {
40
+ return JSON.parse(raw);
41
+ }
42
+ catch {
43
+ return raw;
44
+ }
45
+ }
46
+ /**
47
+ * @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
48
+ * the decision continues on the same label and model (D11). Falls back to the echoed agent name.
49
+ */
50
+ export function buildRunEnvelope(result, agentRef) {
51
+ const output = result?.output ?? [];
52
+ const { agent, version } = parseAgentModel(result?.model);
53
+ // call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
54
+ const outputStatuses = new Map(output
55
+ .filter((i) => i.type === "function_call_output")
56
+ .map((i) => [
57
+ String(i.call_id),
58
+ i.status === "incomplete" ? "incomplete" : "completed",
59
+ ]));
60
+ const pendingApprovals = output
61
+ // Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
62
+ .filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
63
+ .map((i) => ({
64
+ approvalId: String(i.id),
65
+ callId: String(i.call_id),
66
+ tool: String(i.tool),
67
+ arguments: parseArguments(i.arguments),
68
+ policyClass: String(i.policy_class),
69
+ reason: typeof i.reason === "string" && i.reason ? i.reason : null,
70
+ }));
71
+ const withheldCallIds = new Set(pendingApprovals.map((a) => a.callId));
72
+ const toolCalls = [];
73
+ const pendingToolCalls = [];
74
+ for (const item of output) {
75
+ if (item.type !== "function_call")
76
+ continue;
77
+ const callId = String(item.call_id);
78
+ const outputStatus = outputStatuses.get(callId);
79
+ if (outputStatus) {
80
+ toolCalls.push({ tool: String(item.name), callId, status: outputStatus });
81
+ }
82
+ else if (!withheldCallIds.has(callId)) {
83
+ pendingToolCalls.push({ tool: String(item.name), callId, arguments: parseArguments(item.arguments) });
84
+ }
85
+ }
86
+ let status;
87
+ switch (result?.status) {
88
+ case "completed":
89
+ status = "completed";
90
+ break;
91
+ case "incomplete":
92
+ status = "incomplete";
93
+ break;
94
+ case "requires_action":
95
+ status = pendingToolCalls.length > 0 || pendingApprovals.length === 0 ? "requires_tool_output" : "requires_approval";
96
+ break;
97
+ default:
98
+ throw new Error(`Unexpected response status: ${result?.status}`);
99
+ }
100
+ const responseId = result?.id ?? null;
101
+ const ref = agentRef ?? agent;
102
+ // Never an "approve all" hint: the assistant decides only what the user decided (D12).
103
+ const next = status === "requires_approval" && ref && responseId
104
+ ? `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)}.`
105
+ : null;
106
+ return {
107
+ status,
108
+ mode: null,
109
+ agent,
110
+ version,
111
+ responseId,
112
+ conversationId: result?.conversation?.id ?? null,
113
+ text: extractResponseText(result),
114
+ toolCalls,
115
+ pendingApprovals,
116
+ pendingToolCalls,
117
+ incompleteReason: result?.incomplete_details?.reason ?? null,
118
+ usage: result?.usage
119
+ ? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
120
+ : null,
121
+ next,
122
+ };
123
+ }
124
+ //# sourceMappingURL=agent-run.js.map
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ import { continueWithDecisions, fetchPendingApprovals, planDecisions, resolveAgentId, splitAgentRef } from "../lib/agent-decide.js";
4
+ import { buildRunEnvelope } from "../lib/agent-run.js";
3
5
  const modelSchema = z
4
6
  .string()
5
7
  .min(1)
@@ -301,6 +303,30 @@ export function register(server, client) {
301
303
  };
302
304
  }
303
305
  });
306
+ // ── list_agent_skills ───────────────────────────────────────────────────
307
+ server.tool("2kw_list_agent_skills", "List the skills an agent's latest published version binds, in authored order. This is the "
308
+ + "read a chat-only USER key may perform; 2kw_get_latest_agent_version carries the full version "
309
+ + "and needs VIEWER or above. An agent with no published version lists none.", { agentId: z.string().describe("The agent ID") }, async ({ agentId }) => {
310
+ try {
311
+ const { data } = await client.GET("/v1/agents/{agentId}/skills", {
312
+ params: { path: { agentId } },
313
+ });
314
+ const lines = (data ?? []).map((s) => {
315
+ const plugin = s.pluginName ? `, plugin: ${s.pluginName}` : "";
316
+ const description = s.description ? ` — ${s.description}` : "";
317
+ return `- ${s.name} (v${s.versionNumber}, ref: ${s.ref}${plugin})${description}`;
318
+ });
319
+ return {
320
+ content: [{ type: "text", text: `Agent skills:\n${lines.join("\n") || "(none)"}` }],
321
+ };
322
+ }
323
+ catch (error) {
324
+ return {
325
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
326
+ isError: true,
327
+ };
328
+ }
329
+ });
304
330
  // ── get_agent_version ───────────────────────────────────────────────────
305
331
  server.tool("2kw_get_agent_version", "Retrieve a specific version of an agent.", {
306
332
  agentId: z.string().describe("The agent ID"),
@@ -503,6 +529,45 @@ export function register(server, client) {
503
529
  };
504
530
  }
505
531
  });
532
+ // ── decide_agent_approvals ──────────────────────────────────────────────
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
+ + "Decide only what the user decided: show them each pending approval (tool, arguments, policy class) first and never approve on your own. "
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.", {
537
+ agent: z.string().min(1).describe("Agent id or name as the run used it: 'ref[@label][#model]'"),
538
+ responseId: z.string().min(1).describe("The paused response's id (envelope `responseId`)"),
539
+ decisions: z
540
+ .array(z.object({
541
+ approvalId: z.string().min(1).describe("Envelope `pendingApprovals[].approvalId`"),
542
+ decision: z.enum(["approve", "reject"]),
543
+ reason: z.string().optional().describe("Stored with the decision; the model sees it on a reject"),
544
+ remember: z.boolean().optional().describe("Approve this tool for the rest of the conversation; not on destructive tools"),
545
+ }))
546
+ .optional()
547
+ .describe("One entry per pending approval. Mutually exclusive with `decideAll`."),
548
+ decideAll: z.enum(["approve", "reject"]).optional().describe("Decide every pending approval the same way"),
549
+ reason: z.string().optional().describe("Only with `decideAll`: reason stored on every decision"),
550
+ remember: z.boolean().optional().describe("Only with `decideAll`: remember every approval for the conversation"),
551
+ }, async ({ agent, responseId, decisions, decideAll, reason, remember }) => {
552
+ try {
553
+ if (decisions !== undefined && (reason !== undefined || remember !== undefined)) {
554
+ throw new Error("With `decisions`, give `reason` and `remember` per entry.");
555
+ }
556
+ const { name, label, model } = splitAgentRef(agent);
557
+ const agentId = await resolveAgentId(client, name);
558
+ const pending = await fetchPendingApprovals(client, agentId, responseId);
559
+ const items = planDecisions(pending, { decisions, decideAll, reason, remember }, responseId);
560
+ const result = await continueWithDecisions(client, agentId, responseId, items, label, model);
561
+ const envelope = buildRunEnvelope(result, agent);
562
+ return { content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }] };
563
+ }
564
+ catch (error) {
565
+ return {
566
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
567
+ isError: true,
568
+ };
569
+ }
570
+ });
506
571
  // ── list_agent_tool_catalogs ─────────────────────────────────────────────
507
572
  server.tool("2kw_list_agent_tool_catalogs", "List an agent's tool-catalog sync history, newest first. Optionally narrow to one installation. The signed bytes and their signature are never returned.", {
508
573
  agentId: z.string().describe("The agent ID"),
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ import { buildRunEnvelope } from "../lib/agent-run.js";
3
4
  /**
4
5
  * The `model` value for an agent run: `agent/<ref>[@<label>]`, plus `#<model>` when the
5
6
  * request switches to another entry of the version's `models` list (#626, spec #591 §4.1).
@@ -92,7 +93,7 @@ export function register(server, client) {
92
93
  }
93
94
  });
94
95
  // ── create_response ─────────────────────────────────────────────────────
95
- 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. The reply ends with the model that answered. 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.", {
96
97
  input: z.string().min(1).describe("The input text (sent as a single user message)"),
97
98
  agent: z
98
99
  .string()
@@ -137,6 +138,17 @@ export function register(server, client) {
137
138
  body: body,
138
139
  });
139
140
  const result = data;
141
+ if (agent) {
142
+ // The reference the run used, so the envelope's `next` keeps its label and model (D11).
143
+ const agentRef = agentModel !== undefined ? `${agent}#${agentModel}` : agent;
144
+ const envelope = buildRunEnvelope(result, agentRef);
145
+ const content = [{ type: "text", text: JSON.stringify(envelope, null, 2) }];
146
+ // The echoed model is 'agent/<name>@<version>', plus '#<model>' when the request switched it (#591).
147
+ if (typeof result.model === "string") {
148
+ content.push({ type: "text", text: `[model: ${result.model}]` });
149
+ }
150
+ return { content };
151
+ }
140
152
  const parts = [];
141
153
  const output = result.output;
142
154
  for (const item of output ?? []) {
@@ -162,11 +174,6 @@ export function register(server, client) {
162
174
  if (parts.length === 0) {
163
175
  parts.push({ type: "text", text: JSON.stringify(result, null, 2) });
164
176
  }
165
- // The response echoes the model that answered: for an agent run
166
- // 'agent/<name>@<version>', plus '#<model>' when the request switched it (#591).
167
- if (agent && typeof result.model === "string") {
168
- parts.push({ type: "text", text: `[model: ${result.model}]` });
169
- }
170
177
  return { content: parts };
171
178
  }
172
179
  catch (error) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { basename } from "node:path";
4
- import { BackboneApiError, formatErrorForMcp } from "../errors.js";
4
+ import { formatErrorForMcp } from "../errors.js";
5
5
  import { getMimeType } from "../mime.js";
6
6
  const ChunkingStrategy = z.enum(["AUTO", "FIXED", "HIERARCHICAL", "CUSTOM"]);
7
7
  const DocumentStatus = z.enum(["PENDING", "PARSING", "CHUNKING", "EMBEDDING", "READY", "ERROR"]);
@@ -203,29 +203,13 @@ export function register(server, client) {
203
203
  const filename = basename(filePath);
204
204
  formData.append("files", new Blob([buffer], { type: getMimeType(filename) }), filename);
205
205
  }
206
- const { baseUrl, apiKey } = client._config;
207
- const res = await fetch(`${baseUrl}/v1/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/documents`, {
208
- method: "POST",
209
- headers: { Authorization: `Bearer ${apiKey}` },
210
- body: formData,
206
+ const { data } = await client.POST("/v1/knowledge-bases/{knowledgeBaseId}/documents", {
207
+ params: { path: { knowledgeBaseId } },
208
+ // Provide placeholder body for type checking; actual payload comes from bodySerializer
209
+ body: { files: [] },
210
+ bodySerializer: () => formData,
211
211
  });
212
- if (!res.ok) {
213
- let body;
214
- try {
215
- body = await res.json();
216
- }
217
- catch {
218
- body = {
219
- error: res.statusText,
220
- message: `HTTP ${res.status}: ${res.statusText}`,
221
- status: res.status,
222
- timestamp: new Date().toISOString(),
223
- };
224
- }
225
- throw new BackboneApiError(body);
226
- }
227
- const data = (await res.json());
228
- const lines = data.map((r) => r.error
212
+ const lines = (data ?? []).map((r) => r.error
229
213
  ? `- ${r.filename}: rejected — ${r.error}`
230
214
  : `- ${r.filename}: accepted, documentId=${r.upload?.document?.id}, versionId=${r.upload?.acceptedVersion?.id}, status=${r.upload?.acceptedVersion?.status}`);
231
215
  return {
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ /**
4
+ * Agent memory of organization members (agent memory spec §7.1, §7.4). The "my" tools act on
5
+ * the API key owner's own memory; the member tools are for ADMIN and OWNER keys and never
6
+ * show paths or content.
7
+ */
8
+ export declare function register(server: McpServer, client: ApiClient): void;
9
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { formatErrorForMcp } from "../errors.js";
3
+ async function reply(action) {
4
+ try {
5
+ const data = await action();
6
+ return {
7
+ content: [{ type: "text", text: data === undefined ? "Done." : JSON.stringify(data, null, 2) }],
8
+ };
9
+ }
10
+ catch (error) {
11
+ return { content: [{ type: "text", text: formatErrorForMcp(error) }], isError: true };
12
+ }
13
+ }
14
+ const path = z.string().min(1).describe("Memory path, /memories or below, e.g. /memories/preferences.md");
15
+ /**
16
+ * Agent memory of organization members (agent memory spec §7.1, §7.4). The "my" tools act on
17
+ * the API key owner's own memory; the member tools are for ADMIN and OWNER keys and never
18
+ * show paths or content.
19
+ */
20
+ export function register(server, client) {
21
+ server.tool("2kw_list_my_memory_files", "List the files in your agent memory (the API key owner's), ordered by path, without content.", {}, async () => reply(async () => (await client.GET("/v1/memories/me/files")).data));
22
+ server.tool("2kw_read_my_memory_file", "Read one of your agent memory files: the raw content, its version (for ifMatchVersion) and which agent wrote it last.", { path }, async ({ path }) => reply(async () => (await client.GET("/v1/memories/me/file", { params: { query: { path } } })).data));
23
+ server.tool("2kw_write_my_memory_file", "Create or replace one of your agent memory files with the complete content. Pass ifMatchVersion (the version from 2kw_read_my_memory_file) to refuse the write if the file changed since. Credentials are refused; files are limited to 32 KB.", {
24
+ path,
25
+ content: z.string().describe("The complete UTF-8 text content of the file"),
26
+ ifMatchVersion: z.number().int().min(0).optional().describe("Only replace the file if it still has this version"),
27
+ }, async ({ path, content, ifMatchVersion }) => reply(async () => (await client.PUT("/v1/memories/me/file", {
28
+ params: { query: { path } },
29
+ body: { content },
30
+ headers: ifMatchVersion === undefined ? undefined : { "If-Match": `"${ifMatchVersion}"` },
31
+ })).data));
32
+ server.tool("2kw_delete_my_memory_file", "Delete one of your agent memory files, or a directory with everything below it. Irreversible.", { path }, async ({ path }) => reply(async () => {
33
+ await client.DELETE("/v1/memories/me/file", { params: { query: { path } } });
34
+ }));
35
+ server.tool("2kw_forget_my_memory", "Delete your whole agent memory. Irreversible: confirm with the user before calling.", {}, async () => reply(async () => {
36
+ await client.DELETE("/v1/memories/me");
37
+ }));
38
+ server.tool("2kw_list_member_memory_usage", "Agent memory totals per organization member: file count, total bytes and last write. Never paths or content. Needs an ADMIN or OWNER key.", {}, async () => reply(async () => (await client.GET("/v1/memories/users")).data));
39
+ server.tool("2kw_erase_member_memory", "Erase one member's whole agent memory in this organization. Irreversible: confirm with the user before calling. Needs an ADMIN or OWNER key.", { userId: z.string().min(1).describe("The member's user id, from 2kw_list_member_memory_usage") }, async ({ userId }) => reply(async () => {
40
+ await client.DELETE("/v1/memories/users/{userId}", { params: { path: { userId } } });
41
+ }));
42
+ }
43
+ //# sourceMappingURL=memory.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0-dev.3",
3
+ "version": "6.3.0-dev.31",
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": [