@2kw/ai-mcp-server 6.3.0-dev.92 → 6.3.0-dev.96

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
3
+ * every change the caller gave written over it. A change that is `undefined` was not given and keeps
4
+ * the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
5
+ * `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
6
+ */
7
+ export declare function overlay<T extends object>(current: T, changes: Record<string, unknown>): T;
8
+ /** Refuses a name that is present but blank, which a full-replace PUT would otherwise store. */
9
+ export declare function assertNameNotBlank(name: string | undefined): void;
10
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
3
+ * every change the caller gave written over it. A change that is `undefined` was not given and keeps
4
+ * the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
5
+ * `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
6
+ */
7
+ export function overlay(current, changes) {
8
+ const body = { ...current };
9
+ for (const [key, value] of Object.entries(changes)) {
10
+ if (value !== undefined)
11
+ body[key] = value;
12
+ }
13
+ return body;
14
+ }
15
+ /** Refuses a name that is present but blank, which a full-replace PUT would otherwise store. */
16
+ export function assertNameNotBlank(name) {
17
+ if (name !== undefined && !name.trim())
18
+ throw new Error("name must not be empty.");
19
+ }
20
+ //# sourceMappingURL=overlay.js.map
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
3
  import { continueWithDecisions, fetchPendingApprovals, planDecisions, resolveAgentId, splitAgentRef } from "../lib/agent-decide.js";
4
4
  import { buildRunEnvelope, chatUrlFor, MODE_RULE } from "../lib/agent-run.js";
5
+ import { assertNameNotBlank } from "../lib/overlay.js";
5
6
  const modelSchema = z
6
7
  .string()
7
8
  .min(1)
@@ -159,9 +160,9 @@ export function register(server, client) {
159
160
  }
160
161
  });
161
162
  // ── update_agent ────────────────────────────────────────────────────────
162
- 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.", {
163
+ server.tool("2kw_update_agent", "Update an agent's metadata and configuration. Omitted fields keep their current values (the tool reads the agent's name and description first when either is omitted). `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.", {
163
164
  agentId: z.string().describe("The agent ID"),
164
- name: z.string().min(1).describe("Agent name"),
165
+ name: z.string().optional().describe("New agent name"),
165
166
  model: modelSchema.optional(),
166
167
  models: modelsSchema.optional(),
167
168
  description: z.string().optional().describe("Agent description"),
@@ -172,6 +173,18 @@ export function register(server, client) {
172
173
  hitlPolicy: z.unknown().optional().describe("Human-in-the-loop approval policy (JSON object)"),
173
174
  }, async ({ agentId, name, model, models, description, instructions, options, tools, skills, hitlPolicy }) => {
174
175
  try {
176
+ assertNameNotBlank(name);
177
+ // PUT /v1/agents/{id} keeps the configuration fields a body omits, but name is required and
178
+ // description is written as sent, so an omitted description would be cleared (#1195).
179
+ if (name === undefined || description === undefined) {
180
+ const { data: current } = await client.GET("/v1/agents/{id}", {
181
+ params: { path: { id: agentId } },
182
+ });
183
+ if (!current)
184
+ throw new Error(`Agent ${agentId} could not be read.`);
185
+ name ??= current.name;
186
+ description ??= current.description;
187
+ }
175
188
  const body = { name, ...modelFields(model, models, false) };
176
189
  if (description !== undefined)
177
190
  body.description = description;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
3
4
  async function resolveLatestVersionId(client, datasetId) {
4
5
  const { data } = await client.GET("/v1/datasets/{id}/versions/latest", { params: { path: { id: datasetId } } });
5
6
  const versionId = data?.id;
@@ -104,7 +105,7 @@ export function register(server, client) {
104
105
  }
105
106
  });
106
107
  // ── update_dataset ─────────────────────────────────────────────
107
- server.tool("2kw_update_dataset", "Update an existing dataset's details.", {
108
+ server.tool("2kw_update_dataset", "Update an existing dataset's details. Omitted fields keep their current values (the tool reads the dataset first).", {
108
109
  datasetId: z.string().describe("The dataset ID"),
109
110
  name: z.string().optional().describe("New name"),
110
111
  description: z.string().optional().describe("New description"),
@@ -114,22 +115,16 @@ export function register(server, client) {
114
115
  metadata: z.unknown().optional().describe("Arbitrary metadata (JSON object)"),
115
116
  }, async ({ datasetId, name, description, type, inputSchema, expectedOutputSchema, metadata }) => {
116
117
  try {
117
- const body = {};
118
- if (name !== undefined)
119
- body.name = name;
120
- if (description !== undefined)
121
- body.description = description;
122
- if (type !== undefined)
123
- body.type = type;
124
- if (inputSchema !== undefined)
125
- body.inputSchema = inputSchema;
126
- if (expectedOutputSchema !== undefined)
127
- body.expectedOutputSchema = expectedOutputSchema;
128
- if (metadata !== undefined)
129
- body.metadata = metadata;
118
+ assertNameNotBlank(name);
119
+ // PUT /v1/datasets/{id} replaces the whole dataset, so an omitted field would be stored as null.
120
+ const { data: current } = await client.GET("/v1/datasets/{id}", {
121
+ params: { path: { id: datasetId } },
122
+ });
123
+ if (!current)
124
+ throw new Error(`Dataset ${datasetId} could not be read.`);
130
125
  const { data } = await client.PUT("/v1/datasets/{id}", {
131
126
  params: { path: { id: datasetId } },
132
- body: body,
127
+ body: overlay(current, { name, description, type, inputSchema, expectedOutputSchema, metadata }),
133
128
  });
134
129
  return {
135
130
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
3
4
  export function register(server, client) {
4
5
  // ── list_experiments ────────────────────────────────────────────
5
6
  server.tool("2kw_list_experiments", "List experiments in the organization with optional search, status filter, and pagination.", {
@@ -99,7 +100,7 @@ export function register(server, client) {
99
100
  }
100
101
  });
101
102
  // ── update_experiment ───────────────────────────────────────────
102
- server.tool("2kw_update_experiment", "Update an existing experiment's details.", {
103
+ server.tool("2kw_update_experiment", "Update an existing experiment's details. Omitted fields keep their current values (the tool reads the experiment first).", {
103
104
  experimentId: z.string().describe("The experiment ID"),
104
105
  name: z.string().optional().describe("New name"),
105
106
  description: z.string().optional().describe("New description"),
@@ -108,20 +109,16 @@ export function register(server, client) {
108
109
  metadata: z.unknown().optional().describe("Arbitrary metadata (JSON object)"),
109
110
  }, async ({ experimentId, name, description, type, datasetVersionId, metadata }) => {
110
111
  try {
111
- const body = {};
112
- if (name !== undefined)
113
- body.name = name;
114
- if (description !== undefined)
115
- body.description = description;
116
- if (type !== undefined)
117
- body.type = type;
118
- if (datasetVersionId !== undefined)
119
- body.datasetVersionId = datasetVersionId;
120
- if (metadata !== undefined)
121
- body.metadata = metadata;
112
+ assertNameNotBlank(name);
113
+ // PUT /v1/experiments/{id} replaces the whole experiment, so an omitted field would be stored as null.
114
+ const { data: current } = await client.GET("/v1/experiments/{id}", {
115
+ params: { path: { id: experimentId } },
116
+ });
117
+ if (!current)
118
+ throw new Error(`Experiment ${experimentId} could not be read.`);
122
119
  const { data } = await client.PUT("/v1/experiments/{id}", {
123
120
  params: { path: { id: experimentId } },
124
- body: body,
121
+ body: overlay(current, { name, description, type, datasetVersionId, metadata }),
125
122
  });
126
123
  return {
127
124
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -210,7 +207,7 @@ export function register(server, client) {
210
207
  }
211
208
  });
212
209
  // ── update_variant ──────────────────────────────────────────────
213
- server.tool("2kw_update_variant", "Update an existing variant's details.", {
210
+ server.tool("2kw_update_variant", "Update an existing variant's details. Omitted fields keep their current values (the tool reads the variant first).", {
214
211
  experimentId: z.string().describe("The experiment ID"),
215
212
  variantId: z.string().describe("The variant ID"),
216
213
  name: z.string().optional().describe("New variant name"),
@@ -220,20 +217,19 @@ export function register(server, client) {
220
217
  sortOrder: z.number().optional().describe("New sort order for display"),
221
218
  }, async ({ experimentId, variantId, name, taskType, configuration, description, sortOrder }) => {
222
219
  try {
223
- const body = {};
224
- if (name !== undefined)
225
- body.name = name;
226
- if (taskType !== undefined)
227
- body.taskType = taskType;
228
- if (configuration !== undefined)
229
- body.configuration = configuration;
230
- if (description !== undefined)
231
- body.description = description;
232
- if (sortOrder !== undefined)
233
- body.sortOrder = sortOrder;
220
+ assertNameNotBlank(name);
221
+ // PUT .../variants/{variantId} writes name, description and configuration as sent, so an
222
+ // omitted one would be cleared. There is no single-variant GET: read it from the list. The
223
+ // read's version goes back with the body, so a change made since is refused with 409.
224
+ const { data: variants } = await client.GET("/v1/experiments/{id}/variants", {
225
+ params: { path: { id: experimentId } },
226
+ });
227
+ const current = variants?.find((v) => v.id === variantId);
228
+ if (!current)
229
+ throw new Error(`Variant ${variantId} is not a variant of experiment ${experimentId}.`);
234
230
  const { data } = await client.PUT("/v1/experiments/{id}/variants/{variantId}", {
235
231
  params: { path: { id: experimentId, variantId } },
236
- body: body,
232
+ body: overlay(current, { name, taskType, configuration, description, sortOrder }),
237
233
  });
238
234
  return {
239
235
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatErrorForMcp } from "../errors.js";
3
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
3
4
  export function register(server, client) {
4
5
  // ── prompts ─────────────────────────────────────────────────────────────
5
6
  server.tool("2kw_list_prompts", "List org prompts with optional name/type filter and pagination.", {
@@ -74,23 +75,23 @@ export function register(server, client) {
74
75
  };
75
76
  }
76
77
  });
77
- server.tool("2kw_update_prompt", "Update prompt metadata (name, description, type).", {
78
+ server.tool("2kw_update_prompt", "Update prompt metadata (name, description, type). Omitted fields keep their current values (the tool reads the prompt first).", {
78
79
  promptId: z.string(),
79
80
  name: z.string().optional(),
80
81
  description: z.string().optional(),
81
82
  type: z.enum(["TEXT", "CHAT"]).optional(),
82
83
  }, async (params) => {
83
84
  try {
84
- const body = {};
85
- if (params.name !== undefined)
86
- body.name = params.name;
87
- if (params.description !== undefined)
88
- body.description = params.description;
89
- if (params.type !== undefined)
90
- body.type = params.type;
85
+ assertNameNotBlank(params.name);
86
+ // PUT /v1/prompts/{id} replaces the whole prompt, so an omitted field would be stored as null.
87
+ const { data: current } = await client.GET("/v1/prompts/{id}", {
88
+ params: { path: { id: params.promptId } },
89
+ });
90
+ if (!current)
91
+ throw new Error(`Prompt ${params.promptId} could not be read.`);
91
92
  const { data } = await client.PUT("/v1/prompts/{id}", {
92
93
  params: { path: { id: params.promptId } },
93
- body: body,
94
+ body: overlay(current, { name: params.name, description: params.description, type: params.type }),
94
95
  });
95
96
  return {
96
97
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -85,20 +85,27 @@ export function register(server, client) {
85
85
  }
86
86
  });
87
87
  // ── update_schema ───────────────────────────────────────────────────────
88
- server.tool("2kw_update_schema", "Update an existing schema.", {
88
+ server.tool("2kw_update_schema", "Update an existing schema. Omitted fields keep their current values (the tool reads the schema first).", {
89
89
  schemaId: z.string().describe("The schema ID"),
90
90
  name: z.string().optional().describe("New schema name"),
91
91
  description: z.string().optional().describe("New schema description"),
92
92
  }, async ({ schemaId, name, description }) => {
93
93
  try {
94
- const body = {};
95
- if (name !== undefined)
96
- body.name = name;
97
- if (description !== undefined)
98
- body.description = description;
94
+ if (name !== undefined && !name.trim())
95
+ throw new Error("name must not be empty.");
96
+ // PUT /v1/schemas/{id} replaces the whole schema, so an omitted field would be stored as null.
97
+ if (name === undefined || description === undefined) {
98
+ const { data: current } = await client.GET("/v1/schemas/{id}", {
99
+ params: { path: { id: schemaId } },
100
+ });
101
+ if (!current)
102
+ throw new Error(`Schema ${schemaId} could not be read.`);
103
+ name ??= current.name;
104
+ description ??= current.description;
105
+ }
99
106
  const { data } = await client.PUT("/v1/schemas/{id}", {
100
107
  params: { path: { id: schemaId } },
101
- body: body,
108
+ body: { name, description },
102
109
  });
103
110
  return {
104
111
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai-mcp-server",
3
- "version": "6.3.0-dev.92",
3
+ "version": "6.3.0-dev.96",
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": [