@2kw/ai-mcp-server 7.0.0 → 7.1.0-dev.14

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
@@ -1,6 +1,6 @@
1
1
  # 2kw.ai MCP Server
2
2
 
3
- An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server for [2kw.ai](https://2kw.ai) — the EU-hosted AI integration platform. It gives AI coding assistants like Claude Code, Cursor, and Windsurf direct access to an [OpenAI-compatible LLM gateway](https://2kw.ai/gateway), [schema-driven document extraction](https://2kw.ai/extraction), audio transcription, custom agents with a knowledge base, and [cost observability](https://2kw.ai/observability) — 158 tools, straight from your editor.
3
+ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server for [2kw.ai](https://2kw.ai) — the EU-hosted AI integration platform. It gives AI coding assistants like Claude Code, Cursor, and Windsurf direct access to an [OpenAI-compatible LLM gateway](https://2kw.ai/gateway), [schema-driven document extraction](https://2kw.ai/extraction), audio transcription, custom agents with a knowledge base, and [cost observability](https://2kw.ai/observability) — 199 tools, straight from your editor.
4
4
 
5
5
  Typical prompts once installed: *"extract the line items from this invoice PDF"*, *"create a schema for purchase orders and test it against these samples"*, *"transcribe this call recording"*, *"what did our extraction traffic cost this week?"*.
6
6
 
package/dist/client.js CHANGED
@@ -31,6 +31,8 @@ const errorMiddleware = {
31
31
  body.error?.message ??
32
32
  `HTTP ${response.status}: ${response.statusText}`,
33
33
  timestamp: body.timestamp ?? new Date().toISOString(),
34
+ // A background run's 409 names its newest response, so the caller can poll it.
35
+ headResponseId: typeof body.error?.head_response_id === "string" ? body.error.head_response_id : undefined,
34
36
  });
35
37
  },
36
38
  };
package/dist/errors.d.ts CHANGED
@@ -3,11 +3,14 @@ interface ApiErrorBody {
3
3
  message: string;
4
4
  status: number;
5
5
  timestamp: string;
6
+ /** The background run's newest response, named by a 409 the run's state answers. */
7
+ headResponseId?: string;
6
8
  }
7
9
  export declare class BackboneApiError extends Error {
8
10
  readonly status: number;
9
11
  readonly errorType: string;
10
12
  readonly timestamp: string;
13
+ readonly headResponseId?: string;
11
14
  constructor(apiError: ApiErrorBody);
12
15
  }
13
16
  export declare function formatErrorForMcp(error: unknown): string;
package/dist/errors.js CHANGED
@@ -2,17 +2,20 @@ export class BackboneApiError extends Error {
2
2
  status;
3
3
  errorType;
4
4
  timestamp;
5
+ headResponseId;
5
6
  constructor(apiError) {
6
7
  super(apiError.message);
7
8
  this.name = "BackboneApiError";
8
9
  this.status = apiError.status;
9
10
  this.errorType = apiError.error;
10
11
  this.timestamp = apiError.timestamp;
12
+ this.headResponseId = apiError.headResponseId;
11
13
  }
12
14
  }
13
15
  export function formatErrorForMcp(error) {
14
16
  if (error instanceof BackboneApiError) {
15
- return `API Error (${error.status}): ${error.message}`;
17
+ const head = error.headResponseId ? ` (newest response: ${error.headResponseId})` : "";
18
+ return `API Error (${error.status}): ${error.message}${head}`;
16
19
  }
17
20
  if (error instanceof Error) {
18
21
  return `Error: ${error.message}`;
@@ -78,7 +78,8 @@ function parseArguments(raw) {
78
78
  /**
79
79
  * A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
80
80
  * is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
81
- * id, policy class or status, and exists only while the decision is open, so its id doubles as the call id
81
+ * id or status (its policy class is an optional extension, #1084; "unknown" without it), and exists only
82
+ * while the decision is open, so its id doubles as the call id
82
83
  * and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
83
84
  */
84
85
  function connectorApproval(i) {
@@ -87,7 +88,7 @@ function connectorApproval(i) {
87
88
  callId: String(i.id),
88
89
  tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
89
90
  arguments: parseArguments(i.arguments),
90
- policyClass: "unknown",
91
+ policyClass: typeof i.policy_class === "string" ? i.policy_class : "unknown",
91
92
  reason: null,
92
93
  preview: null,
93
94
  };
@@ -274,15 +275,12 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
274
275
  }
275
276
  else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
276
277
  // A connector-only pause (no relayed call, so 2kw_decide_agent_approvals has nothing to decide,
277
- // spec M11) has no tool here to continue it by id, and a connect pause cannot be continued by
278
- // conversation (the gateway answers 400), so the honest ways on are a fresh request or a client
279
- // that continues by response id (#1254 review: continueWithDecisions does send previous_response_id
280
- // for every other pause this server can decide).
278
+ // spec M11) is continued by id with 2kw_create_response `previousResponseId` (#1065); a connect
279
+ // pause cannot be continued by conversation (the gateway answers 400).
281
280
  next =
282
281
  `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors. ` +
283
- `This server cannot continue response ${JSON.stringify(responseId)}: once they have connected, send the request again ` +
284
- "with 2kw_create_response without `conversation` (a connect pause cannot be continued by conversation), " +
285
- "or continue the response by its id from n8n (Previous Response ID) or any client that sends previous_response_id.";
282
+ `Once they have connected, call 2kw_create_response with agent ${JSON.stringify(ref)} and previousResponseId ` +
283
+ `${JSON.stringify(responseId)} and an empty input (not \`conversation\`: a connect pause cannot be continued by conversation).`;
286
284
  }
287
285
  else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
288
286
  // Paused on nothing the envelope decodes: say so, rather than hand back a pause with no way on.
@@ -92,9 +92,23 @@ export function register(server, client) {
92
92
  };
93
93
  }
94
94
  });
95
+ // An empty input resumes a paused response (#1065): the wire takes `[]`, not an empty message.
96
+ const requestInput = (input, mode) => {
97
+ const message = input === "" ? [] : [{ type: "message", role: "user", content: input }];
98
+ if (mode)
99
+ return [...message, modeItem(mode)];
100
+ return input === "" ? [] : input;
101
+ };
95
102
  // ── 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 an approval pause with 2kw_decide_agent_approvals. A run paused on a relayed (client-side) tool call comes back with status requires_tool_output and `pendingToolCalls`; answer it with 2kw_decide_agent_approvals `outputs`. 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. A run where a connector asks the user a question pauses with status requires_tool_output and `pendingInputs` (label, tool, mode, answerUrl): the user answers it in chat at `answerUrl`, not here. Always non-streaming. " + MODE_RULE, {
97
- input: z.string().min(1).describe("The input text (sent as a single user message)"),
103
+ 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 paused on a relayed (client-side) tool call comes back with status requires_tool_output and `pendingToolCalls`; answer it with 2kw_decide_agent_approvals `outputs`. 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; once they have, resume it with 2kw_create_response `previousResponseId` (and an empty `input`). A run where a connector asks the user a question pauses with status requires_tool_output and `pendingInputs` (label, tool, mode, answerUrl): the user answers it in chat at `answerUrl`, not here. Always non-streaming. " + MODE_RULE, {
104
+ input: z
105
+ .string()
106
+ .describe("The input text (sent as a single user message). May be empty only with `previousResponseId`, to resume a paused response without a new message."),
107
+ previousResponseId: z
108
+ .string()
109
+ .min(1)
110
+ .optional()
111
+ .describe("Resume this paused response (sent as previous_response_id), e.g. after the user connected a connector in chat.2kw.ai → Connectors. Use the `responseId` from the pause envelope."),
98
112
  agent: z
99
113
  .string()
100
114
  .optional()
@@ -113,8 +127,14 @@ export function register(server, client) {
113
127
  .enum(["plan", "ask", "auto"])
114
128
  .optional()
115
129
  .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 }) => {
130
+ }, async ({ input, previousResponseId, agent, agentModel, model, conversation, mode }) => {
117
131
  try {
132
+ if (input === "" && previousResponseId === undefined) {
133
+ throw new Error("`input` is empty; it may only be empty with `previousResponseId`.");
134
+ }
135
+ if (input === "" && mode !== undefined) {
136
+ throw new Error("`mode` needs a non-empty `input`: an empty input only resumes a paused response.");
137
+ }
118
138
  if (agent && model) {
119
139
  throw new Error("Use either `agent` or `model`, not both.");
120
140
  }
@@ -136,8 +156,9 @@ export function register(server, client) {
136
156
  // Without `mode` the wire `input` is the bare string (shorthand for one user message; see
137
157
  // ResponseItemInputDeserializer). With it, the backbone:mode item follows the message (#656);
138
158
  // that literal item array is what the cast below is for.
139
- input: mode ? [{ type: "message", role: "user", content: input }, modeItem(mode)] : input,
159
+ input: requestInput(input, mode),
140
160
  stream: false,
161
+ ...(previousResponseId !== undefined && { previous_response_id: previousResponseId }),
141
162
  ...(conversation !== undefined && { conversation }),
142
163
  };
143
164
  const { data } = await client.POST("/v1/responses", {
@@ -190,6 +211,39 @@ export function register(server, client) {
190
211
  };
191
212
  }
192
213
  });
214
+ // ── background responses ────────────────────────────────────────────────
215
+ server.tool("2kw_get_response", "Read a background response (one created with background: true) as it stands now. Its status is queued, in_progress, requires_action, completed, incomplete, failed or cancelled. Its output stays empty while it is queued or in_progress and is filled once the run completes or pauses. Poll it until it leaves queued and in_progress. Only background responses can be read by id; a synchronous one is refused, and an id the caller cannot see answers not found.", {
216
+ responseId: z.string().min(1).describe("The background response id (resp_...)"),
217
+ }, async ({ responseId }) => {
218
+ try {
219
+ const { data } = await client.GET("/v1/responses/{responseId}", {
220
+ params: { path: { responseId } },
221
+ });
222
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
223
+ }
224
+ catch (error) {
225
+ return {
226
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
227
+ isError: true,
228
+ };
229
+ }
230
+ });
231
+ server.tool("2kw_cancel_response", "Cancel the background run whose newest response this is. A queued or paused run is cancelled at once and answers status cancelled; a running one stops at its next step and answers in_progress until it does. Only the caller that started the run can cancel it. An older response of the run is refused with a conflict that names the newest response; a run that already ended is refused the same way.", {
232
+ responseId: z.string().min(1).describe("The run's newest background response id (resp_...)"),
233
+ }, async ({ responseId }) => {
234
+ try {
235
+ const { data } = await client.POST("/v1/responses/{responseId}/cancel", {
236
+ params: { path: { responseId } },
237
+ });
238
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
239
+ }
240
+ catch (error) {
241
+ return {
242
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
243
+ isError: true,
244
+ };
245
+ }
246
+ });
193
247
  // ── list_models ─────────────────────────────────────────────────────────
194
248
  server.tool("2kw_list_models", "List available AI models and providers accessible through the Backbone AI gateway.", {}, async () => {
195
249
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "7.0.0",
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.",
3
+ "version": "7.1.0-dev.14",
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. 199 tools for Claude Code, Cursor, and Windsurf.",
5
5
  "mcpName": "ai.2kw/mcp-server",
6
6
  "keywords": [
7
7
  "mcp",