@2kw/ai-mcp-server 6.3.0 → 6.4.0-dev.8

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.
@@ -81,9 +81,26 @@ function parseArguments(raw) {
81
81
  return raw;
82
82
  }
83
83
  }
84
+ /**
85
+ * A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
86
+ * is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
87
+ * id, policy class or status, and exists only while the decision is open, so its id doubles as the call id
88
+ * and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
89
+ */
90
+ function connectorApproval(i) {
91
+ return {
92
+ approvalId: String(i.id),
93
+ callId: String(i.id),
94
+ tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
95
+ arguments: parseArguments(i.arguments),
96
+ policyClass: "unknown",
97
+ reason: null,
98
+ };
99
+ }
84
100
  /**
85
101
  * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
86
- * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
102
+ * connector calls. A pause on anything else is named.
103
+ * `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
87
104
  */
88
105
  const DECODED_ITEM_TYPES = new Set([
89
106
  "message",
@@ -92,6 +109,8 @@ const DECODED_ITEM_TYPES = new Set([
92
109
  "function_call_output",
93
110
  "backbone:approval_request",
94
111
  "backbone:connector_auth_request",
112
+ "backbone:tool_image",
113
+ "mcp_approval_request",
95
114
  "mcp_call",
96
115
  "mcp_list_tools",
97
116
  ]);
@@ -133,17 +152,20 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
133
152
  String(i.call_id),
134
153
  i.status === "incomplete" ? "incomplete" : "completed",
135
154
  ]));
136
- const pendingApprovals = output
137
- // Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
138
- .filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
139
- .map((i) => ({
140
- approvalId: String(i.id),
141
- callId: String(i.call_id),
142
- tool: String(i.tool),
143
- arguments: parseArguments(i.arguments),
144
- policyClass: String(i.policy_class),
145
- reason: typeof i.reason === "string" && i.reason ? i.reason : null,
146
- }));
155
+ const pendingApprovals = [
156
+ ...output
157
+ // Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
158
+ .filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
159
+ .map((i) => ({
160
+ approvalId: String(i.id),
161
+ callId: String(i.call_id),
162
+ tool: String(i.tool),
163
+ arguments: parseArguments(i.arguments),
164
+ policyClass: String(i.policy_class),
165
+ reason: typeof i.reason === "string" && i.reason ? i.reason : null,
166
+ })),
167
+ ...output.filter((i) => i.type === "mcp_approval_request").map(connectorApproval),
168
+ ];
147
169
  // The one decoding rule (#1086): only an open request with its id, call id, label and host.
148
170
  const connectRequests = pendingConnectionsOf(output);
149
171
  const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
@@ -207,8 +229,8 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
207
229
  const approvalsSuffix = pendingApprovals.length > 0
208
230
  ? " Show the pending approvals to the user and pass their decisions in the same call, within the same window."
209
231
  : "";
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
232
+ // An item type this server does not decode (a connector approval is decoded since #949) can leave
233
+ // the envelope under-reporting what is actually pending; say so rather than sound falsely complete
212
234
  // (#1254 review). The continuation itself still refuses an incomplete answer (400
213
235
  // incomplete_tool_outputs, decideErrorText's hint), this only tells the assistant to expect that.
214
236
  const undecodedTypes = extraUndecodedTypes(output);
@@ -219,6 +241,14 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
219
241
  : "";
220
242
  next = `${connectPrefix}${relay}${approvalsSuffix}${undecodedSuffix}`;
221
243
  }
244
+ else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingApprovals.length > 0 && pendingToolCalls.length === 0 && ref && responseId) {
245
+ // A continuation must decide every pending approval (400 incomplete_tool_outputs otherwise), so a
246
+ // fresh request is no way on; decide answers them and the engine re-checks access itself (#949 review).
247
+ next =
248
+ `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors. ` +
249
+ "Show the pending approvals to the user; after they decide each one, call 2kw_decide_agent_approvals with agent " +
250
+ `${JSON.stringify(ref)} and responseId ${JSON.stringify(responseId)}.`;
251
+ }
222
252
  else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
223
253
  // A connector-only pause (no relayed call, so 2kw_decide_agent_approvals has nothing to decide,
224
254
  // spec M11) has no tool here to continue it by id, and a connect pause cannot be continued by
@@ -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
  /**
7
7
  * Map user-friendly format names to backend OutputFormat enum values.
@@ -167,27 +167,13 @@ export function register(server, client) {
167
167
  if (Object.keys(nestedOpts).length > 0) {
168
168
  formData.append("options", new Blob([JSON.stringify(nestedOpts)], { type: "application/json" }));
169
169
  }
170
- const { baseUrl, apiKey } = client._config;
171
- const endpoint = isAsync ? "/v1/convert/file/async" : "/v1/convert/file";
172
- const url = pipeline
173
- ? `${baseUrl}${endpoint}?pipeline=${encodeURIComponent(pipeline)}`
174
- : `${baseUrl}${endpoint}`;
175
- const res = await fetch(url, {
176
- method: "POST",
177
- headers: { Authorization: `Bearer ${apiKey}` },
178
- body: formData,
170
+ // Placeholder body for the generated type; bodySerializer sends the real parts.
171
+ const result = await client.POST(isAsync ? "/v1/convert/file/async" : "/v1/convert/file", {
172
+ params: { query: { pipeline } },
173
+ body: { files: [] },
174
+ bodySerializer: () => formData,
179
175
  });
180
- if (!res.ok) {
181
- let body;
182
- try {
183
- body = await res.json();
184
- }
185
- catch {
186
- body = { error: res.statusText, message: `HTTP ${res.status}: ${res.statusText}`, status: res.status, timestamp: new Date().toISOString() };
187
- }
188
- throw new BackboneApiError(body);
189
- }
190
- const data = await res.json();
176
+ const data = result.data;
191
177
  if (isAsync) {
192
178
  const taskId = data?.taskId;
193
179
  if (waitForCompletion && taskId) {
@@ -46,7 +46,7 @@ const TEXT_SEARCH_LANGUAGES = [
46
46
  const knowledgeBaseFields = {
47
47
  name: z.string().min(1).describe("Knowledge base name"),
48
48
  slug: z.string().min(1).describe("URL-safe slug, lowercase letters and digits joined by single dashes"),
49
- embeddingProviderId: z.string().min(1).describe("Provider id serving the embedding model"),
49
+ embeddingProviderId: z.string().min(1).describe("Provider id serving the embedding model, or builtin for the platform's built-in models (text-embedding-3-small, text-embedding-3-large; charged per input token). Changing it on update re-embeds nothing: stored vectors stay comparable only if the new provider serves the same model at the same width; the safe move is a new knowledge base with the documents uploaded again."),
50
50
  embeddingModel: z.string().min(1).describe("Embedding model id"),
51
51
  embeddingDim: z.number().int().positive().describe("Embedding dimension. Fixed at creation — on update this must match the existing value, it is not applied as a change."),
52
52
  description: z.string().optional().describe("Knowledge base description"),
@@ -55,7 +55,7 @@ const knowledgeBaseFields = {
55
55
  chunkOverlap: z.number().int().min(0).max(2000).optional().describe("Chunk overlap (0-2000), must be smaller than chunkSize"),
56
56
  parentChunkSize: z.number().int().min(100).max(8000).optional().describe("Parent chunk size for hierarchical chunking (100-8000), must exceed chunkSize"),
57
57
  hybridSearchEnabled: z.boolean().optional().describe("Enable hybrid dense + lexical search"),
58
- rerankerProviderId: z.string().optional().describe("Provider id used for reranking"),
58
+ rerankerProviderId: z.string().optional().describe("Provider id used for reranking. builtin is refused: there is no built-in reranker."),
59
59
  textSearchLanguage: z.enum(TEXT_SEARCH_LANGUAGES).optional().describe("Text-search language for the lexical leg (default simple on create). On update, omit to leave the active and any pending language untouched — resending the current value would cancel a reindex in progress; changing it starts one in the background."),
60
60
  };
61
61
  function buildKnowledgeBaseBody(params) {
@@ -380,7 +380,7 @@ export function register(server, client) {
380
380
  }
381
381
  });
382
382
  // ── list_provider_embedding_models ────────────────────────────────────────
383
- server.tool("2kw_list_provider_embedding_models", "List embedding models served by a provider that fit a provisioned embedding dimension. Empty when the provider type has no catalogued embedding models; never an error. Use this to pick embeddingModel/embeddingDim for 2kw_create_knowledge_base.", { providerId: z.string().describe("The provider ID") }, async ({ providerId }) => {
383
+ server.tool("2kw_list_provider_embedding_models", "List embedding models served by a provider that fit a provisioned embedding dimension. Empty when the provider type has no catalogued embedding models; never an error. Pass builtin to list the platform's built-in embedding models. Use this to pick embeddingModel/embeddingDim for 2kw_create_knowledge_base.", { providerId: z.string().describe("The provider ID, or builtin for the platform's built-in embedding models") }, async ({ providerId }) => {
384
384
  try {
385
385
  const { data } = await client.GET("/v1/providers/{providerId}/embedding-models", {
386
386
  params: { path: { providerId } },
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { basename, extname } from "node:path";
4
- import { BackboneApiError, formatErrorForMcp } from "../errors.js";
4
+ import { formatErrorForMcp } from "../errors.js";
5
5
  // Spring accepts flat pagination parameters despite the generated pageable wrapper.
6
6
  function paginationParams(params) {
7
7
  return {
@@ -160,22 +160,11 @@ export function register(server, client) {
160
160
  formData.append("file", new Blob([buffer], {
161
161
  type: isZip ? "application/zip" : "text/markdown",
162
162
  }), filename);
163
- const { baseUrl, apiKey } = client._config;
164
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/skills/import`, {
165
- method: "POST",
166
- headers: { Authorization: `Bearer ${apiKey}` },
167
- body: formData,
163
+ // Placeholder body for the generated type; bodySerializer sends the real part.
164
+ const { data } = await client.POST("/v1/skills/import", {
165
+ body: { file: "" },
166
+ bodySerializer: () => formData,
168
167
  });
169
- if (!res.ok) {
170
- const body = await res.json().catch(() => ({}));
171
- throw new BackboneApiError({
172
- status: body.status ?? res.status,
173
- error: body.title ?? body.error ?? res.statusText,
174
- message: body.detail ?? body.message ?? `HTTP ${res.status}: ${res.statusText}`,
175
- timestamp: body.timestamp ?? new Date().toISOString(),
176
- });
177
- }
178
- const data = await res.json();
179
168
  return {
180
169
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
181
170
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0",
3
+ "version": "6.4.0-dev.8",
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": [