@idea-base/mcp-server 2.2.0 → 2.3.0
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 +1 -1
- package/package.json +1 -1
- package/src/index.js +49 -7
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Ask Claude to manage your projects:
|
|
|
66
66
|
| `list_projects` | List all projects with task counts and progress |
|
|
67
67
|
| `get_project` | Get project details and statistics |
|
|
68
68
|
| `create_project` | Create a new project or sub-project |
|
|
69
|
-
| `update_project` | Update project name, description, or
|
|
69
|
+
| `update_project` | Update a project name, description, status, parent (re-parent, or `null` to detach) or product |
|
|
70
70
|
|
|
71
71
|
### Tasks
|
|
72
72
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -93,6 +93,18 @@ function redact(text) {
|
|
|
93
93
|
const MAX_STRING_LEN = 20000;
|
|
94
94
|
const STRING_LIMITS = { query: 500, date: 32, start_date: 32, due_date: 32 };
|
|
95
95
|
|
|
96
|
+
// A schema type may be a UNION, e.g. ['number', 'null'] — the only way to say
|
|
97
|
+
// "a number, or null meaning clear this". Both the null handling in
|
|
98
|
+
// validateArgs and coerce's branch selection have to read that union rather
|
|
99
|
+
// than compare `spec.type` with ===, or a nullable field silently behaves as a
|
|
100
|
+
// string field and its null never reaches the handler at all.
|
|
101
|
+
const typesOf = (spec) => {
|
|
102
|
+
const t = spec?.type;
|
|
103
|
+
return Array.isArray(t) ? t : (t ? [t] : []);
|
|
104
|
+
};
|
|
105
|
+
const allowsNull = (spec) => typesOf(spec).includes('null');
|
|
106
|
+
const primaryType = (spec) => typesOf(spec).find((t) => t !== 'null');
|
|
107
|
+
|
|
96
108
|
function validateArgs(toolName, schema, rawArgs) {
|
|
97
109
|
const args = rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs) ? rawArgs : {};
|
|
98
110
|
const props = schema?.properties || {};
|
|
@@ -107,7 +119,16 @@ function validateArgs(toolName, schema, rawArgs) {
|
|
|
107
119
|
for (const [name, value] of Object.entries(args)) {
|
|
108
120
|
const spec = props[name];
|
|
109
121
|
if (!spec) continue; // drop anything the tool does not declare
|
|
110
|
-
if (value === undefined
|
|
122
|
+
if (value === undefined) continue;
|
|
123
|
+
if (value === null) {
|
|
124
|
+
// null is dropped unless the tool DECLARES it meaningful. For
|
|
125
|
+
// update_project.parent_project_id it means "detach this project", and
|
|
126
|
+
// this unconditional drop is why un-parenting stayed impossible through
|
|
127
|
+
// this server: the field reached here and vanished, so the request body
|
|
128
|
+
// came out empty and the API answered "No fields to update".
|
|
129
|
+
if (allowsNull(spec)) clean[name] = null;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
111
132
|
clean[name] = coerce(toolName, name, spec, value);
|
|
112
133
|
}
|
|
113
134
|
|
|
@@ -116,8 +137,9 @@ function validateArgs(toolName, schema, rawArgs) {
|
|
|
116
137
|
|
|
117
138
|
function coerce(toolName, name, spec, value) {
|
|
118
139
|
const label = `${toolName}: "${name}"`;
|
|
140
|
+
const type = primaryType(spec);
|
|
119
141
|
|
|
120
|
-
if (
|
|
142
|
+
if (type === 'number' || type === 'integer') {
|
|
121
143
|
const n = typeof value === 'number' ? value : Number(String(value).trim());
|
|
122
144
|
if (!Number.isFinite(n)) throw new Error(`${label} must be a number`);
|
|
123
145
|
if (name === 'id' || name.endsWith('_id')) {
|
|
@@ -128,9 +150,9 @@ function coerce(toolName, name, spec, value) {
|
|
|
128
150
|
return n;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
if (
|
|
153
|
+
if (type === 'boolean') return Boolean(value);
|
|
132
154
|
|
|
133
|
-
if (
|
|
155
|
+
if (type === 'array') {
|
|
134
156
|
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
135
157
|
if (value.length > 500) throw new Error(`${label} has too many entries (max 500)`);
|
|
136
158
|
return value.map((item, i) => coerce(toolName, `${name}[${i}]`, spec.items || {}, item));
|
|
@@ -232,7 +254,12 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
232
254
|
try {
|
|
233
255
|
const body = JSON.parse(text);
|
|
234
256
|
if (typeof body?.error === 'string' && body.error) {
|
|
235
|
-
|
|
257
|
+
// `error` is the headline and `details` is what to do about it — a
|
|
258
|
+
// plan-limit or product-inheritance refusal is not actionable without
|
|
259
|
+
// the second half, which used to be dropped here.
|
|
260
|
+
const parts = [body.error];
|
|
261
|
+
if (typeof body?.details === 'string' && body.details) parts.push(body.details);
|
|
262
|
+
message = parts.join(' — ').slice(0, MAX_ERROR_CHARS);
|
|
236
263
|
}
|
|
237
264
|
// Machine-readable discriminator, when the endpoint supplies one. A
|
|
238
265
|
// handler needs to tell "you have no open session" (a real answer the
|
|
@@ -327,7 +354,7 @@ const tools = [
|
|
|
327
354
|
},
|
|
328
355
|
{
|
|
329
356
|
name: 'update_project',
|
|
330
|
-
description: 'Update project
|
|
357
|
+
description: 'Update a project: its name, description, status, or where it sits in the hierarchy. Omitted fields are left exactly as they are. Set parent_project_id to re-parent the project, or to null to DETACH it into a top-level project; a detached project keeps the product it had inherited, so product_id is only needed to move it to a DIFFERENT product. A sub-project inherits its parent\'s product, so re-parenting under a parent in a different product moves the project there too. The API refuses a move that is circular, crosses accounts, or exceeds the plan\'s nesting depth or sub-project count, and says which.',
|
|
331
358
|
inputSchema: {
|
|
332
359
|
type: 'object',
|
|
333
360
|
properties: {
|
|
@@ -348,6 +375,14 @@ const tools = [
|
|
|
348
375
|
enum: ['active', 'completed', 'archived'],
|
|
349
376
|
description: 'New status for the project',
|
|
350
377
|
},
|
|
378
|
+
parent_project_id: {
|
|
379
|
+
type: ['number', 'null'],
|
|
380
|
+
description: 'Move the project under this parent. Pass null to detach it and make it top-level — it keeps whatever product it had, so no product_id is needed unless you also want to move it. Omit to leave the parent unchanged.',
|
|
381
|
+
},
|
|
382
|
+
product_id: {
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: 'Move the project to this product. Only valid for a project with no parent — a sub-project cannot hold a product of its own, it inherits its parent\'s. Find ids via list_products.',
|
|
385
|
+
},
|
|
351
386
|
},
|
|
352
387
|
required: ['project_id'],
|
|
353
388
|
},
|
|
@@ -1012,11 +1047,18 @@ const toolHandlers = {
|
|
|
1012
1047
|
};
|
|
1013
1048
|
},
|
|
1014
1049
|
|
|
1015
|
-
async update_project({ project_id, name, description, status }) {
|
|
1050
|
+
async update_project({ project_id, name, description, status, parent_project_id, product_id }) {
|
|
1016
1051
|
const updates = {};
|
|
1017
1052
|
if (name !== undefined) updates.name = name;
|
|
1018
1053
|
if (description !== undefined) updates.description = description;
|
|
1019
1054
|
if (status !== undefined) updates.status = status;
|
|
1055
|
+
// These two are forwarded on `!== undefined`, never on truthiness: null is
|
|
1056
|
+
// the MEANINGFUL value for parent_project_id (it detaches the project), and
|
|
1057
|
+
// PUT /api/projects/:id only writes a column it was actually sent — so an
|
|
1058
|
+
// omitted key leaves parent and product exactly as they were, and a
|
|
1059
|
+
// name-only update can never silently detach anything.
|
|
1060
|
+
if (parent_project_id !== undefined) updates.parent_project_id = parent_project_id;
|
|
1061
|
+
if (product_id !== undefined) updates.product_id = product_id;
|
|
1020
1062
|
|
|
1021
1063
|
const project = await apiRequest(`/projects/${project_id}`, {
|
|
1022
1064
|
method: 'PUT',
|