@2kw/ai-mcp-server 8.0.0-dev.1 → 8.0.0-dev.12

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.
@@ -65,6 +65,18 @@ export interface PendingInput {
65
65
  mode: InputMode;
66
66
  answerUrl: string;
67
67
  }
68
+ /**
69
+ * A table or chart the agent showed the user (#1419): a `backbone:ui` item, its validated `props` as the
70
+ * server sent them. A client renders from `props`, never from the call's arguments.
71
+ */
72
+ export interface ShownComponent {
73
+ id: string;
74
+ callId: string;
75
+ /** `table` or `chart`; another value is a component this build does not know. */
76
+ component: string;
77
+ schemaVersion: number;
78
+ props: unknown;
79
+ }
68
80
  /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
69
81
  export type ConversationMode = "plan" | "ask" | "auto";
70
82
  export declare const CONVERSATION_MODES: readonly ConversationMode[];
@@ -97,6 +109,7 @@ export interface RunEnvelope {
97
109
  pendingToolCalls: PendingToolCall[];
98
110
  pendingConnections: PendingConnection[];
99
111
  pendingInputs: PendingInput[];
112
+ components: ShownComponent[];
100
113
  incompleteReason: string | null;
101
114
  usage: {
102
115
  inputTokens: number;
@@ -108,6 +108,8 @@ const DECODED_ITEM_TYPES = new Set([
108
108
  "backbone:connector_auth_request",
109
109
  "backbone:input_request",
110
110
  "backbone:tool_image",
111
+ // A table or chart a finished ShowTable or ShowChart call showed (#1419): it never holds a run either.
112
+ "backbone:ui",
111
113
  "mcp_approval_request",
112
114
  "mcp_call",
113
115
  "mcp_list_tools",
@@ -302,6 +304,15 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
302
304
  pendingToolCalls,
303
305
  pendingConnections,
304
306
  pendingInputs,
307
+ components: output
308
+ .filter((i) => i.type === "backbone:ui")
309
+ .map((i) => ({
310
+ id: String(i.id),
311
+ callId: String(i.call_id),
312
+ component: String(i.component),
313
+ schemaVersion: Number(i.schema_version),
314
+ props: i.props ?? null,
315
+ })),
305
316
  incompleteReason: result?.incomplete_details?.reason ?? null,
306
317
  usage: result?.usage
307
318
  ? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
@@ -100,7 +100,7 @@ export function register(server, client) {
100
100
  return input === "" ? [] : input;
101
101
  };
102
102
  // ── create_response ─────────────────────────────────────────────────────
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, {
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, the tables and charts the agent showed the user as `components`, 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
104
  input: z
105
105
  .string()
106
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."),
@@ -89,9 +89,9 @@ export function register(server, client) {
89
89
  options: z
90
90
  .object({
91
91
  outputFormats: z.array(z.string()).optional().describe("Output formats, e.g. ['md', 'text', 'html', 'json']. Only relevant for ocr/vlm pipelines."),
92
- ocrEngine: z.string().optional().describe("OCR engine (e.g. 'easyocr', 'tesseract')"),
92
+ ocrEngine: z.string().optional().describe("OCR engine to read text from scanned pages and images. Omit for the default."),
93
93
  ocrLanguages: z.array(z.string()).optional().describe("OCR languages (e.g. ['en', 'de'])"),
94
- pdfBackend: z.string().optional().describe("PDF backend (e.g. 'dlparser', 'pypdfium2')"),
94
+ pdfBackend: z.string().optional().describe("PDF parser to read the document layout. Omit for the default."),
95
95
  tableStructure: z.boolean().optional().describe("Enable table structure detection"),
96
96
  maxPages: z.number().optional().describe("Maximum number of pages to process"),
97
97
  imageExportMode: z.string().optional().describe("Image export mode (e.g. 'placeholder', 'embedded')"),
@@ -6,22 +6,23 @@ import { getMimeType } from "../mime.js";
6
6
  const FilePurpose = z.enum(["agent_input", "knowledge"]);
7
7
  export function register(server, client) {
8
8
  // ── list_files ────────────────────────────────────────────────────────────
9
- server.tool("2kw_list_files", "List the organization's files as a page, newest first by default, optionally narrowed by purpose.", {
9
+ server.tool("2kw_list_files", "List the organization's files, newest first by default, optionally narrowed by purpose. When more files exist, pass the returned last id as after.", {
10
10
  purpose: FilePurpose.optional().describe("Narrow the list to one purpose"),
11
- page: z.number().optional().default(0).describe("Page number (0-based)"),
12
- size: z.number().optional().default(20).describe("Page size"),
13
- }, async ({ purpose, page, size }) => {
11
+ limit: z.number().optional().default(20).describe("How many files to list (1-100)"),
12
+ after: z.string().optional().describe("List the files after this file id (the previous list's last id)"),
13
+ order: z.enum(["asc", "desc"]).optional().default("desc").describe("asc = oldest first, desc = newest first"),
14
+ }, async ({ purpose, limit, after, order }) => {
14
15
  try {
15
16
  const { data } = await client.GET("/v1/files", {
16
- params: { query: { purpose, pageable: { page, size } } },
17
+ params: { query: { purpose, limit, after, order } },
17
18
  });
18
- const result = data;
19
- const lines = (result.content ?? []).map((f) => `- ${f.filename} (id: ${f.id}, purpose: ${f.purpose}, bytes: ${f.bytes})`);
19
+ const lines = (data?.data ?? []).map((f) => `- ${f.filename} (id: ${f.id}, purpose: ${f.purpose}, bytes: ${f.bytes})`);
20
+ const more = data?.has_more ? `\nMore files: pass after=${data.last_id}` : "";
20
21
  return {
21
22
  content: [
22
23
  {
23
24
  type: "text",
24
- text: `Files (${result.totalElements} total, page ${(result.number ?? 0) + 1}/${result.totalPages}):\n${lines.join("\n") || "(none)"}`,
25
+ text: `Files:\n${lines.join("\n") || "(none)"}${more}`,
25
26
  },
26
27
  ],
27
28
  };
@@ -58,7 +58,7 @@ const knowledgeBaseFields = {
58
58
  parentChunkSize: z.number().int().min(100).max(8000).optional().describe("Parent chunk size for hierarchical chunking (100-8000), must exceed chunkSize"),
59
59
  hybridSearchEnabled: z.boolean().optional().describe("Enable hybrid dense + lexical search"),
60
60
  rerankerProviderId: z.string().optional().describe("Provider id used for reranking. builtin is refused: there is no built-in reranker."),
61
- 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."),
61
+ textSearchLanguage: z.enum(TEXT_SEARCH_LANGUAGES).optional().describe("Text-search language for the lexical leg (default simple on create). On update, a language other than the active and the pending one starts a reindex in the background; omitting it, or sending the active or pending language back, leaves both untouched."),
62
62
  };
63
63
  function buildKnowledgeBaseBody(params) {
64
64
  const body = {
@@ -77,6 +77,7 @@ function buildKnowledgeBaseBody(params) {
77
77
  "hybridSearchEnabled",
78
78
  "rerankerProviderId",
79
79
  "textSearchLanguage",
80
+ "cancelTextSearchReindex",
80
81
  ]) {
81
82
  if (params[key] !== undefined)
82
83
  body[key] = params[key];
@@ -161,7 +162,11 @@ export function register(server, client) {
161
162
  }
162
163
  });
163
164
  // ── update_knowledge_base ────────────────────────────────────────────────
164
- server.tool("2kw_update_knowledge_base", "Apply the request's fields to a knowledge base and snapshot the result as the next configuration version. name, slug, embeddingProviderId, embeddingModel and embeddingDim are required on every update; embeddingDim cannot actually change — it must match the existing value or the update is rejected.", { knowledgeBaseId: z.string().describe("The knowledge base ID"), ...knowledgeBaseFields }, async ({ knowledgeBaseId, ...params }) => {
165
+ server.tool("2kw_update_knowledge_base", "Apply the request's fields to a knowledge base and snapshot the result as the next configuration version. name, slug, embeddingProviderId, embeddingModel and embeddingDim are required on every update; embeddingDim cannot actually change — it must match the existing value or the update is rejected.", {
166
+ knowledgeBaseId: z.string().describe("The knowledge base ID"),
167
+ ...knowledgeBaseFields,
168
+ cancelTextSearchReindex: z.boolean().optional().describe("true abandons the text-search reindex in progress and keeps the active language. Applied before textSearchLanguage."),
169
+ }, async ({ knowledgeBaseId, ...params }) => {
165
170
  try {
166
171
  const { data } = await client.PATCH("/v1/knowledge-bases/{id}", {
167
172
  params: { path: { id: knowledgeBaseId } },
@@ -338,7 +343,7 @@ export function register(server, client) {
338
343
  }
339
344
  });
340
345
  // ── search_knowledge_base ─────────────────────────────────────────────────
341
- server.tool("2kw_search_knowledge_base", "Hybrid dense + lexical retrieval with Reciprocal Rank Fusion over a knowledge base. topK is clamped to 1..100. Not paginated — raise topK instead of asking for a later page. Each hit carries a chunkId that can be passed to 2kw_resolve_citation to re-fetch the passage later.", {
346
+ server.tool("2kw_search_knowledge_base", "Hybrid semantic and keyword search over a knowledge base. topK is clamped to 1..100. Not paginated — raise topK instead of asking for a later page. Each hit carries a chunkId that can be passed to 2kw_resolve_citation to re-fetch the passage later.", {
342
347
  knowledgeBaseId: z.string().describe("The knowledge base ID"),
343
348
  query: z.string().min(1).describe("The search query"),
344
349
  topK: z.number().int().min(1).max(100).optional().describe("Number of results to return (default backend-defined, clamped 1-100)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "8.0.0-dev.1",
3
+ "version": "8.0.0-dev.12",
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. 199 tools for Claude Code, Cursor, and Windsurf.",
5
5
  "mcpName": "ai.2kw/mcp-server",
6
6
  "keywords": [