@2kw/ai-mcp-server 6.2.0-dev.14 → 6.2.0-dev.23

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/dist/client.js CHANGED
@@ -2,6 +2,8 @@ import createClient from "openapi-fetch";
2
2
  import { BackboneApiError } from "./errors.js";
3
3
  /**
4
4
  * Error-handling middleware: intercepts non-ok responses and throws BackboneApiError.
5
+ * The api-key filter now returns the OpenAI envelope (#658); the surface
6
+ * filter still sends the flat shape, so both are normalized here.
5
7
  */
6
8
  const errorMiddleware = {
7
9
  async onResponse({ response }) {
@@ -21,8 +23,13 @@ const errorMiddleware = {
21
23
  }
22
24
  throw new BackboneApiError({
23
25
  status: body.status ?? response.status,
24
- error: body.title ?? body.error ?? response.statusText,
25
- message: body.detail ?? body.message ?? `HTTP ${response.status}: ${response.statusText}`,
26
+ error: typeof body.error === "string"
27
+ ? body.error
28
+ : (body.error?.code ?? body.error?.type ?? body.title ?? response.statusText),
29
+ message: body.detail ??
30
+ body.message ??
31
+ body.error?.message ??
32
+ `HTTP ${response.status}: ${response.statusText}`,
26
33
  timestamp: body.timestamp ?? new Date().toISOString(),
27
34
  });
28
35
  },
@@ -1,4 +1,18 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { ApiClient } from "../client.js";
3
+ /**
4
+ * The `model`/`models` pair of an agent write (#626, spec #591 §3.3). The backend owns the
5
+ * rules (derivation, uniqueness, existence); this only refuses the two shapes a caller can
6
+ * get wrong locally, so the error names the tool's own parameters instead of a 400.
7
+ */
8
+ export declare function modelFields(model: string | undefined, models: string[] | undefined, required: boolean): {
9
+ model?: string;
10
+ models?: string[];
11
+ };
12
+ /** `model: a` for a single-model agent, `models: a (default), b` otherwise. */
13
+ export declare function describeModels(row: {
14
+ model?: string;
15
+ models?: string[];
16
+ }): string;
3
17
  export declare function register(server: McpServer, client: ApiClient): void;
4
18
  //# sourceMappingURL=agents.d.ts.map
@@ -1,5 +1,39 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ const modelSchema = z
4
+ .string()
5
+ .min(1)
6
+ .describe("Default model in 'provider/model' format (e.g. 'openai/gpt-4o') or a bare platform model name. Shorthand for a one-entry `models` list; if both are given it must equal models[0].");
7
+ const modelsSchema = z
8
+ .array(z.string().min(1))
9
+ .min(1)
10
+ .max(10)
11
+ .describe("Ordered model list, 1-10 unique entries. The first entry is the default the agent runs on; the others are the models a single request may switch to with `agent/<ref>#<model>` (see 2kw_create_response `agentModel`).");
12
+ /**
13
+ * The `model`/`models` pair of an agent write (#626, spec #591 §3.3). The backend owns the
14
+ * rules (derivation, uniqueness, existence); this only refuses the two shapes a caller can
15
+ * get wrong locally, so the error names the tool's own parameters instead of a 400.
16
+ */
17
+ export function modelFields(model, models, required) {
18
+ if (models !== undefined && models.length > 0) {
19
+ if (model !== undefined && model !== models[0]) {
20
+ throw new Error(`\`model\` ('${model}') must equal the first entry of \`models\` ('${models[0]}'), which is the default. Omit \`model\` when sending \`models\`.`);
21
+ }
22
+ return model !== undefined ? { model, models } : { models };
23
+ }
24
+ if (model !== undefined)
25
+ return { model };
26
+ if (required)
27
+ throw new Error("Either `model` or `models` is required.");
28
+ return {};
29
+ }
30
+ /** `model: a` for a single-model agent, `models: a (default), b` otherwise. */
31
+ export function describeModels(row) {
32
+ const models = row.models && row.models.length > 0 ? row.models : row.model ? [row.model] : [];
33
+ if (models.length <= 1)
34
+ return `model: ${models[0] ?? row.model ?? "?"}`;
35
+ return `models: ${models[0]} (default), ${models.slice(1).join(", ")}`;
36
+ }
3
37
  export function register(server, client) {
4
38
  // ── list_agents ─────────────────────────────────────────────────────────
5
39
  server.tool("2kw_list_agents", "List agents in the organization with optional search and pagination.", {
@@ -18,7 +52,7 @@ export function register(server, client) {
18
52
  },
19
53
  });
20
54
  const result = data;
21
- const lines = (result.content ?? []).map((a) => `- ${a.name} (id: ${a.id}, model: ${a.model})${a.description ? ` — ${a.description}` : ""}`);
55
+ const lines = (result.content ?? []).map((a) => `- ${a.name} (id: ${a.id}, ${describeModels(a)})${a.description ? ` — ${a.description}` : ""}`);
22
56
  return {
23
57
  content: [
24
58
  {
@@ -53,18 +87,19 @@ export function register(server, client) {
53
87
  }
54
88
  });
55
89
  // ── create_agent ────────────────────────────────────────────────────────
56
- server.tool("2kw_create_agent", "Create a new agent for the organization.", {
90
+ server.tool("2kw_create_agent", "Create a new agent for the organization. Give its model as `model` (one model) or `models` (an ordered list whose first entry is the default); one of the two is required.", {
57
91
  name: z.string().min(1).describe("Agent name"),
58
- model: z.string().min(1).describe("Model in 'provider/model' format (e.g. 'openai/gpt-4o')"),
92
+ model: modelSchema.optional(),
93
+ models: modelsSchema.optional(),
59
94
  description: z.string().optional().describe("Agent description"),
60
95
  instructions: z.string().optional().describe("System instructions for the agent"),
61
96
  options: z.unknown().optional().describe("Arbitrary model options (JSON object)"),
62
97
  tools: z.unknown().optional().describe("Tool configuration (JSON object)"),
63
98
  skills: z.unknown().optional().describe("Skill configuration (JSON object)"),
64
99
  hitlPolicy: z.unknown().optional().describe("Human-in-the-loop approval policy (JSON object)"),
65
- }, async ({ name, model, description, instructions, options, tools, skills, hitlPolicy }) => {
100
+ }, async ({ name, model, models, description, instructions, options, tools, skills, hitlPolicy }) => {
66
101
  try {
67
- const body = { name, model };
102
+ const body = { name, ...modelFields(model, models, true) };
68
103
  if (description !== undefined)
69
104
  body.description = description;
70
105
  if (instructions !== undefined)
@@ -92,19 +127,20 @@ export function register(server, client) {
92
127
  }
93
128
  });
94
129
  // ── update_agent ────────────────────────────────────────────────────────
95
- server.tool("2kw_update_agent", "Update an agent's metadata and configuration. name and model are required on every update — the backend treats this as a full replace of those fields.", {
130
+ server.tool("2kw_update_agent", "Update an agent's metadata and configuration. name is required on every update. `model` or `models` replaces the stored model list (a shorter list leaves no tail); omit both to keep the current models. Does not create a version.", {
96
131
  agentId: z.string().describe("The agent ID"),
97
132
  name: z.string().min(1).describe("Agent name"),
98
- model: z.string().min(1).describe("Model in 'provider/model' format (e.g. 'openai/gpt-4o')"),
133
+ model: modelSchema.optional(),
134
+ models: modelsSchema.optional(),
99
135
  description: z.string().optional().describe("Agent description"),
100
136
  instructions: z.string().optional().describe("System instructions for the agent"),
101
137
  options: z.unknown().optional().describe("Arbitrary model options (JSON object)"),
102
138
  tools: z.unknown().optional().describe("Tool configuration (JSON object)"),
103
139
  skills: z.unknown().optional().describe("Skill configuration (JSON object)"),
104
140
  hitlPolicy: z.unknown().optional().describe("Human-in-the-loop approval policy (JSON object)"),
105
- }, async ({ agentId, name, model, description, instructions, options, tools, skills, hitlPolicy }) => {
141
+ }, async ({ agentId, name, model, models, description, instructions, options, tools, skills, hitlPolicy }) => {
106
142
  try {
107
- const body = { name, model };
143
+ const body = { name, ...modelFields(model, models, false) };
108
144
  if (description !== undefined)
109
145
  body.description = description;
110
146
  if (instructions !== undefined)
@@ -160,7 +196,7 @@ export function register(server, client) {
160
196
  params: { path: { agentId }, query: { pageable: { page, size } } },
161
197
  });
162
198
  const result = data;
163
- const lines = (result.content ?? []).map((v) => `- v${v.versionNumber} (id: ${v.id})${v.changeDescription ? ` — ${v.changeDescription}` : ""}`);
199
+ const lines = (result.content ?? []).map((v) => `- v${v.versionNumber} (id: ${v.id}, ${describeModels(v)})${v.changeDescription ? ` — ${v.changeDescription}` : ""}`);
164
200
  return {
165
201
  content: [
166
202
  {
@@ -178,18 +214,19 @@ export function register(server, client) {
178
214
  }
179
215
  });
180
216
  // ── create_agent_version ────────────────────────────────────────────────
181
- server.tool("2kw_create_agent_version", "Create a new version of an agent. Auto-updates the 'latest' label.", {
217
+ server.tool("2kw_create_agent_version", "Create a new version of an agent. Auto-updates the 'latest' label. Give its model as `model` (one model) or `models` (an ordered list whose first entry is the default); one of the two is required.", {
182
218
  agentId: z.string().describe("The agent ID"),
183
- model: z.string().min(1).describe("Model in 'provider/model' format (e.g. 'openai/gpt-4o')"),
219
+ model: modelSchema.optional(),
220
+ models: modelsSchema.optional(),
184
221
  changeDescription: z.string().optional().describe("Description of what changed in this version"),
185
222
  instructions: z.string().optional().describe("System instructions for the agent"),
186
223
  options: z.unknown().optional().describe("Arbitrary model options (JSON object)"),
187
224
  tools: z.unknown().optional().describe("Tool configuration (JSON object)"),
188
225
  skills: z.unknown().optional().describe("Skill configuration (JSON object)"),
189
226
  hitlPolicy: z.unknown().optional().describe("Human-in-the-loop approval policy (JSON object)"),
190
- }, async ({ agentId, model, changeDescription, instructions, options, tools, skills, hitlPolicy }) => {
227
+ }, async ({ agentId, model, models, changeDescription, instructions, options, tools, skills, hitlPolicy }) => {
191
228
  try {
192
- const body = { model };
229
+ const body = { ...modelFields(model, models, true) };
193
230
  if (changeDescription !== undefined)
194
231
  body.changeDescription = changeDescription;
195
232
  if (instructions !== undefined)
@@ -1,4 +1,10 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { ApiClient } from "../client.js";
3
+ /**
4
+ * The `model` value for an agent run: `agent/<ref>[@<label>]`, plus `#<model>` when the
5
+ * request switches to another entry of the version's `models` list (#626, spec #591 §4.1).
6
+ * Returns undefined when no agent is given.
7
+ */
8
+ export declare function agentModelReference(agent: string | undefined, agentModel?: string): string | undefined;
3
9
  export declare function register(server: McpServer, client: ApiClient): void;
4
10
  //# sourceMappingURL=ai-gateway.d.ts.map
@@ -1,5 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ /**
4
+ * The `model` value for an agent run: `agent/<ref>[@<label>]`, plus `#<model>` when the
5
+ * request switches to another entry of the version's `models` list (#626, spec #591 §4.1).
6
+ * Returns undefined when no agent is given.
7
+ */
8
+ export function agentModelReference(agent, agentModel) {
9
+ if (!agent)
10
+ return undefined;
11
+ return agentModel ? `agent/${agent}#${agentModel}` : `agent/${agent}`;
12
+ }
3
13
  export function register(server, client) {
4
14
  // ── chat ────────────────────────────────────────────────────────────────
5
15
  server.tool("2kw_chat", "Send a chat completion request through Backbone's OpenAI-compatible AI gateway. Supports multiple providers (OpenAI, Anthropic, Azure, Vertex AI, etc.). Model format: 'provider/model' (e.g. 'openai/gpt-4o', 'anthropic/claude-sonnet-4-5-20250929'). Always non-streaming.", {
@@ -82,18 +92,23 @@ export function register(server, client) {
82
92
  }
83
93
  });
84
94
  // ── create_response ─────────────────────────────────────────────────────
85
- 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'). Always non-streaming.", {
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.", {
86
96
  input: z.string().min(1).describe("The input text (sent as a single user message)"),
87
97
  agent: z
88
98
  .string()
89
99
  .optional()
90
100
  .describe("Stored agent id or name to invoke, optionally 'ref@label'. Mutually exclusive with `model`."),
101
+ agentModel: z
102
+ .string()
103
+ .min(1)
104
+ .optional()
105
+ .describe("Only with `agent`: run this request on this model instead of the agent's default. Must be one of the resolved version's `models`; sent as 'agent/<ref>#<model>'."),
91
106
  model: z
92
107
  .string()
93
108
  .optional()
94
109
  .describe("Model identifier in 'provider/model' format. Mutually exclusive with `agent`."),
95
110
  conversation: z.string().optional().describe("Conversation ID this response belongs to"),
96
- }, async ({ input, agent, model, conversation }) => {
111
+ }, async ({ input, agent, agentModel, model, conversation }) => {
97
112
  try {
98
113
  if (agent && model) {
99
114
  throw new Error("Use either `agent` or `model`, not both.");
@@ -101,7 +116,13 @@ export function register(server, client) {
101
116
  if (!agent && !model) {
102
117
  throw new Error("Either `agent` or `model` is required.");
103
118
  }
104
- const resolvedModel = agent ? `agent/${agent}` : model;
119
+ if (agentModel !== undefined && !agent) {
120
+ throw new Error("`agentModel` switches a stored agent's model; it needs `agent`.");
121
+ }
122
+ if (agentModel !== undefined && agent?.includes("#")) {
123
+ throw new Error("Give the model switch once: either 'ref#model' in `agent` or `agentModel`.");
124
+ }
125
+ const resolvedModel = agentModelReference(agent, agentModel) ?? model;
105
126
  const body = {
106
127
  model: resolvedModel,
107
128
  // The wire `input` field also accepts a bare string (shorthand for
@@ -141,6 +162,11 @@ export function register(server, client) {
141
162
  if (parts.length === 0) {
142
163
  parts.push({ type: "text", text: JSON.stringify(result, null, 2) });
143
164
  }
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
+ }
144
170
  return { content: parts };
145
171
  }
146
172
  catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { ApiClient } from "../client.js";
3
- /** Register skill read and import tools (#642). */
3
+ /** Register skill read, import and delete tools (#642, #683). */
4
4
  export declare function register(server: McpServer, client: ApiClient): void;
5
5
  //# sourceMappingURL=skills.d.ts.map
@@ -10,7 +10,7 @@ function paginationParams(params) {
10
10
  sort: params.sort ? [params.sort] : undefined,
11
11
  };
12
12
  }
13
- /** Register skill read and import tools (#642). */
13
+ /** Register skill read, import and delete tools (#642, #683). */
14
14
  export function register(server, client) {
15
15
  server.tool("2kw_list_skills", "List org skills with optional name/status filter and pagination.", {
16
16
  search: z.string().optional().describe("Filter by name"),
@@ -131,6 +131,20 @@ export function register(server, client) {
131
131
  };
132
132
  }
133
133
  });
134
+ server.tool("2kw_delete_skill", "Delete a skill and all its versions. Refused with 409 while an agent version binds it; the message names the agents.", { skillId: z.string() }, async (params) => {
135
+ try {
136
+ await client.DELETE("/v1/skills/{id}", { params: { path: { id: params.skillId } } });
137
+ return {
138
+ content: [{ type: "text", text: JSON.stringify({ deleted: params.skillId }, null, 2) }],
139
+ };
140
+ }
141
+ catch (error) {
142
+ return {
143
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
144
+ isError: true,
145
+ };
146
+ }
147
+ });
134
148
  server.tool("2kw_import_skill", "Import a local SKILL.md or .zip file. Returns outcome, version, renamedFrom and skipped files. Zip directories before importing.", { filePath: z.string().min(1).describe("Local path to SKILL.md or a .zip archive") }, async (params) => {
135
149
  try {
136
150
  const info = await stat(params.filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.2.0-dev.14",
3
+ "version": "6.2.0-dev.23",
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": [
@@ -41,6 +41,7 @@
41
41
  "check:generated": "tsx openapi/scripts/check-generated.ts",
42
42
  "fix:generated": "tsx openapi/scripts/check-generated.ts --write",
43
43
  "check:coverage": "tsx ../cli/openapi/scripts/check-coverage.ts",
44
+ "test": "tsx --test tests/*.test.ts",
44
45
  "typecheck": "tsc --noEmit"
45
46
  },
46
47
  "dependencies": {